- P0/P1:影子总账追加调整、关账/退款/资金/积分幂等、成本版本追溯、对账订单项维度 - 迁移收敛:原 103–114 合并为 103_finance_foundation、104_finance_controls、105_work_order_consignment - 数据:L1 钱包 BIGINT/CHECK/奖励幂等;重复奖励审计口径修正与人工核对清单 - 报表:销售毛利缺失成本收入提示,旧收支页明确现金流口径 - 文档:同步迁移基线、合并评估、代码复核与上线说明
229 lines
6.8 KiB
TypeScript
229 lines
6.8 KiB
TypeScript
/**
|
|
* 财务总账 / 期间关账 / 平台对账路由。
|
|
*
|
|
* 总账是影子账:只读写 finance_*,不接管钱包余额。
|
|
*/
|
|
import { Router } from 'express'
|
|
|
|
import {
|
|
closeFinancePeriod,
|
|
getTrialBalance,
|
|
listFinanceAccounts,
|
|
listFinancePeriods,
|
|
listJournalEntries,
|
|
postManualVoucher,
|
|
reopenFinancePeriod,
|
|
syncFinanceLedgerFromFacts,
|
|
} from '../../services/finance/finance-ledger-service.js'
|
|
import {
|
|
getFinanceReconciliation,
|
|
importFinanceReconciliation,
|
|
listFinanceReconciliations,
|
|
resolveFinanceReconciliation,
|
|
} from '../../services/finance/finance-reconciliation-service.js'
|
|
import type { JsonObject } from '../../types/json.js'
|
|
import { formatShanghaiDate } from '../../utils/finance-date-range.js'
|
|
import { bodyOf, idOf, sessionOf } from './request-helpers.js'
|
|
import { createJsonHandler, requireAdminPermission, requireAdminRoles } from './session.js'
|
|
|
|
const router = Router()
|
|
|
|
router.use('/finance', requireAdminRoles(['admin', 'operator', 'finance']))
|
|
|
|
router.get(
|
|
'/finance/ledger/accounts',
|
|
requireAdminPermission('finance.view'),
|
|
createJsonHandler(() => listFinanceAccounts(), {
|
|
successMessage: 'ok',
|
|
errorMessage: '读取科目表失败',
|
|
scope: '[admin/finance/ledger/accounts]',
|
|
}),
|
|
)
|
|
|
|
router.get(
|
|
'/finance/ledger/trial-balance',
|
|
requireAdminPermission('finance.view'),
|
|
createJsonHandler(
|
|
(req) => {
|
|
const query = req.query as Record<string, unknown>
|
|
const today = formatShanghaiDate(new Date())
|
|
return getTrialBalance({
|
|
dateFrom: String(query.dateFrom || query.date_from || `${today.slice(0, 7)}-01`),
|
|
dateTo: String(query.dateTo || query.date_to || today),
|
|
})
|
|
},
|
|
{
|
|
successMessage: 'ok',
|
|
errorMessage: '读取试算平衡失败',
|
|
scope: '[admin/finance/ledger/trial-balance]',
|
|
},
|
|
),
|
|
)
|
|
|
|
router.get(
|
|
'/finance/ledger/entries',
|
|
requireAdminPermission('finance.view'),
|
|
createJsonHandler((req) => listJournalEntries(req.query as JsonObject), {
|
|
successMessage: 'ok',
|
|
errorMessage: '读取总账凭证失败',
|
|
scope: '[admin/finance/ledger/entries]',
|
|
}),
|
|
)
|
|
|
|
router.post(
|
|
'/finance/ledger/sync',
|
|
requireAdminPermission('finance.post'),
|
|
createJsonHandler(
|
|
(req) => {
|
|
const body = bodyOf(req)
|
|
const today = formatShanghaiDate(new Date())
|
|
return syncFinanceLedgerFromFacts({
|
|
dateFrom: String(body.dateFrom || body.date_from || `${today.slice(0, 7)}-01`),
|
|
dateTo: String(body.dateTo || body.date_to || today),
|
|
})
|
|
},
|
|
{
|
|
successMessage: '总账已同步',
|
|
errorMessage: '同步总账失败',
|
|
scope: '[admin/finance/ledger/sync]',
|
|
audit: (req, data) => ({
|
|
action: 'finance_ledger_synced',
|
|
targetType: 'finance_ledger',
|
|
targetId: 'sales_fact',
|
|
data: data && typeof data === 'object' ? (data as JsonObject) : {},
|
|
}),
|
|
},
|
|
),
|
|
)
|
|
|
|
router.post(
|
|
'/finance/ledger/manual-voucher',
|
|
requireAdminPermission('finance.post'),
|
|
createJsonHandler((req) => postManualVoucher(bodyOf(req) as never, sessionOf(req)), {
|
|
successMessage: '凭证已创建',
|
|
errorMessage: '创建凭证失败',
|
|
scope: '[admin/finance/ledger/manual-voucher]',
|
|
audit: (req, data) => ({
|
|
action: 'finance_manual_voucher_created',
|
|
targetType: 'finance_journal_entry',
|
|
targetId: String((data as { entryId?: number })?.entryId || ''),
|
|
data: data && typeof data === 'object' ? (data as JsonObject) : {},
|
|
}),
|
|
}),
|
|
)
|
|
|
|
router.get(
|
|
'/finance/periods',
|
|
requireAdminPermission('finance.view'),
|
|
createJsonHandler(() => listFinancePeriods(), {
|
|
successMessage: 'ok',
|
|
errorMessage: '读取会计期间失败',
|
|
scope: '[admin/finance/periods]',
|
|
}),
|
|
)
|
|
|
|
router.post(
|
|
'/finance/periods/:periodKey/close',
|
|
requireAdminPermission('finance.close'),
|
|
createJsonHandler(
|
|
(req) => {
|
|
const body = bodyOf(req)
|
|
return closeFinancePeriod(
|
|
String((req.params as Record<string, string>).periodKey || ''),
|
|
sessionOf(req),
|
|
String(body.reason || ''),
|
|
)
|
|
},
|
|
{
|
|
successMessage: '期间已关账',
|
|
errorMessage: '关账失败',
|
|
scope: '[admin/finance/periods/close]',
|
|
audit: (req, data) => ({
|
|
action: 'finance_period_closed',
|
|
targetType: 'finance_period',
|
|
targetId: String((req.params as Record<string, string>).periodKey || ''),
|
|
data: data && typeof data === 'object' ? (data as JsonObject) : {},
|
|
}),
|
|
},
|
|
),
|
|
)
|
|
|
|
router.post(
|
|
'/finance/periods/:periodKey/reopen',
|
|
requireAdminPermission('finance.close'),
|
|
createJsonHandler(
|
|
(req) => reopenFinancePeriod(String((req.params as Record<string, string>).periodKey || '')),
|
|
{
|
|
successMessage: '期间已重新打开',
|
|
errorMessage: '反关账失败',
|
|
scope: '[admin/finance/periods/reopen]',
|
|
audit: (req) => ({
|
|
action: 'finance_period_reopened',
|
|
targetType: 'finance_period',
|
|
targetId: String((req.params as Record<string, string>).periodKey || ''),
|
|
data: {},
|
|
}),
|
|
},
|
|
),
|
|
)
|
|
|
|
router.get(
|
|
'/finance/reconciliations',
|
|
requireAdminPermission('finance.view'),
|
|
createJsonHandler((req) => listFinanceReconciliations(req.query as JsonObject), {
|
|
successMessage: 'ok',
|
|
errorMessage: '读取对账批次失败',
|
|
scope: '[admin/finance/reconciliations]',
|
|
}),
|
|
)
|
|
|
|
router.post(
|
|
'/finance/reconciliations/import',
|
|
requireAdminPermission('finance.reconcile'),
|
|
createJsonHandler((req) => importFinanceReconciliation(bodyOf(req), sessionOf(req)), {
|
|
successMessage: '账单已导入',
|
|
errorMessage: '导入账单失败',
|
|
scope: '[admin/finance/reconciliations/import]',
|
|
audit: (req, data) => ({
|
|
action: 'finance_reconciliation_imported',
|
|
targetType: 'finance_reconciliation',
|
|
targetId: String((data as { reconciliationId?: number })?.reconciliationId || ''),
|
|
data: data && typeof data === 'object' ? (data as JsonObject) : {},
|
|
}),
|
|
}),
|
|
)
|
|
|
|
router.get(
|
|
'/finance/reconciliations/:id',
|
|
requireAdminPermission('finance.view'),
|
|
createJsonHandler((req) => getFinanceReconciliation(idOf(req, 'id')), {
|
|
successMessage: 'ok',
|
|
errorMessage: '读取对账明细失败',
|
|
scope: '[admin/finance/reconciliations/detail]',
|
|
}),
|
|
)
|
|
|
|
router.post(
|
|
'/finance/reconciliations/:id/resolve',
|
|
requireAdminPermission('finance.reconcile'),
|
|
createJsonHandler(
|
|
(req) => {
|
|
const body = bodyOf(req)
|
|
return resolveFinanceReconciliation(idOf(req, 'id'), body.note, sessionOf(req))
|
|
},
|
|
{
|
|
successMessage: '对账批次已标记解决',
|
|
errorMessage: '标记对账解决失败',
|
|
scope: '[admin/finance/reconciliations/resolve]',
|
|
audit: (req) => ({
|
|
action: 'finance_reconciliation_resolved',
|
|
targetType: 'finance_reconciliation',
|
|
targetId: String(idOf(req, 'id') || ''),
|
|
data: {},
|
|
}),
|
|
},
|
|
),
|
|
)
|
|
|
|
export default router
|