diff --git a/README.md b/README.md index 71dc125..a81ed41 100644 --- a/README.md +++ b/README.md @@ -28,11 +28,23 @@ pip install -r requirements.txt ``` ### 2. 启动服务 + +**开发环境** ```bash python app.py ``` 服务默认运行在 `http://127.0.0.1:5000`。 +**生产环境 (Gunicorn)** +在 Linux 生产环境中,建议使用 Gunicorn 作为 WSGI 服务器以获得更好的性能和稳定性: +```bash +pip install gunicorn # 如果尚未安装 +gunicorn -w 4 -b 0.0.0.0:5000 app:app +``` +* `-w 4`: 使用 4 个工作进程(通常设为 CPU 核心数 * 2 + 1)。 +* `-b 0.0.0.0:5000`: 绑定所有 IP 且端口为 5000。 +* `app:app`: 加载 `app.py` 中的 `app` 实例。 + --- ## 🛠️ 常用维护命令 diff --git a/app.py b/app.py index b4e25ba..068ac28 100644 --- a/app.py +++ b/app.py @@ -156,6 +156,11 @@ def create_app(): def video_page(): return render_template('video.html') + @app.route('/admin/orders/') + def order_detail(order_id): + # 权限检查可以在这里做,或者让模板里的 JS 向 API 请求时做 (API 已有权限检查) + return render_template('order_detail.html') + @app.route('/files/') def get_file(filename): """Proxy route to serve files from MinIO via the backend""" diff --git a/blueprints/admin.py b/blueprints/admin.py index 4b372b1..2e27470 100644 --- a/blueprints/admin.py +++ b/blueprints/admin.py @@ -1,8 +1,8 @@ -from flask import Blueprint, request, jsonify +from flask import Blueprint, request, jsonify, g from datetime import timedelta from extensions import db from models import User, Role, Permission, SystemDict, SystemNotification, Order, PointsGrant, InviteReward, to_bj_time, get_bj_now -from middlewares.auth import permission_required +from middlewares.auth import permission_required, login_required from services.logger import system_logger admin_bp = Blueprint('admin', __name__, url_prefix='/api/admin') @@ -303,6 +303,40 @@ def get_orders(): } for o in orders] }) +@admin_bp.route('/orders/', methods=['GET']) +@login_required +def get_order_detail(order_id): + order = db.session.get(Order, order_id) + if not order: + return jsonify({"error": "订单不存在"}), 404 + + # 权限检查:必须是管理员或者订单所有者 + if not g.user.has_permission('manage_system') and order.user_id != g.user.id: + return jsonify({"error": "无权访问此订单"}), 403 + + user = order.user + return jsonify({ + "order": { + "id": order.id, + "out_trade_no": order.out_trade_no, + "trade_no": order.trade_no, + "amount": float(order.amount), + "points": order.points, + "status": order.status, + "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 + }, + "buyer": { + "id": user.id, + "phone": user.phone, + "current_points": user.points, + "created_at": user.created_at_bj.strftime('%Y-%m-%d %H:%M:%S'), + "is_banned": user.is_banned, + "role": user.role.name if user.role else "普通用户" + }, + "current_is_admin": g.user.has_permission('manage_system') + }) + # --- 积分发放管理 --- @admin_bp.route('/points/grant', methods=['POST']) @permission_required('manage_system') diff --git a/blueprints/api.py b/blueprints/api.py index e121013..173e5f2 100644 --- a/blueprints/api.py +++ b/blueprints/api.py @@ -130,6 +130,13 @@ def video_generate(): "images": data.get('images', []), "aspect_ratio": data.get('aspect_ratio', '9:16') } + + # 处理 Base64 图片 (去掉 header) + if payload.get("images"): + payload["images"] = [ + img.split(',', 1)[1] if ',' in img else img + for img in payload["images"] + ] # 4. 启动异步视频任务 app = current_app._get_current_object() diff --git a/requirements.txt b/requirements.txt index 8b2ee9d..19d1a2d 100644 Binary files a/requirements.txt and b/requirements.txt differ diff --git a/services/task_service.py b/services/task_service.py index c2464de..4db5f9b 100644 --- a/services/task_service.py +++ b/services/task_service.py @@ -359,10 +359,14 @@ def process_video_generation(app, user_id, internal_task_id, payload, api_key, c elif 'url' in poll_result: video_url = poll_result['url'] break - elif status in ['FAILED', 'ERROR']: - raise Exception(f"视频生成失败: {poll_result.get('fail_reason') or poll_result.get('message') or '未知错误'}") + elif status in ['FAILURE', 'FAILED', 'ERROR']: + reason = poll_result.get('fail_reason') or poll_result.get('message') or '未知错误' + raise Exception(f"视频生成失败: {reason}") if not video_url: + if status in ['FAILURE', 'FAILED', 'ERROR']: # 防止循环结束时正好是失败状态但未抛出的极端情况 + reason = poll_result.get('fail_reason') or poll_result.get('message') or '未知错误' + raise Exception(f"视频生成失败: {reason}") raise Exception("超时未获取到视频地址") # 3. 持久化记录 diff --git a/static/js/video.js b/static/js/video.js index d6faf24..d0a8ceb 100644 --- a/static/js/video.js +++ b/static/js/video.js @@ -149,32 +149,72 @@ document.addEventListener('DOMContentLoaded', () => { showToast('已应用提示词模板', 'success'); }; - // 上传图片逻辑 + // 图片处理辅助函数 + const processImageFile = (file) => new Promise((resolve, reject) => { + const reader = new FileReader(); + 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); + }); + + // 上传图片逻辑 (改为纯前端 Base64 处理) fileInput.onchange = async (e) => { const files = e.target.files; if (files.length === 0) return; - const formData = new FormData(); - formData.append('images', files[0]); - try { submitBtn.disabled = true; - const r = await fetch('/api/upload', { method: 'POST', body: formData }); - const d = await r.json(); - if (d.urls && d.urls.length > 0) { - uploadedImageUrl = d.urls[0]; - imagePreview.innerHTML = ` -
- - + // 压缩并转换为 Base64 + uploadedImageUrl = await processImageFile(files[0]); + + imagePreview.innerHTML = ` +
+ + - `; - lucide.createIcons(); - } +
+ `; + lucide.createIcons(); + showToast('图片已就绪', 'success'); } catch (err) { - showToast('图片上传失败', 'error'); + console.error(err); + showToast('图片处理失败', 'error'); } finally { submitBtn.disabled = false; } diff --git a/templates/buy.html b/templates/buy.html index 99ff7c4..4d434ed 100644 --- a/templates/buy.html +++ b/templates/buy.html @@ -181,7 +181,8 @@ 订单信息 积分/金额 状态 - 时间 + 时间 + 操作 @@ -216,15 +217,21 @@ 已取消 {% endif %} - + {{ order.paid_at_bj.strftime('%Y-%m-%d %H:%M') if order.paid_at_bj else order.created_at_bj.strftime('%Y-%m-%d %H:%M') }} + + + + + {% endfor %} {% else %} - + 暂无充值记录 diff --git a/templates/order_detail.html b/templates/order_detail.html new file mode 100644 index 0000000..ca78fd4 --- /dev/null +++ b/templates/order_detail.html @@ -0,0 +1,238 @@ +{% extends "base.html" %} + +{% block title %}订单详情 - 管理后台{% endblock %} + +{% block content %} +
+
+ +
+
+ + + +
+

