From e8a8baf62f76f313af9d920bf68c4f3a0c41e68c Mon Sep 17 00:00:00 2001 From: lzh Date: Wed, 18 Mar 2026 22:25:17 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(ops):=20=E7=8A=B6=E6=80=81=E6=9C=BA?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=20forceTransition=20=E5=BC=BA=E5=88=B6?= =?UTF-8?q?=E8=B7=B3=E8=BD=AC=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 适用于系统自动结单等场景,允许跳过转换规则直接跳转到终态, 但仍校验终态不可再转换,且完整记录事件流。 重构:抽取 doTransition 公共方法,transition 和 forceTransition 通过 validate 参数区分,消除重复代码。新增 TERMINAL_STATES 显式终态集合替代隐式空 Set 判断。 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../ops/service/fsm/OrderStateMachine.java | 127 +++++++++++------- 1 file changed, 81 insertions(+), 46 deletions(-) diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/fsm/OrderStateMachine.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/fsm/OrderStateMachine.java index 50d098f..1d1acef 100644 --- a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/fsm/OrderStateMachine.java +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/fsm/OrderStateMachine.java @@ -93,22 +93,21 @@ public class OrderStateMachine { WorkOrderStatusEnum.CANCELLED, Collections.emptySet() ); + /** 终态集合,不允许再转换 */ + private static final Set TERMINAL_STATES = Set.of( + WorkOrderStatusEnum.COMPLETED, WorkOrderStatusEnum.CANCELLED + ); + /** - * 执行状态转换(核心方法) - *

- * 此方法负责: - * 1. 验证状态转换合法性 - * 2. 更新工单状态和相关字段 - * 3. 记录事件流 - * 4. 发布状态变更事件 + * 执行状态转换(校验转换规则) * - * @param order 工单对象 - * @param newStatus 目标状态 + * @param order 工单对象 + * @param newStatus 目标状态 * @param operatorType 操作人类型 - * @param operatorId 操作人ID - * @param remark 说明 + * @param operatorId 操作人ID + * @param remark 说明 * @throws IllegalArgumentException 参数不合法 - * @throws IllegalStateException 状态转换不合法 + * @throws IllegalStateException 状态转换不合法 */ @Transactional(rollbackFor = Exception.class) public void transition(OpsOrderDO order, @@ -116,7 +115,66 @@ public class OrderStateMachine { OperatorTypeEnum operatorType, Long operatorId, String remark) { + doTransition(order, newStatus, operatorType, operatorId, remark, true); + } + /** + * 强制状态跳转(不受转换规则限制) + *

+ * 适用于系统自动结单等场景:告警自动解除时工单可能处于任何非终态, + * 需要直接跳转到 COMPLETED / CANCELLED,不经过中间状态。 + *

+ * 与 {@link #transition} 的区别:跳过转换规则校验, + * 其余逻辑(字段更新、事件记录)完全一致。 + * + * @param order 工单对象 + * @param newStatus 目标状态(通常为终态 COMPLETED / CANCELLED) + * @param operatorType 操作人类型 + * @param operatorId 操作人ID + * @param remark 说明 + * @throws IllegalArgumentException 参数不合法 + * @throws IllegalStateException 当前已是终态 + */ + @Transactional(rollbackFor = Exception.class) + public void forceTransition(OpsOrderDO order, + WorkOrderStatusEnum newStatus, + OperatorTypeEnum operatorType, + Long operatorId, + String remark) { + doTransition(order, newStatus, operatorType, operatorId, remark, false); + } + + /** + * 检查状态转换是否合法(不执行转换) + */ + public boolean canTransition(WorkOrderStatusEnum fromStatus, WorkOrderStatusEnum toStatus) { + if (fromStatus == null || toStatus == null) { + return false; + } + Set allowedTargets = TRANSITIONS.get(fromStatus); + return allowedTargets != null && allowedTargets.contains(toStatus); + } + + /** + * 获取当前状态允许转换到的目标状态集合 + */ + public Set getAllowedTransitions(WorkOrderStatusEnum currentStatus) { + return TRANSITIONS.getOrDefault(currentStatus, Collections.emptySet()); + } + + // ==================== 私有方法 ==================== + + /** + * 状态转换核心逻辑(transition / forceTransition 的统一实现) + * + * @param validate true=校验转换规则(transition),false=仅校验终态(forceTransition) + */ + private void doTransition(OpsOrderDO order, + WorkOrderStatusEnum newStatus, + OperatorTypeEnum operatorType, + Long operatorId, + String remark, + boolean validate) { // 1. 参数校验 if (order == null) { throw new IllegalArgumentException("工单对象不能为空"); @@ -128,15 +186,19 @@ public class OrderStateMachine { // 2. 获取当前状态 WorkOrderStatusEnum currentStatus = WorkOrderStatusEnum.valueOf(order.getStatus()); - // 3. 如果目标状态与当前状态相同,直接返回(避免不必要的操作) + // 3. 相同状态,跳过 if (currentStatus == newStatus) { - log.debug("工单状态未变化,跳过状态转换: orderId={}, status={}", - order.getId(), currentStatus); + log.debug("工单状态未变化,跳过: orderId={}, status={}", order.getId(), currentStatus); return; } - // 4. 校验状态转换合法性 - validateTransition(currentStatus, newStatus); + // 4. 校验 + if (validate) { + validateTransition(currentStatus, newStatus); + } else if (TERMINAL_STATES.contains(currentStatus)) { + throw new IllegalStateException(String.format( + "工单已处于终态 %s,无法转换到 %s", currentStatus.name(), newStatus.name())); + } // 5. 更新工单状态和相关字段 WorkOrderStatusEnum oldStatus = currentStatus; @@ -155,37 +217,10 @@ public class OrderStateMachine { remark ); - log.info("工单状态转换成功: orderId={}, {} -> {}, operatorType={}, operatorId={}", - order.getId(), oldStatus, newStatus, operatorType, operatorId); + log.info("工单状态转换: orderId={}, {} -> {}, forced={}, operatorType={}, operatorId={}", + order.getId(), oldStatus, newStatus, !validate, operatorType, operatorId); } - /** - * 检查状态转换是否合法(不执行转换) - * - * @param fromStatus 当前状态 - * @param toStatus 目标状态 - * @return 是否可以转换 - */ - public boolean canTransition(WorkOrderStatusEnum fromStatus, WorkOrderStatusEnum toStatus) { - if (fromStatus == null || toStatus == null) { - return false; - } - Set allowedTargets = TRANSITIONS.get(fromStatus); - return allowedTargets != null && allowedTargets.contains(toStatus); - } - - /** - * 获取当前状态允许转换到的目标状态集合 - * - * @param currentStatus 当前状态 - * @return 允许的目标状态集合 - */ - public Set getAllowedTransitions(WorkOrderStatusEnum currentStatus) { - return TRANSITIONS.getOrDefault(currentStatus, Collections.emptySet()); - } - - // ==================== 私有方法 ==================== - /** * 校验状态转换是否合法 */ From 92a51adcea366b07675af19581ad942bfc7ec2cf Mon Sep 17 00:00:00 2001 From: lzh Date: Wed, 18 Mar 2026 22:25:44 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(ops):=20=E5=AE=89=E4=BF=9D=E5=B7=A5?= =?UTF-8?q?=E5=8D=95=E8=87=AA=E5=8A=A8=E5=AE=8C=E5=8D=95=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=85=A8=E7=8A=B6=E6=80=81=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit autoCompleteOrder 根据当前状态分支处理: - PENDING → transition CANCELLED(未派单,告警已解除) - DISPATCHED/CONFIRMED/ARRIVED/PAUSED → forceTransition COMPLETED - 已终态 → 幂等跳过 falseAlarmOrder 复用 autoCompleteOrder 逻辑,额外更新扩展表误报标记。 移除 falseAlarmOrder 未使用的 operatorId 参数。 补充 4 个单元测试覆盖新增分支。 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../security/SecurityOrderController.java | 2 +- .../security/SecurityOrderOpenController.java | 2 +- .../securityorder/SecurityOrderService.java | 16 ++- .../SecurityOrderServiceImpl.java | 63 ++++++------ .../SecurityOrderServiceTest.java | 98 ++++++++++++++++--- 5 files changed, 134 insertions(+), 47 deletions(-) diff --git a/viewsh-module-ops/viewsh-module-ops-server/src/main/java/com/viewsh/module/ops/controller/admin/security/SecurityOrderController.java b/viewsh-module-ops/viewsh-module-ops-server/src/main/java/com/viewsh/module/ops/controller/admin/security/SecurityOrderController.java index d908323..d7888aa 100644 --- a/viewsh-module-ops/viewsh-module-ops-server/src/main/java/com/viewsh/module/ops/controller/admin/security/SecurityOrderController.java +++ b/viewsh-module-ops/viewsh-module-ops-server/src/main/java/com/viewsh/module/ops/controller/admin/security/SecurityOrderController.java @@ -94,7 +94,7 @@ public class SecurityOrderController { @Operation(summary = "误报标记", description = "将安保工单标记为误报并完成") @PreAuthorize("@ss.hasPermission('ops:security-order:complete')") public CommonResult falseAlarmOrder(@Valid @RequestBody SecurityOrderIdReqVO reqVO) { - securityOrderService.falseAlarmOrder(reqVO.getOrderId(), SecurityFrameworkUtils.getLoginUserId()); + securityOrderService.falseAlarmOrder(reqVO.getOrderId()); return success(true); } diff --git a/viewsh-module-ops/viewsh-module-ops-server/src/main/java/com/viewsh/module/ops/controller/open/security/SecurityOrderOpenController.java b/viewsh-module-ops/viewsh-module-ops-server/src/main/java/com/viewsh/module/ops/controller/open/security/SecurityOrderOpenController.java index aa20670..0e88f0b 100644 --- a/viewsh-module-ops/viewsh-module-ops-server/src/main/java/com/viewsh/module/ops/controller/open/security/SecurityOrderOpenController.java +++ b/viewsh-module-ops/viewsh-module-ops-server/src/main/java/com/viewsh/module/ops/controller/open/security/SecurityOrderOpenController.java @@ -99,7 +99,7 @@ public class SecurityOrderOpenController { @ApiSignature @PermitAll public CommonResult falseAlarmOrder(@Valid @RequestBody SecurityOrderIdReqVO reqVO) { - securityOrderService.falseAlarmOrder(reqVO.getOrderId(), null); + securityOrderService.falseAlarmOrder(reqVO.getOrderId()); return success(true); } diff --git a/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/service/securityorder/SecurityOrderService.java b/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/service/securityorder/SecurityOrderService.java index b77cb47..d04b45c 100644 --- a/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/service/securityorder/SecurityOrderService.java +++ b/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/service/securityorder/SecurityOrderService.java @@ -30,7 +30,14 @@ public interface SecurityOrderService { void confirmOrder(Long orderId, Long userId); /** - * 自动完单(对方系统调用,无需提交结果) + * 自动完单(由边缘系统调用,告警自动解除) + *

+ * 根据当前工单状态: + *

    + *
  • PENDING(未派单)→ 直接取消
  • + *
  • DISPATCHED / CONFIRMED / ARRIVED / PAUSED → 强制完成
  • + *
  • 已终态 → 幂等跳过
  • + *
* * @param orderId 工单ID * @param remark 备注 @@ -45,12 +52,11 @@ public interface SecurityOrderService { void manualCompleteOrder(SecurityOrderCompleteReqDTO req); /** - * 误报标记(将工单标记为误报并完成) + * 误报标记(复用自动完单逻辑,额外标记误报) * - * @param orderId 工单ID - * @param operatorId 操作人ID(可为 null,为 null 时取已分配人员) + * @param orderId 工单ID */ - void falseAlarmOrder(Long orderId, Long operatorId); + void falseAlarmOrder(Long orderId); /** * 根据工单ID查询安保扩展信息 diff --git a/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/service/securityorder/SecurityOrderServiceImpl.java b/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/service/securityorder/SecurityOrderServiceImpl.java index 8f453c9..491ce10 100644 --- a/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/service/securityorder/SecurityOrderServiceImpl.java +++ b/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/service/securityorder/SecurityOrderServiceImpl.java @@ -163,12 +163,42 @@ public class SecurityOrderServiceImpl implements SecurityOrderService { OpsOrderDO order = getOrderOrThrow(orderId); validateOrderType(order); - // 状态转换 → COMPLETED(扩展表时间 + 业务日志由 EventListener 统一记录,主表 endTime 由状态机统一设置) - orderStateMachine.transition(order, WorkOrderStatusEnum.COMPLETED, - OperatorTypeEnum.SYSTEM, null, - StrUtil.isNotBlank(remark) ? remark : "系统自动完单"); + String effectiveRemark = StrUtil.isNotBlank(remark) ? remark : "系统自动完单"; + WorkOrderStatusEnum currentStatus = WorkOrderStatusEnum.valueOf(order.getStatus()); - log.info("安保工单自动完单: orderId={}", orderId); + // 1. 已经是终态,幂等跳过 + if (currentStatus == WorkOrderStatusEnum.COMPLETED || currentStatus == WorkOrderStatusEnum.CANCELLED) { + log.info("安保工单已处于终态,跳过自动完单: orderId={}, status={}", orderId, currentStatus); + return; + } + // 2. PENDING:未派单,直接取消(PENDING → CANCELLED 规则已支持) + if (currentStatus == WorkOrderStatusEnum.PENDING) { + orderStateMachine.transition(order, WorkOrderStatusEnum.CANCELLED, + OperatorTypeEnum.SYSTEM, null, effectiveRemark + "(工单未派单)"); + } + // 3. 其他非终态(DISPATCHED / CONFIRMED / ARRIVED / PAUSED)→ 强制跳转 COMPLETED + else { + orderStateMachine.forceTransition(order, WorkOrderStatusEnum.COMPLETED, + OperatorTypeEnum.SYSTEM, null, effectiveRemark); + } + + log.info("安保工单自动结单: orderId={}, 原始状态={}", orderId, currentStatus); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void falseAlarmOrder(Long orderId) { + // 复用自动完单逻辑 + autoCompleteOrder(orderId, "误报"); + + // 更新扩展表:标记误报 + OpsOrderSecurityExtDO extUpdate = new OpsOrderSecurityExtDO(); + extUpdate.setOpsOrderId(orderId); + extUpdate.setFalseAlarm(true); + extUpdate.setResult("误报"); + securityExtMapper.insertOrUpdateSelective(extUpdate); + + log.info("安保工单误报标记: orderId={}", orderId); } @Override @@ -196,29 +226,6 @@ public class SecurityOrderServiceImpl implements SecurityOrderService { log.info("安保工单人工完单: orderId={}", req.getOrderId()); } - @Override - @Transactional(rollbackFor = Exception.class) - public void falseAlarmOrder(Long orderId, Long operatorId) { - OpsOrderDO order = getOrderOrThrow(orderId); - validateOrderType(order); - - // 如果 operatorId 为 null(open-api 调用),取已分配人员 - Long effectiveOperatorId = resolveOperatorId(orderId, operatorId); - - // 状态转换 → COMPLETED - orderStateMachine.transition(order, WorkOrderStatusEnum.COMPLETED, - OperatorTypeEnum.SECURITY_GUARD, effectiveOperatorId, "误报标记"); - - // 更新扩展表:标记误报 + 结果 - OpsOrderSecurityExtDO extUpdate = new OpsOrderSecurityExtDO(); - extUpdate.setOpsOrderId(orderId); - extUpdate.setFalseAlarm(true); - extUpdate.setResult("误报"); - securityExtMapper.insertOrUpdateSelective(extUpdate); - - log.info("安保工单误报标记: orderId={}", orderId); - } - @Override public OpsOrderSecurityExtDO getSecurityExt(Long opsOrderId) { return securityExtMapper.selectByOpsOrderId(opsOrderId); diff --git a/viewsh-module-ops/viewsh-module-security-biz/src/test/java/com/viewsh/module/ops/security/service/securityorder/SecurityOrderServiceTest.java b/viewsh-module-ops/viewsh-module-security-biz/src/test/java/com/viewsh/module/ops/security/service/securityorder/SecurityOrderServiceTest.java index 343183a..14d9676 100644 --- a/viewsh-module-ops/viewsh-module-security-biz/src/test/java/com/viewsh/module/ops/security/service/securityorder/SecurityOrderServiceTest.java +++ b/viewsh-module-ops/viewsh-module-security-biz/src/test/java/com/viewsh/module/ops/security/service/securityorder/SecurityOrderServiceTest.java @@ -251,8 +251,8 @@ public class SecurityOrderServiceTest { // 执行 securityOrderService.autoCompleteOrder(TEST_ORDER_ID, "告警自动解除"); - // 验证状态机调用 - verify(orderStateMachine).transition( + // 验证状态机调用(ARRIVED 非终态,走 forceTransition) + verify(orderStateMachine).forceTransition( eq(order), eq(WorkOrderStatusEnum.COMPLETED), eq(OperatorTypeEnum.SYSTEM), @@ -260,14 +260,7 @@ public class SecurityOrderServiceTest { eq("告警自动解除") ); - // 验证主表 endTime 更新 - ArgumentCaptor updateCaptor = ArgumentCaptor.forClass(OpsOrderDO.class); - verify(opsOrderMapper).updateById(updateCaptor.capture()); - OpsOrderDO updated = updateCaptor.getValue(); - assertEquals(TEST_ORDER_ID, updated.getId()); - assertNotNull(updated.getEndTime()); - - // 验证不再直接写扩展表时间 + // 验证不直接写扩展表 verify(securityExtMapper, never()).insertOrUpdateSelective(any()); } @@ -281,7 +274,7 @@ public class SecurityOrderServiceTest { securityOrderService.autoCompleteOrder(TEST_ORDER_ID, null); // 验证使用默认备注 - verify(orderStateMachine).transition( + verify(orderStateMachine).forceTransition( any(), eq(WorkOrderStatusEnum.COMPLETED), eq(OperatorTypeEnum.SYSTEM), isNull(), eq("系统自动完单") @@ -298,7 +291,7 @@ public class SecurityOrderServiceTest { securityOrderService.autoCompleteOrder(TEST_ORDER_ID, " "); // 验证使用默认备注 - verify(orderStateMachine).transition( + verify(orderStateMachine).forceTransition( any(), eq(WorkOrderStatusEnum.COMPLETED), eq(OperatorTypeEnum.SYSTEM), isNull(), eq("系统自动完单") @@ -311,6 +304,87 @@ public class SecurityOrderServiceTest { () -> securityOrderService.autoCompleteOrder(999L, "test")); } + @Test + void testAutoCompleteOrder_Pending_ShouldCancel() { + // 准备:PENDING 状态工单 + OpsOrderDO order = buildSecurityOrder(TEST_ORDER_ID, WorkOrderStatusEnum.PENDING); + orderDB.put(TEST_ORDER_ID, order); + + // 执行 + securityOrderService.autoCompleteOrder(TEST_ORDER_ID, "告警自动解除"); + + // 验证:走正常 transition → CANCELLED + verify(orderStateMachine).transition( + eq(order), + eq(WorkOrderStatusEnum.CANCELLED), + eq(OperatorTypeEnum.SYSTEM), + isNull(), + eq("告警自动解除(工单未派单)") + ); + // 不应调用 forceTransition + verify(orderStateMachine, never()).forceTransition(any(), any(), any(), any(), any()); + } + + @Test + void testAutoCompleteOrder_Dispatched_ShouldForceComplete() { + // 准备:DISPATCHED 状态工单 + OpsOrderDO order = buildSecurityOrder(TEST_ORDER_ID, WorkOrderStatusEnum.DISPATCHED); + orderDB.put(TEST_ORDER_ID, order); + + // 执行 + securityOrderService.autoCompleteOrder(TEST_ORDER_ID, "告警自动解除"); + + // 验证:走 forceTransition → COMPLETED + verify(orderStateMachine).forceTransition( + eq(order), + eq(WorkOrderStatusEnum.COMPLETED), + eq(OperatorTypeEnum.SYSTEM), + isNull(), + eq("告警自动解除") + ); + } + + @Test + void testAutoCompleteOrder_AlreadyCompleted_Idempotent() { + // 准备:已完成工单 + OpsOrderDO order = buildSecurityOrder(TEST_ORDER_ID, WorkOrderStatusEnum.COMPLETED); + orderDB.put(TEST_ORDER_ID, order); + + // 执行:不应抛异常 + securityOrderService.autoCompleteOrder(TEST_ORDER_ID, "告警自动解除"); + + // 验证:状态机未被调用 + verify(orderStateMachine, never()).transition(any(), any(), any(), any(), any()); + verify(orderStateMachine, never()).forceTransition(any(), any(), any(), any(), any()); + } + + @Test + void testFalseAlarmOrder_DelegatesToAutoCompleteAndMarksExt() { + // 准备:DISPATCHED 状态工单 + OpsOrderDO order = buildSecurityOrder(TEST_ORDER_ID, WorkOrderStatusEnum.DISPATCHED); + orderDB.put(TEST_ORDER_ID, order); + + // 执行 + securityOrderService.falseAlarmOrder(TEST_ORDER_ID); + + // 验证:forceTransition 被调用(通过 autoCompleteOrder 委托) + verify(orderStateMachine).forceTransition( + eq(order), + eq(WorkOrderStatusEnum.COMPLETED), + eq(OperatorTypeEnum.SYSTEM), + isNull(), + eq("误报") + ); + + // 验证:扩展表写入误报标记 + ArgumentCaptor extCaptor = ArgumentCaptor.forClass(OpsOrderSecurityExtDO.class); + verify(securityExtMapper).insertOrUpdateSelective(extCaptor.capture()); + OpsOrderSecurityExtDO extUpdate = extCaptor.getValue(); + assertEquals(TEST_ORDER_ID, extUpdate.getOpsOrderId()); + assertTrue(extUpdate.getFalseAlarm()); + assertEquals("误报", extUpdate.getResult()); + } + // ==================== 人工完单测试 ==================== @Test