增加预扣积分

This commit is contained in:
2026-07-22 09:27:33 +08:00
parent 4afda8992c
commit 9b13d01d72
6 changed files with 64 additions and 46 deletions
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -28,7 +28,7 @@
} }
})(); })();
</script> </script>
<script type="module" crossorigin src="/assets/index-BLkGzXo7.js"></script> <script type="module" crossorigin src="/assets/index-CtNUj_vl.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D7ShJUt4.css"> <link rel="stylesheet" crossorigin href="/assets/index-D7ShJUt4.css">
</head> </head>
<body> <body>
+4 -3
View File
@@ -178,7 +178,7 @@ const AdminSettings: React.FC = () => {
'协议配置': configs.filter(c => c.key === 'user_agreement_privacy_url'), '协议配置': configs.filter(c => c.key === 'user_agreement_privacy_url'),
'SEO 设置': configs.filter(c => c.key.startsWith('seo_')), 'SEO 设置': configs.filter(c => c.key.startsWith('seo_')),
'用户积分配置': configs.filter(c => c.key.startsWith('user_') && c.key.includes('credits')), '用户积分配置': configs.filter(c => c.key.startsWith('user_') && c.key.includes('credits')),
'其他配置': configs.filter(c => c.key === 'operation_manual'), '其他配置': configs.filter(c => ['operation_manual', 'optimize_hold_credits'].includes(c.key)),
}; };
const getFieldDescription = (config: SystemConfig): string => { const getFieldDescription = (config: SystemConfig): string => {
@@ -194,6 +194,7 @@ const AdminSettings: React.FC = () => {
user_login_credits: '用户每日登录赠送的积分数量', user_login_credits: '用户每日登录赠送的积分数量',
user_login_credits_enabled: '是否启用每日登录赠送积分功能', user_login_credits_enabled: '是否启用每日登录赠送积分功能',
operation_manual: '操作手册链接,前台用户菜单将展示该入口,点击跳转此链接', operation_manual: '操作手册链接,前台用户菜单将展示该入口,点击跳转此链接',
optimize_hold_credits: 'AI创作时预扣积分数量,用于防止并发超卖。预扣后按实际消耗多退少补',
}; };
return descMap[config.key] || config.description || ''; return descMap[config.key] || config.description || '';
}; };
@@ -323,8 +324,8 @@ const AdminSettings: React.FC = () => {
</div> </div>
); );
} }
if (config.key === 'user_register_credits' || config.key === 'user_login_credits') { if (config.key === 'user_register_credits' || config.key === 'user_login_credits' || config.key === 'optimize_hold_credits') {
return <Input type="number" min={0} placeholder={config.description} size="large" />; return <Input type="number" min={1} placeholder={config.description} size="large" />;
} }
return <Input placeholder={config.description} size="large" />; return <Input placeholder={config.description} size="large" />;
}; };
+2 -1
View File
@@ -342,7 +342,7 @@ async def get_site_info(db: AsyncSession = Depends(get_db)):
"""Public endpoint returning site name, logo, agreement and copyright info.""" """Public endpoint returning site name, logo, agreement and copyright info."""
result = await db.execute( result = await db.execute(
select(SystemConfig).where(SystemConfig.key.in_([ select(SystemConfig).where(SystemConfig.key.in_([
"site_name", "site_logo", "user_agreement_privacy_url", "site_copyright", "operation_manual", "login_bg_video" "site_name", "site_logo", "user_agreement_privacy_url", "site_copyright", "operation_manual", "login_bg_video", "optimize_hold_credits"
])) ]))
) )
configs = result.scalars().all() configs = result.scalars().all()
@@ -366,6 +366,7 @@ async def get_site_info(db: AsyncSession = Depends(get_db)):
"site_copyright": info.get("site_copyright", "© 2024 民众智创 版权所有"), "site_copyright": info.get("site_copyright", "© 2024 民众智创 版权所有"),
"operation_manual": info.get("operation_manual", ""), "operation_manual": info.get("operation_manual", ""),
"login_bg_video": to_full_url(info.get("login_bg_video")) if info.get("login_bg_video") else "", "login_bg_video": to_full_url(info.get("login_bg_video")) if info.get("login_bg_video") else "",
"optimize_hold_credits": int(info.get("optimize_hold_credits") or 5),
} }
+20 -5
View File
@@ -10,11 +10,11 @@ from fastapi.responses import RedirectResponse
from sqlalchemy import select, func from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.dependencies import get_db, get_current_user from app.dependencies import get_db, get_current_user
from app.models.user import User from app.models.user import User
from app.models.project import Project from app.models.project import Project
from app.models.generation_record import GenerationRecord from app.models.generation_record import GenerationRecord
from app.models.system_config import SystemConfig
from app.schemas.generation import ( from app.schemas.generation import (
OptimizeParams, OptimizeParams,
GenerateParams, GenerateParams,
@@ -32,7 +32,7 @@ from app.services.generation.pipeline.db_lock_service import (
DatabaseRowLockBusy, DatabaseRowLockBusy,
execute_with_lock_timeout, execute_with_lock_timeout,
) )
from app.services.credits import deduct_credits, calc_text_credits from app.services.credits import deduct_credits, add_credits, calc_text_credits
from app.services.llm import optimize_prompt from app.services.llm import optimize_prompt
from app.services.video_url import generate_temp_url, validate_and_get_record_id, get_video_stream_url from app.services.video_url import generate_temp_url, validate_and_get_record_id, get_video_stream_url
from app.services.resource_accounting_service import ( from app.services.resource_accounting_service import (
@@ -271,9 +271,17 @@ async def optimize(
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
): ):
record = None record = None
# 积分不足直接返回 # 预扣积分(SELECT FOR UPDATE 锁行 + 检查 + 扣费,防止并发超卖)
if (current_user.credits or 0) < 5: hold_credits = 5
raise HTTPException(status_code=402, detail="积分不足,请充值") _hold_cfg = await db.execute(select(SystemConfig).where(SystemConfig.key == "optimize_hold_credits").limit(1))
_hold_row = _hold_cfg.scalar_one_or_none()
if _hold_row and _hold_row.value:
try:
hold_credits = int(_hold_row.value)
except (ValueError, TypeError):
hold_credits = 5
await deduct_credits(db, current_user.id, hold_credits, "AI创作预扣积分", biz_key=f"optimize_hold:{req.idempotency_key or 'nokey'}")
await db.commit()
# Validate parameters based on generation type # Validate parameters based on generation type
if req.gen_type == GenerationType.video: if req.gen_type == GenerationType.video:
if req.duration not in DURATIONS: if req.duration not in DURATIONS:
@@ -377,6 +385,13 @@ async def optimize(
db, token_usage["input_tokens"], token_usage["output_tokens"], db, token_usage["input_tokens"], token_usage["output_tokens"],
) )
# 积分结算:预扣金额按实际消耗多退少补
if text_credits < hold_credits:
await add_credits(db, current_user.id, round(hold_credits - text_credits, 2), f"提示词优化积分退还- {project.name}", biz_key=f"optimize_refund:{req.idempotency_key or 'nokey'}")
elif text_credits > hold_credits:
await deduct_credits(db, current_user.id, round(text_credits - hold_credits, 2), f"提示词优化补扣积分 - {project.name}", biz_key=f"optimize_topup:{req.idempotency_key or 'nokey'}")
await db.commit()
failed_record_id = record.id failed_record_id = record.id
failed_user_id = current_user.id failed_user_id = current_user.id
try: try:
+1
View File
@@ -185,6 +185,7 @@ async def _seed_data():
("user_login_credits_enabled", "false", "启用每日登录赠送积分"), ("user_login_credits_enabled", "false", "启用每日登录赠送积分"),
# Operation manual # Operation manual
("operation_manual", "", "操作手册链接"), ("operation_manual", "", "操作手册链接"),
("optimize_hold_credits", "5", "AI创作预扣积分数量(防止并发超卖)"),
] ]
for key, value, desc in configs: for key, value, desc in configs:
existing = await db.execute( existing = await db.execute(