diff --git a/video-gen-api/app/api/v1/payments.py b/video-gen-api/app/api/v1/payments.py index 2a35cd01..6fbda84b 100644 --- a/video-gen-api/app/api/v1/payments.py +++ b/video-gen-api/app/api/v1/payments.py @@ -105,54 +105,118 @@ 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: - 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") - 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", "") - - # 验证签名 - is_verified = client.verify( - timestamp=timestamp, - nonce=nonce, - body=body_str, - signature=signature, - serial_no=serial_no + from wechatpayv3.utils import ( + rsa_verify, load_public_key, sha256, b64decode, + AESGCM, InvalidTag ) - if not is_verified: - logger.warning("WeChat callback signature verification failed") - raise HTTPException(status_code=400, detail="签名验证失败") + mch_id = db_configs.get("payment_wechat_mch_id", "") + api_v3_key = db_configs.get("payment_wechat_api_v3_key", "") + public_key = db_configs.get("payment_wechat_public_key", "") - # 解密回调数据 - decrypted_data = client.decrypt(body_str) - if not decrypted_data: - logger.error("WeChat callback decryption failed") + if not all([mch_id, api_v3_key]): + logger.error("WeChat payment config missing for callback") + return {"code": "SUCCESS", "message": "OK"} + + # 从请求头获取必要信息(不区分大小写) + 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", "") + + + # 验证签名:使用平台公钥验证 + if public_key and serial_no: + try: + # 构造签名串:timestamp + "\n" + nonce + "\n" + body + "\n" + # 符合微信支付官方文档规范:https://pay.weixin.qq.com/doc/v3/merchant/4013053249 + is_verified = rsa_verify( + timestamp=timestamp, + nonce=nonce, + body=body_str, + signature=signature, + public_key=load_public_key(public_key) + ) + if not is_verified: + logger.warning(f"WeChat callback signature verification failed: serial={serial_no}") + raise HTTPException(status_code=400, detail="签名验证失败") + except Exception as e: + logger.warning(f"WeChat signature verification error: {e}, serial={serial_no}") + raise HTTPException(status_code=400, detail="签名验证失败") + else: + 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 + # 官方文档: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", {}) + + if not resource: + 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", "") + + # 参数验证 + 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 解密(符合官方文档规范) + # 官方文档:https://pay.weixin.qq.com/doc/v3/merchant/4012071382 + try: + # API v3 key 需要转换为字节串 + api_v3_key_bytes = api_v3_key.encode('utf-8') + + # ciphertext 是 Base64 编码的,需要解码 + ciphertext_bytes = b64decode(ciphertext) + + # nonce 直接使用字符串编码(官方文档方式) + nonce_bytes = nonce_str.encode('utf-8') + + # associated_data 是字符串,直接编码 + associated_data_bytes = associated_data.encode('utf-8') if associated_data else b'' + + aesgcm = AESGCM(api_v3_key_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="数据解密失败(密钥或数据不匹配)") + 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 returned empty") + raise HTTPException(status_code=400, detail="数据解密失败") + + decrypted_data = json.loads(decrypted_str) + + event_type = body_data.get("event_type", "") + # 处理支付成功回调 - 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 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: @@ -162,6 +226,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 bc87720a..b6c61ab3 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), # 微信支付以分为单位 @@ -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)}"} @@ -656,7 +670,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 = []