大数跨境

SpringBoot3+Vue3 资产维修闭环:报修、审批、办结后怎样恢复资产状态

SpringBoot3+Vue3 资产维修闭环:报修、审批、办结后怎样恢复资产状态 企业软件源码
2026-10-19
7
导读:资产维修不能止于审批通过。本文结合RuoYi Office的真实页面与Spring Boot3、Vue3实现,串起报修、在途校验、Flowable审批、维修办结和资产状态恢复,解释三套状态的分工、提交

SpringBoot3+Vue3 资产维修闭环:报修、审批、办结后怎样恢复资产状态

▲ 报修、审批、办结与状态恢复共同构成资产维修闭环。

一台已经分配给员工的笔记本开不了机,员工报修,负责人同意送修。接下来,系统至少还要回答两个问题:这台电脑现在能不能再次领用?修好以后,它应该回到“闲置”还是“使用中”?

如果维修单只记录一个“审批通过”,这两个问题都没有答案。审批说明这次维修得到同意,维修结果说明故障已经处理,资产卡片则要告诉其他业务这台设备当前能做什么。

我们在RuoYi Office中把这三件事分开处理。下面沿着真实维修页面,拆解Spring Boot3、Vue3和Flowable如何接起报修、审批、办结与台账恢复这条链。

▲ 提交时记住资产原状态,审批通过才进入维修,办结后再恢复。审批链与实物处理链在这里衔接。

一、维修单要管过程,资产卡片要管可用性

报修人关心故障是否被受理,审批人关心维修原因和费用,资产管理人员关心设备什么时候恢复使用。同一张维修单承接这些信息,但不能用一个状态替所有角色下结论。

当前实现支持选择一项资产,填写故障描述,区分内部维修和外部送修,记录厂商、费用与附件。提交后进入审批;通过后,维修单进入“维修中”,资产卡片同步进入维修态;处理完成后,由有权限的人员登记完成日期和维修结果。

这里的费用是维修业务记录,不能仅凭这个字段推导出已经付款或已经完成财务入账。厂商支持手录或从供应商主数据选择,但本单保存的是厂商名称,也不宜把它理解成完整的应付账款关联。

状态放在哪里
回答什么问题
本文中的变化
维修单审批状态
这次申请是否获准
未提交、审批中、审批通过等
维修单维修状态
故障处理到哪一步
待维修→维修中→已完成
资产卡片状态
其他资产业务能否使用它
闲置或使用中→维修中→恢复原态

维修状态的1、2、3只是维修单自己的枚举值,不能直接拿去更新资产卡片。两张表表达不同含义,即使界面文案都叫“维修中”,也应该使用各自的状态定义。

▲ 列表把“维修状态”和“审批状态”分列展示:审批通过的单据仍可能处于维修中。系统图均保留完整框架,手机上可点开放大;阅读时先看这两列的对应关系。

这也解释了为什么不能在审批结束时直接把维修单设为“已完成”。审批可能当天结束,送修却需要几天。提前结束维修,会让业务人员失去跟踪未完工设备的依据。

二、从报修到办结,页面围绕同一份业务事实展开

选资产时筛一次,提交时再校验一次

报修从资产选择开始。页面采用单选,候选资产限制为闲置或使用中,并排除被在途单据占用的资产。编辑当前维修单时,还要排除它自己,避免重新打开单据就把原资产判为冲突。

选择器只能改善操作体验,不能代替提交校验。用户打开页面后,另一位管理员可能已经办理调拨或处置;因此后端提交时会重新读取资产卡片,检查状态,并查询是否存在冲突单据。冲突提示包含资产编号和占用单据,业务人员才有办法找到原因。

保存草稿和提交审批也有区别。保存负责留下故障、资产快照与附件;提交负责再次检查可用性,记录维修前状态并创建流程实例。当前在途查询把“未提交”和“审批中”都纳入占用范围,因此一张已有草稿也可能挡住另一项业务。它是业务选择,意味着长期搁置的草稿需要有人管理。

审批通过后,保留申请事实,进入实际处理

在修详情中可以同时看到故障、维修类型、厂商、费用和报修资产。已进入审批后的申请字段不应被随意改写,否则审批人当时同意的内容与后续执行依据就会分离。

▲ 这张既有演示单的故障为“电脑开不了机”,选择外部送修,登记费用4000元。顶部已审批通过,底部仍有“办结维修”,表明审批与完工是两个动作;金额只是该演示记录的值。

审批通过后,服务端把维修单设为维修中,并把资产卡片设为维修态。后续领用、借用等业务会检查卡片状态;领用要求闲置资产,维修中的设备因此不能继续按闲置资产领出。

这不是把维修单删掉或藏起来,而是让资产台账成为其他业务判断可用性的共同入口。维修详情仍保留过程信息,便于继续跟进。

办结需要结果,也需要一个恢复目标

