From 9d981c253fb7f7df9b663f29eb1dabd88c6990ff Mon Sep 17 00:00:00 2001 From: yml2213 Date: Tue, 6 Oct 2026 18:02:17 +0800 Subject: [PATCH] =?UTF-8?q?feat(claim):=20Rock=20=E5=90=8C=E4=B8=80?= =?UTF-8?q?=E6=8F=90=E8=B4=A7=E5=8F=B7=E8=BF=87=E6=9C=9F=E5=90=8E=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E6=8D=A2=E6=96=B0=E4=BA=8C=E7=BB=B4=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 腾讯活动链接会过期,领取页不能一直用第一次的签名。过期后向 0666 重取同一提取码的新链接,不另发提货号。 Co-authored-by: Cursor --- .../claim/affiliate-dash-bind-state.test.ts | 36 +++++++ .../claim/kuaishou-cloud-claim-service.ts | 84 +++++++++++++++- .../fulfillment/affiliate-dash/index.ts | 3 + .../pages/claim/ClaimAffiliateDashSteps.tsx | 96 +++++++++++-------- 4 files changed, 177 insertions(+), 42 deletions(-) diff --git a/apps/backend/src/services/claim/affiliate-dash-bind-state.test.ts b/apps/backend/src/services/claim/affiliate-dash-bind-state.test.ts index 79305e7f..2825d210 100644 --- a/apps/backend/src/services/claim/affiliate-dash-bind-state.test.ts +++ b/apps/backend/src/services/claim/affiliate-dash-bind-state.test.ts @@ -8,6 +8,7 @@ import { isAffiliateDashBindSnapshotStale, isAffiliateDashSnapshotFresh, isRockAffiliateDashSku, + isRockBindUrlStale, shouldRefuseRockRebind, shouldRefreshAffiliateDashBindStateForSnapshot, } from './kuaishou-cloud-claim-service.js' @@ -272,6 +273,40 @@ test('order snapshot stays fresh for 45s and bind snapshot only for 15s', () => ) }) +test('isRockBindUrlStale only refreshes an unpaid rock qr on the same token', () => { + const now = Date.parse('2026-10-06T09:57:00.000Z') + const oldTs = Math.floor(Date.parse('2026-10-06T09:10:14.000Z') / 1000) + const freshTs = Math.floor(Date.parse('2026-10-06T09:55:00.000Z') / 1000) + const stale = { + sku: 'rock_black_tshirt', + orderNo: 'FO20261006170653afd37ce7e19', + bindUrl: `https://gp.qq.com/cp/a20241128mobile/index.html?userId=1×tamp=${oldTs}&nonce=a`, + bound: false, + bindMismatch: false, + } + assert.equal(isRockBindUrlStale(stale, now), true) + assert.equal( + isRockBindUrlStale( + { + ...stale, + bindUrl: `https://gp.qq.com/cp/a20241128mobile/index.html?timestamp=${freshTs}&nonce=b`, + }, + now, + ), + false, + ) + assert.equal(isRockBindUrlStale({ ...stale, bound: true }, now), false) + assert.equal(isRockBindUrlStale({ ...stale, bindMismatch: true }, now), false) + assert.equal(isRockBindUrlStale({ ...stale, sku: 'lewan_black' }, now), false) + assert.equal( + isRockBindUrlStale( + { ...stale, rockBindUrlCheckedAt: new Date(now - 10_000).toISOString() }, + now, + ), + false, + ) +}) + test('claim detail skips affiliate-dash sync while the snapshot is fresh', async () => { const { readFile } = await import('node:fs/promises') const src = await readFile(new URL('./kuaishou-cloud-claim-service.ts', import.meta.url), 'utf8') @@ -282,6 +317,7 @@ test('claim detail skips affiliate-dash sync while the snapshot is fresh', async assert.ok(detail.includes('flow.mock?.enabled')) assert.ok(detail.includes('isAffiliateDashSnapshotFresh(flow.lastSyncedAt')) assert.ok(detail.includes('bindFlow.lastBindSyncedAt')) + assert.ok(detail.includes('refreshRockBindUrlIfStale')) assert.ok( !detail.includes('snapshotOnly'), '新鲜快照必须直接不调用同步;snapshotOnly 仍会 updateTask', diff --git a/apps/backend/src/services/claim/kuaishou-cloud-claim-service.ts b/apps/backend/src/services/claim/kuaishou-cloud-claim-service.ts index f49d9e6d..302f3cb9 100644 --- a/apps/backend/src/services/claim/kuaishou-cloud-claim-service.ts +++ b/apps/backend/src/services/claim/kuaishou-cloud-claim-service.ts @@ -86,6 +86,7 @@ export async function getKuaishouCloudClaimDetail(token: unknown): Promise= ROCK_BIND_URL_MAX_AGE_MS + } + return now - issuedAt >= ROCK_BIND_URL_MAX_AGE_MS +} + export function isRockAffiliateDashSku(sku: unknown) { return String(sku || '') .trim() @@ -720,6 +757,51 @@ async function refreshAffiliateDashBindState(task: TaskRow): Promise { + const taskContext = parseTaskContextValue(task) + const flow = normalizeAffiliateDashFlow(taskContext.affiliateDash) + if (!isRockBindUrlStale(flow)) { + return task + } + const expectedUid = resolveAffiliateDashExpectedGameAccount(task, flow) + if (!expectedUid) { + return task + } + + const now = nowIso() + const persist = (patch: { bindUuid?: string; bindUrl?: string; qrUrl?: string }) => + updateTask(task.id, { + context_json: JSON.stringify({ + ...taskContext, + affiliateDash: { + ...flow, + bindUuid: patch.bindUuid || flow.bindUuid, + bindUrl: patch.bindUrl || flow.bindUrl, + qrUrl: patch.qrUrl || flow.qrUrl, + rockBindUrlCheckedAt: now, + }, + }), + updated_at: now, + }) + + try { + const bindResult = await bindAffiliateDashDelivery({ + orderNo: flow.orderNo, + gameAccount: expectedUid, + }) + const nextBindUrl = String(bindResult.bindUrl || '').trim() + return ( + (await persist({ + bindUuid: bindResult.bindUuid || flow.bindUuid, + bindUrl: nextBindUrl || flow.bindUrl, + qrUrl: bindResult.qrUrl || flow.qrUrl, + })) || task + ) + } catch { + return (await persist({})) || task + } +} + function readClaimIdentity(value: unknown) { const source = value && typeof value === 'object' && !Array.isArray(value) diff --git a/apps/backend/src/services/fulfillment/affiliate-dash/index.ts b/apps/backend/src/services/fulfillment/affiliate-dash/index.ts index 5e09ba2b..db44bb17 100644 --- a/apps/backend/src/services/fulfillment/affiliate-dash/index.ts +++ b/apps/backend/src/services/fulfillment/affiliate-dash/index.ts @@ -332,6 +332,8 @@ export type AffiliateDashFlow = { lastSyncedAt: unknown /** 最近一次成功拉取 bind-result 的时间。与订单状态的 lastSyncedAt 分开计。 */ lastBindSyncedAt: unknown + /** 最近一次向 0666 重取同一提货号二维码的时间,用来避免过期链接每轮都打上游。 */ + rockBindUrlCheckedAt: unknown raw: unknown /** dev-mock 标记:enabled 时所有 affiliate-dash 平台调用短路(建单/绑定/提交/同步均不请求外部) */ mock: { enabled: boolean; orderNo: string; createdAt: string } | null @@ -374,6 +376,7 @@ export function normalizeAffiliateDashFlow(value: unknown): AffiliateDashFlow { consumeStatus: String(source.consumeStatus || 'pending').trim(), lastSyncedAt: source.lastSyncedAt || null, lastBindSyncedAt: source.lastBindSyncedAt || null, + rockBindUrlCheckedAt: source.rockBindUrlCheckedAt || null, raw, mock: source.mock && typeof source.mock === 'object' diff --git a/apps/frontend/src/pages/claim/ClaimAffiliateDashSteps.tsx b/apps/frontend/src/pages/claim/ClaimAffiliateDashSteps.tsx index aab405c6..463f1d8f 100644 --- a/apps/frontend/src/pages/claim/ClaimAffiliateDashSteps.tsx +++ b/apps/frontend/src/pages/claim/ClaimAffiliateDashSteps.tsx @@ -169,50 +169,64 @@ export function AffiliateDashClaimPanel({ ) : null} {!canSubmit ? ( -
- +
- Q区用Q扫 - - {hasBindQr ? ( - 绑定二维码 - ) : ( - 绑定二维码生成中,请稍候自动刷新… - )} - - V区用V扫 - -
+ + Q区用Q扫 + + {hasBindQr ? ( + 绑定二维码 + ) : ( + 绑定二维码生成中,请稍候自动刷新… + )} + + V区用V扫 + +
+ {hasBindQr && + String(affiliateDash.sku || '') + .toLowerCase() + .startsWith('rock_') && + !rockTokenLocked ? ( + + 二维码会过期,页面会自动换成新的,请扫当前这一张 + + ) : null} + ) : ( <>