增加微信支付的接口
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.response import success_response, error_response, ResponseModel
|
||||||
from app.core.wechat import WeChatClient,generate_random_string
|
from app.core.wechat import WeChatClient,generate_random_string
|
||||||
from app.core.security import create_access_token, set_jwt_cookie
|
from app.core.security import create_access_token, set_jwt_cookie
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel, Field
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
@ -14,6 +14,8 @@ from app.api.deps import get_current_user
|
|||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
|
from app.models.merchant_order import MerchantOrderDB, MerchantOrderStatus
|
||||||
|
from app.models.merchant_pay_order import MerchantPayOrderDB, MerchantPayOrderStatus
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@ -22,6 +24,11 @@ class PhoneNumberRequest(BaseModel):
|
|||||||
phone_code: str # 手机号验证码
|
phone_code: str # 手机号验证码
|
||||||
referral_code: str = None # 推荐码(可选)
|
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)
|
@router.post("/phone-login", response_model=ResponseModel)
|
||||||
async def wechat_phone_login(
|
async def wechat_phone_login(
|
||||||
request: PhoneNumberRequest,
|
request: PhoneNumberRequest,
|
||||||
@ -97,3 +104,110 @@ async def wechat_phone_login(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
return error_response(code=500, message=f"登录失败: {str(e)}")
|
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_APPID: str = "wx3cc5b7dcb28f2756"
|
||||||
WECHAT_SECRET: str = "fdf03e0ff428097c2a264da50b7d804e"
|
WECHAT_SECRET: str = "fdf03e0ff428097c2a264da50b7d804e"
|
||||||
WECHAT_MCH_ID: str = "1688852888"
|
WECHAT_MCH_ID: str = "1705259837"
|
||||||
WECHAT_PRIVATE_KEY_PATH: str = "app/core/wechat_private_key.pem"
|
WECHAT_PRIVATE_KEY_PATH: str = "app/cert/apiclient_key.pem"
|
||||||
WECHAT_CERT_SERIAL_NO: str = "1688852888"
|
WECHAT_CERT_SERIAL_NO: str = "5958C6605B4651122EC364831A3011C047EE549C"
|
||||||
WECHAT_API_V3_KEY: str = "your-api-v3-key" # API v3密钥
|
WECHAT_API_V3_KEY: str = "OAhAqXqebeT4ZC9VTYFkSWU0CENEahx5" # API v3密钥
|
||||||
WECHAT_PLATFORM_CERT_PATH: str = "app/core/wechat_platform_cert.pem" # 平台证书路径
|
WECHAT_PLATFORM_CERT_PATH: str = "app/cert/apiclient_cert.pem" # 平台证书路径
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import random
|
|||||||
import string
|
import string
|
||||||
from cryptography.x509 import load_pem_x509_certificate
|
from cryptography.x509 import load_pem_x509_certificate
|
||||||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||||||
|
import uuid
|
||||||
|
|
||||||
def generate_random_string(length=32):
|
def generate_random_string(length=32):
|
||||||
"""生成指定长度的随机字符串"""
|
"""生成指定长度的随机字符串"""
|
||||||
@ -23,6 +24,23 @@ class WeChatClient:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.appid = settings.WECHAT_APPID
|
self.appid = settings.WECHAT_APPID
|
||||||
self.secret = settings.WECHAT_SECRET
|
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):
|
async def get_access_token(self):
|
||||||
"""获取小程序全局接口调用凭据"""
|
"""获取小程序全局接口调用凭据"""
|
||||||
@ -100,4 +118,183 @@ class WeChatClient:
|
|||||||
|
|
||||||
return result
|
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