- 根因: CreditReservation.task 是 OneToOneField(CASCADE), AITask.project 也是 CASCADE
—— 删项目→级联删任务→ACTIVE 预留行被吞, 账户冻结份额永不归还(实测 4 账户漂移共 ¥42,
且随删项目持续增长)
- 修法: billing/signals.py 挂 AITask pre_delete, 删除前先 release_credit 释放(级联同样
触发, release 自带行锁+幂等); apps.py ready() 接线
- 回归测试 x3: 直删任务/删项目级联/已扣费不重复释放
- qa/repair_reserved_drift.py: 存量漂移回填脚本(dry-run 默认, --apply 落 RELEASE 流水),
已对线上 4 账户执行并复验清零
- qa/function-audit/output: 本轮 18 页模拟点击审计报告(0 真死按钮 0 JS 报错)
验证: 后端测试 213/213 通过, django check 无警告
补齐 aab4d7e: 该提交带走了 signals.py 之外的测试轮改动, 信号挂载点与回归测试在本提交收口
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
26 lines
1.3 KiB
Python
26 lines
1.3 KiB
Python
"""跨 app 账目守护信号。
|
|
|
|
CreditReservation.task 是 OneToOneField(on_delete=CASCADE):AITask 被硬删(用户删项目
|
|
→ AITask.project CASCADE 级联删任务、或直接删任务)时,预留行会被一并删掉。若该预留
|
|
还处于 ACTIVE,account.reserved_balance 里冻结的份额就永远退不回来 —— 这是资金审计
|
|
I2「冻结不平」的根因(ZWQ-Test / 演示团队实测漂移 ¥10 / ¥30,且随测试删项目持续增长)。
|
|
|
|
修法:在 AITask 删除前(pre_delete,级联删除同样触发)把仍 ACTIVE 的预留正常走
|
|
release_credit 释放 —— 退还冻结、落 RELEASE 流水,然后再让级联删掉预留行。
|
|
release_credit 自带行锁 + 幂等(非 ACTIVE 直接返回),并发安全。
|
|
"""
|
|
from django.db.models.signals import pre_delete
|
|
from django.dispatch import receiver
|
|
|
|
|
|
@receiver(pre_delete, sender="ai.AITask", dispatch_uid="billing.release_reservation_before_task_delete")
|
|
def release_reservation_before_task_delete(sender, instance, **kwargs):
|
|
from apps.billing.models import CreditReservation
|
|
from apps.billing.services.ledger import release_credit
|
|
|
|
reservation = CreditReservation.objects.filter(
|
|
task=instance, status=CreditReservation.Status.ACTIVE
|
|
).first()
|
|
if reservation is not None:
|
|
release_credit(reservation=reservation, reason="任务删除,释放未结预留")
|