大数跨境

SpringBoot+Vue3 业财协同架构:预算、费用分摊、发票、往来与凭证怎样避免五套数字

SpringBoot+Vue3 业财协同架构:预算、费用分摊、发票、往来与凭证怎样避免五套数字 企业软件源码
2026-10-20
6
导读:SpringBoot+Vue3 业财协同架构:预算、费用分摊、发票、往来与凭证怎样避免业财五套数字。

SpringBoot+Vue3 业财协同架构:预算、费用分摊、发票、往来与凭证怎样避免五套数字

预算剩余、报销总额、发票可用额、往来余额和凭证金额都“各自正确”,但放在一起却对不上,是企业系统最昂贵的一类数据问题。解决它的关键不是再加一张汇总表,而是让所有财务结果都能回到同一个业务事实。

▲ 多条业务河流先汇入统一事实账,再流向看板、对账和凭证。

引言:不是多做几个页面,而是统一事实

业财协同最容易陷入“模块都有,数字不通”的假完整:预算系统按部门统计,报销按申请人统计,发票按票面金额统计,往来按客户供应商统计,凭证又按会计科目统计。每套数字都有理由,却缺少共同的来源身份、账套、期间和金额方向。

真正的一体化不是把五张菜单放到同一个侧边栏,而是建立一条可追溯链:业务单据产生事实,预算与发票对事实做资源约束,往来记录结算责任,凭证把已确认事实翻译成会计分录。任何冲销、驳回和退款,都沿原来源反向处理。

常见做法
直接后果
本文主张
预算按申请额、报销按实付额
剩余额度与财务执行额长期漂移
区分预占、执行、释放三种动作
分摊比例各端自行计算
尾差落在不同部门或项目
服务端统一试算并指定尾差行
发票只存关联 ID
部分占用、退回、跨单核销无法解释
建立发票占用明细
往来与凭证异步生成
单据成功但凭证失败无人感知
凭证来源单独记录状态与错误
多账套只在页面筛选
跨账套串数据甚至串科目
所有事实带账套边界并在服务端校验

本文讨论的不是孤立按钮,也不会把前端、后端、表结构拆成三本说明书。我们先看产品闭环如何运转,再用 ER、时序和三段核心代码解释其中最关键的约束。文中的“现有实现”以当前系统与已提交能力为依据;需要因企业规则继续扩展的部分,会明确放在边界与对照中。

一、产品能力与特点

能力
主要角色
不止一张审批单的地方
预算控制
业务部门/财务
申请时预占,办结时执行,驳回时释放
费用分摊
经办人/财务
按部门、项目、费用科目拆分且总额守恒
发票池
财务
按来源占用、核销、释放,可查看剩余票额
往来台账
出纳/会计
按客户供应商沉淀应收应付与核销关系
凭证来源
会计
业务事件先排队,模板翻译后生成凭证
对账视图
负责人
从结果反查业务单、分摊、发票和凭证

这些能力的共同点,是每一步都留下可回查的业务身份、状态和时间。页面只是入口,真正决定系统可信度的是:上一环节的结果能否成为下一环节的明确输入;失败、撤回或重做时,能否沿原关系恢复,而不是人工改数。

二、业务流程怎么串

2.1 第一步:预算先回答“能不能花”,不替代真实费用

费用申请或报销提交时,系统按账套、预算期间、组织、项目和科目定位预算项。申请阶段写预占,审批办结后把预占转执行;驳回、撤回或作废只释放对应来源,不重算整张预算。

▲ 预算执行台账把预算、调整、占用、执行与余额放在同一口径。

2.2 第二步:分摊不是前端算完传结果,服务端必须再守恒

一张报销单可能分到多个部门和项目。前端试算负责交互,服务端以报销含税总额为权威,统一按比例或指定金额计算。四舍五入产生的分币尾差,应明确落到最后一条或最大金额行,不能让每个端各自决定。

▲ 报销详情同时呈现费用明细、分摊与审批上下文。

2.3 第三步:发票“被选中”不等于已经核销

保存草稿可以暂存选择,提交审批后才形成受约束的占用,办结或付款后再转为核销。驳回、撤销与删除要按来源释放。这样一张大额发票可以被多张费用单部分使用,剩余可用额也能准确计算。

▲ 选择发票时可核对票面金额、已占用和本次使用额。

2.4 第四步:凭证生成使用来源队列,不把会计工作塞进业务事务

业务办结后先登记凭证来源,保存业务类型、来源单号、账套、期间和金额摘要。凭证服务再按模板生成借贷分录。即使科目映射缺失,业务单据仍可办结,来源队列会标记失败并保留重试与人工处理入口。

▲ 凭证详情让借贷分录与业务来源可以双向追溯。

走完这条主线,可以发现列表页只是入口。真正重要的是规则配置、业务表单、详情轨迹和最终台账,它们分别回答“为什么这样算”“这次提交了什么”“谁做了什么”“结果落在哪里”。

三、设计怎么落地:思路、ER、时序与核心代码

3.1 先把关键决策写清楚

