大数跨境

SpringBoot+Flowable 业务单据状态设计:审批、执行与作废,为什么不能共用一个 statu+Flowable

SpringBoot+Flowable 业务单据状态设计:审批、执行与作废,为什么不能共用一个 statu+Flowable 企业软件源码
2026-10-20
7
导读:从资产借用与归还的实际业务出发,拆解审批状态、履行进度、单据有效性与历史事实,结合 SpringBoot3 现有回调和来源单校验,讨论合法状态组合、并发命令、幂等、反向操作和前端按钮设计。

SpringBoot+Flowable 业务单据状态设计:审批、执行与作废,为什么不能共用一个 status

一张借用单显示“已通过”,资产管理员却说设备还没有归还;另一张采购单也显示“已通过”,仓库却只收到一半。两个页面都没有错,问题在于“通过”只回答了审批结果,却被用户理解成业务已经结束。当一个 status 同时承担审批、履行和有效性,越完整的企业业务,越容易被迫增加含义模糊的状态。

▲ 状态矩阵是“先审批、后执行”业务的示例,不是所有模块的统一规则。审批、执行和有效性共同决定能做什么,不必被塞进一条线性状态链。

一、状态设计首先是业务语言设计

状态是对某个业务问题的简洁回答。审批状态回答“是否获得授权”,执行进度回答“已经履行多少”,有效性回答“这张单据还能否继续使用”。它们可能有关联,但不是同一个问题。

例如,借用申请通过后,资产可以进入使用状态,但借用关系仍然持续到归还;采购订单通过后,可以分多次入库;报销申请通过后,也未必已经完成付款。把这些记录统一标成“完成”,会让列表简洁,却让业务失去重要信息。

真正需要统一的是状态的定义和操作语义,而不是强迫所有业务共用一套数字枚举。一个框架可以统一流程状态,但不能因此把资产、库存、付款和人事变更都视为相同的履行过程。

维度
需要回答的问题
常见表现
审批
当前授权决定是什么
未提交、审批中、通过、拒绝
履行
业务已经执行到什么程度
未执行、部分执行、完成,或待归还、已归还
有效性
是否允许继续产生新业务
有效、关闭、作废等业务定义
并发版本
用户依据的数据是否还是当前版本
revision、更新时间或其他冲突依据

最后一项并不是业务状态,却会影响状态变更是否安全。两个人同时看到“待归还”,不能仅靠页面上的同一个绿色标签,保证他们不会创建重复归还业务。

也不必给每张表机械增加四个字段。若执行进度能够从已生效的明细事实准确汇总,可以不重复保存;若为了查询性能保留汇总字段,就要明确它是可重建的投影,而不是与明细各自维护的第二份事实。

二、借用、归还与资产卡片,分别在描述什么

1. 借用单获批,表示授权与占用已经发生

在 RuoYi Office 的资产实现里,业务流程回调会更新 processStatus。借用单审批通过后,服务继续处理资产占用,并将借用单的 returnStatus 设置为待归还。这里已经存在两个不同的状态维度。

▲ 真实借用详情页面。页面上的流程结果与业务履行不能混为一谈,借用获批并不表示资产已经归还。

这是一个很好的业务边界:对当前资产借用实现,审批通过可以直接触发资产占用,不需要凭空再发明一个“待发放”状态;但整个借用关系的结束仍然需要归还业务。建模应忠于已有产品动作,而不是为了讲状态机而增加用户根本不会操作的步骤。

2. 归还是一笔新的业务,不是把借用单改回草稿

归还单通过 sourceAllocateId 关联来源借用单。提交时要校验来源是否是借用业务、是否已经通过、是否已归还,以及是否已有正在处理的归还申请。

▲ 真实归还详情页面。该历史样本的来源借用单字段为空,因此这里只展示归还页面结构,不把它当作来源校验已通过的证据;下文关联约束以当前源码为准。

这样保留了两个不同事实:某日批准借出,另一日批准归还。若只把原单状态从“使用中”改成“空闲”,查询当时为什么借出、何时归还、由谁确认时,就会缺少业务证据。

同样的思想可以推广到退货、退款、冲销和调拨,但不能直接复用“归还”的枚举。每一种反向业务都有自己的数量、时间和授权条件,所谓通用框架应该帮助保留这些差别,而不是消灭它们。

