feat(payment): 添加订单状态同步功能和主动查询接口

- 在 app.py 中集成定时任务,每分钟同步最近30分钟内待支付订单状态
- 定时任务调用支付宝接口更新订单状态及用户积分,记录日志
- payment.py 新增主动查询订单接口,支持用户手动触发订单状态同步
- 添加订单简单查询API,返回订单当前状态信息
- 支付异步通知日志记录优化,改为系统日志记录
- 配置文件中调整支付宝回调地址使用 HTTPS 协议
- alipay_service.py 增加支付宝订单状态查询方法,支持主动轮询订单状态
This commit is contained in:
公司git 2026-01-23 17:40:23 +08:00
parent ccc5a057e3
commit 1196809c6a
4 changed files with 229 additions and 5 deletions

83
app.py
View File

@ -5,12 +5,77 @@ from blueprints.auth import auth_bp
from blueprints.api import api_bp from blueprints.api import api_bp
from blueprints.admin import admin_bp from blueprints.admin import admin_bp
from blueprints.payment import payment_bp from blueprints.payment import payment_bp
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.interval import IntervalTrigger
import threading import threading
import time import time
import logging
# 导入模型(必须在 db.create_all() 之前导入) # 导入模律(必需在 db.create_all() 之前导入)
import models import models
# 定时任务函数
def sync_pending_orders():
"""定时任务: 检查并同步30分钟内的待支付订单"""
from models import Order, User, get_bj_now
from services.alipay_service import AlipayService
from services.logger import system_logger
from datetime import timedelta
try:
# 查询最还30分钟内的待支付订单
thirty_min_ago = get_bj_now() - timedelta(minutes=30)
pending_orders = Order.query.filter(
Order.status == 'PENDING',
Order.created_at >= thirty_min_ago
).all()
if not pending_orders:
return
alipay_service = AlipayService()
updated_count = 0
for order in pending_orders:
try:
# 查询订单状态
alipay_result = alipay_service.query_order_status(order.out_trade_no)
if alipay_result and alipay_result.get('trade_status') in ['TRADE_SUCCESS', 'TRADE_FINISHED']:
# 使用行级锁重新查询,防止并发问题
order_locked = Order.query.filter_by(out_trade_no=order.out_trade_no).with_for_update().first()
# 二次校验状态,防止异步回调已经处理
if order_locked and order_locked.status == 'PENDING':
# 更新订单
order_locked.status = 'PAID'
order_locked.trade_no = alipay_result.get('trade_no')
if not order_locked.paid_at:
order_locked.paid_at = get_bj_now()
# 增加用户积分
user = db.session.get(User, order_locked.user_id)
if user:
user.points += order_locked.points
system_logger.info(f"定时任务-订单支付成功", order_id=order_locked.out_trade_no, points=order_locked.points, user_id=user.id)
updated_count += 1
db.session.commit()
elif order_locked and order_locked.status == 'PAID':
# 订单已经被处理,跳过
logging.info(f"定时任务-订单 {order.out_trade_no} 已被处理,跳过")
except Exception as e:
db.session.rollback()
logging.error(f"定时任务处理订单 {order.out_trade_no} 失败: {str(e)}")
if updated_count > 0:
logging.info(f"定时任务完成,帮助更新了{updated_count}个订单")
except Exception as e:
logging.error(f"定时任务异常: {str(e)}", exc_info=True)
# 导入模律(必需在 db.create_all() 之前导入)
def create_app(): def create_app():
app = Flask(__name__) app = Flask(__name__)
app.config.from_object(Config) app.config.from_object(Config)
@ -110,6 +175,22 @@ def create_app():
db.create_all() db.create_all()
print("✅ 数据库表已就绪") print("✅ 数据库表已就绪")
# 创建并启动定时任务调度器
try:
scheduler = BackgroundScheduler(daemon=True)
# 每分钟检查一次待支付订单
scheduler.add_job(
func=sync_pending_orders,
trigger=IntervalTrigger(minutes=1),
id='sync_pending_orders',
name='同步待支付订单',
replace_existing=True
)
scheduler.start()
print("🚀 定时任务引擎已启动,将每分钟检查一次待支付订单")
except Exception as e:
print(f"⚠️ 定时任务启动失败: {str(e)}")
return app return app
app = create_app() app = create_app()

View File

