feat: 任务领取/找回支持企微身份票据,领取自动绑定催办通道
This commit is contained in:
@@ -23,6 +23,11 @@ import {
|
||||
} from "../../../lib/partner-utils";
|
||||
import { extractPublishUrl } from "../../../lib/publish-url";
|
||||
import { parseClaimantIdentifier } from "../../../lib/claimant-identifier";
|
||||
import {
|
||||
resolveWecomClaimant,
|
||||
verifyWecomIdentityTicket,
|
||||
type WecomIdentity,
|
||||
} from "../../../lib/wecom-oauth-service";
|
||||
import {
|
||||
partnerOptions,
|
||||
withPartnerCors,
|
||||
@@ -39,6 +44,7 @@ type PartnerBody = {
|
||||
delegationBundleId?: string;
|
||||
claimantIdentifier?: string;
|
||||
claimantName?: string;
|
||||
identityTicket?: string;
|
||||
quantity?: number;
|
||||
publishUrl?: string;
|
||||
exposure?: number | string;
|
||||
@@ -446,13 +452,27 @@ async function handlePost(request: Request) {
|
||||
}
|
||||
const db = getRawDb();
|
||||
|
||||
let wecomIdentity: WecomIdentity | null = null;
|
||||
if (body.identityTicket) {
|
||||
try {
|
||||
wecomIdentity = await verifyWecomIdentityTicket(
|
||||
textValue(body.identityTicket, 512),
|
||||
);
|
||||
} catch {
|
||||
wecomIdentity = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (body.action === "recover") {
|
||||
const identifierInput = textValue(
|
||||
body.claimantIdentifier ?? body.claimantName,
|
||||
64,
|
||||
);
|
||||
const claimantIdentifier = parseClaimantIdentifier(identifierInput);
|
||||
if (!claimantIdentifier && identifierInput.length < 2) {
|
||||
const wecomPartnerId = wecomIdentity
|
||||
? (await resolveWecomClaimant(db, wecomIdentity.externalUserId)).partnerId
|
||||
: null;
|
||||
if (!claimantIdentifier && identifierInput.length < 2 && !wecomPartnerId) {
|
||||
return Response.json(
|
||||
{ error: "请输入领取时使用的正确微信号或手机号" },
|
||||
{ status: 400 },
|
||||
@@ -466,6 +486,11 @@ async function handlePost(request: Request) {
|
||||
`claimant:${claimantIdentifier.canonical}`,
|
||||
)}`
|
||||
: legacyPartnerId;
|
||||
const partnerCandidates = [
|
||||
...new Set(
|
||||
wecomPartnerId ? [wecomPartnerId] : [partnerId, legacyPartnerId],
|
||||
),
|
||||
];
|
||||
const recovered = await db
|
||||
.prepare(
|
||||
`SELECT
|
||||
@@ -481,12 +506,14 @@ async function handlePost(request: Request) {
|
||||
FROM claims c
|
||||
LEFT JOIN distributions d ON d.claim_id = c.id
|
||||
LEFT JOIN contents co ON co.id = d.content_id
|
||||
WHERE c.task_id = ? AND c.partner_id IN (?, ?)
|
||||
WHERE c.task_id = ? AND c.partner_id IN (${partnerCandidates
|
||||
.map(() => "?")
|
||||
.join(", ")})
|
||||
GROUP BY c.id, c.claim_token, c.quantity, c.created_at
|
||||
ORDER BY c.created_at DESC, c.id DESC
|
||||
LIMIT 20`,
|
||||
)
|
||||
.bind(task.task_type, task.task_type, task.id, partnerId, legacyPartnerId)
|
||||
.bind(task.task_type, task.task_type, task.id, ...partnerCandidates)
|
||||
.all<{
|
||||
claim_token: string;
|
||||
quantity: number;
|
||||
@@ -522,7 +549,7 @@ async function handlePost(request: Request) {
|
||||
);
|
||||
const claimantIdentifier = parseClaimantIdentifier(identifierInput);
|
||||
const quantity = quantityValue(body.quantity);
|
||||
if (!claimantIdentifier) {
|
||||
if (!claimantIdentifier && !wecomIdentity) {
|
||||
return Response.json(
|
||||
{ error: "请输入正确的微信号或手机号" },
|
||||
{ status: 400 },
|
||||
@@ -540,23 +567,45 @@ async function handlePost(request: Request) {
|
||||
if (available.results.length === 0) {
|
||||
return Response.json({ error: "当前任务已领完" }, { status: 409 });
|
||||
}
|
||||
const partnerId = `partner-ext-${hashText(
|
||||
`claimant:${claimantIdentifier.canonical}`,
|
||||
)}`;
|
||||
const claimantName = claimantIdentifier.display;
|
||||
const partnerId = wecomIdentity
|
||||
? (await resolveWecomClaimant(db, wecomIdentity.externalUserId)).partnerId
|
||||
: `partner-ext-${hashText(
|
||||
`claimant:${claimantIdentifier?.canonical}`,
|
||||
)}`;
|
||||
const claimantName = wecomIdentity
|
||||
? wecomIdentity.displayName
|
||||
: (claimantIdentifier?.display ?? "");
|
||||
const claimId = uid("claim");
|
||||
const claimToken = crypto.randomUUID().replaceAll("-", "");
|
||||
const statements: DatabaseStatement[] = [
|
||||
db
|
||||
.prepare(
|
||||
`INSERT INTO partners
|
||||
(id, name, wecom_name, owner, claimed_total, completed_total)
|
||||
VALUES (?, ?, ?, '外部KOC', 0, 0)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
name = excluded.name,
|
||||
wecom_name = excluded.wecom_name`,
|
||||
)
|
||||
.bind(partnerId, claimantName, claimantName),
|
||||
wecomIdentity
|
||||
? db
|
||||
.prepare(
|
||||
`INSERT INTO partners
|
||||
(id, name, wecom_name, owner, claimed_total, completed_total,
|
||||
wecom_external_user_id)
|
||||
VALUES (?, ?, ?, '外部KOC', 0, 0, ?)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
name = excluded.name,
|
||||
wecom_name = excluded.wecom_name,
|
||||
wecom_external_user_id = excluded.wecom_external_user_id`,
|
||||
)
|
||||
.bind(
|
||||
partnerId,
|
||||
claimantName,
|
||||
claimantName,
|
||||
wecomIdentity.externalUserId,
|
||||
)
|
||||
: db
|
||||
.prepare(
|
||||
`INSERT INTO partners
|
||||
(id, name, wecom_name, owner, claimed_total, completed_total)
|
||||
VALUES (?, ?, ?, '外部KOC', 0, 0)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
name = excluded.name,
|
||||
wecom_name = excluded.wecom_name`,
|
||||
)
|
||||
.bind(partnerId, claimantName, claimantName),
|
||||
db
|
||||
.prepare(
|
||||
`INSERT INTO claims
|
||||
|
||||
42
tests/partner-wecom-claim.test.mjs
Normal file
42
tests/partner-wecom-claim.test.mjs
Normal file
@@ -0,0 +1,42 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
const routeSource = readFileSync(
|
||||
new URL("../app/api/partner/route.ts", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
test("partner route accepts identityTicket for wecom-backed claim", () => {
|
||||
assert.match(routeSource, /identityTicket\?: string/);
|
||||
assert.match(routeSource, /verifyWecomIdentityTicket/);
|
||||
assert.match(routeSource, /resolveWecomClaimant/);
|
||||
});
|
||||
|
||||
test("claim keeps manual wechat/phone path untouched when no ticket", () => {
|
||||
assert.match(
|
||||
routeSource,
|
||||
/请输入正确的微信号或手机号/,
|
||||
"manual validation message preserved",
|
||||
);
|
||||
assert.match(
|
||||
routeSource,
|
||||
/VALUES \(\?, \?, \?, '外部KOC', 0, 0\)\s*\n\s*ON CONFLICT\(id\) DO UPDATE SET\s*\n\s*name = excluded\.name,\s*\n\s*wecom_name = excluded\.wecom_name`/,
|
||||
"manual partner upsert statement preserved",
|
||||
);
|
||||
});
|
||||
|
||||
test("claim with ticket upserts wecom_external_user_id for reminders", () => {
|
||||
assert.match(
|
||||
routeSource,
|
||||
/wecom_external_user_id = excluded\.wecom_external_user_id/,
|
||||
);
|
||||
});
|
||||
|
||||
test("recover builds dynamic partner candidates including wecom identity", () => {
|
||||
assert.match(
|
||||
routeSource,
|
||||
/wecomPartnerId \? \[wecomPartnerId\] : \[partnerId, legacyPartnerId\]/,
|
||||
);
|
||||
assert.match(routeSource, /\.bind\(task\.task_type, task\.task_type, task\.id, \.\.\.partnerCandidates\)/);
|
||||
});
|
||||
Reference in New Issue
Block a user