后端迁移订单同步服务
This commit is contained in:
+113
-11
@@ -12,12 +12,95 @@ import { ensureAgisoXianyuClaimMessageDeliveredForTask } from '../platforms/agis
|
|||||||
import { resolveOrderItemForFulfillment } from './product-match-service.js'
|
import { resolveOrderItemForFulfillment } from './product-match-service.js'
|
||||||
import { nowIso } from '../../utils/time.js'
|
import { nowIso } from '../../utils/time.js'
|
||||||
import { logWebhook } from '../../utils/logger.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' })
|
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 now = nowIso()
|
||||||
const exactExisting = await findOrderByPlatformOrderId({
|
const exactExisting = await findOrderByPlatformOrderId({
|
||||||
provider: event.provider,
|
provider: event.provider,
|
||||||
@@ -50,7 +133,7 @@ export async function upsertOrderFromSource(event, { sourceLabel = 'source' } =
|
|||||||
item,
|
item,
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
const configuredItems = resolvedItems.filter((item) => item.isConfigured)
|
const configuredItems = (resolvedItems as FulfillmentOrderItem[]).filter((item) => item.isConfigured)
|
||||||
|
|
||||||
if (configuredItems.length === 0) {
|
if (configuredItems.length === 0) {
|
||||||
logWebhook('[order-service]', `${sourceLabel} 订单已忽略:未命中任何已配置履约商品`, {
|
logWebhook('[order-service]', `${sourceLabel} 订单已忽略:未命中任何已配置履约商品`, {
|
||||||
@@ -104,6 +187,10 @@ export async function upsertOrderFromSource(event, { sourceLabel = 'source' } =
|
|||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (!order) {
|
||||||
|
throw new Error('订单写入失败')
|
||||||
|
}
|
||||||
|
|
||||||
const orderItems = await replaceOrderItems(
|
const orderItems = await replaceOrderItems(
|
||||||
order.id,
|
order.id,
|
||||||
configuredItems.map((item) => ({
|
configuredItems.map((item) => ({
|
||||||
@@ -118,7 +205,7 @@ export async function upsertOrderFromSource(event, { sourceLabel = 'source' } =
|
|||||||
)
|
)
|
||||||
|
|
||||||
const tasks = await syncDeliveryTasksForOrder(order, orderItems)
|
const tasks = await syncDeliveryTasksForOrder(order, orderItems)
|
||||||
const messageDeliveries = []
|
const messageDeliveries: MessageDeliveryResult[] = []
|
||||||
|
|
||||||
logWebhook('[order-service]', `${sourceLabel} 订单 upsert 完成`, {
|
logWebhook('[order-service]', `${sourceLabel} 订单 upsert 完成`, {
|
||||||
orderId: order.id,
|
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([
|
return [...new Set([
|
||||||
String(event?.shopId || '').trim(),
|
String(event?.shopId || '').trim(),
|
||||||
...(Array.isArray(event?.shopIdAliases) ? event.shopIdAliases : []).map((item) => String(item || '').trim()),
|
...(Array.isArray(event?.shopIdAliases) ? event.shopIdAliases : []).map((item) => String(item || '').trim()),
|
||||||
@@ -194,7 +281,10 @@ const PAY_STATUS_PRIORITY = {
|
|||||||
refunded: 2,
|
refunded: 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mergeWebhookOrderState(existing, incoming) {
|
export function mergeWebhookOrderState(
|
||||||
|
existing: OrderStateInput | null | undefined,
|
||||||
|
incoming: OrderStateInput | null | undefined,
|
||||||
|
): OrderStateMergeResult {
|
||||||
const orderStatus = chooseHigherPriorityStatus(
|
const orderStatus = chooseHigherPriorityStatus(
|
||||||
normalizeStateValue(existing?.order_status ?? existing?.orderStatus),
|
normalizeStateValue(existing?.order_status ?? existing?.orderStatus),
|
||||||
normalizeStateValue(incoming?.orderStatus ?? incoming?.order_status),
|
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 existingPriority = resolveStatusPriority(existingValue, priorityMap)
|
||||||
const incomingPriority = resolveStatusPriority(incomingValue, priorityMap)
|
const incomingPriority = resolveStatusPriority(incomingValue, priorityMap)
|
||||||
|
|
||||||
@@ -233,15 +327,23 @@ function chooseHigherPriorityStatus(existingValue, incomingValue, priorityMap) {
|
|||||||
return incomingValue || existingValue
|
return incomingValue || existingValue
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveStatusPriority(value, priorityMap) {
|
function resolveStatusPriority(value: string, priorityMap: StatusPriorityMap): number {
|
||||||
return Number(priorityMap[normalizeStateValue(value)] ?? -1)
|
return Number(priorityMap[normalizeStateValue(value)] ?? -1)
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeStateValue(value) {
|
function normalizeStateValue(value: unknown): string {
|
||||||
return String(value || '').trim().toLowerCase()
|
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))) {
|
if (!['paid', 'refunded'].includes(normalizeStateValue(payStatus))) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -256,7 +358,7 @@ function resolveMergedPaidAt({ existingPaidAt, incomingPaidAt, payStatus }) {
|
|||||||
return existingPaidAt || incomingPaidAt || null
|
return existingPaidAt || incomingPaidAt || null
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseDateValue(value) {
|
function parseDateValue(value: unknown): number | null {
|
||||||
const text = String(value || '').trim()
|
const text = String(value || '').trim()
|
||||||
|
|
||||||
if (!text) {
|
if (!text) {
|
||||||
@@ -272,13 +272,21 @@
|
|||||||
- `npm run typecheck`
|
- `npm run typecheck`
|
||||||
- `npm run build`
|
- `npm run build`
|
||||||
- `npm test` 共 125 个用例通过
|
- `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 模块
|
1. 继续迁移服务层中最核心、最常改的订单 / 履约 / claim 模块
|
||||||
2. 逐步移除服务层 `@ts-nocheck`,优先处理 webhook service、自动发货
|
2. 逐步移除服务层 `@ts-nocheck`,优先处理 webhook service、自动发货、delivery task
|
||||||
|
|
||||||
## 执行原则
|
## 执行原则
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user