生成任务失败积分回退
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user