后端迁移低风险仓储模块
This commit is contained in:
+88
-15
@@ -1,7 +1,72 @@
|
||||
import { query } from '../db/client.js'
|
||||
import type {
|
||||
MessageDeliveryListQueryResult,
|
||||
MessageDeliveryRow,
|
||||
} from '../types/repository-rows.js'
|
||||
|
||||
export async function createMessageDelivery(input) {
|
||||
const result = await query(
|
||||
type MessageDeliveryCreateInput = {
|
||||
provider: string
|
||||
platform: string
|
||||
shopId?: string
|
||||
shopName?: string
|
||||
channel: string
|
||||
orderId?: number | string | null
|
||||
taskId?: number | string | null
|
||||
platformOrderId?: string
|
||||
recipientKey?: string
|
||||
messageContent?: string
|
||||
claimUrl?: string
|
||||
status?: string
|
||||
requestUrl?: string
|
||||
requestHeadersJson?: string | Record<string, unknown>
|
||||
requestBodyJson?: string | Record<string, unknown>
|
||||
responseStatus?: number | string
|
||||
responseJson?: string | Record<string, unknown>
|
||||
errorMessage?: string
|
||||
sentAt?: string | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
type MessageDeliveryPatch = Partial<Pick<
|
||||
MessageDeliveryRow,
|
||||
| 'status'
|
||||
| 'request_url'
|
||||
| 'request_headers_json'
|
||||
| 'request_body_json'
|
||||
| 'response_status'
|
||||
| 'response_json'
|
||||
| 'error_message'
|
||||
| 'sent_at'
|
||||
| 'updated_at'
|
||||
>>
|
||||
|
||||
type LatestSuccessfulMessageDeliveryQuery = {
|
||||
provider?: string
|
||||
platform?: string
|
||||
shopId?: string
|
||||
platformOrderId?: string
|
||||
channel?: string
|
||||
claimUrl?: string
|
||||
}
|
||||
|
||||
type MessageDeliveryListQuery = {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
provider?: string
|
||||
platform?: string
|
||||
status?: string
|
||||
shopId?: string
|
||||
platformOrderId?: string
|
||||
taskNo?: string
|
||||
dateFrom?: string
|
||||
dateTo?: string
|
||||
}
|
||||
|
||||
export async function createMessageDelivery(
|
||||
input: MessageDeliveryCreateInput,
|
||||
): Promise<MessageDeliveryRow | null> {
|
||||
const result = await query<MessageDeliveryRow>(
|
||||
`
|
||||
INSERT INTO message_deliveries (
|
||||
provider,
|
||||
@@ -56,19 +121,24 @@ export async function createMessageDelivery(input) {
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
export async function getMessageDeliveryById(deliveryId) {
|
||||
const result = await query('SELECT * FROM message_deliveries WHERE id = $1 LIMIT 1', [Number(deliveryId)])
|
||||
export async function getMessageDeliveryById(deliveryId: number | string): Promise<MessageDeliveryRow | null> {
|
||||
const result = await query<MessageDeliveryRow>('SELECT * FROM message_deliveries WHERE id = $1 LIMIT 1', [
|
||||
Number(deliveryId),
|
||||
])
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
export async function updateMessageDelivery(deliveryId, patch = {}) {
|
||||
export async function updateMessageDelivery(
|
||||
deliveryId: number | string,
|
||||
patch: MessageDeliveryPatch = {},
|
||||
): Promise<MessageDeliveryRow | null> {
|
||||
const current = await getMessageDeliveryById(deliveryId)
|
||||
if (!current) {
|
||||
return null
|
||||
}
|
||||
|
||||
const next = { ...current, ...patch }
|
||||
const result = await query(
|
||||
const result = await query<MessageDeliveryRow>(
|
||||
`
|
||||
UPDATE message_deliveries
|
||||
SET
|
||||
@@ -101,8 +171,11 @@ export async function updateMessageDelivery(deliveryId, patch = {}) {
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
export async function findLatestSuccessfulMessageDeliveryByTask(taskId, channel) {
|
||||
const result = await query(
|
||||
export async function findLatestSuccessfulMessageDeliveryByTask(
|
||||
taskId: number | string,
|
||||
channel: string,
|
||||
): Promise<MessageDeliveryRow | null> {
|
||||
const result = await query<MessageDeliveryRow>(
|
||||
`
|
||||
SELECT *
|
||||
FROM message_deliveries
|
||||
@@ -123,8 +196,8 @@ export async function findLatestSuccessfulMessageDelivery({
|
||||
platformOrderId = '',
|
||||
channel = '',
|
||||
claimUrl = '',
|
||||
} = {}) {
|
||||
const result = await query(
|
||||
}: LatestSuccessfulMessageDeliveryQuery = {}): Promise<MessageDeliveryRow | null> {
|
||||
const result = await query<MessageDeliveryRow>(
|
||||
`
|
||||
SELECT *
|
||||
FROM message_deliveries
|
||||
@@ -162,10 +235,10 @@ export async function listMessageDeliveries({
|
||||
taskNo = '',
|
||||
dateFrom = '',
|
||||
dateTo = '',
|
||||
} = {}) {
|
||||
}: MessageDeliveryListQuery = {}): Promise<MessageDeliveryListQueryResult> {
|
||||
const offset = (page - 1) * pageSize
|
||||
const filters = []
|
||||
const params = []
|
||||
const filters: string[] = []
|
||||
const params: unknown[] = []
|
||||
|
||||
if (provider) {
|
||||
params.push(provider)
|
||||
@@ -213,14 +286,14 @@ export async function listMessageDeliveries({
|
||||
LEFT JOIN fulfillment_tasks ft ON ft.id = md.task_id
|
||||
`
|
||||
|
||||
const totalResult = await query(
|
||||
const totalResult = await query<{ [column: string]: unknown, total: number }>(
|
||||
`SELECT COUNT(*)::int AS total ${fromClause} ${whereClause}`,
|
||||
params,
|
||||
)
|
||||
|
||||
params.push(pageSize)
|
||||
params.push(offset)
|
||||
const itemsResult = await query(
|
||||
const itemsResult = await query<MessageDeliveryRow>(
|
||||
`
|
||||
SELECT
|
||||
md.*,
|
||||
+55
-37
@@ -1,20 +1,32 @@
|
||||
// @ts-check
|
||||
import type { PoolClient, QueryResult } from 'pg'
|
||||
|
||||
import { query, withTransaction } from '../db/client.js'
|
||||
import type { OrderItemReplaceInput } from '../types/repository-inputs.js'
|
||||
import type { OrderItemRow } from '../types/repository-rows.js'
|
||||
|
||||
/** @typedef {import('../types/repository-inputs.js').OrderItemReplaceInput} OrderItemReplaceInput */
|
||||
/** @typedef {import('../types/repository-rows.js').OrderItemRow} OrderItemRow */
|
||||
type QueryExecutor = (text: string, params?: unknown[]) => Promise<QueryResult<any>>
|
||||
|
||||
/** @returns {Promise<OrderItemRow[]>} */
|
||||
export async function listOrderItemsByOrderId(orderId) {
|
||||
type OrderItemSyncPlan = {
|
||||
updates: Array<{
|
||||
orderItemId: number
|
||||
item: OrderItemReplaceInput
|
||||
}>
|
||||
creates: OrderItemReplaceInput[]
|
||||
deletes: number[]
|
||||
}
|
||||
|
||||
type OrderItemIdentityInput = Partial<OrderItemReplaceInput> & Partial<OrderItemRow>
|
||||
|
||||
export async function listOrderItemsByOrderId(orderId: number | string): Promise<OrderItemRow[]> {
|
||||
return listOrderItemsByOrderIdWithExecutor(query, orderId)
|
||||
}
|
||||
|
||||
/** @returns {Promise<OrderItemRow[]>} */
|
||||
/** @param {OrderItemReplaceInput[]} items */
|
||||
export async function replaceOrderItems(orderId, items) {
|
||||
return withTransaction(async (client) => {
|
||||
const executor = client.query.bind(client)
|
||||
export async function replaceOrderItems(
|
||||
orderId: number | string,
|
||||
items: OrderItemReplaceInput[],
|
||||
): Promise<OrderItemRow[]> {
|
||||
return withTransaction(async (client: PoolClient) => {
|
||||
const executor: QueryExecutor = client.query.bind(client)
|
||||
const existingItems = await listOrderItemsByOrderIdWithExecutor(executor, orderId)
|
||||
const plan = resolveOrderItemSyncPlan(existingItems, items)
|
||||
|
||||
@@ -74,10 +86,7 @@ export async function replaceOrderItems(orderId, items) {
|
||||
const deletableIds = await listDeletableOrderItemIdsWithExecutor(executor, plan.deletes)
|
||||
|
||||
if (deletableIds.length > 0) {
|
||||
await executor(
|
||||
'DELETE FROM order_items WHERE id = ANY($1::bigint[])',
|
||||
[deletableIds],
|
||||
)
|
||||
await executor('DELETE FROM order_items WHERE id = ANY($1::bigint[])', [deletableIds])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,16 +94,15 @@ export async function replaceOrderItems(orderId, items) {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {OrderItemRow[]} existingItems
|
||||
* @param {OrderItemReplaceInput[]} nextItems
|
||||
*/
|
||||
export function resolveOrderItemSyncPlan(existingItems, nextItems) {
|
||||
export function resolveOrderItemSyncPlan(
|
||||
existingItems: OrderItemRow[],
|
||||
nextItems: OrderItemReplaceInput[],
|
||||
): OrderItemSyncPlan {
|
||||
const normalizedExisting = Array.isArray(existingItems) ? existingItems : []
|
||||
const normalizedNext = Array.isArray(nextItems) ? nextItems : []
|
||||
const unmatchedExisting = [...normalizedExisting]
|
||||
const updates = []
|
||||
const creates = []
|
||||
const updates: OrderItemSyncPlan['updates'] = []
|
||||
const creates: OrderItemReplaceInput[] = []
|
||||
|
||||
for (const item of normalizedNext) {
|
||||
const matchedIndex = findMatchingExistingOrderItemIndex(unmatchedExisting, item)
|
||||
@@ -110,10 +118,12 @@ export function resolveOrderItemSyncPlan(existingItems, nextItems) {
|
||||
|
||||
if (unmatchedExisting.length > 0) {
|
||||
const matched = unmatchedExisting.shift()
|
||||
updates.push({
|
||||
orderItemId: matched.id,
|
||||
item,
|
||||
})
|
||||
if (matched) {
|
||||
updates.push({
|
||||
orderItemId: matched.id,
|
||||
item,
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -127,21 +137,27 @@ export function resolveOrderItemSyncPlan(existingItems, nextItems) {
|
||||
}
|
||||
}
|
||||
|
||||
function findMatchingExistingOrderItemIndex(existingItems, nextItem) {
|
||||
function findMatchingExistingOrderItemIndex(
|
||||
existingItems: OrderItemRow[],
|
||||
nextItem: OrderItemReplaceInput,
|
||||
): number {
|
||||
const nextIdentity = buildOrderItemIdentity(nextItem)
|
||||
|
||||
return existingItems.findIndex((item) => buildOrderItemIdentity(item) === nextIdentity)
|
||||
}
|
||||
|
||||
function buildOrderItemIdentity(item) {
|
||||
function buildOrderItemIdentity(item: OrderItemIdentityInput): string {
|
||||
return [
|
||||
String(item?.skuCode ?? item?.sku_code ?? '').trim(),
|
||||
String(item?.skuName ?? item?.sku_name ?? '').trim(),
|
||||
String(Math.max(1, Number(item?.quantity || 1))),
|
||||
String(item.skuCode ?? item.sku_code ?? '').trim(),
|
||||
String(item.skuName ?? item.sku_name ?? '').trim(),
|
||||
String(Math.max(1, Number(item.quantity || 1))),
|
||||
].join('::')
|
||||
}
|
||||
|
||||
async function listDeletableOrderItemIdsWithExecutor(executor, orderItemIds) {
|
||||
async function listDeletableOrderItemIdsWithExecutor(
|
||||
executor: QueryExecutor,
|
||||
orderItemIds: number[],
|
||||
): Promise<number[]> {
|
||||
const normalizedIds = Array.isArray(orderItemIds)
|
||||
? orderItemIds.map((value) => Number(value)).filter((value) => value > 0)
|
||||
: []
|
||||
@@ -166,7 +182,10 @@ async function listDeletableOrderItemIdsWithExecutor(executor, orderItemIds) {
|
||||
return result.rows.map((row) => Number(row.id)).filter((value) => value > 0)
|
||||
}
|
||||
|
||||
async function listOrderItemsByOrderIdWithExecutor(executor, orderId) {
|
||||
async function listOrderItemsByOrderIdWithExecutor(
|
||||
executor: QueryExecutor,
|
||||
orderId: number | string,
|
||||
): Promise<OrderItemRow[]> {
|
||||
const result = await executor(
|
||||
`
|
||||
SELECT *
|
||||
@@ -177,11 +196,10 @@ async function listOrderItemsByOrderIdWithExecutor(executor, orderId) {
|
||||
[Number(orderId)],
|
||||
)
|
||||
|
||||
return /** @type {OrderItemRow[]} */ (result.rows)
|
||||
return result.rows
|
||||
}
|
||||
|
||||
/** @returns {Promise<OrderItemRow | null>} */
|
||||
export async function getOrderItemById(orderItemId) {
|
||||
const result = await query('SELECT * FROM order_items WHERE id = $1 LIMIT 1', [Number(orderItemId)])
|
||||
return /** @type {OrderItemRow | null} */ (result.rows[0] || null)
|
||||
export async function getOrderItemById(orderItemId: number | string): Promise<OrderItemRow | null> {
|
||||
const result = await query<OrderItemRow>('SELECT * FROM order_items WHERE id = $1 LIMIT 1', [Number(orderItemId)])
|
||||
return result.rows[0] || null
|
||||
}
|
||||
+35
-34
@@ -1,17 +1,16 @@
|
||||
// @ts-check
|
||||
|
||||
import { query } from '../db/client.js'
|
||||
import type {
|
||||
WebhookEventCreateInput,
|
||||
WebhookEventListQueryInput,
|
||||
WebhookEventUpdatePatch,
|
||||
} from '../types/repository-inputs.js'
|
||||
import type {
|
||||
WebhookEventListQueryResult,
|
||||
WebhookEventRow,
|
||||
} from '../types/repository-rows.js'
|
||||
|
||||
/** @typedef {import('../types/repository-inputs.js').WebhookEventCreateInput} WebhookEventCreateInput */
|
||||
/** @typedef {import('../types/repository-inputs.js').WebhookEventListQueryInput} WebhookEventListQueryInput */
|
||||
/** @typedef {import('../types/repository-inputs.js').WebhookEventUpdatePatch} WebhookEventUpdatePatch */
|
||||
/** @typedef {import('../types/repository-rows.js').WebhookEventListQueryResult} WebhookEventListQueryResult */
|
||||
/** @typedef {import('../types/repository-rows.js').WebhookEventRow} WebhookEventRow */
|
||||
|
||||
/** @returns {Promise<WebhookEventRow | null>} */
|
||||
/** @param {WebhookEventCreateInput} input */
|
||||
export async function createWebhookEvent(input) {
|
||||
const result = await query(
|
||||
export async function createWebhookEvent(input: WebhookEventCreateInput): Promise<WebhookEventRow | null> {
|
||||
const result = await query<WebhookEventRow>(
|
||||
`
|
||||
INSERT INTO webhook_events (
|
||||
provider,
|
||||
@@ -49,19 +48,20 @@ export async function createWebhookEvent(input) {
|
||||
],
|
||||
)
|
||||
|
||||
return /** @type {WebhookEventRow | null} */ (result.rows[0] || null)
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
/** @returns {Promise<WebhookEventRow | null>} */
|
||||
/** @param {WebhookEventUpdatePatch} patch */
|
||||
export async function updateWebhookEvent(eventId, patch) {
|
||||
export async function updateWebhookEvent(
|
||||
eventId: number | string,
|
||||
patch: WebhookEventUpdatePatch,
|
||||
): Promise<WebhookEventRow | null> {
|
||||
const current = await getWebhookEventById(eventId)
|
||||
if (!current) {
|
||||
return null
|
||||
}
|
||||
|
||||
const next = { ...current, ...patch }
|
||||
const result = await query(
|
||||
const result = await query<WebhookEventRow>(
|
||||
`
|
||||
UPDATE webhook_events
|
||||
SET
|
||||
@@ -74,17 +74,16 @@ export async function updateWebhookEvent(eventId, patch) {
|
||||
[Boolean(next.processed), next.process_error || '', next.related_order_id || null, Number(eventId)],
|
||||
)
|
||||
|
||||
return /** @type {WebhookEventRow | null} */ (result.rows[0] || null)
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
/** @returns {Promise<WebhookEventRow | null>} */
|
||||
export async function getWebhookEventById(eventId) {
|
||||
const result = await query('SELECT * FROM webhook_events WHERE id = $1 LIMIT 1', [Number(eventId)])
|
||||
return /** @type {WebhookEventRow | null} */ (result.rows[0] || null)
|
||||
export async function getWebhookEventById(eventId: number | string): Promise<WebhookEventRow | null> {
|
||||
const result = await query<WebhookEventRow>('SELECT * FROM webhook_events WHERE id = $1 LIMIT 1', [
|
||||
Number(eventId),
|
||||
])
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
/** @returns {Promise<WebhookEventListQueryResult>} */
|
||||
/** @param {WebhookEventListQueryInput} [queryInput] */
|
||||
export async function listWebhookEvents({
|
||||
page = 1,
|
||||
pageSize = 20,
|
||||
@@ -96,10 +95,10 @@ export async function listWebhookEvents({
|
||||
relatedOrderId = '',
|
||||
dateFrom = '',
|
||||
dateTo = '',
|
||||
} = /** @type {WebhookEventListQueryInput} */ ({})) {
|
||||
}: WebhookEventListQueryInput = {}): Promise<WebhookEventListQueryResult> {
|
||||
const offset = (page - 1) * pageSize
|
||||
const filters = []
|
||||
const params = []
|
||||
const filters: string[] = []
|
||||
const params: unknown[] = []
|
||||
|
||||
if (provider) {
|
||||
params.push(provider)
|
||||
@@ -143,11 +142,14 @@ export async function listWebhookEvents({
|
||||
}
|
||||
|
||||
const whereClause = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : ''
|
||||
const totalResult = await query(`SELECT COUNT(*)::int AS total FROM webhook_events ${whereClause}`, params)
|
||||
const totalResult = await query<{ [column: string]: unknown, total: number }>(
|
||||
`SELECT COUNT(*)::int AS total FROM webhook_events ${whereClause}`,
|
||||
params,
|
||||
)
|
||||
|
||||
params.push(pageSize)
|
||||
params.push(offset)
|
||||
const itemsResult = await query(
|
||||
const itemsResult = await query<WebhookEventRow>(
|
||||
`
|
||||
SELECT *
|
||||
FROM webhook_events
|
||||
@@ -159,14 +161,13 @@ export async function listWebhookEvents({
|
||||
)
|
||||
|
||||
return {
|
||||
items: /** @type {WebhookEventRow[]} */ (itemsResult.rows),
|
||||
items: itemsResult.rows,
|
||||
total: Number(totalResult.rows[0]?.total || 0),
|
||||
}
|
||||
}
|
||||
|
||||
/** @returns {Promise<WebhookEventRow[]>} */
|
||||
export async function listWebhookEventsByOrderId(orderId) {
|
||||
const result = await query(
|
||||
export async function listWebhookEventsByOrderId(orderId: number | string): Promise<WebhookEventRow[]> {
|
||||
const result = await query<WebhookEventRow>(
|
||||
`
|
||||
SELECT *
|
||||
FROM webhook_events
|
||||
@@ -176,5 +177,5 @@ export async function listWebhookEventsByOrderId(orderId) {
|
||||
[Number(orderId)],
|
||||
)
|
||||
|
||||
return /** @type {WebhookEventRow[]} */ (result.rows)
|
||||
return result.rows
|
||||
}
|
||||
@@ -119,6 +119,33 @@ export type WebhookEventRow = {
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export type MessageDeliveryRow = {
|
||||
id: number
|
||||
provider: string
|
||||
platform: string
|
||||
shop_id: string
|
||||
shop_name: string
|
||||
channel: string
|
||||
order_id: number | null
|
||||
task_id: number | null
|
||||
platform_order_id: string
|
||||
recipient_key: string
|
||||
message_content: string
|
||||
claim_url: string
|
||||
status: string
|
||||
request_url: string
|
||||
request_headers_json: string | Record<string, unknown>
|
||||
request_body_json: string | Record<string, unknown>
|
||||
response_status: number
|
||||
response_json: string | Record<string, unknown>
|
||||
error_message: string
|
||||
sent_at: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
task_no?: string
|
||||
task_status?: string
|
||||
}
|
||||
|
||||
export type TaskInventoryBindingRow = {
|
||||
id: number
|
||||
task_id: number
|
||||
@@ -184,3 +211,8 @@ export type WebhookEventListQueryResult = {
|
||||
items: WebhookEventRow[]
|
||||
total: number
|
||||
}
|
||||
|
||||
export type MessageDeliveryListQueryResult = {
|
||||
items: MessageDeliveryRow[]
|
||||
total: number
|
||||
}
|
||||
|
||||
@@ -219,12 +219,17 @@
|
||||
- `npm run build`
|
||||
- `npm test`
|
||||
11. `src/types/*.js` 已改写为真正的 `.ts` 类型导出文件,现有 JS JSDoc `import('...js').TypeName` 引用保持可用
|
||||
12. 低风险 repository 第二批已迁移到 `.ts`:
|
||||
- `src/repositories/order-item-repo.ts`
|
||||
- `src/repositories/message-delivery-repo.ts`
|
||||
- `src/repositories/webhook-event-repo.ts`
|
||||
13. `MessageDeliveryRow` 与列表查询结果已进入共享 repository row 类型
|
||||
|
||||
## 下一步建议
|
||||
|
||||
第一批继续推进时,建议按这个顺序:
|
||||
|
||||
1. 继续迁移低风险 repository:`order-item-repo`、`message-delivery-repo`、`webhook-event-repo`
|
||||
1. 继续迁移剩余 repository:`admin-user-repo`、`inventory-repo`、`order-repo`、`task-repo`
|
||||
2. 拆分并迁移 `runtime.js`,把 env 解析、默认配置加载、配置合并分开
|
||||
3. 为 webhook、库存换码、自动发货补测试
|
||||
|
||||
|
||||
Reference in New Issue
Block a user