优化订单流程与数据库关闭

This commit is contained in:
yml2213
2026-08-30 13:15:47 +08:00
parent 04dc363320
commit dcb0fa4c76
5 changed files with 243 additions and 113 deletions
@@ -4,7 +4,7 @@ import { query, withTransaction } from '../db/client.js'
import type { OrderItemReplaceInput } from '../types/repository/inputs.js'
import type { OrderItemRow } from '../types/repository/rows.js'
type QueryExecutor = (text: string, params?: unknown[]) => Promise<QueryResult<any>>
export type QueryExecutor = (text: string, params?: unknown[]) => Promise<QueryResult<any>>
type OrderItemSyncPlan = {
updates: Array<{
@@ -25,8 +25,16 @@ export async function replaceOrderItems(
orderId: number | string,
items: OrderItemReplaceInput[],
): Promise<OrderItemRow[]> {
return withTransaction(async (client: PoolClient) => {
const executor: QueryExecutor = client.query.bind(client)
return withTransaction((client: PoolClient) =>
replaceOrderItemsWithExecutor(client.query.bind(client), orderId, items),
)
}
export async function replaceOrderItemsWithExecutor(
executor: QueryExecutor,
orderId: number | string,
items: OrderItemReplaceInput[],
): Promise<OrderItemRow[]> {
const existingItems = await listOrderItemsByOrderIdWithExecutor(executor, orderId)
const plan = resolveOrderItemSyncPlan(existingItems, items)
@@ -91,7 +99,6 @@ export async function replaceOrderItems(
}
return listOrderItemsByOrderIdWithExecutor(executor, orderId)
})
}
export function resolveOrderItemSyncPlan(
+36 -3
View File
@@ -1,3 +1,5 @@
import type { QueryResult, QueryResultRow } from 'pg'
import { query } from '../db/client.js'
import type {
OrderCreateInput,
@@ -6,6 +8,11 @@ import type {
} from '../types/repository/inputs.js'
import type { OrderListQueryResult, OrderListRow, OrderRow } from '../types/repository/rows.js'
export type OrderQueryExecutor = <T extends QueryResultRow = QueryResultRow>(
text: string,
params?: unknown[],
) => Promise<QueryResult<T>>
type OrderPlatformLookupInput = {
provider?: string
platform: string
@@ -37,7 +44,18 @@ export async function findLatestOrderByPlatformOrderId({
platform,
platformOrderId,
}: Omit<OrderPlatformLookupInput, 'shopId'>): Promise<OrderRow | null> {
const result = await query<OrderRow>(
return findLatestOrderByPlatformOrderIdWithExecutor(query, {
provider,
platform,
platformOrderId,
})
}
export async function findLatestOrderByPlatformOrderIdWithExecutor(
executor: OrderQueryExecutor,
{ provider = '91kaquan', platform, platformOrderId }: Omit<OrderPlatformLookupInput, 'shopId'>,
): Promise<OrderRow | null> {
const result = await executor<OrderRow>(
`
SELECT *
FROM orders
@@ -69,7 +87,14 @@ export async function findLatestOrderByAnyPlatformOrderId(
}
export async function createOrder(input: OrderCreateInput): Promise<OrderRow | null> {
const result = await query<OrderRow>(
return createOrderWithExecutor(query, input)
}
export async function createOrderWithExecutor(
executor: OrderQueryExecutor,
input: OrderCreateInput,
): Promise<OrderRow | null> {
const result = await executor<OrderRow>(
`
INSERT INTO orders (
provider,
@@ -118,7 +143,15 @@ export async function updateOrder(
orderId: number | string,
input: OrderUpdateInput,
): Promise<OrderRow | null> {
const result = await query<OrderRow>(
return updateOrderWithExecutor(query, orderId, input)
}
export async function updateOrderWithExecutor(
executor: OrderQueryExecutor,
orderId: number | string,
input: OrderUpdateInput,
): Promise<OrderRow | null> {
const result = await executor<OrderRow>(
`
UPDATE orders
SET
@@ -1,9 +1,10 @@
import {
createOrder,
findLatestOrderByPlatformOrderId,
updateOrder,
createOrderWithExecutor,
findLatestOrderByPlatformOrderIdWithExecutor,
updateOrderWithExecutor,
} from '../../repositories/order-repo.js'
import { replaceOrderItems } from '../../repositories/order-item-repo.js'
import { replaceOrderItemsWithExecutor } from '../../repositories/order-item-repo.js'
import { withTransaction } from '../../db/client.js'
import { syncDeliveryTasksForOrder } from './delivery-task-service.js'
import { resolveOrderItemForFulfillment } from '../fulfillment/product-resolution-service.js'
import {
@@ -98,11 +99,6 @@ export async function upsertOrderFromSource(
{ sourceLabel = 'source' }: UpsertOrderSourceOptions = {},
): Promise<UpsertOrderResult> {
const now = nowIso()
const existing = await findLatestOrderByPlatformOrderId({
provider: event.provider,
platform: event.platform,
platformOrderId: event.platformOrderId,
})
logIntegration('[order-service]', `开始处理 ${sourceLabel} 订单 upsert`, {
provider: event.provider,
@@ -110,7 +106,6 @@ export async function upsertOrderFromSource(
shopId: event.shopId,
shopName: event.shopName,
platformOrderId: event.platformOrderId,
existingOrderId: existing?.id || null,
})
const resolvedItems = await Promise.all(
@@ -125,6 +120,16 @@ export async function upsertOrderFromSource(
const resolvedFulfillmentItems = resolvedItems as FulfillmentOrderItem[]
const configuredItems = resolvedFulfillmentItems.filter((item) => item.isConfigured)
const persisted = await withTransaction(async (client) => {
const executor = client.query.bind(client)
const lockKey = [event.provider, event.platform, event.platformOrderId].join('|')
await executor('SELECT pg_advisory_xact_lock(hashtextextended($1, 0))', [lockKey])
const existing = await findLatestOrderByPlatformOrderIdWithExecutor(executor, {
provider: event.provider,
platform: event.platform,
platformOrderId: event.platformOrderId,
})
const basePayload = {
provider: event.provider,
platform: event.platform,
@@ -142,14 +147,13 @@ export async function upsertOrderFromSource(
...(event.paidAt !== undefined ? { paidAt: event.paidAt } : {}),
}
const mergedPayload = mergeSourceOrderState(existing, basePayload)
const order = existing
? await updateOrder(existing.id, {
? await updateOrderWithExecutor(executor, existing.id, {
...basePayload,
...mergedPayload,
updatedAt: now,
})
: await createOrder({
: await createOrderWithExecutor(executor, {
...basePayload,
...mergedPayload,
createdAt: now,
@@ -163,7 +167,8 @@ export async function upsertOrderFromSource(
})
}
const orderItems = await replaceOrderItems(
const orderItems = await replaceOrderItemsWithExecutor(
executor,
order.id,
resolvedFulfillmentItems.map((item) => ({
skuCode: item.skuCode,
@@ -176,6 +181,11 @@ export async function upsertOrderFromSource(
})),
)
return { order, orderItems }
})
const { order, orderItems } = persisted
const configuredOrderItems = orderItems.filter(isOrderItemConfiguredForFulfillment)
const readiness =
configuredOrderItems.length > 0
+66
View File
@@ -0,0 +1,66 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import type { Server } from 'node:http'
import { createShutdownController } from './shutdown.js'
import type { StartupState } from './state.js'
function createStartupState(): StartupState {
return {
phase: 'ready',
core: {
running: false,
ready: true,
attemptCount: 1,
lastAttemptAt: '',
lastError: '',
readyAt: '',
},
process: {
lastUnhandledRejection: null,
lastUncaughtException: null,
},
}
}
test('shutdown closes the HTTP server and database pool', async () => {
let serverClosed = false
let databaseClosed = false
const server = {
listening: true,
close(callback: (error?: Error) => void) {
serverClosed = true
callback()
},
} as unknown as Server
const controller = createShutdownController(server, createStartupState(), {
closeDatabase: async () => {
databaseClosed = true
},
})
await controller.shutdown('test')
assert.equal(serverClosed, true)
assert.equal(databaseClosed, true)
assert.equal(controller.isShutdownStarted(), true)
})
test('shutdown closes the database pool when the server is already stopped', async () => {
let databaseClosed = false
const server = {
listening: false,
close() {
throw new Error('close should not be called')
},
} as unknown as Server
const controller = createShutdownController(server, createStartupState(), {
closeDatabase: async () => {
databaseClosed = true
},
})
await controller.shutdown('test')
assert.equal(databaseClosed, true)
})
+19 -5
View File
@@ -2,10 +2,19 @@ import type { Server } from 'node:http'
import { stopKuaishouIndustrySendCallbackRetryWorker } from '../services/platforms/kuaishou-industry/send-code-service.js'
import { stopScheduledJobs } from '../services/scheduler/scheduler-service.js'
import { closeDb } from '../db/client.js'
import { logError, logInfo } from '../utils/logger.js'
import type { StartupState } from './state.js'
export function createShutdownController(server: Server, startupState: StartupState) {
type ShutdownOptions = {
closeDatabase?: () => Promise<void>
}
export function createShutdownController(
server: Server,
startupState: StartupState,
{ closeDatabase = closeDb }: ShutdownOptions = {},
) {
let shutdownStarted = false
async function shutdown(signal: string) {
@@ -20,10 +29,7 @@ export function createShutdownController(server: Server, startupState: StartupSt
stopScheduledJobs()
stopKuaishouIndustrySendCallbackRetryWorker()
if (!server.listening) {
return
}
if (server.listening) {
await new Promise<void>((resolve) => {
server.close((error) => {
if (error) {
@@ -36,6 +42,14 @@ export function createShutdownController(server: Server, startupState: StartupSt
})
}
try {
await closeDatabase()
} catch (error) {
logError('[shutdown]', 'failed to close database pool', error)
process.exitCode = 1
}
}
return {
isShutdownStarted: () => shutdownStarted,
shutdown,