diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/cleanorder/dto/CleanManualCancelReqDTO.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/cleanorder/dto/CleanManualCancelReqDTO.java new file mode 100644 index 0000000..26d07d0 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/cleanorder/dto/CleanManualCancelReqDTO.java @@ -0,0 +1,27 @@ +package com.viewsh.module.ops.environment.service.cleanorder.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * 管理后台 - 保洁手动取消工单 Request DTO + * + * @author lzh + */ +@Schema(description = "管理后台 - 保洁手动取消工单 Request DTO") +@Data +public class CleanManualCancelReqDTO { + + @Schema(description = "工单ID", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "工单ID不能为空") + private Long orderId; + + @Schema(description = "取消原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "误报,无需保洁") + @NotBlank(message = "取消原因不能为空") + private String reason; + + @Schema(description = "操作人ID(由 Controller 注入)", hidden = true) + private Long operatorId; +} diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/cleanorder/dto/CleanManualCreateReqDTO.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/cleanorder/dto/CleanManualCreateReqDTO.java new file mode 100644 index 0000000..7438c08 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/cleanorder/dto/CleanManualCreateReqDTO.java @@ -0,0 +1,39 @@ +package com.viewsh.module.ops.environment.service.cleanorder.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * 管理后台 - 保洁手动创建工单 Request DTO + * + * @author lzh + */ +@Schema(description = "管理后台 - 保洁手动创建工单 Request DTO") +@Data +public class CleanManualCreateReqDTO { + + @Schema(description = "工单标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "三楼电梯厅紧急清洁") + @NotBlank(message = "工单标题不能为空") + private String title; + + @Schema(description = "工单描述", example = "地面有大面积水渍") + private String description; + + @Schema(description = "优先级(0=P0紧急, 1=P1重要, 2=P2普通)", example = "1") + private Integer priority; + + @Schema(description = "区域ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + @NotNull(message = "区域ID不能为空") + private Long areaId; + + @Schema(description = "保洁类型", example = "SPOT") + private String cleaningType; + + @Schema(description = "预计作业时长(分钟)", example = "30") + private Integer expectedDuration; + + @Schema(description = "操作人ID(由 Controller 注入)", hidden = true) + private Long operatorId; +} diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/cleanorder/dto/CleanManualDispatchReqDTO.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/cleanorder/dto/CleanManualDispatchReqDTO.java new file mode 100644 index 0000000..ca5f3fa --- /dev/null +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/cleanorder/dto/CleanManualDispatchReqDTO.java @@ -0,0 +1,29 @@ +package com.viewsh.module.ops.environment.service.cleanorder.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * 管理后台 - 保洁手动派单 Request DTO + * + * @author lzh + */ +@Schema(description = "管理后台 - 保洁手动派单 Request DTO") +@Data +public class CleanManualDispatchReqDTO { + + @Schema(description = "工单ID", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "工单ID不能为空") + private Long orderId; + + @Schema(description = "目标设备ID(工牌设备ID)", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "目标设备ID不能为空") + private Long assigneeId; + + @Schema(description = "派单备注", example = "紧急情况,指定该设备处理") + private String remark; + + @Schema(description = "操作人ID(由 Controller 注入)", hidden = true) + private Long operatorId; +} diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/manual/CleanOrderBusinessStrategy.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/manual/CleanOrderBusinessStrategy.java new file mode 100644 index 0000000..35f441f --- /dev/null +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/manual/CleanOrderBusinessStrategy.java @@ -0,0 +1,41 @@ +package com.viewsh.module.ops.environment.service.manual; + +import com.viewsh.module.ops.api.queue.OrderQueueService; +import com.viewsh.module.ops.core.manual.model.*; +import com.viewsh.module.ops.core.manual.strategy.OrderBusinessStrategy; +import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO; +import com.viewsh.module.ops.enums.WorkOrderStatusEnum; +import com.viewsh.module.ops.enums.WorkOrderTypeEnum; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 保洁条线策略 + *

+ * 处理保洁特有的前置校验和后置副作用。 + * + * @author lzh + */ +@Slf4j +@Component +public class CleanOrderBusinessStrategy implements OrderBusinessStrategy { + + @Resource + private OrderQueueService orderQueueService; + + @Override + public boolean supports(String businessType) { + return WorkOrderTypeEnum.CLEAN.getType().equals(businessType); + } + + @Override + public void afterUpgradePriority(UpgradePriorityCommand cmd, OpsOrderDO order) { + // 如果工单在队列中,触发队列分数重算 + if (WorkOrderStatusEnum.QUEUED.getStatus().equals(order.getStatus()) && order.getAssigneeId() != null) { + orderQueueService.rebuildWaitingTasksByUserId(order.getAssigneeId(), order.getAreaId()); + log.info("[CleanStrategy] 升级优先级后重算队列: orderId={}, assigneeId={}", + cmd.getOrderId(), order.getAssigneeId()); + } + } +} diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/notification/CleanOrderNotificationService.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/notification/CleanOrderNotificationService.java new file mode 100644 index 0000000..1cc9cfc --- /dev/null +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/notification/CleanOrderNotificationService.java @@ -0,0 +1,190 @@ +package com.viewsh.module.ops.environment.service.notification; + +import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO; +import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper; +import com.viewsh.module.ops.environment.constants.CleanNotificationConstants; +import com.viewsh.module.ops.environment.service.voice.TtsQueueMessage; +import com.viewsh.module.ops.environment.service.voice.VoiceBroadcastService; +import com.viewsh.module.system.api.notify.NotifyMessageSendApi; +import com.viewsh.module.system.api.notify.dto.NotifySendSingleToUserReqDTO; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +import java.util.Map; + +/** + * 保洁工单通知服务。 + *

+ * 负责语音播报和站内信发送,供事件监听器、业务服务、策略层复用, + * 避免业务层反向依赖事件监听器形成循环依赖。 + */ +@Slf4j +@Service +public class CleanOrderNotificationService { + + @Resource + private OpsOrderMapper opsOrderMapper; + + @Resource + private VoiceBroadcastService voiceBroadcastService; + + @Resource + private NotifyMessageSendApi notifyMessageSendApi; + + @Async("ops-task-executor") + public void sendNewOrderNotification(Long deviceId, Long orderId) { + try { + OpsOrderDO order = opsOrderMapper.selectById(orderId); + if (order == null) { + log.warn("[新工单通知] 工单不存在: orderId={}", orderId); + return; + } + + log.info("[新工单通知] deviceId={}, orderId={}", deviceId, orderId); + voiceBroadcastService.broadcastLoop(deviceId, + CleanNotificationConstants.VoiceTemplate.NEW_ORDER_RING, orderId); + + sendNotifyMessage(1L, + CleanNotificationConstants.TemplateCode.NEW_ORDER, + CleanNotificationConstants.NotifyParamsBuilder.newOrderParams( + order.getOrderCode(), + order.getTitle(), + CleanNotificationConstants.VoiceBuilder.getAreaName(order.getLocation()))); + } catch (Exception e) { + log.error("[新工单通知] 发送失败: deviceId={}, orderId={}", deviceId, orderId, e); + } + } + + @Async("ops-task-executor") + public void sendQueuedOrderNotification(Long deviceId, int queueCount, Long orderId) { + try { + log.info("[待办增加通知] deviceId={}, queueCount={}", deviceId, queueCount); + + String voiceMessage = CleanNotificationConstants.VoiceBuilder.buildQueuedOrder(queueCount); + playVoice(deviceId, voiceMessage, orderId); + + if (queueCount >= 3) { + sendNotifyMessage(1L, + CleanNotificationConstants.TemplateCode.QUEUED_ORDER, + CleanNotificationConstants.NotifyParamsBuilder.queuedOrderParams(queueCount, 1)); + } + } catch (Exception e) { + log.error("[待办增加通知] 发送失败: deviceId={}", deviceId, e); + } + } + + @Async("ops-task-executor") + public void sendNextTaskNotification(Long deviceId, int queueCount, String orderTitle) { + try { + log.info("[下一任务通知] deviceId={}, queueCount={}, title={}", deviceId, queueCount, orderTitle); + + String voiceMessage = CleanNotificationConstants.VoiceBuilder.buildNextTask(orderTitle); + playVoice(deviceId, voiceMessage); + + sendNotifyMessage(1L, + CleanNotificationConstants.TemplateCode.NEXT_TASK, + CleanNotificationConstants.NotifyParamsBuilder.nextTaskParams(queueCount, orderTitle)); + } catch (Exception e) { + log.error("[下一任务通知] 发送失败: deviceId={}", deviceId, e); + } + } + + @Async("ops-task-executor") + public void sendPriorityUpgradeNotification(Long deviceId, String orderCode, Long orderId) { + try { + log.warn("[P0紧急通知] deviceId={}, orderCode={}", deviceId, orderCode); + + String voiceMessage = CleanNotificationConstants.VoiceBuilder.buildPriorityUpgrade(orderCode); + playVoiceUrgent(deviceId, voiceMessage, orderId); + + sendNotifyMessage(1L, + CleanNotificationConstants.TemplateCode.PRIORITY_UPGRADE, + CleanNotificationConstants.NotifyParamsBuilder.priorityUpgradeParams(orderCode, "P0紧急任务")); + } catch (Exception e) { + log.error("[P0紧急通知] 发送失败: deviceId={}", deviceId, e); + } + } + + @Async("ops-task-executor") + public void sendTaskResumedNotification(Long deviceId, String areaName) { + try { + log.info("[任务恢复通知] deviceId={}, areaName={}", deviceId, areaName); + + String voiceMessage = CleanNotificationConstants.VoiceBuilder.buildTaskResumed(areaName); + playVoice(deviceId, voiceMessage); + + sendNotifyMessage(1L, + CleanNotificationConstants.TemplateCode.TASK_RESUMED, + CleanNotificationConstants.NotifyParamsBuilder.taskResumedParams(areaName)); + } catch (Exception e) { + log.error("[任务恢复通知] 发送失败: deviceId={}", deviceId, e); + } + } + + public void sendOrderCompletedNotification(Long orderId, Long deviceId) { + try { + OpsOrderDO order = opsOrderMapper.selectById(orderId); + if (order == null) { + return; + } + + log.info("[工单完成通知] orderId={}, orderCode={}, areaId={}, deviceId={}", + orderId, order.getOrderCode(), order.getAreaId(), deviceId); + + if (deviceId != null) { + playVoice(deviceId, CleanNotificationConstants.VoiceTemplate.ORDER_COMPLETED, orderId); + } + + sendNotifyMessage(1L, + CleanNotificationConstants.TemplateCode.ORDER_COMPLETED, + CleanNotificationConstants.NotifyParamsBuilder.orderCompletedParams( + order.getOrderCode(), + CleanNotificationConstants.VoiceBuilder.getAreaName(order.getLocation()), + order.getTitle())); + } catch (Exception e) { + log.error("[工单完成通知] 发送失败: orderId={}, deviceId={}", orderId, deviceId, e); + } + } + + @Async("ops-task-executor") + public void playVoiceForNewOrder(Long deviceId) { + playVoice(deviceId, CleanNotificationConstants.VoiceTemplate.NEW_ORDER_SHORT); + } + + private void playVoice(Long deviceId, String message) { + playVoice(deviceId, message, null); + } + + private void playVoice(Long deviceId, String message, Long orderId) { + try { + voiceBroadcastService.broadcastInOrder(deviceId, message, orderId); + log.debug("[语音播报] 调用成功: deviceId={}, message={}", deviceId, message); + } catch (Exception e) { + log.error("[语音播报] 调用失败: deviceId={}, message={}", deviceId, message, e); + } + } + + private void playVoiceUrgent(Long deviceId, String message, Long orderId) { + try { + voiceBroadcastService.broadcastUrgent(deviceId, message, orderId); + log.debug("[语音播报-紧急] 调用成功: deviceId={}, message={}", deviceId, message); + } catch (Exception e) { + log.error("[语音播报-紧急] 调用失败: deviceId={}, message={}", deviceId, message, e); + } + } + + private void sendNotifyMessage(Long userId, String templateCode, Map templateParams) { + try { + NotifySendSingleToUserReqDTO reqDTO = new NotifySendSingleToUserReqDTO(); + reqDTO.setUserId(userId); + reqDTO.setTemplateCode(templateCode); + reqDTO.setTemplateParams(templateParams); + notifyMessageSendApi.sendSingleMessageToMember(reqDTO); + log.debug("[站内信发送成功] userId={}, templateCode={}", userId, templateCode); + } catch (Exception e) { + log.error("[站内信发送失败] userId={}, templateCode={}", userId, templateCode, e); + } + } +} diff --git a/viewsh-module-ops/viewsh-module-ops-api/src/main/java/com/viewsh/module/ops/enums/ManualActionTypeEnum.java b/viewsh-module-ops/viewsh-module-ops-api/src/main/java/com/viewsh/module/ops/enums/ManualActionTypeEnum.java new file mode 100644 index 0000000..2b9d61a --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-api/src/main/java/com/viewsh/module/ops/enums/ManualActionTypeEnum.java @@ -0,0 +1,26 @@ +package com.viewsh.module.ops.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 手动动作类型枚举 + *

+ * 标识管理员手动执行的工单操作类型,用于审计日志的 actionType 字段。 + * + * @author lzh + */ +@Getter +@AllArgsConstructor +public enum ManualActionTypeEnum { + + MANUAL_CREATE("MANUAL_CREATE", "手动创建"), + MANUAL_DISPATCH("MANUAL_DISPATCH", "手动派单"), + MANUAL_UPGRADE_PRIORITY("MANUAL_UPGRADE_PRIORITY", "手动升级优先级"), + MANUAL_CANCEL("MANUAL_CANCEL", "手动取消"), + MANUAL_COMPLETE("MANUAL_COMPLETE", "手动完单"), + ; + + private final String type; + private final String name; +} diff --git a/viewsh-module-ops/viewsh-module-ops-api/src/main/java/com/viewsh/module/ops/enums/OrderActionSourceEnum.java b/viewsh-module-ops/viewsh-module-ops-api/src/main/java/com/viewsh/module/ops/enums/OrderActionSourceEnum.java new file mode 100644 index 0000000..3629089 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-api/src/main/java/com/viewsh/module/ops/enums/OrderActionSourceEnum.java @@ -0,0 +1,24 @@ +package com.viewsh.module.ops.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 工单动作来源枚举 + *

+ * 标识手动动作的发起来源渠道。 + * + * @author lzh + */ +@Getter +@AllArgsConstructor +public enum OrderActionSourceEnum { + + ADMIN_CONSOLE("ADMIN_CONSOLE", "管理后台"), + OPEN_API("OPEN_API", "开放接口"), + SYSTEM_TASK("SYSTEM_TASK", "系统定时任务"), + ; + + private final String source; + private final String name; +} diff --git a/viewsh-module-ops/viewsh-module-ops-api/src/main/java/com/viewsh/module/ops/enums/OrderAuditPayloadKeys.java b/viewsh-module-ops/viewsh-module-ops-api/src/main/java/com/viewsh/module/ops/enums/OrderAuditPayloadKeys.java new file mode 100644 index 0000000..58c177b --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-api/src/main/java/com/viewsh/module/ops/enums/OrderAuditPayloadKeys.java @@ -0,0 +1,32 @@ +package com.viewsh.module.ops.enums; + +/** + * 工单审计 payload 键名常量 + *

+ * 统一 ops_business_event_log.event_payload 中的稳定键名, + * 确保保洁/安保/工程/客服各条线写入一致的 payload 结构。 + * + * @author lzh + */ +public final class OrderAuditPayloadKeys { + + private OrderAuditPayloadKeys() {} + + // ===== 通用字段 ===== + public static final String BUSINESS_TYPE = "businessType"; + public static final String ACTION_TYPE = "actionType"; + public static final String MANUAL = "manual"; + public static final String SOURCE = "source"; + public static final String REASON = "reason"; + + // ===== 派单相关 ===== + public static final String ASSIGNEE_ID = "assigneeId"; + public static final String ASSIGNEE_NAME = "assigneeName"; + + // ===== 优先级相关 ===== + public static final String OLD_PRIORITY = "oldPriority"; + public static final String NEW_PRIORITY = "newPriority"; + + // ===== 完单相关 ===== + public static final String RESULT = "result"; +} diff --git a/viewsh-module-ops/viewsh-module-ops-api/src/main/java/com/viewsh/module/ops/enums/OrderEventTypeEnum.java b/viewsh-module-ops/viewsh-module-ops-api/src/main/java/com/viewsh/module/ops/enums/OrderEventTypeEnum.java new file mode 100644 index 0000000..f877ec1 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-api/src/main/java/com/viewsh/module/ops/enums/OrderEventTypeEnum.java @@ -0,0 +1,34 @@ +package com.viewsh.module.ops.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 工单事件类型枚举 + *

+ * 标准化 ops_order_event.event_type 字段的值, + * 禁止在 controller/service/listener 中散落使用裸字符串。 + * + * @author lzh + */ +@Getter +@AllArgsConstructor +public enum OrderEventTypeEnum { + + CREATE("CREATE", "工单创建"), + ASSIGN("ASSIGN", "工单分配"), + ENQUEUE("QUEUED", "任务入队"), + DISPATCH("DISPATCHED", "任务派发"), + ACCEPT("ACCEPT", "接单确认"), + CONFIRM("CONFIRMED", "工单确认"), + ARRIVE("ARRIVE", "到岗确认"), + PAUSE("PAUSE", "工单暂停"), + RESUME("RESUME", "恢复作业"), + COMPLETE("COMPLETE", "工单完成"), + CANCEL("CANCEL", "工单取消"), + UPGRADE_PRIORITY("UPGRADE_PRIORITY", "优先级升级"), + ; + + private final String type; + private final String name; +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/manual/ManualOrderActionFacade.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/manual/ManualOrderActionFacade.java new file mode 100644 index 0000000..af19d86 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/manual/ManualOrderActionFacade.java @@ -0,0 +1,258 @@ +package com.viewsh.module.ops.core.manual; + +import com.viewsh.module.ops.core.lifecycle.OrderLifecycleManager; +import com.viewsh.module.ops.core.lifecycle.model.OrderTransitionRequest; +import com.viewsh.module.ops.core.lifecycle.model.OrderTransitionResult; +import com.viewsh.module.ops.core.manual.audit.OrderAuditService; +import com.viewsh.module.ops.core.manual.model.*; +import com.viewsh.module.ops.core.manual.strategy.OrderBusinessStrategy; +import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO; +import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper; +import com.viewsh.module.ops.enums.*; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 统一手动动作门面 + *

+ * 所有手动动作(派单/升级/取消/完单)的统一执行骨架: + *

    + *
  1. 查询工单并校验业务类型
  2. + *
  3. 校验基础状态
  4. + *
  5. 调用条线策略做前置校验
  6. + *
  7. 执行状态变更(通过 OrderLifecycleManager)
  8. + *
  9. 写统一审计记录
  10. + *
  11. 调用条线策略处理后置逻辑
  12. + *
+ *

+ * 注意:手动创建(create)因各条线差异过大,不在此门面中统一, + * 由各条线的专用服务直接实现。 + * + * @author lzh + */ +@Slf4j +@Service +public class ManualOrderActionFacade { + + @Resource + private OpsOrderMapper opsOrderMapper; + + @Resource + private OrderLifecycleManager orderLifecycleManager; + + @Resource + private OrderAuditService orderAuditService; + + @Resource + private List strategies; + + // ==================== 手动派单 ==================== + + @Transactional(rollbackFor = Exception.class) + public void dispatch(DispatchOrderCommand cmd) { + // 1. 查单 + 校验 + OpsOrderDO order = getOrderAndValidateType(cmd.getOrderId(), cmd.getBusinessType()); + WorkOrderStatusEnum status = WorkOrderStatusEnum.valueOf(order.getStatus()); + if (status != WorkOrderStatusEnum.PENDING) { + throw new IllegalStateException("当前工单状态不允许手动派单,仅 PENDING 状态可操作"); + } + + // 2. 条线前置校验 + OrderBusinessStrategy strategy = resolveStrategy(cmd.getBusinessType()); + strategy.validateDispatch(cmd, order); + + // 3. 状态变更 + OrderTransitionRequest request = OrderTransitionRequest.builder() + .orderId(cmd.getOrderId()) + .targetStatus(WorkOrderStatusEnum.DISPATCHED) + .assigneeId(cmd.getAssigneeId()) + .assigneeName(cmd.getAssigneeName()) + .operatorType(cmd.getOperator().getOperatorType()) + .operatorId(cmd.getOperator().getOperatorId()) + .reason(cmd.getReason() != null ? cmd.getReason() : "管理员手动派单") + .build(); + OrderTransitionResult result = orderLifecycleManager.transition(request); + if (!result.isSuccess()) { + throw new IllegalStateException("手动派单失败: " + result.getMessage()); + } + + // 4. 更新主表执行人 + order.setAssigneeId(cmd.getAssigneeId()); + order.setAssigneeName(cmd.getAssigneeName()); + opsOrderMapper.updateById(order); + + // 5. 审计 + Map payload = new HashMap<>(); + payload.put(OrderAuditPayloadKeys.ASSIGNEE_ID, cmd.getAssigneeId()); + payload.put(OrderAuditPayloadKeys.ASSIGNEE_NAME, cmd.getAssigneeName()); + if (cmd.getReason() != null) { + payload.put(OrderAuditPayloadKeys.REASON, cmd.getReason()); + } + String message = String.format("管理员手动派单给 %s", + cmd.getAssigneeName() != null ? cmd.getAssigneeName() : cmd.getAssigneeId()); + orderAuditService.record(order, ManualActionTypeEnum.MANUAL_DISPATCH, + cmd.getOperator(), message, payload); + + // 6. 条线后置 + strategy.afterDispatch(cmd, order); + + log.info("[ManualOrderActionFacade] 手动派单完成: orderId={}, assigneeId={}", cmd.getOrderId(), cmd.getAssigneeId()); + } + + // ==================== 手动升级优先级 ==================== + + @Transactional(rollbackFor = Exception.class) + public void upgradePriority(UpgradePriorityCommand cmd) { + // 1. 查单 + 校验 + OpsOrderDO order = getOrderAndValidateType(cmd.getOrderId(), cmd.getBusinessType()); + WorkOrderStatusEnum status = WorkOrderStatusEnum.valueOf(order.getStatus()); + if (status == WorkOrderStatusEnum.COMPLETED || status == WorkOrderStatusEnum.CANCELLED) { + throw new IllegalStateException("已完成或已取消的工单不允许升级优先级"); + } + + // 幂等 + Integer oldPriority = order.getPriority(); + cmd.setOldPriority(oldPriority); + if (oldPriority != null && oldPriority.equals(cmd.getNewPriority())) { + log.info("[ManualOrderActionFacade] 优先级未变化,幂等返回: orderId={}", cmd.getOrderId()); + return; + } + + // 2. 条线前置校验 + OrderBusinessStrategy strategy = resolveStrategy(cmd.getBusinessType()); + strategy.validateUpgradePriority(cmd, order); + + // 3. 更新优先级 + order.setPriority(cmd.getNewPriority()); + opsOrderMapper.updateById(order); + + // 4. 审计 + Map payload = new HashMap<>(); + payload.put(OrderAuditPayloadKeys.OLD_PRIORITY, oldPriority); + payload.put(OrderAuditPayloadKeys.NEW_PRIORITY, cmd.getNewPriority()); + if (cmd.getReason() != null) { + payload.put(OrderAuditPayloadKeys.REASON, cmd.getReason()); + } + String message = String.format("升级优先级 P%d → P%d", + oldPriority != null ? oldPriority : 2, cmd.getNewPriority()); + orderAuditService.record(order, ManualActionTypeEnum.MANUAL_UPGRADE_PRIORITY, + cmd.getOperator(), message, payload); + + // 5. 条线后置 + strategy.afterUpgradePriority(cmd, order); + + log.info("[ManualOrderActionFacade] 升级优先级完成: orderId={}, {} → {}", + cmd.getOrderId(), oldPriority, cmd.getNewPriority()); + } + + // ==================== 手动取消 ==================== + + @Transactional(rollbackFor = Exception.class) + public void cancel(CancelOrderCommand cmd) { + // 1. 查单 + 校验 + OpsOrderDO order = getOrderAndValidateType(cmd.getOrderId(), cmd.getBusinessType()); + WorkOrderStatusEnum status = WorkOrderStatusEnum.valueOf(order.getStatus()); + + // 幂等 + if (status == WorkOrderStatusEnum.CANCELLED) { + log.info("[ManualOrderActionFacade] 工单已取消,幂等返回: orderId={}", cmd.getOrderId()); + return; + } + if (status == WorkOrderStatusEnum.COMPLETED) { + throw new IllegalStateException("已完成的工单不能取消"); + } + + // 2. 条线前置校验 + OrderBusinessStrategy strategy = resolveStrategy(cmd.getBusinessType()); + strategy.validateCancel(cmd, order); + + // 3. 状态变更 + orderLifecycleManager.cancelOrder(cmd.getOrderId(), + cmd.getOperator().getOperatorId(), + cmd.getOperator().getOperatorType(), + cmd.getReason()); + + // 4. 审计 + Map payload = new HashMap<>(); + payload.put(OrderAuditPayloadKeys.REASON, cmd.getReason()); + orderAuditService.record(order, ManualActionTypeEnum.MANUAL_CANCEL, + cmd.getOperator(), "管理员手动取消: " + cmd.getReason(), payload); + + // 5. 条线后置 + strategy.afterCancel(cmd, order); + + log.info("[ManualOrderActionFacade] 手动取消完成: orderId={}", cmd.getOrderId()); + } + + // ==================== 手动完单 ==================== + + @Transactional(rollbackFor = Exception.class) + public void complete(CompleteOrderCommand cmd) { + // 1. 查单 + 校验 + OpsOrderDO order = getOrderAndValidateType(cmd.getOrderId(), cmd.getBusinessType()); + WorkOrderStatusEnum status = WorkOrderStatusEnum.valueOf(order.getStatus()); + + // 幂等 + if (status == WorkOrderStatusEnum.COMPLETED) { + log.info("[ManualOrderActionFacade] 工单已完成,幂等返回: orderId={}", cmd.getOrderId()); + return; + } + if (status == WorkOrderStatusEnum.CANCELLED) { + throw new IllegalStateException("已取消的工单不能完成"); + } + + // 2. 条线前置校验 + OrderBusinessStrategy strategy = resolveStrategy(cmd.getBusinessType()); + strategy.validateComplete(cmd, order); + + // 3. 状态变更 + String remark = cmd.getReason() != null ? cmd.getReason() : "管理员手动完成"; + orderLifecycleManager.completeOrder(cmd.getOrderId(), + cmd.getOperator().getOperatorId(), + cmd.getOperator().getOperatorType(), + remark); + + // 4. 审计 + Map payload = new HashMap<>(); + if (cmd.getReason() != null) { + payload.put(OrderAuditPayloadKeys.REASON, cmd.getReason()); + } + if (cmd.getResult() != null) { + payload.put(OrderAuditPayloadKeys.RESULT, cmd.getResult()); + } + orderAuditService.record(order, ManualActionTypeEnum.MANUAL_COMPLETE, + cmd.getOperator(), remark, payload); + + // 5. 条线后置 + strategy.afterComplete(cmd, order); + + log.info("[ManualOrderActionFacade] 手动完单完成: orderId={}", cmd.getOrderId()); + } + + // ==================== 内部方法 ==================== + + private OpsOrderDO getOrderAndValidateType(Long orderId, String expectedType) { + OpsOrderDO order = opsOrderMapper.selectById(orderId); + if (order == null) { + throw new IllegalArgumentException("工单不存在: orderId=" + orderId); + } + if (!expectedType.equals(order.getOrderType())) { + throw new IllegalStateException( + String.format("工单类型不匹配: 期望 %s,实际 %s", expectedType, order.getOrderType())); + } + return order; + } + + private OrderBusinessStrategy resolveStrategy(String businessType) { + return strategies.stream() + .filter(s -> s.supports(businessType)) + .findFirst() + .orElseThrow(() -> new IllegalStateException("未找到业务条线策略: " + businessType)); + } +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/manual/audit/OrderAuditService.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/manual/audit/OrderAuditService.java new file mode 100644 index 0000000..b881e80 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/manual/audit/OrderAuditService.java @@ -0,0 +1,122 @@ +package com.viewsh.module.ops.core.manual.audit; + +import com.viewsh.module.ops.core.manual.model.OperatorContext; +import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO; +import com.viewsh.module.ops.enums.ManualActionTypeEnum; +import com.viewsh.module.ops.enums.OrderAuditPayloadKeys; +import com.viewsh.module.ops.infrastructure.log.enumeration.EventDomain; +import com.viewsh.module.ops.infrastructure.log.enumeration.LogModule; +import com.viewsh.module.ops.infrastructure.log.enumeration.LogType; +import com.viewsh.module.ops.infrastructure.log.recorder.EventLogRecord; +import com.viewsh.module.ops.infrastructure.log.recorder.EventLogRecorder; +import com.viewsh.module.ops.service.event.OpsOrderEventService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.Map; + +/** + * 统一手动动作审计服务 + *

+ * 所有手动动作通过此服务落库,确保 ops_order_event(时间轴)和 + * ops_business_event_log(审计检索)双写一致。 + *

+ * personId 永远表示真实操作者,被操作目标放入 payload。 + * + * @author lzh + */ +@Slf4j +@Service +public class OrderAuditService { + + @Resource + private OpsOrderEventService opsOrderEventService; + + @Resource + private EventLogRecorder eventLogRecorder; + + /** + * 记录手动动作审计(双写:时间轴 + 业务日志) + * + * @param order 工单 + * @param actionType 手动动作类型 + * @param operator 操作人上下文 + * @param message 可读消息 + * @param payload 审计扩展数据 + */ + public void record(OpsOrderDO order, ManualActionTypeEnum actionType, + OperatorContext operator, String message, + Map payload) { + Long orderId = order.getId(); + + // 1. 时间轴事件(ops_order_event) + String eventType = mapToEventType(actionType); + try { + opsOrderEventService.recordEvent( + orderId, + order.getStatus(), + order.getStatus(), + eventType, + operator.getOperatorType().getType(), + operator.getOperatorId(), + message + ); + } catch (Exception e) { + log.warn("[OrderAuditService] 记录时间轴事件失败: orderId={}, actionType={}", orderId, actionType, e); + } + + // 2. 业务审计日志(ops_business_event_log) + try { + Map auditPayload = new HashMap<>(); + auditPayload.put(OrderAuditPayloadKeys.BUSINESS_TYPE, order.getOrderType()); + auditPayload.put(OrderAuditPayloadKeys.ACTION_TYPE, actionType.getType()); + auditPayload.put(OrderAuditPayloadKeys.MANUAL, true); + auditPayload.put(OrderAuditPayloadKeys.SOURCE, operator.getSource().getSource()); + if (payload != null) { + auditPayload.putAll(payload); + } + + String module = LogModule.fromOrderType(order.getOrderType()); + eventLogRecorder.record(EventLogRecord.builder() + .module(module) + .domain(EventDomain.AUDIT) + .eventType(mapToLogType(actionType).getCode()) + .message(message) + .targetId(orderId) + .targetType("order") + .personId(operator.getOperatorId()) + .payload(auditPayload) + .build()); + } catch (Exception e) { + log.warn("[OrderAuditService] 记录审计日志失败: orderId={}, actionType={}", orderId, actionType, e); + } + } + + /** + * ManualActionTypeEnum → ops_order_event eventType + */ + private String mapToEventType(ManualActionTypeEnum actionType) { + return switch (actionType) { + case MANUAL_CREATE -> "CREATE"; + case MANUAL_DISPATCH -> "DISPATCH"; + case MANUAL_UPGRADE_PRIORITY -> "UPGRADE_PRIORITY"; + case MANUAL_CANCEL -> "CANCEL"; + case MANUAL_COMPLETE -> "COMPLETE"; + }; + } + + /** + * ManualActionTypeEnum → LogType + */ + private LogType mapToLogType(ManualActionTypeEnum actionType) { + return switch (actionType) { + case MANUAL_CREATE -> LogType.ORDER_CREATED; + case MANUAL_DISPATCH -> LogType.ORDER_DISPATCHED; + case MANUAL_UPGRADE_PRIORITY -> LogType.PRIORITY_UPGRADE; + case MANUAL_CANCEL -> LogType.ORDER_CANCELLED; + case MANUAL_COMPLETE -> LogType.ORDER_COMPLETED; + }; + } +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/manual/model/CancelOrderCommand.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/manual/model/CancelOrderCommand.java new file mode 100644 index 0000000..3eaac81 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/manual/model/CancelOrderCommand.java @@ -0,0 +1,26 @@ +package com.viewsh.module.ops.core.manual.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Map; + +/** + * 手动取消工单命令 + * + * @author lzh + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CancelOrderCommand { + + private String businessType; + private Long orderId; + private OperatorContext operator; + private String reason; + private Map payload; +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/manual/model/CompleteOrderCommand.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/manual/model/CompleteOrderCommand.java new file mode 100644 index 0000000..2dd774c --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/manual/model/CompleteOrderCommand.java @@ -0,0 +1,27 @@ +package com.viewsh.module.ops.core.manual.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Map; + +/** + * 手动完单命令 + * + * @author lzh + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CompleteOrderCommand { + + private String businessType; + private Long orderId; + private OperatorContext operator; + private String reason; + private String result; + private Map payload; +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/manual/model/DispatchOrderCommand.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/manual/model/DispatchOrderCommand.java new file mode 100644 index 0000000..8bcbcaf --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/manual/model/DispatchOrderCommand.java @@ -0,0 +1,29 @@ +package com.viewsh.module.ops.core.manual.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Map; + +/** + * 手动派单命令 + * + * @author lzh + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DispatchOrderCommand { + + private String businessType; + private Long orderId; + private OperatorContext operator; + private Long assigneeId; + private String assigneeName; + private String assigneePhone; + private String reason; + private Map payload; +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/manual/model/OperatorContext.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/manual/model/OperatorContext.java new file mode 100644 index 0000000..c26b7ae --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/manual/model/OperatorContext.java @@ -0,0 +1,62 @@ +package com.viewsh.module.ops.core.manual.model; + +import com.viewsh.module.ops.enums.OperatorTypeEnum; +import com.viewsh.module.ops.enums.OrderActionSourceEnum; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 统一操作人上下文 + *

+ * 所有手动动作命令必须携带此上下文,确保审计日志和时间轴事件 + * 能稳定识别真实操作者。 + * + * @author lzh + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class OperatorContext { + + /** 操作人ID */ + private Long operatorId; + + /** 操作人姓名(冗余,可由服务层填充) */ + private String operatorName; + + /** 操作人角色(管理员、安保人员、系统等角色语义) */ + private String operatorRole; + + /** 操作人类型(兼容状态机和事件枚举) */ + private OperatorTypeEnum operatorType; + + /** 动作来源 */ + @Builder.Default + private OrderActionSourceEnum source = OrderActionSourceEnum.ADMIN_CONSOLE; + + /** + * 便捷工厂:管理后台操作 + */ + public static OperatorContext ofAdmin(Long operatorId) { + return OperatorContext.builder() + .operatorId(operatorId) + .operatorType(OperatorTypeEnum.ADMIN) + .source(OrderActionSourceEnum.ADMIN_CONSOLE) + .build(); + } + + /** + * 便捷工厂:管理后台操作(带姓名) + */ + public static OperatorContext ofAdmin(Long operatorId, String operatorName) { + return OperatorContext.builder() + .operatorId(operatorId) + .operatorName(operatorName) + .operatorType(OperatorTypeEnum.ADMIN) + .source(OrderActionSourceEnum.ADMIN_CONSOLE) + .build(); + } +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/manual/model/UpgradePriorityCommand.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/manual/model/UpgradePriorityCommand.java new file mode 100644 index 0000000..164d3bb --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/manual/model/UpgradePriorityCommand.java @@ -0,0 +1,28 @@ +package com.viewsh.module.ops.core.manual.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Map; + +/** + * 手动升级优先级命令 + * + * @author lzh + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UpgradePriorityCommand { + + private String businessType; + private Long orderId; + private OperatorContext operator; + private Integer oldPriority; + private Integer newPriority; + private String reason; + private Map payload; +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/manual/strategy/OrderBusinessStrategy.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/manual/strategy/OrderBusinessStrategy.java new file mode 100644 index 0000000..4b6b603 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/manual/strategy/OrderBusinessStrategy.java @@ -0,0 +1,42 @@ +package com.viewsh.module.ops.core.manual.strategy; + +import com.viewsh.module.ops.core.manual.model.*; +import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO; + +/** + * 工单业务条线策略接口 + *

+ * 每个业务条线(保洁、安保、工程、客服)实现此接口, + * 负责条线特有的前置校验和后置副作用。 + *

+ * 默认方法为空实现,条线按需覆写。 + * + * @author lzh + */ +public interface OrderBusinessStrategy { + + /** + * 是否支持指定业务类型 + */ + boolean supports(String businessType); + + // ==================== 前置校验 ==================== + + default void validateDispatch(DispatchOrderCommand cmd, OpsOrderDO order) {} + + default void validateUpgradePriority(UpgradePriorityCommand cmd, OpsOrderDO order) {} + + default void validateCancel(CancelOrderCommand cmd, OpsOrderDO order) {} + + default void validateComplete(CompleteOrderCommand cmd, OpsOrderDO order) {} + + // ==================== 后置处理 ==================== + + default void afterDispatch(DispatchOrderCommand cmd, OpsOrderDO order) {} + + default void afterUpgradePriority(UpgradePriorityCommand cmd, OpsOrderDO order) {} + + default void afterCancel(CancelOrderCommand cmd, OpsOrderDO order) {} + + default void afterComplete(CompleteOrderCommand cmd, OpsOrderDO order) {} +} diff --git a/viewsh-module-ops/viewsh-module-ops-server/src/main/java/com/viewsh/module/ops/controller/admin/security/vo/SecurityOrderCancelReqVO.java b/viewsh-module-ops/viewsh-module-ops-server/src/main/java/com/viewsh/module/ops/controller/admin/security/vo/SecurityOrderCancelReqVO.java new file mode 100644 index 0000000..6a7f3a1 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-server/src/main/java/com/viewsh/module/ops/controller/admin/security/vo/SecurityOrderCancelReqVO.java @@ -0,0 +1,22 @@ +package com.viewsh.module.ops.controller.admin.security.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * 安保工单手动取消请求 + */ +@Schema(description = "安保工单手动取消请求") +@Data +public class SecurityOrderCancelReqVO { + + @Schema(description = "工单ID", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "工单ID不能为空") + private Long orderId; + + @Schema(description = "取消原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "误报,无需处置") + @NotBlank(message = "取消原因不能为空") + private String reason; +} diff --git a/viewsh-module-ops/viewsh-module-ops-server/src/main/java/com/viewsh/module/ops/controller/admin/security/vo/SecurityOrderDispatchReqVO.java b/viewsh-module-ops/viewsh-module-ops-server/src/main/java/com/viewsh/module/ops/controller/admin/security/vo/SecurityOrderDispatchReqVO.java new file mode 100644 index 0000000..824731a --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-server/src/main/java/com/viewsh/module/ops/controller/admin/security/vo/SecurityOrderDispatchReqVO.java @@ -0,0 +1,27 @@ +package com.viewsh.module.ops.controller.admin.security.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * 安保工单手动派单请求 VO + * + * @author lzh + */ +@Schema(description = "安保工单手动派单请求") +@Data +public class SecurityOrderDispatchReqVO { + + @Schema(description = "工单ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1001") + @NotNull(message = "工单ID不能为空") + private Long orderId; + + @Schema(description = "指定安保人员ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "144") + @NotNull(message = "安保人员ID不能为空") + private Long assigneeId; + + @Schema(description = "派单备注", example = "紧急情况,指定该人员处理") + private String remark; + +} diff --git a/viewsh-module-ops/viewsh-module-ops-server/src/main/java/com/viewsh/module/ops/controller/admin/security/vo/SecurityOrderUpgradePriorityReqVO.java b/viewsh-module-ops/viewsh-module-ops-server/src/main/java/com/viewsh/module/ops/controller/admin/security/vo/SecurityOrderUpgradePriorityReqVO.java new file mode 100644 index 0000000..851535b --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-server/src/main/java/com/viewsh/module/ops/controller/admin/security/vo/SecurityOrderUpgradePriorityReqVO.java @@ -0,0 +1,28 @@ +package com.viewsh.module.ops.controller.admin.security.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * 安保工单优先级升级请求 VO + * + * @author lzh + */ +@Schema(description = "安保工单优先级升级请求") +@Data +public class SecurityOrderUpgradePriorityReqVO { + + @Schema(description = "工单ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1001") + @NotNull(message = "工单ID不能为空") + private Long orderId; + + @Schema(description = "目标优先级(0=P0, 1=P1, 2=P2)", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @NotNull(message = "目标优先级不能为空") + @Min(value = 0, message = "优先级最小为0(P0)") + @Max(value = 2, message = "优先级最大为2(P2)") + private Integer priority; + +} diff --git a/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/service/manual/SecurityOrderBusinessStrategy.java b/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/service/manual/SecurityOrderBusinessStrategy.java new file mode 100644 index 0000000..82f0d88 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/service/manual/SecurityOrderBusinessStrategy.java @@ -0,0 +1,63 @@ +package com.viewsh.module.ops.security.service.manual; + +import com.viewsh.module.ops.api.queue.OrderQueueService; +import com.viewsh.module.ops.core.manual.model.*; +import com.viewsh.module.ops.core.manual.strategy.OrderBusinessStrategy; +import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO; +import com.viewsh.module.ops.enums.WorkOrderStatusEnum; +import com.viewsh.module.ops.enums.WorkOrderTypeEnum; +import com.viewsh.module.ops.security.dal.dataobject.area.OpsAreaSecurityUserDO; +import com.viewsh.module.ops.security.dal.mysql.area.OpsAreaSecurityUserMapper; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import static com.viewsh.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.viewsh.module.ops.enums.ErrorCodeConstants.SECURITY_ASSIGNEE_NOT_BOUND_TO_AREA; + +/** + * 安保条线策略 + *

+ * 处理安保特有的前置校验和后置副作用。 + * + * @author lzh + */ +@Slf4j +@Component +public class SecurityOrderBusinessStrategy implements OrderBusinessStrategy { + + @Resource + private OpsAreaSecurityUserMapper areaSecurityUserMapper; + + @Resource + private OrderQueueService orderQueueService; + + @Override + public boolean supports(String businessType) { + return WorkOrderTypeEnum.SECURITY.getType().equals(businessType); + } + + @Override + public void validateDispatch(DispatchOrderCommand cmd, OpsOrderDO order) { + // 校验目标安保人员:必须绑定到工单所属区域且已启用 + OpsAreaSecurityUserDO binding = areaSecurityUserMapper.selectByAreaIdAndUserId( + order.getAreaId(), cmd.getAssigneeId()); + if (binding == null || !Boolean.TRUE.equals(binding.getEnabled())) { + throw exception(SECURITY_ASSIGNEE_NOT_BOUND_TO_AREA); + } + // 使用绑定记录中的冗余姓名 + if (cmd.getAssigneeName() == null && binding.getUserName() != null) { + cmd.setAssigneeName(binding.getUserName()); + } + } + + @Override + public void afterUpgradePriority(UpgradePriorityCommand cmd, OpsOrderDO order) { + // 如果工单在队列中,触发队列分数重算 + if (WorkOrderStatusEnum.QUEUED.getStatus().equals(order.getStatus()) && order.getAssigneeId() != null) { + orderQueueService.rebuildWaitingTasksByUserId(order.getAssigneeId(), order.getAreaId()); + log.info("[SecurityStrategy] 升级优先级后重算队列: orderId={}, assigneeId={}", + cmd.getOrderId(), order.getAssigneeId()); + } + } +}