增加支付宝支付相关页面和程序

This commit is contained in:
2026-06-10 11:33:23 +08:00
parent d5a7ba8f14
commit 913dfee9e0
9 changed files with 444 additions and 96 deletions
+1
View File
@@ -22,3 +22,4 @@ video-gen-api/dist/
# 忽略特定类型文件但保留目录 # 忽略特定类型文件但保留目录
# *.pyc # *.pyc
# !dir/*.pycnode_modules/ # !dir/*.pycnode_modules/
*.tmp.*
+7
View File
@@ -21,6 +21,7 @@ video_item/
| PostgreSQL | >= 14 | 推荐 16 | | PostgreSQL | >= 14 | 推荐 16 |
| Redis | >= 6 | 可选,推荐用于限流/验证码/Celery | | Redis | >= 6 | 可选,推荐用于限流/验证码/Celery |
| FFmpeg | 任意 | 可选,用于视频封面截帧 | | FFmpeg | 任意 | 可选,用于视频封面截帧 |
| alipay-sdk-python | >=3.7.1160 | 可选,用于支付 |
--- ---
@@ -46,6 +47,12 @@ pip install -e ".[pg,redis]"
# 如需 Celery 异步任务(ChatAPI 生成流水线) # 如需 Celery 异步任务(ChatAPI 生成流水线)
pip install -e ".[pg,redis,celery]" pip install -e ".[pg,redis,celery]"
#安装阿里支付sdk
pip install -e ".[pg,redis,celery,alipay]"
#安装火山sdk
pip install -e ".[pg,redis,celery,alipay,volc]"
``` ```
### 2. 配置环境变量 ### 2. 配置环境变量
+34 -16
View File
@@ -1,13 +1,22 @@
import logging
from fastapi import APIRouter, Depends, HTTPException, Request from fastapi import APIRouter, Depends, HTTPException, Request
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
logger = logging.getLogger("videogen")
from app.dependencies import get_db, get_current_user from app.dependencies import get_db, get_current_user
from app.models.user import User from app.models.user import User
from app.models.payment_order import PaymentOrder from app.models.payment_order import PaymentOrder
from app.models.recharge_package import RechargePackage from app.models.recharge_package import RechargePackage
from app.schemas.payment import RechargeRequest, PaymentOrderOut from app.schemas.payment import RechargeRequest, PaymentOrderOut
from app.services.payment import create_recharge_order, verify_wechat_callback, verify_alipay_callback, process_payment_success from app.services.payment import (
create_recharge_order,
verify_wechat_callback,
verify_alipay_callback,
process_payment_success_by_order_no,
)
router = APIRouter(prefix="/payments", tags=["payments"]) router = APIRouter(prefix="/payments", tags=["payments"])
@@ -18,6 +27,9 @@ async def recharge(
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
): ):
if req.method not in ("wechat", "alipay"):
raise HTTPException(status_code=400, detail="不支持的支付方式")
result = await db.execute( result = await db.execute(
select(RechargePackage).where( select(RechargePackage).where(
RechargePackage.id == req.plan, RechargePackage.id == req.plan,
@@ -35,6 +47,7 @@ async def recharge(
price=pkg.price, price=pkg.price,
label=pkg.name, label=pkg.name,
bonus_credits=pkg.bonus_credits, bonus_credits=pkg.bonus_credits,
method=req.method,
) )
return order return order
@@ -42,30 +55,35 @@ async def recharge(
@router.post("/wechat/callback") @router.post("/wechat/callback")
async def wechat_callback(request: Request, db: AsyncSession = Depends(get_db)): async def wechat_callback(request: Request, db: AsyncSession = Depends(get_db)):
data = await request.json() data = await request.json()
if not await verify_wechat_callback(data): if not await verify_wechat_callback(data, db):
raise HTTPException(status_code=400, detail="签名验证失败") raise HTTPException(status_code=400, detail="签名验证失败")
order_no = data.get("out_trade_no") order_no = data.get("out_trade_no")
result = await db.execute( if order_no:
select(PaymentOrder).where(PaymentOrder.order_no == order_no).limit(1) await process_payment_success_by_order_no(db, order_no)
)
order = result.scalar_one_or_none()
if order:
await process_payment_success(db, order.id)
return {"code": "SUCCESS", "message": "OK"} return {"code": "SUCCESS", "message": "OK"}
@router.post("/alipay/callback") @router.post("/alipay/callback")
async def alipay_callback(request: Request, db: AsyncSession = Depends(get_db)): async def alipay_callback(request: Request, db: AsyncSession = Depends(get_db)):
data = await request.form() form_data = await request.form()
if not await verify_alipay_callback(dict(data)): data = dict(form_data)
logger.info(f"Alipay callback received: {list(data.keys())}")
# Verify signature first
if not await verify_alipay_callback(data, db):
raise HTTPException(status_code=400, detail="签名验证失败") raise HTTPException(status_code=400, detail="签名验证失败")
# Check trade_status only "TRADE_SUCCESS" and "TRADE_FINISHED" mean paid
trade_status = data.get("trade_status", "")
if trade_status not in ("TRADE_SUCCESS", "TRADE_FINISHED"):
logger.info(f"Alipay callback trade_status={trade_status}, ignoring")
return "success"
order_no = data.get("out_trade_no") order_no = data.get("out_trade_no")
result = await db.execute( trade_no = data.get("trade_no", "")
select(PaymentOrder).where(PaymentOrder.order_no == order_no).limit(1) if order_no:
) await process_payment_success_by_order_no(db, order_no, trade_no)
order = result.scalar_one_or_none()
if order:
await process_payment_success(db, order.id)
return "success" return "success"
+1
View File
@@ -56,6 +56,7 @@ class Settings(BaseSettings):
ALIPAY_APP_ID: str = "" ALIPAY_APP_ID: str = ""
ALIPAY_PRIVATE_KEY: str = "" ALIPAY_PRIVATE_KEY: str = ""
ALIPAY_PUBLIC_KEY: str = "" ALIPAY_PUBLIC_KEY: str = ""
ALIPAY_NOTIFY_URL: str = ""
PAYMENT_MOCK: bool = True PAYMENT_MOCK: bool = True
STORAGE_TYPE: str = "local" STORAGE_TYPE: str = "local"
+2
View File
@@ -3,6 +3,7 @@ from pydantic import BaseModel
class RechargeRequest(BaseModel): class RechargeRequest(BaseModel):
plan: str # package id plan: str # package id
method: str = "wechat" # "wechat" or "alipay"
class PaymentOrderOut(BaseModel): class PaymentOrderOut(BaseModel):
@@ -12,5 +13,6 @@ class PaymentOrderOut(BaseModel):
credits: float credits: float
payment_method: str payment_method: str
status: str status: str
qr_url: str | None = None # Alipay QR code URL (transient, not persisted)
model_config = {"from_attributes": True} model_config = {"from_attributes": True}
+268 -29
View File
@@ -1,16 +1,87 @@
import logging import logging
from datetime import datetime from datetime import datetime
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings from app.config import settings
from app.models.payment_order import PaymentOrder from app.models.payment_order import PaymentOrder
from app.models.system_config import SystemConfig
from app.services.credits import add_credits from app.services.credits import add_credits
from app.utils.id_gen import generate_id, generate_order_no from app.utils.id_gen import generate_id, generate_order_no
logger = logging.getLogger("videogen") logger = logging.getLogger("videogen")
# ---------------------------------------------------------------------------
# Config helpers read from system_configs table (admin panel)
# ---------------------------------------------------------------------------
async def _get_payment_configs(db: AsyncSession) -> dict[str, str]:
"""Read all payment_* configs from the database, return as a dict."""
result = await db.execute(
select(SystemConfig).where(SystemConfig.key.like("payment_%"))
)
return {c.key: c.value for c in result.scalars().all()}
def _is_mock_mode(db_configs: dict[str, str]) -> bool:
"""Check if payment mock mode is enabled (from DB or env)."""
db_val = db_configs.get("payment_mock", "")
if db_val:
return db_val.lower() in ("true", "1", "yes")
return settings.PAYMENT_MOCK
# ---------------------------------------------------------------------------
# Alipay client (lazy singleton, recreated when config changes)
# ---------------------------------------------------------------------------
_alipay_client = None
_alipay_client_app_id = None
def _get_alipay_client(app_id: str, private_key: str, public_key: str):
"""Get or create an Alipay client. Recreated if app_id changes."""
global _alipay_client, _alipay_client_app_id
if _alipay_client is not None and _alipay_client_app_id == app_id:
return _alipay_client
try:
from alipay.aop.api.AlipayClientConfig import AlipayClientConfig
from alipay.aop.api.DefaultAlipayClient import DefaultAlipayClient
except ImportError:
logger.error(
"alipay-sdk-python is not installed. "
"Install it with: pip install alipay-sdk-python"
)
return None
config = AlipayClientConfig()
config.server_url = "https://openapi.alipay.com/gateway.do"
config.app_id = app_id
config.app_private_key = private_key
config.alipay_public_key = public_key
config.sign_type = "RSA2"
config.charset = "utf-8"
try:
_alipay_client = DefaultAlipayClient(config)
_alipay_client_app_id = app_id
except Exception:
logger.exception("Failed to initialize Alipay client")
_alipay_client = None
_alipay_client_app_id = None
return _alipay_client
# ---------------------------------------------------------------------------
# Create recharge order
# ---------------------------------------------------------------------------
async def create_recharge_order( async def create_recharge_order(
db: AsyncSession, db: AsyncSession,
user_id: str, user_id: str,
@@ -20,7 +91,12 @@ async def create_recharge_order(
bonus_credits: float = 0.0, bonus_credits: float = 0.0,
method: str = "wechat", method: str = "wechat",
) -> PaymentOrder: ) -> PaymentOrder:
"""Create a payment order. In mock mode, immediately completes payment.""" """Create a payment order.
Reads payment config from the database (admin panel).
Returns the order; for Alipay the ``qr_url`` attribute will be populated
with the scan-to-pay URL.
"""
total_credits = credits + bonus_credits total_credits = credits + bonus_credits
order = PaymentOrder( order = PaymentOrder(
id=generate_id(), id=generate_id(),
@@ -34,7 +110,11 @@ async def create_recharge_order(
db.add(order) db.add(order)
await db.flush() await db.flush()
if settings.PAYMENT_MOCK: # Read config from database
db_configs = await _get_payment_configs(db)
mock_mode = _is_mock_mode(db_configs)
if mock_mode:
# Mock: immediately complete payment # Mock: immediately complete payment
order.status = "paid" order.status = "paid"
order.paid_at = datetime.now() order.paid_at = datetime.now()
@@ -52,57 +132,184 @@ async def create_recharge_order(
else: else:
# Real payment: delegate to WeChat or Alipay # Real payment: delegate to WeChat or Alipay
if method == "wechat": if method == "wechat":
_create_wechat_order(order) _create_wechat_order(order, db_configs)
elif method == "alipay": elif method == "alipay":
_create_alipay_order(order) qr_url = _create_alipay_order(order, db_configs)
if qr_url:
# Attach QR URL to the order instance (transient, not persisted)
order.qr_url = qr_url # type: ignore[attr-defined]
return order return order
def _create_wechat_order(order: PaymentOrder) -> None: # ---------------------------------------------------------------------------
# WeChat (stub)
# ---------------------------------------------------------------------------
def _create_wechat_order(order: PaymentOrder, db_configs: dict[str, str]) -> None:
"""Create a WeChat Pay order. Stub for real integration.""" """Create a WeChat Pay order. Stub for real integration."""
if not settings.WECHAT_MCH_ID or not settings.WECHAT_API_KEY: mch_id = db_configs.get("payment_wechat_mch_id", "")
logger.warning("WeChat payment config missing (WECHAT_MCH_ID / WECHAT_API_KEY)") api_key = db_configs.get("payment_wechat_api_key", "")
if not mch_id or not api_key:
logger.warning("WeChat payment config missing in database")
return return
logger.info( logger.info(
f"WeChat order created: mch_id={settings.WECHAT_MCH_ID}, " f"WeChat order created: mch_id={mch_id}, "
f"order_no={order.order_no}, amount={order.amount}" f"order_no={order.order_no}, amount={order.amount}"
) )
def _create_alipay_order(order: PaymentOrder) -> None: # ---------------------------------------------------------------------------
"""Create an Alipay order. Stub for real integration.""" # Alipay trade.precreate (当面付 预下单)
if not settings.ALIPAY_APP_ID or not settings.ALIPAY_PRIVATE_KEY: # ---------------------------------------------------------------------------
logger.warning("Alipay payment config missing (ALIPAY_APP_ID / ALIPAY_PRIVATE_KEY)")
return
logger.info(
f"Alipay order created: app_id={settings.ALIPAY_APP_ID}, "
f"order_no={order.order_no}, amount={order.amount}"
)
async def verify_wechat_callback(data: dict) -> bool: def _create_alipay_order(order: PaymentOrder, db_configs: dict[str, str]) -> str | None:
"""Call Alipay ``trade.precreate`` to obtain a QR code URL.
Reads all Alipay config from the database (admin panel).
Returns the ``qr_code`` URL on success, or ``None`` on failure.
"""
app_id = db_configs.get("payment_alipay_app_id", "")
private_key = db_configs.get("payment_alipay_private_key", "")
public_key = db_configs.get("payment_alipay_public_key", "")
notify_url = db_configs.get("payment_alipay_notify_url", "")
if not app_id or not private_key:
logger.warning("Alipay config missing in database (app_id / private_key)")
return None
client = _get_alipay_client(app_id, private_key, public_key)
if client is None:
return None
try:
from alipay.aop.api.domain.AlipayTradePrecreateModel import (
AlipayTradePrecreateModel,
)
from alipay.aop.api.request.AlipayTradePrecreateRequest import (
AlipayTradePrecreateRequest,
)
model = AlipayTradePrecreateModel()
model.out_trade_no = order.order_no
model.total_amount = f"{order.amount:.2f}"
model.subject = f"充值订单 {order.order_no}"
body_parts = []
if order.credits > 0:
body_parts.append(f"{order.credits}积分")
if body_parts:
model.body = " ".join(body_parts)
request = AlipayTradePrecreateRequest()
request.biz_model = model
if notify_url:
request.notify_url = notify_url
response = client.execute(request)
if response.code == "10000":
qr_url = response.qr_code
logger.info(
f"Alipay precreate success: order_no={order.order_no}, "
f"qr_url={qr_url}"
)
return qr_url
else:
logger.error(
f"Alipay precreate failed: code={response.code}, "
f"msg={response.msg}, sub_code={response.sub_code}, "
f"sub_msg={response.sub_msg}, order_no={order.order_no}"
)
return None
except Exception:
logger.exception(f"Alipay precreate exception: order_no={order.order_no}")
return None
# ---------------------------------------------------------------------------
# Alipay callback verification
# ---------------------------------------------------------------------------
async def verify_alipay_callback(data: dict, db: AsyncSession) -> bool:
"""Verify Alipay payment callback (async notify) signature.
Reads the Alipay public key from the database and uses the SDK's
built-in RSA2 verification.
"""
db_configs = await _get_payment_configs(db)
mock_mode = _is_mock_mode(db_configs)
if mock_mode:
return True
public_key = db_configs.get("payment_alipay_public_key", "")
if not public_key:
logger.warning("ALIPAY_PUBLIC_KEY not found in database, cannot verify callback")
return False
try:
sign = data.get("sign")
if not sign:
logger.warning("Alipay callback missing 'sign' field")
return False
# Build verification params (exclude sign and sign_type)
verify_data = {
k: v for k, v in data.items()
if k not in ("sign", "sign_type") and v is not None and v != ""
}
from alipay.aop.api.util.Signature import verify_with_rsa
sign_content = "&".join(
f"{k}={v}" for k, v in sorted(verify_data.items())
)
is_valid = verify_with_rsa(
public_key.encode("utf-8"),
sign_content.encode("utf-8"),
sign,
)
if not is_valid:
logger.warning("Alipay callback signature verification FAILED")
return is_valid
except ImportError:
logger.error("alipay-sdk-python not installed, skipping signature verification")
return True
except Exception:
logger.exception("Alipay callback verification error")
return False
# ---------------------------------------------------------------------------
# WeChat callback verification (stub)
# ---------------------------------------------------------------------------
async def verify_wechat_callback(data: dict, db: AsyncSession) -> bool:
"""Verify WeChat payment callback signature.""" """Verify WeChat payment callback signature."""
if settings.PAYMENT_MOCK: db_configs = await _get_payment_configs(db)
mock_mode = _is_mock_mode(db_configs)
if mock_mode:
return True return True
# Real verification would use WECHAT_API_KEY to verify signature
logger.info("WeChat callback verification (real mode not implemented)") logger.info("WeChat callback verification (real mode not implemented)")
return True return True
async def verify_alipay_callback(data: dict) -> bool: # ---------------------------------------------------------------------------
"""Verify Alipay payment callback signature.""" # Process successful payment
if settings.PAYMENT_MOCK: # ---------------------------------------------------------------------------
return True
# Real verification would use ALIPAY_PUBLIC_KEY to verify signature
logger.info("Alipay callback verification (real mode not implemented)")
return True
async def process_payment_success(db: AsyncSession, order_id: str): async def process_payment_success(db: AsyncSession, order_id: str):
"""Process successful payment: update order and add credits.""" """Process successful payment: update order and add credits."""
from sqlalchemy import select
result = await db.execute( result = await db.execute(
select(PaymentOrder).where(PaymentOrder.id == order_id).limit(1) select(PaymentOrder).where(PaymentOrder.id == order_id).limit(1)
) )
@@ -120,3 +327,35 @@ async def process_payment_success(db: AsyncSession, order_id: str):
related_id=order.id, related_id=order.id,
) )
await db.flush() await db.flush()
async def process_payment_success_by_order_no(db: AsyncSession, order_no: str, trade_no: str = ""):
"""Process successful payment by order_no (used by Alipay/WeChat callbacks).
Args:
db: async database session
order_no: the merchant order number (out_trade_no)
trade_no: the Alipay trade number (trade_no), optional
"""
result = await db.execute(
select(PaymentOrder).where(PaymentOrder.order_no == order_no).limit(1)
)
order = result.scalar_one_or_none()
if not order or order.status != "pending":
logger.info(f"Order {order_no} not found or already processed, skipping")
return
order.status = "paid"
order.paid_at = datetime.now()
if trade_no:
order.trade_no = trade_no
await add_credits(
db,
order.user_id,
order.credits,
f"充值成功({order.credits}积分)",
related_id=order.id,
)
await db.flush()
logger.info(f"Payment success processed: order_no={order_no}, trade_no={trade_no}")
+2
View File
@@ -22,6 +22,8 @@ dependencies = [
pg = ["asyncpg>=0.30.0"] pg = ["asyncpg>=0.30.0"]
redis = ["redis>=5.2.0"] redis = ["redis>=5.2.0"]
celery = ["celery>=5.4.0", "redis>=5.2.0"] celery = ["celery>=5.4.0", "redis>=5.2.0"]
alipay = ["alipay-sdk-python>=3.7.1160"]
volc = ["volcengine-python-sdk>=1.1.0"]
dev = [ dev = [
"pytest>=8.3.0", "pytest>=8.3.0",
"pytest-asyncio>=0.24.0", "pytest-asyncio>=0.24.0",
+8
View File
@@ -319,6 +319,14 @@ export async function getRechargePackages(): Promise<any[]> {
return api.get('/recharge-packages'); return api.get('/recharge-packages');
} }
export async function createRechargeOrder(planId: string, method: string = 'wechat'): Promise<any> {
return api.post('/payments/recharge', { plan: planId, method });
}
export async function getPaymentOrders(): Promise<any[]> {
return api.get('/payments/orders');
}
export async function getCreditRatios(): Promise<any[]> { export async function getCreditRatios(): Promise<any[]> {
return api.get('/credits/ratios'); return api.get('/credits/ratios');
+121 -51
View File
@@ -1,5 +1,5 @@
import React, { useEffect, useState, useMemo } from 'react'; import React, { useEffect, useState, useCallback, useRef } from 'react';
import { Layout, Avatar, Dropdown, Space, Modal, Form, Input, message, Tooltip, Tag, Button, Typography } from 'antd'; import { Layout, Avatar, Dropdown, Space, Modal, Form, Input, message, Tooltip, Tag, Button, Typography, Radio } from 'antd';
import { QRCodeSVG } from 'qrcode.react'; import { QRCodeSVG } from 'qrcode.react';
import { import {
PlayCircleOutlined, PlayCircleOutlined,
@@ -21,12 +21,13 @@ import {
FireFilled, FireFilled,
CrownFilled, CrownFilled,
BankFilled, BankFilled,
QrcodeOutlined,
CloseOutlined, CloseOutlined,
WechatOutlined,
AlipayCircleOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { Outlet, useNavigate, useLocation } from 'react-router-dom'; import { Outlet, useNavigate, useLocation } from 'react-router-dom';
import { useAuthStore } from '../../store/useAuthStore'; import { useAuthStore } from '../../store/useAuthStore';
import { getMenuConfigs, getRechargePackages, getNotifications, markNotificationRead, getSiteInfo } from '../../api'; import { getMenuConfigs, getRechargePackages, createRechargeOrder, getPaymentOrders, getNotifications, markNotificationRead, getSiteInfo } from '../../api';
import NotificationPopup from '../NotificationPopup'; import NotificationPopup from '../NotificationPopup';
interface MenuConfig { interface MenuConfig {
@@ -88,7 +89,10 @@ const AppLayout: React.FC = () => {
const [siteName, setSiteName] = useState('VideoGen.AI'); const [siteName, setSiteName] = useState('VideoGen.AI');
const [siteLogo, setSiteLogo] = useState(''); const [siteLogo, setSiteLogo] = useState('');
const [qrCodeModalOpen, setQrCodeModalOpen] = useState(false); const [qrCodeModalOpen, setQrCodeModalOpen] = useState(false);
const [currentPaymentInfo, setCurrentPaymentInfo] = useState<{ price: number; credits: number; qrCode: string } | null>(null); const [currentPaymentInfo, setCurrentPaymentInfo] = useState<{ price: number; credits: number; qrCode: string; method: string } | null>(null);
const [paymentMethod, setPaymentMethod] = useState<string>('alipay');
const [paying, setPaying] = useState(false);
const pollingTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
// 监听预览弹窗状态,关闭浮动按钮 // 监听预览弹窗状态,关闭浮动按钮
useEffect(() => { useEffect(() => {
@@ -173,6 +177,43 @@ const AppLayout: React.FC = () => {
setRechargeModalOpen(true); setRechargeModalOpen(true);
}; };
const stopPolling = useCallback(() => {
if (pollingTimerRef.current) {
clearInterval(pollingTimerRef.current);
pollingTimerRef.current = null;
}
}, []);
const startPolling = useCallback((orderNo: string) => {
stopPolling();
let attempts = 0;
const maxAttempts = 120; // 2 minutes at 1s interval
const timer = setInterval(async () => {
attempts++;
if (attempts > maxAttempts) {
clearInterval(timer);
pollingTimerRef.current = null;
return;
}
try {
const orders = await getPaymentOrders();
const order = orders.find((o: any) => o.order_no === orderNo);
if (order && order.status === 'paid') {
clearInterval(timer);
pollingTimerRef.current = null;
message.success('支付成功!积分已到账');
useAuthStore.getState().refreshUser();
setQrCodeModalOpen(false);
setCurrentPaymentInfo(null);
setSelectedPlan(null);
}
} catch {
// ignore polling errors
}
}, 1000);
pollingTimerRef.current = timer;
}, [stopPolling]);
return ( return (
<Layout style={{ minHeight: '100vh' }}> <Layout style={{ minHeight: '100vh' }}>
{/* Desktop Sidebar */} {/* Desktop Sidebar */}
@@ -516,28 +557,64 @@ const AppLayout: React.FC = () => {
); );
})} })}
</div> </div>
<div style={{ marginTop: 20, display: 'flex', justifyContent: 'flex-end' }}>
{/* Payment method selection */}
<div style={{ marginTop: 20, marginBottom: 8 }}>
<Typography.Text style={{ color: '#64748b', fontSize: 13, marginBottom: 8, display: 'block' }}></Typography.Text>
<Radio.Group value={paymentMethod} onChange={(e) => setPaymentMethod(e.target.value)}
style={{ display: 'flex', gap: 12 }}>
<Radio.Button value="alipay" style={{
flex: 1, textAlign: 'center', borderRadius: 10, height: 44, lineHeight: '42px',
borderColor: paymentMethod === 'alipay' ? '#1677ff' : undefined,
color: paymentMethod === 'alipay' ? '#1677ff' : undefined,
}}>
<AlipayCircleOutlined style={{ fontSize: 16, marginRight: 6 }} />
</Radio.Button>
<Radio.Button value="wechat" style={{
flex: 1, textAlign: 'center', borderRadius: 10, height: 44, lineHeight: '42px',
borderColor: paymentMethod === 'wechat' ? '#07c160' : undefined,
color: paymentMethod === 'wechat' ? '#07c160' : undefined,
}}>
<WechatOutlined style={{ fontSize: 16, marginRight: 6 }} />
</Radio.Button>
</Radio.Group>
</div>
<div style={{ marginTop: 16, display: 'flex', justifyContent: 'flex-end' }}>
<Button size="large" onClick={() => { setRechargeModalOpen(false); setSelectedPlan(null); }} style={{ borderRadius: 10, marginRight: 12 }}></Button> <Button size="large" onClick={() => { setRechargeModalOpen(false); setSelectedPlan(null); }} style={{ borderRadius: 10, marginRight: 12 }}></Button>
<Button type="primary" size="large" disabled={!selectedPlan} <Button type="primary" size="large" disabled={!selectedPlan} loading={paying}
onClick={() => { onClick={async () => {
const plan = rechargeOptions.find((opt: any) => opt.id === selectedPlan); const plan = rechargeOptions.find((opt: any) => opt.id === selectedPlan);
if (plan) { if (!plan) return;
const totalCredits = (plan.credits || 0) + (plan.bonus_credits || plan.bonusCredits || 0); const totalCredits = (plan.credits || 0) + (plan.bonus_credits || plan.bonusCredits || 0);
// 生成随机支付内容(模拟微信支付订单号) try {
const orderId = `WX${Date.now()}${Math.random().toString(36).substr(2, 6).toUpperCase()}`; setPaying(true);
const paymentContent = JSON.stringify({ const order = await createRechargeOrder(plan.id, paymentMethod);
orderId, if (paymentMethod === 'alipay' && order.qr_url) {
amount: plan.price, // Alipay: show the real QR code URL from the backend
credits: totalCredits, setCurrentPaymentInfo({
timestamp: Date.now() price: plan.price,
}); credits: totalCredits,
setCurrentPaymentInfo({ qrCode: order.qr_url,
price: plan.price, method: 'alipay',
credits: totalCredits, });
qrCode: paymentContent setRechargeModalOpen(false);
}); setQrCodeModalOpen(true);
setRechargeModalOpen(false); // Start polling for payment status
setQrCodeModalOpen(true); startPolling(order.order_no);
} else {
// WeChat or mock mode (mock auto-completes, no QR needed)
message.success('充值成功!积分已到账');
useAuthStore.getState().refreshUser();
setRechargeModalOpen(false);
setSelectedPlan(null);
}
} catch (err: any) {
message.error(err?.message || '创建订单失败,请重试');
} finally {
setPaying(false);
} }
}} }}
style={{ style={{
@@ -554,7 +631,7 @@ const AppLayout: React.FC = () => {
{/* QR Code Payment Modal */} {/* QR Code Payment Modal */}
<Modal <Modal
open={qrCodeModalOpen} open={qrCodeModalOpen}
onCancel={() => { setQrCodeModalOpen(false); setCurrentPaymentInfo(null); }} onCancel={() => { stopPolling(); setQrCodeModalOpen(false); setCurrentPaymentInfo(null); }}
footer={null} footer={null}
width={400} width={400}
closable={false} closable={false}
@@ -567,15 +644,25 @@ const AppLayout: React.FC = () => {
<div style={{ textAlign: 'center', marginBottom: 24 }}> <div style={{ textAlign: 'center', marginBottom: 24 }}>
<div style={{ <div style={{
width: 48, height: 48, width: 48, height: 48,
background: 'linear-gradient(135deg, #6366f1, #8b5cf6)', background: currentPaymentInfo?.method === 'alipay'
? 'linear-gradient(135deg, #1677ff, #0958d9)'
: 'linear-gradient(135deg, #07c160, #06ae56)',
borderRadius: 16, borderRadius: 16,
display: 'flex', alignItems: 'center', justifyContent: 'center', display: 'flex', alignItems: 'center', justifyContent: 'center',
margin: '0 auto 12px', margin: '0 auto 12px',
}}> }}>
<QrcodeOutlined style={{ fontSize: 24, color: '#fff' }} /> {currentPaymentInfo?.method === 'alipay'
? <AlipayCircleOutlined style={{ fontSize: 24, color: '#fff' }} />
: <WechatOutlined style={{ fontSize: 24, color: '#fff' }} />}
</div> </div>
<Typography.Title level={4} style={{ margin: 0 }}></Typography.Title> <Typography.Title level={4} style={{ margin: 0 }}>
<Typography.Text style={{ color: '#94a3b8', fontSize: 13 }}>使</Typography.Text> {currentPaymentInfo?.method === 'alipay' ? '支付宝支付' : '微信支付'}
</Typography.Title>
<Typography.Text style={{ color: '#94a3b8', fontSize: 13 }}>
{currentPaymentInfo?.method === 'alipay'
? '请使用支付宝扫描二维码完成支付'
: '请使用微信扫描二维码完成支付'}
</Typography.Text>
</div> </div>
{/* QR Code */} {/* QR Code */}
@@ -640,32 +727,15 @@ const AppLayout: React.FC = () => {
</div> </div>
{/* Footer Buttons */} {/* Footer Buttons */}
<div style={{ marginTop: 20, display: 'flex', gap: 12 }}> <div style={{ marginTop: 20 }}>
<Button <Button
size="large" size="large"
onClick={() => { setQrCodeModalOpen(false); setCurrentPaymentInfo(null); }} block
style={{ flex: 1, borderRadius: 10 }} onClick={() => { stopPolling(); setQrCodeModalOpen(false); setCurrentPaymentInfo(null); setSelectedPlan(null); }}
style={{ borderRadius: 10 }}
> >
</Button> </Button>
<Button
type="primary"
size="large"
onClick={() => {
message.success('支付成功!积分已到账');
setQrCodeModalOpen(false);
setCurrentPaymentInfo(null);
setSelectedPlan(null);
}}
style={{
flex: 1,
borderRadius: 10,
background: 'linear-gradient(135deg, #6366f1, #8b5cf6)',
border: 'none',
}}
>
</Button>
</div> </div>
</div> </div>
</Modal> </Modal>