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:
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
package com.viewsh.module.ops.service.area;
|
||||
|
||||
import com.viewsh.module.ops.dal.dataobject.area.OpsAreaDeviceRelationDO;
|
||||
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.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 com.viewsh.framework.common.exception.ServiceException;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* 区域设备关联管理服务测试
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class AreaDeviceRelationServiceTest {
|
||||
|
||||
@Mock
|
||||
private OpsAreaDeviceRelationMapper opsAreaDeviceRelationMapper;
|
||||
|
||||
@Mock
|
||||
private OpsBusAreaMapper opsBusAreaMapper;
|
||||
|
||||
@InjectMocks
|
||||
private AreaDeviceRelationServiceImpl areaDeviceRelationService;
|
||||
|
||||
private OpsBusAreaDO testArea;
|
||||
private OpsAreaDeviceRelationDO testRelation;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
testArea = OpsBusAreaDO.builder()
|
||||
.id(100L)
|
||||
.areaName("A栋3层电梯厅")
|
||||
.build();
|
||||
|
||||
testRelation = OpsAreaDeviceRelationDO.builder()
|
||||
.id(1L)
|
||||
.areaId(100L)
|
||||
.deviceId(50001L)
|
||||
.deviceKey("TRAFFIC_COUNTER_001")
|
||||
.productId(10L)
|
||||
.productKey("traffic_counter_v1")
|
||||
.relationType("TRAFFIC_COUNTER")
|
||||
.configData(new HashMap<>())
|
||||
.enabled(true)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testListByAreaId_Success() {
|
||||
// Given
|
||||
when(opsAreaDeviceRelationMapper.selectListByAreaId(100L))
|
||||
.thenReturn(Arrays.asList(testRelation));
|
||||
|
||||
// When
|
||||
List<AreaDeviceRelationRespVO> result = areaDeviceRelationService.listByAreaId(100L);
|
||||
|
||||
// Then
|
||||
assertNotNull(result);
|
||||
assertEquals(1, result.size());
|
||||
assertEquals(50001L, result.get(0).getDeviceId());
|
||||
verify(opsAreaDeviceRelationMapper, times(1)).selectListByAreaId(100L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testListByAreaId_Empty_Success() {
|
||||
// Given
|
||||
when(opsAreaDeviceRelationMapper.selectListByAreaId(100L))
|
||||
.thenReturn(Collections.emptyList());
|
||||
|
||||
// When
|
||||
List<AreaDeviceRelationRespVO> result = areaDeviceRelationService.listByAreaId(100L);
|
||||
|
||||
// Then
|
||||
assertNotNull(result);
|
||||
assertTrue(result.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBindDevice_TrafficCounter_Success() {
|
||||
// Given: 1对1约束,但区域尚无该类型设备
|
||||
AreaDeviceBindReqVO bindReq = new AreaDeviceBindReqVO();
|
||||
bindReq.setAreaId(100L);
|
||||
bindReq.setDeviceId(50001L);
|
||||
bindReq.setRelationType("TRAFFIC_COUNTER");
|
||||
|
||||
when(opsBusAreaMapper.selectById(100L)).thenReturn(testArea);
|
||||
when(opsAreaDeviceRelationMapper.countByAreaIdAndType(100L, "TRAFFIC_COUNTER")).thenReturn(0L);
|
||||
when(opsAreaDeviceRelationMapper.insert(any(OpsAreaDeviceRelationDO.class))).thenAnswer(invocation -> {
|
||||
OpsAreaDeviceRelationDO relation = invocation.getArgument(0);
|
||||
relation.setId(1L);
|
||||
return 1;
|
||||
});
|
||||
|
||||
// When
|
||||
Long relationId = areaDeviceRelationService.bindDevice(bindReq);
|
||||
|
||||
// Then
|
||||
assertNotNull(relationId);
|
||||
verify(opsAreaDeviceRelationMapper, times(1)).insert(any(OpsAreaDeviceRelationDO.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBindDevice_TrafficCounter_AlreadyBound_ThrowsException() {
|
||||
// Given: PBT - 1对1约束,区域已有客流计数器
|
||||
AreaDeviceBindReqVO bindReq = new AreaDeviceBindReqVO();
|
||||
bindReq.setAreaId(100L);
|
||||
bindReq.setDeviceId(50002L);
|
||||
bindReq.setRelationType("TRAFFIC_COUNTER");
|
||||
|
||||
when(opsBusAreaMapper.selectById(100L)).thenReturn(testArea);
|
||||
when(opsAreaDeviceRelationMapper.countByAreaIdAndType(100L, "TRAFFIC_COUNTER")).thenReturn(1L);
|
||||
|
||||
// When & Then
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> areaDeviceRelationService.bindDevice(bindReq));
|
||||
assertEquals(ErrorCodeConstants.DEVICE_TYPE_ALREADY_BOUND.getCode(), exception.getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBindDevice_Badge_AlreadyBound_ThrowsException() {
|
||||
// Given: PBT - N对1约束,同一设备已绑定该区域
|
||||
AreaDeviceBindReqVO bindReq = new AreaDeviceBindReqVO();
|
||||
bindReq.setAreaId(100L);
|
||||
bindReq.setDeviceId(30001L);
|
||||
bindReq.setRelationType("BADGE");
|
||||
|
||||
when(opsBusAreaMapper.selectById(100L)).thenReturn(testArea);
|
||||
when(opsAreaDeviceRelationMapper.selectByAreaIdAndDeviceIdAndType(100L, 30001L, "BADGE"))
|
||||
.thenReturn(testRelation);
|
||||
|
||||
// When & Then
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> areaDeviceRelationService.bindDevice(bindReq));
|
||||
assertEquals(ErrorCodeConstants.DEVICE_ALREADY_BOUND.getCode(), exception.getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBindDevice_AreaNotFound_ThrowsException() {
|
||||
// Given
|
||||
AreaDeviceBindReqVO bindReq = new AreaDeviceBindReqVO();
|
||||
bindReq.setAreaId(100L);
|
||||
bindReq.setDeviceId(50001L);
|
||||
bindReq.setRelationType("TRAFFIC_COUNTER");
|
||||
|
||||
when(opsBusAreaMapper.selectById(100L)).thenReturn(null);
|
||||
|
||||
// When & Then
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> areaDeviceRelationService.bindDevice(bindReq));
|
||||
assertEquals(ErrorCodeConstants.AREA_NOT_FOUND.getCode(), exception.getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUpdateRelation_Success() {
|
||||
// Given
|
||||
AreaDeviceUpdateReqVO updateReq = new AreaDeviceUpdateReqVO();
|
||||
updateReq.setId(1L);
|
||||
|
||||
Map<String, Object> newConfig = new HashMap<>();
|
||||
newConfig.put("threshold", 150);
|
||||
|
||||
when(opsAreaDeviceRelationMapper.selectById(1L)).thenReturn(testRelation);
|
||||
when(opsAreaDeviceRelationMapper.updateById(any(OpsAreaDeviceRelationDO.class))).thenReturn(1);
|
||||
|
||||
// When
|
||||
assertDoesNotThrow(() -> areaDeviceRelationService.updateRelation(updateReq));
|
||||
|
||||
// Then
|
||||
verify(opsAreaDeviceRelationMapper, times(1)).updateById(any(OpsAreaDeviceRelationDO.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUpdateRelation_NotFound_ThrowsException() {
|
||||
// Given
|
||||
AreaDeviceUpdateReqVO updateReq = new AreaDeviceUpdateReqVO();
|
||||
updateReq.setId(999L);
|
||||
|
||||
when(opsAreaDeviceRelationMapper.selectById(999L)).thenReturn(null);
|
||||
|
||||
// When & Then
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> areaDeviceRelationService.updateRelation(updateReq));
|
||||
assertEquals(ErrorCodeConstants.DEVICE_RELATION_NOT_FOUND.getCode(), exception.getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUnbindDevice_Success() {
|
||||
// Given
|
||||
when(opsAreaDeviceRelationMapper.selectById(1L)).thenReturn(testRelation);
|
||||
when(opsAreaDeviceRelationMapper.deleteById(1L)).thenReturn(1);
|
||||
|
||||
// When
|
||||
Boolean result = areaDeviceRelationService.unbindDevice(1L);
|
||||
|
||||
// Then
|
||||
assertTrue(result);
|
||||
verify(opsAreaDeviceRelationMapper, times(1)).deleteById(1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUnbindDevice_Idempotency() {
|
||||
// Given: PBT - 幂等性,解除不存在的关联返回 false
|
||||
when(opsAreaDeviceRelationMapper.selectById(1L)).thenReturn(null);
|
||||
|
||||
// When
|
||||
Boolean result = areaDeviceRelationService.unbindDevice(1L);
|
||||
|
||||
// Then
|
||||
assertFalse(result); // 第一次就返回false
|
||||
verify(opsAreaDeviceRelationMapper, never()).deleteById(anyLong());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
package com.viewsh.module.ops.service.area;
|
||||
|
||||
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.OpsBusAreaUpdateReqVO;
|
||||
import com.viewsh.module.ops.enums.ErrorCodeConstants;
|
||||
import com.viewsh.framework.common.exception.ServiceException;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* 业务区域管理服务测试
|
||||
* <p>
|
||||
* 测试内容:
|
||||
* 1. CRUD 基础操作
|
||||
* 2. parent_path 不变性
|
||||
* 3. 删除校验(子区域、设备关联)
|
||||
* 4. 父子循环检测
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class OpsBusAreaServiceTest {
|
||||
|
||||
@Mock
|
||||
private OpsBusAreaMapper opsBusAreaMapper;
|
||||
|
||||
@Mock
|
||||
private OpsAreaDeviceRelationMapper opsAreaDeviceRelationMapper;
|
||||
|
||||
@InjectMocks
|
||||
private OpsBusAreaServiceImpl opsBusAreaService;
|
||||
|
||||
private OpsBusAreaDO testArea;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
// 初始化测试区域
|
||||
testArea = OpsBusAreaDO.builder()
|
||||
.id(1L)
|
||||
.parentId(null)
|
||||
.parentPath(null)
|
||||
.areaName("A园区")
|
||||
.areaCode("PARK_A")
|
||||
.areaType("PARK")
|
||||
.isActive(true)
|
||||
.sort(1)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetAreaTree_Success() {
|
||||
// Given
|
||||
OpsBusAreaPageReqVO reqVO = new OpsBusAreaPageReqVO();
|
||||
reqVO.setAreaType("PARK");
|
||||
|
||||
when(opsBusAreaMapper.selectListByPageReq(any())).thenReturn(Arrays.asList(testArea));
|
||||
|
||||
// When
|
||||
var result = opsBusAreaService.getAreaTree(reqVO);
|
||||
|
||||
// Then
|
||||
assertNotNull(result);
|
||||
assertEquals(1, result.size());
|
||||
assertEquals("A园区", result.get(0).getAreaName());
|
||||
verify(opsBusAreaMapper, times(1)).selectListByPageReq(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetArea_Success() {
|
||||
// Given
|
||||
when(opsBusAreaMapper.selectById(1L)).thenReturn(testArea);
|
||||
|
||||
// When
|
||||
var result = opsBusAreaService.getArea(1L);
|
||||
|
||||
// Then
|
||||
assertNotNull(result);
|
||||
assertEquals("A园区", result.getAreaName());
|
||||
assertEquals(1L, result.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetArea_NotFound_ThrowsException() {
|
||||
// Given
|
||||
when(opsBusAreaMapper.selectById(999L)).thenReturn(null);
|
||||
|
||||
// When & Then
|
||||
ServiceException exception = assertThrows(ServiceException.class, () -> opsBusAreaService.getArea(999L));
|
||||
assertEquals(ErrorCodeConstants.AREA_NOT_FOUND.getCode(), exception.getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateArea_Root_Success() {
|
||||
// Given: 根区域(无父级)
|
||||
OpsBusAreaCreateReqVO createReq = new OpsBusAreaCreateReqVO();
|
||||
createReq.setAreaName("A园区");
|
||||
createReq.setAreaCode("PARK_A");
|
||||
createReq.setAreaType("PARK");
|
||||
|
||||
when(opsBusAreaMapper.selectByAreaCode("PARK_A")).thenReturn(null);
|
||||
when(opsBusAreaMapper.insert(any(OpsBusAreaDO.class))).thenAnswer(invocation -> {
|
||||
OpsBusAreaDO area = invocation.getArgument(0);
|
||||
area.setId(1L);
|
||||
return 1;
|
||||
});
|
||||
|
||||
// When
|
||||
Long areaId = opsBusAreaService.createArea(createReq);
|
||||
|
||||
// Then
|
||||
assertNotNull(areaId);
|
||||
verify(opsBusAreaMapper, times(1)).insert(argThat((OpsBusAreaDO area) ->
|
||||
"A园区".equals(area.getAreaName()) &&
|
||||
area.getParentPath() == null &&
|
||||
area.getIsActive() &&
|
||||
area.getSort() == 0
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateArea_WithParent_Success() {
|
||||
// Given: 子区域
|
||||
OpsBusAreaCreateReqVO createReq = new OpsBusAreaCreateReqVO();
|
||||
createReq.setAreaName("A栋");
|
||||
createReq.setParentId(1L);
|
||||
createReq.setAreaType("BUILDING");
|
||||
|
||||
OpsBusAreaDO parentArea = OpsBusAreaDO.builder()
|
||||
.id(1L)
|
||||
.parentPath("10")
|
||||
.build();
|
||||
|
||||
when(opsBusAreaMapper.selectById(1L)).thenReturn(parentArea);
|
||||
when(opsBusAreaMapper.insert(any(OpsBusAreaDO.class))).thenAnswer(invocation -> {
|
||||
OpsBusAreaDO area = invocation.getArgument(0);
|
||||
area.setId(2L);
|
||||
return 1;
|
||||
});
|
||||
|
||||
// When
|
||||
Long areaId = opsBusAreaService.createArea(createReq);
|
||||
|
||||
// Then
|
||||
assertNotNull(areaId);
|
||||
verify(opsBusAreaMapper, times(1)).insert(argThat((OpsBusAreaDO area) ->
|
||||
"A栋".equals(area.getAreaName()) &&
|
||||
"10/1".equals(area.getParentPath()) // PBT: parent_path 不变性
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateArea_DuplicateCode_ThrowsException() {
|
||||
// Given: 区域编码已存在
|
||||
OpsBusAreaCreateReqVO createReq = new OpsBusAreaCreateReqVO();
|
||||
createReq.setAreaName("A园区");
|
||||
createReq.setAreaCode("PARK_A");
|
||||
|
||||
when(opsBusAreaMapper.selectByAreaCode("PARK_A")).thenReturn(testArea);
|
||||
|
||||
// When & Then
|
||||
ServiceException exception = assertThrows(ServiceException.class, () -> opsBusAreaService.createArea(createReq));
|
||||
assertEquals(ErrorCodeConstants.AREA_CODE_EXISTS.getCode(), exception.getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateArea_ParentNotFound_ThrowsException() {
|
||||
// Given: 父级不存在
|
||||
OpsBusAreaCreateReqVO createReq = new OpsBusAreaCreateReqVO();
|
||||
createReq.setAreaName("A栋");
|
||||
createReq.setParentId(999L);
|
||||
|
||||
when(opsBusAreaMapper.selectById(999L)).thenReturn(null);
|
||||
|
||||
// When & Then
|
||||
ServiceException exception = assertThrows(ServiceException.class, () -> opsBusAreaService.createArea(createReq));
|
||||
assertEquals(ErrorCodeConstants.AREA_NOT_FOUND.getCode(), exception.getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUpdateArea_Success() {
|
||||
// Given
|
||||
OpsBusAreaUpdateReqVO updateReq = new OpsBusAreaUpdateReqVO();
|
||||
updateReq.setId(1L);
|
||||
updateReq.setAreaName("A园区(更新)");
|
||||
|
||||
when(opsBusAreaMapper.selectById(1L)).thenReturn(testArea);
|
||||
when(opsBusAreaMapper.updateById(any(OpsBusAreaDO.class))).thenReturn(1);
|
||||
|
||||
// When
|
||||
assertDoesNotThrow(() -> opsBusAreaService.updateArea(updateReq));
|
||||
|
||||
// Then
|
||||
verify(opsBusAreaMapper, times(1)).updateById(any(OpsBusAreaDO.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUpdateArea_ParentLoop_Self_ThrowsException() {
|
||||
// Given: 将父级设置为自己
|
||||
OpsBusAreaUpdateReqVO updateReq = new OpsBusAreaUpdateReqVO();
|
||||
updateReq.setId(1L);
|
||||
updateReq.setParentId(1L);
|
||||
|
||||
when(opsBusAreaMapper.selectById(1L)).thenReturn(testArea);
|
||||
|
||||
// When & Then
|
||||
ServiceException exception = assertThrows(ServiceException.class, () -> opsBusAreaService.updateArea(updateReq));
|
||||
assertEquals(ErrorCodeConstants.AREA_PARENT_LOOP.getCode(), exception.getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteArea_Success() {
|
||||
// Given
|
||||
when(opsBusAreaMapper.selectById(1L)).thenReturn(testArea);
|
||||
when(opsBusAreaMapper.selectCountByParentId(1L)).thenReturn(0L);
|
||||
when(opsAreaDeviceRelationMapper.selectCountByAreaId(1L)).thenReturn(0L);
|
||||
when(opsBusAreaMapper.deleteById(1L)).thenReturn(1);
|
||||
|
||||
// When
|
||||
Boolean result = opsBusAreaService.deleteArea(1L);
|
||||
|
||||
// Then
|
||||
assertTrue(result);
|
||||
verify(opsBusAreaMapper, times(1)).deleteById(1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteArea_HasChildren_ThrowsException() {
|
||||
// Given: 有子区域
|
||||
when(opsBusAreaMapper.selectById(1L)).thenReturn(testArea);
|
||||
when(opsBusAreaMapper.selectCountByParentId(1L)).thenReturn(2L);
|
||||
|
||||
// When & Then
|
||||
ServiceException exception = assertThrows(ServiceException.class, () -> opsBusAreaService.deleteArea(1L));
|
||||
assertEquals(ErrorCodeConstants.AREA_HAS_CHILDREN.getCode(), exception.getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteArea_HasDevices_ThrowsException() {
|
||||
// Given: 有关联设备
|
||||
when(opsBusAreaMapper.selectById(1L)).thenReturn(testArea);
|
||||
when(opsBusAreaMapper.selectCountByParentId(1L)).thenReturn(0L);
|
||||
when(opsAreaDeviceRelationMapper.selectCountByAreaId(1L)).thenReturn(3L);
|
||||
|
||||
// When & Then
|
||||
ServiceException exception = assertThrows(ServiceException.class, () -> opsBusAreaService.deleteArea(1L));
|
||||
assertEquals(ErrorCodeConstants.AREA_HAS_DEVICES.getCode(), exception.getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteArea_Idempotency() {
|
||||
// Given: PBT - 幂等性,删除不存在的区域返回 false
|
||||
when(opsBusAreaMapper.selectById(1L)).thenReturn(null);
|
||||
|
||||
// When
|
||||
Boolean result = opsBusAreaService.deleteArea(1L);
|
||||
|
||||
// Then
|
||||
assertFalse(result); // 第一次就返回 false
|
||||
verify(opsBusAreaMapper, never()).deleteById(anyLong());
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user