@ -164,13 +164,110 @@ def api_payment_history():
"paid_at": o.paid_at_bj.strftime('%Y-%m-%d %H:%M:%S') if o.paid_at else None "paid_at": o.paid_at_bj.strftime('%Y-%m-%d %H:%M:%S') if o.paid_at else None
} for o in orders] } for o in orders]
}) })
@payment_bp.route('/api/sync_order', methods=['POST'])
def api_sync_order():
"""主动查询订单状态并同步 - 用于用户手动关闭支付页面后检查是否支付成功"""
if 'user_id' not in session:
return jsonify({'code': 401, 'msg': '请先登录'}), 401
out_trade_no = request.form.get('out_trade_no')
if not out_trade_no:
return jsonify({'code': 400, 'msg': '订单号不能为空'}), 400
try:
# 查询订单
order = Order.query.filter_by(out_trade_no=out_trade_no).first()
if not order:
return jsonify({'code': 404, 'msg': '订单不存在'}), 404
# 只有当前用户才能查询自己的订单
if order.user_id != session['user_id']:
return jsonify({'code': 403, 'msg': '无权限访问此订单'}), 403
# 如果订单已经是PAID或FAILED状态直接返回
if order.status in ['PAID', 'FAILED']:
return jsonify({
'code': 200,
'msg': '订单状态已确定',
'status': order.status,
'paid_at': order.paid_at_bj.strftime('%Y-%m-%d %H:%M:%S') if order.paid_at else None
})
# 向支付宝查询订单状态
alipay_service = AlipayService()
alipay_result = alipay_service.query_order_status(out_trade_no)
if not alipay_result:
return jsonify({'code': 500, 'msg': '查询支付宝订单失败,请稍后重试'}), 500
trade_status = alipay_result.get('trade_status')
# 如果支付宝显示已支付,更新本地订单状态
if trade_status in ['TRADE_SUCCESS', 'TRADE_FINISHED']:
# 使用行级锁重新查询,防止并发问题
order_locked = Order.query.filter_by(out_trade_no=out_trade_no).with_for_update().first()
# 二次校验状态,防止异步回调/定时任务已经处理
if order_locked and order_locked.status == 'PENDING':
order_locked.status = 'PAID'
order_locked.trade_no = alipay_result.get('trade_no')
if not order_locked.paid_at:
order_locked.paid_at = get_bj_now()
# 增加用户积分
user = db.session.get(User, order_locked.user_id)
if user:
user.points += order_locked.points
system_logger.info(f"主动查询-订单支付成功", order_id=out_trade_no, points=order_locked.points, user_id=user.id)
db.session.commit()
return jsonify({
'code': 200,
'msg': '订单已支付,积分已增加',
'status': 'PAID',
'points': order_locked.points,
'paid_at': order_locked.paid_at_bj.strftime('%Y-%m-%d %H:%M:%S')
})
elif order_locked and order_locked.status == 'PAID':
# 订单已经被处理,直接返回
return jsonify({
'code': 200,
'msg': '订单已支付',
'status': 'PAID',
'points': order_locked.points,
'paid_at': order_locked.paid_at_bj.strftime('%Y-%m-%d %H:%M:%S') if order_locked.paid_at else None
})
# 支付宝显示未支付
elif trade_status in ['TRADE_CLOSED', 'WAIT_BUYER_PAY']:
return jsonify({
'code': 200,
'msg': '订单未支付',
'status': 'PENDING',
'trade_status': trade_status
})
else:
return jsonify({
'code': 200,
'msg': f'订单状态: {trade_status}',
'status': order.status,
'trade_status': trade_status
})
except Exception as e:
system_logger.error(f"主动查询订单异常: {str(e)}")
db.session.rollback()
return jsonify({'code': 500, 'msg': f'查询失败: {str(e)}'}), 500
@payment_bp.route('/notify', methods=['POST']) @payment_bp.route('/notify', methods=['POST'])
def payment_notify(): def payment_notify():
"""支付宝异步通知""" """支付宝异步通知"""
try: try:
data = request.form.to_dict() data = request.form.to_dict()
with open("e:\\ai_v\\notify.log", "a") as f: # 记录异步通知到系统日志,而不是本地文件
f.write(f"Received data: {data}\n") system_logger.info(f"支付宝异步通知接收", extra_data=str(data))
signature = data.get("sign") signature = data.get("sign")
@ -209,3 +306,31 @@ def payment_notify():
system_logger.error(f"处理异步通知异常: {str(e)}") system_logger.error(f"处理异步通知异常: {str(e)}")
db.session.rollback() db.session.rollback()
return "fail" return "fail"
@payment_bp.route('/api/query/<out_trade_no>', methods=['GET'])
def api_query_order(out_trade_no):
"""简单查询接口 - 获取订单当前状态而不自动更新"""
if 'user_id' not in session:
return jsonify({'code': 401, 'msg': '请先登录'}), 401
try:
order = Order.query.filter_by(out_trade_no=out_trade_no).first()
if not order:
return jsonify({'code': 404, 'msg': '订单不存在'}), 404
if order.user_id != session['user_id']:
return jsonify({'code': 403, 'msg': '无权限访问'}), 403
return jsonify({
'code': 200,
'out_trade_no': order.out_trade_no,
'status': order.status,
'amount': float(order.amount),
'points': order.points,
'trade_no': order.trade_no,
'created_at': order.created_at_bj.strftime('%Y-%m-%d %H:%M:%S'),
'paid_at': order.paid_at_bj.strftime('%Y-%m-%d %H:%M:%S') if order.paid_at else None
})
except Exception as e:
return jsonify({'code': 500, 'msg': f'查询失败: {str(e)}'}), 500

