Merge branch 'main' of https://gitee.com/wg123/video-gen
This commit is contained in:
@@ -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 = () => {
|
||||
<Form.Item name="wechat_notify_url" label="回调地址" extra="用户支付成功后,微信会主动通知此地址,服务器收到通知后给用户加积分">
|
||||
<Input placeholder="https://yourdomain.com/api/payments/wechat/callback" size="large" disabled={!wechatEnabled} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="wechat_public_key"
|
||||
label="微信平台公钥"
|
||||
extra="(可选)微信支付平台公钥,用于验证回调签名"
|
||||
>
|
||||
<Input.TextArea
|
||||
rows={4}
|
||||
placeholder="微信支付平台公钥内容"
|
||||
disabled={!wechatEnabled}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="wechat_public_key_id"
|
||||
label="微信平台公钥ID"
|
||||
extra="(可选)对应平台公钥的ID"
|
||||
>
|
||||
<Input placeholder="平台公钥ID" size="large" disabled={!wechatEnabled} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
@@ -213,11 +235,23 @@ const AdminPaymentConfig: React.FC = () => {
|
||||
<Form.Item name="alipay_app_id" label="AppID">
|
||||
<Input placeholder="支付宝应用AppID" size="large" disabled={!alipayEnabled} />
|
||||
</Form.Item>
|
||||
<Form.Item name="alipay_private_key" label="应用私钥">
|
||||
<Input.TextArea rows={3} placeholder="支付宝应用私钥 (PKCS8格式)" disabled={!alipayEnabled} />
|
||||
<Form.Item
|
||||
name="alipay_private_key"
|
||||
label="应用私钥"
|
||||
extra="支付宝应用私钥(PKCS8格式),不需要添加头部和尾部标记,直接粘贴私钥内容"
|
||||
>
|
||||
<Input.TextArea rows={4} placeholder="
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC...
|
||||
" disabled={!alipayEnabled} />
|
||||
</Form.Item>
|
||||
<Form.Item name="alipay_public_key" label="支付宝公钥">
|
||||
<Input.TextArea rows={3} placeholder="支付宝公钥" disabled={!alipayEnabled} />
|
||||
<Form.Item
|
||||
name="alipay_public_key"
|
||||
label="支付宝公钥"
|
||||
extra="从支付宝开放平台获取的支付宝公钥,不需要添加头部和尾部标记"
|
||||
>
|
||||
<Input.TextArea rows={4} placeholder="
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
|
||||
" disabled={!alipayEnabled} />
|
||||
</Form.Item>
|
||||
<Form.Item name="alipay_gateway" label="网关地址" extra="正式环境: https://openapi.alipay.com/gateway.do 沙箱环境: https://openapi-sandbox.dl.alipaydev.com/gateway.do">
|
||||
<Input placeholder="https://openapi.alipay.com/gateway.do" size="large" disabled={!alipayEnabled} />
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user