增加微信支付的接口
This commit is contained in:
parent
ef3814283d
commit
aae9b5e7df
@ -6,7 +6,7 @@ from app.models.order import ShippingOrderDB, OrderStatus
|
||||
from app.core.response import success_response, error_response, ResponseModel
|
||||
from app.core.wechat import WeChatClient,generate_random_string
|
||||
from app.core.security import create_access_token, set_jwt_cookie
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, Field
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
@ -14,6 +14,8 @@ from app.api.deps import get_current_user
|
||||
from app.core.config import settings
|
||||
import random
|
||||
import string
|
||||
from app.models.merchant_order import MerchantOrderDB, MerchantOrderStatus
|
||||
from app.models.merchant_pay_order import MerchantPayOrderDB, MerchantPayOrderStatus
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@ -22,6 +24,11 @@ class PhoneNumberRequest(BaseModel):
|
||||
phone_code: str # 手机号验证码
|
||||
referral_code: str = None # 推荐码(可选)
|
||||
|
||||
class WechatPayRequest(BaseModel):
|
||||
"""微信支付请求"""
|
||||
order_id: str
|
||||
order_type: str = Field(..., description="订单类型: merchant_order/merchant_pay_order")
|
||||
|
||||
@router.post("/phone-login", response_model=ResponseModel)
|
||||
async def wechat_phone_login(
|
||||
request: PhoneNumberRequest,
|
||||
@ -97,3 +104,110 @@ async def wechat_phone_login(
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
return error_response(code=500, message=f"登录失败: {str(e)}")
|
||||
|
||||
@router.post("/create-payment", response_model=ResponseModel)
|
||||
async def create_payment(
|
||||
request: WechatPayRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: UserDB = Depends(get_current_user)
|
||||
):
|
||||
"""创建微信支付订单"""
|
||||
# 查询订单
|
||||
if request.order_type == "merchant_order":
|
||||
order = db.query(MerchantOrderDB).filter(
|
||||
MerchantOrderDB.order_id == request.order_id,
|
||||
MerchantOrderDB.user_id == current_user.userid,
|
||||
MerchantOrderDB.status == MerchantOrderStatus.CREATED
|
||||
).first()
|
||||
if not order:
|
||||
return error_response(code=404, message="订单不存在或状态不正确")
|
||||
amount = order.pay_amount
|
||||
description = "商家商品订单"
|
||||
|
||||
elif request.order_type == "merchant_pay_order":
|
||||
order = db.query(MerchantPayOrderDB).filter(
|
||||
MerchantPayOrderDB.order_id == request.order_id,
|
||||
MerchantPayOrderDB.user_id == current_user.userid,
|
||||
MerchantPayOrderDB.status == MerchantPayOrderStatus.UNPAID
|
||||
).first()
|
||||
if not order:
|
||||
return error_response(code=404, message="订单不存在或状态不正确")
|
||||
amount = order.amount
|
||||
description = "商家在线买单"
|
||||
|
||||
else:
|
||||
return error_response(code=400, message="不支持的订单类型")
|
||||
|
||||
try:
|
||||
# 初始化微信支付客户端
|
||||
wechat = WeChatClient()
|
||||
|
||||
# 创建支付订单
|
||||
result = await wechat.create_jsapi_payment(
|
||||
openid=current_user.openid,
|
||||
out_trade_no=request.order_id,
|
||||
total_amount=int(float(amount) * 100), # 转换为分
|
||||
description=description
|
||||
)
|
||||
|
||||
if not result:
|
||||
return error_response(code=500, message="创建支付订单失败")
|
||||
|
||||
return success_response(data={
|
||||
"prepay_id": result.get("prepay_id"),
|
||||
"payment_params": result.get("payment_params")
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return error_response(code=500, message=f"创建支付订单失败: {str(e)}")
|
||||
|
||||
@router.post("/payment-notify")
|
||||
async def payment_notify(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""微信支付回调通知"""
|
||||
try:
|
||||
# 初始化微信支付客户端
|
||||
wechat = WeChatClient()
|
||||
|
||||
# 验证并解析回调数据
|
||||
data = await wechat.verify_payment_notify(request)
|
||||
if not data:
|
||||
return error_response(code=400, message="回调数据验证失败")
|
||||
|
||||
# 获取订单信息
|
||||
out_trade_no = data.get("out_trade_no")
|
||||
transaction_id = data.get("transaction_id")
|
||||
trade_state = data.get("trade_state")
|
||||
success_time = data.get("success_time")
|
||||
|
||||
if trade_state != "SUCCESS":
|
||||
return error_response(code=400, message="支付未成功")
|
||||
|
||||
# 更新订单状态
|
||||
if out_trade_no.startswith("M"): # 商家商品订单
|
||||
order = db.query(MerchantOrderDB).filter(
|
||||
MerchantOrderDB.order_id == out_trade_no
|
||||
).first()
|
||||
if order:
|
||||
order.status = MerchantOrderStatus.UNVERIFIED
|
||||
order.pay_time = datetime.fromisoformat(success_time)
|
||||
order.transaction_id = transaction_id
|
||||
|
||||
elif out_trade_no.startswith("P"): # 商家在线买单
|
||||
order = db.query(MerchantPayOrderDB).filter(
|
||||
MerchantPayOrderDB.order_id == out_trade_no
|
||||
).first()
|
||||
if order:
|
||||
order.status = MerchantPayOrderStatus.PAID
|
||||
order.pay_time = datetime.fromisoformat(success_time)
|
||||
order.transaction_id = transaction_id
|
||||
|
||||
db.commit()
|
||||
|
||||
return success_response(message="支付成功")
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
return error_response(code=500, message=f"处理支付回调失败: {str(e)}")
|
||||
25
app/cert/apiclient_cert.pem
Normal file
25
app/cert/apiclient_cert.pem
Normal file
@ -0,0 +1,25 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEJDCCAwygAwIBAgIUWVjGYFtGURIuw2SDGjARwEfuVJwwDQYJKoZIhvcNAQEL
|
||||
BQAwXjELMAkGA1UEBhMCQ04xEzARBgNVBAoTClRlbnBheS5jb20xHTAbBgNVBAsT
|
||||
FFRlbnBheS5jb20gQ0EgQ2VudGVyMRswGQYDVQQDExJUZW5wYXkuY29tIFJvb3Qg
|
||||
Q0EwHhcNMjUwMTIzMTI1MTEyWhcNMzAwMTIyMTI1MTEyWjB+MRMwEQYDVQQDDAox
|
||||
NzA1MjU5ODM3MRswGQYDVQQKDBLlvq7kv6HllYbmiLfns7vnu58xKjAoBgNVBAsM
|
||||
IeaIkOmDveeIseWYiei+sOenkeaKgOaciemZkOWFrOWPuDELMAkGA1UEBhMCQ04x
|
||||
ETAPBgNVBAcMCFNoZW5aaGVuMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
|
||||
AQEAtwr4/6MeJ/YCFdQRG8qH/lv8TqXb/76NoPyj/pOgNuck/kQl/H9hf4kM74z2
|
||||
k7uR56q5bs8NteIHGpLg0Zo5QGMHWnAHamnYu5Sl7SLfIVHByKSWeG0BgIdvJiyO
|
||||
6PkpOyohMqbr55s4+eK6yLkblCaYF/dLE429W8UUoSC79BgVvt+YMWO75lX8OOLI
|
||||
Syz9MZSzm7j7N5ri3exa7BQ+5+j5a97IVZFUxWsHfmWMFRPVX+J7SrLkCeU3uDMo
|
||||
ltKaxGZ5Of3RmzodrgYZPNFbDkufFxnJMWKGSQtcs0wbUSTL1L7+OJ9e5E7huht2
|
||||
di5Lndgq92z7b6CvmS1afQKpRwIDAQABo4G5MIG2MAkGA1UdEwQCMAAwCwYDVR0P
|
||||
BAQDAgP4MIGbBgNVHR8EgZMwgZAwgY2ggYqggYeGgYRodHRwOi8vZXZjYS5pdHJ1
|
||||
cy5jb20uY24vcHVibGljL2l0cnVzY3JsP0NBPTFCRDQyMjBFNTBEQkMwNEIwNkFE
|
||||
Mzk3NTQ5ODQ2QzAxQzNFOEVCRDImc2c9SEFDQzQ3MUI2NTQyMkUxMkIyN0E5RDMz
|
||||
QTg3QUQxQ0RGNTkyNkUxNDAzNzEwDQYJKoZIhvcNAQELBQADggEBAKV4YtDUPdqW
|
||||
8NsWKm5NbIlL8LJBy2s7QcnGx8dWU48ykqn52VIy+HvcVHqwL/bWgHoH3/KQYlwk
|
||||
VmZeuGxiC+0bEcDxCUN8b48DFpcbiQKMvo1J8HAEGm1XAMZn5EZDapCNTzCADJf9
|
||||
a2A+GG6j6AlnkA3RRMWS0PhnZDXPkoBae/zRx9x+bBkltCA+YoUGAaNJFfEgOPkZ
|
||||
5rLD6YBlOC+5BX9gCXZs5efi1aFxLbhQoz/+pJ1zwz5ka231B0t0FPOKBek1Pcu1
|
||||
sZR/cOaen5Sd0p6C09da2WQT8uOD5XaEFZXI7KxrRPJ1RRFhLh8fc1bKR584rpsh
|
||||
hLL4HOEbk1c=
|
||||
-----END CERTIFICATE-----
|
||||
28
app/cert/apiclient_key.pem
Normal file
28
app/cert/apiclient_key.pem
Normal file
@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC3Cvj/ox4n9gIV
|
||||
1BEbyof+W/xOpdv/vo2g/KP+k6A25yT+RCX8f2F/iQzvjPaTu5Hnqrluzw214gca
|
||||
kuDRmjlAYwdacAdqadi7lKXtIt8hUcHIpJZ4bQGAh28mLI7o+Sk7KiEypuvnmzj5
|
||||
4rrIuRuUJpgX90sTjb1bxRShILv0GBW+35gxY7vmVfw44shLLP0xlLObuPs3muLd
|
||||
7FrsFD7n6Plr3shVkVTFawd+ZYwVE9Vf4ntKsuQJ5Te4MyiW0prEZnk5/dGbOh2u
|
||||
Bhk80VsOS58XGckxYoZJC1yzTBtRJMvUvv44n17kTuG6G3Z2Lkud2Cr3bPtvoK+Z
|
||||
LVp9AqlHAgMBAAECggEBAJk+ooDDu/eQyuYjib9OrNSThoUB71IJ4uEpItN8HOJa
|
||||
WmpV+8eNjb8MqrvTtIyyuNDP6jePOddQyMnCtl5FVDFHt1xL9qlsvHsvVEtYqp5m
|
||||
qGqnASMJf/xvZur62xrJn29dMjYJ8e8R0X3ECMUL1L8QIL3P2Bciz6oJMeBEW5db
|
||||
N/cTw4lbF05DeMHlPNAX+uog0e5qoRB/l5BxlT+n40IpcFNTLPRhJurgAPpTijkb
|
||||
EE0+K9v/9l5RG8xEX56MCChIHeCE3bNnVFPNzS6mtoDWu9OdwJ5PpdYG4LekDkfQ
|
||||
RgorIYCEhJ2yaZroaTn5FY3dahSGr7VVlqOAH2QGVIECgYEA4WCiyEEt8nPYN5Am
|
||||
LFG6/GHY7dzem7jY70AOuGAsSsX8dB1xfc3itsdqpm5XAWAQ1Pb5m/DWjlnL53p6
|
||||
YEF83H8r1jV5oPgyWBMoQAg1zpxjo6APz8d9msfeHF7KUfYvj3EE1TcrM+YY6dAq
|
||||
H1voLXCRguj9tIb+EebzOUxDGrcCgYEAz+nM7ygACNIDmt3JSZb+k8qL4grKQCoB
|
||||
iTzQHXR/VYZm8C+F+x8CpCHW5F1RsEqSHxJW2SUQX+xYa/fFrd/Q2aqMjqJ8UpCa
|
||||
7kHHPjUX5QWBgDQpg3tmGrbPkauVtaqeRsYAPb3r9dSdlqepPpsOaT6GgLSuTP4l
|
||||
x41a1RUGlfECgYAucVx6CbxvJuIaaREEtv7iPUOXmJki28+QVdHyupbF/dCNGPgn
|
||||
JYMfiS54B2rUdLhjOlWrhdCg2u5C0CFhrn0NbwNYjAJ5Ykv1jFUSBN8ZqW567GP1
|
||||
vDUs7RzfGcV1aFbapz6ItWqosjTWEbhsZ+MLYhQKNvr49YxrofzjBM0bNwKBgQCq
|
||||
PlhHL+qvTkATbC2o61GzdHOL+KfZWEv/suL6a2zke/QIEfHUSXUhLnBGd78u6jCx
|
||||
7pNcpMO+t8lDRxP/prfds4/6L0Q7WxrxorzhzBmvtw1uC8g+WCmoEC7wqZ4hrf6C
|
||||
FxkVdVEj7x/Gv6yOjeqD9OWvt8LNWoFW4AETX28QEQKBgC2cpAtb0zbPDEqXbnod
|
||||
NmC5W34AJDmVvah9w9SZyQeK4RHkYU604qg0aBEuKtkV9sNxolLydnfn/eqyepjk
|
||||
8XwrEU/2EI9t8uGQMB+TjL/Z7eYoBYVyaugbksyEZo6heD2NvHwGPl1c4s0O/gsY
|
||||
R1k9gdf+oEAcZVVH4V7367Ou
|
||||
-----END PRIVATE KEY-----
|
||||
@ -54,11 +54,11 @@ class Settings(BaseSettings):
|
||||
|
||||
WECHAT_APPID: str = "wx3cc5b7dcb28f2756"
|
||||
WECHAT_SECRET: str = "fdf03e0ff428097c2a264da50b7d804e"
|
||||
WECHAT_MCH_ID: str = "1688852888"
|
||||
WECHAT_PRIVATE_KEY_PATH: str = "app/core/wechat_private_key.pem"
|
||||
WECHAT_CERT_SERIAL_NO: str = "1688852888"
|
||||
WECHAT_API_V3_KEY: str = "your-api-v3-key" # API v3密钥
|
||||
WECHAT_PLATFORM_CERT_PATH: str = "app/core/wechat_platform_cert.pem" # 平台证书路径
|
||||
WECHAT_MCH_ID: str = "1705259837"
|
||||
WECHAT_PRIVATE_KEY_PATH: str = "app/cert/apiclient_key.pem"
|
||||
WECHAT_CERT_SERIAL_NO: str = "5958C6605B4651122EC364831A3011C047EE549C"
|
||||
WECHAT_API_V3_KEY: str = "OAhAqXqebeT4ZC9VTYFkSWU0CENEahx5" # API v3密钥
|
||||
WECHAT_PLATFORM_CERT_PATH: str = "app/cert/apiclient_cert.pem" # 平台证书路径
|
||||
|
||||
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@ import random
|
||||
import string
|
||||
from cryptography.x509 import load_pem_x509_certificate
|
||||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||||
import uuid
|
||||
|
||||
def generate_random_string(length=32):
|
||||
"""生成指定长度的随机字符串"""
|
||||
@ -23,6 +24,23 @@ class WeChatClient:
|
||||
def __init__(self):
|
||||
self.appid = settings.WECHAT_APPID
|
||||
self.secret = settings.WECHAT_SECRET
|
||||
self.mch_id = settings.WECHAT_MCH_ID
|
||||
self.private_key_path = settings.WECHAT_PRIVATE_KEY_PATH
|
||||
self.cert_serial_no = settings.WECHAT_CERT_SERIAL_NO
|
||||
self.api_v3_key = settings.WECHAT_API_V3_KEY
|
||||
self.platform_cert_path = settings.WECHAT_PLATFORM_CERT_PATH
|
||||
|
||||
# 加载商户私钥
|
||||
with open(self.private_key_path, "rb") as f:
|
||||
self.private_key = serialization.load_pem_private_key(
|
||||
f.read(),
|
||||
password=None
|
||||
)
|
||||
|
||||
# 加载平台证书
|
||||
with open(self.platform_cert_path, "rb") as f:
|
||||
self.platform_cert = load_pem_x509_certificate(f.read())
|
||||
self.platform_public_key = self.platform_cert.public_key()
|
||||
|
||||
async def get_access_token(self):
|
||||
"""获取小程序全局接口调用凭据"""
|
||||
@ -100,4 +118,183 @@ class WeChatClient:
|
||||
|
||||
return result
|
||||
|
||||
def sign_message(self, method: str, url_path: str, body: dict) -> tuple:
|
||||
"""生成请求签名
|
||||
|
||||
Returns:
|
||||
tuple: (nonce_str, timestamp, signature)
|
||||
"""
|
||||
nonce_str = str(uuid.uuid4()).replace('-', '')
|
||||
timestamp = str(int(time.time()))
|
||||
|
||||
# 构造签名字符串
|
||||
sign_str = f"{method}\n{url_path}\n{timestamp}\n{nonce_str}\n"
|
||||
if body:
|
||||
sign_str += f"{json.dumps(body)}\n"
|
||||
else:
|
||||
sign_str += "\n"
|
||||
|
||||
# 使用私钥签名
|
||||
signature = self.private_key.sign(
|
||||
sign_str.encode('utf-8'),
|
||||
padding.PKCS1v15(),
|
||||
hashes.SHA256()
|
||||
)
|
||||
|
||||
return nonce_str, timestamp, base64.b64encode(signature).decode()
|
||||
|
||||
def verify_response(self, headers: dict, body: bytes) -> bool:
|
||||
"""验证响应签名"""
|
||||
timestamp = headers.get('Wechatpay-Timestamp')
|
||||
nonce = headers.get('Wechatpay-Nonce')
|
||||
signature = headers.get('Wechatpay-Signature')
|
||||
serial = headers.get('Wechatpay-Serial')
|
||||
|
||||
if not all([timestamp, nonce, signature, serial]):
|
||||
return False
|
||||
|
||||
# 构造验签字符串
|
||||
sign_str = f"{timestamp}\n{nonce}\n{body.decode('utf-8')}\n"
|
||||
|
||||
try:
|
||||
# 验证签名
|
||||
self.platform_public_key.verify(
|
||||
base64.b64decode(signature),
|
||||
sign_str.encode('utf-8'),
|
||||
padding.PKCS1v15(),
|
||||
hashes.SHA256()
|
||||
)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
async def create_jsapi_payment(
|
||||
self,
|
||||
openid: str,
|
||||
out_trade_no: str,
|
||||
total_amount: int,
|
||||
description: str
|
||||
) -> dict:
|
||||
"""创建 JSAPI 支付订单"""
|
||||
url_path = "/v3/pay/transactions/jsapi"
|
||||
api_url = f"https://api.mch.weixin.qq.com{url_path}"
|
||||
|
||||
# 构建请求数据
|
||||
body = {
|
||||
"appid": self.appid,
|
||||
"mchid": self.mch_id,
|
||||
"description": description,
|
||||
"out_trade_no": out_trade_no,
|
||||
"notify_url": f"{settings.API_BASE_URL}/api/wechat/payment-notify",
|
||||
"amount": {
|
||||
"total": total_amount,
|
||||
"currency": "CNY"
|
||||
},
|
||||
"payer": {
|
||||
"openid": openid
|
||||
}
|
||||
}
|
||||
|
||||
# 生成签名
|
||||
nonce_str, timestamp, signature = self.sign_message("POST", url_path, body)
|
||||
|
||||
# 构建认证头
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': (
|
||||
f'WECHATPAY2-SHA256-RSA2048 '
|
||||
f'mchid="{self.mch_id}",'
|
||||
f'nonce_str="{nonce_str}",'
|
||||
f'timestamp="{timestamp}",'
|
||||
f'serial_no="{self.cert_serial_no}",'
|
||||
f'signature="{signature}"'
|
||||
)
|
||||
}
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(api_url, json=body, headers=headers) as response:
|
||||
result = await response.json()
|
||||
|
||||
if response.status != 200:
|
||||
raise Exception(f"请求失败: {result.get('message')}")
|
||||
|
||||
# 验证响应签名
|
||||
if not self.verify_response(response.headers, await response.read()):
|
||||
raise Exception("响应签名验证失败")
|
||||
|
||||
# 生成小程序调起支付的参数
|
||||
prepay_id = result.get("prepay_id")
|
||||
if not prepay_id:
|
||||
raise Exception("未获取到prepay_id")
|
||||
|
||||
timestamp = str(int(time.time()))
|
||||
nonce_str = str(uuid.uuid4()).replace('-', '')
|
||||
package = f"prepay_id={prepay_id}"
|
||||
|
||||
# 签名支付参数
|
||||
sign_str = f"{self.appid}\n{timestamp}\n{nonce_str}\n{package}\n"
|
||||
signature = base64.b64encode(
|
||||
self.private_key.sign(
|
||||
sign_str.encode('utf-8'),
|
||||
padding.PKCS1v15(),
|
||||
hashes.SHA256()
|
||||
)
|
||||
).decode()
|
||||
|
||||
return {
|
||||
"prepay_id": prepay_id,
|
||||
"payment_params": {
|
||||
"appId": self.appid,
|
||||
"timeStamp": timestamp,
|
||||
"nonceStr": nonce_str,
|
||||
"package": package,
|
||||
"signType": "RSA",
|
||||
"paySign": signature
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(f"创建支付订单失败: {str(e)}")
|
||||
|
||||
async def verify_payment_notify(self, request: Request) -> dict:
|
||||
"""验证支付回调通知"""
|
||||
# 获取请求头
|
||||
headers = {
|
||||
'Wechatpay-Signature': request.headers.get('Wechatpay-Signature'),
|
||||
'Wechatpay-Timestamp': request.headers.get('Wechatpay-Timestamp'),
|
||||
'Wechatpay-Nonce': request.headers.get('Wechatpay-Nonce'),
|
||||
'Wechatpay-Serial': request.headers.get('Wechatpay-Serial')
|
||||
}
|
||||
|
||||
# 读取请求体
|
||||
body = await request.body()
|
||||
|
||||
# 验证签名
|
||||
if not self.verify_response(headers, body):
|
||||
raise Exception("回调通知签名验证失败")
|
||||
|
||||
# 解析数据
|
||||
try:
|
||||
data = json.loads(body)
|
||||
resource = data.get('resource', {})
|
||||
|
||||
# 解密数据
|
||||
nonce = base64.b64decode(resource['nonce'])
|
||||
associated_data = resource.get('associated_data', '').encode('utf-8')
|
||||
ciphertext = base64.b64decode(resource['ciphertext'])
|
||||
|
||||
aesgcm = AESGCM(self.api_v3_key.encode('utf-8'))
|
||||
decrypted_data = aesgcm.decrypt(
|
||||
nonce,
|
||||
ciphertext,
|
||||
associated_data
|
||||
)
|
||||
|
||||
return json.loads(decrypted_data)
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(f"解析回调数据失败: {str(e)}")
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user