feat(finance): 支持代录指定客服账户,并上线影子总账/对账/关账/快手平台退款
- 人工确认履约:管理员/财务/运营可传 fundAccountId / creditOwnerAdminUserId 指定客服备用金/积分账户;客服仍强制本人账户 - 新增迁移 112:finance_accounts / journal_entries / journal_lines / periods / reconciliations / reconciliation_items,种子 11 个科目 - 影子总账:销售事实按日生成收入/退款/成本凭证,支持手工凭证、试算平衡; 不接管钱包余额;总账同步接入定时任务与退款重算 - 期间关账/反关账:关账先补凭证并校验试算平衡,closed 期间禁止新增凭证 - 平台对账:JSON 账单导入,按 provider + platform_order_id 匹配订单/退款, 标记 matched/amount_mismatch/missing_order/missing_refund/duplicate - 平台退款:快手行业(91)同意退款 API 接入,成功后落 source=platform; 抖音/拼多多返回 501,保留手工退款 - 新增后台「财务 → 总账与对账」页面(总账/期间/对账三个 Tab) - 集成测试扩展到代录、总账平衡、关账阻断、手工凭证、对账匹配 验证:backend check 844(840 pass / 4 skip);frontend check 172 + build; 迁移 112 在 scratch 库全量执行并重跑幂等;本地生产副本已升级到 112, 销售事实同步 56 条凭证,借贷合计 1,885,351,864 = 1,885,351,864。
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
/**
|
||||
* 财务总账 / 期间关账 / 平台对账路由。
|
||||
*
|
||||
* 总账是影子账:只读写 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 { 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 = new Date().toISOString().slice(0, 10)
|
||||
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.config'),
|
||||
createJsonHandler(
|
||||
(req) => {
|
||||
const body = bodyOf(req)
|
||||
const today = new Date().toISOString().slice(0, 10)
|
||||
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.config'),
|
||||
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.config'),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
closeFinancePeriod(
|
||||
String((req.params as Record<string, string>).periodKey || ''),
|
||||
sessionOf(req),
|
||||
),
|
||||
{
|
||||
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.config'),
|
||||
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.config'),
|
||||
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.config'),
|
||||
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
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from '../../services/admin/write/order-actions.js'
|
||||
import {
|
||||
createManualRefund,
|
||||
createPlatformRefund,
|
||||
listOrderRefunds,
|
||||
voidOrderRefund,
|
||||
} from '../../services/finance/order-refund-service.js'
|
||||
@@ -103,6 +104,42 @@ router.get(
|
||||
),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/orders/:orderId/refunds/platform',
|
||||
requireAdminPermission('finance.config'),
|
||||
createJsonHandler(
|
||||
(req) => {
|
||||
const body = req.body as Record<string, unknown>
|
||||
return createPlatformRefund(
|
||||
{
|
||||
orderId: String((req.params as AdminOrderRouteParams).orderId || ''),
|
||||
orderItemId: body?.orderItemId as string | undefined,
|
||||
amountFen: body?.amountFen as string | undefined,
|
||||
platformRefundId: String(body?.platformRefundId || ''),
|
||||
reason: body?.reason as string | undefined,
|
||||
note: body?.note as string | undefined,
|
||||
refundedAt: body?.refundedAt as string | undefined,
|
||||
sellerId: body?.sellerId as string | undefined,
|
||||
refundHandingWay: body?.refundHandingWay,
|
||||
desc: body?.desc as string | undefined,
|
||||
},
|
||||
req.adminSession || null,
|
||||
)
|
||||
},
|
||||
{
|
||||
successMessage: '平台退款已提交并登记',
|
||||
errorMessage: '平台退款失败',
|
||||
scope: '[admin/orders/:orderId/refunds/platform]',
|
||||
audit: (req, data) => ({
|
||||
action: 'order_platform_refund_created',
|
||||
targetType: 'order',
|
||||
targetId: String((req.params as AdminOrderRouteParams).orderId || ''),
|
||||
data: data && typeof data === 'object' ? (data as Record<string, unknown>) : {},
|
||||
}),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/orders/:orderId/refunds',
|
||||
requireAdminPermission('finance.config'),
|
||||
|
||||
Reference in New Issue
Block a user