SpringBoot+Vue3 凭证生命周期设计:审核、过账、冲销与反过账怎样保持账目一致
本文由 RuoYi Office 团队整理,结合实际项目分享企业软件的设计思路与开发经验。
🌐 文档地址 :https://ruoyioffice.com👇 文章底部获取源码和演示地址 👇
💬 :17156169080(获取产品咨询)
凭证状态说明谁确认了什么,账簿余额说明哪笔分录真正生效。两者必须由同一套操作规则连接。

图1|imagegen独立生成的1500×1000概念图。500元与图内日期均为假设,不是系统记录;"原凭证保持留痕"表示保留分录并建立红冲关联,原单红冲标记仍会更新。图中并发提示是验收边界。
一、一张凭证审核了,为什么账簿还没有变化
会计录了一张500元费用凭证,主管完成审核。
业务同事打开科目余额表,发现费用仍未增加,以为系统漏记了。
另一个同事看到"已过账",想直接把500改成450,又被系统拦住。
这两个现象其实来自同一条设计:确认凭证与使分录入账是两个不同动作,入账后的更正需要显式撤回或补充记录。
在RuoYi Office中,可以从凭证录入进入详情,查看分录与借贷合计;从账簿查询看余额,再从财务设置的审计日志追查操作。
这条路径适合会计日常核对,也适合Java团队审查生命周期实现。
本文基于2026年11月1日当前工作树的Spring Boot 3体系源码。
本地5800/48080运行环境使用既有资产教程包,未从当前源码重新构建。
因此,"源码执行链"和"现有界面展示"分别记录,不拿它们互相替代。
| 读者 | 需要看清的事 | 本篇交付 |
|---|---|---|
| 会计与实施 | 哪个操作影响账、哪个操作只改变确认状态 | 状态机与操作表 |
| 后端开发 | 分录、余额、来源、审计如何共同提交 | 事务时序与真实代码 |
| 测试与交付 | 重复、并发、中途失败如何核对 | 独立金额模型与验收清单 |
第10篇已经讨论账套、科目与期间,第12篇已经讨论来源与模板取数。
这里从"凭证已经存在"开始,聚焦它怎样进入账面,以及错账怎样留下可以解释的更正链。
二、先把状态与红字标记分开
暂存、已审核、已过账、已作废是凭证状态;普通、红字、已被红冲是另一组标记。
原凭证被红冲以后仍是已过账,原分录继续留在历史记录中。
新增的红字凭证也是已过账,靠负金额抵减,而不是把原状态改成"作废"。
| 操作 | 起点与终点 | 余额变化 | 主要约束与留痕 |
|---|---|---|---|
| 保存 | 新建→暂存1 | 无 | 组装校验,保存主表与分录,CREATE日志 |
| 审核 | 暂存1→已审核2 | 无 | 账套启用、期间可记账;人工入口可要求制单审核分离 |
| 反审核 | 已审核2→暂存1 | 无 | 期间可记账;UN_AUDIT日志 |
| 过账 | 已审核2→已过账3 | 增加分录借贷发生额 | 账套启用、期间可记账;POST日志 |
| 反过账 | 已过账3→已审核2 | 撤回这张凭证的增量 | 原单不能已被红冲;期间可记账;UN_POST日志 |
| 作废 | 未过账且未作废→已作废4 | 无 | 结转凭证另有禁止;保留号码与来源关联 |
| 红冲 | 原单保持已过账,新增已过账红字 | 原方向发生额取负 | 原单须已过账、非红字、未被红冲;校验红字日期所在期间 |
| 修改/删除 | 仅暂存 | 无 | 修改还限制来源凭证;结转凭证禁止手工改删 |
这张表表达源码规则,不是本次已点击通过的操作清单。
尤其作废入口没有统一调用期间可记账校验,不能将其他操作的期间限制自动套到它身上。

状态图|按当前服务整理。原单红冲时保留已过账,另建红单;Mermaid源文件存于过程目录。
状态图中红冲的自环指原单状态保持已过账,并不表示没有新增记录。
反过账也只退到已审核,若要修改手工凭证,还要先反审核。
状态图与主图共同表达状态变化;精简ER进一步解释字段关系。

