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,41 @@
|
||||
package com.viewsh.module.iot.api.device;
|
||||
|
||||
import com.viewsh.framework.common.pojo.CommonResult;
|
||||
import com.viewsh.module.iot.api.device.dto.IotDeviceSimpleRespDTO;
|
||||
import com.viewsh.module.iot.enums.ApiConstants;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* IoT 设备查询 API
|
||||
* <p>
|
||||
* 提供 RPC 接口供其他模块(如 Ops 模块)查询设备基本信息
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@FeignClient(name = ApiConstants.NAME)
|
||||
@Tag(name = "RPC 服务 - IoT 设备查询")
|
||||
public interface IotDeviceQueryApi {
|
||||
|
||||
String PREFIX = ApiConstants.PREFIX + "/device";
|
||||
|
||||
@GetMapping(PREFIX + "/simple-list")
|
||||
@Operation(summary = "获取设备精简列表(按类型/产品筛选)")
|
||||
@Parameter(name = "deviceType", description = "设备类型")
|
||||
@Parameter(name = "productId", description = "产品ID")
|
||||
CommonResult<List<IotDeviceSimpleRespDTO>> getDeviceSimpleList(
|
||||
@RequestParam(value = "deviceType", required = false) Integer deviceType,
|
||||
@RequestParam(value = "productId", required = false) Long productId);
|
||||
|
||||
@GetMapping(PREFIX + "/get")
|
||||
@Operation(summary = "获取设备详情")
|
||||
@Parameter(name = "id", description = "设备ID", required = true)
|
||||
CommonResult<IotDeviceSimpleRespDTO> getDevice(@RequestParam("id") Long id);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.viewsh.module.iot.api.device.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* IoT 设备精简响应 DTO
|
||||
* <p>
|
||||
* 用于 RPC 调用,只包含核心字段
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Schema(description = "RPC 服务 - IoT 设备精简 Response DTO")
|
||||
@Data
|
||||
public class IotDeviceSimpleRespDTO {
|
||||
|
||||
@Schema(description = "设备ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "50001")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "设备名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "客流计数器001")
|
||||
private String deviceName;
|
||||
|
||||
@Schema(description = "产品ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
|
||||
private Long productId;
|
||||
|
||||
@Schema(description = "产品标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "traffic_counter_v1")
|
||||
private String productKey;
|
||||
|
||||
@Schema(description = "产品名称", example = "客流计数器")
|
||||
private String productName;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.viewsh.module.iot.api.device;
|
||||
|
||||
import com.viewsh.framework.common.pojo.CommonResult;
|
||||
import com.viewsh.module.iot.api.device.dto.IotDeviceSimpleRespDTO;
|
||||
import com.viewsh.module.iot.controller.admin.device.vo.device.IotDeviceRespVO;
|
||||
import com.viewsh.module.iot.dal.dataobject.device.IotDeviceDO;
|
||||
import com.viewsh.module.iot.service.device.IotDeviceService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.viewsh.framework.common.pojo.CommonResult.success;
|
||||
import static com.viewsh.module.iot.api.device.IotDeviceQueryApi.PREFIX;
|
||||
|
||||
/**
|
||||
* IoT 设备查询 API 实现类
|
||||
* <p>
|
||||
* 提供 RPC 接口供其他模块调用 IoT 设备查询服务
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@RestController
|
||||
@Validated
|
||||
@Primary
|
||||
@Slf4j
|
||||
public class IotDeviceQueryApiImpl implements IotDeviceQueryApi {
|
||||
|
||||
@Resource
|
||||
private IotDeviceService deviceService;
|
||||
|
||||
@Override
|
||||
@GetMapping(PREFIX + "/simple-list")
|
||||
@Operation(summary = "获取设备精简列表(按类型/产品筛选)")
|
||||
public CommonResult<List<IotDeviceSimpleRespDTO>> getDeviceSimpleList(
|
||||
@RequestParam(value = "deviceType", required = false) Integer deviceType,
|
||||
@RequestParam(value = "productId", required = false) Long productId) {
|
||||
try {
|
||||
List<IotDeviceDO> list = deviceService.getDeviceListByCondition(deviceType, productId);
|
||||
List<IotDeviceSimpleRespDTO> result = list.stream()
|
||||
.map(device -> {
|
||||
IotDeviceSimpleRespDTO dto = new IotDeviceSimpleRespDTO();
|
||||
dto.setId(device.getId());
|
||||
dto.setDeviceName(device.getDeviceName());
|
||||
dto.setProductId(device.getProductId());
|
||||
dto.setProductKey(device.getProductKey());
|
||||
// TODO: 从产品服务获取产品名称
|
||||
dto.setProductName("产品_" + device.getProductKey());
|
||||
return dto;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
return success(result);
|
||||
} catch (Exception e) {
|
||||
log.error("[getDeviceSimpleList] 查询设备列表失败: deviceType={}, productId={}", deviceType, productId, e);
|
||||
return success(List.of());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@GetMapping(PREFIX + "/get")
|
||||
@Operation(summary = "获取设备详情")
|
||||
public CommonResult<IotDeviceSimpleRespDTO> getDevice(@RequestParam("id") Long id) {
|
||||
try {
|
||||
IotDeviceDO device = deviceService.getDevice(id);
|
||||
if (device == null) {
|
||||
return success(null);
|
||||
}
|
||||
|
||||
IotDeviceSimpleRespDTO dto = new IotDeviceSimpleRespDTO();
|
||||
dto.setId(device.getId());
|
||||
dto.setDeviceName(device.getDeviceName());
|
||||
dto.setProductId(device.getProductId());
|
||||
dto.setProductKey(device.getProductKey());
|
||||
dto.setProductName("产品_" + device.getProductKey());
|
||||
|
||||
return success(dto);
|
||||
} catch (Exception e) {
|
||||
log.error("[getDevice] 查询设备详情失败: id={}", id, e);
|
||||
return success(null);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.viewsh.module.ops.environment.dal.dataobject;
|
||||
|
||||
import com.viewsh.module.ops.enums.NotifyTypeEnum;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 工牌通知请求 DTO
|
||||
* <p>
|
||||
* 用于发送工牌设备通知(语音、震动)
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Schema(description = "管理后台 - 工牌通知 Request DTO")
|
||||
@Data
|
||||
public class BadgeNotifyReqDTO {
|
||||
|
||||
@Schema(description = "工牌设备ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "3001")
|
||||
@NotNull(message = "工牌设备ID不能为空")
|
||||
private Long badgeId;
|
||||
|
||||
@Schema(description = "通知类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "VOICE")
|
||||
@NotNull(message = "通知类型不能为空")
|
||||
private NotifyTypeEnum type;
|
||||
|
||||
@Schema(description = "语音内容(仅语音通知需要)", example = "请注意,您有待处理工单")
|
||||
private String content;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.viewsh.module.ops.environment.dal.dataobject;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 手动完成工单请求 DTO
|
||||
* <p>
|
||||
* 管理员手动完成工单的兜底操作
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Schema(description = "管理后台 - 手动完成工单 Request DTO")
|
||||
@Data
|
||||
public class ManualCompleteOrderReqDTO {
|
||||
|
||||
@Schema(description = "工单ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1001")
|
||||
@NotNull(message = "工单ID不能为空")
|
||||
private Long orderId;
|
||||
|
||||
@Schema(description = "备注", example = "管理员手动完成")
|
||||
private String remark;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.viewsh.module.ops.environment.dal.dataobject;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 升级工单优先级请求 DTO
|
||||
* <p>
|
||||
* 用于将普通工单升级为 P0 紧急工单
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Schema(description = "管理后台 - 升级工单优先级 Request DTO")
|
||||
@Data
|
||||
public class UpgradePriorityReqDTO {
|
||||
|
||||
@Schema(description = "工单ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1001")
|
||||
@NotNull(message = "工单ID不能为空")
|
||||
private Long orderId;
|
||||
|
||||
@Schema(description = "升级原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "客户投诉急需处理")
|
||||
@NotBlank(message = "升级原因不能为空")
|
||||
private String reason;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.viewsh.module.ops.environment.service.badge;
|
||||
|
||||
import com.viewsh.module.ops.api.badge.BadgeDeviceStatusDTO;
|
||||
import com.viewsh.module.ops.api.clean.BadgeRealtimeStatusRespDTO;
|
||||
import com.viewsh.module.ops.api.clean.BadgeStatusRespDTO;
|
||||
import com.viewsh.module.ops.environment.dal.dataobject.BadgeNotifyReqDTO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 保洁工牌服务接口
|
||||
* <p>
|
||||
* 职责:
|
||||
* 1. 查询工牌实时状态列表
|
||||
* 2. 查询工牌实时状态详情
|
||||
* 3. 发送工牌通知(语音/震动)
|
||||
* <p>
|
||||
* 设计说明:
|
||||
* - 工牌状态来源于 Redis(通过 BadgeDeviceStatusService)
|
||||
* - 设备基本信息来源于 iot_device 表
|
||||
* - 区域关联来源于 ops_area_device_relation 表
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
public interface CleanBadgeService {
|
||||
|
||||
/**
|
||||
* 获取工牌实时状态列表
|
||||
* <p>
|
||||
* 支持按区域和状态筛选
|
||||
*
|
||||
* @param areaId 区域ID(可选)
|
||||
* @param status 状态筛选(可选):IDLE/BUSY/OFFLINE/PAUSED
|
||||
* @return 工牌状态列表
|
||||
*/
|
||||
List<BadgeStatusRespDTO> getBadgeStatusList(Long areaId, String status);
|
||||
|
||||
/**
|
||||
* 获取工牌实时状态详情
|
||||
*
|
||||
* @param badgeId 工牌设备ID
|
||||
* @return 工牌实时状态详情
|
||||
*/
|
||||
BadgeRealtimeStatusRespDTO getBadgeRealtimeStatus(Long badgeId);
|
||||
|
||||
/**
|
||||
* 发送工牌通知
|
||||
* <p>
|
||||
* 支持语音和震动通知
|
||||
*
|
||||
* @param req 通知请求
|
||||
*/
|
||||
void sendBadgeNotify(BadgeNotifyReqDTO req);
|
||||
|
||||
/**
|
||||
* 获取工牌今日完成工单数
|
||||
*
|
||||
* @param deviceId 设备ID(通过设备ID查找关联的用户)
|
||||
* @return 今日完成工单数
|
||||
*/
|
||||
Integer getTodayCompletedCount(Long deviceId);
|
||||
|
||||
/**
|
||||
* 获取工牌今日工作时长(分钟)
|
||||
*
|
||||
* @param deviceId 设备ID
|
||||
* @return 今日工作时长(分钟)
|
||||
*/
|
||||
Integer getTodayWorkMinutes(Long deviceId);
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package com.viewsh.module.ops.environment.service.badge;
|
||||
|
||||
import com.viewsh.module.iot.api.device.IotDeviceControlApi;
|
||||
import com.viewsh.module.iot.api.device.dto.IotDeviceServiceInvokeReqDTO;
|
||||
import com.viewsh.module.ops.api.badge.BadgeDeviceStatusDTO;
|
||||
import com.viewsh.module.ops.api.clean.BadgeRealtimeStatusRespDTO;
|
||||
import com.viewsh.module.ops.api.clean.BadgeStatusRespDTO;
|
||||
import com.viewsh.module.ops.environment.dal.dataobject.BadgeNotifyReqDTO;
|
||||
import com.viewsh.module.ops.service.area.AreaDeviceService;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 保洁工牌服务实现
|
||||
* <p>
|
||||
* 工牌状态数据来源:Redis (ops:badge:status:{deviceId})
|
||||
* 设备基本信息来源:iot_device 表
|
||||
* 区域关联来源:ops_area_device_relation 表
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class CleanBadgeServiceImpl implements CleanBadgeService {
|
||||
|
||||
@Resource
|
||||
private BadgeDeviceStatusService badgeDeviceStatusService;
|
||||
|
||||
@Resource
|
||||
private AreaDeviceService areaDeviceService;
|
||||
|
||||
@Resource
|
||||
private IotDeviceControlApi iotDeviceControlApi;
|
||||
|
||||
private static final String NOTIFY_IDENTIFIER = "NOTIFY";
|
||||
|
||||
@Override
|
||||
public List<BadgeStatusRespDTO> getBadgeStatusList(Long areaId, String status) {
|
||||
try {
|
||||
// 1. 获取设备ID列表
|
||||
List<Long> deviceIds;
|
||||
if (areaId != null) {
|
||||
deviceIds = areaDeviceService.getDeviceIdsByAreaAndType(areaId, "BADGE");
|
||||
} else {
|
||||
// 不指定区域时,从所有活跃工牌获取
|
||||
List<BadgeDeviceStatusDTO> allActiveBadges = badgeDeviceStatusService.listActiveBadges();
|
||||
deviceIds = allActiveBadges.stream()
|
||||
.map(BadgeDeviceStatusDTO::getDeviceId)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
if (deviceIds.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// 2. 批量获取工牌状态
|
||||
List<BadgeDeviceStatusDTO> statusList = badgeDeviceStatusService.batchGetBadgeStatus(deviceIds);
|
||||
|
||||
// 3. 按状态筛选(如果指定)
|
||||
List<BadgeDeviceStatusDTO> filteredList = statusList;
|
||||
if (status != null && !status.isEmpty()) {
|
||||
filteredList = statusList.stream()
|
||||
.filter(dto -> dto.getStatus() != null)
|
||||
.filter(dto -> dto.getStatus().getCode().equalsIgnoreCase(status))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// 4. 转换为响应DTO
|
||||
return filteredList.stream()
|
||||
.map(this::convertToBadgeStatusRespDTO)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[getBadgeStatusList] 查询工牌状态列表失败: areaId={}, status={}", areaId, status, e);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public BadgeRealtimeStatusRespDTO getBadgeRealtimeStatus(Long badgeId) {
|
||||
try {
|
||||
// 1. 获取工牌状态
|
||||
BadgeDeviceStatusDTO status = badgeDeviceStatusService.getBadgeStatus(badgeId);
|
||||
if (status == null) {
|
||||
log.warn("[getBadgeRealtimeStatus] 工牌状态不存在: badgeId={}", badgeId);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2. 构建响应
|
||||
return BadgeRealtimeStatusRespDTO.builder()
|
||||
.deviceId(status.getDeviceId())
|
||||
.deviceKey(status.getDeviceCode())
|
||||
.status(status.getStatusCode())
|
||||
.batteryLevel(status.getBatteryLevel())
|
||||
.lastHeartbeatTime(formatTimestamp(status.getLastHeartbeatTime()))
|
||||
.rssi(null) // RSSI 需要从 IoT 模块获取,暂不实现
|
||||
.isInArea(status.getCurrentAreaId() != null)
|
||||
.areaId(status.getCurrentAreaId())
|
||||
.areaName(status.getCurrentAreaName())
|
||||
.build();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[getBadgeRealtimeStatus] 查询工牌实时状态失败: badgeId={}", badgeId, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendBadgeNotify(BadgeNotifyReqDTO req) {
|
||||
try {
|
||||
Long deviceId = req.getBadgeId();
|
||||
|
||||
if (req.getType() == com.viewsh.module.ops.enums.NotifyTypeEnum.VOICE) {
|
||||
// 语音通知 - 使用 TTS 服务
|
||||
if (req.getContent() == null || req.getContent().isEmpty()) {
|
||||
log.warn("[sendBadgeNotify] 语音通知内容为空: deviceId={}", deviceId);
|
||||
return;
|
||||
}
|
||||
|
||||
IotDeviceServiceInvokeReqDTO reqDTO = new IotDeviceServiceInvokeReqDTO();
|
||||
reqDTO.setDeviceId(deviceId);
|
||||
reqDTO.setIdentifier("TTS");
|
||||
reqDTO.setParams(Map.of(
|
||||
"tts_text", req.getContent(),
|
||||
"tts_flag", 0x08 // 普通通知
|
||||
));
|
||||
|
||||
iotDeviceControlApi.invokeService(reqDTO);
|
||||
log.info("[sendBadgeNotify] 语音通知发送成功: deviceId={}, content={}", deviceId, req.getContent());
|
||||
|
||||
} else if (req.getType() == com.viewsh.module.ops.enums.NotifyTypeEnum.VIBRATE) {
|
||||
// 震动通知
|
||||
IotDeviceServiceInvokeReqDTO reqDTO = new IotDeviceServiceInvokeReqDTO();
|
||||
reqDTO.setDeviceId(deviceId);
|
||||
reqDTO.setIdentifier(NOTIFY_IDENTIFIER);
|
||||
reqDTO.setParams(Map.of(
|
||||
"notify_type", "VIBRATE",
|
||||
"duration", 1000 // 震动1秒
|
||||
));
|
||||
|
||||
iotDeviceControlApi.invokeService(reqDTO);
|
||||
log.info("[sendBadgeNotify] 震动通知发送成功: deviceId={}", deviceId);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[sendBadgeNotify] 发送工牌通知失败: badgeId={}, type={}",
|
||||
req.getBadgeId(), req.getType(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getTodayCompletedCount(Long deviceId) {
|
||||
// 暂不实现,工牌不关联用户,无法统计工牌完成的工单数
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getTodayWorkMinutes(Long deviceId) {
|
||||
// 暂不实现,工牌不关联用户,无法统计工牌工作时长
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ==================== 私有方法 ====================
|
||||
|
||||
/**
|
||||
* 转换为工牌状态响应DTO
|
||||
*/
|
||||
private BadgeStatusRespDTO convertToBadgeStatusRespDTO(BadgeDeviceStatusDTO status) {
|
||||
return BadgeStatusRespDTO.builder()
|
||||
.deviceId(status.getDeviceId())
|
||||
.deviceKey(status.getDeviceCode())
|
||||
.status(status.getStatusCode())
|
||||
.batteryLevel(status.getBatteryLevel())
|
||||
.lastHeartbeatTime(formatTimestamp(status.getLastHeartbeatTime()))
|
||||
.currentAreaId(status.getCurrentAreaId())
|
||||
.currentAreaName(status.getCurrentAreaName())
|
||||
.todayCompletedCount(0)
|
||||
.todayWorkMinutes(0)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化时间戳
|
||||
*/
|
||||
private String formatTimestamp(Long timestamp) {
|
||||
if (timestamp == null) {
|
||||
return null;
|
||||
}
|
||||
return LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault())
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.viewsh.module.ops.environment.service.cleanorder;
|
||||
|
||||
import com.viewsh.module.ops.api.clean.OrderTimelineRespDTO;
|
||||
import com.viewsh.module.ops.environment.dal.dataobject.ManualCompleteOrderReqDTO;
|
||||
import com.viewsh.module.ops.environment.dal.dataobject.UpgradePriorityReqDTO;
|
||||
|
||||
/**
|
||||
* 保洁工单服务接口
|
||||
* <p>
|
||||
* 职责:
|
||||
* 1. 查询工单时间轴
|
||||
* 2. 手动完成工单(兜底操作)
|
||||
* 3. 升级工单优先级(P0插队)
|
||||
* <p>
|
||||
* 设计说明:
|
||||
* - 工单时间轴数据来源于 ops_order_event 表
|
||||
* - 工单操作复用现有的 OpsOrderService
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
public interface CleanWorkOrderService {
|
||||
|
||||
/**
|
||||
* 获取工单时间轴
|
||||
* <p>
|
||||
* 从 ops_order_event 表查询该工单的所有事件记录
|
||||
*
|
||||
* @param orderId 工单ID
|
||||
* @return 工单时间轴
|
||||
*/
|
||||
OrderTimelineRespDTO getOrderTimeline(Long orderId);
|
||||
|
||||
/**
|
||||
* 手动完成工单
|
||||
* <p>
|
||||
* 管理员手动完成工单的兜底操作
|
||||
*
|
||||
* @param req 手动完成请求
|
||||
*/
|
||||
void manualCompleteOrder(ManualCompleteOrderReqDTO req);
|
||||
|
||||
/**
|
||||
* 升级工单优先级
|
||||
* <p>
|
||||
* 将普通工单升级为 P0 紧急工单
|
||||
*
|
||||
* @param req 升级优先级请求
|
||||
*/
|
||||
void upgradePriority(UpgradePriorityReqDTO req);
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
package com.viewsh.module.ops.environment.service.cleanorder;
|
||||
|
||||
import com.viewsh.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import com.viewsh.module.ops.api.clean.OrderTimelineRespDTO;
|
||||
import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO;
|
||||
import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderEventDO;
|
||||
import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderEventMapper;
|
||||
import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper;
|
||||
import com.viewsh.module.ops.environment.dal.dataobject.ManualCompleteOrderReqDTO;
|
||||
import com.viewsh.module.ops.environment.dal.dataobject.UpgradePriorityReqDTO;
|
||||
import com.viewsh.module.ops.service.event.OpsOrderEventService;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 保洁工单服务实现
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class CleanWorkOrderServiceImpl implements CleanWorkOrderService {
|
||||
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
@Resource
|
||||
private OpsOrderMapper opsOrderMapper;
|
||||
|
||||
@Resource
|
||||
private OpsOrderEventMapper opsOrderEventMapper;
|
||||
|
||||
@Resource
|
||||
private OpsOrderEventService opsOrderEventService;
|
||||
|
||||
/**
|
||||
* 事件类型名称映射
|
||||
*/
|
||||
private static final Map<String, String> EVENT_TYPE_NAMES = new HashMap<>();
|
||||
|
||||
static {
|
||||
EVENT_TYPE_NAMES.put("CREATE", "工单创建");
|
||||
EVENT_TYPE_NAMES.put("ASSIGN", "工单分配");
|
||||
EVENT_TYPE_NAMES.put("ACCEPT", "接单确认");
|
||||
EVENT_TYPE_NAMES.put("ARRIVE", "到岗确认");
|
||||
EVENT_TYPE_NAMES.put("PAUSE", "工单暂停");
|
||||
EVENT_TYPE_NAMES.put("RESUME", "恢复作业");
|
||||
EVENT_TYPE_NAMES.put("COMPLETE", "工单完成");
|
||||
EVENT_TYPE_NAMES.put("CANCEL", "工单取消");
|
||||
EVENT_TYPE_NAMES.put("UPGRADE_PRIORITY", "优先级升级");
|
||||
}
|
||||
|
||||
@Override
|
||||
public OrderTimelineRespDTO getOrderTimeline(Long orderId) {
|
||||
try {
|
||||
// 1. 查询工单获取当前状态
|
||||
OpsOrderDO order = opsOrderMapper.selectById(orderId);
|
||||
if (order == null) {
|
||||
log.warn("[getOrderTimeline] 工单不存在: orderId={}", orderId);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2. 查询工单事件
|
||||
List<OpsOrderEventDO> events = opsOrderEventMapper.selectList(
|
||||
new LambdaQueryWrapperX<OpsOrderEventDO>()
|
||||
.eq(OpsOrderEventDO::getOpsOrderId, orderId)
|
||||
.orderByAsc(OpsOrderEventDO::getEventTime)
|
||||
);
|
||||
|
||||
// 3. 转换为时间轴节点
|
||||
List<OrderTimelineRespDTO.TimelineItemDTO> timeline = events.stream()
|
||||
.map(this::convertToTimelineItem)
|
||||
.toList();
|
||||
|
||||
// 4. 构建响应
|
||||
return OrderTimelineRespDTO.builder()
|
||||
.orderId(orderId)
|
||||
.currentStatus(order.getStatus())
|
||||
.timeline(timeline)
|
||||
.build();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[getOrderTimeline] 查询工单时间轴失败: orderId={}", orderId, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void manualCompleteOrder(ManualCompleteOrderReqDTO req) {
|
||||
try {
|
||||
Long orderId = req.getOrderId();
|
||||
|
||||
// 1. 查询工单
|
||||
OpsOrderDO order = opsOrderMapper.selectById(orderId);
|
||||
if (order == null) {
|
||||
log.warn("[manualCompleteOrder] 工单不存在: orderId={}", orderId);
|
||||
return;
|
||||
}
|
||||
|
||||
String fromStatus = order.getStatus();
|
||||
|
||||
// 2. 更新工单状态为已完成
|
||||
OpsOrderDO updateDO = new OpsOrderDO();
|
||||
updateDO.setId(orderId);
|
||||
updateDO.setStatus("COMPLETED");
|
||||
updateDO.setEndTime(LocalDateTime.now());
|
||||
opsOrderMapper.updateById(updateDO);
|
||||
|
||||
// 3. 记录事件
|
||||
opsOrderEventService.recordEvent(
|
||||
orderId,
|
||||
fromStatus,
|
||||
"COMPLETED",
|
||||
"COMPLETE",
|
||||
"ADMIN",
|
||||
null, // 管理员操作,当前未传递具体ID
|
||||
req.getRemark() != null ? req.getRemark() : "管理员手动完成"
|
||||
);
|
||||
|
||||
log.info("[manualCompleteOrder] 手动完成工单成功: orderId={}, remark={}", orderId, req.getRemark());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[manualCompleteOrder] 手动完成工单失败: orderId={}", req.getOrderId(), e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void upgradePriority(UpgradePriorityReqDTO req) {
|
||||
try {
|
||||
Long orderId = req.getOrderId();
|
||||
|
||||
// 1. 查询工单
|
||||
OpsOrderDO order = opsOrderMapper.selectById(orderId);
|
||||
if (order == null) {
|
||||
log.warn("[upgradePriority] 工单不存在: orderId={}", orderId);
|
||||
return;
|
||||
}
|
||||
|
||||
Integer oldPriority = order.getPriority();
|
||||
|
||||
// 2. 更新工单优先级为 P0
|
||||
OpsOrderDO updateDO = new OpsOrderDO();
|
||||
updateDO.setId(orderId);
|
||||
updateDO.setPriority(0); // P0
|
||||
opsOrderMapper.updateById(updateDO);
|
||||
|
||||
// 3. 记录事件
|
||||
opsOrderEventService.recordEvent(
|
||||
orderId,
|
||||
order.getStatus(),
|
||||
order.getStatus(),
|
||||
"UPGRADE_PRIORITY",
|
||||
"ADMIN",
|
||||
null, // 管理员操作
|
||||
String.format("优先级从 P%d 升级为 P0: %s",
|
||||
oldPriority != null ? oldPriority : 1, req.getReason())
|
||||
);
|
||||
|
||||
log.info("[upgradePriority] 升级工单优先级成功: orderId={}, oldPriority={}, reason={}",
|
||||
orderId, oldPriority, req.getReason());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[upgradePriority] 升级工单优先级失败: orderId={}", req.getOrderId(), e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 私有方法 ====================
|
||||
|
||||
/**
|
||||
* 转换为时间轴节点
|
||||
*/
|
||||
private OrderTimelineRespDTO.TimelineItemDTO convertToTimelineItem(OpsOrderEventDO event) {
|
||||
String statusName = EVENT_TYPE_NAMES.getOrDefault(
|
||||
event.getEventType(),
|
||||
event.getEventType()
|
||||
);
|
||||
|
||||
String operator = event.getOperatorName();
|
||||
if (operator == null || operator.isEmpty()) {
|
||||
if ("SYSTEM".equals(event.getOperatorType())) {
|
||||
operator = "系统";
|
||||
} else {
|
||||
operator = "操作员";
|
||||
}
|
||||
}
|
||||
|
||||
return OrderTimelineRespDTO.TimelineItemDTO.builder()
|
||||
.status(event.getToStatus())
|
||||
.statusName(statusName)
|
||||
.time(formatDateTime(event.getEventTime()))
|
||||
.operator(operator)
|
||||
.description(event.getRemark())
|
||||
.extra(buildExtraInfo(event))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期时间
|
||||
*/
|
||||
private String formatDateTime(LocalDateTime dateTime) {
|
||||
if (dateTime == null) {
|
||||
return null;
|
||||
}
|
||||
return dateTime.format(DATE_TIME_FORMATTER);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建额外信息
|
||||
*/
|
||||
private Map<String, Object> buildExtraInfo(OpsOrderEventDO event) {
|
||||
Map<String, Object> extra = new HashMap<>();
|
||||
extra.put("event_type", event.getEventType());
|
||||
extra.put("operator_type", event.getOperatorType());
|
||||
if (event.getOperatorId() != null) {
|
||||
extra.put("operator_id", event.getOperatorId());
|
||||
}
|
||||
return extra;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.viewsh.module.ops.environment.service.dashboard;
|
||||
|
||||
import com.viewsh.module.ops.api.clean.QuickStatsRespDTO;
|
||||
|
||||
/**
|
||||
* 保洁仪表盘服务接口
|
||||
* <p>
|
||||
* 职责:
|
||||
* 1. 提供快速统计数据
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
public interface CleanDashboardService {
|
||||
|
||||
/**
|
||||
* 获取快速统计
|
||||
* <p>
|
||||
* 包括:待分配数量、进行中数量、今日完成数量、在线工<E7BABF><E5B7A5><EFBFBD>数量
|
||||
*
|
||||
* @return 快速统计响应
|
||||
*/
|
||||
QuickStatsRespDTO getQuickStats();
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.viewsh.module.ops.environment.service.dashboard;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.viewsh.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import com.viewsh.module.ops.api.badge.BadgeDeviceStatusDTO;
|
||||
import com.viewsh.module.ops.api.clean.QuickStatsRespDTO;
|
||||
import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO;
|
||||
import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper;
|
||||
import com.viewsh.module.ops.environment.service.badge.BadgeDeviceStatusService;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 保洁仪表盘服务实现
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class CleanDashboardServiceImpl implements CleanDashboardService {
|
||||
|
||||
@Resource
|
||||
private OpsOrderMapper opsOrderMapper;
|
||||
|
||||
@Resource
|
||||
private BadgeDeviceStatusService badgeDeviceStatusService;
|
||||
|
||||
@Override
|
||||
public QuickStatsRespDTO getQuickStats() {
|
||||
try {
|
||||
// 1. 统计待分配数量 (status='PENDING' AND order_type='CLEAN')
|
||||
Integer pendingCount = Math.toIntExact(opsOrderMapper.selectCount(
|
||||
new LambdaQueryWrapperX<OpsOrderDO>()
|
||||
.eq(OpsOrderDO::getStatus, "PENDING")
|
||||
.eq(OpsOrderDO::getOrderType, "CLEAN")
|
||||
));
|
||||
|
||||
// 2. 统计进行中数量 (status IN ('DISPATCHED','CONFIRMED','ARRIVED') AND order_type='CLEAN')
|
||||
Integer inProgressCount = Math.toIntExact(opsOrderMapper.selectCount(
|
||||
new LambdaQueryWrapperX<OpsOrderDO>()
|
||||
.in(OpsOrderDO::getStatus, List.of("DISPATCHED", "CONFIRMED", "ARRIVED"))
|
||||
.eq(OpsOrderDO::getOrderType, "CLEAN")
|
||||
));
|
||||
|
||||
// 3. 统计今日完成数量
|
||||
LocalDateTime todayStart = LocalDateTime.now().withHour(0).withMinute(0).withSecond(0);
|
||||
LocalDateTime todayEnd = todayStart.plusDays(1);
|
||||
|
||||
Integer completedTodayCount = Math.toIntExact(opsOrderMapper.selectCount(
|
||||
new LambdaQueryWrapperX<OpsOrderDO>()
|
||||
.eq(OpsOrderDO::getStatus, "COMPLETED")
|
||||
.eq(OpsOrderDO::getOrderType, "CLEAN")
|
||||
.between(OpsOrderDO::getEndTime, todayStart, todayEnd)
|
||||
));
|
||||
|
||||
// 4. 统计在线工牌数量(从 Redis)
|
||||
List<BadgeDeviceStatusDTO> activeBadges = badgeDeviceStatusService.listActiveBadges();
|
||||
Integer onlineBadgeCount = activeBadges.size();
|
||||
|
||||
return QuickStatsRespDTO.builder()
|
||||
.pendingCount(pendingCount)
|
||||
.inProgressCount(inProgressCount)
|
||||
.completedTodayCount(completedTodayCount)
|
||||
.onlineBadgeCount(onlineBadgeCount)
|
||||
.build();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[getQuickStats] 获取快速统计失败", e);
|
||||
return QuickStatsRespDTO.builder()
|
||||
.pendingCount(0)
|
||||
.inProgressCount(0)
|
||||
.completedTodayCount(0)
|
||||
.onlineBadgeCount(0)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -338,7 +338,7 @@ public class CleanOrderEndToEndTest {
|
||||
verify(eventLogRecorder).record(any());
|
||||
|
||||
// 2. TTS sent (orderId can be null for TTS_REQUEST events)
|
||||
verify(voiceBroadcastService).broadcast(eq(5001L), contains("请回到作业区域"), isNull());
|
||||
verify(voiceBroadcastService).broadcast(eq(5001L), contains("请回到作业区域"), eq((Long) null));
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.viewsh.module.ops.api.clean;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 工牌实时状态详情响应 DTO
|
||||
* <p>
|
||||
* 用于工牌详情页展示
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Schema(description = "管理后台 - 工牌实时状态详情 Response DTO")
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class BadgeRealtimeStatusRespDTO {
|
||||
|
||||
@Schema(description = "设备ID", example = "3001")
|
||||
private Long deviceId;
|
||||
|
||||
@Schema(description = "设备编码", example = "badge_001")
|
||||
private String deviceKey;
|
||||
|
||||
@Schema(description = "设备状态", example = "BUSY")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "电量(0-100)", example = "72")
|
||||
private Integer batteryLevel;
|
||||
|
||||
@Schema(description = "最后心跳时间", example = "2026-01-23 15:00:30")
|
||||
private String lastHeartbeatTime;
|
||||
|
||||
@Schema(description = "信号强度(dBm)", example = "-42")
|
||||
private Integer rssi;
|
||||
|
||||
@Schema(description = "是否在区域内", example = "true")
|
||||
private Boolean isInArea;
|
||||
|
||||
@Schema(description = "当前区域ID", example = "101")
|
||||
private Long areaId;
|
||||
|
||||
@Schema(description = "当前区域名称", example = "A区洗手间")
|
||||
private String areaName;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.viewsh.module.ops.api.clean;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 工牌状态响应 DTO
|
||||
* <p>
|
||||
* 用于工牌列表展示
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Schema(description = "管理后台 - 工牌状态 Response DTO")
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class BadgeStatusRespDTO {
|
||||
|
||||
@Schema(description = "设备ID", example = "3001")
|
||||
private Long deviceId;
|
||||
|
||||
@Schema(description = "设备编码", example = "badge_001")
|
||||
private String deviceKey;
|
||||
|
||||
@Schema(description = "状态(IDLE/BUSY/OFFLINE/PAUSED)", example = "IDLE")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "电量(0-100)", example = "85")
|
||||
private Integer batteryLevel;
|
||||
|
||||
@Schema(description = "最后心跳时间", example = "2026-01-23 14:30:25")
|
||||
private String lastHeartbeatTime;
|
||||
|
||||
@Schema(description = "当前所在区域ID", example = "100")
|
||||
private Long currentAreaId;
|
||||
|
||||
@Schema(description = "当前所在区域名称", example = "A区洗手间")
|
||||
private String currentAreaName;
|
||||
|
||||
@Schema(description = "今日完成工单数", example = "5")
|
||||
private Integer todayCompletedCount;
|
||||
|
||||
@Schema(description = "今日工作时长(分钟)", example = "180")
|
||||
private Integer todayWorkMinutes;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.viewsh.module.ops.api.clean;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 工单时间轴响应 DTO
|
||||
* <p>
|
||||
* 用于工单详情页时间轴展示
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Schema(description = "管理后台 - 工单时间轴 Response DTO")
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class OrderTimelineRespDTO {
|
||||
|
||||
@Schema(description = "工单ID", example = "1001")
|
||||
private Long orderId;
|
||||
|
||||
@Schema(description = "当前状态", example = "ARRIVED")
|
||||
private String currentStatus;
|
||||
|
||||
@Schema(description = "时间轴节点列表")
|
||||
private List<TimelineItemDTO> timeline;
|
||||
|
||||
/**
|
||||
* 时间轴节点 DTO
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "时间轴节点")
|
||||
public static class TimelineItemDTO {
|
||||
|
||||
@Schema(description = "状态", example = "PENDING")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "状态名称", example = "工单创建")
|
||||
private String statusName;
|
||||
|
||||
@Schema(description = "时间", example = "2026-01-23 14:30:25")
|
||||
private String time;
|
||||
|
||||
@Schema(description = "操作人", example = "系统")
|
||||
private String operator;
|
||||
|
||||
@Schema(description = "描述", example = "蓝牙信标触发自动创建")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "额外信息(如RSSI值、信标ID等)")
|
||||
private Map<String, Object> extra;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.viewsh.module.ops.api.clean;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 快速统计响应 DTO
|
||||
* <p>
|
||||
* 用于工单中心仪表盘快速统计展示
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Schema(description = "管理后台 - 快速统计 Response DTO")
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class QuickStatsRespDTO {
|
||||
|
||||
@Schema(description = "待分配数量", example = "8")
|
||||
private Integer pendingCount;
|
||||
|
||||
@Schema(description = "进行中数量", example = "15")
|
||||
private Integer inProgressCount;
|
||||
|
||||
@Schema(description = "今日完成数量", example = "42")
|
||||
private Integer completedTodayCount;
|
||||
|
||||
@Schema(description = "在线工牌数量", example = "12")
|
||||
private Integer onlineBadgeCount;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.viewsh.module.ops.enums;
|
||||
|
||||
import com.viewsh.framework.common.exception.ErrorCode;
|
||||
|
||||
/**
|
||||
* ops 错误码枚举类
|
||||
* <p>
|
||||
* ops 系统,使用 1-020-000-000 段
|
||||
*/
|
||||
public interface ErrorCodeConstants {
|
||||
|
||||
// ========== 业务区域 1-020-001-000 ============
|
||||
ErrorCode AREA_NOT_FOUND = new ErrorCode(1_020_001_000, "区域不存在");
|
||||
ErrorCode AREA_HAS_CHILDREN = new ErrorCode(1_020_001_001, "该区域下存在子区域,请先处理子区域");
|
||||
ErrorCode AREA_HAS_DEVICES = new ErrorCode(1_020_001_002, "该区域已绑定设备,请先解除绑定");
|
||||
ErrorCode AREA_PARENT_LOOP = new ErrorCode(1_020_001_003, "不能将父级设置为自己或子孙节点");
|
||||
ErrorCode AREA_CODE_EXISTS = new ErrorCode(1_020_001_004, "区域编码已存在");
|
||||
|
||||
// ========== 区域设备关联 1-020-002-000 ============
|
||||
ErrorCode DEVICE_NOT_FOUND = new ErrorCode(1_020_002_000, "设备不存在");
|
||||
ErrorCode DEVICE_ALREADY_BOUND = new ErrorCode(1_020_002_001, "该工牌已绑定至此区域");
|
||||
ErrorCode DEVICE_TYPE_ALREADY_BOUND = new ErrorCode(1_020_002_002, "该区域已绑定{},一个区域只能绑定一个");
|
||||
ErrorCode DEVICE_RELATION_NOT_FOUND = new ErrorCode(1_020_002_003, "设备关联关系不存在");
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.viewsh.module.ops.enums;
|
||||
|
||||
import com.viewsh.framework.common.core.ArrayValuable;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* 工牌通知类型枚举
|
||||
* <p>
|
||||
* 用于工牌设备通知(语音、震动等)
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public enum NotifyTypeEnum implements ArrayValuable<String> {
|
||||
|
||||
VOICE("VOICE", "语音通知"),
|
||||
VIBRATE("VIBRATE", "震动通知");
|
||||
|
||||
public static final String[] ARRAYS = Arrays.stream(values()).map(NotifyTypeEnum::getType).toArray(String[]::new);
|
||||
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
private final String type;
|
||||
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
private final String description;
|
||||
|
||||
@Override
|
||||
public String[] array() {
|
||||
return ARRAYS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据类型获取枚举
|
||||
*
|
||||
* @param type 类型
|
||||
* @return 枚举实例,未找到返回 null
|
||||
*/
|
||||
public static NotifyTypeEnum fromType(String type) {
|
||||
if (type == null) {
|
||||
return null;
|
||||
}
|
||||
return Arrays.stream(values())
|
||||
.filter(e -> e.getType().equals(type))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.viewsh.module.ops.controller.admin.area;
|
||||
|
||||
import com.viewsh.framework.apilog.core.annotation.ApiAccessLog;
|
||||
import com.viewsh.framework.common.pojo.CommonResult;
|
||||
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.service.area.AreaDeviceRelationService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.viewsh.framework.apilog.core.enums.OperateTypeEnum.*;
|
||||
import static com.viewsh.framework.common.pojo.CommonResult.success;
|
||||
|
||||
/**
|
||||
* 管理后台 - 区域设备关联管理
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Tag(name = "管理后台 - 区域设备关联")
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/admin-api/ops/area/device-relation")
|
||||
@Validated
|
||||
public class AreaDeviceRelationController {
|
||||
|
||||
@Resource
|
||||
private AreaDeviceRelationService areaDeviceRelationService;
|
||||
|
||||
@GetMapping("/list")
|
||||
@Operation(summary = "查询区域已绑定设备列表")
|
||||
@Parameter(name = "areaId", description = "区域ID", required = true)
|
||||
// @PreAuthorize("@ss.hasPermission('ops:area:query')")
|
||||
public CommonResult<List<AreaDeviceRelationRespVO>> listByAreaId(@RequestParam("areaId") Long areaId) {
|
||||
List<AreaDeviceRelationRespVO> list = areaDeviceRelationService.listByAreaId(areaId);
|
||||
return success(list);
|
||||
}
|
||||
|
||||
@PostMapping("/bind")
|
||||
@Operation(summary = "绑定设备到区域")
|
||||
// @PreAuthorize("@ss.hasPermission('ops:area:bind-device')")
|
||||
@ApiAccessLog(operateType = CREATE)
|
||||
public CommonResult<Long> bindDevice(@Valid @RequestBody AreaDeviceBindReqVO bindReq) {
|
||||
Long relationId = areaDeviceRelationService.bindDevice(bindReq);
|
||||
return success(relationId);
|
||||
}
|
||||
|
||||
@PutMapping("/update")
|
||||
@Operation(summary = "更新设备关联配置")
|
||||
// @PreAuthorize("@ss.hasPermission('ops:area:config-device')")
|
||||
@ApiAccessLog(operateType = UPDATE)
|
||||
public CommonResult<Boolean> updateRelation(@Valid @RequestBody AreaDeviceUpdateReqVO updateReq) {
|
||||
areaDeviceRelationService.updateRelation(updateReq);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@DeleteMapping("/remove")
|
||||
@Operation(summary = "解除设备绑定")
|
||||
@Parameter(name = "id", description = "关联ID", required = true)
|
||||
// @PreAuthorize("@ss.hasPermission('ops:area:bind-device')")
|
||||
@ApiAccessLog(operateType = DELETE)
|
||||
public CommonResult<Boolean> unbindDevice(@RequestParam("id") Long id) {
|
||||
Boolean result = areaDeviceRelationService.unbindDevice(id);
|
||||
return success(result);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.viewsh.module.ops.controller.admin.area;
|
||||
|
||||
import com.viewsh.framework.apilog.core.annotation.ApiAccessLog;
|
||||
import com.viewsh.framework.common.pojo.CommonResult;
|
||||
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.service.area.OpsBusAreaService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.viewsh.framework.apilog.core.enums.OperateTypeEnum.*;
|
||||
import static com.viewsh.framework.common.pojo.CommonResult.success;
|
||||
|
||||
/**
|
||||
* 管理后台 - 业务区域管理
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Tag(name = "管理后台 - 业务区域")
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/admin-api/ops/area")
|
||||
@Validated
|
||||
public class OpsBusAreaController {
|
||||
|
||||
@Resource
|
||||
private OpsBusAreaService opsBusAreaService;
|
||||
|
||||
@GetMapping("/tree")
|
||||
@Operation(summary = "获取区域树(平铺列表,由前端自行组装)")
|
||||
// @PreAuthorize("@ss.hasPermission('ops:area:query')")
|
||||
public CommonResult<List<OpsBusAreaRespVO>> getAreaTree(OpsBusAreaPageReqVO reqVO) {
|
||||
List<OpsBusAreaRespVO> list = opsBusAreaService.getAreaTree(reqVO);
|
||||
return success(list);
|
||||
}
|
||||
|
||||
@GetMapping("/get")
|
||||
@Operation(summary = "获取区域详情")
|
||||
@Parameter(name = "id", description = "区域ID", required = true)
|
||||
// @PreAuthorize("@ss.hasPermission('ops:area:query')")
|
||||
public CommonResult<OpsBusAreaRespVO> getArea(@RequestParam("id") Long id) {
|
||||
OpsBusAreaRespVO area = opsBusAreaService.getArea(id);
|
||||
return success(area);
|
||||
}
|
||||
|
||||
@PostMapping("/create")
|
||||
@Operation(summary = "新增区域")
|
||||
// @PreAuthorize("@ss.hasPermission('ops:area:create')")
|
||||
@ApiAccessLog(operateType = CREATE)
|
||||
public CommonResult<Long> createArea(@Valid @RequestBody OpsBusAreaCreateReqVO createReq) {
|
||||
Long areaId = opsBusAreaService.createArea(createReq);
|
||||
return success(areaId);
|
||||
}
|
||||
|
||||
@PutMapping("/update")
|
||||
@Operation(summary = "更新区域")
|
||||
// @PreAuthorize("@ss.hasPermission('ops:area:update')")
|
||||
@ApiAccessLog(operateType = UPDATE)
|
||||
public CommonResult<Boolean> updateArea(@Valid @RequestBody OpsBusAreaUpdateReqVO updateReq) {
|
||||
opsBusAreaService.updateArea(updateReq);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@DeleteMapping("/delete")
|
||||
@Operation(summary = "删除区域")
|
||||
@Parameter(name = "id", description = "区域ID", required = true)
|
||||
// @PreAuthorize("@ss.hasPermission('ops:area:delete')")
|
||||
@ApiAccessLog(operateType = DELETE)
|
||||
public CommonResult<Boolean> deleteArea(@RequestParam("id") Long id) {
|
||||
Boolean result = opsBusAreaService.deleteArea(id);
|
||||
return success(result);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.viewsh.module.ops.controller.admin.clean;
|
||||
|
||||
import com.viewsh.framework.common.pojo.CommonResult;
|
||||
import com.viewsh.module.ops.api.clean.BadgeRealtimeStatusRespDTO;
|
||||
import com.viewsh.module.ops.api.clean.BadgeStatusRespDTO;
|
||||
import com.viewsh.module.ops.environment.dal.dataobject.BadgeNotifyReqDTO;
|
||||
import com.viewsh.module.ops.environment.service.badge.CleanBadgeService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.viewsh.framework.common.pojo.CommonResult.success;
|
||||
|
||||
/**
|
||||
* 管理后台 - 保洁工牌 Controller
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Tag(name = "管理后台 - 保洁工牌")
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/ops/clean/badge")
|
||||
@Validated
|
||||
public class CleanBadgeController {
|
||||
|
||||
@Resource
|
||||
private CleanBadgeService cleanBadgeService;
|
||||
|
||||
@GetMapping("/list")
|
||||
@Operation(summary = "工牌实时状态列表")
|
||||
@Parameter(name = "areaId", description = "区域ID", required = false)
|
||||
@Parameter(name = "status", description = "状态筛选(IDLE/BUSY/OFFLINE/PAUSED)", required = false)
|
||||
@PreAuthorize("@ss.hasPermission('ops:clean:badge:query')")
|
||||
public CommonResult<List<BadgeStatusRespDTO>> getBadgeList(
|
||||
@RequestParam(value = "areaId", required = false) Long areaId,
|
||||
@RequestParam(value = "status", required = false) String status) {
|
||||
|
||||
List<BadgeStatusRespDTO> result = cleanBadgeService.getBadgeStatusList(areaId, status);
|
||||
return success(result);
|
||||
}
|
||||
|
||||
@GetMapping("/realtime/{badgeId}")
|
||||
@Operation(summary = "工牌实时状态详情")
|
||||
@Parameter(name = "badgeId", description = "工牌设备ID", required = true)
|
||||
@PreAuthorize("@ss.hasPermission('ops:clean:badge:query')")
|
||||
public CommonResult<BadgeRealtimeStatusRespDTO> getBadgeRealtimeStatus(
|
||||
@PathVariable("badgeId") Long badgeId) {
|
||||
|
||||
BadgeRealtimeStatusRespDTO result = cleanBadgeService.getBadgeRealtimeStatus(badgeId);
|
||||
return success(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.viewsh.module.ops.controller.admin.clean;
|
||||
|
||||
import com.viewsh.framework.common.pojo.CommonResult;
|
||||
import com.viewsh.module.ops.api.clean.QuickStatsRespDTO;
|
||||
import com.viewsh.module.ops.environment.service.dashboard.CleanDashboardService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import static com.viewsh.framework.common.pojo.CommonResult.success;
|
||||
|
||||
/**
|
||||
* 管理后台 - 保洁仪表盘 Controller
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Tag(name = "管理后台 - 保洁仪表盘")
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/ops/clean/dashboard")
|
||||
@Validated
|
||||
public class CleanDashboardController {
|
||||
|
||||
@Resource
|
||||
private CleanDashboardService cleanDashboardService;
|
||||
|
||||
@GetMapping("/quick-stats")
|
||||
@Operation(summary = "快速统计")
|
||||
@PreAuthorize("@ss.hasPermission('ops:clean:dashboard:query')")
|
||||
public CommonResult<QuickStatsRespDTO> getQuickStats() {
|
||||
QuickStatsRespDTO result = cleanDashboardService.getQuickStats();
|
||||
return success(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.viewsh.module.ops.controller.admin.clean;
|
||||
|
||||
import com.viewsh.framework.common.pojo.CommonResult;
|
||||
import com.viewsh.module.ops.environment.dal.dataobject.BadgeNotifyReqDTO;
|
||||
import com.viewsh.module.ops.environment.service.badge.CleanBadgeService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import static com.viewsh.framework.common.pojo.CommonResult.success;
|
||||
|
||||
/**
|
||||
* 管后台 - 保洁设备 Controller
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Tag(name = "管理后台 - 保洁设备")
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/ops/clean/device")
|
||||
@Validated
|
||||
public class CleanDeviceController {
|
||||
|
||||
@Resource
|
||||
private CleanBadgeService cleanBadgeService;
|
||||
|
||||
@PostMapping("/notify")
|
||||
@Operation(summary = "发送工牌通知(语音/震动)")
|
||||
@PreAuthorize("@ss.hasPermission('ops:clean:device:notify')")
|
||||
public CommonResult<Boolean> sendBadgeNotify(
|
||||
@Valid @RequestBody BadgeNotifyReqDTO req) {
|
||||
|
||||
cleanBadgeService.sendBadgeNotify(req);
|
||||
return success(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.viewsh.module.ops.controller.admin.clean;
|
||||
|
||||
import com.viewsh.framework.common.pojo.CommonResult;
|
||||
import com.viewsh.module.ops.api.clean.OrderTimelineRespDTO;
|
||||
import com.viewsh.module.ops.environment.dal.dataobject.ManualCompleteOrderReqDTO;
|
||||
import com.viewsh.module.ops.environment.dal.dataobject.UpgradePriorityReqDTO;
|
||||
import com.viewsh.module.ops.environment.service.cleanorder.CleanWorkOrderService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import static com.viewsh.framework.common.pojo.CommonResult.success;
|
||||
|
||||
/**
|
||||
* 管理后台 - 保洁工单 Controller
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Tag(name = "管理后台 - 保洁工单")
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/ops/clean/order")
|
||||
@Validated
|
||||
public class CleanWorkOrderController {
|
||||
|
||||
@Resource
|
||||
private CleanWorkOrderService cleanWorkOrderService;
|
||||
|
||||
@GetMapping("/timeline/{orderId}")
|
||||
@Operation(summary = "工单时间轴")
|
||||
@Parameter(name = "orderId", description = "工单ID", required = true)
|
||||
@PreAuthorize("@ss.hasPermission('ops:clean:order:query')")
|
||||
public CommonResult<OrderTimelineRespDTO> getOrderTimeline(
|
||||
@PathVariable("orderId") Long orderId) {
|
||||
|
||||
OrderTimelineRespDTO result = cleanWorkOrderService.getOrderTimeline(orderId);
|
||||
return success(result);
|
||||
}
|
||||
|
||||
@PostMapping("/manual-complete")
|
||||
@Operation(summary = "手动完成工单")
|
||||
@PreAuthorize("@ss.hasPermission('ops:clean:order:complete')")
|
||||
public CommonResult<Boolean> manualCompleteOrder(
|
||||
@Valid @RequestBody ManualCompleteOrderReqDTO req) {
|
||||
|
||||
cleanWorkOrderService.manualCompleteOrder(req);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@PostMapping("/upgrade-priority")
|
||||
@Operation(summary = "升级工单优先级")
|
||||
@PreAuthorize("@ss.hasPermission('ops:clean:order:upgrade')")
|
||||
public CommonResult<Boolean> upgradePriority(
|
||||
@Valid @RequestBody UpgradePriorityReqDTO req) {
|
||||
|
||||
cleanWorkOrderService.upgradePriority(req);
|
||||
return success(true);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.viewsh.module.ops.framework.rpc.config;
|
||||
|
||||
import com.viewsh.module.iot.api.device.IotDeviceControlApi;
|
||||
import com.viewsh.module.iot.api.device.IotDeviceQueryApi;
|
||||
import com.viewsh.module.iot.api.device.IotDeviceStatusQueryApi;
|
||||
import com.viewsh.module.system.api.notify.NotifyMessageSendApi;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
@@ -10,6 +11,7 @@ import org.springframework.context.annotation.Configuration;
|
||||
@EnableFeignClients(clients = {
|
||||
NotifyMessageSendApi.class,
|
||||
IotDeviceControlApi.class,
|
||||
IotDeviceQueryApi.class,
|
||||
IotDeviceStatusQueryApi.class
|
||||
})
|
||||
public class RpcConfiguration {
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
package com.viewsh.module.ops.controller.admin.area;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
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.service.area.AreaDeviceRelationService;
|
||||
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 org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* 区域设备关联管理 Controller 测试
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class AreaDeviceRelationControllerTest {
|
||||
|
||||
@Mock
|
||||
private AreaDeviceRelationService areaDeviceRelationService;
|
||||
|
||||
@InjectMocks
|
||||
private AreaDeviceRelationController controller;
|
||||
|
||||
private MockMvc mockMvc;
|
||||
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
private AreaDeviceRelationRespVO testRelationVO;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
|
||||
objectMapper = new ObjectMapper();
|
||||
|
||||
testRelationVO = new AreaDeviceRelationRespVO();
|
||||
testRelationVO.setId(1L);
|
||||
testRelationVO.setAreaId(100L);
|
||||
testRelationVO.setAreaName("A栋3层电梯厅");
|
||||
testRelationVO.setDeviceId(50001L);
|
||||
testRelationVO.setDeviceKey("TRAFFIC_COUNTER_001");
|
||||
testRelationVO.setDeviceName("客流计数器001");
|
||||
testRelationVO.setProductId(10L);
|
||||
testRelationVO.setProductKey("traffic_counter_v1");
|
||||
testRelationVO.setProductName("客流计数器");
|
||||
testRelationVO.setRelationType("TRAFFIC_COUNTER");
|
||||
testRelationVO.setConfigData(new HashMap<>());
|
||||
testRelationVO.setEnabled(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testListByAreaId_Success() throws Exception {
|
||||
// Given
|
||||
when(areaDeviceRelationService.listByAreaId(100L))
|
||||
.thenReturn(Arrays.asList(testRelationVO));
|
||||
|
||||
// When & Then
|
||||
mockMvc.perform(get("/admin-api/ops/area/device-relation/list")
|
||||
.param("areaId", "100")
|
||||
.contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data").isArray())
|
||||
.andExpect(jsonPath("$.data[0].deviceId").value(50001))
|
||||
.andExpect(jsonPath("$.data[0].deviceName").value("客流计数器001"));
|
||||
|
||||
verify(areaDeviceRelationService).listByAreaId(100L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testListByAreaId_Empty_Success() throws Exception {
|
||||
// Given
|
||||
when(areaDeviceRelationService.listByAreaId(999L))
|
||||
.thenReturn(Arrays.asList());
|
||||
|
||||
// When & Then
|
||||
mockMvc.perform(get("/admin-api/ops/area/device-relation/list")
|
||||
.param("areaId", "999")
|
||||
.contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data").isArray())
|
||||
.andExpect(jsonPath("$.data").isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBindDevice_Success() throws Exception {
|
||||
// Given
|
||||
AreaDeviceBindReqVO bindReq = new AreaDeviceBindReqVO();
|
||||
bindReq.setAreaId(100L);
|
||||
bindReq.setDeviceId(50001L);
|
||||
bindReq.setRelationType("TRAFFIC_COUNTER");
|
||||
|
||||
Map<String, Object> config = new HashMap<>();
|
||||
config.put("threshold", 100);
|
||||
bindReq.setConfigData(config);
|
||||
|
||||
when(areaDeviceRelationService.bindDevice(any(AreaDeviceBindReqVO.class)))
|
||||
.thenReturn(1L);
|
||||
|
||||
// When & Then
|
||||
mockMvc.perform(post("/admin-api/ops/area/device-relation/bind")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(bindReq)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data").value(1));
|
||||
|
||||
verify(areaDeviceRelationService).bindDevice(any(AreaDeviceBindReqVO.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUpdateRelation_Success() throws Exception {
|
||||
// Given
|
||||
AreaDeviceUpdateReqVO updateReq = new AreaDeviceUpdateReqVO();
|
||||
updateReq.setId(1L);
|
||||
|
||||
Map<String, Object> config = new HashMap<>();
|
||||
config.put("threshold", 150);
|
||||
updateReq.setConfigData(config);
|
||||
|
||||
// When & Then
|
||||
mockMvc.perform(put("/admin-api/ops/area/device-relation/update")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(updateReq)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0));
|
||||
|
||||
verify(areaDeviceRelationService).updateRelation(any(AreaDeviceUpdateReqVO.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUnbindDevice_Success() throws Exception {
|
||||
// Given
|
||||
when(areaDeviceRelationService.unbindDevice(1L)).thenReturn(true);
|
||||
|
||||
// When & Then
|
||||
mockMvc.perform(delete("/admin-api/ops/area/device-relation/remove")
|
||||
.param("id", "1")
|
||||
.contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data").value(true));
|
||||
|
||||
verify(areaDeviceRelationService).unbindDevice(1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUnbindDevice_NotFound() throws Exception {
|
||||
// Given
|
||||
when(areaDeviceRelationService.unbindDevice(999L)).thenReturn(false);
|
||||
|
||||
// When & Then
|
||||
mockMvc.perform(delete("/admin-api/ops/area/device-relation/remove")
|
||||
.param("id", "999")
|
||||
.contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data").value(false));
|
||||
|
||||
verify(areaDeviceRelationService).unbindDevice(999L);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package com.viewsh.module.ops.controller.admin.area;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
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.service.area.OpsBusAreaService;
|
||||
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 org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* 业务区域管理 Controller 测试
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class OpsBusAreaControllerTest {
|
||||
|
||||
@Mock
|
||||
private OpsBusAreaService opsBusAreaService;
|
||||
|
||||
@InjectMocks
|
||||
private OpsBusAreaController controller;
|
||||
|
||||
private MockMvc mockMvc;
|
||||
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
private OpsBusAreaRespVO testAreaResp;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
|
||||
objectMapper = new ObjectMapper();
|
||||
|
||||
testAreaResp = new OpsBusAreaRespVO();
|
||||
testAreaResp.setId(100L);
|
||||
testAreaResp.setParentId(null);
|
||||
testAreaResp.setParentPath(null);
|
||||
testAreaResp.setAreaName("A园区");
|
||||
testAreaResp.setAreaCode("PARK_A");
|
||||
testAreaResp.setAreaType("PARK");
|
||||
testAreaResp.setIsActive(true);
|
||||
testAreaResp.setSort(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetAreaTree_Success() throws Exception {
|
||||
// Given
|
||||
when(opsBusAreaService.getAreaTree(any(OpsBusAreaPageReqVO.class)))
|
||||
.thenReturn(Arrays.asList(testAreaResp));
|
||||
|
||||
// When & Then
|
||||
mockMvc.perform(get("/admin-api/ops/area/tree")
|
||||
.param("areaType", "PARK")
|
||||
.contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data").isArray())
|
||||
.andExpect(jsonPath("$.data[0].areaName").value("A园区"));
|
||||
|
||||
verify(opsBusAreaService).getAreaTree(any(OpsBusAreaPageReqVO.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetAreaTree_Empty_Success() throws Exception {
|
||||
// Given
|
||||
when(opsBusAreaService.getAreaTree(any(OpsBusAreaPageReqVO.class)))
|
||||
.thenReturn(Collections.emptyList());
|
||||
|
||||
// When & Then
|
||||
mockMvc.perform(get("/admin-api/ops/area/tree")
|
||||
.contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data").isArray())
|
||||
.andExpect(jsonPath("$.data").isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetArea_Success() throws Exception {
|
||||
// Given
|
||||
when(opsBusAreaService.getArea(100L)).thenReturn(testAreaResp);
|
||||
|
||||
// When & Then
|
||||
mockMvc.perform(get("/admin-api/ops/area/get")
|
||||
.param("id", "100")
|
||||
.contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.id").value(100))
|
||||
.andExpect(jsonPath("$.data.areaName").value("A园区"));
|
||||
|
||||
verify(opsBusAreaService).getArea(100L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateArea_Success() throws Exception {
|
||||
// Given
|
||||
OpsBusAreaCreateReqVO createReq = new OpsBusAreaCreateReqVO();
|
||||
createReq.setAreaName("A园区");
|
||||
createReq.setAreaCode("PARK_A");
|
||||
createReq.setAreaType("PARK");
|
||||
|
||||
when(opsBusAreaService.createArea(any(OpsBusAreaCreateReqVO.class)))
|
||||
.thenReturn(100L);
|
||||
|
||||
// When & Then
|
||||
mockMvc.perform(post("/admin-api/ops/area/create")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(createReq)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data").value(100));
|
||||
|
||||
verify(opsBusAreaService).createArea(any(OpsBusAreaCreateReqVO.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUpdateArea_Success() throws Exception {
|
||||
// Given
|
||||
OpsBusAreaUpdateReqVO updateReq = new OpsBusAreaUpdateReqVO();
|
||||
updateReq.setId(100L);
|
||||
updateReq.setAreaName("A园区(更新)");
|
||||
|
||||
// When & Then
|
||||
mockMvc.perform(put("/admin-api/ops/area/update")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(updateReq)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0));
|
||||
|
||||
verify(opsBusAreaService).updateArea(any(OpsBusAreaUpdateReqVO.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteArea_Success() throws Exception {
|
||||
// Given
|
||||
when(opsBusAreaService.deleteArea(100L)).thenReturn(true);
|
||||
|
||||
// When & Then
|
||||
mockMvc.perform(delete("/admin-api/ops/area/delete")
|
||||
.param("id", "100")
|
||||
.contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data").value(true));
|
||||
|
||||
verify(opsBusAreaService).deleteArea(100L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteArea_NotFound() throws Exception {
|
||||
// Given
|
||||
when(opsBusAreaService.deleteArea(999L)).thenReturn(false);
|
||||
|
||||
// When & Then
|
||||
mockMvc.perform(delete("/admin-api/ops/area/delete")
|
||||
.param("id", "999")
|
||||
.contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data").value(false));
|
||||
|
||||
verify(opsBusAreaService).deleteArea(999L);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user