From 5faf42b4512192ec0b2fe346fe06292aab192ee8 Mon Sep 17 00:00:00 2001 From: wwwwwwwww <526125649@qq.com> Date: Mon, 15 Jun 2026 09:26:04 +0800 Subject: [PATCH 01/16] 1 --- video-gen-api/app/api/v1/payments.py | 64 +++++++++++++++++----------- 1 file changed, 39 insertions(+), 25 deletions(-) diff --git a/video-gen-api/app/api/v1/payments.py b/video-gen-api/app/api/v1/payments.py index 2a35cd01..17f5d95c 100644 --- a/video-gen-api/app/api/v1/payments.py +++ b/video-gen-api/app/api/v1/payments.py @@ -107,18 +107,16 @@ async def wechat_callback(request: Request, db: AsyncSession = Depends(get_db)): # 真实模式:使用 wechatpayv3 SDK 验证回调并解析数据 try: + from wechatpayv3 import Sign, AesCrypto + mch_id = db_configs.get("payment_wechat_mch_id", "") private_key = db_configs.get("payment_wechat_private_key", "") cert_serial_no = db_configs.get("payment_wechat_cert_serial_no", "") api_v3_key = db_configs.get("payment_wechat_api_v3_key", "") - appid = db_configs.get("payment_wechat_appid", "") public_key = db_configs.get("payment_wechat_public_key", "") - public_key_id = db_configs.get("payment_wechat_public_key_id", "") - notify_url = db_configs.get("payment_wechat_notify_url", "") - client = _get_wechat_client(mch_id, private_key, cert_serial_no, api_v3_key, appid, notify_url, public_key, public_key_id) - if not client: - logger.error("WeChat client not initialized for callback") + if not all([mch_id, private_key, cert_serial_no, api_v3_key]): + logger.error("WeChat payment config missing for callback") return {"code": "SUCCESS", "message": "OK"} # 从请求头获取必要信息 @@ -128,31 +126,47 @@ async def wechat_callback(request: Request, db: AsyncSession = Depends(get_db)): signature = headers.get("Wechatpay-Signature", "") serial_no = headers.get("Wechatpay-Serial", "") - # 验证签名 - is_verified = client.verify( - timestamp=timestamp, - nonce=nonce, - body=body_str, - signature=signature, - serial_no=serial_no - ) + # 验证签名:使用平台公钥验证 + if not public_key: + logger.warning("WeChat platform public key not configured, skipping signature verification") + else: + is_verified = Sign.verify( + public_key=public_key, + timestamp=timestamp, + nonce=nonce, + body=body_str, + signature=signature + ) + if not is_verified: + logger.warning("WeChat callback signature verification failed") + raise HTTPException(status_code=400, detail="签名验证失败") - if not is_verified: - logger.warning("WeChat callback signature verification failed") - raise HTTPException(status_code=400, detail="签名验证失败") + # 解密回调数据:使用 API v3 key + crypto = AesCrypto(api_v3_key) + import json + body_data = json.loads(body_str) if body_str else {} + resource = body_data.get("resource", {}) - # 解密回调数据 - decrypted_data = client.decrypt(body_str) - if not decrypted_data: + if not resource: + logger.error("WeChat callback resource not found") + raise HTTPException(status_code=400, detail="数据格式错误") + + ciphertext = resource.get("ciphertext", "") + associated_data = resource.get("associated_data", "") + nonce_str = resource.get("nonce", "") + + decrypted_str = crypto.decrypt_gcm(ciphertext, associated_data, nonce_str) + if not decrypted_str: logger.error("WeChat callback decryption failed") raise HTTPException(status_code=400, detail="数据解密失败") + decrypted_data = json.loads(decrypted_str) + # 处理支付成功回调 - if decrypted_data.get("event_type") == "TRANSACTION.SUCCESS": - resource = decrypted_data.get("resource", {}) - order_no = resource.get("out_trade_no", "") - transaction_id = resource.get("transaction_id", "") - amount_info = resource.get("amount", {}) + if body_data.get("event_type") == "TRANSACTION.SUCCESS": + order_no = decrypted_data.get("out_trade_no", "") + transaction_id = decrypted_data.get("transaction_id", "") + amount_info = decrypted_data.get("amount", {}) total_amount = amount_info.get("total", 0) / 100 # 转换为元 if order_no: From 5a86350829e86bec0d8660b99581a62345c3e7dc Mon Sep 17 00:00:00 2001 From: wwwwwwwww <526125649@qq.com> Date: Mon, 15 Jun 2026 09:30:25 +0800 Subject: [PATCH 02/16] 1 --- video-gen-api/app/api/v1/payments.py | 56 +++++++++++++++++----------- 1 file changed, 35 insertions(+), 21 deletions(-) diff --git a/video-gen-api/app/api/v1/payments.py b/video-gen-api/app/api/v1/payments.py index 17f5d95c..76ab7ced 100644 --- a/video-gen-api/app/api/v1/payments.py +++ b/video-gen-api/app/api/v1/payments.py @@ -105,17 +105,18 @@ async def wechat_callback(request: Request, db: AsyncSession = Depends(get_db)): logger.exception(f"Mock WeChat callback error: {e}") return {"code": "SUCCESS", "message": "OK"} # 微信要求即使处理失败也返回成功 - # 真实模式:使用 wechatpayv3 SDK 验证回调并解析数据 + # 真实模式:使用 wechatpayv3 SDK 工具验证回调并解析数据 try: - from wechatpayv3 import Sign, AesCrypto + from wechatpayv3.utils import ( + rsa_verify, load_public_key, sha256, b64decode, + AESGCM, InvalidTag + ) mch_id = db_configs.get("payment_wechat_mch_id", "") - private_key = db_configs.get("payment_wechat_private_key", "") - cert_serial_no = db_configs.get("payment_wechat_cert_serial_no", "") api_v3_key = db_configs.get("payment_wechat_api_v3_key", "") public_key = db_configs.get("payment_wechat_public_key", "") - if not all([mch_id, private_key, cert_serial_no, api_v3_key]): + if not all([mch_id, api_v3_key]): logger.error("WeChat payment config missing for callback") return {"code": "SUCCESS", "message": "OK"} @@ -124,25 +125,28 @@ async def wechat_callback(request: Request, db: AsyncSession = Depends(get_db)): timestamp = headers.get("Wechatpay-Timestamp", "") nonce = headers.get("Wechatpay-Nonce", "") signature = headers.get("Wechatpay-Signature", "") - serial_no = headers.get("Wechatpay-Serial", "") # 验证签名:使用平台公钥验证 - if not public_key: - logger.warning("WeChat platform public key not configured, skipping signature verification") - else: - is_verified = Sign.verify( - public_key=public_key, - timestamp=timestamp, - nonce=nonce, - body=body_str, - signature=signature - ) - if not is_verified: - logger.warning("WeChat callback signature verification failed") + if public_key: + try: + # 构造签名串 + sign_string = f"{timestamp}\n{nonce}\n{body_str}\n" + # 验证签名 + is_verified = rsa_verify( + public_key=load_public_key(public_key), + data=sign_string.encode('utf-8'), + signature=b64decode(signature) + ) + if not is_verified: + logger.warning("WeChat callback signature verification failed") + raise HTTPException(status_code=400, detail="签名验证失败") + except Exception as e: + logger.warning(f"WeChat signature verification error: {e}") raise HTTPException(status_code=400, detail="签名验证失败") + else: + logger.warning("WeChat platform public key not configured, skipping signature verification") # 解密回调数据:使用 API v3 key - crypto = AesCrypto(api_v3_key) import json body_data = json.loads(body_str) if body_str else {} resource = body_data.get("resource", {}) @@ -155,9 +159,19 @@ async def wechat_callback(request: Request, db: AsyncSession = Depends(get_db)): associated_data = resource.get("associated_data", "") nonce_str = resource.get("nonce", "") - decrypted_str = crypto.decrypt_gcm(ciphertext, associated_data, nonce_str) + # 使用 AES-GCM 解密 + try: + aesgcm = AESGCM(api_v3_key) + decrypted_str = aesgcm.decrypt(ciphertext, associated_data, nonce_str) + except InvalidTag: + logger.error("WeChat callback decryption failed: Invalid tag") + raise HTTPException(status_code=400, detail="数据解密失败") + except Exception as e: + logger.error(f"WeChat callback decryption failed: {e}") + raise HTTPException(status_code=400, detail="数据解密失败") + if not decrypted_str: - logger.error("WeChat callback decryption failed") + logger.error("WeChat callback decryption returned empty") raise HTTPException(status_code=400, detail="数据解密失败") decrypted_data = json.loads(decrypted_str) From c4c8f158f7fe4e2aabb5d7aa04063a43826e209f Mon Sep 17 00:00:00 2001 From: wwwwwwwww <526125649@qq.com> Date: Mon, 15 Jun 2026 09:39:29 +0800 Subject: [PATCH 03/16] 1 --- video-gen-api/app/api/v1/payments.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/video-gen-api/app/api/v1/payments.py b/video-gen-api/app/api/v1/payments.py index 76ab7ced..18294e51 100644 --- a/video-gen-api/app/api/v1/payments.py +++ b/video-gen-api/app/api/v1/payments.py @@ -129,13 +129,13 @@ async def wechat_callback(request: Request, db: AsyncSession = Depends(get_db)): # 验证签名:使用平台公钥验证 if public_key: try: - # 构造签名串 - sign_string = f"{timestamp}\n{nonce}\n{body_str}\n" - # 验证签名 + # 使用 wechatpayv3.utils.rsa_verify 验证签名 is_verified = rsa_verify( - public_key=load_public_key(public_key), - data=sign_string.encode('utf-8'), - signature=b64decode(signature) + timestamp=timestamp, + nonce=nonce, + body=body_str, + signature=signature, + public_key=load_public_key(public_key) ) if not is_verified: logger.warning("WeChat callback signature verification failed") From 105968f1f7f5016d30708fd7a3d981b0ac663f00 Mon Sep 17 00:00:00 2001 From: wwwwwwwww <526125649@qq.com> Date: Mon, 15 Jun 2026 09:41:13 +0800 Subject: [PATCH 04/16] 1 --- video-gen-api/app/services/payment.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/video-gen-api/app/services/payment.py b/video-gen-api/app/services/payment.py index bc87720a..57b8e23c 100644 --- a/video-gen-api/app/services/payment.py +++ b/video-gen-api/app/services/payment.py @@ -452,7 +452,7 @@ def _create_wechat_order(order: PaymentOrder, db_configs: dict[str, str]) -> str try: # 调用微信支付 Native 下单接口 code, result = client.pay( - description=f"充值订单 {order.order_no}", + description=f"充值积分{order.credits},订单 {order.order_no}", out_trade_no=order.order_no, amount={ "total": int(order.amount * 100), # 微信支付以分为单位 @@ -656,7 +656,7 @@ def _create_alipay_order(order: PaymentOrder, db_configs: dict[str, str]) -> str model = AlipayTradePrecreateModel() model.out_trade_no = order.order_no model.total_amount = f"{order.amount:.2f}" - model.subject = f"充值订单 {order.order_no}" + model.subject = f"充值积分{order.credits},订单 {order.order_no}" model.product_code = "QR_CODE_OFFLINE" body_parts = [] From fb4f59962bf65f9db8f1d51f6d3c6e43a744b3d3 Mon Sep 17 00:00:00 2001 From: wwwwwwwww <526125649@qq.com> Date: Mon, 15 Jun 2026 10:00:14 +0800 Subject: [PATCH 05/16] 1 --- video-gen-api/app/api/v1/payments.py | 28 ++++++++++++++++++- video-gen-api/app/services/payment.py | 40 ++++++++++++++++++--------- 2 files changed, 54 insertions(+), 14 deletions(-) diff --git a/video-gen-api/app/api/v1/payments.py b/video-gen-api/app/api/v1/payments.py index 18294e51..92c09466 100644 --- a/video-gen-api/app/api/v1/payments.py +++ b/video-gen-api/app/api/v1/payments.py @@ -176,8 +176,10 @@ async def wechat_callback(request: Request, db: AsyncSession = Depends(get_db)): decrypted_data = json.loads(decrypted_str) + event_type = body_data.get("event_type", "") + # 处理支付成功回调 - if body_data.get("event_type") == "TRANSACTION.SUCCESS": + if event_type == "TRANSACTION.SUCCESS": order_no = decrypted_data.get("out_trade_no", "") transaction_id = decrypted_data.get("transaction_id", "") amount_info = decrypted_data.get("amount", {}) @@ -190,6 +192,30 @@ async def wechat_callback(request: Request, db: AsyncSession = Depends(get_db)): f"transaction_id={transaction_id}, amount={total_amount}" ) + # 处理退款回调 + elif event_type == "REFUND.SUCCESS": + order_no = decrypted_data.get("out_trade_no", "") + refund_id = decrypted_data.get("refund_id", "") + refund_status = decrypted_data.get("status", "") + + if order_no and refund_status == "SUCCESS": + # 更新订单状态为已退款 + from app.models import PaymentOrder + from sqlalchemy import select + + result = await db.execute(select(PaymentOrder).where(PaymentOrder.order_no == order_no)) + order = result.scalar_one_or_none() + + if order and order.status == "refunding": + order.status = "refunded" + order.transaction_id = refund_id + await db.commit() + + logger.info( + f"WeChat refund callback processed: order_no={order_no}, " + f"refund_id={refund_id}, status={refund_status}" + ) + return {"code": "SUCCESS", "message": "OK"} except Exception as e: logger.exception(f"WeChat callback processing error: {e}") diff --git a/video-gen-api/app/services/payment.py b/video-gen-api/app/services/payment.py index 57b8e23c..b6c61ab3 100644 --- a/video-gen-api/app/services/payment.py +++ b/video-gen-api/app/services/payment.py @@ -565,7 +565,10 @@ async def _refund_wechat_order( refund_reason: str, db_configs: dict[str, str] ) -> dict: - """Call WeChat Pay refund API.""" + """Call WeChat Pay refund API. + + 微信退款是异步的,调用后会返回PROCESSING状态,实际退款结果通过回调通知。 + """ mch_id = db_configs.get("payment_wechat_mch_id", "") private_key = db_configs.get("payment_wechat_private_key", "") cert_serial_no = db_configs.get("payment_wechat_cert_serial_no", "") @@ -599,18 +602,29 @@ async def _refund_wechat_order( data = _parse_wechat_result(result) - if code == 200 and data.get('status') == 'SUCCESS': - logger.info(f"WeChat refund succeeded: order_no={order.order_no}") - return {"success": True, "refund_id": data.get('refund_id')} - else: - logger.error( - f"WeChat refund failed: order_no={order.order_no}, " - f"code={code}, result={result}" - ) - return { - "success": False, - "message": f"微信退款失败: code={code}, {data.get('code', '')}" - } + # 微信退款是异步的,PROCESSING是正常状态,表示退款已受理 + if code == 200: + status = data.get('status') + if status in ('SUCCESS', 'PROCESSING', 'REFUNDCLOSE'): + logger.info( + f"WeChat refund initiated: order_no={order.order_no}, " + f"status={status}, refund_id={data.get('refund_id')}" + ) + return { + "success": True, + "refund_id": data.get('refund_id'), + "status": status, + "message": "退款申请已提交,等待微信处理" + } + + logger.error( + f"WeChat refund failed: order_no={order.order_no}, " + f"code={code}, result={result}" + ) + return { + "success": False, + "message": f"微信退款失败: code={code}, {data.get('message', '')}" + } except Exception as e: logger.exception(f"WeChat refund exception: order_no={order.order_no}") return {"success": False, "message": f"微信退款异常: {str(e)}"} From c00e453f87abf183a6d1fb3809e71cea0f1919d8 Mon Sep 17 00:00:00 2001 From: wwwwwwwww <526125649@qq.com> Date: Mon, 15 Jun 2026 10:11:12 +0800 Subject: [PATCH 06/16] 1 --- video-gen-api/app/api/v1/payments.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/video-gen-api/app/api/v1/payments.py b/video-gen-api/app/api/v1/payments.py index 92c09466..490d2533 100644 --- a/video-gen-api/app/api/v1/payments.py +++ b/video-gen-api/app/api/v1/payments.py @@ -125,11 +125,13 @@ async def wechat_callback(request: Request, db: AsyncSession = Depends(get_db)): timestamp = headers.get("Wechatpay-Timestamp", "") nonce = headers.get("Wechatpay-Nonce", "") signature = headers.get("Wechatpay-Signature", "") + serial_no = headers.get("Wechatpay-Serial", "") # 验证签名:使用平台公钥验证 - if public_key: + if public_key and serial_no: try: - # 使用 wechatpayv3.utils.rsa_verify 验证签名 + # 构造签名串:timestamp + "\n" + nonce + "\n" + body + "\n" + # 符合微信支付官方文档规范:https://pay.weixin.qq.com/doc/v3/merchant/4013053249 is_verified = rsa_verify( timestamp=timestamp, nonce=nonce, @@ -138,13 +140,17 @@ async def wechat_callback(request: Request, db: AsyncSession = Depends(get_db)): public_key=load_public_key(public_key) ) if not is_verified: - logger.warning("WeChat callback signature verification failed") + logger.warning(f"WeChat callback signature verification failed: serial={serial_no}") raise HTTPException(status_code=400, detail="签名验证失败") + logger.debug(f"WeChat callback signature verified: serial={serial_no}") except Exception as e: - logger.warning(f"WeChat signature verification error: {e}") + logger.warning(f"WeChat signature verification error: {e}, serial={serial_no}") raise HTTPException(status_code=400, detail="签名验证失败") else: - logger.warning("WeChat platform public key not configured, skipping signature verification") + if not public_key: + logger.warning("WeChat platform public key not configured, skipping signature verification") + if not serial_no: + logger.warning("Wechatpay-Serial header missing, skipping signature verification") # 解密回调数据:使用 API v3 key import json From b87c0e8f01eebadfeee659d8a72b1098cdcdf50a Mon Sep 17 00:00:00 2001 From: wwwwwwwww <526125649@qq.com> Date: Mon, 15 Jun 2026 10:12:55 +0800 Subject: [PATCH 07/16] 1 --- video-gen-api/app/api/v1/payments.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/video-gen-api/app/api/v1/payments.py b/video-gen-api/app/api/v1/payments.py index 490d2533..e3fc9277 100644 --- a/video-gen-api/app/api/v1/payments.py +++ b/video-gen-api/app/api/v1/payments.py @@ -167,7 +167,9 @@ async def wechat_callback(request: Request, db: AsyncSession = Depends(get_db)): # 使用 AES-GCM 解密 try: - aesgcm = AESGCM(api_v3_key) + # API v3 key 需要转换为字节串 + api_v3_key_bytes = api_v3_key.encode('utf-8') + aesgcm = AESGCM(api_v3_key_bytes) decrypted_str = aesgcm.decrypt(ciphertext, associated_data, nonce_str) except InvalidTag: logger.error("WeChat callback decryption failed: Invalid tag") From fa6543ea54774088a74e3e8288f2a74cba27dde6 Mon Sep 17 00:00:00 2001 From: wwwwwwwww <526125649@qq.com> Date: Mon, 15 Jun 2026 10:17:55 +0800 Subject: [PATCH 08/16] 1 --- video-gen-api/app/api/v1/payments.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/video-gen-api/app/api/v1/payments.py b/video-gen-api/app/api/v1/payments.py index e3fc9277..a94bd1c3 100644 --- a/video-gen-api/app/api/v1/payments.py +++ b/video-gen-api/app/api/v1/payments.py @@ -167,10 +167,11 @@ async def wechat_callback(request: Request, db: AsyncSession = Depends(get_db)): # 使用 AES-GCM 解密 try: - # API v3 key 需要转换为字节串 + # API v3 key 和 nonce 需要转换为字节串 api_v3_key_bytes = api_v3_key.encode('utf-8') + nonce_bytes = nonce_str.encode('utf-8') aesgcm = AESGCM(api_v3_key_bytes) - decrypted_str = aesgcm.decrypt(ciphertext, associated_data, nonce_str) + decrypted_str = aesgcm.decrypt(ciphertext, associated_data, nonce_bytes) except InvalidTag: logger.error("WeChat callback decryption failed: Invalid tag") raise HTTPException(status_code=400, detail="数据解密失败") From d222291135ce2bc719caff1a370dcc4eb2b50603 Mon Sep 17 00:00:00 2001 From: wwwwwwwww <526125649@qq.com> Date: Mon, 15 Jun 2026 10:19:24 +0800 Subject: [PATCH 09/16] 1 --- video-gen-api/app/api/v1/payments.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/video-gen-api/app/api/v1/payments.py b/video-gen-api/app/api/v1/payments.py index a94bd1c3..58b384ae 100644 --- a/video-gen-api/app/api/v1/payments.py +++ b/video-gen-api/app/api/v1/payments.py @@ -167,11 +167,14 @@ async def wechat_callback(request: Request, db: AsyncSession = Depends(get_db)): # 使用 AES-GCM 解密 try: - # API v3 key 和 nonce 需要转换为字节串 + # 所有参数都需要转换为字节串 api_v3_key_bytes = api_v3_key.encode('utf-8') - nonce_bytes = nonce_str.encode('utf-8') + ciphertext_bytes = ciphertext.encode('utf-8') if ciphertext else b'' + associated_data_bytes = associated_data.encode('utf-8') if associated_data else b'' + nonce_bytes = nonce_str.encode('utf-8') if nonce_str else b'' + aesgcm = AESGCM(api_v3_key_bytes) - decrypted_str = aesgcm.decrypt(ciphertext, associated_data, nonce_bytes) + decrypted_str = aesgcm.decrypt(ciphertext_bytes, associated_data_bytes, nonce_bytes) except InvalidTag: logger.error("WeChat callback decryption failed: Invalid tag") raise HTTPException(status_code=400, detail="数据解密失败") From 20d4b8d4225191b9906e257a5c0fce30f3b68fde Mon Sep 17 00:00:00 2001 From: wwwwwwwww <526125649@qq.com> Date: Mon, 15 Jun 2026 10:21:28 +0800 Subject: [PATCH 10/16] 1 --- video-gen-api/app/api/v1/payments.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/video-gen-api/app/api/v1/payments.py b/video-gen-api/app/api/v1/payments.py index 58b384ae..c83d2034 100644 --- a/video-gen-api/app/api/v1/payments.py +++ b/video-gen-api/app/api/v1/payments.py @@ -167,11 +167,15 @@ async def wechat_callback(request: Request, db: AsyncSession = Depends(get_db)): # 使用 AES-GCM 解密 try: - # 所有参数都需要转换为字节串 + # API v3 key 需要转换为字节串 api_v3_key_bytes = api_v3_key.encode('utf-8') - ciphertext_bytes = ciphertext.encode('utf-8') if ciphertext else b'' + + # ciphertext 和 nonce 是 Base64 编码的,需要解码 + ciphertext_bytes = b64decode(ciphertext) if ciphertext else b'' + nonce_bytes = b64decode(nonce_str) if nonce_str else b'' + + # associated_data 是字符串,直接编码 associated_data_bytes = associated_data.encode('utf-8') if associated_data else b'' - nonce_bytes = nonce_str.encode('utf-8') if nonce_str else b'' aesgcm = AESGCM(api_v3_key_bytes) decrypted_str = aesgcm.decrypt(ciphertext_bytes, associated_data_bytes, nonce_bytes) From 013e99cd24b5d7a5f557dbbb080adc2c092c5a1b Mon Sep 17 00:00:00 2001 From: wwwwwwwww <526125649@qq.com> Date: Mon, 15 Jun 2026 10:26:13 +0800 Subject: [PATCH 11/16] 1 --- video-gen-api/app/api/v1/payments.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/video-gen-api/app/api/v1/payments.py b/video-gen-api/app/api/v1/payments.py index c83d2034..5c8f3840 100644 --- a/video-gen-api/app/api/v1/payments.py +++ b/video-gen-api/app/api/v1/payments.py @@ -153,6 +153,7 @@ async def wechat_callback(request: Request, db: AsyncSession = Depends(get_db)): logger.warning("Wechatpay-Serial header missing, skipping signature verification") # 解密回调数据:使用 API v3 key + # 官方文档:https://pay.weixin.qq.com/doc/v3/merchant/4012071382 import json body_data = json.loads(body_str) if body_str else {} resource = body_data.get("resource", {}) @@ -161,18 +162,32 @@ async def wechat_callback(request: Request, db: AsyncSession = Depends(get_db)): logger.error("WeChat callback resource not found") raise HTTPException(status_code=400, detail="数据格式错误") + # 验证加密算法(官方文档要求固定为 AEAD_AES_256_GCM) + algorithm = resource.get("algorithm", "") + if algorithm != "AEAD_AES_256_GCM": + logger.error(f"WeChat callback unsupported algorithm: {algorithm}") + raise HTTPException(status_code=400, detail="不支持的加密算法") + ciphertext = resource.get("ciphertext", "") associated_data = resource.get("associated_data", "") nonce_str = resource.get("nonce", "") - # 使用 AES-GCM 解密 + # 参数验证 + if not ciphertext: + logger.error("WeChat callback ciphertext is empty") + raise HTTPException(status_code=400, detail="密文为空") + if not nonce_str: + logger.error("WeChat callback nonce is empty") + raise HTTPException(status_code=400, detail="随机数为空") + + # 使用 AES-GCM 解密(符合官方文档规范) try: # API v3 key 需要转换为字节串 api_v3_key_bytes = api_v3_key.encode('utf-8') # ciphertext 和 nonce 是 Base64 编码的,需要解码 - ciphertext_bytes = b64decode(ciphertext) if ciphertext else b'' - nonce_bytes = b64decode(nonce_str) if nonce_str else b'' + ciphertext_bytes = b64decode(ciphertext) + nonce_bytes = b64decode(nonce_str) # associated_data 是字符串,直接编码 associated_data_bytes = associated_data.encode('utf-8') if associated_data else b'' @@ -180,8 +195,8 @@ async def wechat_callback(request: Request, db: AsyncSession = Depends(get_db)): aesgcm = AESGCM(api_v3_key_bytes) decrypted_str = aesgcm.decrypt(ciphertext_bytes, associated_data_bytes, nonce_bytes) except InvalidTag: - logger.error("WeChat callback decryption failed: Invalid tag") - raise HTTPException(status_code=400, detail="数据解密失败") + logger.error("WeChat callback decryption failed: Invalid tag (key or data mismatch)") + raise HTTPException(status_code=400, detail="数据解密失败(密钥或数据不匹配)") except Exception as e: logger.error(f"WeChat callback decryption failed: {e}") raise HTTPException(status_code=400, detail="数据解密失败") From ca0cfbd9d9995d770c9feb516093985a65965392 Mon Sep 17 00:00:00 2001 From: wwwwwwwww <526125649@qq.com> Date: Mon, 15 Jun 2026 10:28:02 +0800 Subject: [PATCH 12/16] 1 --- video-gen-api/app/api/v1/payments.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/video-gen-api/app/api/v1/payments.py b/video-gen-api/app/api/v1/payments.py index 5c8f3840..09181f53 100644 --- a/video-gen-api/app/api/v1/payments.py +++ b/video-gen-api/app/api/v1/payments.py @@ -185,18 +185,28 @@ async def wechat_callback(request: Request, db: AsyncSession = Depends(get_db)): # API v3 key 需要转换为字节串 api_v3_key_bytes = api_v3_key.encode('utf-8') - # ciphertext 和 nonce 是 Base64 编码的,需要解码 + # ciphertext 是 Base64 编码的,需要解码 ciphertext_bytes = b64decode(ciphertext) - nonce_bytes = b64decode(nonce_str) + + # nonce 是十六进制字符串,需要转换为字节 + nonce_bytes = bytes.fromhex(nonce_str) # associated_data 是字符串,直接编码 associated_data_bytes = associated_data.encode('utf-8') if associated_data else b'' + # 验证 nonce 长度(AES-GCM 要求 8-128 字节) + if len(nonce_bytes) < 8 or len(nonce_bytes) > 128: + logger.error(f"WeChat callback invalid nonce length: {len(nonce_bytes)} bytes") + raise HTTPException(status_code=400, detail="无效的随机数长度") + aesgcm = AESGCM(api_v3_key_bytes) decrypted_str = aesgcm.decrypt(ciphertext_bytes, associated_data_bytes, nonce_bytes) except InvalidTag: logger.error("WeChat callback decryption failed: Invalid tag (key or data mismatch)") raise HTTPException(status_code=400, detail="数据解密失败(密钥或数据不匹配)") + except ValueError as e: + logger.error(f"WeChat callback decryption failed: Invalid hex string or other error: {e}") + raise HTTPException(status_code=400, detail="数据解密失败(格式错误)") except Exception as e: logger.error(f"WeChat callback decryption failed: {e}") raise HTTPException(status_code=400, detail="数据解密失败") From faa3c165dc9823a1ab870e8cccc83b406710e5ce Mon Sep 17 00:00:00 2001 From: wwwwwwwww <526125649@qq.com> Date: Mon, 15 Jun 2026 10:29:57 +0800 Subject: [PATCH 13/16] 1 --- video-gen-api/app/api/v1/payments.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/video-gen-api/app/api/v1/payments.py b/video-gen-api/app/api/v1/payments.py index 09181f53..2036d7d8 100644 --- a/video-gen-api/app/api/v1/payments.py +++ b/video-gen-api/app/api/v1/payments.py @@ -185,11 +185,9 @@ async def wechat_callback(request: Request, db: AsyncSession = Depends(get_db)): # API v3 key 需要转换为字节串 api_v3_key_bytes = api_v3_key.encode('utf-8') - # ciphertext 是 Base64 编码的,需要解码 + # ciphertext 和 nonce 都是 Base64 编码的,需要解码 ciphertext_bytes = b64decode(ciphertext) - - # nonce 是十六进制字符串,需要转换为字节 - nonce_bytes = bytes.fromhex(nonce_str) + nonce_bytes = b64decode(nonce_str) # associated_data 是字符串,直接编码 associated_data_bytes = associated_data.encode('utf-8') if associated_data else b'' @@ -204,9 +202,6 @@ async def wechat_callback(request: Request, db: AsyncSession = Depends(get_db)): except InvalidTag: logger.error("WeChat callback decryption failed: Invalid tag (key or data mismatch)") raise HTTPException(status_code=400, detail="数据解密失败(密钥或数据不匹配)") - except ValueError as e: - logger.error(f"WeChat callback decryption failed: Invalid hex string or other error: {e}") - raise HTTPException(status_code=400, detail="数据解密失败(格式错误)") except Exception as e: logger.error(f"WeChat callback decryption failed: {e}") raise HTTPException(status_code=400, detail="数据解密失败") From 95847e146e6f44c61f62df0570daa3e5c332339b Mon Sep 17 00:00:00 2001 From: wwwwwwwww <526125649@qq.com> Date: Mon, 15 Jun 2026 10:33:31 +0800 Subject: [PATCH 14/16] 1 --- video-gen-api/app/api/v1/payments.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/video-gen-api/app/api/v1/payments.py b/video-gen-api/app/api/v1/payments.py index 2036d7d8..821935bb 100644 --- a/video-gen-api/app/api/v1/payments.py +++ b/video-gen-api/app/api/v1/payments.py @@ -181,24 +181,22 @@ async def wechat_callback(request: Request, db: AsyncSession = Depends(get_db)): raise HTTPException(status_code=400, detail="随机数为空") # 使用 AES-GCM 解密(符合官方文档规范) + # 官方文档:https://pay.weixin.qq.com/doc/v3/merchant/4012071382 try: # API v3 key 需要转换为字节串 api_v3_key_bytes = api_v3_key.encode('utf-8') - # ciphertext 和 nonce 都是 Base64 编码的,需要解码 + # ciphertext 是 Base64 编码的,需要解码 ciphertext_bytes = b64decode(ciphertext) - nonce_bytes = b64decode(nonce_str) + + # nonce 直接使用字符串编码(官方文档方式) + nonce_bytes = nonce_str.encode('utf-8') # associated_data 是字符串,直接编码 associated_data_bytes = associated_data.encode('utf-8') if associated_data else b'' - # 验证 nonce 长度(AES-GCM 要求 8-128 字节) - if len(nonce_bytes) < 8 or len(nonce_bytes) > 128: - logger.error(f"WeChat callback invalid nonce length: {len(nonce_bytes)} bytes") - raise HTTPException(status_code=400, detail="无效的随机数长度") - aesgcm = AESGCM(api_v3_key_bytes) - decrypted_str = aesgcm.decrypt(ciphertext_bytes, associated_data_bytes, nonce_bytes) + decrypted_str = aesgcm.decrypt(nonce_bytes, ciphertext_bytes, associated_data_bytes) except InvalidTag: logger.error("WeChat callback decryption failed: Invalid tag (key or data mismatch)") raise HTTPException(status_code=400, detail="数据解密失败(密钥或数据不匹配)") From 8091778d687bc8b6cd78c82f5d5f9db3b3bf1099 Mon Sep 17 00:00:00 2001 From: wwwwwwwww <526125649@qq.com> Date: Mon, 15 Jun 2026 10:38:54 +0800 Subject: [PATCH 15/16] 1 --- video-gen-api/app/api/v1/payments.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/video-gen-api/app/api/v1/payments.py b/video-gen-api/app/api/v1/payments.py index 821935bb..44e78681 100644 --- a/video-gen-api/app/api/v1/payments.py +++ b/video-gen-api/app/api/v1/payments.py @@ -120,12 +120,15 @@ async def wechat_callback(request: Request, db: AsyncSession = Depends(get_db)): logger.error("WeChat payment config missing for callback") return {"code": "SUCCESS", "message": "OK"} - # 从请求头获取必要信息 - headers = dict(request.headers) - timestamp = headers.get("Wechatpay-Timestamp", "") - nonce = headers.get("Wechatpay-Nonce", "") - signature = headers.get("Wechatpay-Signature", "") - serial_no = headers.get("Wechatpay-Serial", "") + # 从请求头获取必要信息(不区分大小写) + headers = {k.lower(): v for k, v in dict(request.headers).items()} + timestamp = headers.get("wechatpay-timestamp", "") + nonce = headers.get("wechatpay-nonce", "") + signature = headers.get("wechatpay-signature", "") + serial_no = headers.get("wechatpay-serial", "") + + # 记录调试信息 + logger.debug(f"WeChat callback headers - timestamp:{timestamp}, nonce:{nonce[:10] if nonce else ''}, serial_no:{serial_no}") # 验证签名:使用平台公钥验证 if public_key and serial_no: From d8575cee492f215ef3e41d58c80389c93d4e3700 Mon Sep 17 00:00:00 2001 From: wwwwwwwww <526125649@qq.com> Date: Mon, 15 Jun 2026 10:41:09 +0800 Subject: [PATCH 16/16] 1 --- video-gen-api/app/api/v1/payments.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/video-gen-api/app/api/v1/payments.py b/video-gen-api/app/api/v1/payments.py index 44e78681..6fbda84b 100644 --- a/video-gen-api/app/api/v1/payments.py +++ b/video-gen-api/app/api/v1/payments.py @@ -127,8 +127,6 @@ async def wechat_callback(request: Request, db: AsyncSession = Depends(get_db)): signature = headers.get("wechatpay-signature", "") serial_no = headers.get("wechatpay-serial", "") - # 记录调试信息 - logger.debug(f"WeChat callback headers - timestamp:{timestamp}, nonce:{nonce[:10] if nonce else ''}, serial_no:{serial_no}") # 验证签名:使用平台公钥验证 if public_key and serial_no: @@ -145,7 +143,6 @@ async def wechat_callback(request: Request, db: AsyncSession = Depends(get_db)): if not is_verified: logger.warning(f"WeChat callback signature verification failed: serial={serial_no}") raise HTTPException(status_code=400, detail="签名验证失败") - logger.debug(f"WeChat callback signature verified: serial={serial_no}") except Exception as e: logger.warning(f"WeChat signature verification error: {e}, serial={serial_no}") raise HTTPException(status_code=400, detail="签名验证失败")