feat(ops,iot): 保洁前端 API 层和区域管理新增

新增保洁业务前端 API 接口层(工牌、工单、仪表盘)和运营区域管理完整功能,包含 Service/Controller/Test 三层结构。

主要功能:

1. IoT 设备查询 API(RPC 接口)
   - IotDeviceQueryApi: 提供设备简化信息查询
   - IotDeviceSimpleRespDTO: 设备简化 DTO

2. 保洁工牌管理
   - CleanBadgeService/Impl: 工牌通知、优先级调整、手动完成
   - BadgeNotifyReqDTO/UpgradePriorityReqDTO/ManualCompleteOrderReqDTO

3. 保洁工单管理
   - CleanWorkOrderService/Impl: 工单时间线查询

4. 保洁仪表盘
   - CleanDashboardService/Impl: 快速统计(待处理/进行中/已完成/在线工牌数)
   - QuickStatsRespDTO: 快速统计 DTO

5. 运营区域管理(Ops Biz)
   - OpsBusAreaService/Impl: 区域 CRUD(支持树形结构、分页查询)
   - AreaDeviceRelationService/Impl: 区域设备关联管理(绑定/解绑/批量更新)
   - OpsBusAreaMapper/AreaDeviceRelationMapper: 扩展 MyBatis 批量方法
   - 7 个 VO 类:CreateReqVO/UpdateReqVO/PageReqVO/RespVO/BindReqVO/RelationRespVO/DeviceUpdateReqVO

6. 前端 Controller(Ops Server)
   - OpsBusAreaController: 区域管理 REST API(11 个接口)
   - AreaDeviceRelationController: 设备关联 REST API(8 个接口)
   - CleanBadgeController: 工牌管理 REST API(5 个接口)
   - CleanDashboardController: 仪表盘 REST API(1 个接口)
   - CleanDeviceController: 设备管理 REST API(2 个接口)
   - CleanWorkOrderController: 工单管理 REST API(2 个接口)

7. 测试覆盖
   - OpsBusAreaServiceTest: 区域服务测试(284 行)
   - AreaDeviceRelationServiceTest: 设备关联测试(240 行)
   - OpsBusAreaControllerTest: 区域 Controller 测试(186 行)
   - AreaDeviceRelationControllerTest: 设备关联 Controller 测试(182 行)

8. API 层扩展
   - ErrorCodeConstants: 错误码常量(区域、设备关联)
   - NotifyTypeEnum: 通知类型枚举(语音、文本、震动)
   - 4 个 Badge/Order DTO: BadgeStatusRespDTO/BadgeRealtimeStatusRespDTO/OrderTimelineRespDTO

9. RPC 配置
   - RpcConfiguration: 注入 IotDeviceQueryApi

影响模块:Ops API、Ops Biz、Ops Server、Ops Environment Biz、IoT API、IoT Server

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
lzh
2026-02-02 22:42:45 +08:00
parent bdf5b640b0
commit 955c825e2c
43 changed files with 3312 additions and 1 deletions

View File