页面的办结入口只在审批通过、维修状态为维修中时出现。打开弹窗后,需要填写维修结果;完成日期可以留空,由后端取当天日期。前端会对结果做去空白后的非空检查,后端则继续校验单据所处阶段。

▲ 办结是一个独立动作。此图仅打开已有在修单的弹窗,未填写、未提交;它说明输入要求,不作为完成结果的证明。

▲ 另一张既有测试单已经显示完成日期和“auto-verify 已修复”的结果。它与上图不是同一笔连续操作,测试字样保留原貌;本图证明结果字段的呈现方式。

这些画面来自现有演示、测试记录。页面状态帮助我们看清功能分工,下面的源码则解释状态恢复规则:如果报修前是使用中,修好后仍回到使用中;如果原来是闲置,就回到闲置。办结只恢复资产状态,不顺便改使用人、部门或存放位置。设备修复与资产交接需要各自的业务动作。

三、关键取舍:把审批、实物和历史分开保存

单纯在完成时写一个“闲置”,代码最短,却会把正在员工名下使用的资产变成可再次分配的候选。另一种办法是在完工时根据当前使用人反推状态,但这又把历史事实交给了可能变化的字段。

当前方案是在提交时保存beforeAssetStatus。此时系统已经重新检查资产可用性,保存的是本次送审的起点,比用户首次打开表单时的状态更有意义。审批通过后,卡片会被改为维修中,届时再读卡片就无法知道原先是闲置还是使用中。

决策点
当前做法
业务理由
什么时候进入维修态
审批通过后
申请提交不等于已经获准维修
完工后恢复到哪里
使用提交时的原态快照
保留闲置与在用的区别
如何判断冲突
卡片状态+跨单据在途查询
同时检查当前状态和待执行意图
结果怎样录入
独立办结动作
申请事实与处理结果分别维护

一张维修单,连接卡片、流程与履历

▲ 一张资产卡片可对应多次维修。维修单关联流程实例;履历通过业务类型与业务ID指向来源单据。图示为逻辑关系,不表示数据库已声明外键。

asset_repair保存这一次维修的事实,asset_card保存资产当前状态。单据同时保留资产编号、名称快照,便于在单据上识别对象;真正更新卡片时仍使用assetId。

字段或关联
在闭环中的用途
assetId
、billCode
确定哪项资产、哪次维修
processStatus
、repairStatus
分开表达审批结果与处理进度
beforeAssetStatus
记录提交时的恢复目标
completeDate
、repairResult
保存完工日期与结果
processInstanceId
关联本次审批实例
履历业务类型705+业务ID
从维修履历定位维修单

进入维修时,当前实现会写资产履历,记录前后状态与来源维修单。办结方法则更新维修结果和卡片状态,没有在该方法中追加一条完工履历。如果业务需要逐次追溯每个状态变化,这里还需要补齐事件记录,不能把“有履历表”理解为所有动作已经完整留痕。

一条业务链,分成不同时间点的事务

▲ 提交先校验并留快照,流程结束通知先回写审批状态,再进入通过钩子;实际维修完成后,人员另行发起办结。三段动作并不处在一个跨天数据库事务里。

流程定义标识为asset_repair,创建流程时以维修单ID作为businessKey,再把流程实例ID回写单据。这样审批中心和资产业务可以沿同一业务标识找到对方,而不必把资产字段塞进流程引擎的内部表。

四、让代码对应三个业务节点

以下为当前实现的核心摘录,省略处明确标出,保留各自的业务判断。它们依赖所在工程的Mapper、枚举和流程API,不是脱离工程即可运行的独立示例。

提交时重新判断可用性,再记住原状态

这一段对应“提交报修”:先保存本次表单,再读卡片并执行占用检查。传入当前单据类型和ID,是为了让检查跳过本单,而不是跳过其他维修单。

