diff --git a/apps/backend/src/db/migrations/004_order_platform_order_lookup.sql b/apps/backend/src/db/migrations/004_order_platform_order_lookup.sql new file mode 100644 index 00000000..0253c90f --- /dev/null +++ b/apps/backend/src/db/migrations/004_order_platform_order_lookup.sql @@ -0,0 +1,2 @@ +CREATE INDEX IF NOT EXISTS idx_orders_platform_order_lookup + ON orders(provider, platform, platform_order_id, id DESC); diff --git a/apps/backend/src/repositories/order-repo.ts b/apps/backend/src/repositories/order-repo.ts index 93a6d120..dfe67fad 100644 --- a/apps/backend/src/repositories/order-repo.ts +++ b/apps/backend/src/repositories/order-repo.ts @@ -36,6 +36,25 @@ export async function findOrderByPlatformOrderId({ return result.rows[0] || null } +export async function findLatestOrderByPlatformOrderId({ + provider = '91kaquan', + platform, + platformOrderId, +}: Omit): Promise { + const result = await query( + ` + SELECT * + FROM orders + WHERE provider = $1 AND platform = $2 AND platform_order_id = $3 + ORDER BY id DESC + LIMIT 1 + `, + [provider, platform, platformOrderId], + ) + + return result.rows[0] || null +} + export async function createOrder(input: OrderCreateInput): Promise { const result = await query( ` diff --git a/apps/backend/src/services/open-91/order-query-service.ts b/apps/backend/src/services/open-91/order-query-service.ts index b8ad2550..7368f0de 100644 --- a/apps/backend/src/services/open-91/order-query-service.ts +++ b/apps/backend/src/services/open-91/order-query-service.ts @@ -1,4 +1,4 @@ -import { findOrderByPlatformOrderId } from '../../repositories/order-repo.js' +import { findLatestOrderByPlatformOrderId } from '../../repositories/order-repo.js' import { listTasksByOrderId } from '../../repositories/task-repo.js' import { buildClaimUrl } from '../claim/claim-service.js' import { ensureTaskClaimLink } from '../fulfillment/kuaishou-cloud/index.js' @@ -38,10 +38,9 @@ export async function queryOpen91Order(payload: JsonObject = {}, { requestId = ' assertOpen91Timestamp(normalized.timestamp, config.timestampToleranceSeconds) assertOpen91Signature(payload, config) - const order = await findOrderByPlatformOrderId({ + const order = await findLatestOrderByPlatformOrderId({ provider: OPEN_91_PROVIDER, platform: OPEN_91_PLATFORM, - shopId: config.shopId, platformOrderId: normalized.orderNo, }) diff --git a/apps/backend/src/services/order/delivery-task-service.test.ts b/apps/backend/src/services/order/delivery-task-service.test.ts deleted file mode 100644 index 6368a218..00000000 --- a/apps/backend/src/services/order/delivery-task-service.test.ts +++ /dev/null @@ -1,287 +0,0 @@ -import test from 'node:test' -import assert from 'node:assert/strict' - -import { syncDeliveryTasksForOrderWithDeps } from './delivery-task-service.js' - -const paidOrder = { - id: 10, - provider: '91kaquan', - platform: 'kuaishou', - shop_id: 'shop-1', - shop_name: '测试店铺', - platform_order_id: 'P10001', - pay_status: 'paid', -} - -const orderItems = [ - { - id: 20, - sku_code: 'dnf-cdk-a', - sku_name: 'DNF CDK A', - quantity: 1, - }, -] - -function createTaskFixture(patch = {}) { - return { - id: 30, - order_item_id: 20, - task_status: 'paid', - executor_key: 'claim_link_dispatch', - requires_claim: true, - claim_token: '', - primary_claim_token_id: null, - last_error: '', - context_json: '{}', - ...patch, - } -} - -test('syncDeliveryTasksForOrderWithDeps sends manual dispatch tasks to manual review', async () => { - const notifications = [] - const updates = [] - - const result = await syncDeliveryTasksForOrderWithDeps(paidOrder, orderItems, { - listTasksByOrderId: async () => [createTaskFixture({ - executor_key: 'manual_dispatch', - requires_claim: false, - context_json: '{}', - })], - updateTask: async (taskId, patch) => { - updates.push({ taskId, patch }) - return { id: taskId, ...patch } - }, - notifyTaskAutoManualReview: async (payload) => { - notifications.push(payload) - return { ok: true } - }, - nowIso: () => '2026-04-14T12:01:00.000Z', - }) - - assert.equal(result[0]?.task_status, 'manual_review') - assert.equal(updates[0]?.patch.last_error, '当前任务需要人工履约处理') - assert.deepEqual( - notifications.map((item) => ({ - reason: item.reason, - source: item.source, - taskStatus: item.task.task_status, - })), - [ - { - reason: '当前任务需要人工履约处理', - source: 'manual_dispatch_profile', - taskStatus: 'manual_review', - }, - ], - ) -}) - -test('syncDeliveryTasksForOrderWithDeps prepares kuaishou cloud task with claim token', async () => { - const claimToken = { - token: 'claim-token', - expired_at: '2026-04-15T12:00:00.000Z', - } - const updates = [] - - const result = await syncDeliveryTasksForOrderWithDeps(paidOrder, orderItems, { - listTasksByOrderId: async () => [createTaskFixture({ - executor_key: 'kuaishou_ct_assisted', - task_status: 'paid', - requires_claim: true, - context_json: '{}', - })], - createTaskClaimToken: async (taskId) => ({ ...claimToken, taskId }), - updateTask: async (taskId, patch) => { - updates.push({ taskId, patch }) - return { id: taskId, ...patch } - }, - nowIso: () => '2026-04-14T12:02:00.000Z', - }) - - assert.equal(result[0]?.task_status, 'pending_binding_prepare') - assert.deepEqual(updates, [ - { - taskId: 30, - patch: { - task_status: 'pending_binding_prepare', - claim_token: 'claim-token', - claim_expires_at: '2026-04-15T12:00:00.000Z', - user_action_status: 'pending_claim', - last_error: '领取链接已生成,等待客户提交核销码', - updated_at: '2026-04-14T12:02:00.000Z', - }, - }, - ]) -}) - -test('syncDeliveryTasksForOrderWithDeps creates kuaishou cloud task from cloudtentacles name match snapshot', async () => { - const createdTasks = [] - const updates = [] - - const dynamicOrderItems = [ - { - id: 21, - order_id: 10, - sku_code: '套装-浪漫天命', - sku_name: '套装-浪漫天命', - quantity: 1, - spec_json: '{}', - item_snapshot_json: JSON.stringify({ - matchMode: 'cloudtentacles_name', - cloudtentacles: { - cloudSkuId: 28, - cloudSkuName: '套装-浪漫天命', - cloudSourceKeys: ['account-a', 'account-b'], - }, - }), - }, - ] - - const result = await syncDeliveryTasksForOrderWithDeps(paidOrder, dynamicOrderItems, { - listTasksByOrderId: async () => [], - getFulfillmentProfileByKey: async () => ({ - id: 2, - profile_key: 'kuaishou_ct_assisted', - name: '快手 cloud 履约', - executor_key: 'kuaishou_ct_assisted', - requires_claim: false, - auto_dispatch: false, - config_json: '{}', - }), - createTask: async (input) => { - createdTasks.push(input) - return { - id: 31, - order_id: input.orderId, - order_item_id: input.orderItemId, - task_status: input.taskStatus, - executor_key: input.executorKey, - requires_claim: input.requiresClaim, - claim_token: '', - primary_claim_token_id: null, - last_error: '', - context_json: input.contextJson, - } - }, - createTaskClaimToken: async () => ({ - token: 'claim-token', - expired_at: '2026-04-15T12:00:00.000Z', - }), - updateTask: async (taskId, patch) => { - updates.push({ taskId, patch }) - return { id: taskId, ...patch } - }, - nowIso: () => '2026-04-14T12:03:00.000Z', - randomId: () => 'DT-DYNAMIC', - }) - - assert.equal(createdTasks.length, 1) - assert.equal(createdTasks[0].executorKey, 'kuaishou_ct_assisted') - assert.equal(createdTasks[0].profileId, 2) - assert.equal(createdTasks[0].taskStatus, 'pending_binding_prepare') - - const context = JSON.parse(createdTasks[0].contextJson) - assert.equal(context.kuaishouCloudFulfillment.binding.skuId, 28) - assert.equal(context.kuaishouCloudFulfillment.binding.skuName, '套装-浪漫天命') - assert.deepEqual(context.kuaishouCloudFulfillment.binding.cloudSourceKeys, ['account-a', 'account-b']) - assert.equal(context.kuaishouCloudFulfillment.consume.shopId, 'shop-1') - assert.equal(context.kuaishouCloudFulfillment.consume.shopName, '测试店铺') - assert.deepEqual(context.kuaishouCloudFulfillment.deliveryItems, [ - { - cloudSkuId: 28, - cloudSkuName: '套装-浪漫天命', - quantity: 1, - }, - ]) - assert.equal(result[0]?.claim_token, 'claim-token') - assert.equal(updates[0]?.patch.claim_token, 'claim-token') -}) - -test('syncDeliveryTasksForOrderWithDeps keeps override delivery items and quantities', async () => { - const createdTasks = [] - - const dynamicOrderItems = [ - { - id: 22, - order_id: 10, - sku_code: '套装1', - sku_name: '套装1', - quantity: 1, - spec_json: '{}', - item_snapshot_json: JSON.stringify({ - matchMode: 'cloudtentacles_override', - cloudtentacles: { - matchMode: 'cloudtentacles_override', - normalizedProductName: '套装1', - cloudSkuId: 28, - cloudSkuName: '商品A', - cloudSourceKeys: ['account-a'], - resolvedSourceKey: 'account-a', - deliveryItems: [ - { - cloudSkuId: 28, - cloudSkuName: '商品A', - quantity: 1, - }, - { - cloudSkuId: 29, - cloudSkuName: '商品B', - quantity: 20, - }, - ], - }, - }), - }, - ] - - await syncDeliveryTasksForOrderWithDeps(paidOrder, dynamicOrderItems, { - listTasksByOrderId: async () => [], - getFulfillmentProfileByKey: async () => ({ - id: 2, - profile_key: 'kuaishou_ct_assisted', - name: '快手 cloud 履约', - executor_key: 'kuaishou_ct_assisted', - requires_claim: false, - auto_dispatch: false, - config_json: '{}', - }), - createTask: async (input) => { - createdTasks.push(input) - return { - id: 32, - order_id: input.orderId, - order_item_id: input.orderItemId, - task_status: input.taskStatus, - executor_key: input.executorKey, - requires_claim: input.requiresClaim, - claim_token: '', - primary_claim_token_id: null, - last_error: '', - context_json: input.contextJson, - } - }, - createTaskClaimToken: async () => ({ - token: 'claim-token', - expired_at: '2026-04-15T12:00:00.000Z', - }), - updateTask: async (taskId, patch) => ({ id: taskId, ...patch }), - nowIso: () => '2026-04-14T12:04:00.000Z', - randomId: () => 'DT-OVERRIDE', - }) - - const context = JSON.parse(createdTasks[0].contextJson) - assert.equal(context.kuaishouCloudFulfillment.binding.skuId, 28) - assert.equal(context.kuaishouCloudFulfillment.binding.resolvedSourceKey, 'account-a') - assert.deepEqual(context.kuaishouCloudFulfillment.deliveryItems, [ - { - cloudSkuId: 28, - cloudSkuName: '商品A', - quantity: 1, - }, - { - cloudSkuId: 29, - cloudSkuName: '商品B', - quantity: 20, - }, - ]) -}) diff --git a/apps/backend/src/services/order/delivery-task-service.ts b/apps/backend/src/services/order/delivery-task-service.ts index 01b2f0e3..e479fb45 100644 --- a/apps/backend/src/services/order/delivery-task-service.ts +++ b/apps/backend/src/services/order/delivery-task-service.ts @@ -113,6 +113,7 @@ export async function syncDeliveryTasksForOrderWithDeps( if (!profile) { continue } + const itemSnapshot = parseJsonObject(item.item_snapshot_json) const quantity = Math.max(1, Number(item.quantity || 1)) const fulfillmentConfig = parseJsonObject(profile.config_json) const cloudtentaclesConfig = parseJsonObject(fulfillmentConfig.cloudtentacles) @@ -225,8 +226,8 @@ export async function syncDeliveryTasksForOrderWithDeps( }, consume: { status: 'pending', - shopId: String(kuaishouConsumeConfig.shopId || kuaishouShopConfig.shopId || order.shop_id || '').trim(), - shopName: String(kuaishouConsumeConfig.shopName || kuaishouShopConfig.kshopName || order.shop_name || '').trim(), + shopId: String(kuaishouConsumeConfig.shopId || kuaishouShopConfig.shopId || itemSnapshot.shopId || order.shop_id || '').trim(), + shopName: String(kuaishouConsumeConfig.shopName || kuaishouShopConfig.kshopName || itemSnapshot.shopName || order.shop_name || '').trim(), autoConsumeEnabled: kuaishouConsumeConfig.autoConsumeAfterDispatch === true, consumedAt: null, errorMessage: '', @@ -429,8 +430,8 @@ async function resolveDynamicCloudtentaclesProfile( autoReturnNumberAfterDispatch: true, }, kuaishouConsume: { - shopId: '', - shopName: '', + shopId: String(snapshot.shopId || '').trim(), + shopName: String(snapshot.shopName || '').trim(), autoConsumeAfterDispatch: false, }, notes: cloudtentacles.matchMode === 'cloudtentacles_override' diff --git a/apps/backend/src/services/platforms/ninetyone/order-service.test.ts b/apps/backend/src/services/platforms/ninetyone/order-service.test.ts index 9bc9b47e..c3488bca 100644 --- a/apps/backend/src/services/platforms/ninetyone/order-service.test.ts +++ b/apps/backend/src/services/platforms/ninetyone/order-service.test.ts @@ -44,7 +44,7 @@ test('parseOpen91ProductNo splits product name and kuaishou shop id by four dash ) }) -test('buildOpen91SourceEvent uses productNo suffix as order shop id', () => { +test('buildOpen91SourceEvent keeps productNo suffix for kuaishou consume only', () => { const event = buildOpen91SourceEvent({ orderNo: 'P91KS202605130002', productNo: '测试-关联商品1----3676797936', @@ -58,7 +58,8 @@ test('buildOpen91SourceEvent uses productNo suffix as order shop id', () => { shopName: '91卡券', }) - assert.equal(event.shopId, '3676797936') + assert.equal(event.shopId, '91kaquan') + assert.equal(event.shopName, '91卡券') assert.equal(event.items[0].externalItemId, '测试-关联商品1----3676797936') assert.equal(event.items[0].externalSkuCode, '测试-关联商品1----3676797936') assert.equal(event.items[0].externalSkuName, '测试-关联商品1') @@ -67,4 +68,5 @@ test('buildOpen91SourceEvent uses productNo suffix as order shop id', () => { assert.equal(event.items[0].snapshot.productNo, '测试-关联商品1----3676797936') assert.equal(event.items[0].snapshot.productName, '测试-关联商品1') assert.equal(event.items[0].snapshot.shopId, '3676797936') + assert.equal(event.items[0].spec.shopId, '3676797936') }) diff --git a/apps/backend/src/services/platforms/ninetyone/order-service.ts b/apps/backend/src/services/platforms/ninetyone/order-service.ts index 25aef7df..5b12ecd5 100644 --- a/apps/backend/src/services/platforms/ninetyone/order-service.ts +++ b/apps/backend/src/services/platforms/ninetyone/order-service.ts @@ -12,7 +12,6 @@ import { buildOpen91OutTradeNo } from '../../open-91/response.js' import { createHttpError } from '../../../utils/http.js' import { parseAmountToFen } from '../../../utils/money.js' import { nowIso } from '../../../utils/time.js' -import { findKuaishouEticketShopConfig } from '../kuaishou-eticket/source-config-service.js' import type { OrderItemRow, OrderRow } from '../../../types/repository/rows.js' export const OPEN_91_PENDING_CONFIG_STATUS = 'pending_config' @@ -25,9 +24,8 @@ export function buildOpen91SourceEvent(payload: JsonObject = {}, config: JsonObj const productInfo = parseOpen91ProductNo(normalized.productNo) const productNo = productInfo.rawProductNo const productName = productInfo.productName - const shopId = productInfo.shopId || String(config.shopId || OPEN_91_PROVIDER).trim() || OPEN_91_PROVIDER - const shopConfig = productInfo.shopId ? findKuaishouEticketShopConfig(productInfo.shopId) : null - const shopName = String(shopConfig?.kshopName || config.shopName || '91卡券').trim() || '91卡券' + const shopId = String(config.shopId || OPEN_91_PROVIDER).trim() || OPEN_91_PROVIDER + const shopName = String(config.shopName || '91卡券').trim() || '91卡券' const maxAmountFen = parseAmountToFen(normalized.maxAmount) return { @@ -336,8 +334,10 @@ function buildOpen91SourceEventFromOrder(order: OrderRow, items: OrderItemRow[] return { provider: order.provider, platform: order.platform, - shopId: order.shop_id, - shopName: order.shop_name, + shopId: OPEN_91_PROVIDER, + shopName: String(order.shop_id || '').trim() === OPEN_91_PROVIDER + ? order.shop_name + : '91卡券', platformOrderId: order.platform_order_id, orderStatus: 'paid', payStatus: 'paid',