Files
meijiaka-zy/python-api/app/crud/user_point.py
T
小鱼开发 c6eba97b43 feat(points): 积分消耗系统全链路集成
后端:
- 简化积分服务: 删除 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: 积分消耗集成收尾
2026-05-09 15:42:54 +08:00

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()