update pay ui

This commit is contained in:
2025-12-12 01:14:31 +08:00
parent fb0f449017
commit 93bfecdafc
4 changed files with 253 additions and 25 deletions

Binary file not shown.

122
app.py
View File

@@ -2156,35 +2156,47 @@ def create_payment_order():
db.session.rollback() db.session.rollback()
return jsonify({'success': False, 'error': f'订单创建失败: {str(e)}'}), 500 return jsonify({'success': False, 'error': f'订单创建失败: {str(e)}'}), 500
# 尝试调用真实的微信支付API # 尝试调用真实的微信支付API(使用 subprocess 绕过 eventlet DNS 问题)
try: try:
from wechat_pay import create_wechat_pay_instance, check_wechat_pay_ready import subprocess
import urllib.parse
# 检查微信支付是否就绪 # 使用独立脚本检查配置
is_ready, ready_msg = check_wechat_pay_ready() script_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'wechat_pay_worker.py')
if not is_ready:
# 使用模拟二维码 # 先检查配置
check_result = subprocess.run(
[sys.executable, script_path, 'check'],
capture_output=True, text=True, timeout=10
)
if check_result.returncode != 0:
check_data = json.loads(check_result.stdout) if check_result.stdout else {}
ready_msg = check_data.get('error', check_data.get('message', '未知错误'))
order.qr_code_url = f"https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=wxpay://order/{order.order_no}" order.qr_code_url = f"https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=wxpay://order/{order.order_no}"
order.remark = f"演示模式 - {ready_msg}" order.remark = f"演示模式 - {ready_msg}"
else: else:
wechat_pay = create_wechat_pay_instance()
# 创建微信支付订单 # 创建微信支付订单
plan_display_name = f"{plan_name.upper()}版本-{billing_cycle}" plan_display_name = f"{plan_name.upper()}版本-{billing_cycle}"
wechat_result = wechat_pay.create_native_order( body = f"VFr-{plan_display_name}"
order_no=order.order_no, product_id = f"{plan_name}_{billing_cycle}"
total_fee=float(amount),
body=f"VFr-{plan_display_name}", create_result = subprocess.run(
product_id=f"{plan_name}_{billing_cycle}" [sys.executable, script_path, 'create', order.order_no, str(float(amount)), body, product_id],
capture_output=True, text=True, timeout=60
) )
if wechat_result['success']: print(f"[微信支付] 创建订单返回: {create_result.stdout}")
if create_result.stderr:
print(f"[微信支付] 错误输出: {create_result.stderr}")
wechat_result = json.loads(create_result.stdout) if create_result.stdout else {'success': False, 'error': '无返回'}
if wechat_result.get('success'):
# 获取微信返回的原始code_url # 获取微信返回的原始code_url
wechat_code_url = wechat_result['code_url'] wechat_code_url = wechat_result['code_url']
# 将微信协议URL转换为二维码图片URL # 将微信协议URL转换为二维码图片URL
import urllib.parse
encoded_url = urllib.parse.quote(wechat_code_url, safe='') encoded_url = urllib.parse.quote(wechat_code_url, safe='')
qr_image_url = f"https://api.qrserver.com/v1/create-qr-code/?size=200x200&data={encoded_url}" qr_image_url = f"https://api.qrserver.com/v1/create-qr-code/?size=200x200&data={encoded_url}"
@@ -2196,10 +2208,16 @@ def create_payment_order():
order.qr_code_url = f"https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=wxpay://order/{order.order_no}" order.qr_code_url = f"https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=wxpay://order/{order.order_no}"
order.remark = f"微信支付失败: {wechat_result.get('error')}" order.remark = f"微信支付失败: {wechat_result.get('error')}"
except ImportError as e: except subprocess.TimeoutExpired:
order.qr_code_url = f"https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=wxpay://order/{order.order_no}" order.qr_code_url = f"https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=wxpay://order/{order.order_no}"
order.remark = "微信支付模块未配置" order.remark = "微信支付超时"
except json.JSONDecodeError as e:
order.qr_code_url = f"https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=wxpay://order/{order.order_no}"
order.remark = f"微信支付返回解析失败: {str(e)}"
except Exception as e: except Exception as e:
import traceback
print(f"[微信支付] Exception: {e}")
traceback.print_exc()
order.qr_code_url = f"https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=wxpay://order/{order.order_no}" order.qr_code_url = f"https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=wxpay://order/{order.order_no}"
order.remark = f"支付异常: {str(e)}" order.remark = f"支付异常: {str(e)}"
@@ -10455,12 +10473,39 @@ def broadcast_new_event(event):
# Redis Key 用于多 Worker 协调 # Redis Key 用于多 Worker 协调
REDIS_KEY_LAST_MAX_EVENT_ID = 'vf:event_polling:last_max_id' REDIS_KEY_LAST_MAX_EVENT_ID = 'vf:event_polling:last_max_id'
REDIS_KEY_POLLING_LOCK = 'vf:event_polling:lock' REDIS_KEY_POLLING_LOCK = 'vf:event_polling:lock'
REDIS_KEY_PENDING_EVENTS = 'vf:event_polling:pending_events' # 待推送事件集合(没有 related_stocks 的事件)
# 本地缓存(减少 Redis 查询) # 本地缓存(减少 Redis 查询)
_local_last_max_event_id = 0 _local_last_max_event_id = 0
_polling_initialized = False _polling_initialized = False
def _add_pending_event(event_id):
"""将事件添加到待推送列表"""
try:
redis_client.sadd(REDIS_KEY_PENDING_EVENTS, str(event_id))
except Exception as e:
print(f'[轮询 WARN] 添加待推送事件失败: {e}')
def _remove_pending_event(event_id):
"""从待推送列表移除事件"""
try:
redis_client.srem(REDIS_KEY_PENDING_EVENTS, str(event_id))
except Exception as e:
print(f'[轮询 WARN] 移除待推送事件失败: {e}')
def _get_pending_events():
"""获取所有待推送事件ID"""
try:
pending = redis_client.smembers(REDIS_KEY_PENDING_EVENTS)
return [int(eid) for eid in pending] if pending else []
except Exception as e:
print(f'[轮询 WARN] 获取待推送事件失败: {e}')
return []
def _get_last_max_event_id(): def _get_last_max_event_id():
"""从 Redis 获取最大事件 ID""" """从 Redis 获取最大事件 ID"""
try: try:
@@ -10491,6 +10536,11 @@ def poll_new_events():
1. 使用 Redis 分布式锁,确保同一时刻只有一个 Worker 执行轮询 1. 使用 Redis 分布式锁,确保同一时刻只有一个 Worker 执行轮询
2. 使用 Redis 存储 last_max_event_id所有 Worker 共享状态 2. 使用 Redis 存储 last_max_event_id所有 Worker 共享状态
3. 通过 Redis 消息队列广播到所有 Worker 的客户端 3. 通过 Redis 消息队列广播到所有 Worker 的客户端
待推送事件机制:
- 当事件首次被检测到但没有 related_stocks 时,加入待推送列表
- 每次轮询时检查待推送列表中的事件是否已有 related_stocks
- 有则推送并从列表移除超过24小时的事件自动清理
""" """
import os import os
@@ -10528,6 +10578,36 @@ def poll_new_events():
print(f'[轮询] 数据库查询: 找到 {len(events_in_24h)} 个近24小时内的事件') print(f'[轮询] 数据库查询: 找到 {len(events_in_24h)} 个近24小时内的事件')
# 创建事件ID到事件对象的映射
events_map = {event.id: event for event in events_in_24h}
# === 步骤1: 检查待推送列表中的事件 ===
pending_event_ids = _get_pending_events()
print(f'[轮询] 待推送列表: {len(pending_event_ids)} 个事件')
pushed_from_pending = 0
for pending_id in pending_event_ids:
if pending_id in events_map:
event = events_map[pending_id]
related_stocks_count = event.related_stocks.count()
if related_stocks_count > 0:
# 事件现在有 related_stocks 了,推送它
broadcast_new_event(event)
_remove_pending_event(pending_id)
pushed_from_pending += 1
print(f'[轮询] ✓ 待推送事件 ID={pending_id} 现在有 {related_stocks_count} 个关联股票,已推送')
else:
print(f'[轮询] - 待推送事件 ID={pending_id} 仍无关联股票,继续等待')
else:
# 事件已超过24小时或已删除从待推送列表移除
_remove_pending_event(pending_id)
print(f'[轮询] × 待推送事件 ID={pending_id} 已过期或不存在,已移除')
if pushed_from_pending > 0:
print(f'[轮询] 从待推送列表推送了 {pushed_from_pending} 个事件')
# === 步骤2: 检查新事件 ===
# 找出新插入的事件ID > last_max_event_id # 找出新插入的事件ID > last_max_event_id
new_events = [ new_events = [
event for event in events_in_24h event for event in events_in_24h
@@ -10540,6 +10620,7 @@ def poll_new_events():
print(f'[轮询] 发现 {len(new_events)} 个新事件') print(f'[轮询] 发现 {len(new_events)} 个新事件')
pushed_count = 0 pushed_count = 0
pending_count = 0
for event in new_events: for event in new_events:
# 检查事件是否有关联股票(只推送有关联股票的事件) # 检查事件是否有关联股票(只推送有关联股票的事件)
related_stocks_count = event.related_stocks.count() related_stocks_count = event.related_stocks.count()
@@ -10552,9 +10633,12 @@ def poll_new_events():
pushed_count += 1 pushed_count += 1
print(f'[轮询] ✓ 已推送事件 ID={event.id}') print(f'[轮询] ✓ 已推送事件 ID={event.id}')
else: else:
print(f'[轮询] - 跳过(暂无关联股票)') # 没有关联股票,加入待推送列表
_add_pending_event(event.id)
pending_count += 1
print(f'[轮询] → 加入待推送列表(暂无关联股票)')
print(f'[轮询] 本轮推送 {pushed_count}/{len(new_events)}事件') print(f'[轮询] 本轮: 推送 {pushed_count} 个, 加入待推送 {pending_count}')
# 更新最大事件ID # 更新最大事件ID
new_max_id = max(event.id for event in events_in_24h) new_max_id = max(event.id for event in events_in_24h)

View File

@@ -4,6 +4,10 @@
微信支付真实配置文件 微信支付真实配置文件
请根据您的微信商户平台信息填写 请根据您的微信商户平台信息填写
""" """
import os
# 获取当前文件所在目录(确保无论从哪里启动都能找到证书)
_BASE_DIR = os.path.dirname(os.path.abspath(__file__))
# 微信支付配置 - 请替换为您的真实信息 # 微信支付配置 - 请替换为您的真实信息
WECHAT_PAY_CONFIG = { WECHAT_PAY_CONFIG = {
@@ -12,9 +16,9 @@ WECHAT_PAY_CONFIG = {
'mch_id': '1718543833', # 微信支付商户号 'mch_id': '1718543833', # 微信支付商户号
'api_key': '141a5753c42526bb44bc44d6c4277664', # 微信商户平台设置的API密钥 'api_key': '141a5753c42526bb44bc44d6c4277664', # 微信商户平台设置的API密钥
# 证书文件路径 # 证书文件路径(使用绝对路径,兼容 gunicorn 多进程启动)
'cert_path': './certs/apiclient_cert.pem', 'cert_path': os.path.join(_BASE_DIR, 'certs', 'apiclient_cert.pem'),
'key_path': './certs/apiclient_key.pem', 'key_path': os.path.join(_BASE_DIR, 'certs', 'apiclient_key.pem'),
# 回调配置 # 回调配置
'notify_url': 'https://valuefrontier.cn/api/payment/wechat/callback', 'notify_url': 'https://valuefrontier.cn/api/payment/wechat/callback',
@@ -38,8 +42,7 @@ def validate_config():
if WECHAT_PAY_CONFIG['api_key'].startswith('你的'): if WECHAT_PAY_CONFIG['api_key'].startswith('你的'):
issues.append("❌ api_key 还是示例值请替换为真实的32位API密钥") issues.append("❌ api_key 还是示例值请替换为真实的32位API密钥")
# 检查证书文件 # 检查证书文件(路径已是绝对路径)
import os
for key in ['cert_path', 'key_path']: for key in ['cert_path', 'key_path']:
if not os.path.exists(WECHAT_PAY_CONFIG[key]): if not os.path.exists(WECHAT_PAY_CONFIG[key]):
issues.append(f"❌ 证书文件不存在: {WECHAT_PAY_CONFIG[key]}") issues.append(f"❌ 证书文件不存在: {WECHAT_PAY_CONFIG[key]}")

141
wechat_pay_worker.py Normal file
View File

@@ -0,0 +1,141 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
独立的微信支付脚本(绕过 eventlet DNS 问题)
使用方式:
# 创建订单
python wechat_pay_worker.py create <order_no> <total_fee> <body> [product_id]
# 查询订单
python wechat_pay_worker.py query <order_no>
# 检查配置
python wechat_pay_worker.py check
返回值:
成功返回 0失败返回 1
输出 JSON 格式的结果
"""
import sys
import json
import os
# 添加当前目录到路径
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
def create_order(order_no, total_fee, body, product_id=None):
"""创建微信支付订单"""
try:
from wechat_pay import create_wechat_pay_instance
wechat_pay = create_wechat_pay_instance()
result = wechat_pay.create_native_order(
order_no=order_no,
total_fee=float(total_fee),
body=body,
product_id=product_id
)
print(json.dumps(result, ensure_ascii=False))
return result.get('success', False)
except Exception as e:
print(json.dumps({
'success': False,
'error': f'{type(e).__name__}: {str(e)}'
}, ensure_ascii=False))
return False
def query_order(order_no):
"""查询订单状态"""
try:
from wechat_pay import create_wechat_pay_instance
wechat_pay = create_wechat_pay_instance()
result = wechat_pay.query_order(order_no=order_no)
print(json.dumps(result, ensure_ascii=False))
return result.get('success', False)
except Exception as e:
print(json.dumps({
'success': False,
'error': f'{type(e).__name__}: {str(e)}'
}, ensure_ascii=False))
return False
def check_config():
"""检查微信支付配置"""
try:
from wechat_pay import check_wechat_pay_ready
is_ready, msg = check_wechat_pay_ready()
result = {
'success': is_ready,
'message': msg
}
print(json.dumps(result, ensure_ascii=False))
return is_ready
except Exception as e:
print(json.dumps({
'success': False,
'error': f'{type(e).__name__}: {str(e)}'
}, ensure_ascii=False))
return False
if __name__ == "__main__":
if len(sys.argv) < 2:
print(json.dumps({
'success': False,
'error': 'Usage: python wechat_pay_worker.py <command> [args...]'
}))
sys.exit(1)
command = sys.argv[1]
if command == 'create':
if len(sys.argv) < 5:
print(json.dumps({
'success': False,
'error': 'Usage: python wechat_pay_worker.py create <order_no> <total_fee> <body> [product_id]'
}))
sys.exit(1)
order_no = sys.argv[2]
total_fee = sys.argv[3]
body = sys.argv[4]
product_id = sys.argv[5] if len(sys.argv) > 5 else None
success = create_order(order_no, total_fee, body, product_id)
sys.exit(0 if success else 1)
elif command == 'query':
if len(sys.argv) < 3:
print(json.dumps({
'success': False,
'error': 'Usage: python wechat_pay_worker.py query <order_no>'
}))
sys.exit(1)
order_no = sys.argv[2]
success = query_order(order_no)
sys.exit(0 if success else 1)
elif command == 'check':
success = check_config()
sys.exit(0 if success else 1)
else:
print(json.dumps({
'success': False,
'error': f'Unknown command: {command}'
}))
sys.exit(1)