feat: 任务领取/找回支持企微身份票据,领取自动绑定催办通道
This commit is contained in:
@@ -23,6 +23,11 @@ import {
|
|||||||
} from "../../../lib/partner-utils";
|
} from "../../../lib/partner-utils";
|
||||||
import { extractPublishUrl } from "../../../lib/publish-url";
|
import { extractPublishUrl } from "../../../lib/publish-url";
|
||||||
import { parseClaimantIdentifier } from "../../../lib/claimant-identifier";
|
import { parseClaimantIdentifier } from "../../../lib/claimant-identifier";
|
||||||
|
import {
|
||||||
|
resolveWecomClaimant,
|
||||||
|
verifyWecomIdentityTicket,
|
||||||
|
type WecomIdentity,
|
||||||
|
} from "../../../lib/wecom-oauth-service";
|
||||||
import {
|
import {
|
||||||
partnerOptions,
|
partnerOptions,
|
||||||
withPartnerCors,
|
withPartnerCors,
|
||||||
@@ -39,6 +44,7 @@ type PartnerBody = {
|
|||||||
delegationBundleId?: string;
|
delegationBundleId?: string;
|
||||||
claimantIdentifier?: string;
|
claimantIdentifier?: string;
|
||||||
claimantName?: string;
|
claimantName?: string;
|
||||||
|
identityTicket?: string;
|
||||||
quantity?: number;
|
quantity?: number;
|
||||||
publishUrl?: string;
|
publishUrl?: string;
|
||||||
exposure?: number | string;
|
exposure?: number | string;
|
||||||
@@ -446,13 +452,27 @@ async function handlePost(request: Request) {
|
|||||||
}
|
}
|
||||||
const db = getRawDb();
|
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") {
|
if (body.action === "recover") {
|
||||||
const identifierInput = textValue(
|
const identifierInput = textValue(
|
||||||
body.claimantIdentifier ?? body.claimantName,
|
body.claimantIdentifier ?? body.claimantName,
|
||||||
64,
|
64,
|
||||||
);
|
);
|
||||||
const claimantIdentifier = parseClaimantIdentifier(identifierInput);
|
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(
|
return Response.json(
|
||||||
{ error: "请输入领取时使用的正确微信号或手机号" },
|
{ error: "请输入领取时使用的正确微信号或手机号" },
|
||||||
{ status: 400 },
|
{ status: 400 },
|
||||||
@@ -466,6 +486,11 @@ async function handlePost(request: Request) {
|
|||||||
`claimant:${claimantIdentifier.canonical}`,
|
`claimant:${claimantIdentifier.canonical}`,
|
||||||
)}`
|
)}`
|
||||||
: legacyPartnerId;
|
: legacyPartnerId;
|
||||||
|
const partnerCandidates = [
|
||||||
|
...new Set(
|
||||||
|
wecomPartnerId ? [wecomPartnerId] : [partnerId, legacyPartnerId],
|
||||||
|
),
|
||||||
|
];
|
||||||
const recovered = await db
|
const recovered = await db
|
||||||
.prepare(
|
.prepare(
|
||||||
`SELECT
|
`SELECT
|
||||||
@@ -481,12 +506,14 @@ async function handlePost(request: Request) {
|
|||||||
FROM claims c
|
FROM claims c
|
||||||
LEFT JOIN distributions d ON d.claim_id = c.id
|
LEFT JOIN distributions d ON d.claim_id = c.id
|
||||||
LEFT JOIN contents co ON co.id = d.content_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
|
GROUP BY c.id, c.claim_token, c.quantity, c.created_at
|
||||||
ORDER BY c.created_at DESC, c.id DESC
|
ORDER BY c.created_at DESC, c.id DESC
|
||||||
LIMIT 20`,
|
LIMIT 20`,
|
||||||
)
|
)
|
||||||
.bind(task.task_type, task.task_type, task.id, partnerId, legacyPartnerId)
|
.bind(task.task_type, task.task_type, task.id, ...partnerCandidates)
|
||||||
.all<{
|
.all<{
|
||||||
claim_token: string;
|
claim_token: string;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
@@ -522,7 +549,7 @@ async function handlePost(request: Request) {
|
|||||||
);
|
);
|
||||||
const claimantIdentifier = parseClaimantIdentifier(identifierInput);
|
const claimantIdentifier = parseClaimantIdentifier(identifierInput);
|
||||||
const quantity = quantityValue(body.quantity);
|
const quantity = quantityValue(body.quantity);
|
||||||
if (!claimantIdentifier) {
|
if (!claimantIdentifier && !wecomIdentity) {
|
||||||
return Response.json(
|
return Response.json(
|
||||||
{ error: "请输入正确的微信号或手机号" },
|
{ error: "请输入正确的微信号或手机号" },
|
||||||
{ status: 400 },
|
{ status: 400 },
|
||||||
@@ -540,23 +567,45 @@ async function handlePost(request: Request) {
|
|||||||
if (available.results.length === 0) {
|
if (available.results.length === 0) {
|
||||||
return Response.json({ error: "当前任务已领完" }, { status: 409 });
|
return Response.json({ error: "当前任务已领完" }, { status: 409 });
|
||||||
}
|
}
|
||||||
const partnerId = `partner-ext-${hashText(
|
const partnerId = wecomIdentity
|
||||||
`claimant:${claimantIdentifier.canonical}`,
|
? (await resolveWecomClaimant(db, wecomIdentity.externalUserId)).partnerId
|
||||||
)}`;
|
: `partner-ext-${hashText(
|
||||||
const claimantName = claimantIdentifier.display;
|
`claimant:${claimantIdentifier?.canonical}`,
|
||||||
|
)}`;
|
||||||
|
const claimantName = wecomIdentity
|
||||||
|
? wecomIdentity.displayName
|
||||||
|
: (claimantIdentifier?.display ?? "");
|
||||||
const claimId = uid("claim");
|
const claimId = uid("claim");
|
||||||
const claimToken = crypto.randomUUID().replaceAll("-", "");
|
const claimToken = crypto.randomUUID().replaceAll("-", "");
|
||||||
const statements: DatabaseStatement[] = [
|
const statements: DatabaseStatement[] = [
|
||||||
db
|
wecomIdentity
|
||||||
.prepare(
|
? db
|
||||||
`INSERT INTO partners
|
.prepare(
|
||||||
(id, name, wecom_name, owner, claimed_total, completed_total)
|
`INSERT INTO partners
|
||||||
VALUES (?, ?, ?, '外部KOC', 0, 0)
|
(id, name, wecom_name, owner, claimed_total, completed_total,
|
||||||
ON CONFLICT(id) DO UPDATE SET
|
wecom_external_user_id)
|
||||||
name = excluded.name,
|
VALUES (?, ?, ?, '外部KOC', 0, 0, ?)
|
||||||
wecom_name = excluded.wecom_name`,
|
ON CONFLICT(id) DO UPDATE SET
|
||||||
)
|
name = excluded.name,
|
||||||
.bind(partnerId, claimantName, claimantName),
|
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
|
db
|
||||||
.prepare(
|
.prepare(
|
||||||
`INSERT INTO claims
|
`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