新增开发 Mock 后台菜单与造单能力

非生产环境在后台提供 lewan/feifei 领取造单与 91 查单模拟,CLI 复用同一服务,避免依赖真实平台联调。
This commit is contained in:
yml2213
2026-07-10 13:35:42 +08:00
parent bb060e81a4
commit 3c0d1f108c
13 changed files with 1593 additions and 533 deletions
@@ -0,0 +1,858 @@
/**
* 开发环境专用 Mock 数据工厂。
* 生产环境禁止调用;由路由层 isDevMockEnabled 拦截。
*/
import crypto from 'node:crypto'
import { createOrder, findOrderByPlatformOrderId } from '../../repositories/order-repo.js'
import { replaceOrderItems } from '../../repositories/order-item-repo.js'
import { createTask, updateTask } from '../../repositories/task-repo.js'
import {
getFulfillmentProfileByKey,
upsertFulfillmentProfile,
} from '../../repositories/fulfillment-profile-repo.js'
import { buildClaimUrl, createTaskClaimToken } from '../claim/claim-service.js'
import { assertValidClaimUid, normalizeClaimUid } from '../claim/claim-identity.js'
import { OPEN_91_PLATFORM, OPEN_91_PROVIDER, assertOpen91Config } from '../open-91/config.js'
import { parseOpen91ProductNo } from '../platforms/ninetyone/order-service.js'
import { isProductionLike } from '../../config/runtime-validation.js'
import { createHttpError } from '../../utils/http.js'
import { addHours, nowIso } from '../../utils/time.js'
import { randomId } from '../../utils/random.js'
import { TASK_STATUS } from '../../domain/task-status.js'
export type LewanMockStep = 'uid' | 'binding' | 'confirm' | 'result'
export type FeifeiMockStep = 'uid' | 'ready' | 'completed' | 'failed'
export type DevMockCreateResult = {
platform: 'lewan' | 'feifei'
orderId: number
orderNo: string
taskId: number
taskNo: string
claimToken: string
claimUrl: string
frontendClaimUrl: string
expectedUid: string
step: string
productName: string
tips: string[]
open91QueryHint: string
}
type DeliveryItem = {
cloudSkuId: number
cloudSkuName: string
quantity: number
}
export function isDevMockEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
if (String(env.ENABLE_DEV_MOCK || '').trim() === '1') {
return true
}
if (String(env.ENABLE_DEV_MOCK || '').trim().toLowerCase() === 'true') {
return true
}
return !isProductionLike(env)
}
export function assertDevMockEnabled() {
if (!isDevMockEnabled()) {
throw createHttpError('当前环境未开启开发 Mock(仅非 production 或 ENABLE_DEV_MOCK=1', {
statusCode: 403,
errorCode: 'dev_mock_disabled',
})
}
}
export function getDevMockStatus() {
const enabled = isDevMockEnabled()
return {
enabled,
nodeEnv: String(process.env.NODE_ENV || 'development'),
platforms: enabled
? [
{
key: 'lewan',
name: 'kuaishou-lewan',
description: '本站 claim + 强制 UID 匹配 + mock 兑换(不调 CloudTentacles',
steps: ['uid', 'binding', 'confirm', 'result'],
},
{
key: 'feifei',
name: 'kuaishou-feifei',
description: '本站 claim + 拼 uid 的 H5 链接(不调真实 feifei 下单)',
steps: ['uid', 'ready', 'completed', 'failed'],
},
]
: [],
defaultUid: '10001',
}
}
export async function createLewanMockClaim(input: {
step?: unknown
orderNo?: unknown
productNo?: unknown
uid?: unknown
items?: unknown
frontendBaseUrl?: unknown
} = {}): Promise<DevMockCreateResult> {
assertDevMockEnabled()
const step = normalizeLewanStep(input.step)
const orderNo = String(input.orderNo || `MOCK91${Date.now()}`).trim()
const productNo = String(input.productNo || '套餐_1----3676797936').trim()
const productInfo = parseOpen91ProductNo(productNo)
const productName = productInfo.productName || productInfo.rawProductNo || '套餐_1'
const consumeShopId = productInfo.shopId || '3676797936'
const expectedUid = resolveMockUid(input.uid, step === 'uid' ? '' : '10001')
const deliveryItems = parseDeliveryItems(input.items)
const now = nowIso()
await assertOrderNoAvailable(orderNo)
const profile = await ensureProfile('kuaishou_ct_assisted', 'kuaishou-lewan 履约', false)
const order = await createBaseOrder({
orderNo,
productNo,
productName,
now,
platformTag: 'lewan',
})
const orderItem = await createBaseOrderItem({
orderId: order.id,
orderNo,
productNo,
productName,
consumeShopId,
deliveryItems,
now,
executor: 'lewan',
})
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: 'kuaishou_ct_assisted',
taskStatus: resolveLewanTaskStatus(step),
deliveryStatus: step === 'result' ? 'success' : 'pending',
resultCode: step === 'result' ? 'mock_success' : '',
resultMessage: step === 'result' ? '开发 mock 已模拟兑换成功' : '',
claimToken: '',
claimExpiresAt: null,
automationMode: 'manual',
requiresClaim: true,
userActionStatus: step === 'result' ? 'not_required' : 'pending_claim',
attemptCount: 0,
lastError: '',
contextJson: JSON.stringify(
buildLewanMockContext({
step,
orderNo,
productName,
productNo,
consumeShopId,
deliveryItems,
expectedUid,
timestamp: 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: step === 'uid' ? null : now,
role_confirmed_at: step === 'confirm' || step === 'result' ? now : null,
redeemed_at: step === 'result' ? now : null,
role_id: ['binding', 'confirm', 'result'].includes(step) ? expectedUid || '10001' : '',
role_name: ['binding', 'confirm', 'result'].includes(step) ? '测试角色' : '',
updated_at: now,
})
return buildCreateResult({
platform: 'lewan',
orderId: order.id,
orderNo,
taskId: task.id,
taskNo: task.task_no,
claimToken: claimToken.token,
claimUrl: buildClaimUrl(claimToken.token),
frontendBaseUrl: input.frontendBaseUrl,
expectedUid: expectedUid || '10001',
step,
productName,
tips: buildLewanTips(step, expectedUid || '10001'),
})
}
export async function createFeifeiMockClaim(input: {
step?: unknown
orderNo?: unknown
productName?: unknown
productCode?: unknown
uid?: unknown
h5Url?: unknown
frontendBaseUrl?: unknown
} = {}): Promise<DevMockCreateResult> {
assertDevMockEnabled()
const step = normalizeFeifeiStep(input.step)
const orderNo = String(input.orderNo || `MOCKFF${Date.now()}`).trim()
const productName = String(input.productName || '套装-浪漫天命').trim()
const productCode = String(input.productCode || `MOCK-FF-${Date.now()}`).trim()
const expectedUid = resolveMockUid(input.uid, step === 'uid' ? '' : '166909256')
const rawH5 =
String(input.h5Url || '').trim() ||
`http://skin-exchange.yiquyou.icu/h5/bind?code=mock_${encodeURIComponent(orderNo)}&product_name=${encodeURIComponent(productName)}`
const now = nowIso()
await assertOrderNoAvailable(orderNo)
const profile = await ensureProfile('kuaishou_feifei', 'kuaishou-feifei 履约', true)
const order = await createBaseOrder({
orderNo,
productNo: productName,
productName,
now,
platformTag: 'feifei',
})
const orderItem = await createBaseOrderItem({
orderId: order.id,
orderNo,
productNo: productName,
productName,
consumeShopId: '',
deliveryItems: [],
now,
executor: 'feifei',
productCode,
})
const rechargeStatus =
step === 'completed' ? 30 : step === 'failed' ? 40 : step === 'ready' ? 15 : 15
const rechargeStatusLabel =
step === 'completed'
? '充值成功'
: step === 'failed'
? '充值失败'
: step === 'ready'
? '待绑定'
: '待领取'
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: 'kuaishou_feifei',
taskStatus:
step === 'completed'
? TASK_STATUS.COMPLETED
: step === 'failed'
? TASK_STATUS.MANUAL_REVIEW
: TASK_STATUS.LINK_GENERATED,
deliveryStatus: step === 'completed' ? 'delivered' : 'pending',
resultCode: step === 'completed' ? 'kuaishou_feifei_completed' : step === 'failed' ? 'kuaishou_feifei_status_40' : '',
resultMessage: rechargeStatusLabel,
claimToken: '',
claimExpiresAt: null,
automationMode: 'manual',
requiresClaim: true,
userActionStatus: step === 'completed' ? 'not_required' : 'pending_claim',
attemptCount: 0,
lastError: step === 'failed' ? '开发 mock 模拟 feifei 失败' : '',
contextJson: JSON.stringify({
profileKey: 'kuaishou_feifei',
profileName: 'kuaishou-feifei 履约',
skuCode: productName,
skuName: productName,
claimIdentity: expectedUid
? {
expectedUid,
submittedAt: now,
source: 'dev_mock',
}
: {
expectedUid: '',
submittedAt: null,
source: '',
},
kuaishouFeifei: {
flowType: 'kuaishou_feifei',
productCode,
productName,
platformOrderNo: orderNo,
orderNo: `FF-${orderNo}`,
rechargeStatus,
rechargeStatusLabel,
rechargeResultMessage: step === 'failed' ? '开发 mock 失败' : '',
claimUrl: '',
consumeStatus: step === 'completed' ? 'not_required' : 'pending',
h5: {
entryUrl: rawH5,
rechargeUrl: rawH5,
},
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: 'feifei',
orderId: order.id,
orderNo,
taskId: task.id,
taskNo: task.task_no,
claimToken: claimToken.token,
claimUrl: buildClaimUrl(claimToken.token),
frontendBaseUrl: input.frontendBaseUrl,
expectedUid: expectedUid || '166909256',
step,
productName,
tips: buildFeifeiTips(step, expectedUid || '166909256'),
})
}
/**
* 生成 91 查询请求体(带签名),可选对本机发起查询。
*/
export async function buildOpen91QueryMock(input: {
orderNo?: unknown
execute?: unknown
baseUrl?: unknown
} = {}) {
assertDevMockEnabled()
const orderNo = String(input.orderNo || '').trim()
if (!orderNo) {
throw createHttpError('请填写 orderNo', {
statusCode: 400,
errorCode: 'dev_mock_order_no_required',
})
}
const config = assertOpen91Config()
const timestamp = Math.floor(Date.now() / 1000)
const payload = {
orderNo,
timestamp,
version: config.version || '1.0',
}
const sign = signOpen91Payload(payload, config.secret)
const body = { ...payload, sign }
const port = String(process.env.PORT || process.env.BACKEND_PORT || '3000').trim() || '3000'
const defaultBase = `http://127.0.0.1:${port}`
const baseUrl = String(input.baseUrl || defaultBase)
.replace(/\/#\/claim\/?$/, '')
.replace(/\/$/, '')
const endpoint = `${baseUrl || defaultBase}/api/v1/open/91/orders/query`
let response: unknown = null
if (input.execute === true || input.execute === 'true' || input.execute === 1) {
try {
const res = await fetch(endpoint, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(body),
})
const text = await res.text()
try {
response = { status: res.status, body: JSON.parse(text) }
} catch {
response = { status: res.status, body: text }
}
} catch (error) {
response = {
status: 0,
error: error instanceof Error ? error.message : String(error),
tip: '本机请求失败时,可复制 requestBody 用 curl 手动打到 backend',
}
}
}
return {
endpoint,
requestBody: body,
curl: `curl -sS -X POST '${endpoint}' -H 'content-type: application/json' -d '${JSON.stringify(body)}'`,
response,
}
}
// --- helpers ---
async function assertOrderNoAvailable(orderNo: string) {
const existing = await findOrderByPlatformOrderId({
provider: OPEN_91_PROVIDER,
platform: OPEN_91_PLATFORM,
shopId: OPEN_91_PROVIDER,
platformOrderId: orderNo,
})
if (existing) {
throw createHttpError(`订单号已存在:${orderNo},请换一个或留空自动生成`, {
statusCode: 409,
errorCode: 'dev_mock_order_exists',
})
}
}
async function ensureProfile(profileKey: string, name: string, requiresClaim: boolean) {
const existing = await getFulfillmentProfileByKey(profileKey)
if (existing) {
return existing
}
const now = nowIso()
const profile = await upsertFulfillmentProfile({
profileKey,
name,
executorKey: profileKey,
requiresClaim,
autoDispatch: false,
inventoryStrategy: 'external_platform',
configJson: '{}',
createdAt: now,
updatedAt: now,
})
if (!profile) {
throw createHttpError('履约配置创建失败', { statusCode: 500, errorCode: 'dev_mock_profile_failed' })
}
return profile
}
async function createBaseOrder(input: {
orderNo: string
productNo: string
productName: string
now: string
platformTag: string
}) {
const order = await createOrder({
provider: OPEN_91_PROVIDER,
platform: OPEN_91_PLATFORM,
shopId: OPEN_91_PROVIDER,
shopName: '91卡券',
platformOrderId: input.orderNo,
orderStatus: 'paid',
payStatus: 'paid',
buyerId: 'mock-buyer',
buyerName: 'mock 领取客户',
receiverContact: '',
totalAmount: 100,
currency: 'CNY',
rawPayloadJson: JSON.stringify({
source: OPEN_91_PROVIDER,
mock: true,
platformTag: input.platformTag,
receivedAt: input.now,
body: {
orderNo: input.orderNo,
productNo: input.productNo,
productName: input.productName,
buyNum: 1,
maxAmount: '1.00',
},
}),
paidAt: input.now,
createdAt: input.now,
updatedAt: input.now,
})
if (!order) {
throw createHttpError('订单创建失败', { statusCode: 500, errorCode: 'dev_mock_order_failed' })
}
return order
}
async function createBaseOrderItem(input: {
orderId: number
orderNo: string
productNo: string
productName: string
consumeShopId: string
deliveryItems: DeliveryItem[]
now: string
executor: 'lewan' | 'feifei'
productCode?: string
}) {
const snapshot =
input.executor === 'feifei'
? {
source: OPEN_91_PROVIDER,
orderNo: input.orderNo,
productNo: input.productNo,
productName: input.productName,
kuaishouFeifei: {
productCode: input.productCode || '',
productName: input.productName,
matchMode: 'mock',
},
}
: {
source: OPEN_91_PROVIDER,
orderNo: input.orderNo,
productNo: input.productNo,
productName: input.productName,
shopId: input.consumeShopId,
cloudtentacles: {
matchMode: 'mock',
normalizedProductName: input.productName,
cloudSourceKeys: ['mock-cloudtentacles'],
resolvedSourceKey: 'mock-cloudtentacles',
deliveryItems: input.deliveryItems,
},
}
const [orderItem] = await replaceOrderItems(input.orderId, [
{
skuCode: input.productName,
skuName: input.productName,
quantity: 1,
specJson: JSON.stringify({
source: OPEN_91_PROVIDER,
orderNo: input.orderNo,
productNo: input.productNo,
productName: input.productName,
}),
itemSnapshotJson: JSON.stringify(snapshot),
createdAt: input.now,
updatedAt: input.now,
},
])
if (!orderItem) {
throw createHttpError('订单商品创建失败', { statusCode: 500, errorCode: 'dev_mock_item_failed' })
}
return orderItem
}
function buildLewanMockContext(input: {
step: LewanMockStep
orderNo: string
productName: string
productNo: string
consumeShopId: string
deliveryItems: DeliveryItem[]
expectedUid: string
timestamp: string
}) {
const roleReady = ['binding', 'confirm', 'result'].includes(input.step)
const roleConfirmed = ['confirm', 'result'].includes(input.step)
const completed = input.step === 'result'
const boundUid = roleReady ? input.expectedUid || '10001' : ''
const primaryItem = input.deliveryItems[0] || {
cloudSkuId: 910001,
cloudSkuName: 'Mock 商品',
quantity: 1,
}
return {
profileKey: 'kuaishou_ct_assisted',
profileName: 'kuaishou-lewan 履约',
skuCode: input.productName,
skuName: input.productName,
claimIdentity: input.expectedUid
? {
expectedUid: input.expectedUid,
submittedAt: input.timestamp,
source: 'dev_mock',
}
: {
expectedUid: '',
submittedAt: null,
source: '',
},
kuaishouCloudFulfillment: {
flowType: 'kuaishou_cloud_fulfillment',
configId: `mock:${input.productName}`,
internalSkuCode: input.productName,
internalSkuName: input.productName,
deliveryItems: input.deliveryItems.length
? input.deliveryItems
: [primaryItem],
mock: {
enabled: true,
orderNo: input.orderNo,
productNo: input.productNo,
createdAt: input.timestamp,
},
ticket: {
code: roleReady ? `MOCK-${input.orderNo}` : '',
status: roleReady ? 'verified' : 'pending',
capturedAt: roleReady ? input.timestamp : null,
capturedBy: roleReady ? { source: 'dev_mock' } : null,
verifiedAt: roleReady ? input.timestamp : null,
oid: roleReady ? `MOCK-OID-${input.orderNo}` : '',
formToken: roleReady ? `MOCK-FORM-${input.orderNo}` : '',
leftCount: roleReady ? 1 : 0,
goodsTitle: roleReady ? input.productName : '',
},
binding: {
prepareStatus: roleReady ? 'ready' : 'pending',
cloudSourceKeys: ['mock-cloudtentacles'],
resolvedSourceKey: 'mock-cloudtentacles',
skuId: primaryItem.cloudSkuId,
skuName: primaryItem.cloudSkuName,
vnKey: '1',
vnId: roleReady ? 900001 : 0,
vnPhone: roleReady ? '13800000000' : '',
bindUrl: roleReady
? `https://example.com/mock-kuaishou-cloud-bind/${input.orderNo}`
: '',
bindPreparedAt: roleReady ? input.timestamp : null,
bindExpiresAt: roleReady ? addHours(input.timestamp, 24) : null,
bindProbeAt: roleReady ? input.timestamp : null,
bindProbeStatus: roleReady ? 'success' : '',
bindProbeMessage: '',
roleName: roleReady ? '测试角色' : '',
roleId: boundUid,
},
role: {
status: roleReady ? 'ready' : 'pending',
name: roleReady ? '测试角色' : '',
rid: boundUid,
refreshedAt: roleReady ? input.timestamp : null,
errorMessage: '',
rawInfo: roleReady ? { mock: true } : null,
defaultName: roleReady ? '默认机位角色' : '',
defaultRid: roleReady ? 'DEFAULT-000' : '',
defaultCapturedAt: roleReady ? input.timestamp : null,
defaultCaptureStatus: roleReady ? 'captured' : '',
defaultErrorMessage: '',
isDefaultRole: false,
},
purchase: {
autoBuyEnabled: true,
minAssetReserve: 0,
usedKnapsack: false,
purchaseTriggered: false,
assetBefore: 0,
assetAfter: 0,
purchaseAt: null,
items: [],
},
dispatch: {
status: completed ? 'success' : 'pending',
dispatchAt: completed ? input.timestamp : null,
dispatchBy: completed ? { source: 'dev_mock' } : null,
sendType: 0,
note: completed ? '开发 mock 已模拟发货成功' : '',
items: completed ? input.deliveryItems : [],
},
returnNumber: {
status: completed ? 'success' : 'pending',
returnedAt: completed ? input.timestamp : null,
returnedBy: completed ? { source: 'dev_mock' } : null,
autoReturnEnabled: true,
},
consume: {
status: completed ? 'success' : 'pending',
shopId: input.consumeShopId,
shopName: '',
autoConsumeEnabled: true,
consumedAt: completed ? input.timestamp : null,
errorMessage: '',
},
},
}
}
function resolveLewanTaskStatus(step: LewanMockStep) {
if (step === 'result') return TASK_STATUS.COMPLETED
if (step === 'confirm') return TASK_STATUS.ROLE_CONFIRMED
if (step === 'binding') return TASK_STATUS.WAITING_BINDING
return TASK_STATUS.LINK_GENERATED
}
function normalizeLewanStep(value: unknown): LewanMockStep {
const step = String(value || 'uid').trim()
if (['uid', 'binding', 'confirm', 'result'].includes(step)) {
return step as LewanMockStep
}
// 兼容旧 ticket 命名
if (step === 'ticket') return 'uid'
return 'uid'
}
function normalizeFeifeiStep(value: unknown): FeifeiMockStep {
const step = String(value || 'uid').trim()
if (['uid', 'ready', 'completed', 'failed'].includes(step)) {
return step as FeifeiMockStep
}
return 'uid'
}
function resolveMockUid(value: unknown, fallback: string) {
const raw = String(value ?? '').trim()
if (!raw) {
return normalizeClaimUid(fallback)
}
return assertValidClaimUid(raw)
}
function parseDeliveryItems(value: unknown): DeliveryItem[] {
if (!value) {
return [
{
cloudSkuId: 910001,
cloudSkuName: 'Mock 商品',
quantity: 1,
},
]
}
if (typeof value === 'string') {
try {
return parseDeliveryItems(JSON.parse(value))
} catch {
return parseDeliveryItems(null)
}
}
if (!Array.isArray(value)) {
return parseDeliveryItems(null)
}
const items = value
.map((item) => {
const source = item && typeof item === 'object' ? (item as Record<string, unknown>) : {}
const cloudSkuId = Number(source.cloudSkuId || source.skuId || 0) || 0
const quantity = Number(source.quantity || 1) || 1
if (!cloudSkuId) return null
return {
cloudSkuId,
cloudSkuName: String(source.cloudSkuName || source.skuName || 'Mock 商品').trim(),
quantity: quantity > 0 ? quantity : 1,
}
})
.filter((item): item is DeliveryItem => Boolean(item))
return items.length > 0 ? items : parseDeliveryItems(null)
}
function buildCreateResult(input: {
platform: 'lewan' | 'feifei'
orderId: number
orderNo: string
taskId: number
taskNo: string
claimToken: string
claimUrl: string
frontendBaseUrl?: unknown
expectedUid: string
step: string
productName: string
tips: string[]
}): DevMockCreateResult {
return {
platform: input.platform,
orderId: input.orderId,
orderNo: input.orderNo,
taskId: input.taskId,
taskNo: input.taskNo,
claimToken: input.claimToken,
claimUrl: input.claimUrl,
frontendClaimUrl: buildFrontendClaimUrl(input.claimToken, input.frontendBaseUrl),
expectedUid: input.expectedUid,
step: input.step,
productName: input.productName,
tips: input.tips,
open91QueryHint: `可在 Mock 页「91 查单」使用 orderNo=${input.orderNo},或 CLInpm run mock:open91 -- --mode=query --orderNo=${input.orderNo}`,
}
}
function buildFrontendClaimUrl(token: string, frontendBaseUrl?: unknown) {
const base = String(frontendBaseUrl || '').trim()
if (base) {
return `${base.replace(/\/+$/, '')}/#/claim/${token}`
}
return `http://127.0.0.1/#/claim/${token}`
}
function buildLewanTips(step: LewanMockStep, uid: string) {
if (step === 'uid') {
return [
`打开领取链接后,Step1 填写 UID:${uid}`,
'提交后进入绑定步;mock 绑定链接仅作展示,点击刷新/确认前请保证 UID 与角色 ID 一致',
]
}
if (step === 'binding') {
return [
`已预填 UID=${uid} 且角色 ID 已匹配`,
'可直接点「UID 已匹配,下一步」→ 确认兑换(mock 不会真实发货)',
]
}
if (step === 'confirm') {
return ['已进入确认兑换步,点击确认兑换即可模拟成功']
}
return ['已模拟兑换完成,可直接看结果页']
}
function buildFeifeiTips(step: FeifeiMockStep, uid: string) {
if (step === 'uid') {
return [`打开领取链接,Step1 填 UID${uid}`, '提交后点「打开领取链接」,URL 应带 uid 参数']
}
if (step === 'ready') {
return [`已预填 UID=${uid},可直接打开带 uid 的 H5(mock 链接,无需真实平台)`]
}
if (step === 'failed') {
return ['已模拟 feifei 失败态,用于看领取页结果展示']
}
return ['已模拟 feifei 成功完成态']
}
function signOpen91Payload(payload: Record<string, unknown>, secret: string) {
const queryString = Object.entries(payload)
.filter(([key]) => key !== 'sign')
.sort(([left], [right]) => left.localeCompare(right))
.map(([key, value]) => `${key}=${value == null ? '' : String(value)}`)
.join('&')
return crypto
.createHash('md5')
.update(`${secret}${queryString}${secret}`, 'utf8')
.digest('hex')
.toUpperCase()
}