feat(finance): 完成成本快照运行层、销售事实与人工支出登记

迁移 108-111:快照业务时间与预估转实际、销售事实表、系统成本项、公司人工支出登记。接单平台按工单实际 reward 入账;人工支出支持登记/审批/驳回/报销并写 actual 快照。新增销售毛利页、人工支出页与订单入口。
This commit is contained in:
yml2213
2026-10-22 16:41:03 +08:00
parent 15989af87c
commit 0889235870
39 changed files with 4824 additions and 143 deletions
@@ -31,6 +31,10 @@ import {
updateAdminProduct,
} from '../../services/admin/admin-cost-center-service.js'
import type { AdminSession } from '../../services/admin/admin-auth-service.js'
import {
syncActualWorkOrderCostSnapshots,
syncEstimatedOrderItemCostSnapshots,
} from '../../services/cost-center/cost-snapshot-sync-service.js'
import type { JsonObject } from '../../types/json.js'
import { createJsonHandler, requireAdminPermission, requireAdminRoles } from './session.js'
@@ -490,4 +494,43 @@ router.post(
}),
)
router.post(
'/cost-center/snapshots/sync',
requireAdminPermission('finance.config'),
createJsonHandler(
async (req) => {
const body = bodyOf(req)
const mode = String(body.mode || 'both').trim()
const estimated = mode === 'actual' ? null : await syncEstimatedOrderItemCostSnapshots(body)
const actual = mode === 'estimated' ? null : await syncActualWorkOrderCostSnapshots(body)
return { mode, estimated, actual }
},
{
successMessage: '成本快照回填完成',
errorMessage: '回填成本快照失败',
scope: '[admin/cost-center/snapshots/sync]',
audit: (req, data) => {
const result = data as {
mode?: string
estimated?: { createdCount?: number; exceptionCreatedCount?: number } | null
actual?: { createdCount?: number } | null
}
return {
action: 'cost_snapshot_synced',
targetType: 'cost_snapshot',
targetId: '',
data: {
mode: result.mode || '',
estimatedCreated: result.estimated?.createdCount || 0,
actualCreated: result.actual?.createdCount || 0,
exceptions: result.estimated?.exceptionCreatedCount || 0,
dateFrom: req.body?.dateFrom || '',
dateTo: req.body?.dateTo || '',
},
}
},
},
),
)
export default router
+211 -1
View File
@@ -1,13 +1,48 @@
import type { Request } from 'express'
import { Router } from 'express'
import {
getAdminFinanceSummary,
listAdminFinanceTransactions,
} from '../../services/admin/admin-finance-service.js'
import { createJsonHandler, requireAdminRoles } from './session.js'
import type { AdminSession } from '../../services/admin/admin-auth-service.js'
import {
getSalesProfitOverview,
listSalesDailyFacts,
rebuildSalesDailyFacts,
} from '../../services/finance/sales-facts-service.js'
import {
approveManualExpense,
createManualExpense,
getManualExpense,
getManualExpenseOverview,
listManualExpenseCostItems,
listManualExpenses,
rejectManualExpense,
reimburseManualExpense,
} from '../../services/finance/manual-expense-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']),
@@ -28,4 +63,179 @@ router.get(
}),
)
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-expenses/overview',
requireAnyAdminPermission(['finance.view', 'fund.view']),
createJsonHandler((req) => getManualExpenseOverview(req.query as Record<string, unknown>), {
successMessage: 'ok',
errorMessage: '读取人工支出汇总失败',
scope: '[admin/finance/manual-expenses/overview]',
}),
)
router.get(
'/finance/manual-expenses',
requireAnyAdminPermission(['finance.view', 'fund.view']),
createJsonHandler((req) => listManualExpenses(req.query as Record<string, unknown>), {
successMessage: 'ok',
errorMessage: '读取人工支出失败',
scope: '[admin/finance/manual-expenses]',
}),
)
router.get(
'/finance/manual-expenses/cost-items',
requireAnyAdminPermission(['finance.view', 'fund.view']),
createJsonHandler(() => listManualExpenseCostItems(), {
successMessage: 'ok',
errorMessage: '读取人工支出成本项失败',
scope: '[admin/finance/manual-expenses/cost-items]',
}),
)
router.get(
'/finance/manual-expenses/:expenseId',
requireAnyAdminPermission(['finance.view', 'fund.view']),
createJsonHandler((req) => getManualExpense(idOf(req, 'expenseId')), {
successMessage: 'ok',
errorMessage: '读取人工支出详情失败',
scope: '[admin/finance/manual-expenses/:expenseId]',
}),
)
router.post(
'/finance/manual-expenses',
requireAnyAdminPermission(['fund.expense', 'finance.config']),
createJsonHandler((req) => createManualExpense(bodyOf(req), sessionOf(req)), {
successMessage: '人工支出已登记',
errorMessage: '登记人工支出失败',
scope: '[admin/finance/manual-expenses]',
audit: (req, data) => {
const record = data as { id?: number; expenseNo?: string; amountFen?: number }
return {
action: 'manual_expense_created',
targetType: 'manual_expense',
targetId: String(record.id || ''),
data: {
expenseNo: record.expenseNo || '',
amountFen: record.amountFen || 0,
orderId: req.body?.orderId || '',
orderItemId: req.body?.orderItemId || '',
},
}
},
}),
)
router.post(
'/finance/manual-expenses/:expenseId/approve',
requireAnyAdminPermission(['finance.review']),
createJsonHandler(
(req) => approveManualExpense(idOf(req, 'expenseId'), bodyOf(req), sessionOf(req)),
{
successMessage: '人工支出已通过',
errorMessage: '审核人工支出失败',
scope: '[admin/finance/manual-expenses/:expenseId/approve]',
audit: (req, data) => ({
action: 'manual_expense_approved',
targetType: 'manual_expense',
targetId: String((data as { id?: number })?.id || req.params.expenseId || ''),
data: { note: req.body?.note || '' },
}),
},
),
)
router.post(
'/finance/manual-expenses/:expenseId/reject',
requireAnyAdminPermission(['finance.review']),
createJsonHandler(
(req) => rejectManualExpense(idOf(req, 'expenseId'), bodyOf(req), sessionOf(req)),
{
successMessage: '人工支出已驳回',
errorMessage: '驳回人工支出失败',
scope: '[admin/finance/manual-expenses/:expenseId/reject]',
audit: (req, data) => ({
action: 'manual_expense_rejected',
targetType: 'manual_expense',
targetId: String((data as { id?: number })?.id || req.params.expenseId || ''),
data: { reason: req.body?.rejectReason || req.body?.reject_reason || '' },
}),
},
),
)
router.post(
'/finance/manual-expenses/:expenseId/reimburse',
requireAnyAdminPermission(['finance.review']),
createJsonHandler((req) => reimburseManualExpense(idOf(req, 'expenseId'), sessionOf(req)), {
successMessage: '人工支出已标记报销',
errorMessage: '标记报销失败',
scope: '[admin/finance/manual-expenses/:expenseId/reimburse]',
audit: (req, data) => ({
action: 'manual_expense_reimbursed',
targetType: 'manual_expense',
targetId: String((data as { id?: number })?.id || req.params.expenseId || ''),
data: {},
}),
}),
)
export default router