- 新增迁移 107_product_catalog_carrier:product_catalog 增加 carrier_type, 自动发货只认 fulfillment_tasks.executor_key='affiliate_dash' 的履约证据 (28 个上游 SKU),接单模板商品统一归为 work_order(662 个) - 页面按「全部 / 自动发货 / 接单平台 / 人工客服 / 共用成本」分区; 自动发货只展示自动发货 SKU 列表与采购成本,不展示商品目录/渠道映射 - 新增「同步自动发货商品」接口与按钮,按 aff:<skuCode> 幂等补建 - 列表统一接入公共分页 buildAdminTablePagination,支持 10/20/50/100 每页条数 - 标准商品、自动发货 SKU、渠道映射、成本项目、成本版本支持删除, 带引用保护(映射/版本/快照/订单/接单规则)与生效版本先停用约束
494 lines
16 KiB
TypeScript
494 lines
16 KiB
TypeScript
import type { Request } from 'express'
|
|
import { Router } from 'express'
|
|
|
|
import {
|
|
applyAdminTemplateProductBootstrap,
|
|
backfillAdminNormalizedQuantities,
|
|
backfillAdminProductMappings,
|
|
createAdminChannelMapping,
|
|
createAdminCostItem,
|
|
createAdminCostSnapshot,
|
|
createAdminCostVersion,
|
|
createAdminProduct,
|
|
deleteAdminChannelMapping,
|
|
deleteAdminCostItem,
|
|
deleteAdminCostVersion,
|
|
deleteAdminProduct,
|
|
getAdminCostCenterOptions,
|
|
getAdminCostCenterOverview,
|
|
listAdminChannelMappings,
|
|
listAdminCostExceptions,
|
|
listAdminCostItems,
|
|
listAdminCostVersions,
|
|
listAdminProducts,
|
|
previewAdminCost,
|
|
previewAdminTemplateProductBootstrap,
|
|
resolveAdminCostException,
|
|
retireAdminCostVersion,
|
|
syncAdminAffiliateDashProducts,
|
|
updateAdminChannelMapping,
|
|
updateAdminCostItem,
|
|
updateAdminProduct,
|
|
} from '../../services/admin/admin-cost-center-service.js'
|
|
import type { AdminSession } from '../../services/admin/admin-auth-service.js'
|
|
import type { JsonObject } from '../../types/json.js'
|
|
import { createJsonHandler, requireAdminPermission, requireAdminRoles } from './session.js'
|
|
|
|
const router = Router()
|
|
|
|
function sessionOf(req: Request): AdminSession {
|
|
return req.adminSession as AdminSession
|
|
}
|
|
|
|
function idOf(req: Request, key: string): number {
|
|
return Number(req.params[key])
|
|
}
|
|
|
|
function bodyOf(req: Request): JsonObject {
|
|
return req.body && typeof req.body === 'object' ? (req.body as JsonObject) : {}
|
|
}
|
|
|
|
router.use('/cost-center', requireAdminRoles(['admin', 'operator', 'finance']))
|
|
|
|
router.get(
|
|
'/cost-center/overview',
|
|
requireAdminPermission('finance.view'),
|
|
createJsonHandler(() => getAdminCostCenterOverview(), {
|
|
successMessage: 'ok',
|
|
errorMessage: '读取成本中心总览失败',
|
|
scope: '[admin/cost-center/overview]',
|
|
}),
|
|
)
|
|
|
|
router.get(
|
|
'/cost-center/options',
|
|
requireAdminPermission('finance.view'),
|
|
createJsonHandler(() => getAdminCostCenterOptions(), {
|
|
successMessage: 'ok',
|
|
errorMessage: '读取成本中心选项失败',
|
|
scope: '[admin/cost-center/options]',
|
|
}),
|
|
)
|
|
|
|
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<string, unknown>,
|
|
}),
|
|
}),
|
|
)
|
|
|
|
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<string, unknown>,
|
|
}),
|
|
}),
|
|
)
|
|
|
|
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.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<string, unknown>,
|
|
}),
|
|
}),
|
|
)
|
|
|
|
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<string, unknown>,
|
|
}),
|
|
}),
|
|
)
|
|
|
|
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]',
|
|
}),
|
|
)
|
|
|
|
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,
|
|
},
|
|
}
|
|
},
|
|
}),
|
|
)
|
|
|
|
export default router
|