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);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user