feat(adminpanel): 任务监控「回收退款」手动兜底
新增 POST /api/admin/tasks/<id>/reap/:仅限卡在「已预留」超 10 分钟的僵尸任务(SUBMITTED/ POLLING 故意不许杀,视频合法耗时 5-10 分钟+,强杀会与轮询结算竞态),标失败 + 退还冻结积分, 与自动回收 _reap_stale_standalone_image_tasks 同款账务路径,幂等。 自动回收只在同团队下次提交时顺手触发,团队弃用某功能后冻结积分会永久躺着——这是给超管的 手动出口。序列化器加 reapable 字段(与端点闸同口径),任务监控行级「回收退款」按钮。 4 条测试(成功退款+审计/窗口内拒绝/非 RESERVED 拒绝/非超管 403)。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -118,18 +118,31 @@ class AdminTaskSerializer(serializers.ModelSerializer):
|
||||
cost_anomaly = serializers.SerializerMethodField()
|
||||
# 单任务毛利(¥):actual_cost(积分)÷汇率 − base_cost。base_cost=0(成本未知)时 None,报表侧过滤
|
||||
margin_yuan = serializers.SerializerMethodField()
|
||||
# 可手动回收:卡在 RESERVED 超过 10 分钟(与 admin_task_reap 的服务端闸完全同口径,前端据此显示按钮)
|
||||
reapable = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = AITask
|
||||
fields = [
|
||||
"id", "task_type", "status", "team", "team_name", "model_name",
|
||||
"estimated_cost", "actual_cost", "base_cost", "margin_yuan", "cost_anomaly", "error_code", "created_at",
|
||||
"estimated_cost", "actual_cost", "base_cost", "margin_yuan", "cost_anomaly", "error_code", "reapable", "created_at",
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
||||
def get_cost_anomaly(self, obj) -> bool:
|
||||
return is_cost_anomaly(obj.estimated_cost, obj.actual_cost)
|
||||
|
||||
def get_reapable(self, obj) -> bool:
|
||||
from datetime import timedelta
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
return bool(
|
||||
obj.status == AITask.Status.RESERVED
|
||||
and obj.updated_at is not None
|
||||
and obj.updated_at < timezone.now() - timedelta(minutes=10)
|
||||
)
|
||||
|
||||
def get_margin_yuan(self, obj) -> str | None:
|
||||
base = obj.base_cost or Decimal("0")
|
||||
actual = obj.actual_cost or Decimal("0")
|
||||
|
||||
Reference in New Issue
Block a user