优化:补充半自动履约开发 Mock 与内置 Connector
This commit is contained in:
@@ -8,22 +8,31 @@ import { createOrder, findOrderByPlatformOrderId } from '../../repositories/orde
|
||||
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 { isProductionLike } from '../../config/runtime-validation.js'
|
||||
import { runtimeConfig } from '../../config/runtime.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'
|
||||
|
||||
export type DevMockCreateResult = {
|
||||
platform: 'affiliate_dash'
|
||||
@@ -51,6 +60,21 @@ export type DevMockKuaishouIndustryVoucherResult = {
|
||||
}>
|
||||
}
|
||||
|
||||
export type DevMockSemiAutoResult = {
|
||||
platform: 'semi_auto'
|
||||
orderId: number
|
||||
orderNo: string
|
||||
orderItemId: number
|
||||
fulfillmentId: number
|
||||
configId: number
|
||||
taskId: number
|
||||
skuKey: string
|
||||
connectorMode: 'mock' | 'external'
|
||||
customerPath: string
|
||||
adminPath: string
|
||||
tips: string[]
|
||||
}
|
||||
|
||||
export function isDevMockEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
|
||||
if (String(env.ENABLE_DEV_MOCK || '').trim() === '1') {
|
||||
return true
|
||||
@@ -87,12 +111,32 @@ export function getDevMockStatus() {
|
||||
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: {
|
||||
connectorMode: semiAutoConnectorMode(),
|
||||
connectorConfigured: Boolean(
|
||||
String(runtimeConfig.semiAuto?.connectorBaseUrl || '').trim() &&
|
||||
String(runtimeConfig.semiAuto?.connectorApiKey || '').trim(),
|
||||
),
|
||||
platform: 'huya',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function semiAutoConnectorMode(): 'mock' | 'external' {
|
||||
const baseUrl = String(runtimeConfig.semiAuto?.connectorBaseUrl || '').trim()
|
||||
return baseUrl.includes('/dev-mock/connector') ? 'mock' : 'external'
|
||||
}
|
||||
|
||||
type AffiliateDashMockStep = 'uid' | 'bind' | 'submitted' | 'completed' | 'failed'
|
||||
|
||||
/**
|
||||
@@ -284,9 +328,188 @@ export async function createAffiliateDashMockClaim(
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成电子凭证列表测试数据,不调用快手接口。
|
||||
* 覆盖未使用、已核销、已销毁和发码失败等运营页面常见状态。
|
||||
* 生成半自动履约测试单:人工 SKU 配置、订单、履约和半自动任务都走真实领域服务。
|
||||
* 平台登录、绑定和浏览器票据由内置 Mock Connector 模拟。
|
||||
*/
|
||||
export async function createSemiAutoMock(
|
||||
input: {
|
||||
orderNo?: unknown
|
||||
skuName?: unknown
|
||||
quantity?: unknown
|
||||
pageTitle?: unknown
|
||||
pageDescription?: unknown
|
||||
browserTargetUrl?: unknown
|
||||
} = {},
|
||||
): Promise<DevMockSemiAutoResult> {
|
||||
assertDevMockEnabled()
|
||||
|
||||
const orderNo = String(input.orderNo || `MOCKSA${Date.now()}`).trim()
|
||||
const skuKey = normalizeMockSemiAutoSku(input.skuName)
|
||||
const quantity = normalizeMockSemiAutoQuantity(input.quantity)
|
||||
const pageTitle = String(input.pageTitle || '虎牙账号授权测试')
|
||||
.trim()
|
||||
.slice(0, 60)
|
||||
const pageDescription = String(
|
||||
input.pageDescription || '开发 Mock:完成登录和角色绑定后,客服在后台完成履约。',
|
||||
)
|
||||
.trim()
|
||||
.slice(0, 500)
|
||||
const browserTargetUrl = String(input.browserTargetUrl || '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: 'huya',
|
||||
semiAutoConfig: {
|
||||
pageTitle,
|
||||
pageDescription,
|
||||
loginInstructions: 'Mock 登录:点击获取二维码后,页面轮询两次会自动模拟登录成功。',
|
||||
bindingInstructions: 'Mock 绑定:页面轮询两次后会自动识别角色,点击确认即可。',
|
||||
browserTargets: [{ key: 'huya', label: '虎牙', 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: 'huya', mock: true },
|
||||
}),
|
||||
itemSnapshotJson: JSON.stringify({
|
||||
source: 'dev_mock',
|
||||
skuKey,
|
||||
platform: 'huya',
|
||||
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',
|
||||
})
|
||||
}
|
||||
|
||||
const connectorMode = semiAutoConnectorMode()
|
||||
return {
|
||||
platform: 'semi_auto',
|
||||
orderId: Number(order.id),
|
||||
orderNo,
|
||||
orderItemId,
|
||||
fulfillmentId,
|
||||
configId,
|
||||
taskId,
|
||||
skuKey,
|
||||
connectorMode,
|
||||
customerPath: `/c/${encodeURIComponent(orderNo)}`,
|
||||
adminPath: `/admin/semi-auto-tasks?keyword=${encodeURIComponent(orderNo)}`,
|
||||
tips: [
|
||||
'打开客户页,点击“获取登录二维码”;Mock Connector 会在两次轮询后自动模拟登录成功。',
|
||||
'登录后页面会自动创建绑定二维码,轮询两次后识别角色,点击“确认绑定角色”。',
|
||||
'后台“任务 → 半自动履约”领取任务后生成浏览器票据;扩展消费票据后写入 Mock Cookie。',
|
||||
'完成履约仍走真实确认流程,会写入成本快照和财务确认。',
|
||||
connectorMode === 'mock'
|
||||
? '当前使用内置 Mock Connector,不需要启动 live-hub-py。'
|
||||
: '当前配置的是外部 Connector;请确认 Connector 已启动,否则登录和绑定会失败。',
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeMockSemiAutoSku(value: unknown) {
|
||||
const raw = String(value || '').trim()
|
||||
const withCjk = raw
|
||||
? containsCjk(raw)
|
||||
? raw
|
||||
: `虎牙半自动-${raw}`
|
||||
: `虎牙半自动测试-${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
|
||||
@@ -499,7 +722,9 @@ async function createBaseOrder(input: {
|
||||
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,
|
||||
@@ -511,7 +736,7 @@ async function createBaseOrder(input: {
|
||||
buyerId: 'mock-buyer',
|
||||
buyerName: 'mock 领取客户',
|
||||
receiverContact: '',
|
||||
totalAmount: 100,
|
||||
totalAmount,
|
||||
currency: 'CNY',
|
||||
rawPayloadJson: JSON.stringify({
|
||||
source: OPEN_91_PROVIDER,
|
||||
@@ -523,7 +748,7 @@ async function createBaseOrder(input: {
|
||||
productNo: input.productNo,
|
||||
productName: input.productName,
|
||||
buyNum: 1,
|
||||
maxAmount: '1.00',
|
||||
maxAmount: (totalAmount / 100).toFixed(2),
|
||||
},
|
||||
}),
|
||||
paidAt: input.now,
|
||||
|
||||
Reference in New Issue
Block a user