update pay ui

This commit is contained in:
2025-12-13 10:31:46 +08:00
parent 1949d9b922
commit 5e8c2400a3
4 changed files with 253 additions and 27 deletions

View File

@@ -228,6 +228,78 @@ class AlipayPay:
'error': str(e)
}
def create_wap_pay_url(self, out_trade_no, total_amount, subject, body=None, timeout_express='30m', quit_url=None):
"""
创建手机网站支付URL (H5支付可调起手机支付宝APP)
Args:
out_trade_no: 商户订单号
total_amount: 订单总金额(元,精确到小数点后两位)
subject: 订单标题
body: 订单描述(可选)
timeout_express: 订单超时时间默认30分钟
quit_url: 用户付款中途退出返回商户网站的地址(可选)
Returns:
dict: 包含支付URL的响应
"""
try:
# 金额格式化为两位小数(支付宝要求)
formatted_amount = f"{float(total_amount):.2f}"
# 业务参数
biz_content = {
'out_trade_no': out_trade_no,
'total_amount': formatted_amount,
'subject': subject,
'product_code': 'QUICK_WAP_WAY', # 手机网站支付固定值
'timeout_express': timeout_express,
}
if body:
biz_content['body'] = body
if quit_url:
biz_content['quit_url'] = quit_url
# 公共请求参数
params = {
'app_id': self.app_id,
'method': 'alipay.trade.wap.pay', # 手机网站支付接口
'format': 'json',
'charset': self.charset,
'sign_type': self.sign_type,
'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'version': '1.0',
'notify_url': self.notify_url,
'return_url': self.return_url,
'biz_content': json.dumps(biz_content, ensure_ascii=False),
}
# 生成签名
sign_content = self._get_sign_content(params)
params['sign'] = self._sign(sign_content)
# 构建完整的支付URL
pay_url = f"{self.gateway_url}?{urlencode(params)}"
# 日志输出到 stderr避免影响 subprocess JSON 输出
import sys
print(f"[AlipayPay] WAP Order created: {out_trade_no}, amount: {formatted_amount}", file=sys.stderr)
return {
'success': True,
'pay_url': pay_url,
'order_no': out_trade_no,
'pay_type': 'wap' # 标识为手机网站支付
}
except Exception as e:
import sys
print(f"[AlipayPay] Create WAP order failed: {e}", file=sys.stderr)
return {
'success': False,
'error': str(e)
}
def query_order(self, out_trade_no=None, trade_no=None):
"""
查询订单状态

View File

@@ -6,7 +6,8 @@
用法:
python alipay_pay_worker.py check # 检查配置
python alipay_pay_worker.py create <order_no> <amount> <subject> [body] # 创建订单
python alipay_pay_worker.py create <order_no> <amount> <subject> [body] [pay_type] # 创建订单
pay_type: page=电脑网站支付(默认), wap=手机网站支付
python alipay_pay_worker.py query <order_no> # 查询订单
"""
@@ -31,19 +32,39 @@ def check_config():
}
def create_order(order_no, amount, subject, body=None):
"""创建支付宝订单"""
def create_order(order_no, amount, subject, body=None, pay_type='page'):
"""创建支付宝订单
Args:
order_no: 订单号
amount: 金额
subject: 标题
body: 描述
pay_type: 支付类型 'page'=电脑网站支付, 'wap'=手机网站支付
"""
try:
from alipay_pay import create_alipay_instance
alipay = create_alipay_instance()
result = alipay.create_page_pay_url(
out_trade_no=order_no,
total_amount=str(amount),
subject=subject,
body=body,
timeout_express='30m'
)
if pay_type == 'wap':
# 手机网站支付
result = alipay.create_wap_pay_url(
out_trade_no=order_no,
total_amount=str(amount),
subject=subject,
body=body,
timeout_express='30m',
quit_url='https://valuefrontier.cn/pricing' # 用户取消支付时返回的页面
)
else:
# 电脑网站支付(默认)
result = alipay.create_page_pay_url(
out_trade_no=order_no,
total_amount=str(amount),
subject=subject,
body=body,
timeout_express='30m'
)
return result
except Exception as e:
@@ -108,7 +129,9 @@ def main():
amount = sys.argv[3]
subject = sys.argv[4]
body = sys.argv[5] if len(sys.argv) > 5 else None
result = create_order(order_no, amount, subject, body)
# pay_type: page=电脑网站支付, wap=手机网站支付
pay_type = sys.argv[6] if len(sys.argv) > 6 else 'page'
result = create_order(order_no, amount, subject, body, pay_type)
elif command == 'query':
if len(sys.argv) < 3:

