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:
@@ -878,6 +878,9 @@ export const adminApi = {
|
||||
retryTask(id: string) {
|
||||
return request<{ retried: boolean; task_id: string }>(`/api/admin/tasks/${id}/retry/`, { method: "POST" });
|
||||
},
|
||||
reapTask(id: string) {
|
||||
return request<AdminTask>(`/api/admin/tasks/${id}/reap/`, { method: "POST" });
|
||||
},
|
||||
ledgers(params?: { ledger_type?: string; team?: string; page?: number; page_size?: number }) {
|
||||
const qs = new URLSearchParams();
|
||||
if (params?.ledger_type) qs.set("ledger_type", params.ledger_type);
|
||||
|
||||
@@ -84,6 +84,22 @@ export function AdminTasksPage({ notify }: { notify: Notify }) {
|
||||
}
|
||||
}
|
||||
|
||||
// 手动回收僵尸任务:卡在「已预留」超 10 分钟(worker 从未认领)→ 标失败 + 退还冻结积分。
|
||||
// 自动回收只在同团队下次提交时触发,团队弃用该功能时积分会永久冻结,这是超管兜底出口。
|
||||
async function reap(t: AdminTask) {
|
||||
if (busy) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
await adminApi.reapTask(t.id);
|
||||
notify("success", "已回收:任务标记失败,预留积分已退还");
|
||||
await load();
|
||||
} catch (e) {
|
||||
notify("error", e instanceof Error ? e.message : "回收失败");
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="page-head">
|
||||
@@ -131,6 +147,9 @@ export function AdminTasksPage({ notify }: { notify: Notify }) {
|
||||
{t.status === "failed" && (
|
||||
<button className="btn btn-sm btn-ghost" type="button" disabled={busy} onClick={() => retry(t)}>重投</button>
|
||||
)}
|
||||
{t.reapable && (
|
||||
<button className="btn btn-sm btn-ghost danger" type="button" disabled={busy} title="卡死任务:标记失败并退还预留积分" onClick={() => reap(t)}>回收退款</button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
@@ -129,6 +129,8 @@ export type AdminTask = {
|
||||
margin_yuan?: string | null;
|
||||
cost_anomaly: boolean;
|
||||
error_code: string;
|
||||
// 卡在 RESERVED 超 10 分钟 = 僵尸,可手动「回收退款」(与后端 admin_task_reap 闸同口径)
|
||||
reapable?: boolean;
|
||||
created_at: string;
|
||||
};
|
||||
export type AdminTaskDetail = AdminTask & {
|
||||
|
||||
Reference in New Issue
Block a user