<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Kris_Wen Tech Notes</title><description>把日常开发沉淀成可复用的工程经验</description><link>https://wxy20021116.github.io/</link><language>zh_CN</language><item><title>多阶段任务流中的前置校验与状态推进设计</title><link>https://wxy20021116.github.io/posts/2026-05-28-multi-stage-task-validation/</link><guid isPermaLink="true">https://wxy20021116.github.io/posts/2026-05-28-multi-stage-task-validation/</guid><description>在移动端和后台服务共同参与的系统里，一个任务往往不会只有“开始”和“完成”两个状态。它可能经历准备、绑定、执行、交接、释放、结束等多个阶段。每个阶段都可能由不同入口触发：有些操作在 Web 端发起，有些操作在移动端完成，最终状态又必须由后端</description><pubDate>Thu, 28 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在移动端和后台服务共同参与的系统里，一个任务往往不会只有“开始”和“完成”两个状态。它可能经历准备、绑定、执行、交接、释放、结束等多个阶段。每个阶段都可能由不同入口触发：有些操作在 Web 端发起，有些操作在移动端完成，最终状态又必须由后端统一确认。&lt;/p&gt;
&lt;p&gt;如果状态边界设计得不清楚，系统很容易出现两类问题：一类是用户明明可以继续操作，却被页面过早拦住；另一类是用户能点按钮，但后端状态其实已经不允许这个动作继续执行。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.unsplash.com/photo-1516321318423-f06f85e504b3?auto=format&amp;amp;fit=crop&amp;amp;fm=jpg&amp;amp;q=70&amp;amp;w=1200&quot; alt=&quot;workflow planning&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这篇文章用一个抽象案例，聊聊多阶段任务流里如何同时处理“放宽入口”和“收紧校验”。&lt;/p&gt;
&lt;h2&gt;背景：一个多端协同的任务流&lt;/h2&gt;
&lt;p&gt;假设有一个现场作业系统，任务会在多个阶段之间流转：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;WAITING&lt;/code&gt;：等待开始&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PREPARING&lt;/code&gt;：准备中&lt;/li&gt;
&lt;li&gt;&lt;code&gt;READY&lt;/code&gt;：可绑定资源&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RUNNING&lt;/code&gt;：执行中&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HANDOVER&lt;/code&gt;：交接中&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DONE&lt;/code&gt;：已完成&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;移动端需要支持现场人员选择任务、绑定设备、提交交接结果；后端需要判断当前任务是否允许进入下一阶段；管理端则负责展示任务明细和异常提示。&lt;/p&gt;
&lt;p&gt;看起来只是“多允许几个状态”，但实际上这会牵出一整套状态判断策略。&lt;/p&gt;
&lt;h2&gt;问题拆解&lt;/h2&gt;
&lt;p&gt;这次改造可以抽象成三个核心问题。&lt;/p&gt;
&lt;p&gt;第一，移动端入口太窄。&lt;br /&gt;
如果移动端只允许 &lt;code&gt;WAITING&lt;/code&gt; 状态进入操作页面，那么已经处在准备中或待绑定阶段的任务就无法继续现场处理。用户需要绕回其他页面或等待后台状态修正，体验很差。&lt;/p&gt;
&lt;p&gt;第二，设备选择太宽。&lt;br /&gt;
如果页面只排除一部分不可用设备，而不是明确只允许 &lt;code&gt;IDLE&lt;/code&gt; 状态设备，就可能把执行中、待释放、异常中的设备也展示出来。现场用户一旦重复绑定，就会把后端状态推向更复杂的冲突分支。&lt;/p&gt;
&lt;p&gt;第三，结束动作缺少前置确认。&lt;br /&gt;
任务结束前，如果同一批次、同一组关联任务还没有准备好，直接执行结束或释放动作，会导致后续任务拿不到完整上下文。这个问题不能只靠操作员记忆，系统应该在提交前给出提示，并在后端再次校验。&lt;/p&gt;
&lt;h2&gt;方案设计&lt;/h2&gt;
&lt;p&gt;这类多阶段任务流可以拆成三层校验：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart LR
    A[&quot;Mobile UI Filter&quot;] --&amp;gt; B[&quot;User Operation&quot;]
    B --&amp;gt; C[&quot;Backend Validation&quot;]
    C --&amp;gt; D[&quot;State Transition&quot;]
    D --&amp;gt; E[&quot;Related Task Refresh&quot;]
    E --&amp;gt; F[&quot;Next Operation Visibility&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;移动端负责过滤明显不可选项，后端负责最终一致性，状态推进后再反向刷新相关任务的可见状态。&lt;/p&gt;
&lt;h2&gt;移动端：放宽任务入口，收紧资源选择&lt;/h2&gt;
&lt;p&gt;移动端常见的误区是：任务状态允许范围和资源状态允许范围混在一起判断。&lt;/p&gt;
&lt;p&gt;任务入口可以适当放宽，因为用户需要在多个中间态继续操作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const bindableTaskStatuses = [
  TaskStatus.WAITING,
  TaskStatus.PREPARING,
  TaskStatus.READY
]

function canEnterBindPage(task: TaskRow) {
  return bindableTaskStatuses.includes(task.status)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但资源选择应该更严格。与其排除几个“不可选状态”，不如只允许一个明确的安全状态：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function getAvailableDevices(devices: DeviceRow[]) {
  return devices.filter(device =&amp;gt; device.status === &apos;IDLE&apos;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这两段判断背后的原则不同：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;任务入口判断解决的是“用户能不能继续流程”。&lt;/li&gt;
&lt;li&gt;资源选择判断解决的是“这个动作会不会造成重复占用”。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一个放宽，一个收紧，不能写成同一套模糊规则。&lt;/p&gt;
&lt;h2&gt;后端：状态推进必须基于事实，而不是页面传参&lt;/h2&gt;
&lt;p&gt;移动端可以提前过滤，但后端不能相信前端状态一定最新。尤其在现场操作系统里，弱网、重复点击、多人并发操作都很常见。&lt;/p&gt;
&lt;p&gt;后端更适合用“事实计数”推动状态，而不是只看请求里带来的目标状态。&lt;/p&gt;
&lt;p&gt;例如，当任务需要等待所有子项进入执行状态后，才能把主任务推进到下一阶段：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void refreshTaskRunningState(Long taskId) {
    TaskSnapshot snapshot = taskRepository.loadSnapshot(taskId);

    int requiredDeviceCount = devicePlanRepository.countRequiredDevices(snapshot.getPlanId());
    int runningDeviceCount = deviceTaskRepository.countRunningDevices(taskId);

    if (runningDeviceCount &amp;gt;= requiredDeviceCount) {
        taskRepository.updateStatus(taskId, TaskStatus.RUNNING);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的关键点是：状态推进不是由按钮决定的，而是由后端根据当前事实重新计算。&lt;/p&gt;
&lt;p&gt;这种方式有几个好处：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;前端刷新慢也不会影响最终状态。&lt;/li&gt;
&lt;li&gt;重复提交只会重复计算，不会重复推进。&lt;/li&gt;
&lt;li&gt;后续规则变化时，只需要调整后端状态判断。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;结束前校验：把“隐性依赖”变成明确提示&lt;/h2&gt;
&lt;p&gt;很多复杂流程的坑，来自“当前任务结束会影响关联任务”。用户在移动端看到的是一个结束按钮，但系统内部可能还要检查同批次任务、共享资源、待交接项、未创建的后续任务等条件。&lt;/p&gt;
&lt;p&gt;比较好的做法是做两层检查。&lt;/p&gt;
&lt;p&gt;第一层是提交前提示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async function beforeFinishTask(taskId: number) {
  const result = await api.checkRelatedTasks(taskId)

  if (result.hasMissingTasks) {
    await showConfirm({
      title: &apos;存在未准备完成的关联任务&apos;,
      content: &apos;继续结束可能影响后续流程，是否确认继续？&apos;
    })
  }

  return finishTask(taskId)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第二层是后端强校验：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void finishTask(Long taskId) {
    RelatedTaskCheckResult check = relatedTaskService.checkBeforeFinish(taskId);

    if (check.hasBlockingItems()) {
        throw new BizException(&quot;related tasks are not ready&quot;);
    }

    taskFlowService.finish(taskId);
    sharedResourceService.transferAfterFinish(taskId);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;前端提示是为了减少误操作，后端校验是为了保证数据不会被错误推进。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?auto=format&amp;amp;fit=crop&amp;amp;fm=jpg&amp;amp;q=70&amp;amp;w=1200&quot; alt=&quot;clear road&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;交接逻辑：按组处理比按单条处理更稳&lt;/h2&gt;
&lt;p&gt;多阶段任务流里，交接往往不是单条记录的状态变化，而是一组相关记录共同完成。&lt;/p&gt;
&lt;p&gt;如果按单条记录逐个判断，很容易出现这种情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A 记录已经交接&lt;/li&gt;
&lt;li&gt;B 记录还未交接&lt;/li&gt;
&lt;li&gt;C 记录属于共享资源，需要等另一个任务处理&lt;/li&gt;
&lt;li&gt;页面显示整体完成，但后端还有未处理项&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;更稳的方式是先聚合，再判断：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public HandoverResult completeHandover(Long groupId) {
    List&amp;lt;HandoverItem&amp;gt; items = handoverRepository.findByGroupId(groupId);

    long unfinishedCount = items.stream()
        .filter(item -&amp;gt; !item.isFinished())
        .count();

    if (unfinishedCount &amp;gt; 0) {
        return HandoverResult.partial(unfinishedCount);
    }

    handoverRepository.markGroupFinished(groupId);
    resourceService.releaseOrTransfer(groupId);
    return HandoverResult.completed();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的重点不是代码写法，而是建模方式：把“交接完成”定义在组维度，而不是让每一条明细自己决定整体状态。&lt;/p&gt;
&lt;h2&gt;共享资源流转：清空字段时要显式更新&lt;/h2&gt;
&lt;p&gt;当任务结束后，如果某个共享资源要流向下一个任务，后端通常会做类似处理：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当前任务标记为已处理&lt;/li&gt;
&lt;li&gt;下一个任务提升为当前任务&lt;/li&gt;
&lt;li&gt;清空 &lt;code&gt;nextTaskId&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;如果没有下一个任务，则清空当前任务引用&lt;/li&gt;
&lt;li&gt;如果整组都已完成，则把组状态置为完成&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这里有个很容易被忽略的技术细节：如果使用 MyBatis-Plus，想把字段清空为 &lt;code&gt;NULL&lt;/code&gt;，不能只依赖实体对象的 &lt;code&gt;setXxx(null)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;更可靠的写法是使用 &lt;code&gt;LambdaUpdateWrapper&lt;/code&gt; 显式设置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;shareGroupMapper.update(
    null,
    new LambdaUpdateWrapper&amp;lt;ShareGroupEntity&amp;gt;()
        .eq(ShareGroupEntity::getId, groupId)
        .set(ShareGroupEntity::getCurrentTaskId, nextTaskId)
        .set(ShareGroupEntity::getNextTaskId, null)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原因是 MyBatis-Plus 默认会忽略实体里的 &lt;code&gt;null&lt;/code&gt; 字段。涉及状态流转时，如果该清空的字段没有真正清空，后续判断就会读到旧值，形成隐藏状态污染。&lt;/p&gt;
&lt;h2&gt;容易踩坑的点&lt;/h2&gt;
&lt;h3&gt;1. 用排除法判断设备可选&lt;/h3&gt;
&lt;p&gt;排除法很容易漏状态。设备类资源建议使用白名单判断，只允许明确安全的 &lt;code&gt;IDLE&lt;/code&gt; 状态进入绑定流程。&lt;/p&gt;
&lt;h3&gt;2. 前端放宽入口后，后端没有同步规则&lt;/h3&gt;
&lt;p&gt;移动端允许更多任务状态进入页面后，后端接口也要能识别这些中间态，否则用户会在页面上能操作，但提交时失败。&lt;/p&gt;
&lt;h3&gt;3. 状态推进只看当前请求&lt;/h3&gt;
&lt;p&gt;复杂流程里，状态推进应该基于数据库事实重新计算，而不是直接相信前端传来的目标状态。&lt;/p&gt;
&lt;h3&gt;4. 结束动作缺少关联检查&lt;/h3&gt;
&lt;p&gt;结束一个任务可能影响后续任务、共享资源和交接记录。结束前检查应该独立成服务能力，而不是散落在页面按钮逻辑里。&lt;/p&gt;
&lt;h3&gt;5. 清空字段没有显式 set null&lt;/h3&gt;
&lt;p&gt;状态流转经常需要清空当前指针、下一指针或临时标记。使用 MyBatis-Plus 时，要用 &lt;code&gt;Wrapper.set(field, null)&lt;/code&gt; 明确生成 &lt;code&gt;SET field = NULL&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;可复用经验&lt;/h2&gt;
&lt;p&gt;遇到多阶段任务流，可以按下面的顺序设计：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;先列出所有状态和状态之间允许的动作。&lt;/li&gt;
&lt;li&gt;把“页面可进入状态”和“资源可绑定状态”拆开。&lt;/li&gt;
&lt;li&gt;前端做体验层过滤，后端做权威校验。&lt;/li&gt;
&lt;li&gt;状态推进基于事实计数或聚合结果，不基于按钮意图。&lt;/li&gt;
&lt;li&gt;对关联任务、共享资源、交接组做统一服务封装。&lt;/li&gt;
&lt;li&gt;对字段清空、重复提交、弱网重试做专门处理。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://images.unsplash.com/photo-1498050108023-c5249f4df085?auto=format&amp;amp;fit=crop&amp;amp;fm=jpg&amp;amp;q=70&amp;amp;w=1200&quot; alt=&quot;coding workspace&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;多阶段任务流的复杂度不在于状态数量，而在于每个状态背后的责任边界。&lt;/p&gt;
&lt;p&gt;移动端要让用户能顺畅地继续现场流程，但不能把不安全资源暴露出来；后端要允许合理的中间态操作，但必须在真正推进状态前重新校验事实；交接和共享资源流转要按组建模，避免单条记录各自为政。&lt;/p&gt;
&lt;p&gt;当一个流程开始同时影响移动端入口、后端状态、资源流转和交接结果时，它就不再是“改几个条件判断”的问题，而应该被当成一套状态机和一致性规则来设计。&lt;/p&gt;
</content:encoded></item><item><title>多端协同系统中共享资源分配的状态一致性设计</title><link>https://wxy20021116.github.io/posts/2026-05-22-multi-end-shared-resource-consistency/</link><guid isPermaLink="true">https://wxy20021116.github.io/posts/2026-05-22-multi-end-shared-resource-consistency/</guid><description>在一些复杂的业务系统里，同一份资源并不总是只服务于一个任务。它可能先被任务 A 占用，又在满足条件时被任务 B 临时复用；任务 A 结束后，这份资源还可能继续流转给任务 C，或者回到待处理池。</description><pubDate>Fri, 22 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在一些复杂的业务系统里，同一份资源并不总是只服务于一个任务。它可能先被任务 A 占用，又在满足条件时被任务 B 临时复用；任务 A 结束后，这份资源还可能继续流转给任务 C，或者回到待处理池。&lt;/p&gt;
&lt;p&gt;这个场景看起来只是“多加几个字段”的问题，但真正落到系统实现里，会同时影响后端分配逻辑、Web 管理端展示、PDA/移动端现场操作以及资源回收流程。如果没有统一的状态模型，最容易出现的问题是：后端已经做了复用判断，前端仍按普通资源展示；移动端提交时少传了下一个任务，后端不知道资源该流向哪里；回收时只按普通资源处理，导致部分设备状态或资源状态没有被正确释放。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.unsplash.com/photo-1512944159308-fbe53a63b779?auto=format&amp;amp;fit=crop&amp;amp;fm=jpg&amp;amp;q=70&amp;amp;w=1200&quot; alt=&quot;calm technology workspace&quot; /&gt;&lt;/p&gt;
&lt;p&gt;本文用一个脱敏后的抽象案例，聊聊这类“共享资源分配”在多端协同系统中的设计思路。&lt;/p&gt;
&lt;h2&gt;背景：一个抽象案例&lt;/h2&gt;
&lt;p&gt;假设系统中有三类角色：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;后端服务：负责资源分配、状态流转、回收和一致性校验。&lt;/li&gt;
&lt;li&gt;Web 管理端：负责展示任务明细、标记异常状态、提供重新分配入口。&lt;/li&gt;
&lt;li&gt;PDA/移动端：负责现场绑定、确认、扫码、回收等高频操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;资源默认属于某一个任务，但在特定条件下可以被另一个任务临时使用。为了让系统知道“这份资源当前是正常分配，还是跨任务复用”，需要引入几个抽象概念：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;shared&lt;/code&gt;: 当前记录是否属于共享资源。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ownerTaskId&lt;/code&gt;: 资源原始归属任务。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;currentTaskId&lt;/code&gt;: 资源当前服务任务。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nextTaskId&lt;/code&gt;: 当前任务结束后，资源要继续流向的下一个任务。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sharedGroupId&lt;/code&gt;: 多个任务围绕同一份资源形成的协作组。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;问题的关键不是字段本身，而是这些字段在不同端之间如何保持一致。&lt;/p&gt;
&lt;h2&gt;问题拆解&lt;/h2&gt;
&lt;p&gt;这类需求通常会暴露出四个技术问题。&lt;/p&gt;
&lt;p&gt;第一，资源分配不是单点动作，而是生命周期动作。&lt;br /&gt;
如果只在“首次分配”时处理共享，后续的补充、回收、结束、转交都会变成例外分支。例外分支越多，状态越容易漂移。&lt;/p&gt;
&lt;p&gt;第二，前端展示不能只显示“成功/失败”。&lt;br /&gt;
共享资源和普通资源的风险不同。Web 端需要把“资源来自哪里、为什么被复用、是否需要重新分配”表达清楚；移动端则需要把“是否必须选择下一任务”放在提交前，而不是提交失败后才让用户猜。&lt;/p&gt;
&lt;p&gt;第三，后端必须做最终校验。&lt;br /&gt;
移动端可以做前置判断，但不能相信移动端一定传了正确参数。只要涉及资源归属、状态流转、重复提交，就必须以后端校验为准。&lt;/p&gt;
&lt;p&gt;第四，回收动作要能按粒度处理。&lt;br /&gt;
有些时候不能简单地“一键释放全部资源”，而是要支持按执行位置、按资源标识或按任务关系做局部释放。否则现场操作会被迫绕路，系统状态也更难恢复。&lt;/p&gt;
&lt;h2&gt;方案设计&lt;/h2&gt;
&lt;p&gt;可以把整个链路拆成三层：数据模型、服务编排、多端交互。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart LR
    A[&quot;Resource Pool&quot;] --&amp;gt; B[&quot;Allocation Service&quot;]
    B --&amp;gt; C[&quot;Task A&quot;]
    B --&amp;gt; D[&quot;Task B&quot;]
    C --&amp;gt; E[&quot;Mobile Operation&quot;]
    D --&amp;gt; E
    E --&amp;gt; F[&quot;Backend Validation&quot;]
    F --&amp;gt; G[&quot;State Transition&quot;]
    G --&amp;gt; H[&quot;Web Visibility&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;后端：用状态模型约束资源流转&lt;/h3&gt;
&lt;p&gt;后端不要只保存“资源属于哪个任务”，而要能表达“资源为什么属于这个任务”。普通占用和共享占用在业务上是两种状态，在技术上也应该拆开。&lt;/p&gt;
&lt;p&gt;一个简化后的实体可以这样设计：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ResourceAllocation {
    private Long id;
    private Long resourceId;
    private Long currentTaskId;
    private Long ownerTaskId;
    private Long sharedGroupId;
    private Boolean shared;
    private AllocationStatus status;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当系统自动分配资源时，不建议只做一次简单查询：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Resource resource = resourceRepository.findFirstAvailable(resourceCode);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;更稳妥的做法是把“普通可用资源”和“可共享资源”分成两个候选集，再按优先级合并：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;List&amp;lt;Resource&amp;gt; normalCandidates = resourceRepository.findAvailable(resourceCode);
List&amp;lt;Resource&amp;gt; sharedCandidates = resourceRepository.findShareable(resourceCode, currentTaskId);

List&amp;lt;AllocationPlan&amp;gt; plans = allocationPlanner.plan(
    currentTaskId,
    requiredQuantity,
    normalCandidates,
    sharedCandidates
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样服务层能明确知道每一条分配记录的来源，而不是事后再从状态里反推。&lt;/p&gt;
&lt;h3&gt;Web 端：把隐性冲突变成可见信息&lt;/h3&gt;
&lt;p&gt;Web 管理端的价值不是重复后端逻辑，而是把复杂状态翻译成用户能理解的界面信息。&lt;/p&gt;
&lt;p&gt;比如任务明细表里，普通资源和共享资源不能只靠同一个“正常”标签展示。可以把展示状态做成一个独立映射：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const allocationStatusView = {
  NORMAL: { label: &apos;正常分配&apos;, type: &apos;success&apos; },
  SHARED: { label: &apos;共享占用&apos;, type: &apos;warning&apos; },
  MISSING: { label: &apos;待补充&apos;, type: &apos;danger&apos; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果某条记录来自其他任务，页面可以展示一个抽象提示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function getAllocationTip(row: AllocationRow) {
  if (!row.shared) {
    return &apos;当前资源为普通分配&apos;
  }
  return `该资源来自关联任务，可在详情中查看流转关系`
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意这里不要让前端拼接复杂业务判断。前端只消费后端给出的结构化字段，负责展示、跳转和触发操作。&lt;/p&gt;
&lt;h3&gt;PDA/移动端：把现场操作变成明确选择&lt;/h3&gt;
&lt;p&gt;移动端的挑战是操作场景更紧凑，用户通常不适合阅读大段说明。对于共享资源这类有后续流向的动作，移动端最好把选择前置。&lt;/p&gt;
&lt;p&gt;例如当前任务结束后，如果资源还可以继续给其他任务使用，就需要让用户选择下一站：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const candidates = sharedGroups
  .flatMap(group =&amp;gt; group.nextTaskCandidates)
  .filter(item =&amp;gt; item.enabled)

if (candidates.length &amp;gt; 0 &amp;amp;&amp;amp; !selectedNextTaskId) {
  showToast(&apos;请选择资源后续流向&apos;)
  return
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但这只是体验层校验。真正提交时，后端仍要重新判断：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (shareGroup.hasRemainingTasks() &amp;amp;&amp;amp; request.getNextTaskId() == null) {
    throw new BizException(&quot;shared resource requires next task selection&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一个比较实用的原则是：移动端负责减少误操作，后端负责保证正确性。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?auto=format&amp;amp;fit=crop&amp;amp;fm=jpg&amp;amp;q=70&amp;amp;w=1200&quot; alt=&quot;mountain road&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;关键实现：把“共享”做成服务能力，而不是散落在各处的 if&lt;/h2&gt;
&lt;p&gt;如果共享资源逻辑只写在某个接口里，后面一定会被其他流程绕开。更好的做法是把它沉淀成服务能力。&lt;/p&gt;
&lt;p&gt;比如可以定义一个分配服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface SharedAllocationService {

    boolean hasSharedAllocation(Long taskId);

    List&amp;lt;NextTaskOption&amp;gt; listNextTaskOptions(Long taskId);

    AllocationResult allocateWithSharedPolicy(Long taskId);

    void transferAfterFinish(Long taskId, Long nextTaskId);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样 Web 端检查是否存在共享状态、PDA 获取下一任务候选、后端结束流程处理资源转移，都可以复用同一套语义。&lt;/p&gt;
&lt;p&gt;服务内部再做阶段化处理：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public AllocationResult allocateWithSharedPolicy(Long taskId) {
    TaskSnapshot snapshot = taskRepository.loadSnapshot(taskId);

    AllocationContext context = AllocationContext.from(snapshot);

    List&amp;lt;AllocationPlan&amp;gt; normalPlans = planNormalResources(context);
    List&amp;lt;AllocationPlan&amp;gt; sharedPlans = planShareableResources(context);

    AllocationResult result = allocationWriter.write(taskId, normalPlans, sharedPlans);

    taskStatusUpdater.refreshByAllocationResult(taskId, result);
    return result;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里有两个重点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;先构建快照，再生成计划，最后统一写入，避免边查边改导致状态不稳定。&lt;/li&gt;
&lt;li&gt;分配完成后刷新任务明细状态，让 Web 展示和后端真实分配保持一致。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;回收与释放：不要忽略局部操作&lt;/h2&gt;
&lt;p&gt;共享资源的回收比普通资源更麻烦。普通资源通常只需要释放占用即可，但共享资源还要判断：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当前任务是否真的结束。&lt;/li&gt;
&lt;li&gt;资源是否还有下一个任务。&lt;/li&gt;
&lt;li&gt;是否只释放某个执行位置上的资源。&lt;/li&gt;
&lt;li&gt;是否需要联动外部状态，比如设备提示、缓存状态或现场标识。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一个抽象后的接口可以这样设计：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void releaseByPosition(ReleaseRequest request) {
    ReleaseScope scope = releaseScopeResolver.resolve(request);

    List&amp;lt;ResourceAllocation&amp;gt; allocations = allocationRepository.findByScope(scope);
    List&amp;lt;ResourceAllocation&amp;gt; releasable = releasePolicy.filterReleasable(allocations);

    allocationRepository.markReleased(releasable);
    indicatorService.turnOff(scope.getPositionIds());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的重点是 &lt;code&gt;ReleaseScope&lt;/code&gt;。它把“释放全部”“按位置释放”“按资源释放”等不同入口统一抽象成同一种范围对象，后面的策略就不会被接口形态绑死。&lt;/p&gt;
&lt;h2&gt;容易踩坑的点&lt;/h2&gt;
&lt;h3&gt;1. 只在前端判断是否必选下一任务&lt;/h3&gt;
&lt;p&gt;前端判断是为了体验，后端判断才是为了数据安全。只要下一任务会影响资源流向，就必须在后端重新校验。&lt;/p&gt;
&lt;h3&gt;2. 共享状态只保存在分配表，明细表不回写&lt;/h3&gt;
&lt;p&gt;如果只有底层分配记录知道资源被共享，列表页、详情页、移动端页面都要额外查询或自行推断。更好的做法是让关键展示模型也有简化后的共享标记，前端直接消费。&lt;/p&gt;
&lt;h3&gt;3. 回收时把共享资源当普通资源处理&lt;/h3&gt;
&lt;p&gt;普通释放只关心“是否释放成功”，共享释放还要关心“释放后资源去哪”。如果少了这一步，后续任务可能拿不到资源，或者资源状态还停留在旧任务上。&lt;/p&gt;
&lt;h3&gt;4. 接口字段增加后没有统一类型定义&lt;/h3&gt;
&lt;p&gt;Web 端和移动端都依赖接口字段。新增 &lt;code&gt;shared&lt;/code&gt;、&lt;code&gt;ownerTaskId&lt;/code&gt;、&lt;code&gt;nextTaskOptions&lt;/code&gt; 这类字段时，最好同步更新 TypeScript 类型、请求参数和响应结构，否则页面能跑但状态含义会散掉。&lt;/p&gt;
&lt;h3&gt;5. 忽略重复提交&lt;/h3&gt;
&lt;p&gt;移动端现场操作容易出现重复点击、弱网重试、返回后再次提交。共享资源流转接口应该天然支持幂等判断，例如同一个任务已经完成流转时，不再重复生成转移记录。&lt;/p&gt;
&lt;h2&gt;可复用经验&lt;/h2&gt;
&lt;p&gt;这类多端协同需求可以按下面的顺序设计：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;先定义资源状态模型：普通、共享、释放、转移、异常。&lt;/li&gt;
&lt;li&gt;再定义后端服务语义：分配、检查、候选查询、确认流转、释放。&lt;/li&gt;
&lt;li&gt;然后定义前端展示字段：是否共享、来源说明、下一任务候选、是否允许操作。&lt;/li&gt;
&lt;li&gt;最后补齐移动端体验：前置校验、必选项提示、局部释放、重复提交保护。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我比较推荐把它当成一个“状态一致性问题”，而不是一个“页面加字段问题”。前者会迫使我们思考生命周期和责任边界，后者很容易变成三端各写一堆判断。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.unsplash.com/photo-1498050108023-c5249f4df085?auto=format&amp;amp;fit=crop&amp;amp;fm=jpg&amp;amp;q=70&amp;amp;w=1200&quot; alt=&quot;coding workspace&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;共享资源分配的难点不在于某个接口，也不在于某个页面，而在于它把“资源归属”从单任务模型变成了多任务流转模型。&lt;/p&gt;
&lt;p&gt;后端要负责状态权威和流转校验，Web 端要负责把隐性共享关系展示清楚，PDA/移动端要负责把现场选择做得明确且不打断主流程。只有这三层语义对齐，系统才能既支持灵活操作，又避免资源状态在多个端之间失真。&lt;/p&gt;
&lt;p&gt;从工程实践上看，这类需求最值得沉淀的经验是：当一个字段开始影响多个流程时，就不要把它当作普通字段处理，而要把它提升成一套服务能力和状态模型。这样后续新增分配、回收、转交、展示、告警等能力时，系统还有继续演进的空间。&lt;/p&gt;
</content:encoded></item></channel></rss>