feat: 完善视频任务与 KOC 资源库
This commit is contained in:
@@ -4,7 +4,10 @@ import type { DatabaseStatement } from "../../../lib/database";
|
||||
|
||||
const env = getRuntimeEnv();
|
||||
import { enrichDistributionAccount } from "../../../lib/account-enrichment-service";
|
||||
import { createCollectionRunTasks } from "../../../lib/collection-service";
|
||||
import {
|
||||
createCollectionRunTasks,
|
||||
runDueScheduledCollections,
|
||||
} from "../../../lib/collection-service";
|
||||
import {
|
||||
resolveCollectionMcpConfig,
|
||||
type CollectionMcpBindings,
|
||||
@@ -17,8 +20,8 @@ import {
|
||||
} from "../../../lib/mvp-db";
|
||||
import {
|
||||
accountFromPublishLink,
|
||||
extractXhsPublishUrl,
|
||||
} from "../../../lib/partner-utils";
|
||||
import { extractPublishUrl } from "../../../lib/publish-url";
|
||||
import { parseClaimantIdentifier } from "../../../lib/claimant-identifier";
|
||||
import {
|
||||
partnerOptions,
|
||||
@@ -101,23 +104,28 @@ function publicImageAssets(value: unknown): ImageAsset[] {
|
||||
}
|
||||
}
|
||||
|
||||
type PartnerTask = {
|
||||
id: string;
|
||||
name: string;
|
||||
brand: string;
|
||||
quantity: number;
|
||||
claimed_quantity: number;
|
||||
due_at: string;
|
||||
status: string;
|
||||
task_type: string;
|
||||
platform: string;
|
||||
content_format: string;
|
||||
};
|
||||
|
||||
async function findTask(taskToken: string) {
|
||||
return getRawDb()
|
||||
.prepare(
|
||||
`SELECT id, name, brand, quantity, claimed_quantity, due_at, status, task_type
|
||||
`SELECT id, name, brand, quantity, claimed_quantity, due_at, status,
|
||||
task_type, platform, content_format
|
||||
FROM tasks WHERE share_token = ?`,
|
||||
)
|
||||
.bind(taskToken)
|
||||
.first<{
|
||||
id: string;
|
||||
name: string;
|
||||
brand: string;
|
||||
quantity: number;
|
||||
claimed_quantity: number;
|
||||
due_at: string;
|
||||
status: string;
|
||||
task_type: string;
|
||||
}>();
|
||||
.first<PartnerTask>();
|
||||
}
|
||||
|
||||
async function findDelegationAccess(delegationToken: string) {
|
||||
@@ -132,6 +140,8 @@ async function findDelegationAccess(delegationToken: string) {
|
||||
t.due_at,
|
||||
t.status,
|
||||
t.task_type,
|
||||
t.platform,
|
||||
t.content_format,
|
||||
b.id AS bundle_id,
|
||||
b.label AS bundle_label,
|
||||
b.quantity AS bundle_quantity,
|
||||
@@ -141,15 +151,7 @@ async function findDelegationAccess(delegationToken: string) {
|
||||
WHERE b.share_token = ? AND b.status = 'active'`,
|
||||
)
|
||||
.bind(delegationToken)
|
||||
.first<{
|
||||
id: string;
|
||||
name: string;
|
||||
brand: string;
|
||||
quantity: number;
|
||||
claimed_quantity: number;
|
||||
due_at: string;
|
||||
status: string;
|
||||
task_type: string;
|
||||
.first<PartnerTask & {
|
||||
bundle_id: string;
|
||||
bundle_label: string;
|
||||
bundle_quantity: number;
|
||||
@@ -172,13 +174,15 @@ async function findAccessibleAssignment(
|
||||
d.publish_screenshot_key,
|
||||
d.screenshot_key,
|
||||
d.result_screenshot_key,
|
||||
d.result_submitted_at`;
|
||||
d.result_submitted_at,
|
||||
c.claimant_name`;
|
||||
if (delegationToken) {
|
||||
return db
|
||||
.prepare(
|
||||
`${select}
|
||||
FROM distributions d
|
||||
JOIN delegation_bundles b ON b.id = d.delegation_bundle_id
|
||||
JOIN claims c ON c.id = d.claim_id
|
||||
WHERE d.id = ?
|
||||
AND b.share_token = ?
|
||||
AND b.task_id = ?
|
||||
@@ -194,6 +198,7 @@ async function findAccessibleAssignment(
|
||||
screenshot_key: string | null;
|
||||
result_screenshot_key: string | null;
|
||||
result_submitted_at: string | null;
|
||||
claimant_name: string;
|
||||
}>();
|
||||
}
|
||||
if (!claimToken) return null;
|
||||
@@ -214,6 +219,7 @@ async function findAccessibleAssignment(
|
||||
screenshot_key: string | null;
|
||||
result_screenshot_key: string | null;
|
||||
result_submitted_at: string | null;
|
||||
claimant_name: string;
|
||||
}>();
|
||||
}
|
||||
|
||||
@@ -284,6 +290,7 @@ async function handleGet(request: Request) {
|
||||
c.body,
|
||||
c.source_row,
|
||||
c.image_assets,
|
||||
c.video_assets,
|
||||
a.nickname AS account_nickname,
|
||||
b.id AS delegation_bundle_id,
|
||||
b.label AS delegation_label
|
||||
@@ -326,7 +333,9 @@ async function handleGet(request: Request) {
|
||||
assignments: assignments.results.map((assignment) => ({
|
||||
...assignment,
|
||||
images: publicImageAssets(assignment.image_assets),
|
||||
videos: publicImageAssets(assignment.video_assets),
|
||||
image_assets: undefined,
|
||||
video_assets: undefined,
|
||||
})),
|
||||
delegations: delegations.results,
|
||||
};
|
||||
@@ -349,6 +358,7 @@ async function handleGet(request: Request) {
|
||||
c.body,
|
||||
c.source_row,
|
||||
c.image_assets,
|
||||
c.video_assets,
|
||||
a.nickname AS account_nickname
|
||||
FROM distributions d
|
||||
JOIN contents c ON c.id = d.content_id
|
||||
@@ -366,7 +376,9 @@ async function handleGet(request: Request) {
|
||||
assignments: assignments.results.map((assignment) => ({
|
||||
...assignment,
|
||||
images: publicImageAssets(assignment.image_assets),
|
||||
videos: publicImageAssets(assignment.video_assets),
|
||||
image_assets: undefined,
|
||||
video_assets: undefined,
|
||||
})),
|
||||
};
|
||||
}
|
||||
@@ -379,6 +391,8 @@ async function handleGet(request: Request) {
|
||||
dueAt: task.due_at,
|
||||
status: task.status,
|
||||
type: task.task_type,
|
||||
platform: task.platform,
|
||||
contentFormat: task.content_format,
|
||||
}
|
||||
: {
|
||||
name: task.name,
|
||||
@@ -388,6 +402,8 @@ async function handleGet(request: Request) {
|
||||
dueAt: task.due_at,
|
||||
status: task.status,
|
||||
type: task.task_type,
|
||||
platform: task.platform,
|
||||
contentFormat: task.content_format,
|
||||
availableQuantity: available?.count ?? 0,
|
||||
},
|
||||
claim,
|
||||
@@ -798,14 +814,15 @@ async function handlePost(request: Request) {
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
const publishUrl = extractXhsPublishUrl(publishInput);
|
||||
const platform = task.platform === "抖音" ? "抖音" : "小红书";
|
||||
const publishUrl = extractPublishUrl(publishInput, platform);
|
||||
if (!publishUrl) {
|
||||
return Response.json(
|
||||
{ error: "请粘贴包含小红书长链或短链的分享内容" },
|
||||
{ error: `请粘贴包含${platform}作品链接的分享内容` },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
const account = accountFromPublishLink(publishUrl);
|
||||
const account = accountFromPublishLink(publishUrl, platform);
|
||||
if (!account) {
|
||||
return Response.json({ error: "发布链接格式不正确" }, { status: 400 });
|
||||
}
|
||||
@@ -816,28 +833,69 @@ async function handlePost(request: Request) {
|
||||
delegationToken,
|
||||
);
|
||||
if (!assignment) {
|
||||
return Response.json({ error: "笔记与领取凭证不匹配" }, { status: 403 });
|
||||
return Response.json({ error: "作品与领取凭证不匹配" }, { status: 403 });
|
||||
}
|
||||
if (!assignment.publish_screenshot_key) {
|
||||
return Response.json({ error: "请先上传发布截图" }, { status: 400 });
|
||||
}
|
||||
const reuseExistingAccount =
|
||||
assignment.publish_url === publishUrl && assignment.account_id;
|
||||
const matchedAccount = reuseExistingAccount
|
||||
? null
|
||||
: await db
|
||||
.prepare(
|
||||
`SELECT id FROM accounts
|
||||
WHERE platform = ? AND platform_uid = ?
|
||||
LIMIT 1`,
|
||||
)
|
||||
.bind(account.platform, account.platformUid)
|
||||
.first<{ id: string }>();
|
||||
const accountId =
|
||||
reuseExistingAccount ||
|
||||
matchedAccount?.id ||
|
||||
`account-${hashText(`${account.platform}:${account.platformUid}`)}`;
|
||||
const statements: DatabaseStatement[] = [];
|
||||
const publishUrlChanged = Boolean(
|
||||
assignment.publish_url && assignment.publish_url !== publishUrl,
|
||||
);
|
||||
if (publishUrlChanged) {
|
||||
statements.push(
|
||||
db
|
||||
.prepare(
|
||||
`UPDATE distributions SET
|
||||
d2_likes = NULL, d2_comments = NULL, d2_collects = NULL,
|
||||
d5_likes = NULL, d5_comments = NULL, d5_collects = NULL,
|
||||
d7_likes = NULL, d7_comments = NULL, d7_collects = NULL,
|
||||
latest_likes = NULL, latest_comments = NULL,
|
||||
latest_collects = NULL, latest_shares = NULL,
|
||||
collection_status = 'pending',
|
||||
collection_status_description = '发布链接已更新,等待重新采集',
|
||||
collection_updated_at = NULL, last_collection_day = NULL,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?`,
|
||||
)
|
||||
.bind(assignment.id),
|
||||
db
|
||||
.prepare("DELETE FROM collection_runs WHERE distribution_id = ?")
|
||||
.bind(assignment.id),
|
||||
);
|
||||
}
|
||||
if (!reuseExistingAccount) {
|
||||
statements.push(
|
||||
db
|
||||
.prepare(
|
||||
`INSERT INTO accounts
|
||||
(id, platform, platform_uid, nickname, profile_url, post_count)
|
||||
VALUES (?, ?, ?, ?, ?, 1)
|
||||
(id, platform, platform_uid, nickname, profile_url,
|
||||
current_contact, post_count)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 1)
|
||||
ON CONFLICT(platform, platform_uid) DO UPDATE SET
|
||||
nickname = excluded.nickname,
|
||||
profile_url = excluded.profile_url,
|
||||
post_count = accounts.post_count + ?,
|
||||
current_contact = CASE
|
||||
WHEN excluded.current_contact != ''
|
||||
THEN excluded.current_contact
|
||||
ELSE accounts.current_contact
|
||||
END,
|
||||
last_seen_at = CURRENT_TIMESTAMP`,
|
||||
)
|
||||
.bind(
|
||||
@@ -846,7 +904,7 @@ async function handlePost(request: Request) {
|
||||
account.platformUid,
|
||||
account.nickname,
|
||||
account.profileUrl,
|
||||
assignment.publish_url ? 0 : 1,
|
||||
assignment.claimant_name,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -863,6 +921,26 @@ async function handlePost(request: Request) {
|
||||
)
|
||||
.bind(accountId, publishUrl, assignment.id),
|
||||
);
|
||||
statements.push(
|
||||
db
|
||||
.prepare(
|
||||
`UPDATE accounts SET post_count = (
|
||||
SELECT COUNT(*) FROM distributions WHERE account_id = ?
|
||||
) WHERE id = ?`,
|
||||
)
|
||||
.bind(accountId, accountId),
|
||||
);
|
||||
if (assignment.account_id && assignment.account_id !== accountId) {
|
||||
statements.push(
|
||||
db
|
||||
.prepare(
|
||||
`UPDATE accounts SET post_count = (
|
||||
SELECT COUNT(*) FROM distributions WHERE account_id = ?
|
||||
) WHERE id = ?`,
|
||||
)
|
||||
.bind(assignment.account_id, assignment.account_id),
|
||||
);
|
||||
}
|
||||
if (!assignment.publish_url) {
|
||||
statements.push(
|
||||
db
|
||||
@@ -896,26 +974,46 @@ async function handlePost(request: Request) {
|
||||
collectionDays = [];
|
||||
}
|
||||
if (collectionDays.length > 0) {
|
||||
await db
|
||||
.prepare(
|
||||
`UPDATE distributions SET collection_status = 'scheduled',
|
||||
collection_status_description = ? WHERE id = ?`,
|
||||
)
|
||||
.bind(
|
||||
`已安排${collectionDays.length}个采集日,每日09:00执行`,
|
||||
assignment.id,
|
||||
)
|
||||
.run();
|
||||
await createCollectionRunTasks(
|
||||
db,
|
||||
task.id,
|
||||
collectionSchedule.collection_start_date,
|
||||
collectionDays,
|
||||
);
|
||||
runInBackground(
|
||||
runDueScheduledCollections(
|
||||
db,
|
||||
Date.now(),
|
||||
resolveCollectionMcpConfig(
|
||||
env as unknown as CollectionMcpBindings,
|
||||
),
|
||||
"catchup",
|
||||
task.id,
|
||||
).catch(() => undefined),
|
||||
"collection catchup after partner publish update",
|
||||
);
|
||||
}
|
||||
}
|
||||
if (account.platform === "小红书") {
|
||||
const enrichment = enrichDistributionAccount(
|
||||
db,
|
||||
assignment.id,
|
||||
publishUrl,
|
||||
account.nickname,
|
||||
resolveCollectionMcpConfig(
|
||||
env as unknown as CollectionMcpBindings,
|
||||
),
|
||||
).catch(() => undefined);
|
||||
runInBackground(enrichment, "distribution account enrichment");
|
||||
}
|
||||
const enrichment = enrichDistributionAccount(
|
||||
db,
|
||||
assignment.id,
|
||||
publishUrl,
|
||||
account.nickname,
|
||||
resolveCollectionMcpConfig(
|
||||
env as unknown as CollectionMcpBindings,
|
||||
),
|
||||
).catch(() => undefined);
|
||||
runInBackground(enrichment, "distribution account enrichment");
|
||||
return Response.json({ ok: true });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user