From 8a107ee57574e6d64e210eb76720e40bb5b27164 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E5=8F=B8git?= <240241002@qq.com> Date: Fri, 23 Jan 2026 15:00:34 +0800 Subject: [PATCH 1/9] =?UTF-8?q?fix(payment):=20=E5=90=8C=E6=AD=A5=E5=9B=9E?= =?UTF-8?q?=E8=B0=83=E4=B8=AD=E5=A4=84=E7=90=86=E8=AE=A2=E5=8D=95=E7=8A=B6?= =?UTF-8?q?=E6=80=81=EF=BC=8C=E9=98=B2=E6=AD=A2=E5=BC=82=E6=AD=A5=E5=BB=B6?= =?UTF-8?q?=E8=BF=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 同步回调中加锁查询订单,防止并发导致重复发放 - 将订单状态从PENDING更新为PAID,并记录支付交易号和支付时间 - 支付成功时增加用户积分,记录日志 - 异常时回滚事务并记录错误日志 - 异步通知加锁查询订单,确保并发安全 - 更新支付宝支付跳转和通知地址配置 --- blueprints/payment.py | 34 +++++++++++++++++++++++++++++++++- config.py | 4 ++-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/blueprints/payment.py b/blueprints/payment.py index 2bc2910..ef74597 100644 --- a/blueprints/payment.py +++ b/blueprints/payment.py @@ -76,6 +76,37 @@ def payment_return(): out_trade_no = data.get('out_trade_no') if success: + # 同步回调也进行订单处理,防止异步回调延迟或失败 + out_trade_no = data.get('out_trade_no') + trade_no = data.get('trade_no') + + try: + # 查询订单 (加锁防止并发导致双重发放) + order = Order.query.filter_by(out_trade_no=out_trade_no).with_for_update().first() + + # 如果订单存在且状态为PENDING,则更新为PAID + if order and order.status == 'PENDING': + order.status = 'PAID' + order.trade_no = trade_no + order.paid_at = get_bj_now() + + # 增加用户积分 + user = db.session.get(User, order.user_id) + if user: + user.points += order.points + system_logger.info(f"同步回调-订单支付成功", order_id=out_trade_no, points=order.points, user_id=user.id) + + db.session.commit() + elif order: + # 订单已经是完成状态,不做处理 + pass + else: + system_logger.warning(f"同步回调-未找到订单", order_id=out_trade_no) + + except Exception as e: + db.session.rollback() + system_logger.error(f"同步回调-订单状态更新失败: {str(e)}") + return redirect(url_for('auth.buy_page', success='true', out_trade_no=out_trade_no)) else: system_logger.warning(f"支付同步回调验证失败", order_id=out_trade_no) @@ -150,7 +181,8 @@ def payment_notify(): out_trade_no = data.get('out_trade_no') trade_no = data.get('trade_no') - order = Order.query.filter_by(out_trade_no=out_trade_no).first() + # 加锁查询,确保并发安全 + order = Order.query.filter_by(out_trade_no=out_trade_no).with_for_update().first() if order and order.status == 'PENDING': order.status = 'PAID' order.trade_no = trade_no diff --git a/config.py b/config.py index 1421ee6..754f485 100644 --- a/config.py +++ b/config.py @@ -59,8 +59,8 @@ MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC+BMpGTJMzDoOnjyGh69rDLbV/8rlB ALIPAY_PUBLIC_KEY = """-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlDx4KdOtOQE+tBq6jHKKFenRaRe2gbBnleBk++5gki9IQuxVyZUGTJixstf2gELFHWrGanpnwmGggXsqG+Rm5ZLJOlmFM1k0XeAIDvi6tP/rM+ZDFSu1bMBYtT5vzgVZC7mzIvOp9gsT/puqd3aNZmlviLD0R6OYN0zvFX+5qADZV7A9ziA+nXPFSHreBh7yY/q9ophVZNeHGPoYkDVI5++RrF1cALKOdit0giN5vxpe3ch9z3E6+FZg3LiP+1RW3tMiDQfp/SlVs6bNhLUtmlI5r7+mtFCKDUCEpnQ3S9e0II6rzyVXRyKCFs7qi5YzyhhmO3tJJoe9ilEFyNzfRQIDAQAB -----END PUBLIC KEY-----""" # 支付宝公钥 - ALIPAY_RETURN_URL = "http://331002.xyz:2010/payment/return" # 支付成功跳转地址 - ALIPAY_NOTIFY_URL = "http://331002.xyz:2010/payment/notify" # 支付异步通知地址 + ALIPAY_RETURN_URL = "http://860576.xyz:2010/payment/return" # 支付成功跳转地址 + ALIPAY_NOTIFY_URL = "http://860576.xyz:2010/payment/notify" # 支付异步通知地址 ALIPAY_DEBUG = False # 是否使用沙箱环境 # 开发模式配置 From ecccdbc84dcd2ad8c9a1736edd5097312f74f4ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E5=8F=B8git?= <240241002@qq.com> Date: Fri, 23 Jan 2026 15:06:23 +0800 Subject: [PATCH 2/9] =?UTF-8?q?fix(config):=20=E4=BF=AE=E6=AD=A3=E6=94=AF?= =?UTF-8?q?=E4=BB=98=E5=AE=9D=E5=9B=9E=E8=B0=83=E5=9C=B0=E5=9D=80=E5=8E=BB?= =?UTF-8?q?=E9=99=A4=E7=AB=AF=E5=8F=A3=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除支付宝回调地址中的端口号2010 - 修正支付成功跳转地址ALIPAY_RETURN_URL - 修正支付异步通知地址ALIPAY_NOTIFY_URL --- config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config.py b/config.py index 754f485..faace9a 100644 --- a/config.py +++ b/config.py @@ -59,8 +59,8 @@ MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC+BMpGTJMzDoOnjyGh69rDLbV/8rlB ALIPAY_PUBLIC_KEY = """-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlDx4KdOtOQE+tBq6jHKKFenRaRe2gbBnleBk++5gki9IQuxVyZUGTJixstf2gELFHWrGanpnwmGggXsqG+Rm5ZLJOlmFM1k0XeAIDvi6tP/rM+ZDFSu1bMBYtT5vzgVZC7mzIvOp9gsT/puqd3aNZmlviLD0R6OYN0zvFX+5qADZV7A9ziA+nXPFSHreBh7yY/q9ophVZNeHGPoYkDVI5++RrF1cALKOdit0giN5vxpe3ch9z3E6+FZg3LiP+1RW3tMiDQfp/SlVs6bNhLUtmlI5r7+mtFCKDUCEpnQ3S9e0II6rzyVXRyKCFs7qi5YzyhhmO3tJJoe9ilEFyNzfRQIDAQAB -----END PUBLIC KEY-----""" # 支付宝公钥 - ALIPAY_RETURN_URL = "http://860576.xyz:2010/payment/return" # 支付成功跳转地址 - ALIPAY_NOTIFY_URL = "http://860576.xyz:2010/payment/notify" # 支付异步通知地址 + ALIPAY_RETURN_URL = "http://860576.xyz/payment/return" # 支付成功跳转地址 + ALIPAY_NOTIFY_URL = "http://860576.xyz/payment/notify" # 支付异步通知地址 ALIPAY_DEBUG = False # 是否使用沙箱环境 # 开发模式配置 From 30105c685a3b63d8921fc7106370a713b032ae7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E5=8F=B8git?= <240241002@qq.com> Date: Fri, 23 Jan 2026 15:10:16 +0800 Subject: [PATCH 3/9] =?UTF-8?q?fix(payment):=20=E4=BF=AE=E5=A4=8D=E6=94=AF?= =?UTF-8?q?=E4=BB=98=E5=AE=9D=E9=80=9A=E7=9F=A5=E7=AD=BE=E5=90=8D=E9=AA=8C?= =?UTF-8?q?=E8=AF=81=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在支付通知日志中添加接收到的支付宝数据记录 - 移除签名验证参数中的 sign_type,避免验证错误 - 优化支付宝服务签名验证逻辑,提高兼容性和准确度 --- blueprints/payment.py | 1 + services/alipay_service.py | 1 + 2 files changed, 2 insertions(+) diff --git a/blueprints/payment.py b/blueprints/payment.py index ef74597..01d0f75 100644 --- a/blueprints/payment.py +++ b/blueprints/payment.py @@ -169,6 +169,7 @@ def payment_notify(): """支付宝异步通知""" try: data = request.form.to_dict() + system_logger.info(f"Received Alipay Notify: {data}") signature = data.get("sign") if not signature: diff --git a/services/alipay_service.py b/services/alipay_service.py index 1bf5e91..dfe730f 100644 --- a/services/alipay_service.py +++ b/services/alipay_service.py @@ -52,6 +52,7 @@ class AlipayService: # python-alipay-sdk 的 verify 方法会自动处理 sign 的移除 # 但为了保险,我们手动移除它,保留其他所有字段 verify_data.pop('sign', None) + verify_data.pop('sign_type', None) alipay = self.get_alipay_client() # 对于同步回调,sign_type 实际上是参与签名的(某些版本/接口) From ccc5a057e36969233a6983440bce5e573e437132 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E5=8F=B8git?= <240241002@qq.com> Date: Fri, 23 Jan 2026 17:12:36 +0800 Subject: [PATCH 4/9] =?UTF-8?q?fix(payment):=20=E6=94=B9=E8=BF=9B=E6=94=AF?= =?UTF-8?q?=E4=BB=98=E5=AE=9D=E5=BC=82=E6=AD=A5=E9=80=9A=E7=9F=A5=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E8=AE=B0=E5=BD=95=E6=96=B9=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将日志记录由系统日志改为写入本地文件notify.log - 记录接收到的支付宝异步通知数据内容 - 优化日志信息的持久化存储方式 --- blueprints/payment.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/blueprints/payment.py b/blueprints/payment.py index 01d0f75..6a273a7 100644 --- a/blueprints/payment.py +++ b/blueprints/payment.py @@ -169,7 +169,9 @@ def payment_notify(): """支付宝异步通知""" try: data = request.form.to_dict() - system_logger.info(f"Received Alipay Notify: {data}") + with open("e:\\ai_v\\notify.log", "a") as f: + f.write(f"Received data: {data}\n") + signature = data.get("sign") if not signature: From 1196809c6a3e02ea97ee77e0704efb66a97b6bf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E5=8F=B8git?= <240241002@qq.com> Date: Fri, 23 Jan 2026 17:40:23 +0800 Subject: [PATCH 5/9] =?UTF-8?q?feat(payment):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E8=AE=A2=E5=8D=95=E7=8A=B6=E6=80=81=E5=90=8C=E6=AD=A5=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=92=8C=E4=B8=BB=E5=8A=A8=E6=9F=A5=E8=AF=A2=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 app.py 中集成定时任务,每分钟同步最近30分钟内待支付订单状态 - 定时任务调用支付宝接口更新订单状态及用户积分,记录日志 - payment.py 新增主动查询订单接口,支持用户手动触发订单状态同步 - 添加订单简单查询API,返回订单当前状态信息 - 支付异步通知日志记录优化,改为系统日志记录 - 配置文件中调整支付宝回调地址使用 HTTPS 协议 - alipay_service.py 增加支付宝订单状态查询方法,支持主动轮询订单状态 --- app.py | 83 +++++++++++++++++++++++- blueprints/payment.py | 129 ++++++++++++++++++++++++++++++++++++- config.py | 4 +- services/alipay_service.py | 18 ++++++ 4 files changed, 229 insertions(+), 5 deletions(-) diff --git a/app.py b/app.py index ec5bd79..0996925 100644 --- a/app.py +++ b/app.py @@ -5,12 +5,77 @@ from blueprints.auth import auth_bp from blueprints.api import api_bp from blueprints.admin import admin_bp from blueprints.payment import payment_bp +from apscheduler.schedulers.background import BackgroundScheduler +from apscheduler.triggers.interval import IntervalTrigger import threading import time +import logging -# 导入模型(必须在 db.create_all() 之前导入) +# 导入模律(必需在 db.create_all() 之前导入) 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(): app = Flask(__name__) app.config.from_object(Config) @@ -110,6 +175,22 @@ def create_app(): db.create_all() 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 app = create_app() diff --git a/blueprints/payment.py b/blueprints/payment.py index 6a273a7..4de5924 100644 --- a/blueprints/payment.py +++ b/blueprints/payment.py @@ -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 } 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']) def payment_notify(): """支付宝异步通知""" try: 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") @@ -209,3 +306,31 @@ def payment_notify(): system_logger.error(f"处理异步通知异常: {str(e)}") db.session.rollback() return "fail" + +@payment_bp.route('/api/query/', 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 \ No newline at end of file diff --git a/config.py b/config.py index faace9a..f5696c4 100644 --- a/config.py +++ b/config.py @@ -59,8 +59,8 @@ MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC+BMpGTJMzDoOnjyGh69rDLbV/8rlB ALIPAY_PUBLIC_KEY = """-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlDx4KdOtOQE+tBq6jHKKFenRaRe2gbBnleBk++5gki9IQuxVyZUGTJixstf2gELFHWrGanpnwmGggXsqG+Rm5ZLJOlmFM1k0XeAIDvi6tP/rM+ZDFSu1bMBYtT5vzgVZC7mzIvOp9gsT/puqd3aNZmlviLD0R6OYN0zvFX+5qADZV7A9ziA+nXPFSHreBh7yY/q9ophVZNeHGPoYkDVI5++RrF1cALKOdit0giN5vxpe3ch9z3E6+FZg3LiP+1RW3tMiDQfp/SlVs6bNhLUtmlI5r7+mtFCKDUCEpnQ3S9e0II6rzyVXRyKCFs7qi5YzyhhmO3tJJoe9ilEFyNzfRQIDAQAB -----END PUBLIC KEY-----""" # 支付宝公钥 - ALIPAY_RETURN_URL = "http://860576.xyz/payment/return" # 支付成功跳转地址 - ALIPAY_NOTIFY_URL = "http://860576.xyz/payment/notify" # 支付异步通知地址 + ALIPAY_RETURN_URL = "https://860576.xyz/payment/return" # 支付成功跳转地址 + ALIPAY_NOTIFY_URL = "https://860576.xyz/payment/notify" # 支付异步通知地址 ALIPAY_DEBUG = False # 是否使用沙箱环境 # 开发模式配置 diff --git a/services/alipay_service.py b/services/alipay_service.py index dfe730f..9229eab 100644 --- a/services/alipay_service.py +++ b/services/alipay_service.py @@ -77,3 +77,21 @@ class AlipayService: except Exception as e: logger.error(f"验证签名时发生异常: {str(e)}", exc_info=True) 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 From 93d5c503b29c6c421b34d883f644e8740922b5e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E5=8F=B8git?= <240241002@qq.com> Date: Fri, 23 Jan 2026 17:46:22 +0800 Subject: [PATCH 6/9] =?UTF-8?q?fix(app):=20=E4=BF=AE=E5=A4=8D=E5=AE=9A?= =?UTF-8?q?=E6=97=B6=E4=BB=BB=E5=8A=A1=E5=90=8C=E6=AD=A5=E5=BE=85=E6=94=AF?= =?UTF-8?q?=E4=BB=98=E8=AE=A2=E5=8D=95=E7=9A=84=E4=B8=8A=E4=B8=8B=E6=96=87?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 sync_pending_orders 增加 app 参数以使用 Flask 应用上下文 - 在函数内部使用 app.app_context() 包裹数据库操作,避免上下文错误 - 保持原有逻辑查询并更新30分钟内的待支付订单状态 - 增加日志记录和异常捕获,确保任务稳定运行 - 调度器调用时传入 app 作为参数以支持上下文执行 --- app.py | 108 +++++++++++++++++++++++++++++---------------------------- 1 file changed, 55 insertions(+), 53 deletions(-) diff --git a/app.py b/app.py index 0996925..73cd714 100644 --- a/app.py +++ b/app.py @@ -15,64 +15,65 @@ import logging import models # 定时任务函数 -def sync_pending_orders(): +def sync_pending_orders(app): """定时任务: 检查并同步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() + with app.app_context(): + from models import Order, User, get_bj_now + from services.alipay_service import AlipayService + from services.logger import system_logger + from datetime import timedelta - 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() + 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 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() + 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() - # 增加用户积分 - 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 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}个订单") - if updated_count > 0: - logging.info(f"定时任务完成,帮助更新了{updated_count}个订单") - - except Exception as e: - logging.error(f"定时任务异常: {str(e)}", exc_info=True) + except Exception as e: + logging.error(f"定时任务异常: {str(e)}", exc_info=True) # 导入模律(必需在 db.create_all() 之前导入) @@ -181,6 +182,7 @@ def create_app(): # 每分钟检查一次待支付订单 scheduler.add_job( func=sync_pending_orders, + args=[app], trigger=IntervalTrigger(minutes=1), id='sync_pending_orders', name='同步待支付订单', From 05eba467b476e61bb0478f844b761ec7a7dcd912 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E5=8F=B8git?= <240241002@qq.com> Date: Fri, 23 Jan 2026 17:55:33 +0800 Subject: [PATCH 7/9] =?UTF-8?q?feat(payment):=20=E5=9C=A8=E8=AE=A2?= =?UTF-8?q?=E5=8D=95=E5=A4=84=E7=90=86=E6=B5=81=E7=A8=8B=E4=B8=AD=E6=96=B0?= =?UTF-8?q?=E5=A2=9ERedis=E5=88=86=E5=B8=83=E5=BC=8F=E9=94=81=E6=9C=BA?= =?UTF-8?q?=E5=88=B6=E9=98=B2=E6=AD=A2=E5=B9=B6=E5=8F=91=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在定时任务同步订单状态时使用Redis锁,避免并发导致重复处理 - 同步回调接口中添加分布式锁,确保订单状态更新的原子性 - 主动查询订单支付状态接口增加Redis锁,防止重复发放积分 - 异步通知处理逻辑引入锁机制,处理锁定失败时适当重试或记录日志 - 捕获并区分锁定异常,避免错误日志泛滥,提升系统稳定性 - 保留数据库行级锁作为数据库层并发控制的保障措施 --- app.py | 60 ++++++++------- blueprints/payment.py | 173 ++++++++++++++++++++++++------------------ 2 files changed, 134 insertions(+), 99 deletions(-) diff --git a/app.py b/app.py index 73cd714..b4e25ba 100644 --- a/app.py +++ b/app.py @@ -39,35 +39,43 @@ def sync_pending_orders(app): 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() + # 使用防止并发的锁 + lock_key = f"lock:order:{order.out_trade_no}" + # 尝试获取锁,等待3秒。如果获取不到,说明正在处理,本次跳过 + with redis_client.lock(lock_key, timeout=10, blocking_timeout=3): + # 查询订单状态 + alipay_result = alipay_service.query_order_status(order.out_trade_no) - # 二次校验状态,防止异步回调已经处理 - 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() + 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() - # 增加用户积分 - 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} 已被处理,跳过") + # 二次校验状态,防止异步回调已经处理 + 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)}") + # Redis lock error or other errors + if "LockError" in str(e) or "BlockingIOError" in str(e): + logging.info(f"定时任务-订单 {order.out_trade_no} 锁定失败或正在处理中") + else: + db.session.rollback() + logging.error(f"定时任务处理订单 {order.out_trade_no} 失败: {str(e)}") if updated_count > 0: logging.info(f"定时任务完成,帮助更新了{updated_count}个订单") diff --git a/blueprints/payment.py b/blueprints/payment.py index 4de5924..e38a469 100644 --- a/blueprints/payment.py +++ b/blueprints/payment.py @@ -1,5 +1,5 @@ from flask import Blueprint, request, redirect, url_for, session, jsonify, render_template -from extensions import db +from extensions import db, redis_client from models import Order, User, to_bj_time, get_bj_now from services.alipay_service import AlipayService from services.logger import system_logger @@ -81,31 +81,36 @@ def payment_return(): trade_no = data.get('trade_no') try: - # 查询订单 (加锁防止并发导致双重发放) - order = Order.query.filter_by(out_trade_no=out_trade_no).with_for_update().first() - - # 如果订单存在且状态为PENDING,则更新为PAID - if order and order.status == 'PENDING': - order.status = 'PAID' - order.trade_no = trade_no - order.paid_at = get_bj_now() + # 使用分布式锁防止并发 + lock_key = f"lock:order:{out_trade_no}" + with redis_client.lock(lock_key, timeout=10, blocking_timeout=3): + # 查询订单 (加锁防止并发导致双重发放) + order = Order.query.filter_by(out_trade_no=out_trade_no).with_for_update().first() - # 增加用户积分 - user = db.session.get(User, order.user_id) - if user: - user.points += order.points - system_logger.info(f"同步回调-订单支付成功", order_id=out_trade_no, points=order.points, user_id=user.id) - - db.session.commit() - elif order: - # 订单已经是完成状态,不做处理 - pass - else: - system_logger.warning(f"同步回调-未找到订单", order_id=out_trade_no) + # 如果订单存在且状态为PENDING,则更新为PAID + if order and order.status == 'PENDING': + order.status = 'PAID' + order.trade_no = trade_no + order.paid_at = get_bj_now() + + # 增加用户积分 + user = db.session.get(User, order.user_id) + if user: + user.points += order.points + system_logger.info(f"同步回调-订单支付成功", order_id=out_trade_no, points=order.points, user_id=user.id) + + db.session.commit() + elif order: + # 订单已经是完成状态,不做处理 + pass + else: + system_logger.warning(f"同步回调-未找到订单", order_id=out_trade_no) except Exception as e: db.session.rollback() - system_logger.error(f"同步回调-订单状态更新失败: {str(e)}") + # 忽略锁错误,说明可能已经在处理 + if "LockError" not in str(e) and "BlockingIOError" not in str(e): + system_logger.error(f"同步回调-订单状态更新失败: {str(e)}") return redirect(url_for('auth.buy_page', success='true', out_trade_no=out_trade_no)) else: @@ -204,40 +209,50 @@ def api_sync_order(): # 如果支付宝显示已支付,更新本地订单状态 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 - }) + try: + lock_key = f"lock:order:{out_trade_no}" + with redis_client.lock(lock_key, timeout=10, blocking_timeout=3): + # 使用行级锁重新查询,防止并发问题 + 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 + }) + except Exception as e: + if "LockError" in str(e) or "BlockingIOError" in str(e): + # 如果获取锁失败,可能是因为正在处理中,返回成功状态(前端会重试或刷新) + return jsonify({'code': 200, 'msg': '处理中', 'status': 'PAID'}) + else: + db.session.rollback() + raise e # 支付宝显示未支付 elif trade_status in ['TRADE_CLOSED', 'WAIT_BUYER_PAY']: @@ -282,23 +297,35 @@ def payment_notify(): trade_no = data.get('trade_no') # 加锁查询,确保并发安全 - order = Order.query.filter_by(out_trade_no=out_trade_no).with_for_update().first() - if order and order.status == 'PENDING': - order.status = 'PAID' - order.trade_no = trade_no - order.paid_at = get_bj_now() - - user = db.session.get(User, order.user_id) - if user: - user.points += order.points - system_logger.info(f"订单支付成功", order_id=out_trade_no, points=order.points, user_id=user.id) - - db.session.commit() - return "success" - elif order: - return "success" - else: - return "fail" + try: + lock_key = f"lock:order:{out_trade_no}" + with redis_client.lock(lock_key, timeout=10, blocking_timeout=3): + order = Order.query.filter_by(out_trade_no=out_trade_no).with_for_update().first() + if order and order.status == 'PENDING': + order.status = 'PAID' + order.trade_no = trade_no + order.paid_at = get_bj_now() + + user = db.session.get(User, order.user_id) + if user: + user.points += order.points + system_logger.info(f"订单支付成功", order_id=out_trade_no, points=order.points, user_id=user.id) + + db.session.commit() + return "success" + elif order: + return "success" + else: + return "fail" + except Exception as e: + # 如果锁获取失败,暂时返回fail让支付宝重试,或者返回success如果确信正在处理? + # 返回fail让支付宝重试比较稳妥,因为可能正在处理但还没提交 + system_logger.warning(f"处理异步通知锁定失败: {str(e)}") + if "LockError" in str(e) or "BlockingIOError" in str(e): + # 正在处理中,告诉支付宝由于并发我们正在处理,稍后重试(或视为成功?) + # 如果返回fail,支付宝会重试。 + return "fail" + raise e else: return "fail" From a2f357a8f682d4f6f0aa43f15a7b96dabbe5d6a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E5=8F=B8git?= <240241002@qq.com> Date: Fri, 23 Jan 2026 18:01:54 +0800 Subject: [PATCH 8/9] =?UTF-8?q?feat(task):=20=E5=A2=9E=E5=8A=A0=20Redis=20?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E7=BC=93=E5=AD=98=E6=94=AF=E6=8C=81=E5=BC=82?= =?UTF-8?q?=E6=AD=A5=E4=BB=BB=E5=8A=A1=E8=BF=9B=E5=BA=A6=E8=B7=9F=E8=B8=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在图像生成异步任务开始时设置 Redis 状态为队列中,提示任务已提交等待处理 - 在视频生成异步任务开始时设置 Redis 状态为队列中,提示视频任务已提交准备开始导演 - 图片生成处理函数开始时更新 Redis 状态为处理中,并显示绘制进度提示 - 视频生成轮询过程中定期更新 Redis 状态及进度提示,避免任务超时认为失败 - 增加心跳机制,保持任务活跃状态信息,提升用户体验 --- services/generation_service.py | 5 +++++ services/task_service.py | 11 +++++++++++ 2 files changed, 16 insertions(+) diff --git a/services/generation_service.py b/services/generation_service.py index 9a5380f..da5a347 100644 --- a/services/generation_service.py +++ b/services/generation_service.py @@ -1,5 +1,6 @@ from config import Config from models import SystemDict, GenerationRecord, User, db +from extensions import redis_client from services.logger import system_logger from services.task_service import process_image_generation, process_video_generation import requests @@ -124,6 +125,8 @@ def start_async_image_task(app, user_id, payload, api_key, target_api, cost, mod log_msg = "用户发起验光单解读" if payload.get('prompt') == "解读验光单" else "用户发起生图任务" system_logger.info(log_msg, model=model_value, mode=mode) + redis_client.setex(f"task:{task_id}", 3600, json.dumps({"status": "queued", "message": "任务已提交,等待处理..."})) + threading.Thread( target=process_image_generation, args=(app, user_id, task_id, payload, api_key, target_api, cost, use_trial) @@ -151,6 +154,8 @@ def start_async_video_task(app, user_id, payload, cost, model_value): system_logger.info("用户发起视频生成任务 (积分模式)", model=model_value, cost=cost) + redis_client.setex(f"task:{task_id}", 3600, json.dumps({"status": "queued", "message": "视频任务已提交,准备开始导演..."})) + threading.Thread( target=process_video_generation, args=(app, user_id, task_id, payload, api_key, cost, True) # 视频目前默认为积分模式 diff --git a/services/task_service.py b/services/task_service.py index a216a66..0714eb7 100644 --- a/services/task_service.py +++ b/services/task_service.py @@ -93,6 +93,8 @@ def sync_images_background(app, record_id, raw_urls): def process_image_generation(app, user_id, task_id, payload, api_key, target_api, cost, use_trial=False): """异步执行图片生成并存入 Redis""" with app.app_context(): + # 更新状态为处理中 + redis_client.setex(f"task:{task_id}", 3600, json.dumps({"status": "processing", "message": "正如火如荼地绘制中..."})) try: headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"} # 使用较长的超时时间 (10分钟),确保长耗时任务不被中断 @@ -217,9 +219,18 @@ def process_video_generation(app, user_id, internal_task_id, payload, api_key, c raise Exception(f"未获取到远程任务 ID: {submit_result}") # 2. 轮询状态 + redis_client.setex(f"task:{internal_task_id}", 3600, json.dumps({"status": "processing", "message": "视频生成中,请耐心等待..."})) + max_retries = 90 # 提升到 15 分钟 video_url = None for i in range(max_retries): + # 更新进度 (伪进度或保持活跃) + if i % 2 == 0: # 每20秒更新一次心跳,防止被认为是死任务 + redis_client.setex(f"task:{internal_task_id}", 3600, json.dumps({ + "status": "processing", + "message": f"视频生成中 (已耗时 {i * 10} 秒)..." + })) + time.sleep(10) poll_url = Config.VIDEO_POLL_API.format(task_id=remote_task_id) poll_resp = requests.get(poll_url, headers=headers, timeout=30) From a812e18e63846522c3a931e6d456b46d3d96350a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E5=8F=B8git?= <240241002@qq.com> Date: Fri, 23 Jan 2026 18:34:42 +0800 Subject: [PATCH 9/9] =?UTF-8?q?feat(images):=20=E6=B7=BB=E5=8A=A0=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E5=88=86=E8=BE=A8=E7=8E=87=E6=A3=80=E6=B5=8B=E4=B8=8E?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=8E=8B=E7=BC=A9=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 增加图片分辨率检测,提示用户图片超过2K分辨率 - 对大于2K分辨率图片进行自动缩放压缩,确保宽高不超过2048 - 在生成Base64时保持图片原格式,非PNG格式转为JPEG并设定压缩质量为0.9 - 优化上传流程,避免因大图影响性能和加载速度 - 提示文字及状态更新,提升用户体验 --- static/js/main.js | 59 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 55 insertions(+), 4 deletions(-) diff --git a/static/js/main.js b/static/js/main.js index bf6fb24..83d0613 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -373,6 +373,22 @@ function handleNewFiles(files) { const newFiles = Array.from(files).filter(f => f.type.startsWith('image/')); if (newFiles.length === 0) return; + // 检查分辨率并提示压缩 + let largeFound = false; + Promise.all(newFiles.map(file => new Promise(resolve => { + const url = URL.createObjectURL(file); + const img = new Image(); + img.onload = () => { + if (img.width > 2048 || img.height > 2048) largeFound = true; + URL.revokeObjectURL(url); + resolve(); + }; + img.onerror = resolve; + img.src = url; + }))).then(() => { + if (largeFound) showToast('检测到图片分辨率大于 2K,将为您自动压缩至 2K 分辨率', 'info'); + }); + // 如果处于设置器模式,严格限制为 1 张 if (isSetterActive) { if (newFiles.length > 0) { @@ -505,16 +521,51 @@ document.getElementById('submitBtn').onclick = async () => { try { let image_data = []; - // 1. 将图片转换为 Base64 + // 1. 将图片转换为 Base64 (并压缩过大图片) if (uploadedFiles.length > 0) { btnText.innerText = "正在准备图片数据..."; - const readFileAsBase64 = (file) => new Promise((resolve, reject) => { + const processImageFile = (file) => new Promise((resolve, reject) => { const reader = new FileReader(); - reader.onload = () => resolve(reader.result); + reader.onload = (e) => { + const img = new Image(); + img.onload = () => { + const maxDim = 2048; + let w = img.width; + let h = img.height; + + if (w <= maxDim && h <= maxDim) { + resolve(e.target.result); + return; + } + + if (w > h) { + if (w > maxDim) { + h = Math.round(h * (maxDim / w)); + w = maxDim; + } + } else { + if (h > maxDim) { + w = Math.round(w * (maxDim / h)); + h = maxDim; + } + } + + const canvas = document.createElement('canvas'); + canvas.width = w; + canvas.height = h; + const ctx = canvas.getContext('2d'); + ctx.drawImage(img, 0, 0, w, h); + // 保持原格式,如果不是 png 则默认 jpeg (0.9 质量) + const outType = file.type === 'image/png' ? 'image/png' : 'image/jpeg'; + resolve(canvas.toDataURL(outType, 0.9)); + }; + img.onerror = reject; + img.src = e.target.result; + }; reader.onerror = reject; reader.readAsDataURL(file); }); - image_data = await Promise.all(uploadedFiles.map(f => readFileAsBase64(f))); + image_data = await Promise.all(uploadedFiles.map(f => processImageFile(f))); } // 2. 并行启动多个生成任务