diff --git a/apps/backend/src/repositories/message-delivery-repo.js b/apps/backend/src/repositories/message-delivery-repo.ts similarity index 65% rename from apps/backend/src/repositories/message-delivery-repo.js rename to apps/backend/src/repositories/message-delivery-repo.ts index 824e7454..0dcd5525 100644 --- a/apps/backend/src/repositories/message-delivery-repo.js +++ b/apps/backend/src/repositories/message-delivery-repo.ts @@ -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 + requestBodyJson?: string | Record + responseStatus?: number | string + responseJson?: string | Record + errorMessage?: string + sentAt?: string | null + createdAt: string + updatedAt: string +} + +type MessageDeliveryPatch = Partial> + +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 { + const result = await query( ` 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 { + const result = await query('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 { const current = await getMessageDeliveryById(deliveryId) if (!current) { return null } const next = { ...current, ...patch } - const result = await query( + const result = await query( ` 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 { + const result = await query( ` SELECT * FROM message_deliveries @@ -123,8 +196,8 @@ export async function findLatestSuccessfulMessageDelivery({ platformOrderId = '', channel = '', claimUrl = '', -} = {}) { - const result = await query( +}: LatestSuccessfulMessageDeliveryQuery = {}): Promise { + const result = await query( ` SELECT * FROM message_deliveries @@ -162,10 +235,10 @@ export async function listMessageDeliveries({ taskNo = '', dateFrom = '', dateTo = '', -} = {}) { +}: MessageDeliveryListQuery = {}): Promise { 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( ` SELECT md.*, diff --git a/apps/backend/src/repositories/order-item-repo.js b/apps/backend/src/repositories/order-item-repo.ts similarity index 61% rename from apps/backend/src/repositories/order-item-repo.js rename to apps/backend/src/repositories/order-item-repo.ts index 8f30372a..7752dd24 100644 --- a/apps/backend/src/repositories/order-item-repo.js +++ b/apps/backend/src/repositories/order-item-repo.ts @@ -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> -/** @returns {Promise} */ -export async function listOrderItemsByOrderId(orderId) { +type OrderItemSyncPlan = { + updates: Array<{ + orderItemId: number + item: OrderItemReplaceInput + }> + creates: OrderItemReplaceInput[] + deletes: number[] +} + +type OrderItemIdentityInput = Partial & Partial + +export async function listOrderItemsByOrderId(orderId: number | string): Promise { return listOrderItemsByOrderIdWithExecutor(query, orderId) } -/** @returns {Promise} */ -/** @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 { + 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 { 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 { 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} */ -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 { + const result = await query('SELECT * FROM order_items WHERE id = $1 LIMIT 1', [Number(orderItemId)]) + return result.rows[0] || null } diff --git a/apps/backend/src/repositories/webhook-event-repo.js b/apps/backend/src/repositories/webhook-event-repo.ts similarity index 62% rename from apps/backend/src/repositories/webhook-event-repo.js rename to apps/backend/src/repositories/webhook-event-repo.ts index 9463363f..4b8220ef 100644 --- a/apps/backend/src/repositories/webhook-event-repo.js +++ b/apps/backend/src/repositories/webhook-event-repo.ts @@ -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} */ -/** @param {WebhookEventCreateInput} input */ -export async function createWebhookEvent(input) { - const result = await query( +export async function createWebhookEvent(input: WebhookEventCreateInput): Promise { + const result = await query( ` 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} */ -/** @param {WebhookEventUpdatePatch} patch */ -export async function updateWebhookEvent(eventId, patch) { +export async function updateWebhookEvent( + eventId: number | string, + patch: WebhookEventUpdatePatch, +): Promise { const current = await getWebhookEventById(eventId) if (!current) { return null } const next = { ...current, ...patch } - const result = await query( + const result = await query( ` 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} */ -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 { + const result = await query('SELECT * FROM webhook_events WHERE id = $1 LIMIT 1', [ + Number(eventId), + ]) + return result.rows[0] || null } -/** @returns {Promise} */ -/** @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 { 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( ` 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} */ -export async function listWebhookEventsByOrderId(orderId) { - const result = await query( +export async function listWebhookEventsByOrderId(orderId: number | string): Promise { + const result = await query( ` SELECT * FROM webhook_events @@ -176,5 +177,5 @@ export async function listWebhookEventsByOrderId(orderId) { [Number(orderId)], ) - return /** @type {WebhookEventRow[]} */ (result.rows) + return result.rows } diff --git a/apps/backend/src/types/repository-rows.ts b/apps/backend/src/types/repository-rows.ts index 2fbc2b6c..fbbae5e2 100644 --- a/apps/backend/src/types/repository-rows.ts +++ b/apps/backend/src/types/repository-rows.ts @@ -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 + request_body_json: string | Record + response_status: number + response_json: string | Record + 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 +} diff --git a/docs/backend-typescript-migration-plan.md b/docs/backend-typescript-migration-plan.md index 698cc87c..110c3081 100644 --- a/docs/backend-typescript-migration-plan.md +++ b/docs/backend-typescript-migration-plan.md @@ -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、库存换码、自动发货补测试