SpringBoot+Vue3 凭证生命周期设计:审核、过账、冲销与反过账怎样保持账目一致

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,只读取现成数据。

可以按下面顺序核对页面:

  1. 凭证管理→凭证录入,先确认账套和期间。
  2. 打开既有详情,核对分录方向、金额和借贷合计。
  3. 账簿查询→科目余额表,核对同口径期间与科目层级。
  4. 财务设置→审计日志,查看已有动作详情与说明。
  5. 记录来源号和红冲关联;没有记录时转入隔离验收清单。

本篇不建议为了跟文中步骤而在现有账套点审核、过账、红冲、反过账或结账。

开发接手时可按实际交付说明启动后端与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(获取产品咨询)

打开演示地址直接查看系统。

相关推荐
2601_9621066433 分钟前
HashMap入门|工号-姓名键值存储、两种遍历方式、根据key/value查
java
RuoyiOffice1 小时前
KubeSphere 容器云实践:从管理 K8s 到建设企业 PaaS,还需要哪些能力
spring boot·kubernetes·kubesphere·jenkins·paas·微服务部署·ruoyi office
杨凯凡1 小时前
Spring Framework 7 API 版本管理实战:请求头路由、兼容迁移与废弃策略
java·rest api·springframework·spring boot 4·api版本管理
T_Wizard2 小时前
同一个问题写了 4 遍:官方 SDK / HTTP / Spring AI / LangChain4j,到底该选哪个
java·ai编程
hai_android2 小时前
AudioTrack 深度解析:从原理到实战
android·java·kotlin
斯维赤2 小时前
限流、熔断、降级,你真的分得清吗?
java·后端·架构
aiks_yuan5 小时前
Java CPU 100% 怎么排查:JDK 11 起 jstack 自带线程 CPU 时间,不用再 top -H 换算十六进制(JDK 8、11、17 实测)
java
Flynn Liang6 小时前
AiServices 源码级拆解:一个接口如何变成 RAG Agent
java·langchain·源码分析·rag·langchain4j·aiservices
Flynn Liang6 小时前
LangChain4j 1.23 架构全景:Java 大模型应用的 6 层抽象
java·langchain·大模型·rag·ai agent·langchain4j