diff --git a/apps/backend/src/routes/admin/cost-center-catalog.ts b/apps/backend/src/routes/admin/cost-center-catalog.ts new file mode 100644 index 00000000..3890e059 --- /dev/null +++ b/apps/backend/src/routes/admin/cost-center-catalog.ts @@ -0,0 +1,352 @@ +/** + * 成本中心 · 商品与映射路由。 + * + * 覆盖:接单模板初始化、自动发货/接单平台商品同步与回填、人工商品归一、 + * 标准商品 CRUD、渠道映射 CRUD、未归一队列、归一数量回填。 + */ +import { Router } from 'express' + +import { normalizeManualProducts } from '../../services/cost-center/manual-product-normalization-service.js' +import { + backfillAdminNormalizedQuantities, + backfillAdminProductMappings, + createAdminChannelMapping, + createAdminProduct, + deleteAdminChannelMapping, + deleteAdminProduct, + getAdminChannelMappingDiagnostics, + listAdminChannelMappings, + listAdminProducts, + listAdminUnmappedMappingGroups, + updateAdminChannelMapping, + updateAdminProduct, +} from '../../services/cost-center/product-catalog-service.js' +import { + backfillAdminAffiliateDashSkus, + syncAdminAffiliateDashProducts, +} from '../../services/cost-center/affiliate-product-service.js' +import { + applyAdminTemplateProductBootstrap, + previewAdminTemplateProductBootstrap, +} from '../../services/cost-center/template-bootstrap-service.js' +import { + backfillAdminWorkOrderProductSkus, + syncAdminWorkOrderTemplateCostVersions, +} from '../../services/cost-center/work-order-product-service.js' +import type { JsonObject } from '../../types/json.js' +import { bodyOf, idOf, sessionOf } from './request-helpers.js' +import { createJsonHandler, requireAdminPermission } from './session.js' + +const router = Router() + +router.get( + '/cost-center/template-bootstrap/preview', + requireAdminPermission('finance.view'), + createJsonHandler(() => previewAdminTemplateProductBootstrap(), { + successMessage: 'ok', + errorMessage: '读取接单模板初始化预览失败', + scope: '[admin/cost-center/template-bootstrap/preview]', + }), +) + +router.post( + '/cost-center/template-bootstrap/apply', + requireAdminPermission('finance.config'), + createJsonHandler((req) => applyAdminTemplateProductBootstrap(bodyOf(req), sessionOf(req)), { + successMessage: '接单模板初始化完成', + errorMessage: '接单模板初始化失败', + scope: '[admin/cost-center/template-bootstrap/apply]', + audit: (_req, data) => ({ + action: 'cost_template_bootstrap_applied', + targetType: 'product_catalog', + targetId: '', + data: data as Record, + }), + }), +) + +router.post( + '/cost-center/affiliate-products/sync', + requireAdminPermission('finance.config'), + createJsonHandler(() => syncAdminAffiliateDashProducts(), { + successMessage: '自动发货商品已同步', + errorMessage: '同步自动发货商品失败', + scope: '[admin/cost-center/affiliate-products/sync]', + audit: (_req, data) => ({ + action: 'cost_affiliate_products_synced', + targetType: 'product_catalog', + targetId: '', + data: data as Record, + }), + }), +) + +router.post( + '/cost-center/affiliate-products/backfill', + requireAdminPermission('finance.config'), + createJsonHandler(() => backfillAdminAffiliateDashSkus(), { + successMessage: '自动发货 SKU 已回填', + errorMessage: '回填自动发货 SKU 失败', + scope: '[admin/cost-center/affiliate-products/backfill]', + audit: (_req, data) => ({ + action: 'cost_affiliate_skus_backfilled', + targetType: 'order_items', + targetId: '', + data: data as Record, + }), + }), +) + +router.post( + '/cost-center/work-order-products/backfill', + requireAdminPermission('finance.config'), + createJsonHandler(() => backfillAdminWorkOrderProductSkus(), { + successMessage: '接单平台商品已回填', + errorMessage: '回填接单平台商品失败', + scope: '[admin/cost-center/work-order-products/backfill]', + audit: (_req, data) => ({ + action: 'cost_work_order_products_backfilled', + targetType: 'order_items', + targetId: '', + data: data as Record, + }), + }), +) + +router.post( + '/cost-center/work-order-template-costs/sync', + requireAdminPermission('finance.config'), + createJsonHandler(() => syncAdminWorkOrderTemplateCostVersions(), { + successMessage: '接单模板发单金额已同步', + errorMessage: '同步接单模板发单金额失败', + scope: '[admin/cost-center/work-order-template-costs/sync]', + audit: (_req, data) => ({ + action: 'cost_work_order_template_costs_synced', + targetType: 'cost_versions', + targetId: '', + data: data as Record, + }), + }), +) + +router.post( + '/cost-center/manual-products/normalize', + requireAdminPermission('finance.config'), + createJsonHandler( + (req) => + normalizeManualProducts({ + dateFrom: bodyOf(req).dateFrom as string | undefined, + dateTo: bodyOf(req).dateTo as string | undefined, + dryRun: bodyOf(req).dryRun === true, + }), + { + successMessage: '人工商品归一完成', + errorMessage: '归一人工商品失败', + scope: '[admin/cost-center/manual-products/normalize]', + audit: (_req, data) => ({ + action: 'cost_manual_products_normalized', + targetType: 'product_catalog', + targetId: '', + data: data as Record, + }), + }, + ), +) + +router.get( + '/cost-center/products', + requireAdminPermission('finance.view'), + createJsonHandler((req) => listAdminProducts(req.query as JsonObject), { + successMessage: 'ok', + errorMessage: '读取标准商品失败', + scope: '[admin/cost-center/products]', + }), +) + +router.post( + '/cost-center/products', + requireAdminPermission('finance.config'), + createJsonHandler((req) => createAdminProduct(bodyOf(req), sessionOf(req)), { + successMessage: '标准商品已创建', + errorMessage: '创建标准商品失败', + scope: '[admin/cost-center/products]', + audit: (_req, data) => { + const product = (data as { product?: { id?: number; canonicalSku?: string } })?.product + return { + action: 'cost_product_created', + targetType: 'product_catalog', + targetId: String(product?.id || ''), + data: { canonicalSku: product?.canonicalSku || '' }, + } + }, + }), +) + +router.patch( + '/cost-center/products/:productId', + requireAdminPermission('finance.config'), + createJsonHandler((req) => updateAdminProduct(idOf(req, 'productId'), bodyOf(req)), { + successMessage: '标准商品已更新', + errorMessage: '更新标准商品失败', + scope: '[admin/cost-center/products/:productId]', + audit: (req, data) => { + const product = (data as { product?: { id?: number; canonicalSku?: string } })?.product + return { + action: 'cost_product_updated', + targetType: 'product_catalog', + targetId: String(product?.id || req.params.productId || ''), + data: { canonicalSku: product?.canonicalSku || '' }, + } + }, + }), +) + +router.delete( + '/cost-center/products/:productId', + requireAdminPermission('finance.config'), + createJsonHandler((req) => deleteAdminProduct(idOf(req, 'productId')), { + successMessage: '标准商品已删除', + errorMessage: '删除标准商品失败', + scope: '[admin/cost-center/products/:productId]', + audit: (req, data) => { + const product = (data as { product?: { id?: number; canonicalSku?: string } })?.product + return { + action: 'cost_product_deleted', + targetType: 'product_catalog', + targetId: String(product?.id || req.params.productId || ''), + data: { canonicalSku: product?.canonicalSku || '' }, + } + }, + }), +) + +router.get( + '/cost-center/mappings', + requireAdminPermission('finance.view'), + createJsonHandler((req) => listAdminChannelMappings(req.query as JsonObject), { + successMessage: 'ok', + errorMessage: '读取渠道映射失败', + scope: '[admin/cost-center/mappings]', + }), +) + +router.get( + '/cost-center/mappings/diagnostics', + requireAdminPermission('finance.view'), + createJsonHandler((req) => getAdminChannelMappingDiagnostics(req.query as JsonObject), { + successMessage: 'ok', + errorMessage: '读取渠道映射诊断失败', + scope: '[admin/cost-center/mappings/diagnostics]', + }), +) + +router.get( + '/cost-center/mappings/unmapped', + requireAdminPermission('finance.view'), + createJsonHandler((req) => listAdminUnmappedMappingGroups(req.query as JsonObject), { + successMessage: 'ok', + errorMessage: '读取未归一队列失败', + scope: '[admin/cost-center/mappings/unmapped]', + }), +) + +router.post( + '/cost-center/mappings', + requireAdminPermission('finance.config'), + createJsonHandler((req) => createAdminChannelMapping(bodyOf(req), sessionOf(req)), { + successMessage: '渠道映射已创建', + errorMessage: '创建渠道映射失败', + scope: '[admin/cost-center/mappings]', + audit: (_req, data) => { + const mapping = ( + data as { mapping?: { id?: number; provider?: string; platformSkuCode?: string } } + )?.mapping + return { + action: 'cost_mapping_created', + targetType: 'channel_product_mapping', + targetId: String(mapping?.id || ''), + data: { + provider: mapping?.provider || '', + platformSkuCode: mapping?.platformSkuCode || '', + }, + } + }, + }), +) + +router.patch( + '/cost-center/mappings/:mappingId', + requireAdminPermission('finance.config'), + createJsonHandler((req) => updateAdminChannelMapping(idOf(req, 'mappingId'), bodyOf(req)), { + successMessage: '渠道映射已更新', + errorMessage: '更新渠道映射失败', + scope: '[admin/cost-center/mappings/:mappingId]', + audit: (req, data) => { + const mapping = ( + data as { mapping?: { id?: number; provider?: string; platformSkuCode?: string } } + )?.mapping + return { + action: 'cost_mapping_updated', + targetType: 'channel_product_mapping', + targetId: String(mapping?.id || req.params.mappingId || ''), + data: { + provider: mapping?.provider || '', + platformSkuCode: mapping?.platformSkuCode || '', + }, + } + }, + }), +) + +router.delete( + '/cost-center/mappings/:mappingId', + requireAdminPermission('finance.config'), + createJsonHandler((req) => deleteAdminChannelMapping(idOf(req, 'mappingId')), { + successMessage: '渠道映射已删除', + errorMessage: '删除渠道映射失败', + scope: '[admin/cost-center/mappings/:mappingId]', + audit: (req, data) => { + const mappingId = (data as { mappingId?: number })?.mappingId + return { + action: 'cost_mapping_deleted', + targetType: 'channel_product_mapping', + targetId: String(mappingId || req.params.mappingId || ''), + data: {}, + } + }, + }), +) + +router.post( + '/cost-center/mappings/backfill', + requireAdminPermission('finance.config'), + createJsonHandler((req) => backfillAdminProductMappings(bodyOf(req)), { + successMessage: '商品映射回填完成', + errorMessage: '回填商品映射失败', + scope: '[admin/cost-center/mappings/backfill]', + audit: (_req, data) => ({ + action: 'cost_mapping_backfilled', + targetType: 'order_item', + targetId: '', + data: data as Record, + }), + }), +) + +router.post( + '/cost-center/normalized-quantities/backfill', + requireAdminPermission('finance.config'), + createJsonHandler((req) => backfillAdminNormalizedQuantities(bodyOf(req)), { + successMessage: '归一数量回填完成', + errorMessage: '回填归一数量失败', + scope: '[admin/cost-center/normalized-quantities/backfill]', + audit: (_req, data) => ({ + action: 'order_item_quantity_backfilled', + targetType: 'order_item', + targetId: '', + data: data as Record, + }), + }), +) + +export default router diff --git a/apps/backend/src/routes/admin/cost-center-cost.ts b/apps/backend/src/routes/admin/cost-center-cost.ts new file mode 100644 index 00000000..24e50604 --- /dev/null +++ b/apps/backend/src/routes/admin/cost-center-cost.ts @@ -0,0 +1,293 @@ +/** + * 成本中心 · 成本项/版本/快照/异常路由。 + * + * 改价只走 cost_versions;订单只保存快照;匹配不到成本进异常队列。 + */ +import { Router } from 'express' +import { + createAdminCostItem, + createAdminCostVersion, + deleteAdminCostItem, + deleteAdminCostVersion, + listAdminCostExceptions, + listAdminCostItems, + listAdminCostVersions, + resolveAdminCostException, + retireAdminCostVersion, + summarizeAdminCostExceptions, + updateAdminCostItem, +} from '../../services/cost-center/cost-definition-service.js' +import { + createAdminCostSnapshot, + previewAdminCost, +} from '../../services/cost-center/cost-resolution-service.js' +import { + syncActualWorkOrderCostSnapshots, + syncEstimatedOrderItemCostSnapshots, +} from '../../services/cost-center/cost-snapshot-sync-service.js' +import type { JsonObject } from '../../types/json.js' +import { bodyOf, idOf, sessionOf } from './request-helpers.js' +import { createJsonHandler, requireAdminPermission } from './session.js' + +const router = Router() + +router.get( + '/cost-center/cost-items', + requireAdminPermission('finance.view'), + createJsonHandler((req) => listAdminCostItems(req.query as JsonObject), { + successMessage: 'ok', + errorMessage: '读取成本项目失败', + scope: '[admin/cost-center/cost-items]', + }), +) + +router.post( + '/cost-center/cost-items', + requireAdminPermission('finance.config'), + createJsonHandler((req) => createAdminCostItem(bodyOf(req), sessionOf(req)), { + successMessage: '成本项目已创建', + errorMessage: '创建成本项目失败', + scope: '[admin/cost-center/cost-items]', + audit: (_req, data) => { + const item = (data as { costItem?: { id?: number; code?: string } })?.costItem + return { + action: 'cost_item_created', + targetType: 'cost_item', + targetId: String(item?.id || ''), + data: { code: item?.code || '' }, + } + }, + }), +) + +router.patch( + '/cost-center/cost-items/:costItemId', + requireAdminPermission('finance.config'), + createJsonHandler((req) => updateAdminCostItem(idOf(req, 'costItemId'), bodyOf(req)), { + successMessage: '成本项目已更新', + errorMessage: '更新成本项目失败', + scope: '[admin/cost-center/cost-items/:costItemId]', + audit: (req, data) => { + const item = (data as { costItem?: { id?: number; code?: string } })?.costItem + return { + action: 'cost_item_updated', + targetType: 'cost_item', + targetId: String(item?.id || req.params.costItemId || ''), + data: { code: item?.code || '' }, + } + }, + }), +) + +router.delete( + '/cost-center/cost-items/:costItemId', + requireAdminPermission('finance.config'), + createJsonHandler((req) => deleteAdminCostItem(idOf(req, 'costItemId')), { + successMessage: '成本项目已删除', + errorMessage: '删除成本项目失败', + scope: '[admin/cost-center/cost-items/:costItemId]', + audit: (req, data) => { + const item = (data as { costItem?: { id?: number; code?: string } })?.costItem + return { + action: 'cost_item_deleted', + targetType: 'cost_item', + targetId: String(item?.id || req.params.costItemId || ''), + data: { code: item?.code || '' }, + } + }, + }), +) + +router.get( + '/cost-center/cost-versions', + requireAdminPermission('finance.view'), + createJsonHandler((req) => listAdminCostVersions(req.query as JsonObject), { + successMessage: 'ok', + errorMessage: '读取成本版本失败', + scope: '[admin/cost-center/cost-versions]', + }), +) + +router.post( + '/cost-center/cost-versions', + requireAdminPermission('finance.config'), + createJsonHandler((req) => createAdminCostVersion(bodyOf(req), sessionOf(req)), { + successMessage: '成本版本已创建', + errorMessage: '创建成本版本失败', + scope: '[admin/cost-center/cost-versions]', + audit: (_req, data) => { + const version = (data as { version?: { id?: number; costItemId?: number } })?.version + return { + action: 'cost_version_created', + targetType: 'cost_version', + targetId: String(version?.id || ''), + data: { costItemId: version?.costItemId || 0 }, + } + }, + }), +) + +router.patch( + '/cost-center/cost-versions/:versionId/retire', + requireAdminPermission('finance.config'), + createJsonHandler((req) => retireAdminCostVersion(idOf(req, 'versionId'), bodyOf(req)), { + successMessage: '成本版本已停用', + errorMessage: '停用成本版本失败', + scope: '[admin/cost-center/cost-versions/:versionId/retire]', + audit: (req, data) => { + const version = (data as { version?: { id?: number } })?.version + return { + action: 'cost_version_retired', + targetType: 'cost_version', + targetId: String(version?.id || req.params.versionId || ''), + data: {}, + } + }, + }), +) + +router.delete( + '/cost-center/cost-versions/:versionId', + requireAdminPermission('finance.config'), + createJsonHandler((req) => deleteAdminCostVersion(idOf(req, 'versionId')), { + successMessage: '成本版本已删除', + errorMessage: '删除成本版本失败', + scope: '[admin/cost-center/cost-versions/:versionId]', + audit: (req, data) => { + const version = (data as { version?: { id?: number } })?.version + return { + action: 'cost_version_deleted', + targetType: 'cost_version', + targetId: String(version?.id || req.params.versionId || ''), + data: {}, + } + }, + }), +) + +router.get( + '/cost-center/exceptions', + requireAdminPermission('finance.view'), + createJsonHandler((req) => listAdminCostExceptions(req.query as JsonObject), { + successMessage: 'ok', + errorMessage: '读取成本异常失败', + scope: '[admin/cost-center/exceptions]', + }), +) + +// 必须注册在 /cost-center/exceptions/:exceptionId 之前,避免 summary 被当成 ID。 +router.get( + '/cost-center/exceptions/summary', + requireAdminPermission('finance.view'), + createJsonHandler((req) => summarizeAdminCostExceptions(req.query as JsonObject), { + successMessage: 'ok', + errorMessage: '读取成本异常汇总失败', + scope: '[admin/cost-center/exceptions/summary]', + }), +) + +router.patch( + '/cost-center/exceptions/:exceptionId', + requireAdminPermission('finance.config'), + createJsonHandler( + (req) => resolveAdminCostException(idOf(req, 'exceptionId'), bodyOf(req), sessionOf(req)), + { + successMessage: '成本异常已处理', + errorMessage: '处理成本异常失败', + scope: '[admin/cost-center/exceptions/:exceptionId]', + audit: (req, data) => { + const exception = (data as { exception?: { id?: number; status?: string } })?.exception + return { + action: 'cost_exception_resolved', + targetType: 'cost_exception', + targetId: String(exception?.id || req.params.exceptionId || ''), + data: { status: exception?.status || '' }, + } + }, + }, + ), +) + +router.post( + '/cost-center/preview', + requireAdminPermission('finance.view'), + createJsonHandler((req) => previewAdminCost(bodyOf(req)), { + successMessage: 'ok', + errorMessage: '成本预览失败', + scope: '[admin/cost-center/preview]', + }), +) + +router.post( + '/cost-center/snapshots', + requireAdminPermission('finance.config'), + createJsonHandler((req) => createAdminCostSnapshot(bodyOf(req), sessionOf(req)), { + successMessage: '成本快照已生成', + errorMessage: '生成成本快照失败', + scope: '[admin/cost-center/snapshots]', + audit: (req, data) => { + const result = data as { + createdCount?: number + exceptionCreatedCount?: number + preview?: { context?: { orderItemId?: number } } + } + return { + action: 'cost_snapshot_created', + targetType: 'order_item', + targetId: String(result.preview?.context?.orderItemId || req.body?.orderItemId || ''), + data: { + createdCount: result.createdCount || 0, + exceptionCreatedCount: result.exceptionCreatedCount || 0, + }, + } + }, + }), +) + +router.post( + '/cost-center/snapshots/sync', + requireAdminPermission('finance.config'), + createJsonHandler( + async (req) => { + const body = bodyOf(req) + const mode = String(body.mode || 'both').trim() + const estimated = mode === 'actual' ? null : await syncEstimatedOrderItemCostSnapshots(body) + const actual = mode === 'estimated' ? null : await syncActualWorkOrderCostSnapshots(body) + return { mode, estimated, actual } + }, + { + successMessage: '成本快照回填完成', + errorMessage: '回填成本快照失败', + scope: '[admin/cost-center/snapshots/sync]', + audit: (req, data) => { + const result = data as { + mode?: string + estimated?: { createdCount?: number; exceptionCreatedCount?: number } | null + actual?: { + createdCount?: number + directRewardCount?: number + upgradedCount?: number + estimatedRewardCount?: number + } | null + } + return { + action: 'cost_snapshot_synced', + targetType: 'cost_snapshot', + targetId: '', + data: { + mode: result.mode || '', + estimatedCreated: result.estimated?.createdCount || 0, + actualCreated: + (result.actual?.directRewardCount || 0) + (result.actual?.upgradedCount || 0), + estimatedRewardCreated: result.actual?.estimatedRewardCount || 0, + exceptions: result.estimated?.exceptionCreatedCount || 0, + dateFrom: req.body?.dateFrom || '', + dateTo: req.body?.dateTo || '', + }, + } + }, + }, + ), +) + +export default router diff --git a/apps/backend/src/routes/admin/cost-center-credits.ts b/apps/backend/src/routes/admin/cost-center-credits.ts new file mode 100644 index 00000000..8ed6d6d4 --- /dev/null +++ b/apps/backend/src/routes/admin/cost-center-credits.ts @@ -0,0 +1,110 @@ +/** + * 成本中心 · 客服平台积分钱包路由(南航 / 乐玩 / ROCK)。 + * + * 积分只能通过充值/调整流水变更,账户余额由流水推导。 + */ +import { Router } from 'express' +import { + adjustPlatformCredit, + grantPlatformCredit, + listPlatformCreditAccounts, + listPlatformCreditEntries, + listPlatformCreditOwners, +} from '../../services/cost-center/platform-credit-service.js' +import { bodyOf, sessionOf } from './request-helpers.js' +import { createJsonHandler, requireAdminPermission, requireAnyAdminPermission } from './session.js' + +const router = Router() + +// --------------------------------------------------------------------------- +// 客服平台积分钱包(南航 / 乐玩 / ROCK) +// --------------------------------------------------------------------------- + +router.get( + '/cost-center/platform-credits/accounts', + requireAnyAdminPermission(['fund.view', 'finance.view']), + createJsonHandler((req) => listPlatformCreditAccounts(sessionOf(req)), { + successMessage: 'ok', + errorMessage: '读取积分钱包失败', + scope: '[admin/cost-center/platform-credits/accounts]', + }), +) + +router.get( + '/cost-center/platform-credits/entries', + requireAnyAdminPermission(['fund.view', 'finance.view']), + createJsonHandler( + (req) => listPlatformCreditEntries(req.query as Record, sessionOf(req)), + { + successMessage: 'ok', + errorMessage: '读取积分流水失败', + scope: '[admin/cost-center/platform-credits/entries]', + }, + ), +) + +router.get( + '/cost-center/platform-credits/owners', + requireAdminPermission('finance.config'), + createJsonHandler(() => listPlatformCreditOwners(), { + successMessage: 'ok', + errorMessage: '读取客服列表失败', + scope: '[admin/cost-center/platform-credits/owners]', + }), +) + +router.post( + '/cost-center/platform-credits/grant', + requireAdminPermission('finance.config'), + createJsonHandler((req) => grantPlatformCredit(bodyOf(req), sessionOf(req)), { + successMessage: '积分已充值', + errorMessage: '积分充值失败', + scope: '[admin/cost-center/platform-credits/grant]', + audit: (req, data) => { + const result = data as { + platformKey?: string + quantity?: number + amountFen?: number + } + return { + action: 'platform_credit_granted', + targetType: 'platform_credit_account', + targetId: String(req.body?.ownerAdminUserId || ''), + data: { + platformKey: result.platformKey || '', + quantity: result.quantity || 0, + amountFen: result.amountFen || 0, + }, + } + }, + }), +) + +router.post( + '/cost-center/platform-credits/adjust', + requireAdminPermission('finance.config'), + createJsonHandler((req) => adjustPlatformCredit(bodyOf(req), sessionOf(req)), { + successMessage: '积分已调整', + errorMessage: '积分调整失败', + scope: '[admin/cost-center/platform-credits/adjust]', + audit: (req, data) => { + const result = data as { + platformKey?: string + quantity?: number + amountFen?: number + } + return { + action: 'platform_credit_adjusted', + targetType: 'platform_credit_account', + targetId: String(req.body?.ownerAdminUserId || ''), + data: { + platformKey: result.platformKey || '', + quantity: result.quantity || 0, + amountFen: result.amountFen || 0, + }, + } + }, + }), +) + +export default router diff --git a/apps/backend/src/routes/admin/cost-center-manual.ts b/apps/backend/src/routes/admin/cost-center-manual.ts new file mode 100644 index 00000000..4ab72f9a --- /dev/null +++ b/apps/backend/src/routes/admin/cost-center-manual.ts @@ -0,0 +1,160 @@ +/** + * 成本中心 · 人工履约 SKU 配置路由。 + * + * 链路:扫描内部 SKU → 配置履约渠道与消耗积分 → 重新匹配历史订单项。 + */ +import { Router } from 'express' +import { + deleteManualFulfillmentConfig, + getManualFulfillmentConfig, + getManualSkuOverview, + listManualPlatformOptions, + listManualSkus, + rematchManualFulfillments, + saveManualFulfillmentConfig, + scanManualSkus, +} from '../../services/cost-center/manual-fulfillment-config-service.js' +import { bodyOf } from './request-helpers.js' +import { createJsonHandler, requireAdminPermission } from './session.js' + +const router = Router() + +// --------------------------------------------------------------------------- +// 人工履约 SKU 配置:扫描 → 配置 → 重新匹配 +// --------------------------------------------------------------------------- + +router.get( + '/cost-center/manual-skus', + requireAdminPermission('finance.view'), + createJsonHandler((req) => listManualSkus(req.query as Record), { + successMessage: 'ok', + errorMessage: '读取人工 SKU 清单失败', + scope: '[admin/cost-center/manual-skus]', + }), +) + +router.get( + '/cost-center/manual-skus/overview', + requireAdminPermission('finance.view'), + createJsonHandler(() => getManualSkuOverview(), { + successMessage: 'ok', + errorMessage: '读取人工 SKU 概览失败', + scope: '[admin/cost-center/manual-skus/overview]', + }), +) + +router.get( + '/cost-center/manual-platforms', + requireAdminPermission('finance.view'), + createJsonHandler(() => listManualPlatformOptions(), { + successMessage: 'ok', + errorMessage: '读取人工履约渠道失败', + scope: '[admin/cost-center/manual-platforms]', + }), +) + +router.get( + '/cost-center/manual-configs/:skuKey', + requireAdminPermission('finance.view'), + createJsonHandler((req) => getManualFulfillmentConfig(String(req.params.skuKey || '')), { + successMessage: 'ok', + errorMessage: '读取人工履约配置失败', + scope: '[admin/cost-center/manual-configs/:skuKey]', + }), +) + +router.post( + '/cost-center/manual-skus/scan', + requireAdminPermission('finance.config'), + createJsonHandler((req) => scanManualSkus(bodyOf(req) as never), { + successMessage: '扫描完成', + errorMessage: '扫描人工 SKU 失败', + scope: '[admin/cost-center/manual-skus/scan]', + audit: (req, data) => { + const result = data as { scanned?: number; fromDate?: string; toDate?: string } + return { + action: 'manual_skus_scanned', + targetType: 'manual_sku_registry', + targetId: '', + data: { + scanned: result.scanned || 0, + fromDate: result.fromDate || '', + toDate: result.toDate || '', + }, + } + }, + }), +) + +router.post( + '/cost-center/manual-configs', + requireAdminPermission('finance.config'), + createJsonHandler((req) => saveManualFulfillmentConfig(bodyOf(req)), { + successMessage: '人工履约配置已保存', + errorMessage: '保存人工履约配置失败', + scope: '[admin/cost-center/manual-configs]', + audit: (req, data) => { + const config = data as { + id?: number + skuKey?: string + platformKey?: string + creditQuantity?: number + costFen?: number + } + return { + action: 'manual_fulfillment_config_saved', + targetType: 'manual_fulfillment_config', + targetId: String(config.id || ''), + data: { + skuKey: config.skuKey || '', + platformKey: config.platformKey || '', + creditQuantity: config.creditQuantity || 0, + costFen: config.costFen || 0, + }, + } + }, + }), +) + +router.delete( + '/cost-center/manual-configs/:skuKey', + requireAdminPermission('finance.config'), + createJsonHandler((req) => deleteManualFulfillmentConfig(String(req.params.skuKey || '')), { + successMessage: '人工履约配置已删除', + errorMessage: '删除人工履约配置失败', + scope: '[admin/cost-center/manual-configs/:skuKey]', + audit: (req, data) => { + const result = data as { skuKey?: string; deleted?: number } + return { + action: 'manual_fulfillment_config_deleted', + targetType: 'manual_fulfillment_config', + targetId: String(req.params.skuKey || ''), + data: { skuKey: result.skuKey || '', deleted: result.deleted || 0 }, + } + }, + }), +) + +router.post( + '/cost-center/manual-configs/rematch', + requireAdminPermission('finance.config'), + createJsonHandler((req) => rematchManualFulfillments(bodyOf(req)), { + successMessage: '重新匹配完成', + errorMessage: '重新匹配失败', + scope: '[admin/cost-center/manual-configs/rematch]', + audit: (req, data) => { + const result = data as { updatedCount?: number; skuKey?: string } + return { + action: 'manual_fulfillment_rematched', + targetType: 'order_items', + targetId: '', + data: { + updatedCount: result.updatedCount || 0, + skuKey: result.skuKey || '', + }, + } + }, + }), +) + +export default router diff --git a/apps/backend/src/routes/admin/cost-center.ts b/apps/backend/src/routes/admin/cost-center.ts index 04566036..287208ee 100644 --- a/apps/backend/src/routes/admin/cost-center.ts +++ b/apps/backend/src/routes/admin/cost-center.ts @@ -1,77 +1,26 @@ -import type { Request } from 'express' +/** + * 成本中心 API 入口。 + * + * 48 个端点按域拆到 4 个子路由,本文件只负责角色校验与挂载顺序: + * - cost-center-catalog 商品主数据、渠道映射、模板/自动发货/接单平台回填 + * - cost-center-cost 成本项、成本版本、成本快照、成本异常 + * - cost-center-manual 人工履约 SKU 配置(扫描 → 配置 → 重新匹配) + * - cost-center-credits 客服平台积分钱包 + * + * 子路由里的路径仍是完整路径(/cost-center/...),因此挂载时不需要再加前缀; + * 角色校验放在最前面,子路由只需要各自声明 finance.view / finance.config 权限。 + */ import { Router } from 'express' import { getAdminCostCenterOptions, getAdminCostCenterOverview, } from '../../services/cost-center/cost-center-overview-service.js' -import { - createAdminCostItem, - createAdminCostSnapshot, - createAdminCostVersion, - deleteAdminCostItem, - deleteAdminCostVersion, - listAdminCostExceptions, - listAdminCostItems, - listAdminCostVersions, - previewAdminCost, - resolveAdminCostException, - retireAdminCostVersion, - summarizeAdminCostExceptions, - updateAdminCostItem, -} from '../../services/cost-center/cost-definition-service.js' -import { - backfillAdminNormalizedQuantities, - backfillAdminProductMappings, - createAdminChannelMapping, - createAdminProduct, - deleteAdminChannelMapping, - deleteAdminProduct, - getAdminChannelMappingDiagnostics, - listAdminChannelMappings, - listAdminProducts, - listAdminUnmappedMappingGroups, - updateAdminChannelMapping, - updateAdminProduct, -} from '../../services/cost-center/product-catalog-service.js' -import { - adjustPlatformCredit, - grantPlatformCredit, - listPlatformCreditAccounts, - listPlatformCreditEntries, - listPlatformCreditOwners, -} from '../../services/cost-center/platform-credit-service.js' -import { - deleteManualFulfillmentConfig, - getManualFulfillmentConfig, - getManualSkuOverview, - listManualPlatformOptions, - listManualSkus, - rematchManualFulfillments, - saveManualFulfillmentConfig, - scanManualSkus, -} from '../../services/cost-center/manual-fulfillment-config-service.js' -import { - applyAdminTemplateProductBootstrap, - backfillAdminAffiliateDashSkus, - backfillAdminWorkOrderProductSkus, - previewAdminTemplateProductBootstrap, - syncAdminAffiliateDashProducts, - syncAdminWorkOrderTemplateCostVersions, -} from '../../services/cost-center/template-bootstrap-service.js' -import { normalizeManualProducts } from '../../services/cost-center/manual-product-normalization-service.js' -import { - syncActualWorkOrderCostSnapshots, - syncEstimatedOrderItemCostSnapshots, -} from '../../services/cost-center/cost-snapshot-sync-service.js' -import type { JsonObject } from '../../types/json.js' -import { - createJsonHandler, - requireAdminPermission, - requireAdminRoles, - requireAnyAdminPermission, -} from './session.js' -import { bodyOf, idOf, sessionOf } from './request-helpers.js' +import catalogRoutes from './cost-center-catalog.js' +import costRoutes from './cost-center-cost.js' +import creditRoutes from './cost-center-credits.js' +import manualRoutes from './cost-center-manual.js' +import { createJsonHandler, requireAdminPermission, requireAdminRoles } from './session.js' const router = Router() @@ -97,802 +46,9 @@ router.get( }), ) -router.get( - '/cost-center/template-bootstrap/preview', - requireAdminPermission('finance.view'), - createJsonHandler(() => previewAdminTemplateProductBootstrap(), { - successMessage: 'ok', - errorMessage: '读取接单模板初始化预览失败', - scope: '[admin/cost-center/template-bootstrap/preview]', - }), -) - -router.post( - '/cost-center/template-bootstrap/apply', - requireAdminPermission('finance.config'), - createJsonHandler((req) => applyAdminTemplateProductBootstrap(bodyOf(req), sessionOf(req)), { - successMessage: '接单模板初始化完成', - errorMessage: '接单模板初始化失败', - scope: '[admin/cost-center/template-bootstrap/apply]', - audit: (_req, data) => ({ - action: 'cost_template_bootstrap_applied', - targetType: 'product_catalog', - targetId: '', - data: data as Record, - }), - }), -) - -router.post( - '/cost-center/affiliate-products/sync', - requireAdminPermission('finance.config'), - createJsonHandler(() => syncAdminAffiliateDashProducts(), { - successMessage: '自动发货商品已同步', - errorMessage: '同步自动发货商品失败', - scope: '[admin/cost-center/affiliate-products/sync]', - audit: (_req, data) => ({ - action: 'cost_affiliate_products_synced', - targetType: 'product_catalog', - targetId: '', - data: data as Record, - }), - }), -) - -router.post( - '/cost-center/affiliate-products/backfill', - requireAdminPermission('finance.config'), - createJsonHandler(() => backfillAdminAffiliateDashSkus(), { - successMessage: '自动发货 SKU 已回填', - errorMessage: '回填自动发货 SKU 失败', - scope: '[admin/cost-center/affiliate-products/backfill]', - audit: (_req, data) => ({ - action: 'cost_affiliate_skus_backfilled', - targetType: 'order_items', - targetId: '', - data: data as Record, - }), - }), -) - -router.post( - '/cost-center/work-order-products/backfill', - requireAdminPermission('finance.config'), - createJsonHandler(() => backfillAdminWorkOrderProductSkus(), { - successMessage: '接单平台商品已回填', - errorMessage: '回填接单平台商品失败', - scope: '[admin/cost-center/work-order-products/backfill]', - audit: (_req, data) => ({ - action: 'cost_work_order_products_backfilled', - targetType: 'order_items', - targetId: '', - data: data as Record, - }), - }), -) - -router.post( - '/cost-center/work-order-template-costs/sync', - requireAdminPermission('finance.config'), - createJsonHandler(() => syncAdminWorkOrderTemplateCostVersions(), { - successMessage: '接单模板发单金额已同步', - errorMessage: '同步接单模板发单金额失败', - scope: '[admin/cost-center/work-order-template-costs/sync]', - audit: (_req, data) => ({ - action: 'cost_work_order_template_costs_synced', - targetType: 'cost_versions', - targetId: '', - data: data as Record, - }), - }), -) - -router.post( - '/cost-center/manual-products/normalize', - requireAdminPermission('finance.config'), - createJsonHandler( - (req) => - normalizeManualProducts({ - dateFrom: bodyOf(req).dateFrom as string | undefined, - dateTo: bodyOf(req).dateTo as string | undefined, - dryRun: bodyOf(req).dryRun === true, - }), - { - successMessage: '人工商品归一完成', - errorMessage: '归一人工商品失败', - scope: '[admin/cost-center/manual-products/normalize]', - audit: (_req, data) => ({ - action: 'cost_manual_products_normalized', - targetType: 'product_catalog', - targetId: '', - data: data as Record, - }), - }, - ), -) - -router.get( - '/cost-center/products', - requireAdminPermission('finance.view'), - createJsonHandler((req) => listAdminProducts(req.query as JsonObject), { - successMessage: 'ok', - errorMessage: '读取标准商品失败', - scope: '[admin/cost-center/products]', - }), -) - -router.post( - '/cost-center/products', - requireAdminPermission('finance.config'), - createJsonHandler((req) => createAdminProduct(bodyOf(req), sessionOf(req)), { - successMessage: '标准商品已创建', - errorMessage: '创建标准商品失败', - scope: '[admin/cost-center/products]', - audit: (_req, data) => { - const product = (data as { product?: { id?: number; canonicalSku?: string } })?.product - return { - action: 'cost_product_created', - targetType: 'product_catalog', - targetId: String(product?.id || ''), - data: { canonicalSku: product?.canonicalSku || '' }, - } - }, - }), -) - -router.patch( - '/cost-center/products/:productId', - requireAdminPermission('finance.config'), - createJsonHandler((req) => updateAdminProduct(idOf(req, 'productId'), bodyOf(req)), { - successMessage: '标准商品已更新', - errorMessage: '更新标准商品失败', - scope: '[admin/cost-center/products/:productId]', - audit: (req, data) => { - const product = (data as { product?: { id?: number; canonicalSku?: string } })?.product - return { - action: 'cost_product_updated', - targetType: 'product_catalog', - targetId: String(product?.id || req.params.productId || ''), - data: { canonicalSku: product?.canonicalSku || '' }, - } - }, - }), -) - -router.delete( - '/cost-center/products/:productId', - requireAdminPermission('finance.config'), - createJsonHandler((req) => deleteAdminProduct(idOf(req, 'productId')), { - successMessage: '标准商品已删除', - errorMessage: '删除标准商品失败', - scope: '[admin/cost-center/products/:productId]', - audit: (req, data) => { - const product = (data as { product?: { id?: number; canonicalSku?: string } })?.product - return { - action: 'cost_product_deleted', - targetType: 'product_catalog', - targetId: String(product?.id || req.params.productId || ''), - data: { canonicalSku: product?.canonicalSku || '' }, - } - }, - }), -) - -router.get( - '/cost-center/mappings', - requireAdminPermission('finance.view'), - createJsonHandler((req) => listAdminChannelMappings(req.query as JsonObject), { - successMessage: 'ok', - errorMessage: '读取渠道映射失败', - scope: '[admin/cost-center/mappings]', - }), -) - -router.get( - '/cost-center/mappings/diagnostics', - requireAdminPermission('finance.view'), - createJsonHandler((req) => getAdminChannelMappingDiagnostics(req.query as JsonObject), { - successMessage: 'ok', - errorMessage: '读取渠道映射诊断失败', - scope: '[admin/cost-center/mappings/diagnostics]', - }), -) - -router.get( - '/cost-center/mappings/unmapped', - requireAdminPermission('finance.view'), - createJsonHandler((req) => listAdminUnmappedMappingGroups(req.query as JsonObject), { - successMessage: 'ok', - errorMessage: '读取未归一队列失败', - scope: '[admin/cost-center/mappings/unmapped]', - }), -) - -router.post( - '/cost-center/mappings', - requireAdminPermission('finance.config'), - createJsonHandler((req) => createAdminChannelMapping(bodyOf(req), sessionOf(req)), { - successMessage: '渠道映射已创建', - errorMessage: '创建渠道映射失败', - scope: '[admin/cost-center/mappings]', - audit: (_req, data) => { - const mapping = ( - data as { mapping?: { id?: number; provider?: string; platformSkuCode?: string } } - )?.mapping - return { - action: 'cost_mapping_created', - targetType: 'channel_product_mapping', - targetId: String(mapping?.id || ''), - data: { - provider: mapping?.provider || '', - platformSkuCode: mapping?.platformSkuCode || '', - }, - } - }, - }), -) - -router.patch( - '/cost-center/mappings/:mappingId', - requireAdminPermission('finance.config'), - createJsonHandler((req) => updateAdminChannelMapping(idOf(req, 'mappingId'), bodyOf(req)), { - successMessage: '渠道映射已更新', - errorMessage: '更新渠道映射失败', - scope: '[admin/cost-center/mappings/:mappingId]', - audit: (req, data) => { - const mapping = ( - data as { mapping?: { id?: number; provider?: string; platformSkuCode?: string } } - )?.mapping - return { - action: 'cost_mapping_updated', - targetType: 'channel_product_mapping', - targetId: String(mapping?.id || req.params.mappingId || ''), - data: { - provider: mapping?.provider || '', - platformSkuCode: mapping?.platformSkuCode || '', - }, - } - }, - }), -) - -router.delete( - '/cost-center/mappings/:mappingId', - requireAdminPermission('finance.config'), - createJsonHandler((req) => deleteAdminChannelMapping(idOf(req, 'mappingId')), { - successMessage: '渠道映射已删除', - errorMessage: '删除渠道映射失败', - scope: '[admin/cost-center/mappings/:mappingId]', - audit: (req, data) => { - const mappingId = (data as { mappingId?: number })?.mappingId - return { - action: 'cost_mapping_deleted', - targetType: 'channel_product_mapping', - targetId: String(mappingId || req.params.mappingId || ''), - data: {}, - } - }, - }), -) - -router.post( - '/cost-center/mappings/backfill', - requireAdminPermission('finance.config'), - createJsonHandler((req) => backfillAdminProductMappings(bodyOf(req)), { - successMessage: '商品映射回填完成', - errorMessage: '回填商品映射失败', - scope: '[admin/cost-center/mappings/backfill]', - audit: (_req, data) => ({ - action: 'cost_mapping_backfilled', - targetType: 'order_item', - targetId: '', - data: data as Record, - }), - }), -) - -router.post( - '/cost-center/normalized-quantities/backfill', - requireAdminPermission('finance.config'), - createJsonHandler((req) => backfillAdminNormalizedQuantities(bodyOf(req)), { - successMessage: '归一数量回填完成', - errorMessage: '回填归一数量失败', - scope: '[admin/cost-center/normalized-quantities/backfill]', - audit: (_req, data) => ({ - action: 'order_item_quantity_backfilled', - targetType: 'order_item', - targetId: '', - data: data as Record, - }), - }), -) - -router.get( - '/cost-center/cost-items', - requireAdminPermission('finance.view'), - createJsonHandler((req) => listAdminCostItems(req.query as JsonObject), { - successMessage: 'ok', - errorMessage: '读取成本项目失败', - scope: '[admin/cost-center/cost-items]', - }), -) - -router.post( - '/cost-center/cost-items', - requireAdminPermission('finance.config'), - createJsonHandler((req) => createAdminCostItem(bodyOf(req), sessionOf(req)), { - successMessage: '成本项目已创建', - errorMessage: '创建成本项目失败', - scope: '[admin/cost-center/cost-items]', - audit: (_req, data) => { - const item = (data as { costItem?: { id?: number; code?: string } })?.costItem - return { - action: 'cost_item_created', - targetType: 'cost_item', - targetId: String(item?.id || ''), - data: { code: item?.code || '' }, - } - }, - }), -) - -router.patch( - '/cost-center/cost-items/:costItemId', - requireAdminPermission('finance.config'), - createJsonHandler((req) => updateAdminCostItem(idOf(req, 'costItemId'), bodyOf(req)), { - successMessage: '成本项目已更新', - errorMessage: '更新成本项目失败', - scope: '[admin/cost-center/cost-items/:costItemId]', - audit: (req, data) => { - const item = (data as { costItem?: { id?: number; code?: string } })?.costItem - return { - action: 'cost_item_updated', - targetType: 'cost_item', - targetId: String(item?.id || req.params.costItemId || ''), - data: { code: item?.code || '' }, - } - }, - }), -) - -router.delete( - '/cost-center/cost-items/:costItemId', - requireAdminPermission('finance.config'), - createJsonHandler((req) => deleteAdminCostItem(idOf(req, 'costItemId')), { - successMessage: '成本项目已删除', - errorMessage: '删除成本项目失败', - scope: '[admin/cost-center/cost-items/:costItemId]', - audit: (req, data) => { - const item = (data as { costItem?: { id?: number; code?: string } })?.costItem - return { - action: 'cost_item_deleted', - targetType: 'cost_item', - targetId: String(item?.id || req.params.costItemId || ''), - data: { code: item?.code || '' }, - } - }, - }), -) - -router.get( - '/cost-center/cost-versions', - requireAdminPermission('finance.view'), - createJsonHandler((req) => listAdminCostVersions(req.query as JsonObject), { - successMessage: 'ok', - errorMessage: '读取成本版本失败', - scope: '[admin/cost-center/cost-versions]', - }), -) - -router.post( - '/cost-center/cost-versions', - requireAdminPermission('finance.config'), - createJsonHandler((req) => createAdminCostVersion(bodyOf(req), sessionOf(req)), { - successMessage: '成本版本已创建', - errorMessage: '创建成本版本失败', - scope: '[admin/cost-center/cost-versions]', - audit: (_req, data) => { - const version = (data as { version?: { id?: number; costItemId?: number } })?.version - return { - action: 'cost_version_created', - targetType: 'cost_version', - targetId: String(version?.id || ''), - data: { costItemId: version?.costItemId || 0 }, - } - }, - }), -) - -router.patch( - '/cost-center/cost-versions/:versionId/retire', - requireAdminPermission('finance.config'), - createJsonHandler((req) => retireAdminCostVersion(idOf(req, 'versionId'), bodyOf(req)), { - successMessage: '成本版本已停用', - errorMessage: '停用成本版本失败', - scope: '[admin/cost-center/cost-versions/:versionId/retire]', - audit: (req, data) => { - const version = (data as { version?: { id?: number } })?.version - return { - action: 'cost_version_retired', - targetType: 'cost_version', - targetId: String(version?.id || req.params.versionId || ''), - data: {}, - } - }, - }), -) - -router.delete( - '/cost-center/cost-versions/:versionId', - requireAdminPermission('finance.config'), - createJsonHandler((req) => deleteAdminCostVersion(idOf(req, 'versionId')), { - successMessage: '成本版本已删除', - errorMessage: '删除成本版本失败', - scope: '[admin/cost-center/cost-versions/:versionId]', - audit: (req, data) => { - const version = (data as { version?: { id?: number } })?.version - return { - action: 'cost_version_deleted', - targetType: 'cost_version', - targetId: String(version?.id || req.params.versionId || ''), - data: {}, - } - }, - }), -) - -router.get( - '/cost-center/exceptions', - requireAdminPermission('finance.view'), - createJsonHandler((req) => listAdminCostExceptions(req.query as JsonObject), { - successMessage: 'ok', - errorMessage: '读取成本异常失败', - scope: '[admin/cost-center/exceptions]', - }), -) - -// 必须注册在 /cost-center/exceptions/:exceptionId 之前,避免 summary 被当成 ID。 -router.get( - '/cost-center/exceptions/summary', - requireAdminPermission('finance.view'), - createJsonHandler((req) => summarizeAdminCostExceptions(req.query as JsonObject), { - successMessage: 'ok', - errorMessage: '读取成本异常汇总失败', - scope: '[admin/cost-center/exceptions/summary]', - }), -) - -router.patch( - '/cost-center/exceptions/:exceptionId', - requireAdminPermission('finance.config'), - createJsonHandler( - (req) => resolveAdminCostException(idOf(req, 'exceptionId'), bodyOf(req), sessionOf(req)), - { - successMessage: '成本异常已处理', - errorMessage: '处理成本异常失败', - scope: '[admin/cost-center/exceptions/:exceptionId]', - audit: (req, data) => { - const exception = (data as { exception?: { id?: number; status?: string } })?.exception - return { - action: 'cost_exception_resolved', - targetType: 'cost_exception', - targetId: String(exception?.id || req.params.exceptionId || ''), - data: { status: exception?.status || '' }, - } - }, - }, - ), -) - -router.post( - '/cost-center/preview', - requireAdminPermission('finance.view'), - createJsonHandler((req) => previewAdminCost(bodyOf(req)), { - successMessage: 'ok', - errorMessage: '成本预览失败', - scope: '[admin/cost-center/preview]', - }), -) - -router.post( - '/cost-center/snapshots', - requireAdminPermission('finance.config'), - createJsonHandler((req) => createAdminCostSnapshot(bodyOf(req), sessionOf(req)), { - successMessage: '成本快照已生成', - errorMessage: '生成成本快照失败', - scope: '[admin/cost-center/snapshots]', - audit: (req, data) => { - const result = data as { - createdCount?: number - exceptionCreatedCount?: number - preview?: { context?: { orderItemId?: number } } - } - return { - action: 'cost_snapshot_created', - targetType: 'order_item', - targetId: String(result.preview?.context?.orderItemId || req.body?.orderItemId || ''), - data: { - createdCount: result.createdCount || 0, - exceptionCreatedCount: result.exceptionCreatedCount || 0, - }, - } - }, - }), -) - -router.post( - '/cost-center/snapshots/sync', - requireAdminPermission('finance.config'), - createJsonHandler( - async (req) => { - const body = bodyOf(req) - const mode = String(body.mode || 'both').trim() - const estimated = mode === 'actual' ? null : await syncEstimatedOrderItemCostSnapshots(body) - const actual = mode === 'estimated' ? null : await syncActualWorkOrderCostSnapshots(body) - return { mode, estimated, actual } - }, - { - successMessage: '成本快照回填完成', - errorMessage: '回填成本快照失败', - scope: '[admin/cost-center/snapshots/sync]', - audit: (req, data) => { - const result = data as { - mode?: string - estimated?: { createdCount?: number; exceptionCreatedCount?: number } | null - actual?: { - createdCount?: number - directRewardCount?: number - upgradedCount?: number - estimatedRewardCount?: number - } | null - } - return { - action: 'cost_snapshot_synced', - targetType: 'cost_snapshot', - targetId: '', - data: { - mode: result.mode || '', - estimatedCreated: result.estimated?.createdCount || 0, - actualCreated: - (result.actual?.directRewardCount || 0) + (result.actual?.upgradedCount || 0), - estimatedRewardCreated: result.actual?.estimatedRewardCount || 0, - exceptions: result.estimated?.exceptionCreatedCount || 0, - dateFrom: req.body?.dateFrom || '', - dateTo: req.body?.dateTo || '', - }, - } - }, - }, - ), -) - -// --------------------------------------------------------------------------- -// 人工履约 SKU 配置:扫描 → 配置 → 重新匹配 -// --------------------------------------------------------------------------- - -router.get( - '/cost-center/manual-skus', - requireAdminPermission('finance.view'), - createJsonHandler((req) => listManualSkus(req.query as Record), { - successMessage: 'ok', - errorMessage: '读取人工 SKU 清单失败', - scope: '[admin/cost-center/manual-skus]', - }), -) - -router.get( - '/cost-center/manual-skus/overview', - requireAdminPermission('finance.view'), - createJsonHandler(() => getManualSkuOverview(), { - successMessage: 'ok', - errorMessage: '读取人工 SKU 概览失败', - scope: '[admin/cost-center/manual-skus/overview]', - }), -) - -router.get( - '/cost-center/manual-platforms', - requireAdminPermission('finance.view'), - createJsonHandler(() => listManualPlatformOptions(), { - successMessage: 'ok', - errorMessage: '读取人工履约渠道失败', - scope: '[admin/cost-center/manual-platforms]', - }), -) - -router.get( - '/cost-center/manual-configs/:skuKey', - requireAdminPermission('finance.view'), - createJsonHandler((req) => getManualFulfillmentConfig(String(req.params.skuKey || '')), { - successMessage: 'ok', - errorMessage: '读取人工履约配置失败', - scope: '[admin/cost-center/manual-configs/:skuKey]', - }), -) - -router.post( - '/cost-center/manual-skus/scan', - requireAdminPermission('finance.config'), - createJsonHandler((req) => scanManualSkus(bodyOf(req) as never), { - successMessage: '扫描完成', - errorMessage: '扫描人工 SKU 失败', - scope: '[admin/cost-center/manual-skus/scan]', - audit: (req, data) => { - const result = data as { scanned?: number; fromDate?: string; toDate?: string } - return { - action: 'manual_skus_scanned', - targetType: 'manual_sku_registry', - targetId: '', - data: { - scanned: result.scanned || 0, - fromDate: result.fromDate || '', - toDate: result.toDate || '', - }, - } - }, - }), -) - -router.post( - '/cost-center/manual-configs', - requireAdminPermission('finance.config'), - createJsonHandler((req) => saveManualFulfillmentConfig(bodyOf(req)), { - successMessage: '人工履约配置已保存', - errorMessage: '保存人工履约配置失败', - scope: '[admin/cost-center/manual-configs]', - audit: (req, data) => { - const config = data as { - id?: number - skuKey?: string - platformKey?: string - creditQuantity?: number - costFen?: number - } - return { - action: 'manual_fulfillment_config_saved', - targetType: 'manual_fulfillment_config', - targetId: String(config.id || ''), - data: { - skuKey: config.skuKey || '', - platformKey: config.platformKey || '', - creditQuantity: config.creditQuantity || 0, - costFen: config.costFen || 0, - }, - } - }, - }), -) - -router.delete( - '/cost-center/manual-configs/:skuKey', - requireAdminPermission('finance.config'), - createJsonHandler((req) => deleteManualFulfillmentConfig(String(req.params.skuKey || '')), { - successMessage: '人工履约配置已删除', - errorMessage: '删除人工履约配置失败', - scope: '[admin/cost-center/manual-configs/:skuKey]', - audit: (req, data) => { - const result = data as { skuKey?: string; deleted?: number } - return { - action: 'manual_fulfillment_config_deleted', - targetType: 'manual_fulfillment_config', - targetId: String(req.params.skuKey || ''), - data: { skuKey: result.skuKey || '', deleted: result.deleted || 0 }, - } - }, - }), -) - -router.post( - '/cost-center/manual-configs/rematch', - requireAdminPermission('finance.config'), - createJsonHandler((req) => rematchManualFulfillments(bodyOf(req)), { - successMessage: '重新匹配完成', - errorMessage: '重新匹配失败', - scope: '[admin/cost-center/manual-configs/rematch]', - audit: (req, data) => { - const result = data as { updatedCount?: number; skuKey?: string } - return { - action: 'manual_fulfillment_rematched', - targetType: 'order_items', - targetId: '', - data: { - updatedCount: result.updatedCount || 0, - skuKey: result.skuKey || '', - }, - } - }, - }), -) - -// --------------------------------------------------------------------------- -// 客服平台积分钱包(南航 / 乐玩 / ROCK) -// --------------------------------------------------------------------------- - -router.get( - '/cost-center/platform-credits/accounts', - requireAnyAdminPermission(['fund.view', 'finance.view']), - createJsonHandler((req) => listPlatformCreditAccounts(sessionOf(req)), { - successMessage: 'ok', - errorMessage: '读取积分钱包失败', - scope: '[admin/cost-center/platform-credits/accounts]', - }), -) - -router.get( - '/cost-center/platform-credits/entries', - requireAnyAdminPermission(['fund.view', 'finance.view']), - createJsonHandler( - (req) => listPlatformCreditEntries(req.query as Record, sessionOf(req)), - { - successMessage: 'ok', - errorMessage: '读取积分流水失败', - scope: '[admin/cost-center/platform-credits/entries]', - }, - ), -) - -router.get( - '/cost-center/platform-credits/owners', - requireAdminPermission('finance.config'), - createJsonHandler(() => listPlatformCreditOwners(), { - successMessage: 'ok', - errorMessage: '读取客服列表失败', - scope: '[admin/cost-center/platform-credits/owners]', - }), -) - -router.post( - '/cost-center/platform-credits/grant', - requireAdminPermission('finance.config'), - createJsonHandler((req) => grantPlatformCredit(bodyOf(req), sessionOf(req)), { - successMessage: '积分已充值', - errorMessage: '积分充值失败', - scope: '[admin/cost-center/platform-credits/grant]', - audit: (req, data) => { - const result = data as { - platformKey?: string - quantity?: number - amountFen?: number - } - return { - action: 'platform_credit_granted', - targetType: 'platform_credit_account', - targetId: String(req.body?.ownerAdminUserId || ''), - data: { - platformKey: result.platformKey || '', - quantity: result.quantity || 0, - amountFen: result.amountFen || 0, - }, - } - }, - }), -) - -router.post( - '/cost-center/platform-credits/adjust', - requireAdminPermission('finance.config'), - createJsonHandler((req) => adjustPlatformCredit(bodyOf(req), sessionOf(req)), { - successMessage: '积分已调整', - errorMessage: '积分调整失败', - scope: '[admin/cost-center/platform-credits/adjust]', - audit: (req, data) => { - const result = data as { - platformKey?: string - quantity?: number - amountFen?: number - } - return { - action: 'platform_credit_adjusted', - targetType: 'platform_credit_account', - targetId: String(req.body?.ownerAdminUserId || ''), - data: { - platformKey: result.platformKey || '', - quantity: result.quantity || 0, - amountFen: result.amountFen || 0, - }, - } - }, - }), -) +router.use(catalogRoutes) +router.use(costRoutes) +router.use(manualRoutes) +router.use(creditRoutes) export default router