feat(image): 封面形象抠图增加积分消耗(每次 10 积分)

- config/points-config.yaml: 添加 cover_avatar: 10 固定积分
- point_service.py: _CATEGORY_MAP 添加 cover_avatar → 封面形象
- image.py: remove_background 接口前置余额检查 + 后置扣费
- CoverAvatarLibrary.tsx: 上传弹窗显示积分提示,余额不足友好提示
This commit is contained in:
小鱼开发
2026-05-23 10:59:47 +08:00
parent 9589d7c78a
commit 53371aabcd
4 changed files with 51 additions and 12 deletions
+26 -1
View File
@@ -7,17 +7,20 @@
import io
import logging
import time
import uuid
from pathlib import Path
import httpx
from fastapi import APIRouter, Depends, File, HTTPException, Request, UploadFile
from pydantic import BaseModel, Field
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import get_current_user
from app.api.deps import get_current_user, get_db
from app.config import get_settings
from app.models.user import User
from app.schemas.common import ApiResponse, success_response
from app.services import point_service as ps
from app.services.qiniu_service import get_qiniu_service
from app.services.volcengine_mediakit_service import VolcengineMediakitService
from app.utils.file_validation import check_upload_file
@@ -163,12 +166,23 @@ async def remove_background(
req: RemoveBackgroundRequest,
current_user: User = Depends(get_current_user),
mediakit_service: VolcengineMediakitService = Depends(get_mediakit_service),
db: AsyncSession = Depends(get_db),
) -> ApiResponse[RemoveBackgroundResponse]:
"""
AI 抠图(火山引擎 MediaKit
移除图片背景,返回透明背景图片 URL。
每次调用消耗 10 积分。
"""
# 前置积分检查
required_points = ps._calculate_cost("cover_avatar")
check = await ps.check_balance(db, current_user.id, required_points)
if not check["sufficient"]:
raise HTTPException(
status_code=402,
detail=f"积分不足,需要 {required_points} 积分,当前余额 {check['balance']}",
)
try:
logger.info(
f"[RemoveBackground] 开始抠图: image_url={req.image_url[:80]}..., scene={req.scene}"
@@ -216,6 +230,17 @@ async def remove_background(
logger.info(f"[RemoveBackground] 结果已转存七牛云: {qiniu_url[:80]}...")
# 后置扣费(服务已调用成功)
await ps.consume(
db,
user_id=current_user.id,
points=required_points,
source_type="cover_avatar",
source_id=f"cover_avatar_{current_user.id}_{int(time.time() * 1000)}",
description="【封面形象抠图】",
)
await db.commit()
return success_response(
data=RemoveBackgroundResponse(url=qiniu_url),
message="抠图成功",
+1
View File
@@ -329,6 +329,7 @@ _CATEGORY_MAP: dict[str, str] = {
"compose": "压制成片",
"subtitle_burn": "字幕烧录",
"cover_design": "封面设计",
"cover_avatar": "封面形象",
"wxpay": "充值",
"compensation": "充值",
"invite": "充值",
+3
View File
@@ -29,6 +29,9 @@ fixed_costs:
# 封面设计:根据视频内容自动生成封面图
cover_design: 5
# 封面形象:上传人物照片 AI 抠图生成透明背景形象
cover_avatar: 10
# 压制成片:将多个素材片段合并为最终视频(FFmpeg 拼接)
compose: 5
@@ -103,8 +103,13 @@ export default function CoverAvatarLibrary() {
toast.success(`${avatar.name}」已保存`);
} catch (err) {
const msg = err instanceof Error ? err.message : '上传失败';
progress.error(msg);
toast.error(msg);
if (msg.includes('积分不足')) {
progress.error('积分不足');
toast.error('积分不足,每次上传封面形象消耗 10 积分,请先充值');
} else {
progress.error(msg);
toast.error(msg);
}
}
setUploadName('');
@@ -256,15 +261,20 @@ export default function CoverAvatarLibrary() {
</div>
</div>
<div style={{ display: 'flex', gap: 12, justifyContent: 'flex-end' }}>
<button className="btn btn-secondary" onClick={resetUploadModal}></button>
<button
className="btn btn-primary"
onClick={handleUpload}
disabled={!uploadName.trim() || !selectedFile}
>
</button>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<span style={{ fontSize: 'var(--font-xs)', color: 'var(--text-tertiary)' }}>
<strong style={{ color: 'var(--primary)' }}>10</strong>
</span>
<div style={{ display: 'flex', gap: 12 }}>
<button className="btn btn-secondary" onClick={resetUploadModal}></button>
<button
className="btn btn-primary"
onClick={handleUpload}
disabled={!uploadName.trim() || !selectedFile}
>
</button>
</div>
</div>
</div>
</Modal>