1245 lines
37 KiB
TypeScript
1245 lines
37 KiB
TypeScript
/**
|
||
* 开发环境专用 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 { upsertKuaishouIndustryVoucher } from '../../repositories/kuaishou-industry-voucher-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 {
|
||
getKuaishouIndustrySourceConfig,
|
||
listKuaishouIndustryShopConfigs,
|
||
} from '../platforms/kuaishou-industry/source-config-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' | 'affiliate_dash'
|
||
orderId: number
|
||
orderNo: string
|
||
taskId: number
|
||
taskNo: string
|
||
claimToken: string
|
||
claimUrl: string
|
||
frontendClaimUrl: string
|
||
expectedUid: string
|
||
step: string
|
||
productName: string
|
||
tips: string[]
|
||
open91QueryHint: string
|
||
}
|
||
|
||
export type DevMockKuaishouIndustryVoucherResult = {
|
||
createdCount: number
|
||
sellerId: string
|
||
vouchers: Array<{
|
||
voucherCode: string
|
||
oid: string
|
||
status: 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'],
|
||
},
|
||
{
|
||
key: 'affiliate_dash',
|
||
name: 'affiliate-dash',
|
||
description: '本站 claim + mock 绑定二维码/提交发货(不调真实 affiliate-dash 平台)',
|
||
steps: ['uid', 'bind', 'submitted', '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'),
|
||
})
|
||
}
|
||
|
||
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),
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 生成电子凭证列表测试数据,不调用快手接口。
|
||
* 覆盖未使用、已核销、已销毁和发码失败等运营页面常见状态。
|
||
*/
|
||
export async function createKuaishouIndustryVoucherMockData(
|
||
input: {
|
||
sellerId?: unknown
|
||
} = {},
|
||
): Promise<DevMockKuaishouIndustryVoucherResult> {
|
||
assertDevMockEnabled()
|
||
|
||
const now = nowIso()
|
||
const timestamp = Date.now()
|
||
const sellerId = resolveMockIndustrySellerId(input.sellerId)
|
||
const specimens = [
|
||
{ status: 'UNUSED', callbackStatus: 'success', productName: '周卡 VIP 尊享礼包' },
|
||
{
|
||
status: 'UNUSED',
|
||
callbackStatus: 'failed',
|
||
callbackError: '开发 Mock 模拟发码回调失败',
|
||
productName: '幸运盲盒 限定款',
|
||
},
|
||
{
|
||
status: 'CONSUMED',
|
||
callbackStatus: 'success',
|
||
consumeSerialNum: `CONSUME-MOCK-${timestamp}-3`,
|
||
consumedAt: now,
|
||
productName: '星轨通行证月卡',
|
||
},
|
||
{ status: 'UNUSED', callbackStatus: 'success', productName: '精英荣耀专属礼盒' },
|
||
{
|
||
status: 'DESTROYED',
|
||
callbackStatus: 'success',
|
||
destroyedAt: now,
|
||
productName: '退款销毁测试券',
|
||
},
|
||
{
|
||
status: 'CONSUMED',
|
||
callbackStatus: 'success',
|
||
consumeSerialNum: `CONSUME-MOCK-${timestamp}-6`,
|
||
consumedAt: now,
|
||
productName: '浪漫天命皮肤礼包',
|
||
},
|
||
]
|
||
|
||
const vouchers: DevMockKuaishouIndustryVoucherResult['vouchers'] = []
|
||
for (const [index, specimen] of specimens.entries()) {
|
||
const oid = `MOCK-KS-${timestamp}-${index + 1}`
|
||
const voucher = await upsertKuaishouIndustryVoucher({
|
||
oid,
|
||
unitIndex: 1,
|
||
sellerId,
|
||
token: `mock-token-${timestamp}-${index + 1}`,
|
||
eticketType: 'GAME_OPEN_TICKET_CONSUME',
|
||
status: specimen.status,
|
||
validStartTime: timestamp - 60 * 60 * 1000,
|
||
validEndTime: timestamp + 7 * 24 * 60 * 60 * 1000,
|
||
consumeSerialNum: specimen.consumeSerialNum || '',
|
||
consumedAt: specimen.consumedAt || null,
|
||
destroyedAt: specimen.destroyedAt || null,
|
||
sendCallbackStatus: specimen.callbackStatus,
|
||
sendCallbackAttemptCount: specimen.callbackStatus === 'failed' ? 1 : 0,
|
||
sendCallbackLastError: specimen.callbackError || '',
|
||
sendCallbackResponseJson: {
|
||
mock: true,
|
||
success: specimen.callbackStatus === 'success',
|
||
},
|
||
sendCallbackSentAt: now,
|
||
rawPayloadJson: {
|
||
source: 'dev_mock',
|
||
mock: true,
|
||
productName: specimen.productName,
|
||
},
|
||
createdAt: now,
|
||
updatedAt: now,
|
||
})
|
||
|
||
if (!voucher) {
|
||
throw createHttpError('电子凭证 Mock 创建失败', {
|
||
statusCode: 500,
|
||
errorCode: 'dev_mock_kuaishou_industry_voucher_failed',
|
||
})
|
||
}
|
||
|
||
vouchers.push({
|
||
voucherCode: voucher.voucher_code,
|
||
oid: voucher.oid,
|
||
status: voucher.status,
|
||
})
|
||
}
|
||
|
||
return {
|
||
createdCount: vouchers.length,
|
||
sellerId,
|
||
vouchers,
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 生成 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' | 'affiliate_dash'
|
||
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',
|
||
},
|
||
}
|
||
: 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,
|
||
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 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) {
|
||
return normalizeClaimUid(fallback)
|
||
}
|
||
return assertValidClaimUid(raw)
|
||
}
|
||
|
||
function resolveMockIndustrySellerId(value: unknown): string {
|
||
const sellerId = String(value || '').trim()
|
||
if (sellerId) {
|
||
return sellerId
|
||
}
|
||
|
||
const shop = listKuaishouIndustryShopConfigs(getKuaishouIndustrySourceConfig()).find(
|
||
(item) => item.enabled !== false && (item.sellerId || item.shopId),
|
||
)
|
||
return String(shop?.sellerId || shop?.shopId || '141242642').trim()
|
||
}
|
||
|
||
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' | 'affiliate_dash'
|
||
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},或 CLI:npm 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 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')
|
||
.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()
|
||
}
|