图2|本次只读查看既有凭证id=32,2026-08转-0001,借贷合计各500。列表观察为已过账、普通,来源FI504-2026090600008;详情只证明现有展示,不是本文执行审核过账的结果,也不是报销id=3的成功链。
审核确认责任,不提前累计发生额
人工审核先确认暂存态,再检查账套、期间与制单审核分离。
审核方法没有调用余额服务,已审核不会自动改变余额。
这让财务可以在复核完成后单独决定何时过账。
源码摘录1:FinanceVoucherServiceImpl.java,359---382行,连续原文;完整路径与SHA-256见来源审计。
java
private void doAudit(Long id, boolean skipSeparation) {
FinanceVoucherDO voucher = validateVoucherExists(id);
if (!FinanceVoucherStatusEnum.DRAFT.getStatus().equals(voucher.getStatus())) {
throw exception(VOUCHER_STATUS_FORBIDDEN);
}
FinanceBookDO book = bookValidator.validateEnabled(voucher.getBookId());
periodService.validatePostable(book.getId(), voucher.getVoucherDate());
Long userId = SecurityFrameworkUtils.getLoginUserId();
// 制单审核分离是内控要求,账套级可关;系统自动生成的凭证不受此限
if (!skipSeparation && Boolean.TRUE.equals(book.getMakerAuditorSeparate())
&& ObjUtil.equals(userId, voucher.getMakerId())) {
throw exception(VOUCHER_MAKER_AUDITOR_SAME);
}
voucherMapper.updateById(FinanceVoucherDO.builder()
.id(id)
.status(FinanceVoucherStatusEnum.AUDITED.getStatus())
.auditorId(userId)
.auditorName(SecurityFrameworkUtils.getLoginUserNickname())
.auditTime(LocalDateTime.now())
.build());
auditLogService.log(voucher.getBookId(), voucher.getCompanyId(), AUDIT_BIZ_TYPE, id,
voucher.getVoucherNoText(), "AUDIT",
StrUtil.format("审核凭证 {}", voucher.getVoucherNoText()));
}
这里的skipSeparation有实际含义:自动审核和红冲内部审核会传入true。
它跳过的是制单人与审核人相同的限制,并没有跳过账套和期间校验。
不能因此宣传"所有审核都强制双人分离"。
模板组合也需要按控制流判断:
| autoAudit | autoPost | 生成后的状态 |
|---|---|---|
| false | false | 暂存 |
| false | true | 暂存,未进入自动审核调用 |
| true | false | 已审核,未过账 |
| true | true | 自动审核后继续过账 |
核对位置是模板服务315---318行,以及凭证服务630---635行。
来源队列标为"已生成"也不等于"已过账":即使凭证停在暂存或已审核,生成服务仍会标记来源已生成,防止顺序重复制证。
三、过账要同时改变余额与凭证状态
过账的业务承诺是:成功时余额包含本张分录,凭证也处于已过账;失败时不留下半张生效的账。
代码顺序是先校验,后调用余额服务,再改凭证状态、更新来源、写审计。
这些写入需要在正常Spring代理调用和同一事务资源下共同提交。
源码摘录2:FinanceVoucherServiceImpl.java,410---429行,连续原文;完整路径与SHA-256见来源审计。
java
private void doPost(Long id) {
FinanceVoucherDO voucher = validateVoucherExists(id);
if (FinanceVoucherStatusEnum.POSTED.getStatus().equals(voucher.getStatus())) {
throw exception(VOUCHER_ALREADY_POSTED);
}
if (!FinanceVoucherStatusEnum.AUDITED.getStatus().equals(voucher.getStatus())) {
throw exception(VOUCHER_NOT_AUDITED);
}
bookValidator.validateEnabled(voucher.getBookId());
periodService.validatePostable(voucher.getBookId(), voucher.getVoucherDate());
List<FinanceVoucherItemDO> items = voucherItemMapper.selectListByVoucherId(id);
balanceService.post(voucher, items);
voucherMapper.updateById(FinanceVoucherDO.builder()
.id(id)
.status(FinanceVoucherStatusEnum.POSTED.getStatus())
.posterId(SecurityFrameworkUtils.getLoginUserId())
.posterName(SecurityFrameworkUtils.getLoginUserNickname())
.postTime(LocalDateTime.now())
.build());
这段连续源码展示余额与状态的相邻关系。
后续430---436行更新来源并写POST日志;对外postVoucher在404---408行声明回滚事务。
余额服务的post也有事务声明,默认传播加入外层事务,没有声明独立提交。

图3|按源码整理的事务时序示意,原始Mermaid保存在过程目录。尚未对产品执行余额失败、日志失败或数据库断连注入。
时序上,余额先写并不意味着其他请求应该提前看到未提交的中间状态。
实际可见性还取决于数据库隔离与事务配置,本次没有连接业务库检查隔离级别。
同样,单凭@Transactional存在,不能宣布跨服务资源、代理入口与所有失败路径已经验收。
同一张凭证内,先汇总再更新
余额服务先把分录归并成科目增量。
每个末级科目的祖先链一并累计,辅助余额按科目与辅助键另行归并。
反过账复用这条路径,只把每条金额再取一次负值。
源码摘录3:FinanceBalanceServiceImpl.java,81---103行,连续原文;完整路径与SHA-256见来源审计。
java
// 1. 先把分录汇总成「科目 → 借贷增量」,同一科目在一张凭证里出现多次只更新一次
Map<Long, Delta> subjectDeltas = new LinkedHashMap<>();
Map<String, AuxDelta> auxDeltas = new LinkedHashMap<>();
for (FinanceVoucherItemDO item : items) {
BigDecimal amount = FinanceBalanceUtils.nullToZero(item.getAmount());
if (reverse) {
amount = amount.negate();
}
// 祖先链一起累加,父科目余额就等于下级汇总,账簿查询不必递归
for (FinanceSubjectDO subject : resolveChain(context, item.getSubjectId())) {
subjectDeltas.computeIfAbsent(subject.getId(), id -> new Delta(subject))
.add(item.getDirection(), amount);
}
if (StrUtil.isNotBlank(item.getAuxKey())) {
FinanceSubjectDO subject = context.subjects.get(item.getSubjectId());
if (subject != null) {
auxDeltas.computeIfAbsent(item.getSubjectId() + "#" + item.getAuxKey(),
key -> new AuxDelta(subject, item.getAuxKey(), item.getAuxData()))
.add(item.getDirection(), amount);
}
}
}
随后105---115行落库,科目与辅助各自执行"确保余额行存在、SQL增量、向后重算"。
科目增量用debit_amount = debit_amount + (...)和贷方同类表达式,避免不同凭证更新同一余额时简单覆盖发生额。
首次建行会捕获唯一键冲突,schema基线也包含科目、辅助余额维度唯一索引。
但这些机制只解决了部分问题:余额行增量不覆盖,不等于同一凭证只累计一次。
后续重算会读取已有期间余额并按ID更新期初、累计与期末;跨期并发补录、不同辅助更新顺序仍应集成验证。

图4|精简逻辑ER。余额是分录按维度归并的结果,图中连接不代表全部存在物理外键。红冲关系与业务来源关系必须分别保存。
| 设计决策 | 当前实现 | 为什么这样组织 | 尚需验证 |
|---|---|---|---|
| 状态与红字独立 | status与redFlag分字段 | 已过账原单和红单都能保留 | 红字撤回后的关联闭合 |
| 分录留存 | 主表与分录分离,过账后禁止直接修改 | 账簿能回查原始记录 | 并发修改与过账的互斥 |
| 余额增量 | 科目祖先链、末级辅助组合 | 支撑科目与辅助两种核对 | 并发重算、死锁与重试 |
| 期间保护 | 日期推导期间并校验可记账 | 防止正常入口改已关闭期间 | 与结账并发的统一协议 |
| 来源独立 | sourceQueueId与来源业务ID | 解释凭证从哪张单产生 | 指针重置后的再次生成 |
| 审计同链 | 操作人与动作、说明落库 | 保留操作责任 | 全量前后快照、失败尝试留存 |
四、用一组独立数字,复算过账与更正
下面全部是独立假设,不取自截图账套,也没有写入任何财务数据库。
假设8月存在一张费用确认凭证V1:借费用500,贷其他应付款500。
两个科目期初均为零,不考虑税、付款、结转与其他业务。
这只是生命周期案例,不重复第12篇模板如何取数。
同期撤回与重新过账
| 阶段 | V1状态 | 费用借方累计 | 应付款贷方累计 | 费用期末 | 应付款期末 |
|---|---|---|---|---|---|
| 保存 | 暂存 | 0 | 0 | 0 | 0 |
| 审核 | 已审核 | 0 | 0 | 0 | 0 |
| 第一次过账 | 已过账 | 500 | 500 | 借500 | 贷500 |
| 反过账 | 已审核 | 0 | 0 | 0 | 0 |
| 再次过账 | 已过账 | 500 | 500 | 借500 | 贷500 |
反过账撤回的是本张凭证对余额的贡献,不是把整个科目的余额清零。
如果还有另一张费用200元已过账,撤回V1后应保留那200元。
父科目与对应辅助组合也应撤回同一增量,不能只改总账。
余额统一可以按"借为正、贷为负"理解:
text
带符号期末 = 带符号期初 + 本期借方发生 - 本期贷方发生
费用:0 + 500 - 0 = +500(借)
应付款:0 + 0 - 500 = -500(贷)
反过账后:两条对应发生额各减回500,余额贡献回到0
表中的期末是"金额+方向"展示,不是让所有数据库余额字段都存负数。
源码通过FinanceBalanceUtils在带符号计算与金额、方向字段之间转换。
父子科目不能全层相加,否则会把同一笔发生额重复统计。
同月红冲与正确凭证
假设V1已过账后发现应记450元,采用"全额红冲,再记正确凭证V2"的方案。
该方案与原单改金额的区别,在于原500与红字-500均可追溯。
| 记录 | 费用借方 | 应付款贷方 | 说明 |
|---|---|---|---|
| 原凭证V1 | +500 | +500 | 继续保留已过账分录 |
| 红字R1 | -500 | -500 | 保留原方向,金额取负 |
| 正确凭证V2 | +450 | +450 | 需按来源或手工规则重新建立 |
| 同月净发生 | 450 | 450 | 500-500+450,可独立复算 |
这不是把借贷方向互换:换成贷费用500、借应付500也能影响净余额,但会改变借贷发生额的列口径。
当前产品红冲明确采用原方向负金额。
因此验收时除了净余额,还要比对借方、贷方发生额及红冲关系。
跨月红冲不会抹去原月
如果8月已经关闭,红冲日期指定为可记账的9月:8月费用借方仍为500,9月费用借方为-500。
假设9月没有其他发生,费用的9月期初为借500,期末回到零。
年度净发生归零,8月原始发生仍然存在。
红冲请求未指定日期时,源码默认使用原凭证日期。
原月已关闭则默认日期会被期间校验拦截,不能宣传系统会自动寻找当前开放月份。
要在制度允许的更正日期明确操作,本次只分析规则,没有执行跨期红冲。
五、更正要保留原单、红字与来源三种关系
红冲不是修改原单,也不是解除原始业务单据的事实责任。
原凭证保留sourceType、sourceId、sourceQueueId;红字复制这些来源,并用redSourceId指回原凭证。
成功后原单写入redTargetId,红字标记变为"已被红冲"。
红字分录复制业务坐标,金额取负
代码保留科目、方向、辅助组合、币种、汇率与现金流项目。
本币金额、原币金额和数量取负,单价保留。
这些字段决定红冲是否真正落到原来的核算位置。
源码摘录4:FinanceVoucherServiceImpl.java,543---565行,连续原文;完整路径与SHA-256见来源审计。
java
for (FinanceVoucherItemDO originItem : originItems) {
FinanceVoucherItemDO redItem = FinanceVoucherItemDO.builder()
.seq(originItem.getSeq())
.summary(StrUtil.format("红冲:{}", StrUtil.blankToDefault(originItem.getSummary(), reason)))
.subjectId(originItem.getSubjectId())
.subjectCode(originItem.getSubjectCode())
.subjectName(originItem.getSubjectName())
.direction(originItem.getDirection())
// 红字凭证保持原方向、金额取负,账簿上体现为对应方向的冲减
.amount(originItem.getAmount().negate())
.auxKey(originItem.getAuxKey())
.auxData(originItem.getAuxData())
.currencyCode(originItem.getCurrencyCode())
.exchangeRate(originItem.getExchangeRate())
.origAmount(originItem.getOrigAmount() == null ? null : originItem.getOrigAmount().negate())
.quantity(originItem.getQuantity() == null ? null : originItem.getQuantity().negate())
.price(originItem.getPrice())
.cashFlowItem(originItem.getCashFlowItem())
.build();
assembly.items.add(redItem);
}
insertWithVoucherNo(assembly, red);
saveItems(assembly, red);
红冲不会按最新科目映射重新解释原分录。
这对追溯有价值,也意味着"原科目被停用或辅助对象变更后怎样处理"应纳入隔离环境测试。
红冲入口直接复制分录,不能把手工组装时的全部科目有效性校验当成红冲已经重复执行。
建红单、过账和标记原单要共同成功
入口先建红字暂存凭证并保存分录,再自动审核过账,最后更新原单关联与RED审计。
它不把红字半成品作为正常成功结果留给用户。
源码摘录5:FinanceVoucherServiceImpl.java,567---581行,连续原文;完整路径与SHA-256见来源审计。
java
// 红冲的目的就是把账冲平,因此红字凭证直接审核并过账,不留半成品状态
doAudit(red.getId(), true);
doPost(red.getId());
voucherMapper.updateById(FinanceVoucherDO.builder()
.id(origin.getId())
.redFlag(FinanceVoucherRedFlagEnum.REVERSED.getFlag())
.redTargetId(red.getId())
.build());
auditLogService.log(book.getId(), book.getCompanyId(), AUDIT_BIZ_TYPE, origin.getId(),
origin.getVoucherNoText(), "RED",
StrUtil.format("红冲凭证 {},生成红字凭证 {},原因:{}", origin.getVoucherNoText(),
red.getVoucherNoText(), reason));
return red.getId();
}
红冲方法497---499行声明事务;内部doAudit、doPost是同对象私有调用,依赖外层事务,而不是各自重新开事务。
如果过账或日志异常向外传播,预期回滚红单、余额、来源与原单标记。
这里仍是源码上的原子性目标,本次没有做数据库故障注入来证明实际部署满足它。
来源队列的当前指针不是完整历史
过账会调用markVoucherGenerated把来源指针更新到当前凭证。
红字复制sourceQueueId并执行过账,来源指针因此也可能指向红单。
业务侧撤销的另一路径在红冲后把来源重置待生成;直接红冲入口没有同样的最终重置。
selectBySourceQueueId按ID倒序取最新凭证,不筛普通或红字。
所以"按来源找凭证"不一定永远拿到原单。
追溯原单、红字和新正确凭证,应分别检查双向红冲ID、来源身份及来源当前指针。
反过账同样会把来源标成待生成,而原凭证仍存在且是已审核。
如果随后重新生成来源,再对旧凭证重新过账,是否会出现两个有效结果,不能只看队列状态判断。
生成入口按来源状态防重,没有等价于"所有历史凭证中只允许一个生效结果"的完整证明。
这是隔离环境应优先验证的组合路径,不是本次真实账套已发生重复的结论。
红字反过账是额外的验收点
当前反过账禁止的是"已被红冲"的原凭证,对redFlag为RED的已过账凭证没有同样拦截。
如果红字被反过账,余额会撤回负增量;但这段路径没有同步清理原单redTargetId与已被红冲标记。
从算术看,原500与红-500本已抵零,撤回红字后净额又回到500。
这说明"先处理红字凭证"的注释不等于完整恢复链已经闭合。
必须在隔离环境核对后续能否再次红冲、来源指向谁、原单如何解除关联,制定明确的更正制度。
本文不为写稿修产品,也不会把这条路径描述成已验收的标准操作。
六、账簿与审计,要各自证明一件事
凭证详情证明分录内容,余额表证明聚合结果,审计证明已记录的操作责任。
三者不能互相替代,尤其不能用一条POST日志代替余额对账。

