refactor(ops): 提取 AreaPathBuilder 公共组件,消除保洁/安保 buildAreaPath 重复代码

将 CleanOrderServiceImpl 中的 buildAreaPath 私有方法提取到 ops-biz 公共层
AreaPathBuilder 组件,供各业务模块(保洁、安保等)共享使用。同时优化:
- 用正则 matches("\d+") 替代 try-catch NumberFormatException 做数字校验
- 增加相邻重复ID去重保护

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
lzh
2026-03-15 10:30:03 +08:00
parent 2a20f7a89f
commit 825c8eecca
5 changed files with 174 additions and 96 deletions

View File

@@ -10,9 +10,7 @@ import com.viewsh.module.ops.core.event.OrderCreatedEvent;
import com.viewsh.module.ops.core.event.OrderEventPublisher;
import com.viewsh.module.ops.core.lifecycle.OrderLifecycleManager;
import com.viewsh.module.ops.core.lifecycle.model.OrderTransitionRequest;
import com.viewsh.module.ops.dal.dataobject.area.OpsBusAreaDO;
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;
@@ -21,6 +19,7 @@ import com.viewsh.module.ops.environment.dal.dataobject.CleanOrderAutoCreateReqD
import com.viewsh.module.ops.environment.dal.dataobject.workorder.OpsOrderCleanExtDO;
import com.viewsh.module.ops.environment.dal.mysql.workorder.OpsOrderCleanExtMapper;
import com.viewsh.module.ops.environment.integration.listener.CleanOrderEventListener;
import com.viewsh.module.ops.infrastructure.area.AreaPathBuilder;
import com.viewsh.module.ops.infrastructure.code.OrderCodeGenerator;
import com.viewsh.module.ops.infrastructure.id.OrderIdGenerator;
import jakarta.annotation.Resource;
@@ -30,11 +29,7 @@ import org.springframework.transaction.annotation.Transactional;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 保洁工单服务实现(重构版)
@@ -88,7 +83,7 @@ public class CleanOrderServiceImpl implements CleanOrderService {
private ObjectMapper objectMapper;
@Resource
private OpsBusAreaMapper opsBusAreaMapper;
private AreaPathBuilder areaPathBuilder;
// ==================== 工单创建 ====================
@@ -109,7 +104,7 @@ public class CleanOrderServiceImpl implements CleanOrderService {
.priority(createReq.getPriority() != null ? createReq.getPriority() : PriorityEnum.P2.getPriority())
.status(WorkOrderStatusEnum.PENDING.getStatus())
.areaId(createReq.getAreaId())
.location(buildAreaPath(createReq.getAreaId()))
.location(areaPathBuilder.buildPath(createReq.getAreaId()))
.sourceType(createReq.getSourceType() != null ? createReq.getSourceType() : "TRAFFIC")
// IoT集成字段
.triggerSource(createReq.getTriggerSource())
@@ -448,91 +443,4 @@ public class CleanOrderServiceImpl implements CleanOrderService {
}
}
/**
* 根据区域ID构建完整路径如"园区/A栋/B层/电梯厅"
*
* @param areaId 区域ID
* @return 完整区域路径,用 "/" 分隔
*/
private String buildAreaPath(Long areaId) {
// 1. 参数校验
if (areaId == null) {
return null;
}
// 2. 查询当前区域
OpsBusAreaDO area = opsBusAreaMapper.selectById(areaId);
if (area == null) {
log.warn("区域不存在: areaId={}", areaId);
return null;
}
// 3. 无父级路径,直接返回区域名称
String parentPath = area.getParentPath();
if (StrUtil.isEmpty(parentPath)) {
return area.getAreaName();
}
// 4. 解析父级ID列表使用 Stream 过滤无效ID
List<Long> parentIds = Arrays.stream(parentPath.split("/"))
.filter(StrUtil::isNotBlank) // 过滤空字符串
.filter(pid -> {
try {
Long.parseLong(pid);
return true;
} catch (NumberFormatException e) {
log.warn("父级区域ID格式错误: areaId={}, parentId={}", areaId, pid);
return false;
}
})
.map(Long::parseLong)
.filter(pid -> !pid.equals(areaId)) // 排除当前区域,避免循环引用
.collect(Collectors.toList());
// 5. 在ID层面去重相邻重复的ID只去除数据错误导致的重复保留不同ID的相同名称
List<Long> deduplicatedIds = new ArrayList<>();
Long lastId = null;
for (Long parentId : parentIds) {
if (!parentId.equals(lastId)) {
deduplicatedIds.add(parentId);
lastId = parentId;
} else {
log.warn("检测到parent_path中重复的ID: areaId={}, duplicateId={}", areaId, parentId);
}
}
// 6. 无有效父级,直接返回区域名称
if (deduplicatedIds.isEmpty()) {
return area.getAreaName();
}
// 7. 批量查询所有父级区域(避免 N+1 查询)
List<OpsBusAreaDO> parents = opsBusAreaMapper.selectBatchIds(deduplicatedIds);
if (parents == null || parents.isEmpty()) {
log.warn("未找到父级区域: areaId={}, parentIds={}", areaId, deduplicatedIds);
return area.getAreaName();
}
// 8. 构建ID到区域的映射
Map<Long, String> parentNameMap = parents.stream()
.collect(Collectors.toMap(
OpsBusAreaDO::getId,
OpsBusAreaDO::getAreaName,
(existing, replacement) -> existing // 处理重复key
));
// 9. 按顺序拼接区域路径保持ID顺序
List<String> pathSegments = deduplicatedIds.stream()
.filter(parentNameMap::containsKey) // 过滤掉不存在的父级
.map(parentNameMap::get)
.collect(Collectors.toList());
// 10. 拼接完整路径
String path = String.join("/", pathSegments);
if (StrUtil.isBlank(path)) {
return area.getAreaName();
}
return path + "/" + area.getAreaName();
}
}

