Files
order_site/apps/backend/src/services/dev-mock/dev-mock-service.ts
T
yml2213 3c0d1f108c 新增开发 Mock 后台菜单与造单能力
非生产环境在后台提供 lewan/feifei 领取造单与 91 查单模拟,CLI 复用同一服务,避免依赖真实平台联调。
2026-07-10 13:35:42 +08:00

859 lines
26 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 开发环境专用 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()
}