Files
order_site/apps/backend/src/services/dev-mock/dev-mock-service.ts
T
yml 9d738becd3 优化半自动履约客户页与后台操作布局
用户侧统一显示等待客服处理并精简平台标题;移除 Cookie 等专业表述;后台操作改为两行行内按钮,取消操作下拉。
2026-11-14 01:23:15 +08:00

945 lines
29 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 { upsertKuaishouIndustryVoucher } from '../../repositories/kuaishou-industry-voucher-repo.js'
import { query, withTransaction } from '../../db/client.js'
import {
getFulfillmentProfileByKey,
upsertFulfillmentProfile,
} from '../../repositories/fulfillment-profile-repo.js'
import { buildOrderClaimUrl, createTaskClaimToken } from '../claim/claim-service.js'
import { assertValidClaimUid, normalizeClaimUid } from '../claim/claim-identity.js'
import { saveManualFulfillmentConfig } from '../cost-center/manual-fulfillment-config-service.js'
import {
assignFulfillmentChannelsWithExecutor,
attachFulfillmentConfigurationsWithExecutor,
} from '../fulfillment/fulfillment-state-service.js'
import { containsCjk } from '../fulfillment/internal-sku-resolver.js'
import { OPEN_91_PLATFORM, OPEN_91_PROVIDER, assertOpen91Config } from '../open-91/config.js'
import {
getKuaishouIndustrySourceConfig,
listKuaishouIndustryShopConfigs,
} from '../platforms/kuaishou-industry/source-config-service.js'
import { createHttpError } from '../../utils/http.js'
import { nowIso } from '../../utils/time.js'
import { randomId } from '../../utils/random.js'
import { TASK_STATUS } from '../../domain/task-status.js'
import { ensureSemiAutoTasksForOrder } from '../semi-auto/semi-auto-task-service.js'
import {
defaultSemiAutoConnectorMode,
isExternalSemiAutoConnectorConfigured,
type SemiAutoConnectorMode,
} from '../semi-auto/huya-connector-client.js'
import { assertDevMockEnabled, isDevMockEnabled } from './dev-mock-enabled.js'
export { assertDevMockEnabled, isDevMockEnabled }
export type DevMockCreateResult = {
platform: '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
}>
}
export type DevMockSemiAutoResult = {
platform: 'semi_auto'
semiAutoPlatform: 'huya' | 'weibo'
orderId: number
orderNo: string
orderItemId: number
fulfillmentId: number
configId: number
taskId: number
skuKey: string
connectorMode: SemiAutoConnectorMode
connectorConfigured: boolean
customerPath: string
adminPath: string
tips: string[]
}
export function getDevMockStatus() {
const enabled = isDevMockEnabled()
return {
enabled,
nodeEnv: String(process.env.NODE_ENV || 'development'),
platforms: enabled
? [
{
key: 'affiliate_dash',
name: 'affiliate-dash',
description: '本站 claim + mock 绑定二维码/提交发货(不调真实 affiliate-dash 平台)',
steps: ['uid', 'bind', 'submitted', 'completed', 'failed'],
},
{
key: 'semi_auto',
name: '半自动履约',
description:
'创建人工渠道半自动测试单;可选择仅 Mock 订单,登录、绑定和票据注入走真实 Connector',
steps: ['create_order', 'login', 'binding', 'browser_ticket', 'complete'],
},
]
: [],
defaultUid: '10001',
semiAuto: {
defaultConnectorMode: defaultSemiAutoConnectorMode(),
externalConnectorConfigured: isExternalSemiAutoConnectorConfigured(),
mockConnectorAvailable: enabled,
platform: 'huya',
},
}
}
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,
now,
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, orderNo)
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: buildOrderClaimUrl(orderNo),
frontendBaseUrl: input.frontendBaseUrl,
expectedUid: fallbackUid,
step,
productName,
tips: buildAffiliateDashTips(step, fallbackUid),
})
}
/**
* 生成半自动履约测试单:人工 SKU 配置、订单、履约和半自动任务都走真实领域服务。
* 平台登录、绑定和浏览器票据按 connectorMode 选择真实 Connector 或内置 Mock。
*/
export async function createSemiAutoMock(
input: {
platform?: unknown
orderNo?: unknown
skuName?: unknown
quantity?: unknown
pageTitle?: unknown
pageDescription?: unknown
browserTargetUrl?: unknown
connectorMode?: unknown
} = {},
): Promise<DevMockSemiAutoResult> {
assertDevMockEnabled()
const platform = normalizeMockSemiAutoPlatform(input.platform)
const orderNo = String(
input.orderNo || `${platform === 'weibo' ? 'MOCKWB' : 'MOCKSA'}${Date.now()}`,
).trim()
const skuKey = normalizeMockSemiAutoSku(input.skuName, platform)
const quantity = normalizeMockSemiAutoQuantity(input.quantity)
// 微博目前只支持内置 Connector,忽略其他模式,避免生成必然失败的测试单。
const connectorMode =
platform === 'weibo' ? 'embedded' : normalizeMockSemiAutoConnectorMode(input.connectorMode)
const pageTitle = String(
input.pageTitle || (platform === 'weibo' ? '微博发帖授权测试' : '虎牙账号授权测试'),
)
.trim()
.slice(0, 60)
const pageDescription = String(
input.pageDescription ||
(platform === 'weibo'
? '开发 Mock:完成微博登录即视为授权,客服在后台完成发帖履约。'
: '开发 Mock:完成登录和角色绑定后,客服在后台完成履约。'),
)
.trim()
.slice(0, 500)
const browserTargetUrl = String(
input.browserTargetUrl ||
(platform === 'weibo' ? 'https://weibo.com/' : 'https://www.huya.com/'),
)
.trim()
.slice(0, 1000)
await assertOrderNoAvailable(orderNo)
const config = await saveManualFulfillmentConfig({
skuKey,
enabled: true,
note: '开发 Mock 半自动履约',
channels: [{ platformKey: 'fund_cash', creditQuantity: 100, enabled: true }],
semiAutoEnabled: true,
semiAutoPlatform: platform,
semiAutoConfig: {
pageTitle,
pageDescription,
loginInstructions:
platform === 'weibo'
? '真实登录:使用微博绑定手机号接收验证码登录,或使用微博 App 扫码登录。登录信息仅用于本次订单履约。'
: connectorMode === 'mock'
? 'Mock 登录:点击获取二维码后,页面轮询两次会自动模拟登录成功。'
: '真实登录:点击获取二维码后,请使用虎牙 App 扫码并在手机上确认登录。',
...(platform === 'weibo' ? { loginMethods: ['sms', 'qr'] } : {}),
browserTargets: [
{
key: platform,
label: platform === 'weibo' ? '微博' : '虎牙',
url: browserTargetUrl,
},
],
},
})
const configId = Number(config.id || 0)
const canonicalProductId = config.canonicalProductId ? Number(config.canonicalProductId) : null
if (!configId) {
throw createHttpError('半自动履约 Mock 配置创建失败', {
statusCode: 500,
errorCode: 'dev_mock_semi_auto_config_failed',
})
}
const now = nowIso()
const order = await createBaseOrder({
orderNo,
productNo: skuKey,
productName: skuKey,
now,
platformTag: 'semi-auto',
totalAmount: 100 * quantity,
})
const [orderItem] = await replaceOrderItems(order.id, [
{
skuCode: skuKey,
skuName: skuKey,
quantity,
specJson: JSON.stringify({
source: 'dev_mock',
skuNick: skuKey,
semiAuto: { platform, mock: true },
}),
itemSnapshotJson: JSON.stringify({
source: 'dev_mock',
skuKey,
platform,
mock: true,
}),
createdAt: now,
updatedAt: now,
},
])
const orderItemId = Number(orderItem?.id || 0)
if (!orderItemId) {
throw createHttpError('半自动履约 Mock 订单项创建失败', {
statusCode: 500,
errorCode: 'dev_mock_semi_auto_item_failed',
})
}
const fulfillmentResult = await query<{ id: number }>(
'SELECT id FROM fulfillments WHERE order_item_id = $1 LIMIT 1',
[orderItemId],
)
const fulfillmentId = Number(fulfillmentResult.rows[0]?.id || 0)
if (!fulfillmentId) {
throw createHttpError('半自动履约 Mock 履约单创建失败', {
statusCode: 500,
errorCode: 'dev_mock_semi_auto_fulfillment_failed',
})
}
await withTransaction(async (client) => {
await assignFulfillmentChannelsWithExecutor(client, {
assignments: [{ orderItemId, channel: 'manual', source: 'manual_confirm' }],
eventType: 'dev_mock_semi_auto_channel_assigned',
reason: 'dev_mock',
actor: { type: 'system' },
})
await attachFulfillmentConfigurationsWithExecutor(client, {
mappings: [
{
orderItemId,
configId,
...(canonicalProductId ? { canonicalProductId } : {}),
},
],
expected: { status: 'assigned', channel: 'manual' },
onlyWhenConfigNull: true,
})
})
const ensured = await ensureSemiAutoTasksForOrder(orderNo)
let taskId = Number(ensured.createdIds[0] || 0)
if (!taskId) {
const existing = await query<{ id: number }>(
'SELECT id FROM semi_auto_tasks WHERE order_item_id = $1 LIMIT 1',
[orderItemId],
)
taskId = Number(existing.rows[0]?.id || 0)
}
if (!taskId) {
throw createHttpError('半自动履约 Mock 任务创建失败', {
statusCode: 500,
errorCode: 'dev_mock_semi_auto_task_failed',
})
}
await query('UPDATE semi_auto_tasks SET connector_mode = $2, updated_at = NOW() WHERE id = $1', [
taskId,
connectorMode,
])
const connectorConfigured =
connectorMode === 'external' ? isExternalSemiAutoConnectorConfigured() : true
return {
platform: 'semi_auto',
semiAutoPlatform: platform,
orderId: Number(order.id),
orderNo,
orderItemId,
fulfillmentId,
configId,
taskId,
skuKey,
connectorMode,
connectorConfigured,
customerPath: `/c/${encodeURIComponent(orderNo)}`,
adminPath: `/admin/semi-auto-tasks?keyword=${encodeURIComponent(orderNo)}`,
tips:
platform === 'weibo'
? [
'完成履约仍走真实确认流程,会写入成本快照和财务确认。',
'微博使用内置 Connector:短信验证码会真实发送到客户手机号;扫码登录需在微博 App 中确认。',
'后台领取任务后生成 weibo-ticket-* 浏览器票据,扩展消费票据后写入微博 Cookie。',
'微博登录成功即视为授权完成,任务会直接进入等待客服处理。',
]
: [
'完成履约仍走真实确认流程,会写入成本快照和财务确认。',
connectorMode === 'mock'
? '当前使用内置 Mock Connector:打开客户页点击登录,轮询两次后自动登录;绑定同样自动识别角色。'
: connectorMode === 'external'
? connectorConfigured
? '当前使用远程真实 Connector:订单是 Mock 的,登录、绑定和浏览器票据都走外部服务。'
: '当前选择远程真实 Connector,但 SEMI_AUTO_CONNECTOR_BASE_URL / API_KEY 未配置,登录会失败。'
: '当前使用 order_site 内置真实 Connector:无需启动 live-hub-py,扫码登录、绑定和票据都在本进程完成。',
connectorMode === 'mock'
? '后台领取任务后生成浏览器票据;扩展消费票据后写入 Mock Cookie。'
: connectorMode === 'external'
? '请确保远程 Connector 已启动,并保证两边 SEMI_AUTO_CONNECTOR_API_KEY 一致。'
: '登录凭据只保存在当前进程内存中,重启后端会要求客户重新扫码。',
],
}
}
function normalizeMockSemiAutoPlatform(value: unknown): 'huya' | 'weibo' {
return String(value || '')
.trim()
.toLowerCase() === 'weibo'
? 'weibo'
: 'huya'
}
function normalizeMockSemiAutoConnectorMode(value: unknown): SemiAutoConnectorMode {
const mode = String(value || '')
.trim()
.toLowerCase()
if (mode === 'external' || mode === 'mock') return mode
return 'embedded'
}
function normalizeMockSemiAutoSku(value: unknown, platform: 'huya' | 'weibo' = 'huya') {
const prefix = platform === 'weibo' ? '微博半自动' : '虎牙半自动'
const raw = String(value || '').trim()
const withCjk = raw
? containsCjk(raw)
? raw
: `${prefix}-${raw}`
: `${prefix}测试-${Date.now()}`
return withCjk.slice(0, 48)
}
function normalizeMockSemiAutoQuantity(value: unknown) {
const parsed = Math.trunc(Number(value || 1))
if (!Number.isFinite(parsed) || parsed < 1) return 1
return Math.min(10, parsed)
}
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
totalAmount?: number
}) {
const totalAmount = input.totalAmount ?? 100
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,
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: (totalAmount / 100).toFixed(2),
},
}),
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
now: string
productCode?: string
}) {
const snapshot = {
source: OPEN_91_PROVIDER,
orderNo: input.orderNo,
productNo: input.productNo,
productName: input.productName,
affiliateDash: {
sku: input.productCode || input.productNo,
productName: input.productName,
matchMode: 'mock',
},
}
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 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 buildCreateResult(input: {
platform: '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: buildOrderClaimUrl(input.orderNo, input.frontendBaseUrl || undefined),
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 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()
}