View File

@@ -0,0 +1,108 @@
package com.viewsh.module.ops.infrastructure.area;
import cn.hutool.core.util.StrUtil;
import com.viewsh.module.ops.dal.dataobject.area.OpsBusAreaDO;
import com.viewsh.module.ops.dal.mysql.area.OpsBusAreaMapper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 区域路径构建器
* <p>
* 根据 areaId 拼接完整的区域路径,如 "A园区/A栋/3层/电梯厅"。
* 供保洁、安保等各业务模块共享使用。
*
* @author lzh
*/
@Slf4j
@Component
public class AreaPathBuilder {
@Resource
private OpsBusAreaMapper opsBusAreaMapper;
/**
* 根据已查询到的区域对象构建完整路径
*
* @param area 区域对象(非 null
* @return 完整区域路径,用 "/" 分隔
*/
public String buildPath(OpsBusAreaDO area) {
if (area == null) {
return null;
}
String parentPath = area.getParentPath();
if (StrUtil.isEmpty(parentPath)) {
return area.getAreaName();
}
// 解析父级ID列表
List<Long> parentIds = Arrays.stream(parentPath.split("/"))
.filter(StrUtil::isNotBlank)
.filter(pid -> pid.matches("\\d+"))
.map(Long::parseLong)
.filter(pid -> !pid.equals(area.getId()))
.collect(Collectors.toList());
// ID层面去重相邻重复数据异常保护
List<Long> deduplicatedIds = new ArrayList<>();
Long lastId = null;
for (Long parentId : parentIds) {
if (!parentId.equals(lastId)) {
deduplicatedIds.add(parentId);
lastId = parentId;
} else {
log.warn("检测到parent_path中重复的ID: areaId={}, duplicateId={}", area.getId(), parentId);
}
}
if (deduplicatedIds.isEmpty()) {
return area.getAreaName();
}
// 批量查询父级区域
List<OpsBusAreaDO> parents = opsBusAreaMapper.selectBatchIds(deduplicatedIds);
if (parents == null || parents.isEmpty()) {
log.warn("未找到父级区域: areaId={}, parentIds={}", area.getId(), deduplicatedIds);
return area.getAreaName();
}
Map<Long, String> parentNameMap = parents.stream()
.collect(Collectors.toMap(OpsBusAreaDO::getId, OpsBusAreaDO::getAreaName, (a, b) -> a));
List<String> pathSegments = deduplicatedIds.stream()
.filter(parentNameMap::containsKey)
.map(parentNameMap::get)
.collect(Collectors.toList());
String path = String.join("/", pathSegments);
return StrUtil.isBlank(path) ? area.getAreaName() : path + "/" + area.getAreaName();
}
/**
* 根据 areaId 查询并构建完整路径
*
* @param areaId 区域ID
* @return 完整区域路径,用 "/" 分隔;区域不存在时返回 null
*/
public String buildPath(Long areaId) {
if (areaId == null) {
return null;
}
OpsBusAreaDO area = opsBusAreaMapper.selectById(areaId);
if (area == null) {
log.warn("区域不存在: areaId={}", areaId);
return null;
}
return buildPath(area);
}
}

View File

@@ -0,0 +1,20 @@
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 SecurityOrderFalseAlarmReqVO {
@Schema(description = "工单ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1001")
@NotNull(message = "工单ID不能为空")
private Long orderId;
}

View File

@@ -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.NotNull;
import lombok.Data;
/**
* 安保工单 - 仅含工单ID的通用请求 VO
* <p>
* 用于误报标记、开放接口确认等只需要工单ID的场景
*
* @author lzh
*/
@Schema(description = "安保工单ID请求")
@Data
public class SecurityOrderIdReqVO {
@Schema(description = "工单ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1001")
@NotNull(message = "工单ID不能为空")
private Long orderId;
}

View File

@@ -0,0 +1,20 @@
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无需传 userId默认使用已分配人员
*
* @author lzh
*/
@Schema(description = "安保工单确认请求(开放接口)")
@Data
public class SecurityOrderOpenConfirmReqVO {
@Schema(description = "工单ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1001")
@NotNull(message = "工单ID不能为空")
private Long orderId;
}