diff --git a/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/service/area/OpsAreaSecurityUserService.java b/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/service/area/OpsAreaSecurityUserService.java new file mode 100644 index 0000000..39eb00e --- /dev/null +++ b/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/service/area/OpsAreaSecurityUserService.java @@ -0,0 +1,51 @@ +package com.viewsh.module.ops.security.service.area; + +import com.viewsh.module.ops.security.dal.dataobject.area.OpsAreaSecurityUserDO; + +import java.util.List; + +/** + * 区域-安保人员绑定服务 + * + * @author lzh + */ +public interface OpsAreaSecurityUserService { + + /** + * 查询区域绑定的启用安保人员 + * + * @param areaId 区域ID + * @return 安保人员列表 + */ + List listByAreaId(Long areaId); + + /** + * 绑定安保人员到区域 + * + * @param areaId 区域ID + * @param userId 用户ID + * @param userName 用户姓名 + * @param teamId 班组ID + * @param sort 排序值 + * @return 绑定记录ID + */ + Long bindUser(Long areaId, Long userId, String userName, Long teamId, Integer sort); + + /** + * 更新绑定信息 + * + * @param id 绑定记录ID + * @param enabled 是否启用 + * @param sort 排序值 + * @param teamId 班组ID + */ + void updateBinding(Long id, Boolean enabled, Integer sort, Long teamId); + + /** + * 解除绑定 + * + * @param id 绑定记录ID + */ + void unbindUser(Long id); + +} diff --git a/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/service/area/OpsAreaSecurityUserServiceImpl.java b/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/service/area/OpsAreaSecurityUserServiceImpl.java new file mode 100644 index 0000000..137bb44 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/service/area/OpsAreaSecurityUserServiceImpl.java @@ -0,0 +1,94 @@ +package com.viewsh.module.ops.security.service.area; + +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.dao.DuplicateKeyException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static com.viewsh.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.viewsh.module.ops.enums.ErrorCodeConstants.*; + +/** + * 区域-安保人员绑定服务实现 + * + * @author lzh + */ +@Slf4j +@Service +public class OpsAreaSecurityUserServiceImpl implements OpsAreaSecurityUserService { + + @Resource + private OpsAreaSecurityUserMapper areaSecurityUserMapper; + + @Override + public List listByAreaId(Long areaId) { + return areaSecurityUserMapper.selectListByAreaId(areaId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long bindUser(Long areaId, Long userId, String userName, Long teamId, Integer sort) { + // 唯一性校验 + OpsAreaSecurityUserDO existing = areaSecurityUserMapper.selectByAreaIdAndUserId(areaId, userId); + if (existing != null) { + throw exception(SECURITY_AREA_USER_DUPLICATE); + } + + OpsAreaSecurityUserDO record = OpsAreaSecurityUserDO.builder() + .areaId(areaId) + .userId(userId) + .userName(userName) + .teamId(teamId) + .enabled(true) + .sort(sort != null ? sort : 0) + .build(); + try { + areaSecurityUserMapper.insert(record); + } catch (DuplicateKeyException e) { + throw exception(SECURITY_AREA_USER_DUPLICATE); + } + + log.info("绑定安保人员到区域: areaId={}, userId={}, userName={}", areaId, userId, userName); + return record.getId(); + } + + @Override + public void updateBinding(Long id, Boolean enabled, Integer sort, Long teamId) { + OpsAreaSecurityUserDO existing = areaSecurityUserMapper.selectById(id); + if (existing == null) { + throw exception(SECURITY_AREA_USER_NOT_FOUND); + } + + OpsAreaSecurityUserDO update = new OpsAreaSecurityUserDO(); + update.setId(id); + if (enabled != null) { + update.setEnabled(enabled); + } + if (sort != null) { + update.setSort(sort); + } + if (teamId != null) { + update.setTeamId(teamId); + } + areaSecurityUserMapper.updateById(update); + + log.info("更新安保人员绑定: id={}, enabled={}, sort={}", id, enabled, sort); + } + + @Override + public void unbindUser(Long id) { + OpsAreaSecurityUserDO existing = areaSecurityUserMapper.selectById(id); + if (existing == null) { + throw exception(SECURITY_AREA_USER_NOT_FOUND); + } + + areaSecurityUserMapper.deleteById(id); + log.info("解除安保人员绑定: id={}, areaId={}, userId={}", id, existing.getAreaId(), existing.getUserId()); + } + +} diff --git a/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/service/dispatch/SecurityAreaAssignStrategy.java b/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/service/dispatch/SecurityAreaAssignStrategy.java new file mode 100644 index 0000000..f007585 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/service/dispatch/SecurityAreaAssignStrategy.java @@ -0,0 +1,76 @@ +package com.viewsh.module.ops.security.service.dispatch; + +import cn.hutool.core.collection.CollUtil; +import com.viewsh.module.ops.core.dispatch.DispatchEngine; +import com.viewsh.module.ops.core.dispatch.model.AssigneeRecommendation; +import com.viewsh.module.ops.core.dispatch.model.OrderDispatchContext; +import com.viewsh.module.ops.core.dispatch.strategy.AssignStrategy; +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.PostConstruct; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * 安保工单区域分配策略 + *

+ * 根据区域绑定的安保人员随机分配。 + * + * @author lzh + */ +@Slf4j +@Component +public class SecurityAreaAssignStrategy implements AssignStrategy { + + private static final String STRATEGY_NAME = "security_area_user"; + private static final String BUSINESS_TYPE = WorkOrderTypeEnum.SECURITY.getType(); + + @Resource + private DispatchEngine dispatchEngine; + + @Resource + private OpsAreaSecurityUserMapper areaSecurityUserMapper; + + @PostConstruct + public void init() { + dispatchEngine.registerAssignStrategy(BUSINESS_TYPE, this); + log.info("注册安保分配策略: {}", STRATEGY_NAME); + } + + @Override + public String getName() { + return STRATEGY_NAME; + } + + @Override + public String getSupportedBusinessType() { + return BUSINESS_TYPE; + } + + @Override + public AssigneeRecommendation recommend(OrderDispatchContext context) { + Long areaId = context.getAreaId(); + if (areaId == null) { + log.warn("安保派单缺少区域ID: orderId={}", context.getOrderId()); + return AssigneeRecommendation.none(); + } + + List users = areaSecurityUserMapper.selectListByAreaId(areaId); + if (CollUtil.isEmpty(users)) { + log.info("区域 {} 无绑定安保人员,工单 {} 等待手动分配", areaId, context.getOrderId()); + return AssigneeRecommendation.none(); + } + + // 选择 sort 值最小的人员(sort 越小优先级越高,由 Mapper 已按 sort ASC 排序) + OpsAreaSecurityUserDO chosen = users.get(0); + AssigneeRecommendation recommendation = AssigneeRecommendation.of( + chosen.getUserId(), chosen.getUserName(), 50, "区域排序优先分配"); + recommendation.setAreaId(areaId); + return recommendation; + } + +} diff --git a/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/service/dispatch/SecurityScheduleStrategy.java b/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/service/dispatch/SecurityScheduleStrategy.java new file mode 100644 index 0000000..99a0f56 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/service/dispatch/SecurityScheduleStrategy.java @@ -0,0 +1,59 @@ +package com.viewsh.module.ops.security.service.dispatch; + +import com.viewsh.module.ops.core.dispatch.DispatchEngine; +import com.viewsh.module.ops.core.dispatch.model.DispatchDecision; +import com.viewsh.module.ops.core.dispatch.model.DispatchPath; +import com.viewsh.module.ops.core.dispatch.model.OrderDispatchContext; +import com.viewsh.module.ops.core.dispatch.strategy.ScheduleStrategy; +import com.viewsh.module.ops.enums.WorkOrderTypeEnum; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 安保工单调度策略 + *

+ * 安保工单调度相对简单: + * - 有可用人员 → 直接派单 + * - 人员忙碌 → 入队等待 + * + * @author lzh + */ +@Slf4j +@Component +public class SecurityScheduleStrategy implements ScheduleStrategy { + + private static final String STRATEGY_NAME = "security_schedule"; + private static final String BUSINESS_TYPE = WorkOrderTypeEnum.SECURITY.getType(); + + @Resource + private DispatchEngine dispatchEngine; + + @PostConstruct + public void init() { + dispatchEngine.registerScheduleStrategy(BUSINESS_TYPE, this); + log.info("注册安保调度策略: {}", STRATEGY_NAME); + } + + @Override + public String getName() { + return STRATEGY_NAME; + } + + @Override + public String getSupportedBusinessType() { + return BUSINESS_TYPE; + } + + @Override + public DispatchDecision decide(OrderDispatchContext context) { + // 安保工单默认直接派单 + // DispatchEngine 会根据执行人状态自动选择路径 + return DispatchDecision.builder() + .path(DispatchPath.DIRECT_DISPATCH) + .reason("安保工单直接派单") + .build(); + } + +} diff --git a/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/service/securityorder/SecurityOrderCompleteReqDTO.java b/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/service/securityorder/SecurityOrderCompleteReqDTO.java new file mode 100644 index 0000000..ba1a094 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/service/securityorder/SecurityOrderCompleteReqDTO.java @@ -0,0 +1,37 @@ +package com.viewsh.module.ops.security.service.securityorder; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 安保工单人工完单请求 DTO(Service 层内部使用) + * + * @author lzh + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SecurityOrderCompleteReqDTO { + + @NotNull(message = "工单ID不能为空") + private Long orderId; + + @NotBlank(message = "处理结果不能为空") + private String result; + + private List resultImgUrls; + + /** + * 操作人ID(由 Controller 层填充) + */ + @NotNull(message = "操作人ID不能为空") + private Long operatorId; + +} diff --git a/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/service/securityorder/SecurityOrderCreateReqDTO.java b/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/service/securityorder/SecurityOrderCreateReqDTO.java new file mode 100644 index 0000000..56d90ed --- /dev/null +++ b/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/service/securityorder/SecurityOrderCreateReqDTO.java @@ -0,0 +1,47 @@ +package com.viewsh.module.ops.security.service.securityorder; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 安保工单创建请求 DTO(Service 层内部使用) + * + * @author lzh + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SecurityOrderCreateReqDTO { + + @NotBlank(message = "工单标题不能为空") + private String title; + + private String description; + + private Integer priority; + + @NotNull(message = "区域ID不能为空") + private Long areaId; + + private String location; + + // ==================== 告警来源 ==================== + + private String alarmId; + + private String alarmType; + + private String cameraId; + + private String roiId; + + private String imageUrl; + + private String sourceType; + +} diff --git a/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/service/securityorder/SecurityOrderExtQueryHandler.java b/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/service/securityorder/SecurityOrderExtQueryHandler.java new file mode 100644 index 0000000..84137af --- /dev/null +++ b/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/service/securityorder/SecurityOrderExtQueryHandler.java @@ -0,0 +1,102 @@ +package com.viewsh.module.ops.security.service.securityorder; + +import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO; +import com.viewsh.module.ops.security.dal.dataobject.workorder.OpsOrderSecurityExtDO; +import com.viewsh.module.ops.security.dal.mysql.workorder.OpsOrderSecurityExtMapper; +import com.viewsh.module.ops.service.OrderDetailVO; +import com.viewsh.module.ops.service.OrderExtQueryHandler; +import com.viewsh.module.ops.service.OrderSummaryVO; +import com.viewsh.module.ops.enums.WorkOrderTypeEnum; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 安保工单扩展查询处理器 + *

+ * 实现 OrderExtQueryHandler 接口,为工单中心查询提供安保扩展信息加载能力 + * + * @author lzh + */ +@Slf4j +@Component +public class SecurityOrderExtQueryHandler implements OrderExtQueryHandler { + + @Resource + private OpsOrderSecurityExtMapper securityExtMapper; + + private static final String ORDER_TYPE_SECURITY = WorkOrderTypeEnum.SECURITY.getType(); + + @Override + public boolean supports(String orderType) { + return ORDER_TYPE_SECURITY.equals(orderType); + } + + @Override + public void enrichWithExtInfo(OrderSummaryVO vo, Long orderId) { + OpsOrderSecurityExtDO securityExt = securityExtMapper.selectByOpsOrderId(orderId); + if (securityExt != null) { + vo.setExtInfo(buildExtInfoMap(securityExt)); + } + } + + @Override + public OrderDetailVO buildDetailVO(OpsOrderDO order) { + OpsOrderSecurityExtDO securityExt = securityExtMapper.selectByOpsOrderId(order.getId()); + + OrderDetailVO vo = OrderDetailVO.builder() + .id(order.getId()) + .orderCode(order.getOrderCode()) + .orderType(order.getOrderType()) + .sourceType(order.getSourceType()) + .title(order.getTitle()) + .description(order.getDescription()) + .priority(order.getPriority()) + .status(order.getStatus()) + .areaId(order.getAreaId()) + .location(order.getLocation()) + .urgentReason(order.getUrgentReason()) + .assigneeId(order.getAssigneeId()) + .assigneeName(order.getAssigneeName()) + .inspectorId(order.getInspectorId()) + .startTime(order.getStartTime()) + .endTime(order.getEndTime()) + .qualityScore(order.getQualityScore()) + .qualityComment(order.getQualityComment()) + .responseSeconds(order.getResponseSeconds()) + .completionSeconds(order.getCompletionSeconds()) + .createTime(order.getCreateTime()) + .updateTime(order.getUpdateTime()) + .build(); + + if (securityExt != null) { + vo.setExtInfo(buildExtInfoMap(securityExt)); + } + + return vo; + } + + /** + * 构建安保工单扩展信息 Map(列表/详情统一使用) + */ + private Map buildExtInfoMap(OpsOrderSecurityExtDO ext) { + Map extInfo = new LinkedHashMap<>(16); + extInfo.put("alarmId", ext.getAlarmId()); + extInfo.put("alarmType", ext.getAlarmType()); + extInfo.put("cameraId", ext.getCameraId()); + extInfo.put("roiId", ext.getRoiId()); + extInfo.put("imageUrl", ext.getImageUrl()); + extInfo.put("assignedUserId", ext.getAssignedUserId()); + extInfo.put("assignedUserName", ext.getAssignedUserName()); + extInfo.put("assignedTeamId", ext.getAssignedTeamId()); + extInfo.put("result", ext.getResult()); + extInfo.put("resultImgUrls", ext.getResultImgUrls()); + extInfo.put("dispatchedTime", ext.getDispatchedTime()); + extInfo.put("confirmedTime", ext.getConfirmedTime()); + extInfo.put("completedTime", ext.getCompletedTime()); + return extInfo; + } +} 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 new file mode 100644 index 0000000..245d602 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/service/securityorder/SecurityOrderService.java @@ -0,0 +1,55 @@ +package com.viewsh.module.ops.security.service.securityorder; + +import com.viewsh.module.ops.security.dal.dataobject.workorder.OpsOrderSecurityExtDO; + +/** + * 安保工单服务接口 + *

+ * 提供安保工单的创建、确认、自动完单、人工完单等能力 + * + * @author lzh + */ +public interface SecurityOrderService { + + /** + * 创建安保工单(对外接口,由外部告警系统调用) + *

+ * 流程:创建主表 + 扩展表 → 自动派单 + * + * @param createReq 创建请求 + * @return 工单ID + */ + Long createSecurityOrder(SecurityOrderCreateReqDTO createReq); + + /** + * 确认工单(安保人员确认接单) + * + * @param orderId 工单ID + * @param userId 安保人员user_id + */ + void confirmOrder(Long orderId, Long userId); + + /** + * 自动完单(对方系统调用,无需提交结果) + * + * @param orderId 工单ID + * @param remark 备注 + */ + void autoCompleteOrder(Long orderId, String remark); + + /** + * 人工完单(安保人员提交处理结果) + * + * @param req 完单请求(包含 result + resultImgUrls) + */ + void manualCompleteOrder(SecurityOrderCompleteReqDTO req); + + /** + * 根据工单ID查询安保扩展信息 + * + * @param opsOrderId 工单ID + * @return 扩展信息 + */ + OpsOrderSecurityExtDO getSecurityExt(Long opsOrderId); + +} 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 new file mode 100644 index 0000000..206a44c --- /dev/null +++ b/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/service/securityorder/SecurityOrderServiceImpl.java @@ -0,0 +1,209 @@ +package com.viewsh.module.ops.security.service.securityorder; + +import cn.hutool.core.util.StrUtil; +import com.viewsh.module.ops.core.event.OrderEventPublisher; +import com.viewsh.module.ops.core.event.OrderCreatedEvent; +import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO; +import com.viewsh.module.ops.dal.mysql.area.OpsBusAreaMapper; +import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper; +import com.viewsh.module.ops.enums.OperatorTypeEnum; +import com.viewsh.module.ops.enums.PriorityEnum; +import com.viewsh.module.ops.enums.SourceTypeEnum; +import com.viewsh.module.ops.enums.WorkOrderStatusEnum; +import com.viewsh.module.ops.enums.WorkOrderTypeEnum; +import com.viewsh.module.ops.infrastructure.code.OrderCodeGenerator; +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; +// 注意:confirm/complete 的业务日志由 SecurityOrderEventListener 统一记录 +// 本类仅记录 CREATE 日志(创建不经过状态变更事件) +import static com.viewsh.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.viewsh.module.ops.enums.ErrorCodeConstants.*; +import com.viewsh.module.ops.infrastructure.id.OrderIdGenerator; +import com.viewsh.module.ops.security.dal.dataobject.workorder.OpsOrderSecurityExtDO; +import com.viewsh.module.ops.security.dal.mysql.workorder.OpsOrderSecurityExtMapper; +import com.viewsh.module.ops.service.fsm.OrderStateMachine; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * 安保工单服务实现 + * + * @author lzh + */ +@Slf4j +@Service +public class SecurityOrderServiceImpl implements SecurityOrderService { + + @Resource + private OpsOrderMapper opsOrderMapper; + + @Resource + private OpsOrderSecurityExtMapper securityExtMapper; + + @Resource + private OrderIdGenerator orderIdGenerator; + + @Resource + private OrderCodeGenerator orderCodeGenerator; + + @Resource + private OrderEventPublisher orderEventPublisher; + + @Resource + private OpsBusAreaMapper opsBusAreaMapper; + + @Resource + private OrderStateMachine orderStateMachine; + + @Resource + private EventLogRecorder eventLogRecorder; + + private static final String BUSINESS_TYPE = WorkOrderTypeEnum.SECURITY.getType(); + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createSecurityOrder(SecurityOrderCreateReqDTO createReq) { + // 0. 校验区域是否存在 + if (opsBusAreaMapper.selectById(createReq.getAreaId()) == null) { + throw exception(AREA_NOT_FOUND); + } + + // 1. 生成ID和编号 + Long orderId = orderIdGenerator.generate(); + String orderCode = orderCodeGenerator.generate(BUSINESS_TYPE); + + // 2. 确定来源类型 + String sourceType = StrUtil.isNotBlank(createReq.getSourceType()) + ? createReq.getSourceType() + : (StrUtil.isNotBlank(createReq.getAlarmId()) ? SourceTypeEnum.ALARM.getType() : SourceTypeEnum.MANUAL.getType()); + + // 3. 构建主表记录 + OpsOrderDO order = OpsOrderDO.builder() + .id(orderId) + .orderCode(orderCode) + .orderType(BUSINESS_TYPE) + .sourceType(sourceType) + .title(createReq.getTitle()) + .description(createReq.getDescription()) + .priority(createReq.getPriority() != null ? createReq.getPriority() : PriorityEnum.P2.getPriority()) + .status(WorkOrderStatusEnum.PENDING.getStatus()) + .areaId(createReq.getAreaId()) + .location(createReq.getLocation()) + .build(); + opsOrderMapper.insert(order); + + // 4. 构建扩展表记录 + OpsOrderSecurityExtDO securityExt = OpsOrderSecurityExtDO.builder() + .opsOrderId(orderId) + .alarmId(createReq.getAlarmId()) + .alarmType(createReq.getAlarmType()) + .cameraId(createReq.getCameraId()) + .roiId(createReq.getRoiId()) + .imageUrl(createReq.getImageUrl()) + .build(); + securityExtMapper.insert(securityExt); + + // 5. 发布工单创建事件(触发自动派单) + OrderCreatedEvent event = OrderCreatedEvent.builder() + .orderId(orderId) + .orderType(BUSINESS_TYPE) + .orderCode(orderCode) + .title(createReq.getTitle()) + .areaId(createReq.getAreaId()) + .priority(order.getPriority()) + .createTime(order.getCreateTime()) + .build(); + orderEventPublisher.publishOrderCreated(event); + + // 6. 记录业务日志 + eventLogRecorder.record(EventLogRecord.builder() + .module(LogModule.SECURITY) + .domain(EventDomain.DISPATCH) + .eventType(LogType.ORDER_CREATED.getCode()) + .message("安保工单创建") + .targetId(orderId) + .targetType("order") + .build()); + + log.info("创建安保工单成功: orderId={}, orderCode={}, alarmId={}, areaId={}", + orderId, orderCode, createReq.getAlarmId(), createReq.getAreaId()); + + return orderId; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void confirmOrder(Long orderId, Long userId) { + OpsOrderDO order = getOrderOrThrow(orderId); + validateOrderType(order); + + // 状态转换:DISPATCHED → CONFIRMED(扩展表时间 + 业务日志由 EventListener 统一记录) + orderStateMachine.transition(order, WorkOrderStatusEnum.CONFIRMED, + OperatorTypeEnum.SECURITY_GUARD, userId, "安保人员确认接单"); + + log.info("安保工单确认: orderId={}, userId={}", orderId, userId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void autoCompleteOrder(Long orderId, String remark) { + OpsOrderDO order = getOrderOrThrow(orderId); + validateOrderType(order); + + // 状态转换 → COMPLETED(扩展表时间 + 业务日志由 EventListener 统一记录,主表 endTime 由状态机统一设置) + orderStateMachine.transition(order, WorkOrderStatusEnum.COMPLETED, + OperatorTypeEnum.SYSTEM, null, + StrUtil.isNotBlank(remark) ? remark : "系统自动完单"); + + log.info("安保工单自动完单: orderId={}", orderId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void manualCompleteOrder(SecurityOrderCompleteReqDTO req) { + OpsOrderDO order = getOrderOrThrow(req.getOrderId()); + validateOrderType(order); + + // 状态转换 → COMPLETED(扩展表 completedTime + 业务日志由 EventListener 统一记录,主表 endTime 由状态机统一设置) + orderStateMachine.transition(order, WorkOrderStatusEnum.COMPLETED, + OperatorTypeEnum.SECURITY_GUARD, req.getOperatorId(), "安保人员提交处理结果"); + + // 更新扩展表:结果 + 图片 + OpsOrderSecurityExtDO extUpdate = new OpsOrderSecurityExtDO(); + extUpdate.setOpsOrderId(req.getOrderId()); + extUpdate.setResult(req.getResult()); + if (req.getResultImgUrls() != null && !req.getResultImgUrls().isEmpty()) { + extUpdate.setResultImgUrls(cn.hutool.json.JSONUtil.toJsonStr(req.getResultImgUrls())); + } + securityExtMapper.insertOrUpdateSelective(extUpdate); + + log.info("安保工单人工完单: orderId={}", req.getOrderId()); + } + + @Override + public OpsOrderSecurityExtDO getSecurityExt(Long opsOrderId) { + return securityExtMapper.selectByOpsOrderId(opsOrderId); + } + + // ==================== 内部方法 ==================== + + private OpsOrderDO getOrderOrThrow(Long orderId) { + OpsOrderDO order = opsOrderMapper.selectById(orderId); + if (order == null) { + throw exception(SECURITY_ORDER_NOT_FOUND); + } + return order; + } + + private void validateOrderType(OpsOrderDO order) { + if (!BUSINESS_TYPE.equals(order.getOrderType())) { + throw exception(SECURITY_ORDER_TYPE_MISMATCH); + } + } + +}