把「配置 → 待办 → 确认履约 → 扣额度 → 写成本快照」整条链路打通, 客服点一下就完成履约,不需要填复杂的东西。 积分钱包(P1.3): - platform-credit-service.ts:充值 / 调整 / 消耗 / 流水查询。 - 三平台积分互不通用,每客服每平台一个账户;余额只能由流水产生, 消耗不足直接拒绝(409),不允许负余额,与备用金口径一致。 - 换算率按平台:南航、乐玩 1 积分 = 1 分,ROCK 1 积分 = 1 元。 - 充值 / 调整只对 admin/operator/finance 开放;客服只能看自己的钱包。 - 备用金现金不在这里,走现有 fund_* 链路。 确认履约(P1.4): - manual-fulfillment-service.ts:待办列表 + 确认履约。 - 待办口径:order_items.fulfillment_mode='manual' 且已配 fulfillment_config_id。 人工单**不建 fulfillment_tasks**——那是自动发货通道的体系,人工单历来不走它 (本地 102,955 个人工单的任务行数为 0)。 - 「已履约」判定:该订单项存在有效 manual_case 成本快照。 - 固定成本(消耗积分 > 0)→ 零输入;成本不固定(= 0)→ 只填实际成本金额。 - 确认时:积分平台扣积分钱包、备用金现金走 writeFundExpense, 随后写 actual 成本快照(主体 = 积分流水 id 或备用金支出单 id)。 - 渠道与消耗积分永远提前配好,客服不选;重复履约直接拒绝。 前端: - 新增「人工履约」页(客服菜单):待确认 / 已履约 / 全部,确认履约弹窗。 - 备用金页新增「平台积分」Tab(客服端与管理端各一份): 余额卡片、积分流水、管理员充值与调整。 E2E 实测:配置 ROCK 100 积分 → 成本 ¥100.00(1 积分 = 1 元);充值 5000 积分 = ¥5000;确认履约后积分 5000 → 4900,成本快照 actual ¥100.00 写入; 重复履约被拒绝。测试数据已清理,登记表保留 357 条扫描结果供运营使用。 校验:后端 765 项 / 0 失败,前端 143 项全通过(含 tsc -b 与 build)。 Co-Authored-By: Claude Opus 4.5 (1M context) <noreply@anthropic.com>
208 lines
6.2 KiB
TypeScript
208 lines
6.2 KiB
TypeScript
import type { Request } from 'express'
|
|
import { Router } from 'express'
|
|
|
|
import {
|
|
getAdminFinanceSummary,
|
|
listAdminFinanceTransactions,
|
|
} from '../../services/admin/admin-finance-service.js'
|
|
import type { AdminSession } from '../../services/admin/admin-auth-service.js'
|
|
import {
|
|
getSalesProfitOverview,
|
|
listSalesDailyFacts,
|
|
rebuildSalesDailyFacts,
|
|
} from '../../services/finance/sales-facts-service.js'
|
|
import {
|
|
createManualOrderCost,
|
|
listManualOrderCostItems,
|
|
} from '../../services/finance/manual-order-cost-service.js'
|
|
import {
|
|
confirmManualFulfillment,
|
|
listManualFulfillmentPending,
|
|
} from '../../services/finance/manual-fulfillment-service.js'
|
|
import type { JsonObject } from '../../types/json.js'
|
|
import {
|
|
createJsonHandler,
|
|
requireAdminPermission,
|
|
requireAdminRoles,
|
|
requireAnyAdminPermission,
|
|
} from './session.js'
|
|
|
|
const router = Router()
|
|
|
|
function sessionOf(req: Request): AdminSession {
|
|
return req.adminSession as AdminSession
|
|
}
|
|
|
|
function idOf(req: Request, key: string) {
|
|
return Number(req.params[key])
|
|
}
|
|
|
|
function bodyOf(req: Request): JsonObject {
|
|
return req.body && typeof req.body === 'object' ? (req.body as JsonObject) : {}
|
|
}
|
|
|
|
router.get(
|
|
'/finance/summary',
|
|
requireAdminRoles(['admin', 'operator', 'finance']),
|
|
createJsonHandler((req) => getAdminFinanceSummary(req.query), {
|
|
successMessage: 'ok',
|
|
errorMessage: '读取财务汇总失败',
|
|
scope: '[admin/finance/summary]',
|
|
}),
|
|
)
|
|
|
|
router.get(
|
|
'/finance/transactions',
|
|
requireAdminRoles(['admin', 'operator', 'finance']),
|
|
createJsonHandler((req) => listAdminFinanceTransactions(req.query), {
|
|
successMessage: 'ok',
|
|
errorMessage: '读取财务明细失败',
|
|
scope: '[admin/finance/transactions]',
|
|
}),
|
|
)
|
|
|
|
router.get(
|
|
'/finance/sales-profit/overview',
|
|
requireAdminRoles(['admin', 'operator', 'finance']),
|
|
requireAdminPermission('finance.view'),
|
|
createJsonHandler((req) => getSalesProfitOverview(req.query as Record<string, unknown>), {
|
|
successMessage: 'ok',
|
|
errorMessage: '读取销售毛利失败',
|
|
scope: '[admin/finance/sales-profit/overview]',
|
|
}),
|
|
)
|
|
|
|
router.get(
|
|
'/finance/sales-facts',
|
|
requireAdminRoles(['admin', 'operator', 'finance']),
|
|
requireAdminPermission('finance.view'),
|
|
createJsonHandler((req) => listSalesDailyFacts(req.query as Record<string, unknown>), {
|
|
successMessage: 'ok',
|
|
errorMessage: '读取销售事实失败',
|
|
scope: '[admin/finance/sales-facts]',
|
|
}),
|
|
)
|
|
|
|
router.post(
|
|
'/finance/sales-facts/rebuild',
|
|
requireAdminRoles(['admin', 'operator', 'finance']),
|
|
requireAdminPermission('finance.config'),
|
|
createJsonHandler(
|
|
(req) =>
|
|
rebuildSalesDailyFacts(
|
|
req.body && typeof req.body === 'object' ? (req.body as Record<string, unknown>) : {},
|
|
),
|
|
{
|
|
successMessage: '销售事实已重算',
|
|
errorMessage: '重算销售事实失败',
|
|
scope: '[admin/finance/sales-facts/rebuild]',
|
|
audit: (req, data) => {
|
|
const result = data as {
|
|
range?: { fromDate?: string; toDate?: string }
|
|
insertedCount?: number
|
|
deletedCount?: number
|
|
}
|
|
return {
|
|
action: 'sales_daily_facts_rebuilt',
|
|
targetType: 'sales_daily_facts',
|
|
targetId: '',
|
|
data: {
|
|
dateFrom: result.range?.fromDate || req.body?.dateFrom || '',
|
|
dateTo: result.range?.toDate || req.body?.dateTo || '',
|
|
insertedCount: result.insertedCount || 0,
|
|
deletedCount: result.deletedCount || 0,
|
|
},
|
|
}
|
|
},
|
|
},
|
|
),
|
|
)
|
|
|
|
router.get(
|
|
'/finance/manual-order-cost-items',
|
|
requireAnyAdminPermission(['finance.view', 'fund.view']),
|
|
createJsonHandler(() => listManualOrderCostItems(), {
|
|
successMessage: 'ok',
|
|
errorMessage: '读取人工录单成本项失败',
|
|
scope: '[admin/finance/manual-order-cost-items]',
|
|
}),
|
|
)
|
|
|
|
router.post(
|
|
'/finance/manual-order-costs',
|
|
requireAnyAdminPermission(['fund.expense', 'finance.config']),
|
|
createJsonHandler((req) => createManualOrderCost(bodyOf(req), sessionOf(req)), {
|
|
successMessage: '订单人工成本已录入',
|
|
errorMessage: '录入订单人工成本失败',
|
|
scope: '[admin/finance/manual-order-costs]',
|
|
audit: (req, data) => {
|
|
const result = data as {
|
|
expenseNo?: string
|
|
amountFen?: number
|
|
orderId?: number
|
|
costSnapshotId?: number | null
|
|
}
|
|
return {
|
|
action: 'manual_order_cost_created',
|
|
targetType: 'fund_expense',
|
|
targetId: result.expenseNo || '',
|
|
data: {
|
|
expenseNo: result.expenseNo || '',
|
|
amountFen: result.amountFen || 0,
|
|
orderId: result.orderId || req.body?.orderId || '',
|
|
costSnapshotId: result.costSnapshotId || null,
|
|
},
|
|
}
|
|
},
|
|
}),
|
|
)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 人工履约待办与确认履约
|
|
// ---------------------------------------------------------------------------
|
|
|
|
router.get(
|
|
'/finance/manual-fulfillment/pending',
|
|
requireAdminRoles(['admin', 'operator', 'support']),
|
|
createJsonHandler(
|
|
(req) => listManualFulfillmentPending(req.query as Record<string, unknown>, sessionOf(req)),
|
|
{
|
|
successMessage: 'ok',
|
|
errorMessage: '读取人工履约待办失败',
|
|
scope: '[admin/finance/manual-fulfillment/pending]',
|
|
},
|
|
),
|
|
)
|
|
|
|
router.post(
|
|
'/finance/manual-fulfillment/confirm',
|
|
requireAdminRoles(['admin', 'operator', 'support']),
|
|
createJsonHandler((req) => confirmManualFulfillment(bodyOf(req), sessionOf(req)), {
|
|
successMessage: '履约已确认',
|
|
errorMessage: '确认履约失败',
|
|
scope: '[admin/finance/manual-fulfillment/confirm]',
|
|
audit: (req, data) => {
|
|
const result = data as {
|
|
orderItemId?: number
|
|
costSnapshotId?: number
|
|
platformKey?: string
|
|
creditQuantity?: number
|
|
costFen?: number
|
|
}
|
|
return {
|
|
action: 'manual_fulfillment_confirmed',
|
|
targetType: 'order_item',
|
|
targetId: String(result.orderItemId || ''),
|
|
data: {
|
|
costSnapshotId: result.costSnapshotId || null,
|
|
platformKey: result.platformKey || '',
|
|
creditQuantity: result.creditQuantity || 0,
|
|
costFen: result.costFen || 0,
|
|
},
|
|
}
|
|
},
|
|
}),
|
|
)
|
|
|
|
export default router
|