c6eba97b43
后端: - 简化积分服务: 删除 freeze/settle/refund, 保留 consume/recharge/expire - 计费配置化: config/points-config.yaml 驱动 fixed/duration/free 三种模式 - TTS 时长探测: app/utils/audio_utils.py (httpx + mutagen 纯 Python) - Python 层扣费: script(5)/polish(1)/title(1)/voice_clone(200)/tts(按秒)/video(按秒) - 字幕 free_services: caption/auto_align 不扣费 - 新增 POST /points/consume 端点(402余额预检) - 新增 check_balance + /points/cost 返回 sufficient/balance/required - 新增 expire_batches 定时回收, 接入 scheduler main(每5分钟) - 删除废弃 tts_handler.py - Alembic 迁移: 删除 frozen/total_refunded 字段 - 同步 requirements.lock 添加 mutagen 前端: - Rust/IPC 层扣费: compose(5)/subtitle_burn(2)/cover_design(2) - 字幕打轴改异步: 走 scheduler subtitle handler - 对口型传 duration: VideoGeneration 传 actualDuration - 创建 pointStore: 全局余额 + fetchBalance + 充值弹窗控制 - 402 欠费弹 RechargeModal: VideoGeneration/SubtitleBurning/CoverDesign - 修复 VoiceDubbing.tsx 类型错误 (alignResult never) - 同步 PointBalance 类型(删除 frozen/available/totalRefunded) Refs: 积分消耗集成收尾
146 lines
3.9 KiB
Python
146 lines
3.9 KiB
Python
"""
|
|
用户积分汇总 CRUD
|
|
=================
|
|
|
|
核心操作:查询余额、原子更新(充值/消费/冻结/返还/过期)。
|
|
所有更新操作使用 SELECT ... FOR UPDATE 防止并发超扣。
|
|
"""
|
|
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.crud.base import CRUDBase
|
|
from app.models.user_point import UserPoint
|
|
|
|
|
|
class UserPointCRUD(CRUDBase[UserPoint]):
|
|
"""用户积分汇总数据访问对象"""
|
|
|
|
def __init__(self) -> None:
|
|
super().__init__(UserPoint)
|
|
|
|
async def get_by_user_id(
|
|
self, db: AsyncSession, *, user_id: str
|
|
) -> UserPoint | None:
|
|
"""根据用户 ID 获取积分汇总记录"""
|
|
result = await db.execute(
|
|
select(UserPoint).where(UserPoint.user_id == user_id)
|
|
)
|
|
return result.scalar_one_or_none()
|
|
|
|
async def get_or_create(
|
|
self, db: AsyncSession, *, user_id: str
|
|
) -> UserPoint:
|
|
"""获取或创建用户积分记录"""
|
|
point = await self.get_by_user_id(db, user_id=user_id)
|
|
if point is None:
|
|
point = await self.create(db, obj_in={"user_id": user_id})
|
|
return point
|
|
|
|
async def get_by_user_id_for_update(
|
|
self, db: AsyncSession, *, user_id: str
|
|
) -> UserPoint | None:
|
|
"""
|
|
根据用户 ID 获取积分记录并锁定(FOR UPDATE)。
|
|
|
|
用于充值、消费、冻结等需要原子操作的场景。
|
|
"""
|
|
result = await db.execute(
|
|
select(UserPoint)
|
|
.where(UserPoint.user_id == user_id)
|
|
.with_for_update()
|
|
)
|
|
return result.scalar_one_or_none()
|
|
|
|
async def recharge(
|
|
self,
|
|
db: AsyncSession,
|
|
*,
|
|
user_id: str,
|
|
points: int,
|
|
) -> UserPoint:
|
|
"""
|
|
充值积分。
|
|
|
|
增加 balance 和 total_recharged。
|
|
"""
|
|
point = await self.get_by_user_id_for_update(db, user_id=user_id)
|
|
if point is None:
|
|
point = await self.create(
|
|
db,
|
|
obj_in={
|
|
"user_id": user_id,
|
|
"balance": points,
|
|
"total_recharged": points,
|
|
},
|
|
)
|
|
else:
|
|
point.balance += points
|
|
point.total_recharged += points
|
|
await db.commit()
|
|
await db.refresh(point)
|
|
return point
|
|
|
|
async def freeze(
|
|
self,
|
|
db: AsyncSession,
|
|
*,
|
|
user_id: str,
|
|
points: int,
|
|
) -> UserPoint:
|
|
"""
|
|
冻结积分(预扣)。
|
|
|
|
减少 balance,增加 frozen。
|
|
调用前必须确保 balance >= points。
|
|
"""
|
|
point = await self.get_by_user_id_for_update(db, user_id=user_id)
|
|
point.balance -= points
|
|
point.frozen += points
|
|
await db.commit()
|
|
await db.refresh(point)
|
|
return point
|
|
|
|
async def consume_from_frozen(
|
|
self,
|
|
db: AsyncSession,
|
|
*,
|
|
user_id: str,
|
|
points: int,
|
|
) -> UserPoint:
|
|
"""
|
|
结算:将冻结的积分转为实际消费。
|
|
|
|
减少 frozen,增加 total_consumed。
|
|
"""
|
|
point = await self.get_by_user_id_for_update(db, user_id=user_id)
|
|
point.frozen -= points
|
|
point.total_consumed += points
|
|
await db.commit()
|
|
await db.refresh(point)
|
|
return point
|
|
|
|
async def expire(
|
|
self,
|
|
db: AsyncSession,
|
|
*,
|
|
user_id: str,
|
|
points: int,
|
|
) -> UserPoint:
|
|
"""
|
|
过期积分。
|
|
|
|
减少 balance,增加 total_expired。
|
|
注意:过期的积分不能是冻结中的。
|
|
"""
|
|
point = await self.get_by_user_id_for_update(db, user_id=user_id)
|
|
point.balance -= points
|
|
point.total_expired += points
|
|
await db.commit()
|
|
await db.refresh(point)
|
|
return point
|
|
|
|
|
|
# 导出实例
|
|
user_point = UserPointCRUD()
|