SpringBoot+Vue3 业财协同架构:预算、费用分摊、发票、往来与凭证怎样避免五套数字
预算剩余、报销总额、发票可用额、往来余额和凭证金额都“各自正确”,但放在一起却对不上,是企业系统最昂贵的一类数据问题。解决它的关键不是再加一张汇总表,而是让所有财务结果都能回到同一个业务事实。
▲ 多条业务河流先汇入统一事实账,再流向看板、对账和凭证。
引言:不是多做几个页面,而是统一事实
业财协同最容易陷入“模块都有,数字不通”的假完整:预算系统按部门统计,报销按申请人统计,发票按票面金额统计,往来按客户供应商统计,凭证又按会计科目统计。每套数字都有理由,却缺少共同的来源身份、账套、期间和金额方向。
真正的一体化不是把五张菜单放到同一个侧边栏,而是建立一条可追溯链:业务单据产生事实,预算与发票对事实做资源约束,往来记录结算责任,凭证把已确认事实翻译成会计分录。任何冲销、驳回和退款,都沿原来源反向处理。
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
本文讨论的不是孤立按钮,也不会把前端、后端、表结构拆成三本说明书。我们先看产品闭环如何运转,再用 ER、时序和三段核心代码解释其中最关键的约束。文中的“现有实现”以当前系统与已提交能力为依据;需要因企业规则继续扩展的部分,会明确放在边界与对照中。
一、产品能力与特点
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
这些能力的共同点,是每一步都留下可回查的业务身份、状态和时间。页面只是入口,真正决定系统可信度的是:上一环节的结果能否成为下一环节的明确输入;失败、撤回或重做时,能否沿原关系恢复,而不是人工改数。
二、业务流程怎么串
2.1 第一步:预算先回答“能不能花”,不替代真实费用
费用申请或报销提交时,系统按账套、预算期间、组织、项目和科目定位预算项。申请阶段写预占,审批办结后把预占转执行;驳回、撤回或作废只释放对应来源,不重算整张预算。
▲ 预算执行台账把预算、调整、占用、执行与余额放在同一口径。
2.2 第二步:分摊不是前端算完传结果,服务端必须再守恒
一张报销单可能分到多个部门和项目。前端试算负责交互,服务端以报销含税总额为权威,统一按比例或指定金额计算。四舍五入产生的分币尾差,应明确落到最后一条或最大金额行,不能让每个端各自决定。
▲ 报销详情同时呈现费用明细、分摊与审批上下文。
2.3 第三步:发票“被选中”不等于已经核销
保存草稿可以暂存选择,提交审批后才形成受约束的占用,办结或付款后再转为核销。驳回、撤销与删除要按来源释放。这样一张大额发票可以被多张费用单部分使用,剩余可用额也能准确计算。
▲ 选择发票时可核对票面金额、已占用和本次使用额。
2.4 第四步:凭证生成使用来源队列,不把会计工作塞进业务事务
业务办结后先登记凭证来源,保存业务类型、来源单号、账套、期间和金额摘要。凭证服务再按模板生成借贷分录。即使科目映射缺失,业务单据仍可办结,来源队列会标记失败并保留重试与人工处理入口。
▲ 凭证详情让借贷分录与业务来源可以双向追溯。
走完这条主线,可以发现列表页只是入口。真正重要的是规则配置、业务表单、详情轨迹和最终台账,它们分别回答“为什么这样算”“这次提交了什么”“谁做了什么”“结果落在哪里”。
三、设计怎么落地:思路、ER、时序与核心代码
3.1 先把关键决策写清楚
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3.2 核心实体只围绕闭环建模
▲ ER 图只保留支撑本文主链路的实体;字典、组织和通用审计表不在这里展开。
|
|
|
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3.3 一条主时序比十个接口列表更容易验收
▲ 从一次用户或调度动作出发,串起端侧、领域服务、流程或账本,失败补偿在状态与幂等规则中处理。
3.4 把第一条关键规则收进服务端
服务端分摊试算保证明细之和严格等于单据总额,最后一行吸收分币尾差。
public List<ShareResult> calculate(BigDecimal total,List<ShareInput> inputs) {validatePercentTotal(inputs);BigDecimal allocated = BigDecimal.ZERO;List<ShareResult> results = new ArrayList<>();for (int i = 0; i < inputs.size(); i++) {ShareInput input = inputs.get(i);BigDecimal amount;if (i == inputs.size() - 1) {amount = total.subtract(allocated);} else {amount = total.multiply(input.percent()).divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP);allocated = allocated.add(amount);}results.add(new ShareResult(input.dimension(), amount));}return results;}
3.5 让核心写入可重试也不重复
发票占用锁定发票记录并按来源幂等写入,解决并发超占与重复提交。
public void occupyInvoice(Long invoiceId, Long billId, BigDecimal amount) {FinanceInvoiceDO invoice = invoiceMapper.selectByIdForUpdate(invoiceId);BigDecimal occupied = occupyMapper.sumActiveAmount(invoiceId);BigDecimal available = invoice.getAmount().subtract(occupied);if (available.compareTo(amount) < 0) {throw exception(INVOICE_AVAILABLE_AMOUNT_NOT_ENOUGH);}String sourceNo = "REIMBURSE:" + billId;FinanceInvoiceOccupyDO existing = occupyMapper.selectBySource(invoiceId, sourceNo);if (existing != null) {existing.setAmount(amount);occupyMapper.updateById(existing);return;}occupyMapper.insert(buildOccupy(invoiceId, sourceNo, amount));}
3.6 让回调与派生结果可追溯
业务办结只登记凭证来源并发布事件,避免科目模板异常回滚整个业务单据。
(rollbackFor = Exception.class)public void enqueueVoucherSource(FinanceBill bill) {String sourceKey = bill.getBillType() + ":" + bill.getId();if (voucherSourceMapper.existsBySourceKey(sourceKey)) {return;}FinanceVoucherSourceDO source = new FinanceVoucherSourceDO();source.setSourceKey(sourceKey);source.setAccountSetId(bill.getAccountSetId());source.setBusinessDate(bill.getBusinessDate());source.setAmount(bill.getApprovedAmount());source.setStatus(VoucherSourceStatus.PENDING.getStatus());voucherSourceMapper.insert(source);eventPublisher.publishEvent(new VoucherSourceCreatedEvent(source.getId(), source.getAccountSetId()));}
以上代码是围绕业务约束裁剪后的核心摘录,省略了权限注解、转换器、日志和通用异常包装。落地时仍应沿用项目现有的 Controller、Service、Mapper 分层,以及租户、数据权限和操作日志约定。
四、边界与对照:系统真正容易出错的地方
|
|
|
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
这里有一个通用判断:不要用“最终数值看起来对”替代过程可解释。企业系统迟早会遇到撤回、补录、跨期、重试和历史规则变化。只有保存来源、版本、状态动作和结果引用,系统才具备修复能力。
五、用一张 10,000 元报销单检验数字是否真的统一
假设一张报销单含税总额 10,000 元,属于 A 账套 2026 年 10 月期间;其中研发项目分摊 60%,市场项目分摊 40%。经办人选择两张发票,分别占用 6,000 元和 4,000 元;预算申请阶段已预占 10,000 元,审批通过后需要转为执行,随后生成借记费用、贷记其他应付款的凭证来源。
这里至少存在四次守恒:分摊明细合计必须等于 10,000 元;发票本次使用额合计必须等于允许核销的票额;预算预占减少 10,000 元的同时执行增加 10,000 元;凭证借贷双方必须相等。四次守恒不能分别由四个页面“看起来差不多”来保证,而应在服务端用同一批准金额执行。
|
|
|
|
|
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
如果第二张发票实际只能使用 3,800 元,系统应明确提示差额由无票费用承担、补票后再核销,或阻止提交,不能静默把 200 元尾差塞进第一张票。制度可以不同,但口径必须显式。
对账页应该能回答三个方向的问题
从业务单向下,要看到预算动作、发票占用、往来明细和凭证号;从凭证向上,要反查来源单与分摊维度;从预算或发票资源向外,要看到被哪些单据占用。只有三向都能追溯,财务人员才不需要导出五张 Excel 手工 VLOOKUP。
五、快速体验
在线演示地址:https://ruoyioffice.com/web,可使用演示页提供的账号进入。建议不要只看列表,而是沿着本文主线完整走一次:
-
预算先回答“能不能花”,不替代真实费用 -
分摊不是前端算完传结果,服务端必须再守恒 -
发票“被选中”不等于已经核销 -
凭证生成使用来源队列,不把会计工作塞进业务事务
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
常见问题(FAQ)
统一业务事实是不是一张超级大表?
不是。统一的是来源身份、账套、期间和金额语义;预算占用、发票占用、往来和凭证仍是各自领域表。
预算占用应该在保存草稿时发生吗?
通常不建议。可在提交审批时预占,避免大量长期草稿冻结预算;具体策略可按企业管理强度配置。
分摊比例和金额能否混用?
可以,但服务端要归一成最终金额明细并校验总额守恒,不能只保存无法复算的前端显示值。
凭证必须与业务单据同一事务吗?
通常不必。先可靠登记凭证来源,再异步或人工生成凭证,更容易处理模板缺失和会计期间关闭。
结语
预算剩余、报销总额、发票可用额、往来余额和凭证金额都“各自正确”,但放在一起却对不上,是企业系统最昂贵的一类数据问题。解决它的关键不是再加一张汇总表,而是让所有财务结果都能回到同一个业务事实。 好的企业系统不会把复杂性藏在一条超长 SQL、一个万能表或一堆端侧 if/else 里,而是把业务日期、来源身份、状态迁移、版本和幂等边界设计成可观察、可验证的契约。
RuoYi Office 把流程、人力、财务、合同、项目、资产与移动端放在同一套企业管理平台中,适合用来验证本文的完整闭环,也便于开发者继续阅读 Spring Boot 3、Vue3、Flowable 与 UniApp 的实现。
如果这篇对你有用,点个「在看」或收藏。

