feat(finance): 人工履约 SKU 配置清单 + 四钱包(南航/乐玩/ROCK积分 + 备用金现金)

客服人工履约有四个钱包,成本与渠道在后台配置好,客服确认履约时零输入。

口径(2026-10-24 确认):
- 四个钱包:南航积分 / 乐玩积分 / ROCK 积分 / 备用金现金。三平台积分互不通用,
  每客服每平台一个积分账户;现金复用现有 fund_accounts / fund_entries。
- 积分换算率按平台不同:南航 1 积分 = 1 分,乐玩 1 积分 = 1 分,
  ROCK 1 积分 = 1 元(100 分)。真源是履约档案 config_json.rechargeUnitPriceFen。
- 积分是货币,幸运币是商品;消耗积分与成本金额按换算率一一对应。
- 配置主键是**内部 SKU**(快手 spec_json.skuNick / 拼多多 spec_json.outSkuSn),
  不是商品标题(标题是引流)。同名 SKU 算同一商品,跨渠道共用一份配置。
- 配置链路没有匹配引擎、没有打分:扫描 → 配置 → 重新匹配。
  10 月口径下人工单 17,469 单 / 357 个内部 SKU,Top 50 覆盖 88% 单量。
  更早的老单没配置意义,不纳入统计口径。
- 消耗积分填 0 = 成本不固定,客服在确认履约时手填实际成本;
  渠道始终提前配好,客服不选。

迁移 110:
- manual_sku_registry:扫描到的内部 SKU 登记表(统计 + 配置状态)。
- manual_fulfillment_configs:内部 SKU → 渠道 + 消耗积分 + 成本模式。
- platform_credit_accounts / platform_credit_entries:三平台积分钱包与流水。
- order_items.fulfillment_config_id:订单项挂配置,重新匹配时回填。
- 4 条 manual_* 履约档案(manual_dispatch 执行器 + config_json 区分平台)。
  不复用 kuaishou_ct_assisted / kuaishou_feifei——那是已下线的历史自动发货通道。

性能:spec_json->>'skuNick' 无索引时扫描 2.4s / 回填 5.4s;
新增 idx_order_items_manual_internal_sku 表达式索引后回填降到 1.7s。

后端:manual-fulfillment-config-service.ts(扫描/清单/配置CRUD/重新匹配),
成本中心挂 7 个接口。
前端:成本中心新增「人工 SKU 配置」Tab(扫描、清单、配置弹窗、重新匹配)。

校验:后端 765 项 / 0 失败(含 check:sql 迁移护栏),前端 143 项全通过;
E2E 实测扫描 357 个 SKU、配置四平台换算正确、重新匹配回填 7,604 个订单项;
本地库迁移 110 执行后数据量与改造前一致。

Co-Authored-By: Claude Opus 4.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
yml2213
2026-10-24 13:12:46 +08:00
co-authored by Claude Opus 4.5
parent fb679d76f6
commit 4bcb4e6b57
7 changed files with 1693 additions and 0 deletions
@@ -31,6 +31,16 @@ import {
updateAdminChannelMapping,
updateAdminProduct,
} from '../../services/cost-center/product-catalog-service.js'
import {
deleteManualFulfillmentConfig,
getManualFulfillmentConfig,
getManualSkuOverview,
listManualPlatformOptions,
listManualSkus,
rematchManualFulfillments,
saveManualFulfillmentConfig,
scanManualSkus,
} from '../../services/cost-center/manual-fulfillment-config-service.js'
import {
applyAdminTemplateProductBootstrap,
backfillAdminAffiliateDashSkus,
@@ -597,4 +607,142 @@ router.post(
),
)
// ---------------------------------------------------------------------------
// 人工履约 SKU 配置:扫描 → 配置 → 重新匹配
// ---------------------------------------------------------------------------
router.get(
'/cost-center/manual-skus',
requireAdminPermission('finance.view'),
createJsonHandler((req) => listManualSkus(req.query as Record<string, unknown>), {
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