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

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 { OrderItemReplaceInput } from '../types/repository/inputs.js'
import type { OrderItemRow } from '../types/repository/rows.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 = { type OrderItemSyncPlan = {
updates: Array<{ updates: Array<{
@@ -25,14 +25,22 @@ export async function replaceOrderItems(
orderId: number | string, orderId: number | string,
items: OrderItemReplaceInput[], items: OrderItemReplaceInput[],
): Promise<OrderItemRow[]> { ): Promise<OrderItemRow[]> {
return withTransaction(async (client: PoolClient) => { return withTransaction((client: PoolClient) =>
const executor: QueryExecutor = client.query.bind(client) replaceOrderItemsWithExecutor(client.query.bind(client), orderId, items),
const existingItems = await listOrderItemsByOrderIdWithExecutor(executor, orderId) )
const plan = resolveOrderItemSyncPlan(existingItems, items) }
for (const update of plan.updates) { export async function replaceOrderItemsWithExecutor(
await executor( executor: QueryExecutor,
` orderId: number | string,
items: OrderItemReplaceInput[],
): Promise<OrderItemRow[]> {
const existingItems = await listOrderItemsByOrderIdWithExecutor(executor, orderId)
const plan = resolveOrderItemSyncPlan(existingItems, items)
for (const update of plan.updates) {
await executor(
`
UPDATE order_items UPDATE order_items
SET SET
sku_code = $1, sku_code = $1,
@@ -43,21 +51,21 @@ export async function replaceOrderItems(
updated_at = $6 updated_at = $6
WHERE id = $7 WHERE id = $7
`, `,
[ [
update.item.skuCode, update.item.skuCode,
update.item.skuName, update.item.skuName,
update.item.quantity, update.item.quantity,
update.item.specJson || '{}', update.item.specJson || '{}',
update.item.itemSnapshotJson || update.item.specJson || '{}', update.item.itemSnapshotJson || update.item.specJson || '{}',
update.item.updatedAt, update.item.updatedAt,
update.orderItemId, update.orderItemId,
], ],
) )
} }
for (const create of plan.creates) { for (const create of plan.creates) {
await executor( await executor(
` `
INSERT INTO order_items ( INSERT INTO order_items (
order_id, order_id,
sku_code, sku_code,
@@ -69,29 +77,28 @@ export async function replaceOrderItems(
updated_at updated_at
) VALUES ($1, $2, $3, $4, $5::jsonb, $6::jsonb, $7, $8) ) VALUES ($1, $2, $3, $4, $5::jsonb, $6::jsonb, $7, $8)
`, `,
[ [
Number(orderId), Number(orderId),
create.skuCode, create.skuCode,
create.skuName, create.skuName,
create.quantity, create.quantity,
create.specJson || '{}', create.specJson || '{}',
create.itemSnapshotJson || create.specJson || '{}', create.itemSnapshotJson || create.specJson || '{}',
create.createdAt, create.createdAt,
create.updatedAt, create.updatedAt,
], ],
) )
}
if (plan.deletes.length > 0) {
const deletableIds = await listDeletableOrderItemIdsWithExecutor(executor, plan.deletes)
if (deletableIds.length > 0) {
await executor('DELETE FROM order_items WHERE id = ANY($1::bigint[])', [deletableIds])
} }
}
if (plan.deletes.length > 0) { return listOrderItemsByOrderIdWithExecutor(executor, orderId)
const deletableIds = await listDeletableOrderItemIdsWithExecutor(executor, plan.deletes)
if (deletableIds.length > 0) {
await executor('DELETE FROM order_items WHERE id = ANY($1::bigint[])', [deletableIds])
}
}
return listOrderItemsByOrderIdWithExecutor(executor, orderId)
})
} }
export function resolveOrderItemSyncPlan( export function resolveOrderItemSyncPlan(
+36 -3
View File
@@ -1,3 +1,5 @@
import type { QueryResult, QueryResultRow } from 'pg'
import { query } from '../db/client.js' import { query } from '../db/client.js'
import type { import type {
OrderCreateInput, OrderCreateInput,
@@ -6,6 +8,11 @@ import type {
} from '../types/repository/inputs.js' } from '../types/repository/inputs.js'
import type { OrderListQueryResult, OrderListRow, OrderRow } from '../types/repository/rows.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 = { type OrderPlatformLookupInput = {
provider?: string provider?: string
platform: string platform: string
@@ -37,7 +44,18 @@ export async function findLatestOrderByPlatformOrderId({
platform, platform,
platformOrderId, platformOrderId,
}: Omit<OrderPlatformLookupInput, 'shopId'>): Promise<OrderRow | null> { }: 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 * SELECT *
FROM orders FROM orders
@@ -69,7 +87,14 @@ export async function findLatestOrderByAnyPlatformOrderId(
} }
export async function createOrder(input: OrderCreateInput): Promise<OrderRow | null> { 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 ( INSERT INTO orders (
provider, provider,
@@ -118,7 +143,15 @@ export async function updateOrder(
orderId: number | string, orderId: number | string,
input: OrderUpdateInput, input: OrderUpdateInput,
): Promise<OrderRow | null> { ): 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 UPDATE orders
SET SET
@@ -1,9 +1,10 @@
import { import {
createOrder, createOrderWithExecutor,
findLatestOrderByPlatformOrderId, findLatestOrderByPlatformOrderIdWithExecutor,
updateOrder, updateOrderWithExecutor,
} from '../../repositories/order-repo.js' } 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 { syncDeliveryTasksForOrder } from './delivery-task-service.js'
import { resolveOrderItemForFulfillment } from '../fulfillment/product-resolution-service.js' import { resolveOrderItemForFulfillment } from '../fulfillment/product-resolution-service.js'
import { import {
@@ -98,11 +99,6 @@ export async function upsertOrderFromSource(
{ sourceLabel = 'source' }: UpsertOrderSourceOptions = {}, { sourceLabel = 'source' }: UpsertOrderSourceOptions = {},
): Promise<UpsertOrderResult> { ): Promise<UpsertOrderResult> {
const now = nowIso() const now = nowIso()
const existing = await findLatestOrderByPlatformOrderId({
provider: event.provider,
platform: event.platform,
platformOrderId: event.platformOrderId,
})
logIntegration('[order-service]', `开始处理 ${sourceLabel} 订单 upsert`, { logIntegration('[order-service]', `开始处理 ${sourceLabel} 订单 upsert`, {
provider: event.provider, provider: event.provider,
@@ -110,7 +106,6 @@ export async function upsertOrderFromSource(
shopId: event.shopId, shopId: event.shopId,
shopName: event.shopName, shopName: event.shopName,
platformOrderId: event.platformOrderId, platformOrderId: event.platformOrderId,
existingOrderId: existing?.id || null,
}) })
const resolvedItems = await Promise.all( const resolvedItems = await Promise.all(
@@ -125,56 +120,71 @@ export async function upsertOrderFromSource(
const resolvedFulfillmentItems = resolvedItems as FulfillmentOrderItem[] const resolvedFulfillmentItems = resolvedItems as FulfillmentOrderItem[]
const configuredItems = resolvedFulfillmentItems.filter((item) => item.isConfigured) const configuredItems = resolvedFulfillmentItems.filter((item) => item.isConfigured)
const basePayload = { const persisted = await withTransaction(async (client) => {
provider: event.provider, const executor = client.query.bind(client)
platform: event.platform, const lockKey = [event.provider, event.platform, event.platformOrderId].join('|')
shopId: String(existing?.shop_id || event.shopId || '').trim(), await executor('SELECT pg_advisory_xact_lock(hashtextextended($1, 0))', [lockKey])
shopName: String(existing?.shop_name || event.shopName || '').trim(),
platformOrderId: event.platformOrderId,
orderStatus: event.orderStatus,
payStatus: event.payStatus,
buyerId: event.buyerId,
buyerName: event.buyerName,
receiverContact: event.receiverContact,
totalAmount: event.totalAmount,
currency: event.currency,
rawPayloadJson: JSON.stringify(event.rawPayload),
...(event.paidAt !== undefined ? { paidAt: event.paidAt } : {}),
}
const mergedPayload = mergeSourceOrderState(existing, basePayload)
const order = existing const existing = await findLatestOrderByPlatformOrderIdWithExecutor(executor, {
? await updateOrder(existing.id, { provider: event.provider,
...basePayload, platform: event.platform,
...mergedPayload, platformOrderId: event.platformOrderId,
updatedAt: now, })
const basePayload = {
provider: event.provider,
platform: event.platform,
shopId: String(existing?.shop_id || event.shopId || '').trim(),
shopName: String(existing?.shop_name || event.shopName || '').trim(),
platformOrderId: event.platformOrderId,
orderStatus: event.orderStatus,
payStatus: event.payStatus,
buyerId: event.buyerId,
buyerName: event.buyerName,
receiverContact: event.receiverContact,
totalAmount: event.totalAmount,
currency: event.currency,
rawPayloadJson: JSON.stringify(event.rawPayload),
...(event.paidAt !== undefined ? { paidAt: event.paidAt } : {}),
}
const mergedPayload = mergeSourceOrderState(existing, basePayload)
const order = existing
? await updateOrderWithExecutor(executor, existing.id, {
...basePayload,
...mergedPayload,
updatedAt: now,
})
: await createOrderWithExecutor(executor, {
...basePayload,
...mergedPayload,
createdAt: now,
updatedAt: now,
})
if (!order) {
throw createHttpError('订单写入失败', {
statusCode: 500,
errorCode: 'order_write_failed',
}) })
: await createOrder({ }
...basePayload,
...mergedPayload, const orderItems = await replaceOrderItemsWithExecutor(
executor,
order.id,
resolvedFulfillmentItems.map((item) => ({
skuCode: item.skuCode,
skuName: item.skuName,
quantity: item.quantity,
specJson: JSON.stringify(item.spec || {}),
itemSnapshotJson: JSON.stringify(item.snapshot || item.spec || {}),
createdAt: now, createdAt: now,
updatedAt: now, updatedAt: now,
}) })),
)
if (!order) { return { order, orderItems }
throw createHttpError('订单写入失败', { })
statusCode: 500,
errorCode: 'order_write_failed',
})
}
const orderItems = await replaceOrderItems( const { order, orderItems } = persisted
order.id,
resolvedFulfillmentItems.map((item) => ({
skuCode: item.skuCode,
skuName: item.skuName,
quantity: item.quantity,
specJson: JSON.stringify(item.spec || {}),
itemSnapshotJson: JSON.stringify(item.snapshot || item.spec || {}),
createdAt: now,
updatedAt: now,
})),
)
const configuredOrderItems = orderItems.filter(isOrderItemConfiguredForFulfillment) const configuredOrderItems = orderItems.filter(isOrderItemConfiguredForFulfillment)
const readiness = const readiness =
+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)
})
+27 -13
View File
@@ -2,10 +2,19 @@ import type { Server } from 'node:http'
import { stopKuaishouIndustrySendCallbackRetryWorker } from '../services/platforms/kuaishou-industry/send-code-service.js' import { stopKuaishouIndustrySendCallbackRetryWorker } from '../services/platforms/kuaishou-industry/send-code-service.js'
import { stopScheduledJobs } from '../services/scheduler/scheduler-service.js' import { stopScheduledJobs } from '../services/scheduler/scheduler-service.js'
import { closeDb } from '../db/client.js'
import { logError, logInfo } from '../utils/logger.js' import { logError, logInfo } from '../utils/logger.js'
import type { StartupState } from './state.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 let shutdownStarted = false
async function shutdown(signal: string) { async function shutdown(signal: string) {
@@ -20,20 +29,25 @@ export function createShutdownController(server: Server, startupState: StartupSt
stopScheduledJobs() stopScheduledJobs()
stopKuaishouIndustrySendCallbackRetryWorker() stopKuaishouIndustrySendCallbackRetryWorker()
if (!server.listening) { if (server.listening) {
return await new Promise<void>((resolve) => {
server.close((error) => {
if (error) {
logError('[shutdown]', 'failed to close HTTP server', error)
process.exitCode = 1
}
resolve()
})
})
} }
await new Promise<void>((resolve) => { try {
server.close((error) => { await closeDatabase()
if (error) { } catch (error) {
logError('[shutdown]', 'failed to close HTTP server', error) logError('[shutdown]', 'failed to close database pool', error)
process.exitCode = 1 process.exitCode = 1
} }
resolve()
})
})
} }
return { return {