From 4e9c4162354c2565c9987f2ee55df2bd826648cb Mon Sep 17 00:00:00 2001 From: yml2213 Date: Wed, 5 Aug 2026 19:39:01 +0800 Subject: [PATCH] =?UTF-8?q?affiliate-dash=20=E9=A2=86=E5=8F=96=E7=95=8C?= =?UTF-8?q?=E9=9D=A2=E4=BC=98=E5=8C=96(=E5=90=88=E5=B9=B6=E5=A4=9A?= =?UTF-8?q?=E6=AC=A1=E8=BF=AD=E4=BB=A3=E4=B8=BA=E4=B8=80=E6=AC=A1=E6=8F=90?= =?UTF-8?q?=E4=BA=A4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 第 2 步绑定页:移除金额/单号/商品信息区,移除标题上方 affiliate-dash 标签 - 二维码区参考旧设计:左右竖排红字 Q区用Q扫/V区用V扫,去浅蓝背景,放大至 220px - 第 4 步结果页:废弃 canvas 生成发货成功图,改用旧版结果卡片(绿勾+兑换成功+信息网格+商品明细) - 开发 Mock:新增 affiliate-dash 领取五步 mock(uid/bind/submitted/completed/failed,不调真实平台) --- apps/backend/src/routes/admin/dev-mock.ts | 20 ++ .../claim/kuaishou-cloud-claim-service.ts | 21 +- .../src/services/dev-mock/dev-mock-service.ts | 242 +++++++++++++++++- .../fulfillment/affiliate-dash/index.ts | 21 +- .../fulfillment/product-resolution-service.ts | 10 +- .../src/pages/admin/AdminDevMockPage.tsx | 90 ++++++- .../pages/claim/ClaimAffiliateDashSteps.tsx | 116 +++++---- apps/frontend/src/pages/claim/ClaimPage.tsx | 3 - apps/frontend/src/services/admin/dev-mock.ts | 12 +- .../affiliate-dash对接.md | 49 ++++ 10 files changed, 519 insertions(+), 65 deletions(-) diff --git a/apps/backend/src/routes/admin/dev-mock.ts b/apps/backend/src/routes/admin/dev-mock.ts index 7e344c36..8992fbe1 100644 --- a/apps/backend/src/routes/admin/dev-mock.ts +++ b/apps/backend/src/routes/admin/dev-mock.ts @@ -2,6 +2,7 @@ import { Router } from 'express' import { buildOpen91QueryMock, + createAffiliateDashMockClaim, createFeifeiMockClaim, createLewanMockClaim, getDevMockStatus, @@ -81,6 +82,25 @@ router.post( }), ) +router.post( + '/dev-mock/affiliate-dash', + createJsonHandler((req) => createAffiliateDashMockClaim(req.body || {}), { + successMessage: 'affiliate-dash mock 已生成', + errorMessage: '生成 affiliate-dash mock 失败', + scope: '[admin/dev-mock/affiliate-dash]', + audit: (_req, data) => ({ + action: 'dev_mock_create_affiliate_dash', + targetType: 'dev_mock', + targetId: String((data as { orderNo?: string })?.orderNo || ''), + data: { + platform: 'affiliate_dash', + orderNo: (data as { orderNo?: string })?.orderNo, + step: (data as { step?: string })?.step, + }, + }), + }), +) + router.post( '/dev-mock/open91/query', createJsonHandler((req) => buildOpen91QueryMock(req.body || {}), { 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 ad79bd77..2e0334d7 100644 --- a/apps/backend/src/services/claim/kuaishou-cloud-claim-service.ts +++ b/apps/backend/src/services/claim/kuaishou-cloud-claim-service.ts @@ -414,11 +414,18 @@ export async function submitAffiliateDashClaim( } const now = nowIso() - const result = await submitAffiliateDashDelivery({ - orderNo: flow.orderNo, - gameAccount, - bindUuid, - }) + const isMock = Boolean(flow.mock?.enabled) + const result = isMock + ? { + status: 'delivered', + message: '开发 mock 已模拟提交发货', + providerOrderNo: `MOCKAD-${flow.orderNo}`, + } + : await submitAffiliateDashDelivery({ + orderNo: flow.orderNo, + gameAccount, + bindUuid, + }) const nextFlow = { ...flow, @@ -505,6 +512,10 @@ async function bindAffiliateDashClaimForTask(task: TaskRow, gameAccount: string) async function refreshAffiliateDashBindState(task: TaskRow): Promise { const taskContext = parseTaskContextValue(task) const flow = normalizeAffiliateDashFlow(taskContext.affiliateDash) + if (flow.mock?.enabled) { + // dev-mock:不请求 affiliate-dash 平台,绑定状态以 context 为准 + return task + } if (!flow.orderNo || !flow.bindUuid) { return task } diff --git a/apps/backend/src/services/dev-mock/dev-mock-service.ts b/apps/backend/src/services/dev-mock/dev-mock-service.ts index e2022b15..8dc27e6a 100644 --- a/apps/backend/src/services/dev-mock/dev-mock-service.ts +++ b/apps/backend/src/services/dev-mock/dev-mock-service.ts @@ -25,7 +25,7 @@ export type LewanMockStep = 'uid' | 'binding' | 'confirm' | 'result' export type FeifeiMockStep = 'uid' | 'ready' | 'completed' | 'failed' export type DevMockCreateResult = { - platform: 'lewan' | 'feifei' + platform: 'lewan' | 'feifei' | 'affiliate_dash' orderId: number orderNo: string taskId: number @@ -84,6 +84,12 @@ export function getDevMockStatus() { description: '本站 claim + 拼 uid 的 H5 链接(不调真实 feifei 下单)', steps: ['uid', 'ready', 'completed', 'failed'], }, + { + key: 'affiliate_dash', + name: 'affiliate-dash', + description: '本站 claim + mock 绑定二维码/提交发货(不调真实 affiliate-dash 平台)', + steps: ['uid', 'bind', 'submitted', 'completed', 'failed'], + }, ] : [], defaultUid: '10001', @@ -356,6 +362,198 @@ export async function createFeifeiMockClaim(input: { }) } +type AffiliateDashMockStep = 'uid' | 'bind' | 'submitted' | 'completed' | 'failed' + +/** + * 生成 affiliate-dash 领取 mock:不调用真实 affiliate-dash 平台。 + * 上下文带 mock 标记,sync/refresh/submit 全部短路,领取页可走完四步。 + */ +export async function createAffiliateDashMockClaim(input: { + step?: unknown + orderNo?: unknown + productName?: unknown + productSku?: unknown + uid?: unknown + frontendBaseUrl?: unknown +} = {}): Promise { + assertDevMockEnabled() + + const step = normalizeAffiliateDashStep(input.step) + const orderNo = String(input.orderNo || `MOCKAD${Date.now()}`).trim() + const productName = String(input.productName || '幸运币90个').trim() + const productSku = String(input.productSku || 'lucky_coin_x90').trim() + const expectedUid = resolveMockUid(input.uid, step === 'uid' ? '' : '166909256') + const fallbackUid = expectedUid || '166909256' + const bindUrl = + step === 'uid' + ? '' + : `https://skin.khhao.com/mock/bind?order=${encodeURIComponent(orderNo)}&uid=${encodeURIComponent(fallbackUid)}` + const bound = step === 'submitted' || step === 'completed' || step === 'failed' + const orderStatus = + step === 'completed' + ? 'delivered' + : step === 'failed' + ? 'ship_failed' + : step === 'submitted' + ? 'delivering' + : 'paid' + const now = nowIso() + + await assertOrderNoAvailable(orderNo) + + const profile = await ensureProfile('affiliate_dash', 'affiliate-dash 履约', true) + const order = await createBaseOrder({ + orderNo, + productNo: productSku, + productName, + now, + platformTag: 'affiliate-dash', + }) + const orderItem = await createBaseOrderItem({ + orderId: order.id, + orderNo, + productNo: productSku, + productName, + consumeShopId: '', + deliveryItems: [], + now, + executor: 'affiliate_dash', + productCode: productSku, + }) + + const task = await createTask({ + orderId: order.id, + orderItemId: orderItem.id, + unitIndex: 1, + provider: OPEN_91_PROVIDER, + platform: OPEN_91_PLATFORM, + shopId: OPEN_91_PROVIDER, + shopName: '91卡券', + platformOrderId: orderNo, + taskNo: randomId('DT'), + profileId: profile.id, + executorKey: 'affiliate_dash', + taskStatus: + step === 'completed' + ? TASK_STATUS.REDEEMED + : step === 'failed' + ? TASK_STATUS.RETRY_PENDING + : step === 'submitted' + ? TASK_STATUS.REDEEMING + : TASK_STATUS.LINK_GENERATED, + deliveryStatus: + step === 'completed' + ? 'delivered' + : step === 'submitted' + ? 'delivering' + : 'pending', + resultCode: + step === 'completed' + ? 'affiliate_dash_delivered' + : step === 'failed' + ? 'affiliate_dash_ship_failed' + : '', + resultMessage: + step === 'completed' + ? 'affiliate-dash 履约成功' + : step === 'failed' + ? '开发 mock 模拟发货失败' + : '等待用户领取', + claimToken: '', + claimExpiresAt: null, + automationMode: 'manual', + requiresClaim: true, + userActionStatus: step === 'completed' ? 'not_required' : 'pending_claim', + attemptCount: 0, + lastError: step === 'failed' ? '开发 mock 模拟发货失败' : '', + contextJson: JSON.stringify({ + profileKey: 'affiliate_dash', + profileName: 'affiliate-dash 履约', + skuCode: productName, + skuName: productName, + claimIdentity: expectedUid + ? { + expectedUid, + submittedAt: now, + source: 'dev_mock', + } + : { + expectedUid: '', + submittedAt: null, + source: '', + }, + affiliateDash: { + flowType: 'affiliate_dash', + sku: productSku, + productName, + orderNo: `AD-${orderNo}`, + clientOrderNo: '', + orderStatus, + canShip: true, + cannotShipReason: '', + providerOrderNo: '', + failureReason: step === 'failed' ? '开发 mock 模拟发货失败' : '', + amount: 20, + currency: 'POINT', + bindUuid: bound ? `mock-bind-${orderNo}` : '', + bindUrl, + qrUrl: '', + gameAccount: bound ? fallbackUid : '', + expectedGameAccount: expectedUid || '', + bindMismatch: false, + bound, + boundAccount: bound ? fallbackUid : '', + gameChannel: 'Android', + submitStatus: + step === 'completed' + ? 'delivered' + : step === 'failed' + ? 'ship_failed' + : step === 'submitted' + ? 'submitting' + : '', + consumeStatus: step === 'completed' ? 'not_required' : 'pending', + lastSyncedAt: now, + mock: { + enabled: true, + orderNo, + createdAt: now, + }, + }, + }), + createdAt: now, + updatedAt: now, + }) + + if (!task) { + throw createHttpError('履约任务创建失败', { statusCode: 500, errorCode: 'dev_mock_task_failed' }) + } + + const claimToken = await createTaskClaimToken(task.id) + await updateTask(task.id, { + claim_token: claimToken.token, + claim_expires_at: claimToken.expired_at, + claimed_at: expectedUid ? now : null, + redeemed_at: step === 'completed' ? now : null, + updated_at: now, + }) + + return buildCreateResult({ + platform: 'affiliate_dash', + orderId: order.id, + orderNo, + taskId: task.id, + taskNo: task.task_no, + claimToken: claimToken.token, + claimUrl: buildClaimUrl(claimToken.token), + frontendBaseUrl: input.frontendBaseUrl, + expectedUid: fallbackUid, + step, + productName, + tips: buildAffiliateDashTips(step, fallbackUid), + }) +} + /** * 生成 91 查询请求体(带签名),可选对本机发起查询。 */ @@ -515,7 +713,7 @@ async function createBaseOrderItem(input: { consumeShopId: string deliveryItems: DeliveryItem[] now: string - executor: 'lewan' | 'feifei' + executor: 'lewan' | 'feifei' | 'affiliate_dash' productCode?: string }) { const snapshot = @@ -531,7 +729,19 @@ async function createBaseOrderItem(input: { matchMode: 'mock', }, } - : { + : input.executor === 'affiliate_dash' + ? { + source: OPEN_91_PROVIDER, + orderNo: input.orderNo, + productNo: input.productNo, + productName: input.productName, + affiliateDash: { + sku: input.productCode || input.productNo, + productName: input.productName, + matchMode: 'mock', + }, + } + : { source: OPEN_91_PROVIDER, orderNo: input.orderNo, productNo: input.productNo, @@ -725,6 +935,14 @@ function normalizeFeifeiStep(value: unknown): FeifeiMockStep { return 'uid' } +function normalizeAffiliateDashStep(value: unknown): AffiliateDashMockStep { + const step = String(value || 'uid').trim() + if (['uid', 'bind', 'submitted', 'completed', 'failed'].includes(step)) { + return step as AffiliateDashMockStep + } + return 'uid' +} + function resolveMockUid(value: unknown, fallback: string) { const raw = String(value ?? '').trim() if (!raw) { @@ -774,7 +992,7 @@ function parseDeliveryItems(value: unknown): DeliveryItem[] { } function buildCreateResult(input: { - platform: 'lewan' | 'feifei' + platform: 'lewan' | 'feifei' | 'affiliate_dash' orderId: number orderNo: string taskId: number @@ -844,6 +1062,22 @@ function buildFeifeiTips(step: FeifeiMockStep, uid: string) { return ['已模拟 feifei 成功完成态'] } +function buildAffiliateDashTips(step: AffiliateDashMockStep, uid: string) { + if (step === 'uid') { + return [`打开领取链接,Step1 填 UID:${uid}(或自定义)`, '提交后进入绑定步,mock 会给出二维码'] + } + if (step === 'bind') { + return [`已预填 UID=${uid},绑定二维码为 mock 生成(扫描无效)`, '正常流程:扫码完成真实绑定后自动进入下一步'] + } + if (step === 'submitted') { + return ['已模拟绑定成功,可点「提交发货」(mock 直接模拟发货成功)'] + } + if (step === 'failed') { + return ['已模拟发货失败态,用于看领取页结果展示'] + } + return ['已模拟 affiliate-dash 发货完成态'] +} + function signOpen91Payload(payload: Record, secret: string) { const queryString = Object.entries(payload) .filter(([key]) => key !== 'sign') diff --git a/apps/backend/src/services/fulfillment/affiliate-dash/index.ts b/apps/backend/src/services/fulfillment/affiliate-dash/index.ts index 83926d97..d0a4b90b 100644 --- a/apps/backend/src/services/fulfillment/affiliate-dash/index.ts +++ b/apps/backend/src/services/fulfillment/affiliate-dash/index.ts @@ -166,7 +166,16 @@ export async function syncAffiliateDashTaskStatus(task: TaskRow) { } const now = nowIso() - const order = await getAffiliateDashOrder(flow.orderNo) + // dev-mock 任务:不请求 affiliate-dash 平台,直接用本地快照(flow 自身)收敛状态 + const order = flow.mock?.enabled + ? ({ + orderNo: flow.orderNo, + orderStatus: flow.orderStatus, + failureReason: flow.failureReason, + canShip: flow.canShip, + raw: null, + } as unknown as AffiliateDashOrder) + : await getAffiliateDashOrder(flow.orderNo) let nextTaskStatus = task.task_status let deliveryStatus = task.delivery_status let redeemedAt = task.redeemed_at @@ -286,6 +295,8 @@ export type AffiliateDashFlow = { consumeStatus: string lastSyncedAt: unknown raw: unknown + /** dev-mock 标记:enabled 时所有 affiliate-dash 平台调用短路(建单/绑定/提交/同步均不请求外部) */ + mock: { enabled: boolean; orderNo: string; createdAt: string } | null } export function normalizeAffiliateDashFlow(value: unknown): AffiliateDashFlow { @@ -319,6 +330,14 @@ export function normalizeAffiliateDashFlow(value: unknown): AffiliateDashFlow { consumeStatus: String(source.consumeStatus || 'pending').trim(), lastSyncedAt: source.lastSyncedAt || null, raw: source.raw && typeof source.raw === 'object' ? source.raw : null, + mock: + source.mock && typeof source.mock === 'object' + ? { + enabled: Boolean((source.mock as JsonObject).enabled), + orderNo: String((source.mock as JsonObject).orderNo || '').trim(), + createdAt: String((source.mock as JsonObject).createdAt || '').trim(), + } + : null, } } diff --git a/apps/backend/src/services/fulfillment/product-resolution-service.ts b/apps/backend/src/services/fulfillment/product-resolution-service.ts index b9e66f30..da7e59ea 100644 --- a/apps/backend/src/services/fulfillment/product-resolution-service.ts +++ b/apps/backend/src/services/fulfillment/product-resolution-service.ts @@ -15,6 +15,7 @@ import { getAffiliateDashConfig } from '../platforms/affiliate-dash/config.js' import { listAllAffiliateDashProducts } from '../platforms/affiliate-dash/product-service.js' import type { AffiliateDashSkuMapping } from '../../types/runtime-config.js' import { FULFILLMENT_EXECUTOR_KEYS } from './executors/types.js' +import { logIntegration } from '../../utils/logger.js' export type FulfillmentItem = { itemId?: string @@ -378,8 +379,13 @@ async function getAffiliateDashSkuSet(): Promise> { const skus = new Set(result.list.map((product) => String(product.sku || '').trim()).filter(Boolean)) affiliateDashSkuCache = { skus, fetchedAt: now } return skus - } catch { - // 拉取失败:返回上次缓存(即使过期)或空集合,匹配 miss 走其他通道,不阻塞下单 + } catch (error) { + // 拉取失败:返回上次缓存(即使过期)或空集合,匹配 miss 走其他通道,不阻塞下单; + // warn 日志便于线上排查(密钥未配/平台不可用都会导致透传降级为 miss)。 + logIntegration('[affiliate-dash]', 'affiliate-dash 商品列表拉取失败,透传降级为未命中', { + cachedSkuCount: affiliateDashSkuCache?.skus.size || 0, + error: error instanceof Error ? error.message : String(error), + }, { level: 'warn' }) return affiliateDashSkuCache?.skus || new Set() } } diff --git a/apps/frontend/src/pages/admin/AdminDevMockPage.tsx b/apps/frontend/src/pages/admin/AdminDevMockPage.tsx index 4ea7ef94..3c4bfa37 100644 --- a/apps/frontend/src/pages/admin/AdminDevMockPage.tsx +++ b/apps/frontend/src/pages/admin/AdminDevMockPage.tsx @@ -21,6 +21,7 @@ import { Link } from 'react-router' import PageHeader from '@/components/admin/PageHeader' import { buildOpen91QueryDevMock, + createAffiliateDashDevMock, createFeifeiDevMock, createLewanDevMock, fetchDevMockStatus, @@ -64,6 +65,17 @@ export default function AdminDevMockPage() { }, }) + const affiliateDashMutation = useMutation({ + mutationFn: createAffiliateDashDevMock, + onSuccess: (response) => { + setLastCreate(response.data) + message.success('affiliate-dash mock 已生成') + }, + onError: (error) => { + message.error(error instanceof Error ? error.message : '生成失败') + }, + }) + const open91Mutation = useMutation({ mutationFn: buildOpen91QueryDevMock, onSuccess: (response) => { @@ -145,8 +157,17 @@ export default function AdminDevMockPage() { + + + affiliateDashMutation.mutate(values)} + /> + + + - +
) } + +function AffiliateDashMockForm({ + loading, + onSubmit, +}: { + loading: boolean + onSubmit: (values: Record) => void +}) { + const [form] = Form.useForm() + const step = Form.useWatch('step', form) + const uidRequired = step !== 'uid' + + return ( + + + + + + + + + + + + + + + + ) +} diff --git a/apps/frontend/src/pages/claim/ClaimAffiliateDashSteps.tsx b/apps/frontend/src/pages/claim/ClaimAffiliateDashSteps.tsx index 6e993a79..914edca1 100644 --- a/apps/frontend/src/pages/claim/ClaimAffiliateDashSteps.tsx +++ b/apps/frontend/src/pages/claim/ClaimAffiliateDashSteps.tsx @@ -1,18 +1,13 @@ -import { CheckCircleOutlined, LinkOutlined } from '@ant-design/icons' +import { CheckCircleOutlined, ExclamationCircleOutlined } from '@ant-design/icons' import { Alert, Button, Card, Typography } from 'antd' -import type { - ClaimAffiliateDashFlowInfo, - ClaimOrderInfo, - ClaimProductInfo, -} from '@/types/claim' +import { formatAdminDateTime } from '@/utils/admin-time' +import type { ClaimAffiliateDashFlowInfo } from '@/types/claim' import type { ClaimSnapshot } from './claim-snapshot' -import { InfoTile, UidEditRow } from './claim-shared' +import { ClaimProductItems, InfoTile, UidEditRow } from './claim-shared' export function AffiliateDashClaimPanel({ affiliateDash, expectedUid, - order, - product, taskLastError, qrCodeDataUrl, submittingUid, @@ -21,12 +16,9 @@ export function AffiliateDashClaimPanel({ onUidChange, onResubmitUid, onSubmit, - onOpenBindUrl, }: { affiliateDash: ClaimAffiliateDashFlowInfo expectedUid: string - order: ClaimOrderInfo | null - product: ClaimProductInfo | null taskLastError?: string qrCodeDataUrl: string submittingUid: boolean @@ -35,16 +27,13 @@ export function AffiliateDashClaimPanel({ onUidChange: (value: string) => void onResubmitUid: () => void onSubmit: () => void - onOpenBindUrl: () => void }) { - const bindReady = Boolean(affiliateDash.bindUrl || affiliateDash.qrUrl) const bound = Boolean(affiliateDash.bound) const bindMismatch = Boolean(affiliateDash.bindMismatch) return (
- affiliate-dash {bound ? '第 3 步:确认并提交发货' : '第 2 步:绑定领取账号'} @@ -58,14 +47,6 @@ export function AffiliateDashClaimPanel({ onSubmit={onResubmitUid} /> -
- - - - - -
- {taskLastError ? ( +
+ + Q区用Q扫 + {qrCodeDataUrl ? ( 绑定二维码 - ) : null} - {bindReady ? ( - ) : ( - 绑定链接生成中,请稍候自动刷新… + 绑定二维码生成中,请稍候自动刷新… )} + + V区用V扫 +
) : (