决策点
落地方式
为什么这样做
统一来源身份
sourceType + sourceId + sourceLineId
所有派生事实都能回到原业务行
金额使用十进制定点
BigDecimal 并统一精度与舍入
避免前后端浮点误差和累计尾差
资源动作显式化
预占、确认、释放分别记明细
状态变化可重放、可审计
账套边界下沉服务端
查询和写入都校验 accountSetId
防止只靠前端筛选造成串账
凭证异步但可感知
来源队列保存状态、错误和重试次数
业务成功与会计生成成功不再混为一谈

3.2 核心实体只围绕闭环建模

▲ ER 图只保留支撑本文主链路的实体;字典、组织和通用审计表不在这里展开。

关键字段
业务含义
source_type/source_id
统一业务来源身份
account_set_id
账套边界,所有财务事实必带
period_id
预算期间或会计期间
amount/direction
金额及借贷、收付方向
occupy_status
预占、确认、释放状态
share_dimension
部门、项目、费用科目等分摊维度
voucher_source_status
待生成、成功、失败、忽略
reversal_source_id
冲销指向的原事实

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 让核心写入可重试也不重复

发票占用锁定发票记录并按来源幂等写入,解决并发超占与重复提交。

@Transactional(rollbackFor = Exception.class)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 让回调与派生结果可追溯

业务办结只登记凭证来源并发布事件,避免科目模板异常回滚整个业务单据。

@Transactional(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 元;凭证借贷双方必须相等。四次守恒不能分别由四个页面“看起来差不多”来保证,而应在服务端用同一批准金额执行。

变化时点
预算动作
发票动作
往来与凭证结果
保存草稿
仅试算
仅选择或软占用
不生成往来与凭证来源
提交审批
预占 10,000
占用 10,000
按制度决定是否暂估或登记待处理来源
审批通过
预占转执行
占用转核销
形成应付 10,000 并登记凭证来源
驳回/撤回
释放原来源预占
释放原来源占用
不形成或冲回往来,取消待生成来源
后续付款
不重复执行
不重复核销
应付余额递减并生成付款类来源

如果第二张发票实际只能使用 3,800 元,系统应明确提示差额由无票费用承担、补票后再核销,或阻止提交,不能静默把 200 元尾差塞进第一张票。制度可以不同,但口径必须显式。

对账页应该能回答三个方向的问题

从业务单向下,要看到预算动作、发票占用、往来明细和凭证号;从凭证向上,要反查来源单与分摊维度;从预算或发票资源向外,要看到被哪些单据占用。只有三向都能追溯,财务人员才不需要导出五张 Excel 手工 VLOOKUP。

五、快速体验

在线演示地址:https://ruoyioffice.com/web,可使用演示页提供的账号进入。建议不要只看列表,而是沿着本文主线完整走一次:

  1. 预算先回答“能不能花”,不替代真实费用
  2. 分摊不是前端算完传结果,服务端必须再守恒
  3. 发票“被选中”不等于已经核销
  4. 凭证生成使用来源队列,不把会计工作塞进业务事务
资源
地址
适合谁
在线演示
https://ruoyioffice.com/web
产品、实施、技术负责人先验证交互与业务闭环
GitHub 源码
https://github.com/yuqing2026/ruoyi-office
需要阅读 Spring Boot、Vue3 与流程实现的开发者
Gitee 源码
https://gitee.com/yqzy1688/ruoyi-office
国内网络环境下拉取与对照代码

常见问题(FAQ)

统一业务事实是不是一张超级大表?

不是。统一的是来源身份、账套、期间和金额语义;预算占用、发票占用、往来和凭证仍是各自领域表。

预算占用应该在保存草稿时发生吗?

通常不建议。可在提交审批时预占,避免大量长期草稿冻结预算;具体策略可按企业管理强度配置。

分摊比例和金额能否混用?

可以,但服务端要归一成最终金额明细并校验总额守恒,不能只保存无法复算的前端显示值。

凭证必须与业务单据同一事务吗?

通常不必。先可靠登记凭证来源,再异步或人工生成凭证,更容易处理模板缺失和会计期间关闭。

结语

预算剩余、报销总额、发票可用额、往来余额和凭证金额都“各自正确”,但放在一起却对不上,是企业系统最昂贵的一类数据问题。解决它的关键不是再加一张汇总表,而是让所有财务结果都能回到同一个业务事实。 好的企业系统不会把复杂性藏在一条超长 SQL、一个万能表或一堆端侧 if/else 里,而是把业务日期、来源身份、状态迁移、版本和幂等边界设计成可观察、可验证的契约。

RuoYi Office 把流程、人力、财务、合同、项目、资产与移动端放在同一套企业管理平台中,适合用来验证本文的完整闭环,也便于开发者继续阅读 Spring Boot 3、Vue3、Flowable 与 UniApp 的实现。

如果这篇对你有用,点个「在看」或收藏。

【声明】内容源于网络
0
0
企业软件源码
RuoyiOffice 是一套基于 Spring Boot + Vue3 +Uniapp 的企业一体化管理平台,集 OA、CRM、ERP、工作流、HR、资产、合同、项目、AI应用等业务于一体,帮助企业用一个系统协同管理多类核心业务。
内容 161
粉丝 0
企业软件源码 RuoyiOffice 是一套基于 Spring Boot + Vue3 +Uniapp 的企业一体化管理平台,集 OA、CRM、ERP、工作流、HR、资产、合同、项目、AI应用等业务于一体,帮助企业用一个系统协同管理多类核心业务。
总阅读3.3k
粉丝0
内容161