3. 资产卡片反映当前结果,单据记录形成结果的过程

归还审批通过后,当前实现会释放资产卡片上的使用信息,并回写来源借用单的归还状态和日期。卡片、借用单、归还单各有职责,不能把其中任意一个的状态当作全部事实。

▲ 真实资产卡片页面,用来查看当前结果。此处卡片与前两张单据并非同一资产,只用于说明页面职责,不作为同一次借还联调的证明。

需要注意,“当前结果”通常是不断变化的。如果设备之后再次借出,卡片会显示新的使用情况;历史借用单仍然应保留当时的业务依据。用当前卡片替代历史单据,会让过去的记录跟着现在一起变化。

三、先定义合法组合,再定义按钮和迁移

多维状态并不是任意组合。以“必须审批后执行”的业务为例,审批中且已经执行完成,通常不应出现;已通过且未执行,则可能是完全正常的待处理状态。但对于审批本身就触发执行的业务,某些中间组合可能只有瞬时意义,根本不需要展示给用户。

因此,应先列出业务不变量,再决定状态字段。例如:“未获授权不得占用资产”“已归还的借用关系不得再次归还”“来源业务不匹配不得回写”“失效单据不得继续增加执行事实”。这些规则比一张画得漂亮的状态流转图更有约束力。

操作意图
典型前置条件
形成的事实
提交借用
必填完整、资产可借、单据可提交
新的流程实例或审批请求
审批通过
回调属于当前单据和流程
授权结果及对应资产占用
发起归还
来源已通过、尚未归还、无在途重复申请
独立归还单据
归还通过
来源与资产关系仍然有效
释放占用并回写来源履行结果
关闭或作废
符合该业务的失效条件
保留原事实,限制后续动作

不要先把“撤回、取消、作废、冲正”都画成一根指向草稿的箭头。撤回通常针对审批过程;取消针对尚未履行的业务意图;作废意味着原凭证不再作为后续依据;冲正则通过新的事实抵消既有影响。这些词在企业里可能有不同约定,产品必须给出自己的定义。

已经产生库存、占用或资金影响后,直接恢复草稿尤其危险。页面看似回到了起点,相关系统却未必恢复。正确方式是先判断能否执行相应反向业务,以及反向业务是否需要新的审批和记录。

四、让每一次状态变化都能找到业务证据

一个可维护的设计会把主单、执行事实、流程关联与操作记录分开。主单适合提供当前概览;执行事实说明发生了什么;流程实例说明授权如何形成;审计记录说明是谁用什么命令改变了系统。

▲ 通用设计示意。当前资产模块的 processStatus、returnStatus、sourceAllocateId 提供实际参照;图中的统一命令记录和事件体系属于扩展设计。

如果业务支持部分执行,不建议只保存一个 PARTIAL 状态。还需要可计算的数量或金额依据。例如申请十件、实际执行六件、撤销一件,剩余量如何计算,必须有明细支持。状态只是对这些事实的归纳,不是替代事实的字符串。

对不支持部分归还的现有模块,也不能在文章里因为图中出现“部分执行”就宣称已经支持分批归还。是否允许、如何关联明细和校验数量,都需要额外设计和实现。抽象模型可以覆盖更广场景,但产品能力必须逐项核实。

流程回调先确认“我在处理哪张单”

下面保留当前资产回调的主要逻辑。它先查询业务单据,再校验流程业务类型,最后更新审批状态并处理获批动作;与这些逻辑无关的注解和周边代码已省略。

@Transactional(rollbackFor = Exception.class)public void updateProcessStatusForFlow(        AssetBillTypeEnum flowBillType, String businessKey, Integer status) {    Long id = Long.parseLong(businessKey);    AssetAllocateDO exist = allocateMapper.selectById(id);    if (exist == null) {        throw exception(ASSET_ALLOCATE_NOT_EXISTS);    }    assertFlowMatchesBill(flowBillType, exist);    allocateMapper.updateById(new AssetAllocateDO()            .setId(id)            .setProcessStatus(status));    if (BpmProcessInstanceStatusEnum.APPROVE.getStatus().equals(status)) {        onAllocateApproved(exist, flowBillType);    }}

这段实现证明了审批状态与业务动作的连接点,但不能仅凭 @Transactional 就断言具备跨系统恰好一次处理、乱序回调防护或完整事件去重。若业务允许撤回重提、重新发起流程,还应明确回调是否属于当前绑定的流程实例。

