All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m37s
消息中心:全量渲染 → 真·后端分页滚动加载 - backend(ops/views): NotificationPagination(10/页,page_size 可覆盖)+ 响应回 type_counts(按收件人绝对计数,不受分页/搜索影响) - frontend(messages): 自管分页,滚到底加载下一批;tab/搜索走服务端并重置到第1页; 代号作废在途旧请求防切换卡空白;乐观标已读;「已加载 X / Y」分母用当前筛选总数 - api/App/types: listNotifications 支持 page/page_size/search;allNotifications 携带 type_counts 命令面板(侧边栏搜索):修复点开后 UI 错位 - app-shell: 遮罩 className 漏了基类 shell-command-bg(只有 .show)致无定位塌到左下; 补回基类 + header 类名对齐 .shell-command-h - messages-page.css: 工作台收进视口高度,收件箱在面板内滚动 本次提交一并带入此前若干未提交 WIP(account/ai-tools/library/pipeline/products/settings + accounts/ai/assets/billing/projects 后端),按用户要求整体推 dev。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
94 lines
3.3 KiB
Python
94 lines
3.3 KiB
Python
from decimal import Decimal
|
|
|
|
from django.db import transaction
|
|
|
|
from apps.billing.models import CreditAccount, CreditLedger, CreditReservation
|
|
|
|
|
|
@transaction.atomic
|
|
def reserve_credit(*, team, user, task, amount: Decimal) -> CreditReservation:
|
|
account, _ = CreditAccount.objects.select_for_update().get_or_create(team=team)
|
|
available = account.balance - account.reserved_balance
|
|
if available < amount:
|
|
raise ValueError("insufficient credit")
|
|
|
|
account.reserved_balance += amount
|
|
account.save(update_fields=["reserved_balance", "updated_at"])
|
|
reservation = CreditReservation.objects.create(
|
|
team=team,
|
|
user=user,
|
|
project=task.project,
|
|
task=task,
|
|
amount=amount,
|
|
)
|
|
CreditLedger.objects.create(
|
|
team=team,
|
|
user=user,
|
|
project=task.project,
|
|
task=task,
|
|
ledger_type=CreditLedger.Type.RESERVE,
|
|
amount=amount,
|
|
balance_after=account.balance,
|
|
reason="AI 任务预扣额度",
|
|
)
|
|
return reservation
|
|
|
|
|
|
@transaction.atomic
|
|
def release_credit(*, reservation: CreditReservation, reason: str = "") -> None:
|
|
account = CreditAccount.objects.select_for_update().get(team=reservation.team)
|
|
if reservation.status != CreditReservation.Status.ACTIVE:
|
|
return
|
|
|
|
account.reserved_balance -= reservation.amount
|
|
account.save(update_fields=["reserved_balance", "updated_at"])
|
|
reservation.status = CreditReservation.Status.RELEASED
|
|
reservation.save(update_fields=["status", "updated_at"])
|
|
CreditLedger.objects.create(
|
|
team=reservation.team,
|
|
user=reservation.user,
|
|
project=reservation.project,
|
|
task=reservation.task,
|
|
ledger_type=CreditLedger.Type.RELEASE,
|
|
amount=reservation.amount,
|
|
balance_after=account.balance,
|
|
reason=reason or "释放预留额度",
|
|
)
|
|
|
|
|
|
@transaction.atomic
|
|
def charge_reserved_credit(*, reservation: CreditReservation, actual_amount: Decimal) -> None:
|
|
account = CreditAccount.objects.select_for_update().get(team=reservation.team)
|
|
if reservation.status != CreditReservation.Status.ACTIVE:
|
|
raise ValueError("reservation is not active")
|
|
if actual_amount > reservation.amount:
|
|
raise ValueError("actual amount exceeds reserved amount")
|
|
|
|
account.balance -= actual_amount
|
|
account.reserved_balance -= reservation.amount
|
|
account.save(update_fields=["balance", "reserved_balance", "updated_at"])
|
|
reservation.status = CreditReservation.Status.CHARGED
|
|
reservation.save(update_fields=["status", "updated_at"])
|
|
CreditLedger.objects.create(
|
|
team=reservation.team,
|
|
user=reservation.user,
|
|
project=reservation.project,
|
|
task=reservation.task,
|
|
ledger_type=CreditLedger.Type.CHARGE,
|
|
amount=actual_amount,
|
|
balance_after=account.balance,
|
|
reason="AI 任务扣费",
|
|
)
|
|
if reservation.amount > actual_amount:
|
|
CreditLedger.objects.create(
|
|
team=reservation.team,
|
|
user=reservation.user,
|
|
project=reservation.project,
|
|
task=reservation.task,
|
|
ledger_type=CreditLedger.Type.RELEASE,
|
|
amount=reservation.amount - actual_amount,
|
|
balance_after=account.balance,
|
|
reason="释放未用预留额度",
|
|
)
|
|
|