订单详情

+

+ + 订单号: ... +

+
+
+
+ +
+
+ +
+ +
+ +
+
+

+ + 商品信息 +

+
+ +
+
+
+
+ +
+
+

账户充值积分

+

虚拟商品 · 即时到账

+
+
+
+
+0 Pts +
+
¥0.00
+
+
+ +
+
+ 商品总额 + ¥0.00 +
+
+ 优惠金额 + -¥0.00 +
+
+ 实付款 + ¥0.00 +
+
+
+
+ + +
+

+ + 支付信息 +

+ +
+
+ +
+ 支付宝 (Alipay) +
+
+
+ +

--

+
+
+ +

--

+
+
+ +

--

+
+
+
+
+ + +
+
+

+ + 买家信息 +

+ +
+
+ +
+
+

--

+
+ -- + +
+
+
+ +
+
+ 用户 ID + #0 +
+
+ 注册时间 + -- +
+
+ 当前余额 +
+ + 0 +
+
+
+ + +
+ + +
+
+ +

系统提示

+
+

+ 该订单由系统自动处理并完成积分发放。如有异常,请及时联系技术人员查看服务器日志。 +

+
+
+
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/templates/orders.html b/templates/orders.html index a91d21b..479704f 100644 --- a/templates/orders.html +++ b/templates/orders.html @@ -16,31 +16,41 @@
- +
-
-
+
- - - - - + + + + + + - - - - `; - lucide.createIcons(); + renderEmptyState(); return; } @@ -116,28 +116,48 @@ - + `).join(''); lucide.createIcons(); } + function renderEmptyState() { + const list = document.getElementById('orderList'); + list.innerHTML = ` + + + + `; + lucide.createIcons(); + } + document.getElementById('orderSearch').oninput = (e) => { const val = e.target.value.toLowerCase(); - const filtered = allOrders.filter(o => - o.user_phone.includes(val) || + const filtered = allOrders.filter(o => + o.user_phone.includes(val) || o.out_trade_no.toLowerCase().includes(val) ); renderOrders(filtered); @@ -145,4 +165,4 @@ loadOrders(); -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/templates/recharge_history.html b/templates/recharge_history.html index 5b196f3..4ae2133 100644 --- a/templates/recharge_history.html +++ b/templates/recharge_history.html @@ -38,6 +38,8 @@ + @@ -84,11 +86,17 @@ {% endif %} + {% endfor %} {% else %} -
用户信息订单详情积分/金额状态时间用户信息 + 订单详情 + 积分/金额 + 状态 + 时间 + 操作 +
+

正在获取记录...

@@ -73,17 +83,7 @@ function renderOrders(orders) { const list = document.getElementById('orderList'); if (orders.length === 0) { - list.innerHTML = ` -
-
- -

暂无记录

-
-
- ${o.status === 'PAID' - ? '已完成' - : o.status === 'PENDING' - ? '待支付' - : '已取消' - } + ${o.status === 'PAID' + ? '已完成' + : o.status === 'PENDING' + ? '待支付' + : '已取消' + } +
创建: ${o.created_at} ${o.paid_at ? '完成: ' + o.paid_at : ''}
+ + + +
+
+ +

暂无记录

+
+
支付时间 操作 +
+ + + +
+

暂无充值记录