优化订单履约分流与后台筛选
This commit is contained in:
@@ -6,7 +6,9 @@ import type { OrderItemRow, OrderRow, TaskRow } from '../../types/repository/row
|
||||
import type { JsonObject } from '../../types/json.js'
|
||||
|
||||
type CreatedTaskInput = {
|
||||
orderId?: number
|
||||
orderItemId?: number
|
||||
unitIndex?: number
|
||||
contextJson?: string
|
||||
taskStatus?: string
|
||||
executorKey?: string
|
||||
@@ -59,9 +61,9 @@ function stubOrderItem(
|
||||
function stubCreatedTask(id: number, input: CreatedTaskInput): TaskRow {
|
||||
return {
|
||||
id,
|
||||
order_id: 0,
|
||||
order_id: Number(input.orderId || 0),
|
||||
order_item_id: Number(input.orderItemId || 0),
|
||||
unit_index: 1,
|
||||
unit_index: Number(input.unitIndex || 1),
|
||||
platform_order_id: '',
|
||||
profile_id: 0,
|
||||
task_no: 'DT-test',
|
||||
@@ -145,6 +147,7 @@ test('syncDeliveryTasksForOrder 多账号候选时不提前固定 cloudtentacles
|
||||
},
|
||||
nowIso: () => '2026-05-29T09:05:46.658Z',
|
||||
randomId: () => 'DT-test',
|
||||
createTaskEvent: async () => null,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -194,6 +197,7 @@ test('syncDeliveryTasksForOrder 单账号候选时保留固定 cloudtentacles
|
||||
},
|
||||
nowIso: () => '2026-05-29T09:05:46.658Z',
|
||||
randomId: () => 'DT-test',
|
||||
createTaskEvent: async () => null,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -203,3 +207,229 @@ test('syncDeliveryTasksForOrder 单账号候选时保留固定 cloudtentacles
|
||||
assert.deepEqual(binding.cloudSourceKeys, ['account-a'])
|
||||
assert.equal(binding.resolvedSourceKey, 'account-a')
|
||||
})
|
||||
|
||||
test('syncDeliveryTasksForOrder 已有部分任务时按订单项补建缺失任务', async () => {
|
||||
const existingTask = stubCreatedTask(201, {
|
||||
orderId: 3,
|
||||
orderItemId: 20,
|
||||
unitIndex: 1,
|
||||
executorKey: 'kuaishou_ct_assisted',
|
||||
})
|
||||
const createdInputs: CreatedTaskInput[] = []
|
||||
const taskEvents: Array<{ taskId: number; eventType: string }> = []
|
||||
|
||||
const tasks = await syncDeliveryTasksForOrderWithDeps(
|
||||
stubOrder({ id: 3, platform_order_id: 'partial-order' }),
|
||||
[
|
||||
stubOrderItem({
|
||||
id: 20,
|
||||
order_id: 3,
|
||||
sku_code: 'SKU-existing',
|
||||
item_snapshot_json: buildCloudSnapshot(74, '商品 A'),
|
||||
}),
|
||||
stubOrderItem({
|
||||
id: 21,
|
||||
order_id: 3,
|
||||
sku_code: 'SKU-missing',
|
||||
item_snapshot_json: buildCloudSnapshot(75, '商品 B'),
|
||||
}),
|
||||
],
|
||||
{
|
||||
listTasksByOrderId: async () => [existingTask],
|
||||
getFulfillmentProfileByKey: async () => buildCloudProfile(),
|
||||
createTask: async (input) => {
|
||||
createdInputs.push(input as CreatedTaskInput)
|
||||
return stubCreatedTask(202, input as CreatedTaskInput)
|
||||
},
|
||||
createTaskEvent: async (taskId, eventType) => {
|
||||
taskEvents.push({ taskId: Number(taskId), eventType })
|
||||
return null
|
||||
},
|
||||
logInfo: () => null,
|
||||
logWarn: () => null,
|
||||
nowIso: () => '2026-05-29T09:05:46.658Z',
|
||||
randomId: () => 'DT-partial',
|
||||
},
|
||||
)
|
||||
|
||||
assert.deepEqual(
|
||||
createdInputs.map((input) => [input.orderItemId, input.unitIndex]),
|
||||
[[21, 1]],
|
||||
)
|
||||
assert.deepEqual(
|
||||
tasks.map((task) => task.id),
|
||||
[201, 202],
|
||||
)
|
||||
assert.deepEqual(taskEvents, [{ taskId: 202, eventType: 'fulfillment_task_planned' }])
|
||||
})
|
||||
|
||||
test('syncDeliveryTasksForOrder 数量增加时只补建缺失单元', async () => {
|
||||
const existingTask = stubCreatedTask(301, {
|
||||
orderId: 4,
|
||||
orderItemId: 30,
|
||||
unitIndex: 1,
|
||||
executorKey: 'kuaishou_ct_assisted',
|
||||
})
|
||||
const createdInputs: CreatedTaskInput[] = []
|
||||
|
||||
const tasks = await syncDeliveryTasksForOrderWithDeps(
|
||||
stubOrder({ id: 4, platform_order_id: 'quantity-increased' }),
|
||||
[
|
||||
stubOrderItem({
|
||||
id: 30,
|
||||
order_id: 4,
|
||||
sku_code: 'SKU-quantity',
|
||||
quantity: 2,
|
||||
item_snapshot_json: buildCloudSnapshot(74, '商品 A'),
|
||||
}),
|
||||
],
|
||||
{
|
||||
listTasksByOrderId: async () => [existingTask],
|
||||
getFulfillmentProfileByKey: async () => buildCloudProfile(),
|
||||
createTask: async (input) => {
|
||||
createdInputs.push(input as CreatedTaskInput)
|
||||
return stubCreatedTask(302, input as CreatedTaskInput)
|
||||
},
|
||||
createTaskEvent: async () => null,
|
||||
logInfo: () => null,
|
||||
logWarn: () => null,
|
||||
nowIso: () => '2026-05-29T09:05:46.658Z',
|
||||
randomId: () => 'DT-quantity',
|
||||
},
|
||||
)
|
||||
|
||||
assert.deepEqual(
|
||||
createdInputs.map((input) => input.unitIndex),
|
||||
[2],
|
||||
)
|
||||
assert.deepEqual(
|
||||
tasks.map((task) => task.unit_index),
|
||||
[1, 2],
|
||||
)
|
||||
})
|
||||
|
||||
test('syncDeliveryTasksForOrder 规划失败时记录可定位的路由诊断', async () => {
|
||||
const warnings: Array<{ message: string; detail: Record<string, unknown> }> = []
|
||||
|
||||
const tasks = await syncDeliveryTasksForOrderWithDeps(
|
||||
stubOrder({ id: 5, platform_order_id: 'planning-failed' }),
|
||||
[
|
||||
stubOrderItem({
|
||||
id: 40,
|
||||
order_id: 5,
|
||||
sku_code: 'SKU-broken',
|
||||
item_snapshot_json: {
|
||||
isConfigured: true,
|
||||
fulfillmentRoute: {
|
||||
selectedExecutorKey: 'kuaishou_ct_assisted',
|
||||
reason: '规则指定云履约',
|
||||
},
|
||||
cloudtentacles: {
|
||||
cloudSkuId: 74,
|
||||
cloudSkuName: '商品 A',
|
||||
cloudSourceKeys: [],
|
||||
},
|
||||
} as JsonObject,
|
||||
}),
|
||||
],
|
||||
{
|
||||
listTasksByOrderId: async () => [],
|
||||
getFulfillmentProfileByKey: async () => buildCloudProfile(),
|
||||
createTaskEvent: async () => null,
|
||||
logInfo: () => null,
|
||||
logWarn: (_scope, message, detail) => {
|
||||
warnings.push({
|
||||
message: String(message),
|
||||
detail: detail as Record<string, unknown>,
|
||||
})
|
||||
return null
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert.deepEqual(tasks, [])
|
||||
assert.equal(warnings.length, 1)
|
||||
assert.equal(warnings[0]?.message, '订单项已配置履约路由但无法生成任务计划')
|
||||
assert.equal(warnings[0]?.detail.orderItemId, 40)
|
||||
assert.deepEqual(warnings[0]?.detail.missingUnitIndexes, [1])
|
||||
assert.deepEqual(warnings[0]?.detail.planningDiagnostic, {
|
||||
snapshotIsConfigured: true,
|
||||
selectedExecutorKey: 'kuaishou_ct_assisted',
|
||||
routeReason: '规则指定云履约',
|
||||
candidateAvailability: [],
|
||||
sourceItemTitle: '',
|
||||
sourceSkuNick: '',
|
||||
cloudSkuId: 74,
|
||||
cloudSkuName: '商品 A',
|
||||
cloudSourceKeys: [],
|
||||
feifeiProductCode: '',
|
||||
affiliateDashSku: '',
|
||||
})
|
||||
})
|
||||
|
||||
test('syncDeliveryTasksForOrder 并发创建冲突时复用已落库任务', async () => {
|
||||
const concurrentTask = stubCreatedTask(401, {
|
||||
orderId: 6,
|
||||
orderItemId: 50,
|
||||
unitIndex: 1,
|
||||
executorKey: 'kuaishou_ct_assisted',
|
||||
})
|
||||
let listCount = 0
|
||||
let eventCount = 0
|
||||
|
||||
const tasks = await syncDeliveryTasksForOrderWithDeps(
|
||||
stubOrder({ id: 6, platform_order_id: 'concurrent-order' }),
|
||||
[
|
||||
stubOrderItem({
|
||||
id: 50,
|
||||
order_id: 6,
|
||||
sku_code: 'SKU-concurrent',
|
||||
item_snapshot_json: buildCloudSnapshot(74, '商品 A'),
|
||||
}),
|
||||
],
|
||||
{
|
||||
listTasksByOrderId: async () => (listCount++ === 0 ? [] : [concurrentTask]),
|
||||
getFulfillmentProfileByKey: async () => buildCloudProfile(),
|
||||
createTask: async () => {
|
||||
throw new Error('duplicate key value violates unique constraint')
|
||||
},
|
||||
createTaskEvent: async () => {
|
||||
eventCount += 1
|
||||
return null
|
||||
},
|
||||
logInfo: () => null,
|
||||
logWarn: () => null,
|
||||
nowIso: () => '2026-05-29T09:05:46.658Z',
|
||||
randomId: () => 'DT-concurrent',
|
||||
},
|
||||
)
|
||||
|
||||
assert.deepEqual(
|
||||
tasks.map((task) => task.id),
|
||||
[401],
|
||||
)
|
||||
assert.equal(eventCount, 0)
|
||||
})
|
||||
|
||||
function buildCloudSnapshot(cloudSkuId: number, cloudSkuName: string): JsonObject {
|
||||
return {
|
||||
isConfigured: true,
|
||||
fulfillmentRoute: {
|
||||
selectedExecutorKey: 'kuaishou_ct_assisted',
|
||||
},
|
||||
cloudtentacles: {
|
||||
cloudSourceKeys: ['account-a'],
|
||||
cloudSkuId,
|
||||
cloudSkuName,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function buildCloudProfile() {
|
||||
return {
|
||||
id: 9,
|
||||
profile_key: 'kuaishou_ct_assisted',
|
||||
profile_name: 'kuaishou-lewan 履约',
|
||||
executor_key: 'kuaishou_ct_assisted',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createTask, listTasksByOrderId, updateTask } from '../../repositories/task-repo.js'
|
||||
import { createTaskEvent } from '../../repositories/task-event-repo.js'
|
||||
import { getFulfillmentProfileByKey } from '../../repositories/fulfillment-profile-repo.js'
|
||||
import { createTaskClaimToken } from '../claim/claim-service.js'
|
||||
import { notifyTaskAutoManualReview } from '../notification/domain-notifications.js'
|
||||
@@ -10,7 +11,9 @@ import { preparePaidFulfillmentTask } from '../fulfillment/executors/registry.js
|
||||
import type { FulfillmentPrepareDeps } from '../fulfillment/executors/types.js'
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
import { randomId } from '../../utils/random.js'
|
||||
import { logInfo, logWarn } from '../../utils/logger.js'
|
||||
import { TASK_STATUS, resolveInitialPaidTaskStatus } from '../../domain/task-status.js'
|
||||
import type { JsonObject } from '../../types/json.js'
|
||||
import type { OrderItemRow, OrderRow, TaskRow } from '../../types/repository/rows.js'
|
||||
|
||||
type ClaimTokenLike = {
|
||||
@@ -37,6 +40,9 @@ type DeliveryTaskDeps = {
|
||||
}) => Promise<unknown> | unknown
|
||||
nowIso?: () => string
|
||||
randomId?: (prefix?: string) => string
|
||||
createTaskEvent?: typeof createTaskEvent
|
||||
logInfo?: typeof logInfo
|
||||
logWarn?: typeof logWarn
|
||||
}
|
||||
|
||||
export async function syncDeliveryTasksForOrder(
|
||||
@@ -60,6 +66,9 @@ export async function syncDeliveryTasksForOrderWithDeps(
|
||||
notifyTaskAutoManualReview: notifyManualReview = notifyTaskAutoManualReview,
|
||||
nowIso: getNowIso = nowIso,
|
||||
randomId: createRandomId = randomId,
|
||||
createTaskEvent: appendTaskEvent = createTaskEvent,
|
||||
logInfo: writeInfoLog = logInfo,
|
||||
logWarn: writeWarnLog = logWarn,
|
||||
} = deps
|
||||
|
||||
const runtimeDeps: FulfillmentPrepareDeps = {
|
||||
@@ -70,90 +79,216 @@ export async function syncDeliveryTasksForOrderWithDeps(
|
||||
}
|
||||
|
||||
const existingTasks = await listTasks(order.id)
|
||||
const tasks: DeliveryTaskRow[] = [...existingTasks]
|
||||
const existingTaskKeys = new Set(existingTasks.map(buildTaskUnitKey))
|
||||
|
||||
if (existingTasks.length > 0) {
|
||||
if (order.pay_status !== 'paid') {
|
||||
return existingTasks
|
||||
}
|
||||
|
||||
return preparePaidTasks(existingTasks, runtimeDeps)
|
||||
}
|
||||
|
||||
const tasks: DeliveryTaskRow[] = []
|
||||
writeInfoLog('[delivery-task-service]', '开始核对订单履约任务完整性', {
|
||||
...buildOrderLogContext(order),
|
||||
orderItemCount: orderItems.length,
|
||||
existingTaskCount: existingTasks.length,
|
||||
})
|
||||
|
||||
for (const item of orderItems) {
|
||||
const plan = await planFulfillmentTaskForOrderItem({
|
||||
order,
|
||||
item,
|
||||
getProfileByKey,
|
||||
})
|
||||
const quantity = Math.max(1, Number(item.quantity || 1))
|
||||
const missingUnitIndexes = Array.from({ length: quantity }, (_, index) => index + 1).filter(
|
||||
(unitIndex) => !existingTaskKeys.has(buildOrderItemUnitKey(item.id, unitIndex)),
|
||||
)
|
||||
|
||||
if (!plan) {
|
||||
if (missingUnitIndexes.length === 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
const quantity = Math.max(1, Number(item.quantity || 1))
|
||||
let plan
|
||||
try {
|
||||
plan = await planFulfillmentTaskForOrderItem({
|
||||
order,
|
||||
item,
|
||||
getProfileByKey,
|
||||
})
|
||||
} catch (error) {
|
||||
writeWarnLog('[delivery-task-service]', '订单项履约任务规划执行异常', {
|
||||
...buildOrderLogContext(order),
|
||||
...buildOrderItemLogContext(item),
|
||||
missingUnitIndexes,
|
||||
planningDiagnostic: buildPlanningDiagnostic(item),
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
})
|
||||
throw error
|
||||
}
|
||||
|
||||
for (let index = 0; index < quantity; index += 1) {
|
||||
if (!plan) {
|
||||
writeWarnLog('[delivery-task-service]', '订单项已配置履约路由但无法生成任务计划', {
|
||||
...buildOrderLogContext(order),
|
||||
...buildOrderItemLogContext(item),
|
||||
missingUnitIndexes,
|
||||
planningDiagnostic: buildPlanningDiagnostic(item),
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
for (const unitIndex of missingUnitIndexes) {
|
||||
const createdAt = getNowIso()
|
||||
const initialStatus =
|
||||
order.pay_status === 'paid'
|
||||
? resolvePaidTaskStatus(plan.profile)
|
||||
: TASK_STATUS.PENDING_PAYMENT
|
||||
|
||||
const task = await createDeliveryTask({
|
||||
orderId: order.id,
|
||||
orderItemId: item.id,
|
||||
unitIndex: index + 1,
|
||||
provider: order.provider,
|
||||
platform: order.platform,
|
||||
shopId: order.shop_id,
|
||||
shopName: order.shop_name,
|
||||
platformOrderId: order.platform_order_id,
|
||||
taskNo: createRandomId('DT'),
|
||||
profileId: plan.profileId,
|
||||
executorKey: plan.executorKey,
|
||||
taskStatus: initialStatus,
|
||||
deliveryStatus: 'pending',
|
||||
resultCode: '',
|
||||
resultMessage: '',
|
||||
claimToken: '',
|
||||
claimExpiresAt: null,
|
||||
automationMode: plan.autoDispatch ? 'automatic' : 'manual',
|
||||
requiresClaim: plan.requiresClaim,
|
||||
userActionStatus: plan.requiresClaim ? 'pending_claim' : 'not_required',
|
||||
attemptCount: 0,
|
||||
lastError: '',
|
||||
contextJson: JSON.stringify(plan.context),
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
})
|
||||
let createdFresh = true
|
||||
let task: TaskRow | null
|
||||
try {
|
||||
task = await createDeliveryTask({
|
||||
orderId: order.id,
|
||||
orderItemId: item.id,
|
||||
unitIndex,
|
||||
provider: order.provider,
|
||||
platform: order.platform,
|
||||
shopId: order.shop_id,
|
||||
shopName: order.shop_name,
|
||||
platformOrderId: order.platform_order_id,
|
||||
taskNo: createRandomId('DT'),
|
||||
profileId: plan.profileId,
|
||||
executorKey: plan.executorKey,
|
||||
taskStatus: initialStatus,
|
||||
deliveryStatus: 'pending',
|
||||
resultCode: '',
|
||||
resultMessage: '',
|
||||
claimToken: '',
|
||||
claimExpiresAt: null,
|
||||
automationMode: plan.autoDispatch ? 'automatic' : 'manual',
|
||||
requiresClaim: plan.requiresClaim,
|
||||
userActionStatus: plan.requiresClaim ? 'pending_claim' : 'not_required',
|
||||
attemptCount: 0,
|
||||
lastError: '',
|
||||
contextJson: JSON.stringify(plan.context),
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
})
|
||||
} catch (error) {
|
||||
const concurrentTasks = await listTasks(order.id)
|
||||
task =
|
||||
concurrentTasks.find(
|
||||
(candidate) =>
|
||||
buildTaskUnitKey(candidate) === buildOrderItemUnitKey(item.id, unitIndex),
|
||||
) || null
|
||||
if (!task) {
|
||||
writeWarnLog('[delivery-task-service]', '履约任务创建执行异常', {
|
||||
...buildOrderLogContext(order),
|
||||
...buildOrderItemLogContext(item),
|
||||
unitIndex,
|
||||
profileId: plan.profileId,
|
||||
profileKey: plan.profileKey,
|
||||
executorKey: plan.executorKey,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
})
|
||||
throw error
|
||||
}
|
||||
|
||||
createdFresh = false
|
||||
writeInfoLog('[delivery-task-service]', '并发同步已创建相同履约任务,本次直接复用', {
|
||||
...buildOrderLogContext(order),
|
||||
...buildOrderItemLogContext(item),
|
||||
unitIndex,
|
||||
taskId: task.id,
|
||||
taskNo: task.task_no,
|
||||
})
|
||||
}
|
||||
|
||||
if (task) {
|
||||
tasks.push({
|
||||
const deliveryTask = {
|
||||
...task,
|
||||
skuCode: item.sku_code,
|
||||
skuName: item.sku_name,
|
||||
}
|
||||
tasks.push(deliveryTask)
|
||||
existingTaskKeys.add(buildTaskUnitKey(task))
|
||||
if (createdFresh) {
|
||||
await appendTaskEvent(
|
||||
task.id,
|
||||
'fulfillment_task_planned',
|
||||
{
|
||||
source: 'order_task_reconciliation',
|
||||
orderId: order.id,
|
||||
platformOrderId: order.platform_order_id,
|
||||
orderItemId: item.id,
|
||||
unitIndex,
|
||||
skuCode: item.sku_code,
|
||||
skuName: item.sku_name,
|
||||
profileId: plan.profileId,
|
||||
profileKey: plan.profileKey,
|
||||
executorKey: plan.executorKey,
|
||||
automationMode: plan.autoDispatch ? 'automatic' : 'manual',
|
||||
initialStatus,
|
||||
},
|
||||
createdAt,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
writeWarnLog('[delivery-task-service]', '履约任务创建未返回记录', {
|
||||
...buildOrderLogContext(order),
|
||||
...buildOrderItemLogContext(item),
|
||||
unitIndex,
|
||||
profileId: plan.profileId,
|
||||
profileKey: plan.profileKey,
|
||||
executorKey: plan.executorKey,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
writeInfoLog('[delivery-task-service]', '订单履约任务完整性核对完成', {
|
||||
...buildOrderLogContext(order),
|
||||
existingTaskCount: existingTasks.length,
|
||||
createdTaskCount: tasks.length - existingTasks.length,
|
||||
totalTaskCount: tasks.length,
|
||||
expectedTaskCount: orderItems.reduce(
|
||||
(total, item) => total + Math.max(1, Number(item.quantity || 1)),
|
||||
0,
|
||||
),
|
||||
})
|
||||
|
||||
if (order.pay_status !== 'paid') {
|
||||
return tasks
|
||||
}
|
||||
|
||||
const preparedTasks = await Promise.all(
|
||||
tasks.map((task) => preparePaidFulfillmentTask(task, runtimeDeps)),
|
||||
tasks.map((task) => preparePaidTaskWithDiagnostics(order, task, runtimeDeps, writeWarnLog)),
|
||||
)
|
||||
return preparedTasks.filter(isTaskRow)
|
||||
}
|
||||
|
||||
async function preparePaidTasks(tasks: TaskRow[], deps: FulfillmentPrepareDeps) {
|
||||
const preparedTasks = await Promise.all(
|
||||
tasks.map((task) => preparePaidFulfillmentTask(task, deps)),
|
||||
)
|
||||
return preparedTasks.filter(isTaskRow)
|
||||
async function preparePaidTaskWithDiagnostics(
|
||||
order: OrderRow,
|
||||
task: TaskRow,
|
||||
deps: FulfillmentPrepareDeps,
|
||||
writeWarnLog: typeof logWarn,
|
||||
) {
|
||||
try {
|
||||
const prepared = await preparePaidFulfillmentTask(task, deps)
|
||||
if (!prepared) {
|
||||
writeWarnLog('[delivery-task-service]', '已支付履约任务准备未返回任务记录', {
|
||||
...buildOrderLogContext(order),
|
||||
taskId: task.id,
|
||||
taskNo: task.task_no,
|
||||
orderItemId: task.order_item_id,
|
||||
unitIndex: task.unit_index,
|
||||
executorKey: task.executor_key,
|
||||
taskStatus: task.task_status,
|
||||
})
|
||||
}
|
||||
return prepared
|
||||
} catch (error) {
|
||||
writeWarnLog('[delivery-task-service]', '已支付履约任务准备执行异常', {
|
||||
...buildOrderLogContext(order),
|
||||
taskId: task.id,
|
||||
taskNo: task.task_no,
|
||||
orderItemId: task.order_item_id,
|
||||
unitIndex: task.unit_index,
|
||||
executorKey: task.executor_key,
|
||||
taskStatus: task.task_status,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
})
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
function resolvePaidTaskStatus(profile: FulfillmentBindingLike | null | undefined): string {
|
||||
@@ -163,3 +298,71 @@ function resolvePaidTaskStatus(profile: FulfillmentBindingLike | null | undefine
|
||||
function isTaskRow(task: TaskRow | DeliveryTaskRow | null | undefined): task is TaskRow {
|
||||
return Boolean(task && Number(task.id || 0) > 0)
|
||||
}
|
||||
|
||||
function buildTaskUnitKey(task: Pick<TaskRow, 'order_item_id' | 'unit_index'>) {
|
||||
return buildOrderItemUnitKey(task.order_item_id, task.unit_index)
|
||||
}
|
||||
|
||||
function buildOrderItemUnitKey(orderItemId: number | string, unitIndex: number | string) {
|
||||
return `${Number(orderItemId)}:${Math.max(1, Number(unitIndex || 1))}`
|
||||
}
|
||||
|
||||
function buildOrderLogContext(order: OrderRow) {
|
||||
return {
|
||||
orderId: order.id,
|
||||
provider: order.provider,
|
||||
platform: order.platform,
|
||||
shopId: order.shop_id,
|
||||
platformOrderId: order.platform_order_id,
|
||||
payStatus: order.pay_status,
|
||||
}
|
||||
}
|
||||
|
||||
function buildOrderItemLogContext(item: OrderItemRow) {
|
||||
return {
|
||||
orderItemId: item.id,
|
||||
skuCode: item.sku_code,
|
||||
skuName: item.sku_name,
|
||||
quantity: item.quantity,
|
||||
}
|
||||
}
|
||||
|
||||
function buildPlanningDiagnostic(item: OrderItemRow) {
|
||||
const snapshot = parseJsonObject(item.item_snapshot_json)
|
||||
const route = parseJsonObject(snapshot.fulfillmentRoute)
|
||||
const cloudtentacles = parseJsonObject(snapshot.cloudtentacles)
|
||||
const kuaishouFeifei = parseJsonObject(snapshot.kuaishouFeifei)
|
||||
const affiliateDash = parseJsonObject(snapshot.affiliateDash)
|
||||
const kuaishouSendCode = parseJsonObject(snapshot.kuaishouSendCode)
|
||||
|
||||
return {
|
||||
snapshotIsConfigured: Boolean(snapshot.isConfigured),
|
||||
selectedExecutorKey: String(route.selectedExecutorKey || '').trim(),
|
||||
routeReason: String(route.reason || '').trim(),
|
||||
candidateAvailability: route.candidates || route.candidateAvailability || [],
|
||||
sourceItemTitle: String(kuaishouSendCode.itemTitle || '').trim(),
|
||||
sourceSkuNick: String(kuaishouSendCode.skuNick || '').trim(),
|
||||
cloudSkuId: Number(cloudtentacles.cloudSkuId || 0) || 0,
|
||||
cloudSkuName: String(cloudtentacles.cloudSkuName || '').trim(),
|
||||
cloudSourceKeys: Array.isArray(cloudtentacles.cloudSourceKeys)
|
||||
? cloudtentacles.cloudSourceKeys
|
||||
: [],
|
||||
feifeiProductCode: String(kuaishouFeifei.productCode || '').trim(),
|
||||
affiliateDashSku: String(affiliateDash.sku || '').trim(),
|
||||
}
|
||||
}
|
||||
|
||||
function parseJsonObject(value: unknown): JsonObject {
|
||||
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||
return value as JsonObject
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(String(value || '{}'))
|
||||
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
||||
? (parsed as JsonObject)
|
||||
: {}
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
publishWorkOrderRealtimeChange,
|
||||
} from '../realtime/realtime-event-service.js'
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
import { logIntegration } from '../../utils/logger.js'
|
||||
import { logInfo, logIntegration, logWarn } from '../../utils/logger.js'
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
import type { OrderItemRow, OrderRow, TaskRow } from '../../types/repository/rows.js'
|
||||
|
||||
@@ -210,6 +210,22 @@ export async function upsertOrderFromSource(
|
||||
source: `${sourceLabel}_order_upsert`,
|
||||
autoOnly: true,
|
||||
})
|
||||
logInfo('[order-consumer-router]', '来源订单消费者分流完成', {
|
||||
source: sourceLabel,
|
||||
orderId: order.id,
|
||||
platformOrderId: order.platform_order_id,
|
||||
fulfillment: {
|
||||
result: 'blocked',
|
||||
reason: readiness.blockReason,
|
||||
detail: readiness.reason,
|
||||
orderItemIds: configuredOrderItems.map((item) => item.id),
|
||||
},
|
||||
workerPlatform: {
|
||||
createdCount: workOrderSync.createdCount,
|
||||
skippedCount: workOrderSync.skippedCount,
|
||||
decisions: workOrderSync.decisions,
|
||||
},
|
||||
})
|
||||
publishSourceOrderRealtimeChanges(order.id, workOrderSync.created)
|
||||
return {
|
||||
ignoreReason: readiness.blockReason || 'fulfillment_not_ready',
|
||||
@@ -231,6 +247,16 @@ export async function upsertOrderFromSource(
|
||||
source: `${sourceLabel}_order_upsert`,
|
||||
autoOnly: true,
|
||||
})
|
||||
logOrderConsumerOutcome({
|
||||
sourceLabel,
|
||||
order,
|
||||
orderItems,
|
||||
configuredOrderItems,
|
||||
tasks,
|
||||
workerDecisions: workOrderSync.decisions,
|
||||
workerCreatedCount: workOrderSync.createdCount,
|
||||
workerSkippedCount: workOrderSync.skippedCount,
|
||||
})
|
||||
publishSourceOrderRealtimeChanges(order.id, workOrderSync.created)
|
||||
|
||||
logIntegration('[order-service]', `${sourceLabel} 订单 upsert 完成`, {
|
||||
@@ -262,6 +288,124 @@ function publishSourceOrderRealtimeChanges(
|
||||
}
|
||||
}
|
||||
|
||||
function logOrderConsumerOutcome({
|
||||
sourceLabel,
|
||||
order,
|
||||
orderItems,
|
||||
configuredOrderItems,
|
||||
tasks,
|
||||
workerDecisions,
|
||||
workerCreatedCount,
|
||||
workerSkippedCount,
|
||||
}: {
|
||||
sourceLabel: string
|
||||
order: OrderRow
|
||||
orderItems: OrderItemRow[]
|
||||
configuredOrderItems: OrderItemRow[]
|
||||
tasks: TaskRow[]
|
||||
workerDecisions: Array<Record<string, unknown>>
|
||||
workerCreatedCount: number
|
||||
workerSkippedCount: number
|
||||
}) {
|
||||
const configuredItemIds = new Set(configuredOrderItems.map((item) => Number(item.id)))
|
||||
const taskCountByOrderItemId = new Map<number, number>()
|
||||
for (const task of tasks) {
|
||||
const orderItemId = Number(task.order_item_id || 0)
|
||||
taskCountByOrderItemId.set(orderItemId, (taskCountByOrderItemId.get(orderItemId) || 0) + 1)
|
||||
}
|
||||
|
||||
const missingFulfillmentItems = configuredOrderItems
|
||||
.filter(
|
||||
(item) =>
|
||||
(taskCountByOrderItemId.get(Number(item.id)) || 0) <
|
||||
Math.max(1, Number(item.quantity || 1)),
|
||||
)
|
||||
.map((item) => ({
|
||||
orderItemId: item.id,
|
||||
skuCode: item.sku_code,
|
||||
skuName: item.sku_name,
|
||||
expectedTaskCount: Math.max(1, Number(item.quantity || 1)),
|
||||
actualTaskCount: taskCountByOrderItemId.get(Number(item.id)) || 0,
|
||||
route: resolveOrderItemRouteDiagnostic(item),
|
||||
}))
|
||||
const workerAcceptedItemIds = new Set(
|
||||
workerDecisions
|
||||
.filter((decision) => decision.result === 'created' || decision.result === 'existing')
|
||||
.map((decision) => Number(decision.orderItemId || 0))
|
||||
.filter((orderItemId) => orderItemId > 0),
|
||||
)
|
||||
const dualConsumedOrderItemIds = Array.from(workerAcceptedItemIds).filter((orderItemId) =>
|
||||
configuredItemIds.has(orderItemId),
|
||||
)
|
||||
const unhandledOrderItems = orderItems
|
||||
.filter((item) => {
|
||||
const orderItemId = Number(item.id)
|
||||
return !configuredItemIds.has(orderItemId) && !workerAcceptedItemIds.has(orderItemId)
|
||||
})
|
||||
.map((item) => ({
|
||||
orderItemId: item.id,
|
||||
skuCode: item.sku_code,
|
||||
skuName: item.sku_name,
|
||||
}))
|
||||
|
||||
const detail = {
|
||||
source: sourceLabel,
|
||||
orderId: order.id,
|
||||
platformOrderId: order.platform_order_id,
|
||||
fulfillment: {
|
||||
configuredOrderItemIds: Array.from(configuredItemIds),
|
||||
taskIds: tasks.map((task) => task.id),
|
||||
taskCount: tasks.length,
|
||||
missingItems: missingFulfillmentItems,
|
||||
},
|
||||
workerPlatform: {
|
||||
createdCount: workerCreatedCount,
|
||||
skippedCount: workerSkippedCount,
|
||||
decisions: workerDecisions,
|
||||
},
|
||||
dualConsumedOrderItemIds,
|
||||
unhandledOrderItems,
|
||||
}
|
||||
|
||||
logInfo('[order-consumer-router]', '来源订单消费者分流完成', detail)
|
||||
if (missingFulfillmentItems.length > 0) {
|
||||
logWarn('[order-consumer-router]', '已配置履约的订单项存在任务缺口', detail)
|
||||
}
|
||||
if (dualConsumedOrderItemIds.length > 0) {
|
||||
logWarn('[order-consumer-router]', '同一订单项被履约与发单平台消费者同时接收', detail)
|
||||
}
|
||||
if (unhandledOrderItems.length > 0) {
|
||||
logWarn('[order-consumer-router]', '订单项未被履约或发单平台消费者接收', detail)
|
||||
}
|
||||
}
|
||||
|
||||
function resolveOrderItemRouteDiagnostic(item: OrderItemRow) {
|
||||
const snapshot = parseOrderItemSnapshot(item.item_snapshot_json) || {}
|
||||
const sendCode =
|
||||
snapshot.kuaishouSendCode &&
|
||||
typeof snapshot.kuaishouSendCode === 'object' &&
|
||||
!Array.isArray(snapshot.kuaishouSendCode)
|
||||
? (snapshot.kuaishouSendCode as Record<string, unknown>)
|
||||
: {}
|
||||
const route =
|
||||
snapshot.fulfillmentRoute &&
|
||||
typeof snapshot.fulfillmentRoute === 'object' &&
|
||||
!Array.isArray(snapshot.fulfillmentRoute)
|
||||
? (snapshot.fulfillmentRoute as Record<string, unknown>)
|
||||
: {}
|
||||
|
||||
return {
|
||||
snapshotIsConfigured: snapshot.isConfigured === true,
|
||||
selectedExecutorKey: String(route.selectedExecutorKey || '').trim(),
|
||||
selectedRuleId: String(route.selectedRuleId || '').trim(),
|
||||
reason: String(route.reason || '').trim(),
|
||||
sourceItemTitle: String(sendCode.itemTitle || '').trim(),
|
||||
sourceSkuNick: String(sendCode.skuNick || '').trim(),
|
||||
candidates: Array.isArray(route.candidates) ? route.candidates : [],
|
||||
skipped: Array.isArray(route.skipped) ? route.skipped : [],
|
||||
}
|
||||
}
|
||||
|
||||
const ORDER_STATUS_PRIORITY = {
|
||||
created: 0,
|
||||
paid: 1,
|
||||
|
||||
Reference in New Issue
Block a user