Files
video-gen/video-gen-api/app/services/credit/expiration_service.py
T
2026-08-11 09:24:18 +08:00

139 lines
5.0 KiB
Python

from __future__ import annotations
from collections import Counter
from datetime import datetime
from decimal import Decimal
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.enums.credit_balance import CreditAllocationAction, CreditBalanceStatus
from app.enums.credit_record import CreditRecordBillingScene, CreditRecordType
from app.models.credit.allocation import CreditRecordAllocation
from app.models.credit.balance import UserCreditBalance
from app.models.credit_record import CreditRecord
from app.services.credit.locking import acquire_user_credit_lock
from app.services.credit.query_service import get_available_credits
from app.services.credit.utils import to_credit_decimal, utc_now
from app.utils.id_gen import generate_id
async def archive_expired_user_balances(
db: AsyncSession,
*,
user_id: str,
request_time: datetime | None = None,
limit: int = 500,
) -> int:
checked_at = request_time or utc_now()
await acquire_user_credit_lock(db, user_id)
result = await db.execute(
select(UserCreditBalance)
.where(
UserCreditBalance.user_id == user_id,
UserCreditBalance.expires_at <= checked_at,
UserCreditBalance.unspent_amount > 0,
UserCreditBalance.expired_processed_at.is_(None),
UserCreditBalance.revoked_at.is_(None),
)
.order_by(UserCreditBalance.expires_at.asc(), UserCreditBalance.id.asc())
.limit(max(1, limit))
.with_for_update()
)
balances = list(result.scalars().all())
if not balances:
return 0
current_available = await get_available_credits(db, user_id, request_time=checked_at)
for balance in balances:
amount = to_credit_decimal(balance.unspent_amount)
if amount <= 0:
continue
before_consumed = to_credit_decimal(balance.consumed_amount)
record = CreditRecord(
id=generate_id(),
user_id=user_id,
type=CreditRecordType.EXPIRE.value,
amount=-amount,
balance_delta=Decimal("0.00"),
expired_amount=amount,
balance_after=current_available,
description=f"积分到期:{float(amount):.2f}",
related_id=balance.id,
request_time=checked_at,
biz_key=f"credit-balance:{balance.id}:expire",
billing_scene=CreditRecordBillingScene.CREDIT_EXPIRE.value,
credit_level_snapshot=balance.credit_level,
)
db.add(record)
await db.flush()
db.add(
CreditRecordAllocation(
id=generate_id(),
credit_record_id=record.id,
credit_balance_id=balance.id,
user_id=user_id,
allocation_action=CreditAllocationAction.EXPIRE.value,
amount=amount,
request_time=checked_at,
credit_level_snapshot=balance.credit_level,
source_type_snapshot=balance.source_type,
source_id_snapshot=balance.source_id,
valid_from_snapshot=balance.valid_from,
expires_at_snapshot=balance.expires_at,
unspent_before=amount,
unspent_after=Decimal("0.00"),
consumed_before=before_consumed,
consumed_after=before_consumed,
)
)
balance.unspent_amount = Decimal("0.00")
balance.expired_amount = to_credit_decimal(balance.expired_amount) + amount
balance.expired_processed_at = checked_at
balance.status = CreditBalanceStatus.EXPIRED.value
await db.flush()
return len(balances)
async def list_expired_balance_user_limits(
db: AsyncSession,
*,
request_time: datetime,
batch_size: int = 500,
) -> list[tuple[str, int]]:
result = await db.execute(
select(UserCreditBalance.id, UserCreditBalance.user_id)
.where(
UserCreditBalance.expires_at <= request_time,
UserCreditBalance.unspent_amount > 0,
UserCreditBalance.expired_processed_at.is_(None),
UserCreditBalance.revoked_at.is_(None),
)
.order_by(UserCreditBalance.expires_at.asc(), UserCreditBalance.id.asc())
.limit(max(1, batch_size))
)
selected = [(str(row.id), str(row.user_id)) for row in result.all()]
per_user_limit = Counter(user_id for _, user_id in selected)
return sorted(per_user_limit.items(), key=lambda item: item[0])
async def archive_expired_balances_batch(
db: AsyncSession,
*,
request_time: datetime | None = None,
batch_size: int = 500,
) -> int:
checked_at = request_time or utc_now()
user_limits = await list_expired_balance_user_limits(
db, request_time=checked_at, batch_size=batch_size
)
processed = 0
for user_id, user_limit in user_limits:
processed += await archive_expired_user_balances(
db,
user_id=user_id,
request_time=checked_at,
limit=user_limit,
)
return processed