生成任务失败积分回退

This commit is contained in:
2026-06-03 16:15:45 +08:00
parent 7c56c02135
commit 1079a5a7d1
14 changed files with 931 additions and 259 deletions
+95 -14
View File
@@ -141,30 +141,71 @@ async def calc_image_credits(
return round(base_cost * multiplier, 2)
async def _get_existing_credit_record_by_biz_key(
db: AsyncSession,
*,
user_id: str,
biz_key: str | None,
) -> CreditRecord | None:
"""按正式业务幂等键查找已有积分流水。"""
if not biz_key:
return None
result = await db.execute(
select(CreditRecord)
.where(CreditRecord.user_id == user_id, CreditRecord.biz_key == biz_key)
.limit(1)
)
return result.scalar_one_or_none()
async def deduct_credits(
db: AsyncSession,
user_id: str,
amount: float,
description: str,
related_id: str | None = None,
*,
biz_key: str | None = None,
refund_for_biz_key: str | None = None,
) -> User:
"""Atomically deduct credits from user. Raises InsufficientCreditsError."""
result = await db.execute(
select(User).where(User.id == user_id).limit(1)
)
"""扣减用户积分,并写入消费流水。
并发安全点:
- 先用 SELECT ... FOR UPDATE 锁住 users 行,避免余额覆盖。
- biz_key 不为空时,作为正式业务幂等键;重复调用直接返回当前用户,不重复扣。
"""
amount = round(float(amount or 0), 2)
if amount <= 0:
result = await db.execute(select(User).where(User.id == user_id).with_for_update().limit(1))
user = result.scalar_one_or_none()
if not user:
raise ValueError("User not found")
return user
result = await db.execute(select(User).where(User.id == user_id).with_for_update().limit(1))
user = result.scalar_one_or_none()
if not user or user.credits < amount:
if not user:
raise ValueError("User not found")
if biz_key:
existing = await _get_existing_credit_record_by_biz_key(db, user_id=user_id, biz_key=biz_key)
if existing:
return user
if float(user.credits or 0) < amount:
raise InsufficientCreditsError()
user.credits = round(user.credits - amount, 2)
user.credits = round(float(user.credits or 0) - amount, 2)
record = CreditRecord(
id=generate_id(),
user_id=user_id,
type="consume",
amount=-round(amount, 2),
amount=-amount,
balance_after=user.credits,
description=description,
related_id=related_id,
biz_key=biz_key,
refund_for_biz_key=refund_for_biz_key,
)
db.add(record)
await db.flush()
@@ -177,30 +218,70 @@ async def add_credits(
amount: float,
description: str,
related_id: str | None = None,
*,
record_type: str = "recharge",
biz_key: str | None = None,
refund_for_biz_key: str | None = None,
) -> User:
"""Add credits to user."""
result = await db.execute(
select(User).where(User.id == user_id).limit(1)
)
"""增加用户积分,并写入流水。
record_type 默认保持原来的 recharge;生成失败回退时传 refund。
biz_key 不为空时幂等,重复调用不会重复加积分。
"""
amount = round(float(amount or 0), 2)
result = await db.execute(select(User).where(User.id == user_id).with_for_update().limit(1))
user = result.scalar_one_or_none()
if not user:
raise ValueError("User not found")
user.credits = round(user.credits + amount, 2)
if biz_key:
existing = await _get_existing_credit_record_by_biz_key(db, user_id=user_id, biz_key=biz_key)
if existing:
return user
if amount <= 0:
return user
user.credits = round(float(user.credits or 0) + amount, 2)
record = CreditRecord(
id=generate_id(),
user_id=user_id,
type="recharge",
amount=round(amount, 2),
type=record_type,
amount=amount,
balance_after=user.credits,
description=description,
related_id=related_id,
biz_key=biz_key,
refund_for_biz_key=refund_for_biz_key,
)
db.add(record)
await db.flush()
return user
async def refund_credits(
db: AsyncSession,
user_id: str,
amount: float,
description: str,
related_id: str | None = None,
*,
biz_key: str | None = None,
refund_for_biz_key: str | None = None,
) -> User:
"""生成失败积分回退。"""
return await add_credits(
db,
user_id=user_id,
amount=amount,
description=description,
related_id=related_id,
record_type="refund",
biz_key=biz_key,
refund_for_biz_key=refund_for_biz_key,
)
async def get_records(db: AsyncSession, user_id: str) -> list[CreditRecord]:
result = await db.execute(
select(CreditRecord)