Files
order_site/apps/backend/src/routes/admin/fulfillment-channel.ts
T
yml2213 0ce3c8fa97 feat(fulfillment): 渠道联动与管道兜底
- 人工配置保存/删除同步渠道规则,人工确认写 channel/status
- 按 SKU 指定渠道默认同时落规则
- 管道补充:载体缺失、人工成本未记录、事实与总账按水位追赶
2026-10-31 19:52:22 +08:00

239 lines
6.9 KiB
TypeScript

/**
* 履约渠道 v2 路由:待分类队列、运营指定渠道、渠道规则、管道触发。
*
* 与成本中心的分工:
* · 这里只处理「渠道」;
* · 商品归一、成本版本、异常处置仍在成本中心的既有页面里。
*/
import { Router } from 'express'
import {
assignChannelBySku,
assignChannelToOrderItems,
deleteChannelRule,
listChannelRules,
listPendingChannelGroups,
unlockChannel,
upsertChannelRule,
} from '../../services/fulfillment/channel-assign-service.js'
import { listExceptionQueue } from '../../services/cost-center/cost-exception-service.js'
import {
runFinancePipelineOnce,
runSingleFinancePipeline,
} from '../../services/scheduler/finance-pipeline-jobs.js'
import {
getFinancePipelineStatus,
isFinancePipelineName,
} from '../../services/scheduler/finance-pipeline-status.js'
import { bodyOf } from './request-helpers.js'
import { createJsonHandler, requireAdminPermission, requireAdminRoles } from './session.js'
const router = Router()
router.use('/fulfillment', requireAdminRoles(['admin', 'operator', 'finance', 'support']))
router.get(
'/fulfillment/pending',
requireAdminPermission('finance.view'),
createJsonHandler(
(req) =>
listPendingChannelGroups({
page: Number(req.query.page),
pageSize: Number(req.query.pageSize),
keyword: String(req.query.keyword || ''),
}),
{
successMessage: 'ok',
errorMessage: '读取待分类队列失败',
scope: '[admin/fulfillment/pending]',
},
),
)
router.get(
'/fulfillment/exceptions',
requireAdminPermission('finance.view'),
createJsonHandler(
(req) =>
listExceptionQueue({
reason: String(req.query.reason || ''),
status: String(req.query.status || 'open'),
page: Number(req.query.page),
pageSize: Number(req.query.pageSize),
}),
{
successMessage: 'ok',
errorMessage: '读取问题队列失败',
scope: '[admin/fulfillment/exceptions]',
},
),
)
router.post(
'/fulfillment/assign',
requireAdminPermission('finance.config'),
createJsonHandler(
async (req) => {
const body = bodyOf(req)
const channel = String(body.channel || '').trim()
const adminUserId = req.adminSession?.userId ?? null
const note = String(body.note || '')
if (body.skuCode) {
return assignChannelBySku({
provider: String(body.provider || ''),
shopId: String(body.shopId || ''),
skuCode: String(body.skuCode),
channel,
adminUserId,
note,
createRule: body.createRule !== false,
})
}
const ids = Array.isArray(body.orderItemIds)
? body.orderItemIds.map((value: unknown) => Number(value))
: [Number(body.orderItemId)].filter((value) => Number.isInteger(value) && value > 0)
return assignChannelToOrderItems({ orderItemIds: ids, channel, adminUserId, note })
},
{
successMessage: '已指定渠道',
errorMessage: '指定渠道失败',
scope: '[admin/fulfillment/assign]',
audit: (req, data) => ({
action: 'fulfillment.assign_channel',
targetType: 'order_item',
targetId: String(bodyOf(req).skuCode || bodyOf(req).orderItemId || ''),
data: { payload: bodyOf(req), result: data as Record<string, unknown> },
}),
},
),
)
router.post(
'/fulfillment/unlock',
requireAdminPermission('finance.config'),
createJsonHandler(
async (req) => {
const body = bodyOf(req)
const ids = Array.isArray(body.orderItemIds)
? body.orderItemIds.map((value: unknown) => Number(value))
: []
return unlockChannel(ids)
},
{
successMessage: '已解锁',
errorMessage: '解锁失败',
scope: '[admin/fulfillment/unlock]',
},
),
)
router.get(
'/fulfillment/rules',
requireAdminPermission('finance.view'),
createJsonHandler(
(req) =>
listChannelRules({
channel: String(req.query.channel || ''),
...(req.query.enabled === undefined
? {}
: { enabled: String(req.query.enabled) !== 'false' }),
}),
{
successMessage: 'ok',
errorMessage: '读取渠道规则失败',
scope: '[admin/fulfillment/rules]',
},
),
)
router.post(
'/fulfillment/rules',
requireAdminPermission('finance.config'),
createJsonHandler(
async (req) => {
const body = bodyOf(req)
return upsertChannelRule({
...(body.id ? { id: Number(body.id) } : {}),
provider: String(body.provider || ''),
shopId: String(body.shopId || ''),
matchKeyType: String(body.matchKeyType || 'sku_code'),
matchKey: String(body.matchKey || ''),
channel: String(body.channel || ''),
priority: Number(body.priority),
source: String(body.source || 'operator'),
enabled: body.enabled !== false,
note: String(body.note || ''),
adminUserId: req.adminSession?.userId ?? null,
})
},
{
successMessage: '规则已保存',
errorMessage: '保存渠道规则失败',
scope: '[admin/fulfillment/rules]',
audit: (req, data) => ({
action: 'fulfillment.upsert_rule',
targetType: 'sku_channel_rule',
targetId: String((data as { id?: number })?.id || ''),
data: { payload: bodyOf(req) },
}),
},
),
)
router.delete(
'/fulfillment/rules/:id',
requireAdminPermission('finance.config'),
createJsonHandler((req) => deleteChannelRule(Number(req.params.id)), {
successMessage: '规则已删除',
errorMessage: '删除渠道规则失败',
scope: '[admin/fulfillment/rules]',
}),
)
router.get(
'/fulfillment/pipeline/status',
requireAdminPermission('finance.view'),
createJsonHandler(() => getFinancePipelineStatus(), {
successMessage: 'ok',
errorMessage: '读取管道状态失败',
scope: '[admin/fulfillment/pipeline/status]',
}),
)
router.post(
'/fulfillment/pipeline/run',
requireAdminPermission('finance.config'),
createJsonHandler(
async (req) => {
const body = bodyOf(req)
const days = Number(body.days)
// 指定 pipeline 时只跑单步,便于排障与补跑。
if (body.pipeline) {
if (!isFinancePipelineName(body.pipeline)) {
throw new Error(`未知管道:${String(body.pipeline)}`)
}
return runSingleFinancePipeline(body.pipeline, {
config: Number.isFinite(days) ? { days } : {},
})
}
return runFinancePipelineOnce({ days: Number.isFinite(days) ? days : 3 })
},
{
successMessage: '管道已执行',
errorMessage: '执行管道失败',
scope: '[admin/fulfillment/pipeline]',
audit: (req, data) => ({
action: 'fulfillment.run_pipeline',
targetType: 'pipeline',
targetId: 'finance',
data: { payload: bodyOf(req), result: data as Record<string, unknown> },
}),
},
),
)
export default router