diff --git a/video-gen-admin/src/pages/AdminPaymentConfig.tsx b/video-gen-admin/src/pages/AdminPaymentConfig.tsx index 22a33915..d8353f8f 100644 --- a/video-gen-admin/src/pages/AdminPaymentConfig.tsx +++ b/video-gen-admin/src/pages/AdminPaymentConfig.tsx @@ -27,6 +27,8 @@ const AdminPaymentConfig: React.FC = () => { wechat_cert_serial_no: map['payment_wechat_cert_serial_no'] || '', wechat_api_v3_key: map['payment_wechat_api_v3_key'] || '', wechat_notify_url: map['payment_wechat_notify_url'] || '', + wechat_public_key: map['payment_wechat_public_key'] || '', + wechat_public_key_id: map['payment_wechat_public_key_id'] || '', alipay_app_id: map['payment_alipay_app_id'] || '', alipay_private_key: map['payment_alipay_private_key'] || '', alipay_public_key: map['payment_alipay_public_key'] || '', @@ -58,6 +60,8 @@ const AdminPaymentConfig: React.FC = () => { payment_wechat_cert_serial_no: values.wechat_cert_serial_no || '', payment_wechat_api_v3_key: values.wechat_api_v3_key || '', payment_wechat_notify_url: values.wechat_notify_url || '', + payment_wechat_public_key: values.wechat_public_key || '', + payment_wechat_public_key_id: values.wechat_public_key_id || '', payment_alipay_enabled: String(alipayEnabled), payment_alipay_app_id: values.alipay_app_id || '', payment_alipay_private_key: values.alipay_private_key || '', @@ -188,6 +192,24 @@ const AdminPaymentConfig: React.FC = () => { + + + + + + @@ -213,11 +235,23 @@ const AdminPaymentConfig: React.FC = () => { - - + + - - + + diff --git a/video-gen-api/app/api/v1/payments.py b/video-gen-api/app/api/v1/payments.py index d85a6236..2a35cd01 100644 --- a/video-gen-api/app/api/v1/payments.py +++ b/video-gen-api/app/api/v1/payments.py @@ -112,8 +112,11 @@ async def wechat_callback(request: Request, db: AsyncSession = Depends(get_db)): 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) + 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"} diff --git a/video-gen-api/app/services/payment.py b/video-gen-api/app/services/payment.py index 2568b0d2..bc87720a 100644 --- a/video-gen-api/app/services/payment.py +++ b/video-gen-api/app/services/payment.py @@ -1,5 +1,6 @@ import logging import os +import json from datetime import datetime, timedelta # 尝试设置 SSL 证书路径 @@ -25,7 +26,7 @@ from app.utils.id_gen import generate_id, generate_order_no import time as _time logger = logging.getLogger("payment") -logger.setLevel(logging.INFO) +logger.setLevel(logging.DEBUG) _log_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "log", "payment") os.makedirs(_log_dir, exist_ok=True) @@ -351,13 +352,32 @@ _wechat_client = None _wechat_mch_id = None +def _parse_wechat_result(result) -> dict: + """解析微信支付SDK返回的result为字典""" + data = {} + if isinstance(result, dict): + data = result + elif isinstance(result, str): + try: + data = json.loads(result) + except: + pass + elif hasattr(result, 'get'): + data = result + elif hasattr(result, '__dict__'): + data = result.__dict__ + return data + + def _get_wechat_client( mch_id: str, private_key: str, cert_serial_no: str, api_v3_key: str, appid: str, - notify_url: str = "" + notify_url: str = "", + public_key: str = None, + public_key_id: str = None ): """Get or create a WeChat Pay client. Recreated if config changes.""" global _wechat_client, _wechat_mch_id @@ -375,30 +395,25 @@ def _get_wechat_client( return None try: - import os - # 创建并确保微信支付证书目录存在 - wechat_cert_dir = os.path.abspath(os.path.join( - os.path.dirname(os.path.dirname(__file__)), - "..", - "storage", - "wechat_certs" - )) - os.makedirs(wechat_cert_dir, exist_ok=True) - logger.info("WeChat Pay platform certificates will be stored in: %s", wechat_cert_dir) - logger.info(f"mch_id={mch_id}, cert_serial_no={cert_serial_no}, appid={appid}, notify_url={notify_url}, api_v3_key={api_v3_key}, private_key={private_key}") - # 初始化微信支付客户端,cert_dir=None,让 SDK 在线获取证书 - _wechat_client = WeChatPay( - wechatpay_type=WeChatPayType.NATIVE, - mchid=mch_id, - private_key=private_key.strip(), - cert_serial_no=cert_serial_no, - appid=appid, - apiv3_key=api_v3_key, - notify_url=notify_url, - cert_dir=wechat_cert_dir, # 不缓存证书,每次都从微信服务器获取 - ) + # 初始化微信支付客户端 + wechatpay_args = { + "wechatpay_type": WeChatPayType.NATIVE, + "mchid": mch_id, + "private_key": private_key.strip(), + "cert_serial_no": cert_serial_no, + "appid": appid, + "apiv3_key": api_v3_key, + "notify_url": notify_url, + "logger": logger, + } + # 如果配置了 public_key 和 public_key_id,就使用它们,否则不设置 + if public_key and public_key_id: + wechatpay_args["public_key"] = public_key + wechatpay_args["public_key_id"] = public_key_id + else: + logger.info("No platform public key configured, will try to download") + _wechat_client = WeChatPay(**wechatpay_args) _wechat_mch_id = mch_id - logger.info("WeChat Pay client initialized successfully") return _wechat_client except Exception as e: logger.exception( @@ -424,17 +439,19 @@ def _create_wechat_order(order: PaymentOrder, db_configs: dict[str, str]) -> str api_v3_key = db_configs.get("payment_wechat_api_v3_key", "") appid = db_configs.get("payment_wechat_appid", "") notify_url = db_configs.get("payment_wechat_notify_url", "") + public_key = db_configs.get("payment_wechat_public_key", "") + public_key_id = db_configs.get("payment_wechat_public_key_id", "") if not all([mch_id, private_key, cert_serial_no, api_v3_key, appid]): logger.warning("WeChat payment config missing in database") return None - client = _get_wechat_client(mch_id, private_key, cert_serial_no, api_v3_key, appid, 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 client is None: return None try: # 调用微信支付 Native 下单接口 - result = client.pay( + code, result = client.pay( description=f"充值订单 {order.order_no}", out_trade_no=order.order_no, amount={ @@ -447,14 +464,16 @@ def _create_wechat_order(order: PaymentOrder, db_configs: dict[str, str]) -> str } ) - if result.get('code_url'): + data = _parse_wechat_result(result) + + if code == 200 and data.get('code_url'): # 注意:微信返回的 code_url 可能需要进一步处理成二维码图片地址 logger.info(f"WeChat order created successfully: order_no={order.order_no}") - return result.get('code_url') + return data.get('code_url') else: logger.error( f"WeChat pay failed: order_no={order.order_no}, " - f"result={result}" + f"code={code}, result={result}" ) return None except Exception as e: @@ -471,8 +490,11 @@ async def _close_wechat_order(db: AsyncSession, order: PaymentOrder, db_configs: 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) + client = _get_wechat_client(mch_id, private_key, cert_serial_no, api_v3_key, appid, notify_url, public_key, public_key_id) if client is None: return False @@ -482,12 +504,12 @@ async def _close_wechat_order(db: AsyncSession, order: PaymentOrder, db_configs: return True try: - result = client.close(out_trade_no=order.order_no) - if result: + code, result = client.close(out_trade_no=order.order_no) + if code == 204: logger.info(f"WeChat order closed: order_no={order.order_no}") return True else: - logger.error(f"WeChat close failed: order_no={order.order_no}") + logger.error(f"WeChat close failed: order_no={order.order_no}, code={code}, result={result}") return False except Exception as e: logger.exception(f"WeChat close exception: order_no={order.order_no}") @@ -503,8 +525,11 @@ async def _query_wechat_order(db: AsyncSession, order: PaymentOrder, db_configs: 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) + client = _get_wechat_client(mch_id, private_key, cert_serial_no, api_v3_key, appid, notify_url, public_key, public_key_id) if client is None: return None @@ -514,17 +539,18 @@ async def _query_wechat_order(db: AsyncSession, order: PaymentOrder, db_configs: return {"trade_state": "SUCCESS"} try: - result = client.query(out_trade_no=order.order_no) - if result.get('trade_state'): + code, result = client.query(out_trade_no=order.order_no) + data = _parse_wechat_result(result) + if code == 200 and data.get('trade_state'): logger.info( f"WeChat query succeeded: order_no={order.order_no}, " - f"trade_state={result.get('trade_state')}" + f"trade_state={data.get('trade_state')}" ) - return result + return data else: logger.error( f"WeChat query failed: order_no={order.order_no}, " - f"result={result}" + f"code={code}, result={result}" ) return None except Exception as e: @@ -545,8 +571,11 @@ async def _refund_wechat_order( 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) + client = _get_wechat_client(mch_id, private_key, cert_serial_no, api_v3_key, appid, notify_url, public_key, public_key_id) if client is None: return {"success": False, "message": "微信支付客户端初始化失败"} @@ -557,7 +586,7 @@ async def _refund_wechat_order( try: out_refund_no = f"{order.order_no}_refund_{int(datetime.now().timestamp())}" - result = client.refund( + code, result = client.refund( out_trade_no=order.order_no, out_refund_no=out_refund_no, amount={ @@ -568,17 +597,19 @@ async def _refund_wechat_order( reason=refund_reason ) - if result.get('status') == 'SUCCESS': + 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": result.get('refund_id')} + return {"success": True, "refund_id": data.get('refund_id')} else: logger.error( f"WeChat refund failed: order_no={order.order_no}, " - f"result={result}" + f"code={code}, result={result}" ) return { "success": False, - "message": f"微信退款失败: {result.get('code', '')}" + "message": f"微信退款失败: code={code}, {data.get('code', '')}" } except Exception as e: logger.exception(f"WeChat refund exception: order_no={order.order_no}") @@ -1105,7 +1136,7 @@ async def process_payment_success_by_order_no( db, order.user_id, order.credits, - f"充值成功({order.credits}积分)", + f"充值成功({order.credits}积分), 订单号: {order_no}, 金额: {order.amount}", related_id=order.id, ) await db.commit() diff --git a/video-gen-app/src/components/Layout/AppLayout.tsx b/video-gen-app/src/components/Layout/AppLayout.tsx index c19e1424..fffe327f 100644 --- a/video-gen-app/src/components/Layout/AppLayout.tsx +++ b/video-gen-app/src/components/Layout/AppLayout.tsx @@ -693,13 +693,15 @@ const AppLayout: React.FC = () => { try { setPaying(true); const order = await createRechargeOrder(plan.id, paymentMethod); - if (order.paymentMethod === 'alipay' && order.qrUrl) { - // Alipay: show the real QR code URL from the backend + // 检查是否有二维码信息,支持支付宝和微信支付 + const qrCode = order.qrUrl || order.codeUrl || order.qr_code || order.code_url; + if ((order.paymentMethod === 'alipay' || order.paymentMethod === 'wechat') && qrCode) { + // Alipay or WeChat Pay: show the QR code const paymentInfo = { price: plan.price, credits: totalCredits, - qrCode: order.qrUrl, - method: 'alipay', + qrCode: qrCode, + method: order.paymentMethod, }; setCurrentPaymentInfo(paymentInfo); setRechargeModalOpen(false); @@ -711,8 +713,8 @@ const AppLayout: React.FC = () => { orderNo: order.orderNo, price: plan.price, credits: totalCredits, - qrCode: order.qrUrl, - method: 'alipay', + qrCode: qrCode, + method: order.paymentMethod, createdAt: order.createdAt || new Date().toISOString(), timeoutSeconds: 180, })); @@ -720,7 +722,7 @@ const AppLayout: React.FC = () => { // Start polling for payment status startPolling(order.orderNo); } else { - // WeChat or mock mode (mock auto-completes, no QR needed) + // Mock mode (auto-completes, no QR needed) message.success('充值成功!积分已到账'); useAuthStore.getState().refreshUser(); setRechargeModalOpen(false);