Files
koc-loop/lib/distribution-release-service.ts
ABAPPLO e2a93e22cf feat: 任务模型物理合并,发布+回收并为单任务双阶段
- tasks 新增 recovery_enabled/recovery_due_at/recovery_instructions/recovery_example_images,回收挂自己名下(先发布后交截图),不再置 complete
- 存量 screenshot_collect 迁移并入父任务后删除(mysql/0020 含 DELETE,生产执行前务必备份)
- 新 action task_enable_recovery:ghost 面板一键开通回收,截止默认=发布截止
- 企微催办双阶段(待发布/截图待提交);超期自动释放仅发布阶段
- admin 删 groupTaskMasters 聚合改单卡双面板;portal 详情页回收截图区块+批次回收进度
- 测试 309 全绿 + Playwright E2E 20/20
2026-09-24 15:52:45 +08:00

154 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { DatabaseClient } from "./database";
import { refreshTaskStatus } from "./task-service";
type ReleasableDistribution = {
id: string;
task_id: string;
content_id: string;
content_title: string;
partner_id: string;
partner_name: string;
claim_id: string | null;
delegation_bundle_id: string | null;
publish_url: string | null;
result_submitted_at: string | null;
};
export class DistributionReleaseError extends Error {
readonly status: number;
constructor(message: string, status: number) {
super(message);
this.name = "DistributionReleaseError";
this.status = status;
}
}
export function distributionReleaseBlockReason(input: {
publishUrl?: string | null;
resultSubmittedAt?: string | null;
}) {
if (input.publishUrl) return "已回填发布链接的笔记不能释放";
if (input.resultSubmittedAt) return "已提交结果截图的任务不能释放";
return null;
}
export async function releaseUnfinishedDistribution(
database: DatabaseClient,
distributionId: string,
) {
if (!distributionId) {
throw new DistributionReleaseError("请选择需要释放的领取记录", 400);
}
const result = await database.transaction(async (db) => {
const distribution = await db
.prepare(
`SELECT
d.id,
d.task_id,
d.content_id,
c.title AS content_title,
d.partner_id,
p.name AS partner_name,
d.claim_id,
d.delegation_bundle_id,
d.publish_url,
d.result_submitted_at
FROM distributions d
JOIN tasks t ON t.id = d.task_id
JOIN contents c ON c.id = d.content_id
JOIN partners p ON p.id = d.partner_id
WHERE d.id = ?
FOR UPDATE`,
)
.bind(distributionId)
.first<ReleasableDistribution>();
if (!distribution) {
throw new DistributionReleaseError("领取记录不存在或已被释放", 404);
}
const blocked = distributionReleaseBlockReason({
publishUrl: distribution.publish_url,
resultSubmittedAt: distribution.result_submitted_at,
});
if (blocked) throw new DistributionReleaseError(blocked, 409);
await db
.prepare("DELETE FROM collection_runs WHERE distribution_id = ?")
.bind(distribution.id)
.run();
await db
.prepare("DELETE FROM distributions WHERE id = ?")
.bind(distribution.id)
.run();
await db
.prepare("UPDATE contents SET status = 'available' WHERE id = ?")
.bind(distribution.content_id)
.run();
await db
.prepare(
`UPDATE tasks
SET claimed_quantity = GREATEST(claimed_quantity - 1, 0)
WHERE id = ?`,
)
.bind(distribution.task_id)
.run();
await db
.prepare(
`UPDATE partners
SET claimed_total = GREATEST(claimed_total - 1, 0)
WHERE id = ?`,
)
.bind(distribution.partner_id)
.run();
if (distribution.delegation_bundle_id) {
await db
.prepare(
`UPDATE delegation_bundles
SET quantity = GREATEST(quantity - 1, 0),
status = CASE WHEN quantity <= 1 THEN 'revoked' ELSE status END,
revoked_at = CASE WHEN quantity <= 1 THEN CURRENT_TIMESTAMP ELSE revoked_at END,
updated_at = CURRENT_TIMESTAMP
WHERE id = ?`,
)
.bind(distribution.delegation_bundle_id)
.run();
}
if (distribution.claim_id) {
await db
.prepare(
`UPDATE claims
SET quantity = GREATEST(quantity - 1, 0)
WHERE id = ?`,
)
.bind(distribution.claim_id)
.run();
await db
.prepare(
`DELETE FROM claims
WHERE id = ?
AND quantity <= 0
AND NOT EXISTS (
SELECT 1 FROM distributions WHERE claim_id = ?
)`,
)
.bind(distribution.claim_id, distribution.claim_id)
.run();
}
return {
distributionId: distribution.id,
taskId: distribution.task_id,
contentId: distribution.content_id,
contentTitle: distribution.content_title,
partnerName: distribution.partner_name,
};
});
// 释放会让任务从 completed 回到 active(content 重新 available)
await refreshTaskStatus(result.taskId, database).catch(() => undefined);
return result;
}