后端迁移订单同步服务
This commit is contained in:
+113
-11
@@ -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<string, unknown>
|
||||
snapshot?: Record<string, unknown>
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
type FulfillmentOrderItem = SourceOrderItem & {
|
||||
isConfigured: boolean
|
||||
skuCode: string
|
||||
skuName: string
|
||||
quantity: number
|
||||
spec?: Record<string, unknown>
|
||||
snapshot?: Record<string, unknown>
|
||||
}
|
||||
|
||||
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<string, number>
|
||||
|
||||
export async function upsertOrderFromWebhook(event: SourceOrderEvent): Promise<UpsertOrderResult> {
|
||||
return upsertOrderFromSource(event, { sourceLabel: 'webhook' })
|
||||
}
|
||||
|
||||
export async function upsertOrderFromSource(event, { sourceLabel = 'source' } = {}) {
|
||||
export async function upsertOrderFromSource(
|
||||
event: SourceOrderEvent,
|
||||
{ sourceLabel = 'source' }: UpsertOrderSourceOptions = {},
|
||||
): Promise<UpsertOrderResult> {
|
||||
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<SourceOrderEvent, 'shopId' | 'shopIdAliases'>): 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) {
|
||||
@@ -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
|
||||
|
||||
## 执行原则
|
||||
|
||||
|
||||
Reference in New Issue
Block a user