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
+ * 标识管理员手动执行的工单操作类型,用于审计日志的 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;
+
+/**
+ * 统一手动动作门面
+ *
+ * 所有手动动作(派单/升级/取消/完单)的统一执行骨架:
+ *
+ * 注意:手动创建(create)因各条线差异过大,不在此门面中统一,
+ * 由各条线的专用服务直接实现。
+ *
+ * @author lzh
+ */
+@Slf4j
+@Service
+public class ManualOrderActionFacade {
+
+ @Resource
+ private OpsOrderMapper opsOrderMapper;
+
+ @Resource
+ private OrderLifecycleManager orderLifecycleManager;
+
+ @Resource
+ private OrderAuditService orderAuditService;
+
+ @Resource
+ private List
+ * 所有手动动作通过此服务落库,确保 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
+ * 所有手动动作命令必须携带此上下文,确保审计日志和时间轴事件
+ * 能稳定识别真实操作者。
+ *
+ * @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
+ * 每个业务条线(保洁、安保、工程、客服)实现此接口,
+ * 负责条线特有的前置校验和后置副作用。
+ *
+ * 默认方法为空实现,条线按需覆写。
+ *
+ * @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());
+ }
+ }
+}
+ *
+ *