diff --git a/apps/backend/src/services/order/order-service.js b/apps/backend/src/services/order/order-service.ts similarity index 71% rename from apps/backend/src/services/order/order-service.js rename to apps/backend/src/services/order/order-service.ts index cc9c92d0..b1290a4d 100644 --- a/apps/backend/src/services/order/order-service.js +++ b/apps/backend/src/services/order/order-service.ts @@ -12,12 +12,95 @@ import { ensureAgisoXianyuClaimMessageDeliveredForTask } from '../platforms/agis import { resolveOrderItemForFulfillment } from './product-match-service.js' import { nowIso } from '../../utils/time.js' import { logWebhook } from '../../utils/logger.js' +import type { OrderItemRow, OrderRow, TaskRow } from '../../types/repository-rows.js' -export async function upsertOrderFromWebhook(event) { +type SourceOrderItem = { + quantity?: number + spec?: Record + snapshot?: Record + [key: string]: unknown +} + +type FulfillmentOrderItem = SourceOrderItem & { + isConfigured: boolean + skuCode: string + skuName: string + quantity: number + spec?: Record + snapshot?: Record +} + +type SourceOrderEvent = { + provider: string + platform: string + shopId: string + shopIdAliases?: string[] + shopName: string + platformOrderId: string + orderStatus: string + payStatus: string + buyerId: string + buyerName: string + receiverContact: string + totalAmount: number | string + currency: string + rawPayload: unknown + paidAt?: string | null + items: SourceOrderItem[] +} + +type UpsertOrderSourceOptions = { + sourceLabel?: string +} + +type MessageDeliveryResult = { + taskId: number + [key: string]: unknown +} + +type UpsertOrderIgnoredResult = { + ignored: true + ignoreReason: string + order: null + orderItems: [] + tasks: [] + messageDeliveries: [] +} + +type UpsertOrderResult = { + ignored?: false + ignoreReason?: string + order: OrderRow + orderItems: OrderItemRow[] + tasks: TaskRow[] + messageDeliveries: MessageDeliveryResult[] +} | UpsertOrderIgnoredResult + +type OrderStateInput = { + order_status?: string + orderStatus?: string + pay_status?: string + payStatus?: string + paid_at?: string | null + paidAt?: string | null +} + +type OrderStateMergeResult = { + orderStatus: string + payStatus: string + paidAt: string | null +} + +type StatusPriorityMap = Record + +export async function upsertOrderFromWebhook(event: SourceOrderEvent): Promise { return upsertOrderFromSource(event, { sourceLabel: 'webhook' }) } -export async function upsertOrderFromSource(event, { sourceLabel = 'source' } = {}) { +export async function upsertOrderFromSource( + event: SourceOrderEvent, + { sourceLabel = 'source' }: UpsertOrderSourceOptions = {}, +): Promise { const now = nowIso() const exactExisting = await findOrderByPlatformOrderId({ provider: event.provider, @@ -50,7 +133,7 @@ export async function upsertOrderFromSource(event, { sourceLabel = 'source' } = item, })), ) - const configuredItems = resolvedItems.filter((item) => item.isConfigured) + const configuredItems = (resolvedItems as FulfillmentOrderItem[]).filter((item) => item.isConfigured) if (configuredItems.length === 0) { logWebhook('[order-service]', `${sourceLabel} 订单已忽略:未命中任何已配置履约商品`, { @@ -104,6 +187,10 @@ export async function upsertOrderFromSource(event, { sourceLabel = 'source' } = updatedAt: now, }) + if (!order) { + throw new Error('订单写入失败') + } + const orderItems = await replaceOrderItems( order.id, configuredItems.map((item) => ({ @@ -118,7 +205,7 @@ export async function upsertOrderFromSource(event, { sourceLabel = 'source' } = ) const tasks = await syncDeliveryTasksForOrder(order, orderItems) - const messageDeliveries = [] + const messageDeliveries: MessageDeliveryResult[] = [] logWebhook('[order-service]', `${sourceLabel} 订单 upsert 完成`, { orderId: order.id, @@ -173,7 +260,7 @@ export async function upsertOrderFromSource(event, { sourceLabel = 'source' } = } } -function resolveEventShopIdCandidates(event) { +function resolveEventShopIdCandidates(event: Pick): string[] { return [...new Set([ String(event?.shopId || '').trim(), ...(Array.isArray(event?.shopIdAliases) ? event.shopIdAliases : []).map((item) => String(item || '').trim()), @@ -194,7 +281,10 @@ const PAY_STATUS_PRIORITY = { refunded: 2, } -export function mergeWebhookOrderState(existing, incoming) { +export function mergeWebhookOrderState( + existing: OrderStateInput | null | undefined, + incoming: OrderStateInput | null | undefined, +): OrderStateMergeResult { const orderStatus = chooseHigherPriorityStatus( normalizeStateValue(existing?.order_status ?? existing?.orderStatus), normalizeStateValue(incoming?.orderStatus ?? incoming?.order_status), @@ -218,7 +308,11 @@ export function mergeWebhookOrderState(existing, incoming) { } } -function chooseHigherPriorityStatus(existingValue, incomingValue, priorityMap) { +function chooseHigherPriorityStatus( + existingValue: string, + incomingValue: string, + priorityMap: StatusPriorityMap, +): string { const existingPriority = resolveStatusPriority(existingValue, priorityMap) const incomingPriority = resolveStatusPriority(incomingValue, priorityMap) @@ -233,15 +327,23 @@ function chooseHigherPriorityStatus(existingValue, incomingValue, priorityMap) { return incomingValue || existingValue } -function resolveStatusPriority(value, priorityMap) { +function resolveStatusPriority(value: string, priorityMap: StatusPriorityMap): number { return Number(priorityMap[normalizeStateValue(value)] ?? -1) } -function normalizeStateValue(value) { +function normalizeStateValue(value: unknown): string { return String(value || '').trim().toLowerCase() } -function resolveMergedPaidAt({ existingPaidAt, incomingPaidAt, payStatus }) { +function resolveMergedPaidAt({ + existingPaidAt, + incomingPaidAt, + payStatus, +}: { + existingPaidAt: string | null + incomingPaidAt: string | null + payStatus: string +}): string | null { if (!['paid', 'refunded'].includes(normalizeStateValue(payStatus))) { return null } @@ -256,7 +358,7 @@ function resolveMergedPaidAt({ existingPaidAt, incomingPaidAt, payStatus }) { return existingPaidAt || incomingPaidAt || null } -function parseDateValue(value) { +function parseDateValue(value: unknown): number | null { const text = String(value || '').trim() if (!text) { diff --git a/docs/backend-typescript-migration-plan.md b/docs/backend-typescript-migration-plan.md index a654fb59..b1dc663d 100644 --- a/docs/backend-typescript-migration-plan.md +++ b/docs/backend-typescript-migration-plan.md @@ -272,13 +272,21 @@ - `npm run typecheck` - `npm run build` - `npm test` 共 125 个用例通过 +31. 订单 upsert 服务迁移到 `.ts`: + - `src/services/order/order-service.ts` +32. 外部订单事件、订单履约商品、订单状态合并结果、upsert 返回结构已显式类型化 +33. Docker 内验证通过: + - 订单 / webhook / open91 相关 3 个测试文件共 15 个用例通过 + - `npm run typecheck` + - `npm run build` + - `npm test` 共 125 个用例通过 ## 下一步建议 第一批继续推进时,建议按这个顺序: 1. 继续迁移服务层中最核心、最常改的订单 / 履约 / claim 模块 -2. 逐步移除服务层 `@ts-nocheck`,优先处理 webhook service、自动发货 +2. 逐步移除服务层 `@ts-nocheck`,优先处理 webhook service、自动发货、delivery task ## 执行原则