This commit is contained in:
24024 2026-01-21 19:32:09 +08:00
commit 455a63f20f
6 changed files with 72 additions and 60 deletions

View File

@ -5,6 +5,14 @@
### 1. 环境准备 ### 1. 环境准备
确保已安装 Python 3.8+ 和 PostgreSQL / Redis。 确保已安装 Python 3.8+ 和 PostgreSQL / Redis。
**创建虚拟环境**
```bash
# 如果 python 命令不可用,请尝试使用 py
python -m venv .venv
# 或者
py -m venv .venv
```
**激活虚拟环境** (推荐) **激活虚拟环境** (推荐)
```bash ```bash
# Windows (PowerShell) # Windows (PowerShell)

View File

@ -68,7 +68,7 @@ def generate():
# 2. 扣除积分 (如果是试用模式) # 2. 扣除积分 (如果是试用模式)
if use_trial: if use_trial:
deduct_points(user, cost) deduct_points(user_id, cost)
model_value = data.get('model') model_value = data.get('model')
prompt = data.get('prompt') prompt = data.get('prompt')
@ -95,7 +95,7 @@ def generate():
# 5. 启动异步生图任务 # 5. 启动异步生图任务
app = current_app._get_current_object() app = current_app._get_current_object()
task_id = start_async_image_task(app, user_id, payload, api_key, target_api, cost, data.get('mode'), model_value) task_id = start_async_image_task(app, user_id, payload, api_key, target_api, cost, data.get('mode'), model_value, use_trial)
return jsonify({ return jsonify({
"task_id": task_id, "task_id": task_id,
@ -119,7 +119,7 @@ def video_generate():
return jsonify({"error": error}), 400 return jsonify({"error": error}), 400
# 2. 扣除积分 # 2. 扣除积分
deduct_points(user, cost) deduct_points(user_id, cost)
# 3. 构造 Payload # 3. 构造 Payload
payload = { payload = {

View File

@ -4,7 +4,7 @@ from models import Order, User, to_bj_time, get_bj_now
from services.alipay_service import AlipayService from services.alipay_service import AlipayService
from services.logger import system_logger from services.logger import system_logger
import uuid import uuid
from datetime import timedelta from datetime import datetime, timedelta
payment_bp = Blueprint('payment', __name__, url_prefix='/payment') payment_bp = Blueprint('payment', __name__, url_prefix='/payment')
@ -102,8 +102,7 @@ def payment_history():
) )
).order_by(Order.created_at.desc()).all() ).order_by(Order.created_at.desc()).all()
import datetime as dt_module return render_template('recharge_history.html', orders=orders, modules={'datetime': datetime})
return render_template('recharge_history.html', orders=orders, modules={'datetime': dt_module})
@payment_bp.route('/api/history', methods=['GET']) @payment_bp.route('/api/history', methods=['GET'])
def api_payment_history(): def api_payment_history():

View File

@ -50,31 +50,34 @@ def validate_generation_request(user, data):
else: else:
return None, None, 0, False, "可用积分已耗尽,请充值或切换至自定义 Key 模式" return None, None, 0, False, "可用积分已耗尽,请充值或切换至自定义 Key 模式"
# 计算单价
cost = get_model_cost(model_value, is_video=False) cost = get_model_cost(model_value, is_video=False)
if use_trial and is_premium: if use_trial and is_premium:
cost *= 2 cost *= 2
if use_trial: if use_trial:
if user.points < cost: if user.points < cost:
return None, None, cost, True, "可用积分不足" return None, None, cost, True, f"可用积分不足(本次需要 {cost} 积分)"
return api_key, target_api, cost, use_trial, None return api_key, target_api, cost, use_trial, None
def deduct_points(user, cost): def deduct_points(user_id, cost):
"""扣除积分""" """原子扣除积分"""
user = db.session.query(User).filter_by(id=user_id).populate_existing().with_for_update().first()
if user:
user.points -= cost user.points -= cost
user.has_used_points = True user.has_used_points = True
db.session.commit() db.session.commit()
def refund_points(user_id, cost): def refund_points(user_id, cost):
"""退还积分""" """原子退还积分"""
try: try:
user = db.session.get(User, user_id) user = db.session.query(User).filter_by(id=user_id).populate_existing().with_for_update().first()
if user: if user:
user.points += cost user.points += cost
db.session.commit() db.session.commit()
except: except:
pass db.session.rollback()
def handle_chat_generation_sync(user_id, api_key, model_value, prompt, use_trial, cost): def handle_chat_generation_sync(user_id, api_key, model_value, prompt, use_trial, cost):
"""同步处理对话类模型""" """同步处理对话类模型"""
@ -114,7 +117,7 @@ def handle_chat_generation_sync(user_id, api_key, model_value, prompt, use_trial
refund_points(user_id, cost) refund_points(user_id, cost)
return {"error": str(e)}, 500 return {"error": str(e)}, 500
def start_async_image_task(app, user_id, payload, api_key, target_api, cost, mode, model_value): def start_async_image_task(app, user_id, payload, api_key, target_api, cost, mode, model_value, use_trial=False):
"""启动异步生图任务""" """启动异步生图任务"""
task_id = str(uuid.uuid4()) task_id = str(uuid.uuid4())
@ -123,7 +126,7 @@ def start_async_image_task(app, user_id, payload, api_key, target_api, cost, mod
threading.Thread( threading.Thread(
target=process_image_generation, target=process_image_generation,
args=(app, user_id, task_id, payload, api_key, target_api, cost) args=(app, user_id, task_id, payload, api_key, target_api, cost, use_trial)
).start() ).start()
return task_id return task_id
@ -150,7 +153,7 @@ def start_async_video_task(app, user_id, payload, cost, model_value):
threading.Thread( threading.Thread(
target=process_video_generation, target=process_video_generation,
args=(app, user_id, task_id, payload, api_key, cost) args=(app, user_id, task_id, payload, api_key, cost, True) # 视频目前默认为积分模式
).start() ).start()
return task_id return task_id

View File

@ -90,7 +90,7 @@ def sync_images_background(app, record_id, raw_urls):
except Exception as e: except Exception as e:
print(f"❌ 更新记录失败: {e}") print(f"❌ 更新记录失败: {e}")
def process_image_generation(app, user_id, task_id, payload, api_key, target_api, cost): def process_image_generation(app, user_id, task_id, payload, api_key, target_api, cost, use_trial=False):
"""异步执行图片生成并存入 Redis""" """异步执行图片生成并存入 Redis"""
with app.app_context(): with app.app_context():
try: try:
@ -99,10 +99,9 @@ def process_image_generation(app, user_id, task_id, payload, api_key, target_api
resp = requests.post(target_api, json=payload, headers=headers, timeout=1000) resp = requests.post(target_api, json=payload, headers=headers, timeout=1000)
if resp.status_code != 200: if resp.status_code != 200:
user = db.session.get(User, user_id) if use_trial:
if user and "sk-" in api_key: from services.generation_service import refund_points
user.points += cost refund_points(user_id, cost)
db.session.commit()
# 记录详细的失败上下文 # 记录详细的失败上下文
system_logger.error(f"生图任务失败: {resp.text}", user_id=user_id, task_id=task_id, prompt=payload.get('prompt'), model=payload.get('model')) system_logger.error(f"生图任务失败: {resp.text}", user_id=user_id, task_id=task_id, prompt=payload.get('prompt'), model=payload.get('model'))
@ -135,10 +134,9 @@ def process_image_generation(app, user_id, task_id, payload, api_key, target_api
except Exception as e: except Exception as e:
# 异常处理:退还积分 # 异常处理:退还积分
user = db.session.get(User, user_id) if use_trial:
if user and "sk-" in api_key: from services.generation_service import refund_points
user.points += cost refund_points(user_id, cost)
db.session.commit()
system_logger.error(f"生图任务异常: {str(e)}", user_id=user_id, task_id=task_id, prompt=payload.get('prompt'), model=payload.get('model')) system_logger.error(f"生图任务异常: {str(e)}", user_id=user_id, task_id=task_id, prompt=payload.get('prompt'), model=payload.get('model'))
redis_client.setex(f"task:{task_id}", 3600, json.dumps({"status": "error", "message": str(e)})) redis_client.setex(f"task:{task_id}", 3600, json.dumps({"status": "error", "message": str(e)}))
@ -203,7 +201,7 @@ def sync_video_background(app, record_id, raw_url, internal_task_id=None):
except Exception as dbe: except Exception as dbe:
system_logger.error(f"更新视频记录失败: {str(dbe)}") system_logger.error(f"更新视频记录失败: {str(dbe)}")
def process_video_generation(app, user_id, internal_task_id, payload, api_key, cost): def process_video_generation(app, user_id, internal_task_id, payload, api_key, cost, use_trial=True):
"""异步提交并查询视频任务状态""" """异步提交并查询视频任务状态"""
with app.app_context(): with app.app_context():
try: try:
@ -273,11 +271,10 @@ def process_video_generation(app, user_id, internal_task_id, payload, api_key, c
except Exception as e: except Exception as e:
system_logger.error(f"视频生成执行异常: {str(e)}", user_id=user_id, task_id=internal_task_id, prompt=payload.get('prompt')) system_logger.error(f"视频生成执行异常: {str(e)}", user_id=user_id, task_id=internal_task_id, prompt=payload.get('prompt'))
# 尝试退费 # 尝试退费
if use_trial:
try: try:
user = db.session.get(User, user_id) from services.generation_service import refund_points
if user: refund_points(user_id, cost)
user.points += cost
db.session.commit()
except Exception as re: except Exception as re:
system_logger.error(f"退费失败: {str(re)}") system_logger.error(f"退费失败: {str(re)}")

View File

@ -195,6 +195,8 @@ async function init() {
if (modeTrialBtn) modeTrialBtn.onclick = () => switchMode('trial'); if (modeTrialBtn) modeTrialBtn.onclick = () => switchMode('trial');
if (modeKeyBtn) modeKeyBtn.onclick = () => switchMode('key'); if (modeKeyBtn) modeKeyBtn.onclick = () => switchMode('key');
if (isPremiumCheckbox) isPremiumCheckbox.onchange = () => updateCostPreview(); if (isPremiumCheckbox) isPremiumCheckbox.onchange = () => updateCostPreview();
const numSelect = document.getElementById('numSelect');
if (numSelect) numSelect.onchange = () => updateCostPreview();
// 历史记录控制 // 历史记录控制
const historyDrawer = document.getElementById('historyDrawer'); const historyDrawer = document.getElementById('historyDrawer');
@ -305,13 +307,16 @@ function fillSelect(id, list) {
function updateCostPreview() { function updateCostPreview() {
const modelSelect = document.getElementById('modelSelect'); const modelSelect = document.getElementById('modelSelect');
const costPreview = document.getElementById('costPreview'); const costPreview = document.getElementById('costPreview');
const numSelect = document.getElementById('numSelect');
const isPremium = document.getElementById('isPremium')?.checked || false; const isPremium = document.getElementById('isPremium')?.checked || false;
const selectedOption = modelSelect.options[modelSelect.selectedIndex]; const selectedOption = modelSelect.options[modelSelect.selectedIndex];
if (currentMode === 'trial' && selectedOption) { if (currentMode === 'trial' && selectedOption) {
let cost = parseInt(selectedOption.getAttribute('data-cost') || 0); let baseCost = parseInt(selectedOption.getAttribute('data-cost') || 0);
if (isPremium) cost *= 2; // 优质模式 2 倍积分 let num = parseInt(numSelect?.value || 1);
costPreview.innerText = `本次生成将消耗 ${cost} 积分`; let totalCost = baseCost * num;
if (isPremium) totalCost *= 2; // 优质模式 2 倍积分
costPreview.innerText = `本次生成将消耗 ${totalCost} 积分`;
costPreview.classList.remove('hidden'); costPreview.classList.remove('hidden');
} else { } else {
costPreview.classList.add('hidden'); costPreview.classList.add('hidden');
@ -572,8 +577,21 @@ document.getElementById('submitBtn').onclick = async () => {
} }
}; };
// 提取结果展示逻辑 const tasks = Array.from({ length: num }, (_, i) => startTask(i));
const displayResult = (slot, data) => { await Promise.all(tasks);
} catch (e) {
showToast('创作引擎中断: ' + e.message, 'error');
document.getElementById('placeholder').classList.remove('hidden');
} finally {
btn.disabled = false;
btnText.innerText = "立即生成作品";
document.getElementById('statusInfo').classList.add('hidden');
}
};
// 提取结果展示逻辑
const displayResult = (slot, data) => {
if (data.type === 'text') { if (data.type === 'text') {
slot.className = 'image-frame relative bg-white border border-slate-100 p-8 rounded-[2.5rem] shadow-xl overflow-y-auto max-h-[600px]'; slot.className = 'image-frame relative bg-white border border-slate-100 p-8 rounded-[2.5rem] shadow-xl overflow-y-auto max-h-[600px]';
slot.innerHTML = `<div class="prose prose-slate prose-sm max-w-none text-slate-600 font-medium leading-relaxed">${data.content.replace(/\n/g, '<br>')}</div>`; slot.innerHTML = `<div class="prose prose-slate prose-sm max-w-none text-slate-600 font-medium leading-relaxed">${data.content.replace(/\n/g, '<br>')}</div>`;
@ -592,19 +610,6 @@ document.getElementById('submitBtn').onclick = async () => {
`; `;
} }
lucide.createIcons(); lucide.createIcons();
};
const tasks = Array.from({ length: num }, (_, i) => startTask(i));
await Promise.all(tasks);
} catch (e) {
showToast('创作引擎中断: ' + e.message, 'error');
document.getElementById('placeholder').classList.remove('hidden');
} finally {
btn.disabled = false;
btnText.innerText = "立即生成作品";
document.getElementById('statusInfo').classList.add('hidden');
}
}; };
init(); init();