旧流程的迟到消息不能覆盖新流程的结果。更稳妥的扩展是让回调同时携带流程实例标识、业务关联和事件身份,在受控事务中检查匹配关系。无法确认归属的消息进入异常记录,而不是猜测应该作用于当前单据。

来源校验比“按钮只显示一次”更可靠

当前归还提交逻辑已经把来源业务与归还进度放到服务端校验。以下按源码整理关键分支,保留业务错误语义。

private void validateReturnSource(AssetAllocateSaveReqVO request) {    Long sourceId = request.getSourceAllocateId();    if (sourceId == null) return;    AssetAllocateDO source = allocateMapper.selectById(sourceId);    if (source == null || !Integer.valueOf(ALLOCATE_TYPE_BORROW)            .equals(source.getAllocateType())) {        throw exception(ASSET_ALLOCATE_RETURN_SOURCE_INVALID);    }    if (!BpmProcessInstanceStatusEnum.APPROVE.getStatus()            .equals(source.getProcessStatus())) {        throw exception(ASSET_ALLOCATE_BORROW_NOT_APPROVED);    }    if (Integer.valueOf(RETURN_STATUS_RETURNED).equals(source.getReturnStatus())) {        throw exception(ASSET_ALLOCATE_ALREADY_RETURNED);    }    assertNoActiveReturn(sourceId, request.getId());}

真实方法还会校验资产使用情况和公司范围,不能在落地时遗漏。查询到“没有在途归还”只是规则判断的一部分;若要在高并发下严格防止两个请求同时通过,还需要锁定同一来源资源或使用其他数据库原子约束。文章不把先查后写描述为天然并发安全。

五、并发问题要按命令处理,而不是按页面处理

用户打开页面之后,状态可能已经被别人改变。前端隐藏按钮可以减少误操作,但不能保证提交时的事实仍然相同。服务端必须在执行命令时重新检查状态、权限和数据版本。

▲ 建议时序:命令记录、状态和执行事实一起提交,外部消费者按事件标识处理。Outbox 等事务事件机制是增强方案,不代表所有现有模块都已采用。

下面是并发命令的设计伪代码。仓储接口代表数据库事务能力,前提是命令标识具有租户内唯一约束,版本更新是原子操作,冲突时会回滚本次事务。

@Transactional(rollbackFor = Exception.class)public Result execute(Command command, Subject subject) {    Result previous = commands.findMatchingResult(            subject.tenantId(), command.id(), command.payloadDigest());    if (previous != null) return previous;    Bill bill = bills.lockForUpdate(subject.tenantId(), command.billId());    permission.requireOperate(subject, bill, command.action());    if (bill.revision() != command.expectedRevision()) {        throw new StateConflictException("业务已变化,请刷新后确认");    }    rules.requireAllowed(bill, command);    Execution fact = executionFactory.create(bill, command);    executions.insert(fact);    bills.applyTransition(bill, fact);    Result result = Result.from(fact);    commands.saveUnique(command, subject, result);    outbox.append(BusinessEvent.from(fact));    return result;}

这里的幂等不是“重复请求直接返回成功”。同一个命令标识携带不同参数时,应报冲突;同一个业务对象上的不同合法操作,也不能被误判成重复。命令身份、业务身份和执行事实身份分别承担不同职责。

事务事件还应防止“数据库提交了,通知却丢了”。使用 Outbox 等方式时,先把待发送事件与业务事实一起保存,再由独立投递器重试。消费者仍然需要去重,因为投递成功但确认丢失时可能再次收到消息。

如果业务动作涉及外部设备、第三方接口或另一个数据库,单个本地事务不能把它们全部包住。此时可以设计“处理中、待核实、需要补偿”等明确的处理状态,但这些是操作执行状态,不应随手污染业务审批枚举。

六、前端应该展示业务语言,而不是暴露状态码组合

后端可以把可执行操作作为结果的一部分返回,例如允许查看、编辑、提交或发起归还,并带上不允许操作的原因。前端仍应遵守权限体系,但不需要在多个页面复制几十组状态判断。

显示上也应保留维度。借用单可以同时展示“审批通过”和“待归还”,而不是合成一个含义模糊的“处理中”。列表需要简洁时,可以选一个业务主状态,再在详情说明构成它的事实,关键是始终保持定义一致。

