From 6cb6f9fc65c660cea13472a1dd4213149f9a8374 Mon Sep 17 00:00:00 2001
From: 24024 <240241002@qq.com>
Date: Thu, 5 Feb 2026 20:47:14 +0800
Subject: [PATCH 1/4] =?UTF-8?q?```=20feat(api):=20=E6=B7=BB=E5=8A=A0Base64?=
=?UTF-8?q?=E5=9B=BE=E7=89=87=E5=A4=84=E7=90=86=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 在后端API中添加Base64图片处理逻辑,自动去除data URL头部信息
- 处理前端传来的包含header的Base64图片数据
refactor(video.js): 优化图片上传处理流程
- 将图片上传逻辑改为纯前端Base64处理,移除对后端上传接口的依赖
- 实现图片压缩功能,限制最大尺寸为2048像素
- 支持PNG和JPEG格式的图片压缩处理
- 移除原有的文件上传FormData方式,改用直接处理Base64数据
```
---
blueprints/api.py | 7 +++++
static/js/video.js | 76 +++++++++++++++++++++++++++++++++++-----------
2 files changed, 65 insertions(+), 18 deletions(-)
diff --git a/blueprints/api.py b/blueprints/api.py
index 1c2e399..38b51e2 100644
--- a/blueprints/api.py
+++ b/blueprints/api.py
@@ -129,6 +129,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/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;
}
From 5e1f037d4c66738d3b7e0240a64891f8f1106339 Mon Sep 17 00:00:00 2001
From: 24024 <240241002@qq.com>
Date: Thu, 5 Feb 2026 20:58:25 +0800
Subject: [PATCH 2/4] =?UTF-8?q?```=20feat(task-service):=20=E5=A2=9E?=
=?UTF-8?q?=E5=BC=BA=E8=A7=86=E9=A2=91=E7=94=9F=E6=88=90=E4=BB=BB=E5=8A=A1?=
=?UTF-8?q?=E5=A4=B1=E8=B4=A5=E5=A4=84=E7=90=86=E6=9C=BA=E5=88=B6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
支持 FAILURE 状态识别并添加循环结束时的状态检查,
防止极端情况下失败状态未被正确抛出异常的问题
```
---
services/task_service.py | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
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. 持久化记录
From bd80414c4d8ee01bde3615bac1c51f7903232469 Mon Sep 17 00:00:00 2001
From: 24024 <240241002@qq.com>
Date: Sun, 8 Feb 2026 20:39:35 +0800
Subject: [PATCH 3/4] =?UTF-8?q?```=20feat(admin):=20=E6=B7=BB=E5=8A=A0?=
=?UTF-8?q?=E8=AE=A2=E5=8D=95=E8=AF=A6=E6=83=85=E9=A1=B5=E9=9D=A2=E5=92=8C?=
=?UTF-8?q?API=E6=8E=A5=E5=8F=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 新增 `/admin/orders/
` 路由用于显示订单详情页面
- 在管理后台的订单列表中添加查看操作按钮
- 实现 `get_order_detail` API 接口,提供订单详细信息
- 添加权限控制,确保只有管理员或订单所有者可访问
- 在充值历史页面也增加订单详情查看功能
- 更新表格布局以适应新增的操作列
```
---
app.py | 5 +
blueprints/admin.py | 38 ++++-
templates/buy.html | 13 +-
templates/order_detail.html | 238 ++++++++++++++++++++++++++++++++
templates/orders.html | 80 +++++++----
templates/recharge_history.html | 10 +-
6 files changed, 348 insertions(+), 36 deletions(-)
create mode 100644 templates/order_detail.html
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/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.00
+
+
+ 优惠金额
+ -¥0.00
+
+
+ 实付款
+ ¥0.00
+
+
+
+
+
+
+
+
+
+ 支付信息
+
+
+
+
+
+
+ 支付宝 (Alipay)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 买家信息
+
+
+
+
+
+
+ 用户 ID
+ #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 @@
-
+
- | 用户信息 |
- 订单详情 |
- 积分/金额 |
- 状态 |
- 时间 |
+ 用户信息
+ |
+ 订单详情
+ |
+ 积分/金额
+ |
+ 状态
+ |
+ 时间
+ |
+ 操作
+ |
- |
+ |
正在获取记录...
@@ -73,17 +83,7 @@
function renderOrders(orders) {
const list = document.getElementById('orderList');
if (orders.length === 0) {
- list.innerHTML = `
-
- |
-
- |
-
- `;
- lucide.createIcons();
+ renderEmptyState();
return;
}
@@ -116,28 +116,48 @@
|
- ${o.status === 'PAID'
- ? '已完成'
- : o.status === 'PENDING'
- ? '待支付'
- : '已取消'
- }
+ ${o.status === 'PAID'
+ ? '已完成'
+ : o.status === 'PENDING'
+ ? '待支付'
+ : '已取消'
+ }
|
-
+ |
创建: ${o.created_at}
${o.paid_at ? '完成: ' + o.paid_at : ''}
|
+
+
+
+
+ |
`).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 %}
- |
+ |
暂无充值记录
From dd140d88d714f56bd9832e0c451fe3efd0affa3c Mon Sep 17 00:00:00 2001
From: 24024 <240241002@qq.com>
Date: Sun, 15 Feb 2026 10:07:38 +0800
Subject: [PATCH 4/4] =?UTF-8?q?```=20docs(README):=20=E6=9B=B4=E6=96=B0?=
=?UTF-8?q?=E5=90=AF=E5=8A=A8=E6=9C=8D=E5=8A=A1=E6=96=87=E6=A1=A3=E5=B9=B6?=
=?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=94=9F=E4=BA=A7=E7=8E=AF=E5=A2=83=E9=83=A8?=
=?UTF-8?q?=E7=BD=B2=E8=AF=B4=E6=98=8E?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 添加了开发环境启动说明
- 新增生产环境 Gunicorn 部署指南
- 包含 Gunicorn 安装和配置参数说明
```
---
README.md | 12 ++++++++++++
requirements.txt | Bin 2118 -> 2156 bytes
2 files changed, 12 insertions(+)
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/requirements.txt b/requirements.txt
index 8b2ee9d5f60e76655bef84b38ee3bae781c07428..19d1a2dba9eea112b2a93c69ed7c5832612cb227 100644
GIT binary patch
delta 46
vcmX>m@J3*R8;2SfLpnn#LmopWLo!1?LlKZ=3xq}t#teE424Kv~z{LOn`j`k9
delta 7
OcmaDOa7 8wUUlodVGS
|