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
This commit is contained in:
ABAPPLO
2026-09-24 15:52:45 +08:00
parent ab78b187c0
commit e2a93e22cf
29 changed files with 2219 additions and 2195 deletions

View File

@@ -40,6 +40,7 @@ type ExportRow = {
publish_url: string | null;
publish_time: string | null;
publish_screenshot_key: string | null;
result_submitted_at: string | null;
screenshot_key: string | null;
exposure: number | null;
views: number | null;
@@ -252,9 +253,17 @@ export async function GET(request: Request) {
}
}
const task = await db
.prepare("SELECT id, name, brand, platform FROM tasks WHERE id = ?")
.prepare(
"SELECT id, name, brand, platform, recovery_enabled FROM tasks WHERE id = ?",
)
.bind(taskId)
.first<{ id: string; name: string; brand: string; platform: string }>();
.first<{
id: string;
name: string;
brand: string;
platform: string;
recovery_enabled: number;
}>();
if (!task) {
return Response.json({ error: "没有找到这个任务" }, { status: 404 });
}
@@ -270,6 +279,7 @@ export async function GET(request: Request) {
d.publish_url,
d.publish_time,
d.publish_screenshot_key,
d.result_submitted_at,
d.screenshot_key,
d.exposure,
d.views,
@@ -304,34 +314,7 @@ export async function GET(request: Request) {
.bind(taskId)
.all<ExportRow>();
const rows = result.results;
const companionTaskRow = await db
.prepare(
"SELECT id FROM tasks WHERE related_task_id = ? AND task_type = 'screenshot_collect' LIMIT 1",
)
.bind(taskId)
.first<{ id: string }>();
let companionSubmittedByContent: Map<string, boolean> | null = null;
if (companionTaskRow) {
const companionRows = await db
.prepare(
`SELECT c.related_content_id, d.result_submitted_at
FROM distributions d
JOIN contents c ON c.id = d.content_id
WHERE d.task_id = ? AND c.related_content_id != ''`,
)
.bind(companionTaskRow.id)
.all<{
related_content_id: string;
result_submitted_at: string | null;
}>();
companionSubmittedByContent = new Map<string, boolean>();
for (const row of companionRows.results) {
companionSubmittedByContent.set(
row.related_content_id,
Boolean(row.result_submitted_at),
);
}
}
const recoveryEnabled = Number(task.recovery_enabled) === 1;
const assetsByContent = new Map(
rows.map((row) => [row.content_id, parseAssets(row.image_assets)]),
);
@@ -384,7 +367,7 @@ export async function GET(request: Request) {
"采集状态",
"合作方",
"领取人微信号/手机号",
...(companionSubmittedByContent ? ["收录截图"] : []),
...(recoveryEnabled ? ["收录截图"] : []),
];
const originalImageStart = 3;
const accountColumn = originalImageStart + maxContentImages;
@@ -427,10 +410,10 @@ export async function GET(request: Request) {
row.distribution_id ? collectionLabel(row) : "未领取",
row.partner_name || "",
row.claimant_name || "",
...(companionSubmittedByContent
...(recoveryEnabled
? [
companionSubmittedByContent.has(row.content_id)
? companionSubmittedByContent.get(row.content_id)
row.distribution_id
? row.result_submitted_at
? "已提交"
: "未提交"
: "—",
@@ -471,7 +454,7 @@ export async function GET(request: Request) {
24,
18,
22,
...(companionSubmittedByContent ? [12] : []),
...(recoveryEnabled ? [12] : []),
];
const workbook = buildRecoveryWorkbook({
sheetName: `${task.name}-数据回收`,