错误提示要告诉用户如何继续。“状态异常”几乎没有帮助;“该借用单已存在审批中的归还申请,请查看原申请”则能引导用户处理重复业务。对于并发冲突,返回最新状态和关联单号通常比要求用户反复点击更有效。

但是,可执行操作列表只能作为交互提示,不能作为授权令牌。攻击者可以跳过页面直接发请求,旧页面也可能携带过期操作。最终校验必须留在服务端的命令处理入口。

七、怎样验证状态模型没有隐藏矛盾

状态模型应围绕不变量测试,而不只测试“从 A 走到 B”。同一条业务路径的重复、逆序、部分失败与并发,往往比正常路径更容易暴露设计问题。

测试场景
要守住的不变量
预期结果
同一回调重复到达
不能重复制造执行影响
识别已处理事实或安全重放
旧流程回调晚到
不覆盖新实例状态
拒绝或记录为过期事件
两个归还申请同时提交
来源关系不能重复结束
按原子约束只接受合法请求
页面打开后别人已操作
不按旧状态继续写入
返回冲突并提示刷新
外部动作结果不明
不伪造业务成功
保留待核实状态与证据
作废已有履行的单据
不抹掉已经发生的事实
要求明确反向操作或拒绝

数据修复也需要同样的纪律。直接修改状态字段也许能让按钮重新出现,却可能破坏台账、数量和流程之间的关系。管理后台若提供纠错入口,应要求说明原因、列出影响对象,并通过专用业务操作完成,而不是对所有人暴露“任意改状态”。

对于历史数据,可以定期检查不变量,例如已归还来源单是否有对应有效归还记录、在用资产是否有可解释的占用来源。发现差异时,应先区分展示汇总滞后和真正业务缺失,再选择重建投影或补充纠错记录。

八、现有工程怎样渐进改造

第一步不是引入一个复杂状态机框架,而是给当前状态字段写清定义。把“审批状态”和“业务进度”混用的地方标出来,确认每个操作真正产生的业务事实。

第二步把散落在 Controller、页面和回调里的关键规则集中到业务服务,保留已有权限与租户边界。对于资产借用归还,可以先围绕来源单、在途归还和卡片占用建立测试。

第三步补齐并发与历史证据:识别命令、绑定流程实例、限定迁移、记录反向业务。并不要求每个模块马上引入事件总线,但不能让网络重试和人工纠错继续依赖运气。

第四步才考虑抽象共用组件。可以统一操作日志、冲突提示和命令执行骨架,但“什么算完成、什么情况下可以取消”必须留给业务定义。过早统一枚举,往往只是把不同模块的复杂性压进一个越来越长的 switch。

体验时,可按资产借用单、归还单、资产卡片三个入口查看真实行为。本文状态矩阵用于解释通用设计,资产实现以实际的借用和归还语义为准,不把未实现的部分执行或通用补偿引擎当作已有功能。

常见问题

一个状态字段一定是错误设计吗?

不是。简单、严格线性的业务可以只有一个状态。问题是把互相独立、会同时存在的业务事实塞进同一字段,导致组合越来越难解释。

审批通过后是否应该统一进入“待执行”?

不应一刀切。有的业务在审批回调中就完成主数据变更或资产占用,有的还需要仓库、财务等角色继续执行。状态应对应真实动作,不为了形式完整增加无用步骤。

有了数据库事务,是否还需要幂等?

需要。本地事务保证一次执行内部的提交关系,不自动识别两次请求是不是同一业务意图,也不保证跨系统消息只投递一次。

作废单据可以物理删除吗?

是否允许删除由业务留存与审计要求决定。已经形成履行事实的单据,通常需要保留可解释的来源和反向记录;不能只因为界面想隐藏,就删除支撑台账的证据。

结语

一个可维护的企业系统,不是状态枚举最多的系统,而是每个状态都能回答明确问题、每个动作都能找到业务证据的系统。审批、履行和有效性分清之后,前端更容易解释,后端更容易校验,发生异常时也更容易恢复。

RuoYi Office 的资产借用归还和流程回调提供了实际业务基础。在这些事实之上逐步补齐命令、并发与历史治理,比把所有问题交给一个 status 字段,更接近企业系统长期运行的真实需要。

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

【声明】内容源于网络
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