affiliate-dash 领取界面优化(合并多次迭代为一次提交)

- 第 2 步绑定页:移除金额/单号/商品信息区,移除标题上方 affiliate-dash 标签
- 二维码区参考旧设计:左右竖排红字 Q区用Q扫/V区用V扫,去浅蓝背景,放大至 220px
- 第 4 步结果页:废弃 canvas 生成发货成功图,改用旧版结果卡片(绿勾+兑换成功+信息网格+商品明细)
- 开发 Mock:新增 affiliate-dash 领取五步 mock(uid/bind/submitted/completed/failed,不调真实平台)
This commit is contained in:
yml2213
2026-08-05 19:39:15 +08:00
parent 316ee6d135
commit 4e9c416235
10 changed files with 519 additions and 65 deletions
+20
View File
@@ -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 || {}), {
@@ -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<TaskRow | null> {
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
}
@@ -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<DevMockCreateResult> {
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<string, unknown>, secret: string) {
const queryString = Object.entries(payload)
.filter(([key]) => key !== 'sign')
@@ -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,
}
}
@@ -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<Set<string>> {
const skus = new Set<string>(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<string>()
}
}