feat(admin): 履约渠道接口与断代运维脚本
- 新增 /admin/fulfillment 待分类、指定渠道、规则 CRUD、问题队列、管道触发 - backfill-fulfillment-channel:438k 行分批回填,幂等可续跑 - finance-cutover-v2:默认 dry-run,--execute 才清理派生数据,禁止重复执行 - rebuild-after-cutover:按断代日重跑三条管道并生成期初分录
This commit is contained in:
@@ -7,6 +7,7 @@ import dashboardRouter from './admin/dashboard.js'
|
||||
import devMockRouter from './admin/dev-mock.js'
|
||||
import douyinMashidaRouter from './admin/douyin-mashida.js'
|
||||
import financeRouter from './admin/finance.js'
|
||||
import fulfillmentChannelRouter from './admin/fulfillment-channel.js'
|
||||
import financeLedgerRouter from './admin/finance-ledger.js'
|
||||
import fundRouter from './admin/fund.js'
|
||||
import filesRouter from './admin/files.js'
|
||||
@@ -31,6 +32,7 @@ router.use(dashboardRouter)
|
||||
router.use(financeRouter)
|
||||
router.use(financeLedgerRouter)
|
||||
router.use(costCenterRouter)
|
||||
router.use(fulfillmentChannelRouter)
|
||||
router.use(fundRouter)
|
||||
router.use(usersRouter)
|
||||
router.use(rbacRouter)
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* 履约渠道 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 } from '../../services/scheduler/finance-pipeline-jobs.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,
|
||||
})
|
||||
}
|
||||
|
||||
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.post(
|
||||
'/fulfillment/pipeline/run',
|
||||
requireAdminPermission('finance.config'),
|
||||
createJsonHandler(
|
||||
async (req) => {
|
||||
const body = bodyOf(req)
|
||||
const days = Number(body.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
|
||||
Reference in New Issue
Block a user