@Transactional(rollbackFor = Exception.class)public Long submit(AssetRepairSaveReqVO saveReqVO) {    Long id = save(saveReqVO);    AssetRepairDO row = repairMapper.selectById(id);    // ... 省略资产非空、重复送审检查    AssetCardDO card = assetCardMapper.selectById(row.getAssetId());    if (card == null) {        throw exception(ASSET_CARD_NOT_EXISTS);    }    billLockService.assertAssetsAvailable(List.of(row.getAssetId()),            List.of(AssetStatusEnum.IDLE.getStatus(),                    AssetStatusEnum.IN_USE.getStatus()),            AssetBillLockService.BILL_REPAIR, id);    row.setBeforeAssetStatus(card.getAssetStatus());    row.setProcessStatus(BpmTaskStatusEnum.RUNNING.getStatus());    // ... 省略组织变量、流程创建及实例ID回写    repairMapper.updateById(row);    return id;}

这里没有把卡片改为维修中。此时的限制主要来自在途单据检查;申请被拒绝或取消,也就不需要因为“提前进入维修”再额外反向恢复卡片。

审批通过,维修单和卡片一起进入维修阶段

流程通知回写审批状态后,通过钩子推进实际业务。维修单与资产卡片各写自己的状态,避免把审批枚举直接当成资产枚举。

@Transactional(rollbackFor = Exception.class)public void onProcessApproved(String businessKey) {    Long id = Long.parseLong(businessKey);    AssetRepairDO row = validateRepairExists(id);    AssetRepairDO u = new AssetRepairDO();    u.setId(id);    u.setRepairStatus(REPAIR_STATUS_REPAIRING);    repairMapper.updateById(u);    AssetCardDO card = assetCardMapper.selectById(row.getAssetId());    Integer beforeStatus = card != null            ? card.getAssetStatus() : row.getBeforeAssetStatus();    Integer afterStatus = AssetStatusEnum.REPAIRING.getStatus();    AssetCardDO cardUpd = new AssetCardDO();    cardUpd.setId(row.getAssetId());    cardUpd.setAssetStatus(afterStatus);    assetCardMapper.updateById(cardUpd);    // ... 省略日志文本拼装及维修履历写入}

审批状态回写与业务钩子是两个调用,不能仅看“审批通过”标签就推断后续回写一定成功。尤其是重试、异常和重复通知场景,需要关注钩子的执行结果;本方法没有显式的重复回调拦截,不能据此宣称回调已具备完整幂等保障。

办结先检查阶段,再恢复提交时的状态

完成操作同时维护单据结果与资产卡片。它拒绝未审批通过、已经完成以及不处于维修中的单据;顺序重复点击不会重新推进一次业务。

@Transactional(rollbackFor = Exception.class)public void complete(Long id, String repairResult, LocalDate completeDate) {    AssetRepairDO row = validateRepairExists(id);    if (!BpmTaskStatusEnum.APPROVE.getStatus().equals(row.getProcessStatus())) {        throw exception(ASSET_REPAIR_NOT_APPROVED);    }    if (REPAIR_STATUS_DONE == row.getRepairStatus()) {        throw exception(ASSET_REPAIR_ALREADY_COMPLETED);    }    if (!Integer.valueOf(REPAIR_STATUS_REPAIRING).equals(row.getRepairStatus())) {        throw exception(ASSET_REPAIR_STATUS_FORBIDDEN);    }    LocalDate doneDate = completeDate != null ? completeDate : LocalDate.now();    AssetRepairDO upd = new AssetRepairDO();    upd.setId(id);    upd.setCompleteDate(doneDate);    upd.setRepairResult(repairResult);    upd.setRepairStatus(REPAIR_STATUS_DONE);    repairMapper.updateById(upd);    Integer targetStatus = row.getBeforeAssetStatus();    if (!AssetStatusEnum.IDLE.getStatus().equals(targetStatus)            && !AssetStatusEnum.IN_USE.getStatus().equals(targetStatus)) {        targetStatus = AssetStatusEnum.IDLE.getStatus();    }    AssetCardDO cardUpd = new AssetCardDO();    cardUpd.setId(row.getAssetId());    cardUpd.setAssetStatus(targetStatus);    assetCardMapper.updateById(cardUpd);}

当快照不是闲置或使用中时,当前代码默认回到闲置。这是兼容异常、历史快照的策略,并不等于能够自动判断资产真实归属。对必须严格保留责任人的场景,更稳妥的业务选择可能是阻止办结并要求核对,但那属于进一步设计,不能当作当前已实现能力。

五、把业务门禁与并发保障分清楚

上述链路解决的是“该不该推进”,还需要注意“两个请求同时推进会怎样”。在途服务虽然名为AssetBillLockService,当前实现核心是查询卡片与关联单据,没有因为名字里带Lock就自动获得数据库行锁或分布式锁。

它能识别已有的占用:处置、领用借用归还、调拨、维修等单据会参与判断。但两个提交若同时查到可用,仅靠先查后写不能保证原子互斥。要支撑严格的并发竞争,还需要结合部署形态选择行锁、条件更新或唯一占用记录,并确保所有入口使用相同约束。

办结前检查“已完成”也有相同边界:它可以拦住后到的顺序请求,不等于已经解决两个并发请求同时读取旧状态的问题。审批回调若可能重复投递,还应结合业务状态、事件标识或更新条件设计幂等,而不能只依赖前端隐藏按钮。

另一个容易遗漏的边界是结果校验。当前页面要求维修结果非空,办结接口在所核对实现中没有同等的非空注解或服务内空白校验。页面约束与接口约束不能混为一谈;如果增加其他调用端,应补齐一致的输入规则。

资产维修的闭环,最终落在几个明确的时间点上:提交时保留起点,审批通过时改变可用性,办结时记录结果并恢复原态。把这几个事实分清,维修单才既能服务处理人员,也能让领用、调拨等后续业务读到有意义的资产状态。

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