图5|本次只读查看P1操作学习账2026-08余额表:2241贷方500,5602及其末级560203借方500,末级合计500/500。它与图2处于同账套期间,属于既有结果核对,不是本次执行过账的前后对比,也未验全部账簿报表。
余额表主要从科目余额表读取;明细类账簿通过selectPostedList选择已过账凭证,再取分录。
两条取数路径应能按同账套、同期间、同科目与辅助口径核对。
凭证汇总服务则会汇总未作废凭证,不只已过账,不能直接把凭证汇总金额当作已过账余额。

图6|本次只读查看既有POST日志,业务对象转-0001,借贷合计500,操作时间2026-09-06 21:51:04。响应bizId=32可关联图2既有凭证;这是历史记录,不是本次执行过账或红冲的成功证据。前后快照显示"无",未人为补造冲销记录。
审计服务有beforeData和afterData字段,但凭证生命周期调用的是简化重载,前后数据传入null。
现有证据足以说明"保存动作、说明、操作者、IP和时间",不足以说明"每次分录修改都有完整前后快照"。
需要完整更正复盘时,应把原凭证分录、红单分录、关联ID与业务材料一起保留。
同事务写审计还有另一面:失败导致事务回滚时,成功动作日志也会回滚。
不能因此推断失败尝试自动有一条永久财务审计记录。
失败接口日志、业务告警与审计日志是不同证据来源,需要分别设计与验收。
七、重复请求要分顺序与并发验证
状态检查能拒绝已完成操作的顺序重试,但不能天然阻止两个请求同时通过旧状态。
当前doPost先普通selectById,再增余额,最后updateById。
本路径未见凭证行锁、带旧状态条件的原子抢占、版本字段或统一操作幂等键。
凭证号唯一索引约束的是号码,不是同一凭证过账次数。
| 场景 | 当前源码判断 | 可能的问题 | 验收目标 |
|---|---|---|---|
| 已过账后再次过账 | 报已过账 | 返回错误而非同结果成功 | 余额不再增加 |
| 反过账后再次反过账 | 报未过账 | 顺序重试被拒 | 余额不再减少 |
| 红冲完成后再次红冲原单 | 已被红冲标记拦截 | 并发时两个请求可能先读旧标记 | 只产生一张有效红单 |
| 同凭证并发过账 | 两者可能都读到已审核 | SQL增量可能各加500 | 状态与余额只生效一次 |
| 过账与反审核并发 | 都可能先通过旧状态 | 状态与金额可能错位 | 相互串行、最终结果可解释 |
| 过账与结账并发 | 先校验期间,后写账 | 校验后期间可能变化 | 使用共同的期间并发协议 |
| 来源再生成与旧单再过账 | 来源待生成,旧单仍存在 | 可能形成两个生效结果 | 同源有效结果政策明确 |
结账服务确实存在按账套的分布式锁;本次核对的凭证过账路径没有参与同一个锁。
因此不能从"结账有锁"推导"结账与所有记账操作已经互斥"。
这些是从控制流得到的候选风险,不是对现有业务库压测所得的故障报告。
text
独立交错模型,单位元:
A读取凭证=已审核;B也读取凭证=已审核
A累加借500、贷500;B再累加借500、贷500
A写已过账;B也写已过账
最终状态只有一个"已过账",发生额却可能累计1000
可选改进思路是在隔离分支中统一凭证与期间的加锁顺序,或用带预期状态的原子更新抢占操作资格。
若使用条件更新,必须检查影响行数,并让抢占、余额、状态、来源、日志处于同一事务。
还需要明确定义超时重试:返回原结果,还是返回"已处理",不能让客户端靠报错猜余额。
这些是二开建议,本文没有把它们加进产品代码。
本地模型实际验了什么
本次运行model-checks.mjs,使用整数分、自建对象与断言完成15项检查。
它没有加载产品服务,没有Spring、MyBatis、Redis或数据库。
因此"全部通过"只代表模型中的算术和指定交错符合预期。
| 检查组 | 项数 | 结果与含义 |
|---|---|---|
| 暂存、审核、过账及顺序重复 | 5 | 余额只在模型首次合法过账增加 |
| 反过账、重过账、同月和跨月红冲、更正450 | 5 | 加减可复算,跨月保留原发生 |
| 银行期末与跨年累计 | 2 | 余额承接与本年累计重置分开 |
| 并发旧状态、事务故障、红字撤回风险 | 3 | 说明风险交错与目标,不是产品集成通过 |
可复查的运行记录是过程目录model-validation.json。
源码连续摘录另有逐字、行号与SHA-256校验,不拿自建模型冒充原始产品测试。
八、把失败路径写进交付验收
采购验收应检查操作后的状态、发生额、余额、来源和日志,而不只看按钮提示成功。
以下写操作均应在独立账套或隔离数据库执行,本篇没有在现有财务数据库执行它们。
| 验收项 | 输入或故障 | 应核对的结果 | 本次证据 |
|---|---|---|---|
| 审核不入账 | 暂存500→审核 | 状态2,发生额不变 | 源码+独立模型 |
| 制单审核分离 | 相同用户人工审核 | 开启分离时拒绝,无金额变化 | 源码;未角色实操 |
| 首次过账 | 已审核500 | 状态3,科目/父级/辅助各增对应500 | 源码+独立模型 |
| 顺序重复 | 同ID连续过账两次 | 第二次拒绝,发生额仍500 | 源码+独立模型 |
| 同ID并发 | 两请求同时过账 | 只记一次,日志与来源可解释 | 候选缺口;未产品压测 |
| 期间关闭 | 关闭期审核或过账 | 校验拒绝,无余额写入 | 源码;未写库验证 |
| 中途失败 | 辅助余额或审计插入失败 | 主表、各余额、来源共同回滚 | 时序核对;未故障注入 |
| 同期反过账 | 已过账500撤回 | 状态2;只撤本张,其他凭证不受影响 | 源码+独立模型 |
| 红冲 | 原500→红-500 | 双向ID、红标记、原方向负额、净零 | 源码+独立模型 |
| 跨期红冲 | 8月500,9月-500 | 原月保留;9月发生及期末正确 | 源码+独立模型 |
| 红字再撤回 | 反过账红单 | 原单标记、关联及来源状态闭合 | 候选缺口;未实操 |
| 来源再生成 | 撤回后旧单仍在 | 有效结果不重复,历史能追溯 | 组合路径待验 |
| 批量部分失败 | 合法与非法凭证混合 | 逐张事务与成功/失败列表一致 | 源码;未批量操作 |
| 账簿勾稽 | 同账套期间科目辅助 | 已过账明细与余额同口径一致 | 只读展示;未全量勾稽 |
批量审核、过账通过self代理逐张调用,单张事务分开。
但批量循环只捕获ServiceException;数据库等其他异常可能终止后续处理,之前成功的张数可能已提交。
客户端遇到整批请求异常,不能直接假设整批全部没做,需要逐张查询结果。
反审核、反过账代码将审核人或过账人字段设置为null再updateById。
实际数据库是否清空,还取决于MyBatis字段更新策略;相关字段未见显式ALWAYS配置,本次未执行SQL核验。
不能仅凭builder里写了null,就承诺界面上旧操作人时间一定清除。
原始操作留痕应以审计为依据,同时测试状态字段与展示字段的一致性。
P1学习账套2026-08曾出现报表"不平衡,差额-500.00",损益未结转也阻断月结。
这些现象本篇保留未定性,没有改库或修产品,也没有裁掉提示来制造通过证据。
一张凭证借贷平衡,只证明该凭证合计相等,不能证明报表、结转与月结全部通过。
九、按只读路线体验,再决定隔离验收范围
在线体验入口见文末演示地址,演示账号按页面提示使用。
本地已启动的5800环境,本次使用开发演示账号admin,只读取现成数据。
可以按下面顺序核对页面:
- 凭证管理→凭证录入,先确认账套和期间。
- 打开既有详情,核对分录方向、金额和借贷合计。
- 账簿查询→科目余额表,核对同口径期间与科目层级。
- 财务设置→审计日志,查看已有动作详情与说明。
- 记录来源号和红冲关联;没有记录时转入隔离验收清单。
本篇不建议为了跟文中步骤而在现有账套点审核、过账、红冲、反过账或结账。
开发接手时可按实际交付说明启动后端与PC端;命令只作定位,本次没有重启共享服务。
powershell
cd E:\ruoyi-office\ruoyi-office
mvn -P boot -DskipTests compile
# 按交付配置启动 yudao-server
# 另一个终端启动 PC 管理端
cd E:\ruoyi-office\ruoyi-office-vben
pnpm dev:antd
财务源码属于实际商业交付范围,公开仓库不代表所有财务模块都包含或可以免费商用。
对Java团队,源码的价值是可以定位真实事务边界、扩展更正政策并建立回归样例;这些工作需要与授权范围和交付清单一起确认。
FAQ
1. 审核凭证以后,科目余额为什么没有增加?
审核确认凭证,过账才累计余额。当前审核方法只更新状态、审核人与日志;必须完成过账,分录才进入已过账明细和余额更新路径。
2. 反审核、反过账和红冲有什么区别?
反审核把已审核退回暂存,不动余额;反过账撤回原增量并退回已审核;红冲保留原已过账记录,新增同方向负金额凭证,建立原单与红单关联。不同路径的期间与来源策略要分别验收。
3. 已过账凭证可以直接改金额吗?
当前修改入口只允许暂存,来源生成凭证还禁止直接改分录。手工凭证若制度允许,可在开放期间先反过账再反审核;已关闭期间通常需要按允许日期红冲并建立正确记录,不能绕开期间规则直接改库。
4. 加了事务和SQL自增,是否就不会重复过账?
不能这样推导。事务处理一次操作的共同提交,SQL自增防止增量覆盖;同一凭证的两个并发请求还需要共同的准入保护。本篇发现候选缺口,未完成产品并发压测。
5. 有审计日志,是否表示每次修改都有完整快照?
不一定。当前凭证调用简化日志接口,前后数据为空,主要记录动作、说明、用户与时间。完整更正追溯还应保留原分录、红字分录、来源身份和关联ID,并在隔离环境验收。
来源与本次边界
资料与配图完整定位见来源说明,包含文件SHA-256、连续摘录行号、截图路由和验证材料。
设计图由当前执行链整理,主图为AI生成;没有使用外部产品图片,没有把生成图当实测界面。
本篇参考账套、科目与期间建模、业务单据自动制证及前篇来源审计,聚焦生命周期而非重复取数模板。
报销id=3及预算id=7历史流程缺失问题不在本次修复范围,现有运行包也未重新构建。
本次完成只读页面取证、源码核查、自建金额模型及文章双端预览。
没有创建保存财务单据,没有审批、付款、核销、制证、审核、过账、红冲、反过账、结转、改库或压力测试。
文章布局通过只说明图文可读,不代表财务产品采购验收通过。
你们更正错账时,最难统一的是期间政策、红字撤回,还是来源单据再次生成的规则?
如果这篇对你有用,欢迎点赞支持。
我们会持续分享企业软件开发实践与 RuoYi Office 产品进展。
🌐 演示地址 :https://ruoyioffice.com/web
📦 GitHub 源码 :https://github.com/yuqing2026/ruoyi-office
📦 Gitee 源码 :https://gitee.com/yqzy1688/ruoyi-office
💬 微信 :17156169080(获取产品咨询)
打开演示地址直接查看系统。
