update pay ui
This commit is contained in:
@@ -228,6 +228,78 @@ class AlipayPay:
|
|||||||
'error': str(e)
|
'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):
|
def query_order(self, out_trade_no=None, trade_no=None):
|
||||||
"""
|
"""
|
||||||
查询订单状态
|
查询订单状态
|
||||||
|
|||||||
@@ -6,7 +6,8 @@
|
|||||||
|
|
||||||
用法:
|
用法:
|
||||||
python alipay_pay_worker.py check # 检查配置
|
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> # 查询订单
|
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:
|
try:
|
||||||
from alipay_pay import create_alipay_instance
|
from alipay_pay import create_alipay_instance
|
||||||
alipay = create_alipay_instance()
|
alipay = create_alipay_instance()
|
||||||
|
|
||||||
result = alipay.create_page_pay_url(
|
if pay_type == 'wap':
|
||||||
out_trade_no=order_no,
|
# 手机网站支付
|
||||||
total_amount=str(amount),
|
result = alipay.create_wap_pay_url(
|
||||||
subject=subject,
|
out_trade_no=order_no,
|
||||||
body=body,
|
total_amount=str(amount),
|
||||||
timeout_express='30m'
|
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
|
return result
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -108,7 +129,9 @@ def main():
|
|||||||
amount = sys.argv[3]
|
amount = sys.argv[3]
|
||||||
subject = sys.argv[4]
|
subject = sys.argv[4]
|
||||||
body = sys.argv[5] if len(sys.argv) > 5 else None
|
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':
|
elif command == 'query':
|
||||||
if len(sys.argv) < 3:
|
if len(sys.argv) < 3:
|
||||||
|
|||||||
34
app.py
34
app.py
@@ -2687,7 +2687,8 @@ def create_alipay_order():
|
|||||||
{
|
{
|
||||||
"plan_name": "pro",
|
"plan_name": "pro",
|
||||||
"billing_cycle": "yearly",
|
"billing_cycle": "yearly",
|
||||||
"promo_code": "WELCOME2025" // 可选
|
"promo_code": "WELCOME2025", // 可选
|
||||||
|
"is_mobile": true // 可选,是否为手机端(自动使用 WAP 支付)
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
@@ -2698,6 +2699,8 @@ def create_alipay_order():
|
|||||||
plan_name = data.get('plan_name')
|
plan_name = data.get('plan_name')
|
||||||
billing_cycle = data.get('billing_cycle')
|
billing_cycle = data.get('billing_cycle')
|
||||||
promo_code = (data.get('promo_code') or '').strip() or None
|
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:
|
if not plan_name or not billing_cycle:
|
||||||
return jsonify({'success': False, 'error': '参数不完整'}), 400
|
return jsonify({'success': False, 'error': '参数不完整'}), 400
|
||||||
@@ -2782,8 +2785,12 @@ def create_alipay_order():
|
|||||||
# 金额格式化为两位小数(支付宝要求)
|
# 金额格式化为两位小数(支付宝要求)
|
||||||
amount_str = f"{float(amount):.2f}"
|
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(
|
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
|
capture_output=True, text=True, timeout=60
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -3104,6 +3111,29 @@ def check_alipay_order_status(order_id):
|
|||||||
return jsonify({'success': False, 'error': '查询失败'}), 500
|
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'])
|
@app.route('/api/auth/session', methods=['GET'])
|
||||||
def get_session_info():
|
def get_session_info():
|
||||||
"""获取当前登录用户信息"""
|
"""获取当前登录用户信息"""
|
||||||
|
|||||||
@@ -185,6 +185,79 @@ export default function SubscriptionContentNew() {
|
|||||||
fetchSubscriptionPlans();
|
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 () => {
|
const fetchSubscriptionPlans = async () => {
|
||||||
try {
|
try {
|
||||||
logger.debug('SubscriptionContentNew', '正在获取订阅套餐');
|
logger.debug('SubscriptionContentNew', '正在获取订阅套餐');
|
||||||
@@ -409,6 +482,9 @@ export default function SubscriptionContentNew() {
|
|||||||
? '/api/payment/alipay/create-order'
|
? '/api/payment/alipay/create-order'
|
||||||
: '/api/payment/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, {
|
const response = await fetch(apiUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -419,6 +495,7 @@ export default function SubscriptionContentNew() {
|
|||||||
plan_name: selectedPlan.name,
|
plan_name: selectedPlan.name,
|
||||||
billing_cycle: selectedCycle,
|
billing_cycle: selectedCycle,
|
||||||
promo_code: promoCodeApplied ? promoCode : null,
|
promo_code: promoCodeApplied ? promoCode : null,
|
||||||
|
is_mobile: isMobileDevice, // 传递设备类型,用于支付宝选择 page/wap 支付
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -428,22 +505,46 @@ export default function SubscriptionContentNew() {
|
|||||||
if (paymentMethod === 'alipay') {
|
if (paymentMethod === 'alipay') {
|
||||||
// 支付宝:跳转到支付页面
|
// 支付宝:跳转到支付页面
|
||||||
if (data.data.pay_url) {
|
if (data.data.pay_url) {
|
||||||
setPaymentOrder(data.data);
|
// 检测是否为移动端设备
|
||||||
setPaymentCountdown(30 * 60);
|
const isMobileDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||||
startAutoPaymentCheck(data.data.id, 'alipay');
|
|
||||||
|
|
||||||
toast({
|
if (isMobileDevice) {
|
||||||
title: '订单创建成功',
|
// 手机端:直接在当前页面跳转(可调起支付宝APP)
|
||||||
description: '正在跳转到支付宝支付页面...',
|
toast({
|
||||||
status: 'success',
|
title: '订单创建成功',
|
||||||
duration: 3000,
|
description: '正在跳转到支付宝...',
|
||||||
isClosable: true,
|
status: 'success',
|
||||||
});
|
duration: 2000,
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
|
|
||||||
// 延迟跳转,让用户看到提示
|
// 保存订单信息到 sessionStorage,支付完成后返回时可以用来检查状态
|
||||||
setTimeout(() => {
|
sessionStorage.setItem('alipay_order_id', data.data.id);
|
||||||
window.open(data.data.pay_url, '_blank');
|
sessionStorage.setItem('alipay_order_no', data.data.order_no);
|
||||||
}, 500);
|
|
||||||
|
// 延迟跳转,让用户看到提示
|
||||||
|
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 {
|
} else {
|
||||||
throw new Error('支付链接获取失败');
|
throw new Error('支付链接获取失败');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user