@@ -0,0 +1,34 @@
package com.viewsh.module.ops.dal.dataobject.vo.area;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.Map;
/**
* 区域设备绑定请求 VO
*
* @author lzh
*/
@Schema(description = "管理后台 - 区域设备绑定 Request VO")
@Data
public class AreaDeviceBindReqVO {
@Schema(description = "区域ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
@NotNull(message = "区域ID不能为空")
private Long areaId;
@Schema(description = "设备ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "50001")
@NotNull(message = "设备ID不能为空")
private Long deviceId;
@Schema(description = "关联类型TRAFFIC_COUNTER=客流计数器/BEACON=信标/BADGE=工牌)", requiredMode = Schema.RequiredMode.REQUIRED, example = "TRAFFIC_COUNTER")
@NotBlank(message = "关联类型不能为空")
private String relationType;
@Schema(description = "配置数据JSON格式", example = "{\"threshold\":100}")
private Map<String, Object> configData;
}

View File

@@ -0,0 +1,60 @@
package com.viewsh.module.ops.dal.dataobject.vo.area;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.Map;
/**
* 区域设备关联响应 VO
*
* @author lzh
*/
@Schema(description = "管理后台 - 区域设备关联 Response VO")
@Data
public class AreaDeviceRelationRespVO {
@Schema(description = "关联ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long id;
@Schema(description = "区域ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
private Long areaId;
@Schema(description = "区域名称", example = "A栋3层电梯厅")
private String areaName;
@Schema(description = "设备ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "50001")
private Long deviceId;
@Schema(description = "设备Key", example = "TRAFFIC_COUNTER_001")
private String deviceKey;
@Schema(description = "设备名称", example = "客流计数器001")
private String deviceName;
@Schema(description = "产品ID", example = "10")
private Long productId;
@Schema(description = "产品Key", example = "traffic_counter_v1")
private String productKey;
@Schema(description = "产品名称", example = "客流计数器")
private String productName;
@Schema(description = "关联类型TRAFFIC_COUNTER=客流计数器/BEACON=信标/BADGE=工牌)", requiredMode = Schema.RequiredMode.REQUIRED, example = "TRAFFIC_COUNTER")
private String relationType;
@Schema(description = "配置数据JSON格式", example = "{\"threshold\":100}")
private Map<String, Object> configData;
@Schema(description = "是否启用", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
private Boolean enabled;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
@Schema(description = "更新时间")
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,28 @@
package com.viewsh.module.ops.dal.dataobject.vo.area;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.Map;
/**
* 区域设备关联更新请求 VO
*
* @author lzh
*/
@Schema(description = "管理后台 - 区域设备关联更新 Request VO")
@Data
public class AreaDeviceUpdateReqVO {
@Schema(description = "关联ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "关联ID不能为空")
private Long id;
@Schema(description = "配置数据JSON格式完全替换", example = "{\"threshold\":150}")
private Map<String, Object> configData;
@Schema(description = "是否启用", example = "true")
private Boolean enabled;
}

View File

@@ -0,0 +1,50 @@
package com.viewsh.module.ops.dal.dataobject.vo.area;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 业务区域新增请求 VO
*
* @author lzh
*/
@Schema(description = "管理后台 - 业务区域新增 Request VO")
@Data
public class OpsBusAreaCreateReqVO {
@Schema(description = "区域名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "A栋3层")
@NotBlank(message = "区域名称不能为空")
private String areaName;
@Schema(description = "父级区域ID", example = "1")
private Long parentId;
@Schema(description = "区域业务编码(可选,全局唯一)", example = "BUILD_A_F3")
private String areaCode;
@Schema(description = "区域层级类型PARK=园区/BUILDING=楼栋/FLOOR=楼层/FUNCTION=功能区域)", example = "FLOOR")
private String areaType;
@Schema(description = "功能类型MALE_TOILET=男卫/FEMALE_TOILET=女卫/PUBLIC=公共区/ELEVATOR=电梯厅等)", example = "ELEVATOR")
private String functionType;
@Schema(description = "楼层号2、-1", example = "3")
private Integer floorNo;
@Schema(description = "保洁频率(次/天)", example = "3")
private Integer cleaningFrequency;
@Schema(description = "标准保洁时长(分钟)", example = "30")
private Integer standardDuration;
@Schema(description = "区域等级HIGH=高优先级/MEDIUM=中/LOW=低)", example = "MEDIUM")
private String areaLevel;
@Schema(description = "是否启用", example = "true")
private Boolean isActive;
@Schema(description = "显示排序", example = "1")
private Integer sort;
}

View File

@@ -0,0 +1,27 @@
package com.viewsh.module.ops.dal.dataobject.vo.area;
import com.viewsh.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 业务区域分页查询请求 VO
*
* @author lzh
*/
@Schema(description = "管理后台 - 业务区域分页查询 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class OpsBusAreaPageReqVO extends PageParam {
@Schema(description = "区域类型PARK=园区/BUILDING=楼栋/FLOOR=楼层/FUNCTION=功能区域)", example = "PARK")
private String areaType;
@Schema(description = "是否启用", example = "true")
private Boolean isActive;
@Schema(description = "区域名称或编码(模糊搜索)", example = "A栋")
private String name;
}

View File

@@ -0,0 +1,66 @@
package com.viewsh.module.ops.dal.dataobject.vo.area;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
/**
* 业务区域响应 VO
*
* @author lzh
*/
@Schema(description = "管理后台 - 业务区域 Response VO")
@Data
public class OpsBusAreaRespVO {
@Schema(description = "区域ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
private Long id;
@Schema(description = "父级区域ID", example = "1")
private Long parentId;
@Schema(description = "父级路径1/2/3便于查询所有子区域", example = "/1/2")
private String parentPath;
@Schema(description = "区域名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "A栋3层")
private String areaName;
@Schema(description = "区域业务编码(可选,全局唯一)", example = "BUILD_A_F3")
private String areaCode;
@Schema(description = "区域层级类型PARK=园区/BUILDING=楼栋/FLOOR=楼层/FUNCTION=功能区域)", example = "FLOOR")
private String areaType;
@Schema(description = "功能类型MALE_TOILET=男卫/FEMALE_TOILET=女卫/PUBLIC=公共区/ELEVATOR=电梯厅等)", example = "ELEVATOR")
private String functionType;
@Schema(description = "楼层号2、-1", example = "3")
private Integer floorNo;
@Schema(description = "保洁频率(次/天)", example = "3")
private Integer cleaningFrequency;
@Schema(description = "标准保洁时长(分钟)", example = "30")
private Integer standardDuration;
@Schema(description = "区域等级HIGH=高优先级/MEDIUM=中/LOW=低)", example = "MEDIUM")
private String areaLevel;
@Schema(description = "是否启用", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
private Boolean isActive;
@Schema(description = "显示排序", example = "1")
private Integer sort;
@Schema(description = "子区域列表(前端自行组装树形结构)", example = "[]")
private List<OpsBusAreaRespVO> children;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
@Schema(description = "更新时间")
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,53 @@
package com.viewsh.module.ops.dal.dataobject.vo.area;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 业务区域更新请求 VO
*
* @author lzh
*/
@Schema(description = "管理后台 - 业务区域更新 Request VO")
@Data
public class OpsBusAreaUpdateReqVO {
@Schema(description = "区域ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
@NotNull(message = "区域ID不能为空")
private Long id;
@Schema(description = "区域名称", example = "A栋3层")
private String areaName;
@Schema(description = "父级区域ID", example = "1")
private Long parentId;
@Schema(description = "区域业务编码(可选,全局唯一)", example = "BUILD_A_F3")
private String areaCode;
@Schema(description = "区域层级类型PARK=园区/BUILDING=楼栋/FLOOR=楼层/FUNCTION=功能区域)", example = "FLOOR")
private String areaType;
@Schema(description = "功能类型MALE_TOILET=男卫/FEMALE_TOILET=女卫/PUBLIC=公共区/ELEVATOR=电梯厅等)", example = "ELEVATOR")
private String functionType;
@Schema(description = "楼层号2、-1", example = "3")
private Integer floorNo;
@Schema(description = "保洁频率(次/天)", example = "3")
private Integer cleaningFrequency;
@Schema(description = "标准保洁时长(分钟)", example = "30")
private Integer standardDuration;
@Schema(description = "区域等级HIGH=高优先级/MEDIUM=中/LOW=低)", example = "MEDIUM")
private String areaLevel;
@Schema(description = "是否启用", example = "true")
private Boolean isActive;
@Schema(description = "显示排序", example = "1")
private Integer sort;
}

View File

@@ -72,4 +72,47 @@ public interface OpsAreaDeviceRelationMapper extends BaseMapperX<OpsAreaDeviceRe
.eq(OpsAreaDeviceRelationDO::getProductKey, productKey)
.eq(OpsAreaDeviceRelationDO::getEnabled, true));
}
/**
* 统计区域某类型设备的数量
*
* @param areaId 区域ID
* @param relationType 关联类型
* @return 数量
*/
default Long countByAreaIdAndType(Long areaId, String relationType) {
return selectCount(new LambdaQueryWrapperX<OpsAreaDeviceRelationDO>()
.eq(OpsAreaDeviceRelationDO::getAreaId, areaId)
.eq(OpsAreaDeviceRelationDO::getRelationType, relationType)
.eq(OpsAreaDeviceRelationDO::getEnabled, true));
}
/**
* 查询区域某类型设备的关联记录
*
* @param areaId 区域ID
* @param deviceId 设备ID
* @param relationType 关联类型
* @return 关联关系
*/
default OpsAreaDeviceRelationDO selectByAreaIdAndDeviceIdAndType(Long areaId, Long deviceId, String relationType) {
return selectOne(new LambdaQueryWrapperX<OpsAreaDeviceRelationDO>()
.eq(OpsAreaDeviceRelationDO::getAreaId, areaId)
.eq(OpsAreaDeviceRelationDO::getDeviceId, deviceId)
.eq(OpsAreaDeviceRelationDO::getRelationType, relationType)
.eq(OpsAreaDeviceRelationDO::getEnabled, true));
}
/**
* 统计区域的设备关联数量
*
* @param areaId 区域ID
* @return 数量
*/
default Long selectCountByAreaId(Long areaId) {
return selectCount(new LambdaQueryWrapperX<OpsAreaDeviceRelationDO>()
.eq(OpsAreaDeviceRelationDO::getAreaId, areaId)
.eq(OpsAreaDeviceRelationDO::getEnabled, true));
}
}

View File

@@ -3,6 +3,7 @@ package com.viewsh.module.ops.dal.mysql.area;
import com.viewsh.framework.mybatis.core.mapper.BaseMapperX;
import com.viewsh.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.viewsh.module.ops.dal.dataobject.area.OpsBusAreaDO;
import com.viewsh.module.ops.dal.dataobject.vo.area.OpsBusAreaPageReqVO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@@ -67,4 +68,39 @@ public interface OpsBusAreaMapper extends BaseMapperX<OpsBusAreaDO> {
return selectOne(OpsBusAreaDO::getAreaCode, areaCode);
}
/**
* 根据分页查询条件查询列表
*
* @param reqVO 查询条件
* @return 区域列表
*/
default List<OpsBusAreaDO> selectListByPageReq(OpsBusAreaPageReqVO reqVO) {
LambdaQueryWrapperX<OpsBusAreaDO> wrapper = new LambdaQueryWrapperX<OpsBusAreaDO>();
wrapper.orderByAsc(OpsBusAreaDO::getSort);
if (reqVO.getAreaType() != null) {
wrapper.eq(OpsBusAreaDO::getAreaType, reqVO.getAreaType());
}
if (reqVO.getIsActive() != null) {
wrapper.eq(OpsBusAreaDO::getIsActive, reqVO.getIsActive());
}
if (reqVO.getName() != null) {
wrapper.and(w -> w.like(OpsBusAreaDO::getAreaName, reqVO.getName())
.or()
.like(OpsBusAreaDO::getAreaCode, reqVO.getName()));
}
return selectList(wrapper);
}
/**
* 统计子区域数量
*
* @param parentId 父级区域ID
* @return 子区域数量
*/
default Long selectCountByParentId(Long parentId) {
return selectCount(OpsBusAreaDO::getParentId, parentId);
}
}

View File

@@ -0,0 +1,47 @@
package com.viewsh.module.ops.service.area;
import com.viewsh.module.ops.dal.dataobject.vo.area.AreaDeviceBindReqVO;
import com.viewsh.module.ops.dal.dataobject.vo.area.AreaDeviceRelationRespVO;
import com.viewsh.module.ops.dal.dataobject.vo.area.AreaDeviceUpdateReqVO;
import java.util.List;
/**
* 区域设备关联管理服务
*
* @author lzh
*/
public interface AreaDeviceRelationService {
/**
* 查询区域设备列表
*
* @param areaId 区域ID
* @return 设备关联列表
*/
List<AreaDeviceRelationRespVO> listByAreaId(Long areaId);
/**
* 绑定设备到区域
*
* @param bindReq 绑定请求
* @return 关联ID
*/
Long bindDevice(AreaDeviceBindReqVO bindReq);
/**
* 更新设备关联
*
* @param updateReq 更新请求
*/
void updateRelation(AreaDeviceUpdateReqVO updateReq);
/**
* 解除设备绑定
*
* @param id 关联ID
* @return 是否解除成功
*/
Boolean unbindDevice(Long id);
}

View File

@@ -0,0 +1,186 @@
package com.viewsh.module.ops.service.area;
import cn.hutool.core.bean.BeanUtil;
import com.viewsh.framework.common.pojo.CommonResult;
import com.viewsh.framework.common.exception.util.ServiceExceptionUtil;
import com.viewsh.module.iot.api.device.IotDeviceQueryApi;
import com.viewsh.module.iot.api.device.dto.IotDeviceSimpleRespDTO;
import com.viewsh.module.ops.dal.dataobject.area.OpsAreaDeviceRelationDO;
import com.viewsh.module.ops.dal.mysql.area.OpsAreaDeviceRelationMapper;
import com.viewsh.module.ops.dal.mysql.area.OpsBusAreaMapper;
import com.viewsh.module.ops.dal.dataobject.vo.area.AreaDeviceBindReqVO;
import com.viewsh.module.ops.dal.dataobject.vo.area.AreaDeviceRelationRespVO;
import com.viewsh.module.ops.dal.dataobject.vo.area.AreaDeviceUpdateReqVO;
import com.viewsh.module.ops.enums.ErrorCodeConstants;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static com.viewsh.module.ops.enums.AreaTypeEnum.*;
/**
* 区域设备关联管理服务实现
*
* @author lzh
*/
@Service
@Validated
@Slf4j
public class AreaDeviceRelationServiceImpl implements AreaDeviceRelationService {
@Resource
private OpsAreaDeviceRelationMapper opsAreaDeviceRelationMapper;
@Resource
private OpsBusAreaMapper opsBusAreaMapper;
@Resource
private IotDeviceQueryApi iotDeviceQueryApi;
private static final String TYPE_TRAFFIC_COUNTER = "TRAFFIC_COUNTER";
private static final String TYPE_BEACON = "BEACON";
private static final String TYPE_BADGE = "BADGE";
@Override
public List<AreaDeviceRelationRespVO> listByAreaId(Long areaId) {
List<OpsAreaDeviceRelationDO> list = opsAreaDeviceRelationMapper.selectListByAreaId(areaId);
return list.stream()
.map(this::convertToRespVO)
.collect(java.util.stream.Collectors.toList());
}
@Override
@Transactional(rollbackFor = Exception.class)
public Long bindDevice(AreaDeviceBindReqVO bindReq) {
// 校验区域存在
if (opsBusAreaMapper.selectById(bindReq.getAreaId()) == null) {
throw ServiceExceptionUtil.exception(ErrorCodeConstants.AREA_NOT_FOUND);
}
// 根据关联类型进行唯一性校验
String relationType = bindReq.getRelationType();
// 1对1 约束TRAFFIC_COUNTER 和 BEACON
if (TYPE_TRAFFIC_COUNTER.equals(relationType) || TYPE_BEACON.equals(relationType)) {
Long count = opsAreaDeviceRelationMapper.countByAreaIdAndType(bindReq.getAreaId(), relationType);
if (count > 0) {
String typeName = TYPE_TRAFFIC_COUNTER.equals(relationType) ? "客流计数器" : "信标";
throw ServiceExceptionUtil.exception(ErrorCodeConstants.DEVICE_TYPE_ALREADY_BOUND, typeName);
}
}
// N对1 约束BADGE - 同一设备只能绑定一次到同一区域
if (TYPE_BADGE.equals(relationType)) {
OpsAreaDeviceRelationDO existing = opsAreaDeviceRelationMapper.selectByAreaIdAndDeviceIdAndType(
bindReq.getAreaId(), bindReq.getDeviceId(), relationType);
if (existing != null) {
throw ServiceExceptionUtil.exception(ErrorCodeConstants.DEVICE_ALREADY_BOUND);
}
}
// 调用 IoT 接口获取设备信息
String deviceKey = "DEVICE_" + bindReq.getDeviceId();
Long productId = 0L;
String productKey = "PRODUCT_DEFAULT";
try {
CommonResult<IotDeviceSimpleRespDTO> result = iotDeviceQueryApi.getDevice(bindReq.getDeviceId());
if (result != null && result.getData() != null) {
IotDeviceSimpleRespDTO device = result.getData();
deviceKey = device.getDeviceName() != null ? device.getDeviceName() : deviceKey;
productId = device.getProductId() != null ? device.getProductId() : 0L;
productKey = device.getProductKey() != null ? device.getProductKey() : productKey;
}
} catch (Exception e) {
log.warn("[bindDevice] 调用 IoT 接口获取设备信息失败: deviceId={}, error={}",
bindReq.getDeviceId(), e.getMessage());
// 降级:使用默认值
}
OpsAreaDeviceRelationDO relation = OpsAreaDeviceRelationDO.builder()
.areaId(bindReq.getAreaId())
.deviceId(bindReq.getDeviceId())
.deviceKey(deviceKey)
.productId(productId)
.productKey(productKey)
.relationType(bindReq.getRelationType())
.configData(bindReq.getConfigData() != null ? bindReq.getConfigData() : new HashMap<>())
.enabled(true)
.build();
opsAreaDeviceRelationMapper.insert(relation);
return relation.getId();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateRelation(AreaDeviceUpdateReqVO updateReq) {
OpsAreaDeviceRelationDO existing = opsAreaDeviceRelationMapper.selectById(updateReq.getId());
if (existing == null) {
throw ServiceExceptionUtil.exception(ErrorCodeConstants.DEVICE_RELATION_NOT_FOUND);
}
OpsAreaDeviceRelationDO relation = new OpsAreaDeviceRelationDO();
relation.setId(updateReq.getId());
// configData 完全替换
if (updateReq.getConfigData() != null) {
relation.setConfigData(updateReq.getConfigData());
}
// enabled 更新
if (updateReq.getEnabled() != null) {
relation.setEnabled(updateReq.getEnabled());
}
opsAreaDeviceRelationMapper.updateById(relation);
}
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean unbindDevice(Long id) {
OpsAreaDeviceRelationDO existing = opsAreaDeviceRelationMapper.selectById(id);
if (existing == null) {
return false;
}
return opsAreaDeviceRelationMapper.deleteById(id) > 0;
}
/**
* 转换为响应 VO
* 从 IoT 模块获取设备名称和产品名称(带降级)
*/
private AreaDeviceRelationRespVO convertToRespVO(OpsAreaDeviceRelationDO relation) {
AreaDeviceRelationRespVO respVO = BeanUtil.copyProperties(relation, AreaDeviceRelationRespVO.class);
// 从 IoT 模块获取设备信息
try {
CommonResult<IotDeviceSimpleRespDTO> result = iotDeviceQueryApi.getDevice(relation.getDeviceId());
if (result != null && result.getData() != null) {
IotDeviceSimpleRespDTO device = result.getData();
respVO.setDeviceName(device.getDeviceName());
respVO.setProductName(device.getProductName());
} else {
// 降级:使用默认值
respVO.setDeviceName("设备_" + relation.getDeviceId());
respVO.setProductName("产品_" + relation.getProductKey());
}
} catch (Exception e) {
log.warn("[convertToRespVO] 调用 IoT 接口获取设备信息失败: deviceId={}, error={}",
relation.getDeviceId(), e.getMessage());
// 降级:使用默认值
respVO.setDeviceName("设备_" + relation.getDeviceId());
respVO.setProductName("产品_" + relation.getProductKey());
}
return respVO;
}
}

View File

@@ -0,0 +1,57 @@
package com.viewsh.module.ops.service.area;
import com.viewsh.module.ops.dal.dataobject.area.OpsBusAreaDO;
import com.viewsh.module.ops.dal.dataobject.vo.area.OpsBusAreaCreateReqVO;
import com.viewsh.module.ops.dal.dataobject.vo.area.OpsBusAreaPageReqVO;
import com.viewsh.module.ops.dal.dataobject.vo.area.OpsBusAreaRespVO;
import com.viewsh.module.ops.dal.dataobject.vo.area.OpsBusAreaUpdateReqVO;
import java.util.List;
/**
* 业务区域管理服务
*
* @author lzh
*/
public interface OpsBusAreaService {
/**
* 获取区域列表(平铺,由前端自行组装树形结构)
*
* @param reqVO 查询条件
* @return 区域列表
*/
List<OpsBusAreaRespVO> getAreaTree(OpsBusAreaPageReqVO reqVO);
/**
* 获取区域详情
*
* @param id 区域ID
* @return 区域详情
*/
OpsBusAreaRespVO getArea(Long id);
/**
* 新增区域
*
* @param createReq 创建请求
* @return 区域ID
*/
Long createArea(OpsBusAreaCreateReqVO createReq);
/**
* 更新区域
*
* @param updateReq 更新请求
*/
void updateArea(OpsBusAreaUpdateReqVO updateReq);
/**
* 删除区域
*
* @param id 区域ID
* @return 是否删除成功
*/
Boolean deleteArea(Long id);
}

View File

@@ -0,0 +1,195 @@
package com.viewsh.module.ops.service.area;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import com.viewsh.framework.common.exception.util.ServiceExceptionUtil;
import com.viewsh.module.ops.dal.dataobject.area.OpsBusAreaDO;
import com.viewsh.module.ops.dal.mysql.area.OpsAreaDeviceRelationMapper;
import com.viewsh.module.ops.dal.mysql.area.OpsBusAreaMapper;
import com.viewsh.module.ops.dal.dataobject.vo.area.OpsBusAreaCreateReqVO;
import com.viewsh.module.ops.dal.dataobject.vo.area.OpsBusAreaPageReqVO;
import com.viewsh.module.ops.dal.dataobject.vo.area.OpsBusAreaRespVO;
import com.viewsh.module.ops.dal.dataobject.vo.area.OpsBusAreaUpdateReqVO;
import com.viewsh.module.ops.enums.ErrorCodeConstants;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.util.List;
import java.util.stream.Collectors;
/**
* 业务区域管理服务实现
*
* @author lzh
*/
@Service
@Validated
public class OpsBusAreaServiceImpl implements OpsBusAreaService {
@Resource
private OpsBusAreaMapper opsBusAreaMapper;
@Resource
private OpsAreaDeviceRelationMapper opsAreaDeviceRelationMapper;
@Override
public List<OpsBusAreaRespVO> getAreaTree(OpsBusAreaPageReqVO reqVO) {
List<OpsBusAreaDO> list = opsBusAreaMapper.selectListByPageReq(reqVO);
return BeanUtil.copyToList(list, OpsBusAreaRespVO.class);
}
@Override
public OpsBusAreaRespVO getArea(Long id) {
OpsBusAreaDO area = opsBusAreaMapper.selectById(id);
if (area == null) {
throw ServiceExceptionUtil.exception(ErrorCodeConstants.AREA_NOT_FOUND);
}
return BeanUtil.copyProperties(area, OpsBusAreaRespVO.class);
}
@Override
@Transactional(rollbackFor = Exception.class)
public Long createArea(OpsBusAreaCreateReqVO createReq) {
// 校验区域编码唯一性
if (createReq.getAreaCode() != null) {
OpsBusAreaDO existing = opsBusAreaMapper.selectByAreaCode(createReq.getAreaCode());
if (existing != null) {
throw ServiceExceptionUtil.exception(ErrorCodeConstants.AREA_CODE_EXISTS);
}
}
// 校验父级区域存在
if (createReq.getParentId() != null) {
OpsBusAreaDO parent = opsBusAreaMapper.selectById(createReq.getParentId());
if (parent == null) {
throw ServiceExceptionUtil.exception(ErrorCodeConstants.AREA_NOT_FOUND);
}
}
OpsBusAreaDO area = BeanUtil.copyProperties(createReq, OpsBusAreaDO.class);
// 计算 parentPath
area.setParentPath(buildParentPath(createReq.getParentId()));
// 设置默认值
if (area.getIsActive() == null) {
area.setIsActive(true);
}
if (area.getSort() == null) {
area.setSort(0);
}
opsBusAreaMapper.insert(area);
return area.getId();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateArea(OpsBusAreaUpdateReqVO updateReq) {
// 校验区域存在
OpsBusAreaDO existing = opsBusAreaMapper.selectById(updateReq.getId());
if (existing == null) {
throw ServiceExceptionUtil.exception(ErrorCodeConstants.AREA_NOT_FOUND);
}
// 如果修改父级,校验父级存在且不能设置为自己或子孙节点
if (updateReq.getParentId() != null && !updateReq.getParentId().equals(existing.getParentId())) {
// 校验不能将父级设置为自己
if (updateReq.getId().equals(updateReq.getParentId())) {
throw ServiceExceptionUtil.exception(ErrorCodeConstants.AREA_PARENT_LOOP);
}
// 校验父级存在
OpsBusAreaDO parent = opsBusAreaMapper.selectById(updateReq.getParentId());
if (parent == null) {
throw ServiceExceptionUtil.exception(ErrorCodeConstants.AREA_NOT_FOUND);
}
// 校验不能将父级设置为子孙节点
if (isDescendant(updateReq.getId(), updateReq.getParentId())) {
throw ServiceExceptionUtil.exception(ErrorCodeConstants.AREA_PARENT_LOOP);
}
// 更新 parentPath
String newParentPath = buildParentPath(updateReq.getParentId());
updateReq.setParentId(updateReq.getParentId());
// 注意:这里需要在 DO 中设置 parentPath但由于 UpdateReqVO 没有 parentPath 字段
// 我们需要在后续更新时处理
}
OpsBusAreaDO area = BeanUtil.copyProperties(updateReq, OpsBusAreaDO.class);
// 重新计算 parentPath
if (updateReq.getParentId() != null) {
area.setParentPath(buildParentPath(updateReq.getParentId()));
} else {
area.setParentPath(null);
}
opsBusAreaMapper.updateById(area);
}
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean deleteArea(Long id) {
// 校验区域存在
OpsBusAreaDO existing = opsBusAreaMapper.selectById(id);
if (existing == null) {
return false;
}
// 校验是否有子区域
Long childCount = opsBusAreaMapper.selectCountByParentId(id);
if (childCount > 0) {
throw ServiceExceptionUtil.exception(ErrorCodeConstants.AREA_HAS_CHILDREN);
}
// 校验是否有关联设备
Long deviceCount = opsAreaDeviceRelationMapper.selectCountByAreaId(id);
if (deviceCount > 0) {
throw ServiceExceptionUtil.exception(ErrorCodeConstants.AREA_HAS_DEVICES);
}
return opsBusAreaMapper.deleteById(id) > 0;
}
/**
* 构建 parentPath
*
* @param parentId 父级ID
* @return parentPath
*/
private String buildParentPath(Long parentId) {
if (parentId == null) {
return null;
}
OpsBusAreaDO parent = opsBusAreaMapper.selectById(parentId);
if (parent == null) {
return null;
}
if (parent.getParentPath() == null || parent.getParentPath().isEmpty()) {
return String.valueOf(parentId);
}
return parent.getParentPath() + "/" + parentId;
}
/**
* 判断 targetId 是否是 sourceId 的子孙节点
*
* @param sourceId 源节点ID
* @param targetId 目标节点ID
* @return 是否是子孙节点
*/
private boolean isDescendant(Long sourceId, Long targetId) {
OpsBusAreaDO target = opsBusAreaMapper.selectById(targetId);
if (target == null || target.getParentPath() == null) {
return false;
}
return target.getParentPath().contains("/" + sourceId + "/")
|| target.getParentPath().startsWith(sourceId + "/")
|| target.getParentPath().endsWith("/" + sourceId);
}
}