diff --git a/apps/backend/src/services/admin/admin-inventory-read-helpers.js b/apps/backend/src/services/admin/admin-inventory-read-helpers.js new file mode 100644 index 00000000..180ae3a7 --- /dev/null +++ b/apps/backend/src/services/admin/admin-inventory-read-helpers.js @@ -0,0 +1,51 @@ +// @ts-check + +import { getInventoryItemById } from '../../repositories/inventory-repo.js' +import { getOrderById } from '../../repositories/order-repo.js' +import { getTaskById } from '../../repositories/task-repo.js' +import { createHttpError } from '../../utils/http.js' +import { mapAdminTaskSummary } from './admin-task-read-helpers.js' + +/** @typedef {import('../../types/admin-read-models.js').AdminInventoryItemListItem} AdminInventoryItemListItem */ +/** @typedef {import('../../types/repository-rows.js').InventoryItemRow} InventoryItemRow */ + +/** + * @param {InventoryItemRow} item + * @returns {Promise} + */ +export async function mapAdminInventoryListItem(item) { + const task = item.reserved_by_task_id ? await getTaskById(item.reserved_by_task_id) : null + const order = task?.order_id ? await getOrderById(task.order_id) : null + const taskSummary = task ? mapAdminTaskSummary(task) : null + + return { + inventoryItemId: item.id, + skuCode: item.sku_code, + batchNo: item.batch_no, + credentialType: item.credential_type || 'tencent_code', + displayValue: item.display_value, + status: item.status, + reservedByTaskId: item.reserved_by_task_id, + reservedByTaskNo: task?.task_no || '', + platformOrderId: order?.platform_order_id || '', + systemBindingStatus: item.status === 'consumed' ? 'system_bound' : (taskSummary?.systemBindingStatus || 'pending_binding'), + userBindingStatus: item.status === 'consumed' ? 'binding_completed' : (taskSummary?.userBindingStatus || 'not_started'), + invalidReason: item.invalid_reason || '', + deliveredAt: item.delivered_at, + createdAt: item.created_at, + updatedAt: item.updated_at, + } +} + +export async function getRequiredInventoryItem(inventoryItemId) { + const inventoryItem = await getInventoryItemById(Number(inventoryItemId)) + + if (!inventoryItem) { + throw createHttpError('库存项不存在', { + statusCode: 404, + errorCode: 'admin_inventory_not_found', + }) + } + + return inventoryItem +} diff --git a/apps/backend/src/services/admin/admin-order-read-helpers.js b/apps/backend/src/services/admin/admin-order-read-helpers.js new file mode 100644 index 00000000..b6d4266d --- /dev/null +++ b/apps/backend/src/services/admin/admin-order-read-helpers.js @@ -0,0 +1,74 @@ +// @ts-check + +import { listOrderItemsByOrderId } from '../../repositories/order-item-repo.js' +import { listTasksByOrderId } from '../../repositories/task-repo.js' +import { formatFenToAmount, normalizeFen } from '../../utils/money.js' +import { resolveDisplayShopName, resolveOrderItemTitle } from './admin-read-shared-helpers.js' +import { buildOrderBindingSummary, getTaskBindingSummaryMap } from './admin-task-read-helpers.js' + +/** @typedef {import('../../types/admin-read-models.js').AdminOrderListItem} AdminOrderListItem */ +/** @typedef {import('../../types/repository-rows.js').OrderListRow} OrderListRow */ + +/** + * @param {OrderListRow} item + * @returns {Promise} + */ +export async function mapAdminOrderListItem(item) { + const [tasks, orderItems] = await Promise.all([ + listTasksByOrderId(item.id), + listOrderItemsByOrderId(item.id), + ]) + const taskBindingSummaryMap = await getTaskBindingSummaryMap(tasks.map((task) => task.id)) + const bindingSummary = buildOrderBindingSummary(tasks, taskBindingSummaryMap) + const itemSummary = summarizeOrderItems(orderItems) + + return { + orderId: item.id, + provider: item.provider || 'agiso', + platform: item.platform, + shopId: item.shop_id || '', + shopName: resolveDisplayShopName(item.provider, item.shop_id, item.shop_name), + platformOrderId: item.platform_order_id, + orderStatus: item.order_status, + payStatus: item.pay_status, + buyerId: item.buyer_id || '', + buyerName: item.buyer_name, + receiverContact: item.receiver_contact || '', + totalAmount: formatFenToAmount(item.total_amount), + totalAmountFen: normalizeFen(item.total_amount), + currency: item.currency, + paidAt: item.paid_at, + createdAt: item.created_at, + updatedAt: item.updated_at, + itemCount: orderItems.length, + totalQuantity: orderItems.reduce((sum, orderItem) => sum + Math.max(1, Number(orderItem.quantity || 1)), 0), + itemSummary, + taskCount: Number(item.task_count || tasks.length || 0), + systemBindingStatus: bindingSummary.systemBindingStatus, + userBindingStatus: bindingSummary.userBindingStatus, + systemBoundTaskCount: bindingSummary.systemBoundTaskCount, + completedBindingTaskCount: bindingSummary.completedBindingTaskCount, + totalBindingCount: bindingSummary.totalBindingCount, + reservedBindingCount: bindingSummary.reservedBindingCount, + consumedBindingCount: bindingSummary.consumedBindingCount, + releasedBindingCount: bindingSummary.releasedBindingCount, + } +} + +export function summarizeOrderItems(items) { + const normalizedItems = Array.isArray(items) ? items : [] + + if (normalizedItems.length === 0) { + return '' + } + + const [firstItem] = normalizedItems + const firstLabel = resolveOrderItemTitle(firstItem) + || String(firstItem?.sku_name || firstItem?.sku_code || '').trim() + + if (normalizedItems.length === 1) { + return firstLabel + } + + return `${firstLabel} 等 ${normalizedItems.length} 项` +} diff --git a/apps/backend/src/services/admin/admin-read-helpers.js b/apps/backend/src/services/admin/admin-read-helpers.js deleted file mode 100644 index 4ca03941..00000000 --- a/apps/backend/src/services/admin/admin-read-helpers.js +++ /dev/null @@ -1,308 +0,0 @@ -// @ts-check - -import { extractAgisoTradePayload, resolveAgisoTradePlatformOrderId } from '../order/agiso-trade-parsing.js' -import { getInventoryItemById } from '../../repositories/inventory-repo.js' -import { listOrderItemsByOrderId } from '../../repositories/order-item-repo.js' -import { getOrderById } from '../../repositories/order-repo.js' -import { getTaskById, listTasksByOrderId } from '../../repositories/task-repo.js' -import { createHttpError } from '../../utils/http.js' -import { formatFenToAmount, normalizeFen, parseAmountToFen } from '../../utils/money.js' -import { safeParseJson } from './admin-query-utils.js' -import { resolveDisplayShopName, resolveOrderItemTitle } from './admin-read-shared-helpers.js' -import { buildOrderBindingSummary, getTaskBindingSummaryMap, mapAdminTaskSummary } from './admin-task-read-helpers.js' - -/** @typedef {import('../../types/admin-read-models.js').AdminInventoryItemListItem} AdminInventoryItemListItem */ -/** @typedef {import('../../types/admin-read-models.js').AdminOrderListItem} AdminOrderListItem */ -/** @typedef {import('../../types/admin-read-models.js').AdminWebhookEventListItem} AdminWebhookEventListItem */ -/** @typedef {import('../../types/repository-rows.js').InventoryItemRow} InventoryItemRow */ -/** @typedef {import('../../types/repository-rows.js').OrderListRow} OrderListRow */ -/** @typedef {import('../../types/repository-rows.js').WebhookEventRow} WebhookEventRow */ - -/** - * @param {WebhookEventRow} item - * @returns {Promise} - */ -export async function mapAdminWebhookEvent(item, { includeRaw = false } = {}) { - const headers = normalizeRecord(safeParseJson(item.headers_json)) - const query = normalizeRecord(safeParseJson(item.query_json)) - const body = normalizeRecord(safeParseJson(item.body_json)) - const payload = extractWebhookPayload(body) - const itemSources = extractWebhookItemSources(payload) - const rawAmount = pickFirstNonEmpty([ - payload.total_fee, - payload.totalFee, - payload.TotalFee, - payload.pay_fee, - payload.payFee, - payload.Payment, - payload.payment, - payload.buy_amount, - payload.buyAmount, - ]) - const totalAmountFen = parseAmountToFen(rawAmount) - const taskCount = item.related_order_id ? (await listTasksByOrderId(item.related_order_id)).length : 0 - - const mapped = { - eventId: item.id, - provider: item.provider || 'agiso', - platform: item.platform, - platformRaw: pickFirstNonEmpty([ - query.fromPlatform, - query.from_platform, - body.fromPlatform, - body.from_platform, - payload.fromPlatform, - payload.FromPlatform, - payload.platform, - payload.Platform, - ]), - shopId: item.shop_id || pickFirstNonEmpty([ - payload.PlatformUserId, - payload.platformUserId, - payload.platform_user_id, - payload.seller_id, - payload.sellerId, - payload.SellerId, - payload.seller_nick, - payload.sellerNick, - payload.SellerNick, - ]), - shopName: resolveDisplayShopName( - item.provider, - item.shop_id || pickFirstNonEmpty([ - payload.PlatformUserId, - payload.platformUserId, - payload.platform_user_id, - payload.seller_id, - payload.sellerId, - payload.SellerId, - payload.seller_nick, - payload.sellerNick, - payload.SellerNick, - ]), - item.shop_name || pickFirstNonEmpty([ - payload.shopName, - payload.shop_name, - payload.ShopName, - payload.seller_nick, - payload.sellerNick, - payload.SellerNick, - ]), - ), - eventType: item.event_type, - eventKey: item.event_key, - signatureValid: Boolean(item.signature_valid), - processed: Boolean(item.processed), - processError: item.process_error, - relatedOrderId: item.related_order_id, - createdAt: item.created_at, - platformOrderId: resolveAgisoTradePlatformOrderId(payload), - buyerId: pickFirstNonEmpty([ - payload.buyer_id, - payload.buyerId, - payload.BuyerId, - payload.openid, - payload.BuyerOpenUid, - payload.buyer_open_uid, - payload.buyerOpenUid, - ]), - buyerName: pickFirstNonEmpty([ - payload.buyer_name, - payload.buyerName, - payload.BuyerName, - payload.nick, - payload.BuyerNick, - payload.buyer_nick, - payload.buyerNick, - ]), - totalAmount: rawAmount ? formatFenToAmount(totalAmountFen) : '', - totalAmountFen, - currency: pickFirstNonEmpty([payload.currency, payload.Currency, 'CNY']) || 'CNY', - itemCount: itemSources.length, - taskCount, - messageText: pickFirstNonEmpty([ - payload.MsgTypeDes, - payload.msgTypeDes, - payload.msg_type_desc, - payload.message, - payload.msg, - ]), - requestTimestamp: pickFirstNonEmpty([query.timestamp]), - requestSign: pickFirstNonEmpty([query.sign]), - aopic: pickFirstNonEmpty([query.aopic]), - sourceHost: pickFirstNonEmpty([headers['x-forwarded-host'], headers.host]), - sourceIp: pickFirstNonEmpty([headers['x-forwarded-for'], headers['x-real-ip']]), - userAgent: pickFirstNonEmpty([headers['user-agent'], headers['User-Agent']]), - } - - if (!includeRaw) { - return mapped - } - - return { - ...mapped, - headers, - query, - body, - payload, - } -} - -/** - * @param {InventoryItemRow} item - * @returns {Promise} - */ -export async function mapAdminInventoryListItem(item) { - const task = item.reserved_by_task_id ? await getTaskById(item.reserved_by_task_id) : null - const order = task?.order_id ? await getOrderById(task.order_id) : null - const taskSummary = task ? mapAdminTaskSummary(task) : null - - return { - inventoryItemId: item.id, - skuCode: item.sku_code, - batchNo: item.batch_no, - credentialType: item.credential_type || 'tencent_code', - displayValue: item.display_value, - status: item.status, - reservedByTaskId: item.reserved_by_task_id, - reservedByTaskNo: task?.task_no || '', - platformOrderId: order?.platform_order_id || '', - systemBindingStatus: item.status === 'consumed' ? 'system_bound' : (taskSummary?.systemBindingStatus || 'pending_binding'), - userBindingStatus: item.status === 'consumed' ? 'binding_completed' : (taskSummary?.userBindingStatus || 'not_started'), - invalidReason: item.invalid_reason || '', - deliveredAt: item.delivered_at, - createdAt: item.created_at, - updatedAt: item.updated_at, - } -} - -export async function getRequiredInventoryItem(inventoryItemId) { - const inventoryItem = await getInventoryItemById(Number(inventoryItemId)) - - if (!inventoryItem) { - throw createHttpError('库存项不存在', { - statusCode: 404, - errorCode: 'admin_inventory_not_found', - }) - } - - return inventoryItem -} - -/** - * @param {OrderListRow} item - * @returns {Promise} - */ -export async function mapAdminOrderListItem(item) { - const [tasks, orderItems] = await Promise.all([ - listTasksByOrderId(item.id), - listOrderItemsByOrderId(item.id), - ]) - const taskBindingSummaryMap = await getTaskBindingSummaryMap(tasks.map((task) => task.id)) - const bindingSummary = buildOrderBindingSummary(tasks, taskBindingSummaryMap) - const itemSummary = summarizeOrderItems(orderItems) - - return { - orderId: item.id, - provider: item.provider || 'agiso', - platform: item.platform, - shopId: item.shop_id || '', - shopName: resolveDisplayShopName(item.provider, item.shop_id, item.shop_name), - platformOrderId: item.platform_order_id, - orderStatus: item.order_status, - payStatus: item.pay_status, - buyerId: item.buyer_id || '', - buyerName: item.buyer_name, - receiverContact: item.receiver_contact || '', - totalAmount: formatFenToAmount(item.total_amount), - totalAmountFen: normalizeFen(item.total_amount), - currency: item.currency, - paidAt: item.paid_at, - createdAt: item.created_at, - updatedAt: item.updated_at, - itemCount: orderItems.length, - totalQuantity: orderItems.reduce((sum, orderItem) => sum + Math.max(1, Number(orderItem.quantity || 1)), 0), - itemSummary, - taskCount: Number(item.task_count || tasks.length || 0), - systemBindingStatus: bindingSummary.systemBindingStatus, - userBindingStatus: bindingSummary.userBindingStatus, - systemBoundTaskCount: bindingSummary.systemBoundTaskCount, - completedBindingTaskCount: bindingSummary.completedBindingTaskCount, - totalBindingCount: bindingSummary.totalBindingCount, - reservedBindingCount: bindingSummary.reservedBindingCount, - consumedBindingCount: bindingSummary.consumedBindingCount, - releasedBindingCount: bindingSummary.releasedBindingCount, - } -} - -export function summarizeOrderItems(items) { - const normalizedItems = Array.isArray(items) ? items : [] - - if (normalizedItems.length === 0) { - return '' - } - - const [firstItem] = normalizedItems - const firstLabel = resolveOrderItemTitle(firstItem) - || String(firstItem?.sku_name || firstItem?.sku_code || '').trim() - - if (normalizedItems.length === 1) { - return firstLabel - } - - return `${firstLabel} 等 ${normalizedItems.length} 项` -} - -function extractWebhookPayload(body) { - return extractAgisoTradePayload(body) -} - -function extractWebhookItemSources(payload) { - const normalizedPayload = normalizeRecord(payload) - const candidates = [ - normalizedPayload.orders, - normalizedPayload.Orders, - normalizedPayload.items, - normalizedPayload.Items, - normalizedPayload.order_items, - ] - - for (const candidate of candidates) { - if (Array.isArray(candidate) && candidate.length > 0) { - return candidate.map((item) => normalizeRecord(item)).filter((item) => Object.keys(item).length > 0) - } - } - - if ( - pickFirstNonEmpty([ - normalizedPayload.item_id, - normalizedPayload.itemId, - normalizedPayload.ItemId, - normalizedPayload.num_iid, - normalizedPayload.numIid, - normalizedPayload.NumIid, - ]) - ) { - return [normalizedPayload] - } - - return [] -} - -function normalizeRecord(value) { - return value && typeof value === 'object' && !Array.isArray(value) ? value : {} -} - -function pickFirstNonEmpty(values) { - for (const value of values) { - if (typeof value === 'string' && value.trim()) { - return value.trim() - } - - if (typeof value === 'number' && Number.isFinite(value)) { - return String(value) - } - } - - return '' -} diff --git a/apps/backend/src/services/admin/admin-read-service.js b/apps/backend/src/services/admin/admin-read-service.js index 6e1ae77e..b253c842 100644 --- a/apps/backend/src/services/admin/admin-read-service.js +++ b/apps/backend/src/services/admin/admin-read-service.js @@ -31,12 +31,9 @@ import { resolveOrderItemDeliveryMode, resolveOrderItemTitle, } from './admin-read-shared-helpers.js' -import { - mapAdminInventoryListItem, - mapAdminOrderListItem, - mapAdminWebhookEvent, - summarizeOrderItems, -} from './admin-read-helpers.js' +import { mapAdminInventoryListItem } from './admin-inventory-read-helpers.js' +import { mapAdminOrderListItem, summarizeOrderItems } from './admin-order-read-helpers.js' +import { mapAdminWebhookEvent } from './admin-webhook-read-helpers.js' import { buildOrderAgisoAutoDeliverySummary, buildOrderBindingSummary, diff --git a/apps/backend/src/services/admin/admin-service.js b/apps/backend/src/services/admin/admin-service.js index 2512483f..053cca8c 100644 --- a/apps/backend/src/services/admin/admin-service.js +++ b/apps/backend/src/services/admin/admin-service.js @@ -3,9 +3,12 @@ export { getRequiredInventoryItem, mapAdminInventoryListItem, +} from './admin-inventory-read-helpers.js' +export { mapAdminOrderListItem, - mapAdminWebhookEvent, -} from './admin-read-helpers.js' + summarizeOrderItems, +} from './admin-order-read-helpers.js' +export { mapAdminWebhookEvent } from './admin-webhook-read-helpers.js' export { buildOrderAgisoAutoDeliverySummary, diff --git a/apps/backend/src/services/admin/admin-webhook-read-helpers.js b/apps/backend/src/services/admin/admin-webhook-read-helpers.js new file mode 100644 index 00000000..72c21f25 --- /dev/null +++ b/apps/backend/src/services/admin/admin-webhook-read-helpers.js @@ -0,0 +1,185 @@ +// @ts-check + +import { listTasksByOrderId } from '../../repositories/task-repo.js' +import { formatFenToAmount, parseAmountToFen } from '../../utils/money.js' +import { extractAgisoTradePayload, resolveAgisoTradePlatformOrderId } from '../order/agiso-trade-parsing.js' +import { safeParseJson } from './admin-query-utils.js' +import { resolveDisplayShopName } from './admin-read-shared-helpers.js' + +/** @typedef {import('../../types/admin-read-models.js').AdminWebhookEventListItem} AdminWebhookEventListItem */ +/** @typedef {import('../../types/repository-rows.js').WebhookEventRow} WebhookEventRow */ + +/** + * @param {WebhookEventRow} item + * @returns {Promise} + */ +export async function mapAdminWebhookEvent(item, { includeRaw = false } = {}) { + const headers = normalizeRecord(safeParseJson(item.headers_json)) + const query = normalizeRecord(safeParseJson(item.query_json)) + const body = normalizeRecord(safeParseJson(item.body_json)) + const payload = extractWebhookPayload(body) + const itemSources = extractWebhookItemSources(payload) + const rawAmount = pickFirstNonEmpty([ + payload.total_fee, + payload.totalFee, + payload.TotalFee, + payload.pay_fee, + payload.payFee, + payload.Payment, + payload.payment, + payload.buy_amount, + payload.buyAmount, + ]) + const totalAmountFen = parseAmountToFen(rawAmount) + const taskCount = item.related_order_id ? (await listTasksByOrderId(item.related_order_id)).length : 0 + const shopId = item.shop_id || pickFirstNonEmpty([ + payload.PlatformUserId, + payload.platformUserId, + payload.platform_user_id, + payload.seller_id, + payload.sellerId, + payload.SellerId, + payload.seller_nick, + payload.sellerNick, + payload.SellerNick, + ]) + + const mapped = { + eventId: item.id, + provider: item.provider || 'agiso', + platform: item.platform, + platformRaw: pickFirstNonEmpty([ + query.fromPlatform, + query.from_platform, + body.fromPlatform, + body.from_platform, + payload.fromPlatform, + payload.FromPlatform, + payload.platform, + payload.Platform, + ]), + shopId, + shopName: resolveDisplayShopName( + item.provider, + shopId, + item.shop_name || pickFirstNonEmpty([ + payload.shopName, + payload.shop_name, + payload.ShopName, + payload.seller_nick, + payload.sellerNick, + payload.SellerNick, + ]), + ), + eventType: item.event_type, + eventKey: item.event_key, + signatureValid: Boolean(item.signature_valid), + processed: Boolean(item.processed), + processError: item.process_error, + relatedOrderId: item.related_order_id, + createdAt: item.created_at, + platformOrderId: resolveAgisoTradePlatformOrderId(payload), + buyerId: pickFirstNonEmpty([ + payload.buyer_id, + payload.buyerId, + payload.BuyerId, + payload.openid, + payload.BuyerOpenUid, + payload.buyer_open_uid, + payload.buyerOpenUid, + ]), + buyerName: pickFirstNonEmpty([ + payload.buyer_name, + payload.buyerName, + payload.BuyerName, + payload.nick, + payload.BuyerNick, + payload.buyer_nick, + payload.buyerNick, + ]), + totalAmount: rawAmount ? formatFenToAmount(totalAmountFen) : '', + totalAmountFen, + currency: pickFirstNonEmpty([payload.currency, payload.Currency, 'CNY']) || 'CNY', + itemCount: itemSources.length, + taskCount, + messageText: pickFirstNonEmpty([ + payload.MsgTypeDes, + payload.msgTypeDes, + payload.msg_type_desc, + payload.message, + payload.msg, + ]), + requestTimestamp: pickFirstNonEmpty([query.timestamp]), + requestSign: pickFirstNonEmpty([query.sign]), + aopic: pickFirstNonEmpty([query.aopic]), + sourceHost: pickFirstNonEmpty([headers['x-forwarded-host'], headers.host]), + sourceIp: pickFirstNonEmpty([headers['x-forwarded-for'], headers['x-real-ip']]), + userAgent: pickFirstNonEmpty([headers['user-agent'], headers['User-Agent']]), + } + + if (!includeRaw) { + return mapped + } + + return { + ...mapped, + headers, + query, + body, + payload, + } +} + +function extractWebhookPayload(body) { + return extractAgisoTradePayload(body) +} + +function extractWebhookItemSources(payload) { + const normalizedPayload = normalizeRecord(payload) + const candidates = [ + normalizedPayload.orders, + normalizedPayload.Orders, + normalizedPayload.items, + normalizedPayload.Items, + normalizedPayload.order_items, + ] + + for (const candidate of candidates) { + if (Array.isArray(candidate) && candidate.length > 0) { + return candidate.map((item) => normalizeRecord(item)).filter((item) => Object.keys(item).length > 0) + } + } + + if ( + pickFirstNonEmpty([ + normalizedPayload.item_id, + normalizedPayload.itemId, + normalizedPayload.ItemId, + normalizedPayload.num_iid, + normalizedPayload.numIid, + normalizedPayload.NumIid, + ]) + ) { + return [normalizedPayload] + } + + return [] +} + +function normalizeRecord(value) { + return value && typeof value === 'object' && !Array.isArray(value) ? value : {} +} + +function pickFirstNonEmpty(values) { + for (const value of values) { + if (typeof value === 'string' && value.trim()) { + return value.trim() + } + + if (typeof value === 'number' && Number.isFinite(value)) { + return String(value) + } + } + + return '' +} diff --git a/apps/backend/src/services/admin/admin-write-service.js b/apps/backend/src/services/admin/admin-write-service.js index 619a8358..ec7ddc37 100644 --- a/apps/backend/src/services/admin/admin-write-service.js +++ b/apps/backend/src/services/admin/admin-write-service.js @@ -37,7 +37,7 @@ import { import { getRequiredInventoryItem, mapAdminInventoryListItem, -} from './admin-read-helpers.js' +} from './admin-inventory-read-helpers.js' import { getRequiredTask, mapTaskActionPayload, diff --git a/docs/backend-typescript-migration-plan.md b/docs/backend-typescript-migration-plan.md index 20677e3e..09480265 100644 --- a/docs/backend-typescript-migration-plan.md +++ b/docs/backend-typescript-migration-plan.md @@ -184,6 +184,10 @@ 58. `admin-read-service / admin-write-service / admin-service` 已将 task 相关 helper 改为直接依赖独立 task helper 模块 59. `admin-read-helpers.js` 已进一步收敛到 webhook / inventory / order-list 三类能力 60. `npm --prefix apps/backend run typecheck` 已在 task helper 拆分后继续通过 +61. `admin-read-helpers.js` 已完成最终拆分,并删除过渡文件 +62. 新增 `src/services/admin/admin-inventory-read-helpers.js`,库存读取映射与必需实体校验已独立 +63. 新增 `src/services/admin/admin-order-read-helpers.js` 与 `src/services/admin/admin-webhook-read-helpers.js`,订单列表汇总与 webhook 事件映射已按领域独立 +64. `admin-read-service / admin-write-service / admin-service` 已全部改为直接依赖 inventory / order / webhook / task / shared 五类 helper,读侧 helper 拆分阶段可以收口 这一步的设计原则是“只加静态约束,不动运行链路”,所以不会影响: @@ -195,11 +199,10 @@ 第一批继续推进时,建议按这个顺序: -1. 为 `admin` 路由层补请求体 / 查询参数的共享输入类型 -2. 继续把 `admin-read-helpers.js` 细拆成按领域 helper 模块,下一步优先拆 `webhook` 或 `order-list` helper -3. 为 `admin` 路由层继续补响应模型与审计 payload 的共享类型,减少 `unknown` 到处就地断言 -4. 继续补 repository / service 的边界约束,但不急着改 `.ts` 扩展名 -5. 为 webhook、库存换码、自动发货补测试 +1. 为 `admin` 路由层继续补响应模型与审计 payload 的共享类型,减少 `unknown` 到处就地断言 +2. 继续补 repository / service 的边界约束,但不急着改 `.ts` 扩展名 +3. 恢复并固定 `npm --prefix apps/backend run typecheck` 为日常校验步骤,确保本地与 CI 环境都能直接执行 +4. 为 webhook、库存换码、自动发货补测试 ## 执行原则