feat: 采集失败KOC补交截图闭环 + 任务中心分发详情弹窗

公开数据补交闭环(F1):
- distributions 新增 public_metrics_screenshot_key(mysql/0022 迁移)
- KOC 门户:链接已回填但采集失败时展示补交数据截图入口(kind=public-metrics)
- 运营端:回收队列"核对补录"支持公开互动数据(赞/藏/评),按补交截图补录后采集状态置成功
- 企微催办:采集失败纳入待办(公开数据待补交)
- task-result-image 支持 kind=publish 输出发布截图

门户文案(F2):
- 自动采集数据明确"由系统每天自动采集,无需你填写",仅识别失败时才手动填写曝光/阅读

任务中心分发详情:
- 任务中心/数据回收的分发表行可点击,弹窗查看详情
- 详情按任务类型适配:发布阶段/数据回收(自动采集)/创作者数据(第7天)/截图回收四区块三段式布局
This commit is contained in:
ABAPPLO
2026-10-09 17:02:46 +08:00
parent c686003cfb
commit c11afd5f99
15 changed files with 1250 additions and 67 deletions

View File

@@ -656,19 +656,62 @@ export async function POST(request: Request) {
const forbidden = await distributionOwnerGate(body.distributionId);
if (forbidden) return forbidden;
const distributionId = String(body.distributionId ?? "");
const exposure = Math.max(0, numberValue(body.exposure));
const views = Math.max(0, numberValue(body.views));
const hasCreatorMetrics =
body.exposure !== undefined &&
body.exposure !== null &&
body.views !== undefined &&
body.views !== null;
const exposure = hasCreatorMetrics
? Math.max(0, numberValue(body.exposure))
: null;
const views = hasCreatorMetrics ? Math.max(0, numberValue(body.views)) : null;
// 采集失败兜底:运营按 KOC 补交的数据截图核对补录公开互动数据
const hasPublicMetrics =
body.publicLikes !== undefined ||
body.publicCollects !== undefined ||
body.publicComments !== undefined;
const publicLikes = Math.max(0, numberValue(body.publicLikes));
const publicCollects = Math.max(0, numberValue(body.publicCollects));
const publicComments = Math.max(0, numberValue(body.publicComments));
await db
.prepare(
`UPDATE distributions SET
exposure = ?,
views = ?,
ocr_status = 'manual',
exposure = CASE WHEN ? THEN ? ELSE exposure END,
views = CASE WHEN ? THEN ? ELSE views END,
ocr_status = CASE WHEN ? THEN 'manual' ELSE ocr_status END,
status = CASE WHEN exposure IS NOT NULL AND views IS NOT NULL THEN 'complete' ELSE status END,
latest_likes = CASE WHEN ? THEN ? ELSE latest_likes END,
latest_collects = CASE WHEN ? THEN ? ELSE latest_collects END,
latest_comments = CASE WHEN ? THEN ? ELSE latest_comments END,
collection_status = CASE WHEN ? THEN 'success' ELSE collection_status END,
collection_status_description = CASE
WHEN ? THEN '运营已按补交截图核对补录'
ELSE collection_status_description
END,
collection_updated_at = CASE
WHEN ? THEN CURRENT_TIMESTAMP
ELSE collection_updated_at
END,
updated_at = CURRENT_TIMESTAMP
WHERE id = ?`,
)
.bind(exposure, views, distributionId)
.bind(
hasCreatorMetrics ? 1 : 0,
exposure,
hasCreatorMetrics ? 1 : 0,
views,
hasCreatorMetrics ? 1 : 0,
hasPublicMetrics ? 1 : 0,
publicLikes,
hasPublicMetrics ? 1 : 0,
publicCollects,
hasPublicMetrics ? 1 : 0,
publicComments,
hasPublicMetrics ? 1 : 0,
hasPublicMetrics ? 1 : 0,
hasPublicMetrics ? 1 : 0,
distributionId,
)
.run();
const ownerTask = await db
.prepare("SELECT task_id FROM distributions WHERE id = ?")

View File

@@ -10,20 +10,33 @@ export async function GET(request: Request) {
const isAdmin = await isAdminRequest(request);
try {
await ensureSchema();
const distributionId = new URL(request.url).searchParams
.get("distribution")
?.trim();
const url = new URL(request.url);
const distributionId = url.searchParams.get("distribution")?.trim();
// kind=public-metrics 时返回 KOC 补交的公开数据截图(仅后台核对用)
const imageKind = url.searchParams.get("kind") === "public-metrics"
? "public-metrics"
: "creator";
if (!distributionId) {
return Response.json({ error: "缺少分发记录" }, { status: 400 });
}
const row = await getRawDb()
.prepare(
`SELECT screenshot_key FROM distributions
`SELECT screenshot_key, public_metrics_screenshot_key FROM distributions
WHERE id = ?`,
)
.bind(distributionId)
.first<{ screenshot_key: string | null }>();
const mcpToken = new URL(request.url).searchParams.get("mcp_token") || "";
.first<{
screenshot_key: string | null;
public_metrics_screenshot_key: string | null;
}>();
const mcpToken = url.searchParams.get("mcp_token") || "";
const key =
imageKind === "public-metrics"
? row?.public_metrics_screenshot_key ?? null
: row?.screenshot_key ?? null;
if (imageKind === "public-metrics" && !isAdmin) {
return adminForbidden();
}
if (
!isAdmin &&
(!row?.screenshot_key ||
@@ -36,18 +49,20 @@ export async function GET(request: Request) {
return adminForbidden();
}
if (
!row?.screenshot_key ||
!row.screenshot_key.startsWith("creator-center/")
!key ||
!key.startsWith(
imageKind === "public-metrics" ? "public-metrics/" : "creator-center/",
)
) {
return Response.json({ error: "创作者截图不存在" }, { status: 404 });
return Response.json({ error: "截图不存在" }, { status: 404 });
}
const object = await getUploadBucket().get(row.screenshot_key);
const object = await getUploadBucket().get(key);
if (!object) {
return Response.json({ error: "创作者截图不存在" }, { status: 404 });
return Response.json({ error: "截图不存在" }, { status: 404 });
}
const headers = new Headers({
"Cache-Control": "private, no-store",
"Content-Disposition": 'inline; filename="creator-center-screenshot"',
"Content-Disposition": `inline; filename="${imageKind}-screenshot"`,
"X-Content-Type-Options": "nosniff",
});
object.writeHttpMetadata(headers);

View File

@@ -76,6 +76,7 @@ async function handleGet(request: Request) {
d.result_screenshot_key,
d.publish_screenshot_key,
d.screenshot_key,
d.public_metrics_screenshot_key,
t.recovery_example_images
FROM distributions d
JOIN contents c ON c.id = d.content_id
@@ -92,6 +93,7 @@ async function handleGet(request: Request) {
result_screenshot_key: string | null;
publish_screenshot_key: string | null;
screenshot_key: string | null;
public_metrics_screenshot_key: string | null;
recovery_example_images: string;
}>()
: await getRawDb()
@@ -101,6 +103,7 @@ async function handleGet(request: Request) {
d.result_screenshot_key,
d.publish_screenshot_key,
d.screenshot_key,
d.public_metrics_screenshot_key,
t.recovery_example_images
FROM distributions d
JOIN contents c ON c.id = d.content_id
@@ -118,6 +121,7 @@ async function handleGet(request: Request) {
result_screenshot_key: string | null;
publish_screenshot_key: string | null;
screenshot_key: string | null;
public_metrics_screenshot_key: string | null;
recovery_example_images: string;
}>();
const asset =
@@ -142,6 +146,10 @@ async function handleGet(request: Request) {
? row?.screenshot_key?.startsWith("creator-center/")
? { index: 1, key: row.screenshot_key }
: undefined
: imageKind === "public-metrics"
? row?.public_metrics_screenshot_key?.startsWith("public-metrics/")
? { index: 1, key: row.public_metrics_screenshot_key }
: undefined
: imageKind === "video"
? row
? findAsset(row.video_assets, imageIndex, ["content-videos/"])
@@ -196,7 +204,9 @@ async function handleGet(request: Request) {
const headers = new Headers();
object.writeHttpMetadata(headers);
const isMutableEvidence =
imageKind === "publish" || imageKind === "creator";
imageKind === "publish" ||
imageKind === "creator" ||
imageKind === "public-metrics";
headers.set(
"Cache-Control",
isMutableEvidence ? "private, no-store" : "private, max-age=3600",

View File

@@ -87,7 +87,12 @@ async function handlePost(request: Request) {
}
const isCreatorCenter = upload.uploadKind === "creator-center";
const isTaskResult = upload.uploadKind === "task-result";
if (!["publish", "creator-center", "task-result"].includes(upload.uploadKind)) {
const isPublicMetrics = upload.uploadKind === "public-metrics";
if (
!["publish", "creator-center", "task-result", "public-metrics"].includes(
upload.uploadKind,
)
) {
return Response.json({ error: "不支持的截图类型" }, { status: 400 });
}
const assignment = upload.delegationToken
@@ -119,7 +124,7 @@ async function handlePost(request: Request) {
if (!assignment) {
return Response.json({ error: "笔记与领取凭证不匹配" }, { status: 403 });
}
if (isCreatorCenter && !assignment.publish_url) {
if ((isCreatorCenter || isPublicMetrics) && !assignment.publish_url) {
return Response.json(
{ error: "请先回填这篇笔记的发布信息" },
{ status: 409 },
@@ -143,6 +148,8 @@ async function handlePost(request: Request) {
? `task-results/${upload.distributionId}/${uid("shot")}.${extension}`
: isCreatorCenter
? `creator-center/${upload.distributionId}/${uid("shot")}.${extension}`
: isPublicMetrics
? `public-metrics/${upload.distributionId}/${uid("shot")}.${extension}`
: `publish-evidence/${upload.distributionId}/${uid("shot")}.${extension}`;
await getUploadBucket().put(key, upload.fileBytes, {
httpMetadata: { contentType: upload.fileType },
@@ -160,6 +167,18 @@ async function handlePost(request: Request) {
upload.distributionId,
)
.run();
} else if (isPublicMetrics) {
// 公开数据自动采集失败时的兜底凭证:运营据此核对补录互动数据
await getRawDb()
.prepare(
`UPDATE distributions SET
public_metrics_screenshot_key = ?,
collection_status_description = 'KOC已补交数据截图,待运营核对',
updated_at = CURRENT_TIMESTAMP
WHERE id = ?`,
)
.bind(key, upload.distributionId)
.run();
} else if (isCreatorCenter) {
await getRawDb()
.prepare(
@@ -226,7 +245,9 @@ async function handlePost(request: Request) {
? "task-result"
: isCreatorCenter
? "creator-center"
: "publish",
: isPublicMetrics
? "public-metrics"
: "publish",
ocrStatus: isCreatorCenter ? "failed" : undefined,
});
} catch (error) {

View File

@@ -326,6 +326,12 @@ async function handleGet(request: Request) {
d.ocr_status,
d.exposure,
d.views,
d.collection_status,
d.latest_likes,
d.latest_comments,
d.latest_collects,
d.collection_updated_at,
d.public_metrics_screenshot_key,
c.title,
c.body,
c.source_row,
@@ -400,6 +406,12 @@ async function handleGet(request: Request) {
d.ocr_status,
d.exposure,
d.views,
d.collection_status,
d.latest_likes,
d.latest_comments,
d.latest_collects,
d.collection_updated_at,
d.public_metrics_screenshot_key,
c.title,
c.body,
c.source_row,

View File

@@ -7,9 +7,8 @@ export async function GET(request: Request) {
if (!(await isAdminRequest(request))) return adminForbidden();
try {
await ensureSchema();
const distributionId = new URL(request.url).searchParams
.get("distribution")
?.trim();
const url = new URL(request.url);
const distributionId = url.searchParams.get("distribution")?.trim();
const scope = await getOwnerScope(request);
if (
scope.kind === "owner" &&
@@ -17,23 +16,31 @@ export async function GET(request: Request) {
) {
return ownerForbidden();
}
// kind=publish 时返回 KOC 上传的发布截图(分发详情页核对用)
const imageKind = url.searchParams.get("kind") === "publish" ? "publish" : "result";
const imageIndex = Math.max(
1,
Number(new URL(request.url).searchParams.get("index") || 1),
Number(url.searchParams.get("index") || 1),
);
if (!distributionId) {
return Response.json({ error: "缺少任务记录" }, { status: 400 });
}
const row = await getRawDb()
.prepare(
`SELECT result_screenshot_key FROM distributions
`SELECT result_screenshot_key, publish_screenshot_key FROM distributions
WHERE id = ?`,
)
.bind(distributionId)
.first<{ result_screenshot_key: string | null }>();
const screenshotKey = row
? parseResultScreenshotKeys(row.result_screenshot_key)[imageIndex - 1]
: undefined;
.first<{
result_screenshot_key: string | null;
publish_screenshot_key: string | null;
}>();
const screenshotKey =
imageKind === "publish"
? row?.publish_screenshot_key ?? null
: row
? parseResultScreenshotKeys(row.result_screenshot_key)[imageIndex - 1]
: undefined;
if (!screenshotKey) {
return Response.json({ error: "任务截图不存在" }, { status: 404 });
}