34
app.py
View File

@@ -2687,7 +2687,8 @@ def create_alipay_order():
{
"plan_name": "pro",
"billing_cycle": "yearly",
"promo_code": "WELCOME2025" // 可选
"promo_code": "WELCOME2025", // 可选
"is_mobile": true // 可选,是否为手机端(自动使用 WAP 支付)
}
"""
try:
@@ -2698,6 +2699,8 @@ def create_alipay_order():
plan_name = data.get('plan_name')
billing_cycle = data.get('billing_cycle')
promo_code = (data.get('promo_code') or '').strip() or None
# 前端传入的设备类型,用于决定使用 page 支付还是 wap 支付
is_mobile = data.get('is_mobile', False)
if not plan_name or not billing_cycle:
return jsonify({'success': False, 'error': '参数不完整'}), 400
@@ -2782,8 +2785,12 @@ def create_alipay_order():
# 金额格式化为两位小数(支付宝要求)
amount_str = f"{float(amount):.2f}"
# 根据设备类型选择支付方式wap=手机网站支付page=电脑网站支付
pay_type = 'wap' if is_mobile else 'page'
print(f"[支付宝] 设备类型: {'手机' if is_mobile else '电脑'}, 支付方式: {pay_type}")
create_result = subprocess.run(
[sys.executable, script_path, 'create', order.order_no, amount_str, subject, body],
[sys.executable, script_path, 'create', order.order_no, amount_str, subject, body, pay_type],
capture_output=True, text=True, timeout=60
)
@@ -3104,6 +3111,29 @@ def check_alipay_order_status(order_id):
return jsonify({'success': False, 'error': '查询失败'}), 500
@app.route('/api/payment/alipay/order-by-no/<order_no>/status', methods=['GET'])
def check_alipay_order_status_by_no(order_no):
"""通过订单号查询支付宝订单支付状态(用于手机端支付返回)"""
try:
if 'user_id' not in session:
return jsonify({'success': False, 'error': '未登录'}), 401
# 通过订单号查找订单
order = PaymentOrder.query.filter_by(
order_no=order_no,
user_id=session['user_id']
).first()
if not order:
return jsonify({'success': False, 'error': '订单不存在'}), 404
# 复用现有的状态检查逻辑
return check_alipay_order_status(str(order.id))
except Exception as e:
return jsonify({'success': False, 'error': '查询失败'}), 500
@app.route('/api/auth/session', methods=['GET'])
def get_session_info():
"""获取当前登录用户信息"""

View File

@@ -185,6 +185,79 @@ export default function SubscriptionContentNew() {
fetchSubscriptionPlans();
}, []);
// 检查是否从支付宝支付返回(手机端支付完成后会跳转回来)
useEffect(() => {
const checkAlipayReturn = async () => {
// 检查 URL 参数是否包含支付宝返回标记
const urlParams = new URLSearchParams(window.location.search);
const paymentReturn = urlParams.get('payment_return');
// 支付宝返回的参数是 out_trade_no后端重定向时会转成 order_no
const orderNo = urlParams.get('order_no') || urlParams.get('out_trade_no');
if (paymentReturn === 'alipay' && orderNo) {
// 从支付宝返回,检查支付状态
toast({
title: '正在确认支付结果...',
status: 'info',
duration: 2000,
isClosable: true,
});
try {
// 优先使用 sessionStorage 中的 orderId否则使用 order_no 查询
const orderId = sessionStorage.getItem('alipay_order_id');
const statusUrl = orderId
? `/api/payment/alipay/order/${orderId}/status`
: `/api/payment/alipay/order-by-no/${orderNo}/status`;
const response = await fetch(statusUrl, {
credentials: 'include',
});
if (response.ok) {
const data = await response.json();
if (data.success && (data.data?.status === 'paid' || data.payment_success)) {
toast({
title: '支付成功!',
description: '您的订阅已激活',
status: 'success',
duration: 5000,
isClosable: true,
});
// 清理 sessionStorage
sessionStorage.removeItem('alipay_order_id');
sessionStorage.removeItem('alipay_order_no');
// 清除 URL 参数并刷新页面
window.history.replaceState({}, document.title, window.location.pathname);
setTimeout(() => window.location.reload(), 2000);
} else {
toast({
title: '支付状态待确认',
description: '如已完成支付,请稍候或刷新页面',
status: 'warning',
duration: 5000,
isClosable: true,
});
// 清除 URL 参数
window.history.replaceState({}, document.title, window.location.pathname);
}
} else {
// 清除 URL 参数
window.history.replaceState({}, document.title, window.location.pathname);
}
} catch (error) {
logger.error('SubscriptionContentNew', 'checkAlipayReturn', error);
// 清除 URL 参数
window.history.replaceState({}, document.title, window.location.pathname);
}
}
};
checkAlipayReturn();
}, [toast]);
const fetchSubscriptionPlans = async () => {
try {
logger.debug('SubscriptionContentNew', '正在获取订阅套餐');
@@ -409,6 +482,9 @@ export default function SubscriptionContentNew() {
? '/api/payment/alipay/create-order'
: '/api/payment/create-order';
// 检测是否为移动端设备
const isMobileDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
@@ -419,6 +495,7 @@ export default function SubscriptionContentNew() {
plan_name: selectedPlan.name,
billing_cycle: selectedCycle,
promo_code: promoCodeApplied ? promoCode : null,
is_mobile: isMobileDevice, // 传递设备类型,用于支付宝选择 page/wap 支付
}),
});
@@ -428,22 +505,46 @@ export default function SubscriptionContentNew() {
if (paymentMethod === 'alipay') {
// 支付宝:跳转到支付页面
if (data.data.pay_url) {
setPaymentOrder(data.data);
setPaymentCountdown(30 * 60);
startAutoPaymentCheck(data.data.id, 'alipay');
// 检测是否为移动端设备
const isMobileDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
toast({
title: '订单创建成功',
description: '正在跳转到支付宝支付页面...',
status: 'success',
duration: 3000,
isClosable: true,
});
if (isMobileDevice) {
// 手机端直接在当前页面跳转可调起支付宝APP
toast({
title: '订单创建成功',
description: '正在跳转到支付宝...',
status: 'success',
duration: 2000,
isClosable: true,
});
// 延迟跳转,让用户看到提示
setTimeout(() => {
window.open(data.data.pay_url, '_blank');
}, 500);
// 保存订单信息到 sessionStorage支付完成后返回时可以用来检查状态
sessionStorage.setItem('alipay_order_id', data.data.id);
sessionStorage.setItem('alipay_order_no', data.data.order_no);
// 延迟跳转,让用户看到提示
setTimeout(() => {
window.location.href = data.data.pay_url;
}, 500);
} else {
// PC端新窗口打开
setPaymentOrder(data.data);
setPaymentCountdown(30 * 60);
startAutoPaymentCheck(data.data.id, 'alipay');
toast({
title: '订单创建成功',
description: '正在跳转到支付宝支付页面...',
status: 'success',
duration: 3000,
isClosable: true,
});
// 延迟跳转,让用户看到提示
setTimeout(() => {
window.open(data.data.pay_url, '_blank');
}, 500);
}
} else {
throw new Error('支付链接获取失败');
}