2026-01-21 19:32:02 +08:00
|
|
|
|
from flask import Flask, render_template, jsonify, Response, stream_with_context
|
2026-01-12 00:53:31 +08:00
|
|
|
|
from config import Config
|
2026-01-21 19:32:02 +08:00
|
|
|
|
from extensions import db, redis_client, migrate, s3_client
|
2026-01-12 00:53:31 +08:00
|
|
|
|
from blueprints.auth import auth_bp
|
|
|
|
|
|
from blueprints.api import api_bp
|
|
|
|
|
|
from blueprints.admin import admin_bp
|
2026-01-14 17:00:43 +08:00
|
|
|
|
from blueprints.payment import payment_bp
|
2026-01-23 17:40:23 +08:00
|
|
|
|
from apscheduler.schedulers.background import BackgroundScheduler
|
|
|
|
|
|
from apscheduler.triggers.interval import IntervalTrigger
|
2026-01-12 00:53:31 +08:00
|
|
|
|
import threading
|
|
|
|
|
|
import time
|
2026-01-23 17:40:23 +08:00
|
|
|
|
import logging
|
2026-01-12 00:53:31 +08:00
|
|
|
|
|
2026-01-23 17:40:23 +08:00
|
|
|
|
# 导入模律(必需在 db.create_all() 之前导入)
|
2026-01-12 00:53:31 +08:00
|
|
|
|
import models
|
|
|
|
|
|
|
2026-01-23 17:40:23 +08:00
|
|
|
|
# 定时任务函数
|
2026-01-23 17:46:22 +08:00
|
|
|
|
def sync_pending_orders(app):
|
2026-01-23 17:40:23 +08:00
|
|
|
|
"""定时任务: 检查并同步30分钟内的待支付订单"""
|
2026-01-23 17:46:22 +08:00
|
|
|
|
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
|
2026-01-23 17:40:23 +08:00
|
|
|
|
|
2026-01-23 17:46:22 +08:00
|
|
|
|
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:
|
2026-01-23 17:55:33 +08:00
|
|
|
|
# 使用防止并发的锁
|
|
|
|
|
|
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)
|
2026-01-23 17:40:23 +08:00
|
|
|
|
|
2026-01-23 17:55:33 +08:00
|
|
|
|
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()
|
2026-01-23 17:46:22 +08:00
|
|
|
|
|
2026-01-23 17:55:33 +08:00
|
|
|
|
# 二次校验状态,防止异步回调已经处理
|
|
|
|
|
|
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} 已被处理,跳过")
|
2026-01-23 17:46:22 +08:00
|
|
|
|
except Exception as e:
|
2026-01-23 17:55:33 +08:00
|
|
|
|
# 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)}")
|
2026-01-23 17:46:22 +08:00
|
|
|
|
|
|
|
|
|
|
if updated_count > 0:
|
|
|
|
|
|
logging.info(f"定时任务完成,帮助更新了{updated_count}个订单")
|
2026-01-23 17:40:23 +08:00
|
|
|
|
|
2026-01-23 17:46:22 +08:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logging.error(f"定时任务异常: {str(e)}", exc_info=True)
|
2026-01-23 17:40:23 +08:00
|
|
|
|
|
|
|
|
|
|
# 导入模律(必需在 db.create_all() 之前导入)
|
|
|
|
|
|
|
2026-01-12 00:53:31 +08:00
|
|
|
|
def create_app():
|
|
|
|
|
|
app = Flask(__name__)
|
|
|
|
|
|
app.config.from_object(Config)
|
|
|
|
|
|
|
|
|
|
|
|
# 初始化扩展
|
|
|
|
|
|
db.init_app(app)
|
|
|
|
|
|
redis_client.init_app(app)
|
2026-01-16 22:24:14 +08:00
|
|
|
|
migrate.init_app(app, db)
|
2026-01-12 00:53:31 +08:00
|
|
|
|
|
|
|
|
|
|
# 注册蓝图
|
|
|
|
|
|
app.register_blueprint(auth_bp)
|
|
|
|
|
|
app.register_blueprint(api_bp)
|
|
|
|
|
|
app.register_blueprint(admin_bp)
|
2026-01-14 17:00:43 +08:00
|
|
|
|
app.register_blueprint(payment_bp)
|
2026-01-12 00:53:31 +08:00
|
|
|
|
|
2026-01-16 22:24:14 +08:00
|
|
|
|
from flask import g, session
|
|
|
|
|
|
from models import User, SystemLog
|
|
|
|
|
|
|
|
|
|
|
|
@app.before_request
|
|
|
|
|
|
def load_user():
|
|
|
|
|
|
"""在每个请求前加载用户信息到 g,供日志系统使用"""
|
|
|
|
|
|
user_id = session.get('user_id')
|
|
|
|
|
|
if user_id:
|
|
|
|
|
|
g.user_id = user_id
|
|
|
|
|
|
g.user = db.session.get(User, user_id)
|
|
|
|
|
|
else:
|
|
|
|
|
|
g.user_id = None
|
|
|
|
|
|
g.user = None
|
|
|
|
|
|
|
|
|
|
|
|
@app.context_processor
|
|
|
|
|
|
def inject_menu():
|
|
|
|
|
|
"""将导航菜单注入所有模板,实现服务端渲染,解决闪烁问题"""
|
|
|
|
|
|
from blueprints.auth import get_user_menu
|
|
|
|
|
|
menu = get_user_menu(g.user) if hasattr(g, 'user') else []
|
|
|
|
|
|
return dict(nav_menu=menu)
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/api/system_logs')
|
|
|
|
|
|
def get_system_logs():
|
|
|
|
|
|
"""获取系统日志数据 (供后台管理界面使用)"""
|
|
|
|
|
|
# 这里可以加入权限检查
|
|
|
|
|
|
logs = SystemLog.query.order_by(SystemLog.created_at.desc()).limit(100).all()
|
|
|
|
|
|
return jsonify([{
|
|
|
|
|
|
'id': log.id,
|
|
|
|
|
|
'user_id': log.user_id,
|
|
|
|
|
|
'level': log.level,
|
|
|
|
|
|
'module': log.module,
|
|
|
|
|
|
'message': log.message,
|
|
|
|
|
|
'extra': log.extra,
|
|
|
|
|
|
'ip': log.ip,
|
|
|
|
|
|
'path': log.path,
|
|
|
|
|
|
'method': log.method,
|
|
|
|
|
|
'user_agent': log.user_agent,
|
2026-01-17 23:15:58 +08:00
|
|
|
|
'created_at': log.created_at_bj.strftime('%Y-%m-%d %H:%M:%S')
|
2026-01-16 22:24:14 +08:00
|
|
|
|
} for log in logs])
|
|
|
|
|
|
|
2026-01-12 00:53:31 +08:00
|
|
|
|
@app.route('/')
|
|
|
|
|
|
def index():
|
|
|
|
|
|
return render_template('index.html')
|
|
|
|
|
|
|
2026-01-14 00:00:23 +08:00
|
|
|
|
@app.route('/ocr')
|
|
|
|
|
|
def ocr():
|
|
|
|
|
|
return render_template('ocr.html')
|
|
|
|
|
|
|
2026-01-15 21:42:03 +08:00
|
|
|
|
@app.route('/visualizer')
|
|
|
|
|
|
def visualizer():
|
|
|
|
|
|
return render_template('kongzhiqi.html')
|
|
|
|
|
|
|
2026-01-16 22:24:14 +08:00
|
|
|
|
@app.route('/video')
|
|
|
|
|
|
def video_page():
|
|
|
|
|
|
return render_template('video.html')
|
2026-01-21 19:32:02 +08:00
|
|
|
|
|
2026-03-13 22:16:08 +08:00
|
|
|
|
@app.route('/mobile')
|
|
|
|
|
|
def mobile_page():
|
|
|
|
|
|
"""移动端图片生成页面"""
|
|
|
|
|
|
return render_template('mobile.html')
|
|
|
|
|
|
|
2026-02-08 20:39:35 +08:00
|
|
|
|
@app.route('/admin/orders/<int:order_id>')
|
|
|
|
|
|
def order_detail(order_id):
|
|
|
|
|
|
# 权限检查可以在这里做,或者让模板里的 JS 向 API 请求时做 (API 已有权限检查)
|
|
|
|
|
|
return render_template('order_detail.html')
|
|
|
|
|
|
|
2026-01-21 19:32:02 +08:00
|
|
|
|
@app.route('/files/<path:filename>')
|
|
|
|
|
|
def get_file(filename):
|
|
|
|
|
|
"""Proxy route to serve files from MinIO via the backend"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
# Use s3_client to get the object
|
|
|
|
|
|
file_obj = s3_client.get_object(Bucket=Config.MINIO["bucket"], Key=filename)
|
|
|
|
|
|
|
|
|
|
|
|
def generate():
|
|
|
|
|
|
for chunk in file_obj['Body'].iter_chunks(chunk_size=4096):
|
|
|
|
|
|
yield chunk
|
|
|
|
|
|
|
|
|
|
|
|
return Response(
|
|
|
|
|
|
stream_with_context(generate()),
|
|
|
|
|
|
mimetype=file_obj['ContentType'],
|
|
|
|
|
|
headers={
|
|
|
|
|
|
"Cache-Control": "public, max-age=86400"
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
# system_logger.error(f"File proxy error: {str(e)}") # Optional logging
|
|
|
|
|
|
return jsonify({"error": "File not found"}), 404
|
2026-01-16 22:24:14 +08:00
|
|
|
|
|
2026-01-12 00:53:31 +08:00
|
|
|
|
# 自动创建数据库表
|
|
|
|
|
|
with app.app_context():
|
|
|
|
|
|
print("🔧 正在检查并创建数据库表...")
|
|
|
|
|
|
db.create_all()
|
|
|
|
|
|
print("✅ 数据库表已就绪")
|
|
|
|
|
|
|
2026-01-23 17:40:23 +08:00
|
|
|
|
# 创建并启动定时任务调度器
|
|
|
|
|
|
try:
|
|
|
|
|
|
scheduler = BackgroundScheduler(daemon=True)
|
|
|
|
|
|
# 每分钟检查一次待支付订单
|
|
|
|
|
|
scheduler.add_job(
|
|
|
|
|
|
func=sync_pending_orders,
|
2026-01-23 17:46:22 +08:00
|
|
|
|
args=[app],
|
2026-01-23 17:40:23 +08:00
|
|
|
|
trigger=IntervalTrigger(minutes=1),
|
|
|
|
|
|
id='sync_pending_orders',
|
|
|
|
|
|
name='同步待支付订单',
|
|
|
|
|
|
replace_existing=True
|
|
|
|
|
|
)
|
|
|
|
|
|
scheduler.start()
|
|
|
|
|
|
print("🚀 定时任务引擎已启动,将每分钟检查一次待支付订单")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"⚠️ 定时任务启动失败: {str(e)}")
|
|
|
|
|
|
|
2026-01-12 00:53:31 +08:00
|
|
|
|
return app
|
|
|
|
|
|
|
|
|
|
|
|
app = create_app()
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
|
|
app.run(host='0.0.0.0', port=5000, debug=True)
|