View File

@ -59,8 +59,8 @@ MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC+BMpGTJMzDoOnjyGh69rDLbV/8rlB
ALIPAY_PUBLIC_KEY = """-----BEGIN PUBLIC KEY----- ALIPAY_PUBLIC_KEY = """-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlDx4KdOtOQE+tBq6jHKKFenRaRe2gbBnleBk++5gki9IQuxVyZUGTJixstf2gELFHWrGanpnwmGggXsqG+Rm5ZLJOlmFM1k0XeAIDvi6tP/rM+ZDFSu1bMBYtT5vzgVZC7mzIvOp9gsT/puqd3aNZmlviLD0R6OYN0zvFX+5qADZV7A9ziA+nXPFSHreBh7yY/q9ophVZNeHGPoYkDVI5++RrF1cALKOdit0giN5vxpe3ch9z3E6+FZg3LiP+1RW3tMiDQfp/SlVs6bNhLUtmlI5r7+mtFCKDUCEpnQ3S9e0II6rzyVXRyKCFs7qi5YzyhhmO3tJJoe9ilEFyNzfRQIDAQAB MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlDx4KdOtOQE+tBq6jHKKFenRaRe2gbBnleBk++5gki9IQuxVyZUGTJixstf2gELFHWrGanpnwmGggXsqG+Rm5ZLJOlmFM1k0XeAIDvi6tP/rM+ZDFSu1bMBYtT5vzgVZC7mzIvOp9gsT/puqd3aNZmlviLD0R6OYN0zvFX+5qADZV7A9ziA+nXPFSHreBh7yY/q9ophVZNeHGPoYkDVI5++RrF1cALKOdit0giN5vxpe3ch9z3E6+FZg3LiP+1RW3tMiDQfp/SlVs6bNhLUtmlI5r7+mtFCKDUCEpnQ3S9e0II6rzyVXRyKCFs7qi5YzyhhmO3tJJoe9ilEFyNzfRQIDAQAB
-----END PUBLIC KEY-----""" # 支付宝公钥 -----END PUBLIC KEY-----""" # 支付宝公钥
ALIPAY_RETURN_URL = "http://860576.xyz/payment/return" # 支付成功跳转地址 ALIPAY_RETURN_URL = "https://860576.xyz/payment/return" # 支付成功跳转地址
ALIPAY_NOTIFY_URL = "http://860576.xyz/payment/notify" # 支付异步通知地址 ALIPAY_NOTIFY_URL = "https://860576.xyz/payment/notify" # 支付异步通知地址
ALIPAY_DEBUG = False # 是否使用沙箱环境 ALIPAY_DEBUG = False # 是否使用沙箱环境
# 开发模式配置 # 开发模式配置

View File

@ -77,3 +77,21 @@ class AlipayService:
except Exception as e: except Exception as e:
logger.error(f"验证签名时发生异常: {str(e)}", exc_info=True) logger.error(f"验证签名时发生异常: {str(e)}", exc_info=True)
return False return False
def query_order_status(self, out_trade_no):
"""查询订单状态 - 主动查询支付宝获取真实支付状态"""
try:
alipay = self.get_alipay_client()
# 调用支付宝订单查询接口
result = alipay.api_alipay_trade_query(out_trade_no=out_trade_no)
if result:
logger.info(f"订单查询成功: {out_trade_no}, 状态: {result.get('trade_status')}")
return result
else:
logger.warning(f"订单查询返回为空: {out_trade_no}")
return None
except Exception as e:
logger.error(f"查询订单状态异常: {str(e)}", exc_info=True)
return None