背景:一个抽象场景
很多系统默认“一个资源只属于一个任务”。这个假设在简单业务里很自然:任务创建后分配资源,任务完成后释放资源,资源回到池子里等待下一次使用。
但在更复杂的流程系统里,资源可能出现跨任务复用:
- 任务 A 已经占用了某个资源。
- 任务 B 在满足条件时也需要使用同一资源。
- 任务 A 结束后,资源不一定立即退回资源池,而是可能流向任务 B 或任务 C。
- 移动端现场操作需要知道这个资源是“正常处理”还是“继续交接给下一个任务”。
- Web 管理端需要展示资源来源,避免排查时只看到一条普通分配记录。
这类需求不能只靠增加一个 shared 字段解决。真正困难的是:共享资源会贯穿分配、绑定、展示、操作、退回和释放整个生命周期。
问题拆解
1. 分配阶段:不能覆盖原始占用者
共享资源最容易犯的错误,是把“复用”实现成“重新占用”。
如果任务 B 需要使用任务 A 已占用的资源,系统直接把资源占用者改成任务 B,短期看任务 B 能跑通,但任务 A 的上下文丢了。后续结单、退回、追溯时,系统已经不知道资源原本属于谁。
更稳的模型是:
- 资源主表保留原始占用者。
- 分配明细记录当前服务任务。
- 共享分配记录额外保存原始占用任务。
- 当前任务结束时,根据共享关系决定是退回、跳过还是继续流转。
2. 绑定阶段:前端选择必须服务于后端状态模型
当一个任务涉及多个共享组时,移动端不能只提交“我绑定了哪些节点”。它还要表达共享资源后续要流向哪里。
不过这里也不能设计得过重。一个常见取舍是:
- 后端返回当前任务可选的后续任务列表。
- 前端把候选项合并展示,避免用户在多个共享组里重复选择。
- 提交时把选择结果映射回共享组维度。
- 后端允许部分共享组没有后续任务,但必须能区分“未涉及”和“漏传”。
flowchart LR A["任务选择"] --> B["查询共享组选项"] B --> C["前端合并候选任务"] C --> D["用户选择后续任务"] D --> E["按共享组提交选择"] E --> F["后端校验并绑定关系"]3. 展示阶段:共享状态要在 Web 端可见
共享状态如果只存在于后端,排查问题会很痛苦。Web 管理端至少要展示:
- 当前记录是否为共享资源。
- 原始占用任务或来源任务。
- 当前服务任务。
- 是否可以触发重新分配。
- 资源状态是否与任务状态一致。
这不是为了“页面更丰富”,而是为了让状态可解释。复杂流程一旦出现异常,排查人员需要知道这条记录为什么不是普通资源。
4. 退回阶段:释放动作要支持局部粒度
共享资源在退回或交接时,往往不能简单地“一键全部释放”。有些资源要继续给后续任务使用,有些资源需要退回池子,有些资源只需要关闭现场设备状态。
因此退回逻辑要支持更细的粒度:
- 按资源唯一标识处理。
- 按执行位置处理。
- 按共享组关系处理。
- 按后续任务是否存在决定流向。
如果粒度太粗,现场只能通过人工绕路来修正状态;如果粒度太细但没有后端校验,又会增加误操作风险。
方案设计
数据模型:表达“归属”和“服务”的区别
共享资源建模的核心,是把资源的原始归属和当前服务对象拆开。
public class ResourceAllocation { private Long resourceId; private Long currentTaskId; private Long ownerTaskId; private Long sharedGroupId; private Boolean shared; private Integer allocationOrder;}这里的 ownerTaskId 表示资源最初被谁占用,currentTaskId 表示当前这条分配记录服务于谁。普通资源可以让两者相同或只保存当前任务;共享资源则必须保留原始归属。
更进一步,还可以把共享关系独立成组:
public class SharedResourceGroup { private Long id; private Long resourceKey; private Integer status;}
public class SharedResourceGroupItem { private Long groupId; private Long taskId; private Long nextTaskId; private Integer sortNo;}这样做的好处是,分配记录负责“资源怎么用”,共享组负责“任务之间怎么接续”。两个问题分开后,后续扩展会轻很多。
分配服务:先抢占,再复用,最后对账
一个可复用的分配顺序是:
- 优先查找未被占用的资源。
- 未找到时,再查找可共享的已占用资源。
- 普通占用要使用条件更新,避免并发重复占用。
- 共享分配不改写资源主表占用者。
- 分配完成后做一次状态对账,修正缺料、正常、共享等状态。
抽象后的关键逻辑如下:
int updated = resourceMapper.occupyIfFree(resourceId, taskId);if (updated == 0) { Resource owner = resourceMapper.getById(resourceId); if (canShare(owner, taskId)) { allocationRepository.createShared(taskId, resourceId, owner.getTaskId()); } else { throw new BizException("资源已被其他任务占用"); }}这里的并发控制点在 occupyIfFree:只有资源尚未被占用时才允许写入。如果更新行数为 0,说明资源状态已经变化,后续只能进入共享判断或失败分支。
移动端:把共享资源变成明确操作路径
移动端现场操作要避免让用户猜。共享资源最好在界面上变成独立区域或明确标识:
- 普通待处理资源:继续扫码、确认、完成。
- 共享待流转资源:提示放置到指定区域或交接给后续任务。
- 不需要退回的资源:明确标识“无需扫码退回”。
- 需要局部释放的资源:提供按位置或按资源维度的操作入口。
这类提示不只是 UI 优化,它会直接减少错误扫码和重复处理。
Web 端:让后台视图成为排查入口
Web 端的职责不是复刻移动端操作,而是提供可解释的全局视图。共享资源至少要在明细列表中有明显标识,并能跳转到来源任务或关联任务。
一个好的后台列表不一定复杂,但要能回答三个问题:
- 这条资源为什么在当前任务里?
- 它原来属于哪个任务?
- 当前状态异常时,应该从分配、绑定还是退回环节排查?
常见坑
1. 用普通更新覆盖占用者
资源占用字段一旦被覆盖,原始任务关系就丢失了。共享场景里更推荐新增分配记录,而不是改写资源主表的占用者。
2. 只在新增分配时处理共享
共享资源不是一次性动作。补料、重建分配、结单、退回、设备状态刷新都可能需要识别共享关系。实现时要避免只有某一个入口支持共享,其他入口仍按普通资源处理。
3. 前端必填和后端允许不一致
如果前端强制选择后续任务,但后端允许部分共享组不传,就会出现体验和规则不一致。更好的方式是由后端返回候选结构,前端根据结构决定是否必填,提交后后端再按共享组逐项校验。
4. 退回时没有区分“退回”和“继续流转”
共享资源在当前任务结束后可能并不回池,而是进入下一任务的暂存或待处理状态。退回服务如果一律释放资源,就会破坏后续任务的资源来源。
可复用经验
第一,把“资源归属”和“资源当前服务对象”拆开建模。
第二,普通占用使用条件更新,避免并发重复占用。
第三,共享分配不覆盖原占用者,而是记录共享关系。
第四,分配完成后做状态对账,避免页面状态和实际分配漂移。
第五,移动端负责把共享路径讲清楚,后端负责最终校验。
第六,退回和释放必须支持局部粒度,不能只做全量动作。
第七,Web 端要展示共享来源,降低后续排查成本。
总结
共享资源的一致性难点,不在“能不能让两个任务看到同一份资源”,而在这份资源从分配、绑定、使用、结束到退回的每一步是否都有明确状态。
一套稳定的设计应该让普通资源和共享资源在模型上可区分、在服务上可校验、在移动端可操作、在 Web 端可解释。
当系统开始出现跨流程复用时,不要急着把逻辑塞进某个接口分支里。先把生命周期画清楚,再决定字段、接口和交互如何承接。这样做,后面的补料、结单、退回、局部释放才不会变成一串难以维护的特殊判断。