Initial commit: KOC LOOP platform
This commit is contained in:
164
app/api/partner-upload/route.ts
Normal file
164
app/api/partner-upload/route.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import {
|
||||
ensureSchema,
|
||||
getRawDb,
|
||||
getUploadBucket,
|
||||
uid,
|
||||
} from "../../../lib/mvp-db";
|
||||
import {
|
||||
partnerOptions,
|
||||
withPartnerCors,
|
||||
} from "../../../lib/partner-cors";
|
||||
|
||||
export const runtime = "edge";
|
||||
|
||||
async function readUpload(request: Request) {
|
||||
const contentType = request.headers.get("content-type") ?? "";
|
||||
if (contentType.startsWith("multipart/form-data")) {
|
||||
const form = await request.formData();
|
||||
const file = form.get("file");
|
||||
if (!(file instanceof File)) return null;
|
||||
return {
|
||||
taskToken: String(form.get("taskToken") ?? "").trim(),
|
||||
claimToken: String(form.get("claimToken") ?? "").trim(),
|
||||
delegationToken: String(form.get("delegationToken") ?? "").trim(),
|
||||
distributionId: String(form.get("distributionId") ?? "").trim(),
|
||||
uploadKind: String(form.get("uploadKind") ?? "publish").trim(),
|
||||
fileName: file.name,
|
||||
fileType: file.type,
|
||||
fileBytes: await file.arrayBuffer(),
|
||||
};
|
||||
}
|
||||
const encodedName = request.headers.get("x-koc-file-name") ?? "screenshot.jpg";
|
||||
let fileName = "screenshot.jpg";
|
||||
try {
|
||||
fileName = decodeURIComponent(encodedName);
|
||||
} catch {
|
||||
fileName = "screenshot.jpg";
|
||||
}
|
||||
return {
|
||||
taskToken: String(request.headers.get("x-koc-task") ?? "").trim(),
|
||||
claimToken: String(request.headers.get("x-koc-claim") ?? "").trim(),
|
||||
delegationToken: String(
|
||||
request.headers.get("x-koc-delegation") ?? "",
|
||||
).trim(),
|
||||
distributionId: String(
|
||||
request.headers.get("x-koc-distribution") ?? "",
|
||||
).trim(),
|
||||
uploadKind: String(
|
||||
request.headers.get("x-koc-upload-kind") ?? "publish",
|
||||
).trim(),
|
||||
fileName,
|
||||
fileType: contentType.split(";")[0].trim(),
|
||||
fileBytes: await request.arrayBuffer(),
|
||||
};
|
||||
}
|
||||
|
||||
async function handlePost(request: Request) {
|
||||
try {
|
||||
await ensureSchema();
|
||||
const upload = await readUpload(request);
|
||||
if (
|
||||
!upload ||
|
||||
(!upload.claimToken && !upload.delegationToken) ||
|
||||
(!upload.delegationToken && !upload.taskToken) ||
|
||||
!upload.distributionId ||
|
||||
upload.fileBytes.byteLength === 0
|
||||
) {
|
||||
return Response.json({ error: "请选择发布截图" }, { status: 400 });
|
||||
}
|
||||
if (
|
||||
!upload.fileType.startsWith("image/") ||
|
||||
upload.fileBytes.byteLength > 8_000_000
|
||||
) {
|
||||
return Response.json(
|
||||
{ error: "仅支持8MB以内的图片" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
const isCreatorCenter = upload.uploadKind === "creator-center";
|
||||
const assignment = upload.delegationToken
|
||||
? await getRawDb()
|
||||
.prepare(
|
||||
`SELECT d.id, d.publish_url
|
||||
FROM distributions d
|
||||
JOIN delegation_bundles b ON b.id = d.delegation_bundle_id
|
||||
WHERE d.id = ?
|
||||
AND b.share_token = ?
|
||||
AND b.status = 'active'`,
|
||||
)
|
||||
.bind(upload.distributionId, upload.delegationToken)
|
||||
.first<{ id: string; publish_url: string | null }>()
|
||||
: await getRawDb()
|
||||
.prepare(
|
||||
`SELECT d.id, d.publish_url
|
||||
FROM distributions d
|
||||
JOIN claims c ON c.id = d.claim_id
|
||||
JOIN tasks t ON t.id = d.task_id
|
||||
WHERE d.id = ?
|
||||
AND c.claim_token = ?
|
||||
AND t.share_token = ?
|
||||
AND c.task_id = t.id`,
|
||||
)
|
||||
.bind(upload.distributionId, upload.claimToken, upload.taskToken)
|
||||
.first<{ id: string; publish_url: string | null }>();
|
||||
if (!assignment) {
|
||||
return Response.json({ error: "笔记与领取凭证不匹配" }, { status: 403 });
|
||||
}
|
||||
if (isCreatorCenter && !assignment.publish_url) {
|
||||
return Response.json(
|
||||
{ error: "请先回填这篇笔记的发布信息" },
|
||||
{ status: 409 },
|
||||
);
|
||||
}
|
||||
const extension =
|
||||
upload.fileName.split(".").at(-1)?.replace(/[^a-zA-Z0-9]/g, "") || "jpg";
|
||||
const key = isCreatorCenter
|
||||
? `creator-center/${upload.distributionId}/${uid("shot")}.${extension}`
|
||||
: `publish-evidence/${upload.distributionId}/${uid("shot")}.${extension}`;
|
||||
await getUploadBucket().put(key, upload.fileBytes, {
|
||||
httpMetadata: { contentType: upload.fileType },
|
||||
});
|
||||
if (isCreatorCenter) {
|
||||
await getRawDb()
|
||||
.prepare(
|
||||
`UPDATE distributions SET
|
||||
screenshot_key = ?,
|
||||
ocr_status = CASE
|
||||
WHEN exposure IS NOT NULL AND views IS NOT NULL THEN ocr_status
|
||||
ELSE 'uploaded'
|
||||
END,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?`,
|
||||
)
|
||||
.bind(key, upload.distributionId)
|
||||
.run();
|
||||
} else {
|
||||
await getRawDb()
|
||||
.prepare(
|
||||
`UPDATE distributions SET
|
||||
publish_screenshot_key = ?,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?`,
|
||||
)
|
||||
.bind(key, upload.distributionId)
|
||||
.run();
|
||||
}
|
||||
return Response.json({
|
||||
uploaded: true,
|
||||
kind: isCreatorCenter ? "creator-center" : "publish",
|
||||
});
|
||||
} catch (error) {
|
||||
return Response.json(
|
||||
{ error: error instanceof Error ? error.message : "上传失败" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
return withPartnerCors(request, await handlePost(request));
|
||||
}
|
||||
|
||||
export async function OPTIONS(request: Request) {
|
||||
return partnerOptions(request);
|
||||
}
|
||||
Reference in New Issue
Block a user