Merge branch 'master' into feat/multi-tenant
# Conflicts: # viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/dal/redis/TrafficActiveOrderRedisDAO.java # viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/badge/BadgeDeviceStatusServiceImpl.java # viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/dispatch/UserDispatchStatusServiceImpl.java
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
package com.viewsh.module.ops.dal.dataobject.trajectory;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.viewsh.framework.tenant.core.db.TenantBaseDO;
|
||||
import lombok.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 设备轨迹记录 DO
|
||||
* <p>
|
||||
* 记录工牌设备进出各区域的轨迹
|
||||
* 一条记录表示一次"进入-离开"周期
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@TableName("ops_device_trajectory")
|
||||
@KeySequence("ops_device_trajectory_seq")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class OpsDeviceTrajectoryDO extends TenantBaseDO {
|
||||
|
||||
/**
|
||||
* 主键
|
||||
*/
|
||||
@TableId
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 工牌设备ID
|
||||
*/
|
||||
private Long deviceId;
|
||||
|
||||
/**
|
||||
* 设备名称(冗余)
|
||||
*/
|
||||
private String deviceName;
|
||||
|
||||
/**
|
||||
* 设备备注名称(冗余)
|
||||
*/
|
||||
private String nickname;
|
||||
|
||||
/**
|
||||
* 人员ID(预留)
|
||||
*/
|
||||
private Long personId;
|
||||
|
||||
/**
|
||||
* 人员名称(预留)
|
||||
*/
|
||||
private String personName;
|
||||
|
||||
/**
|
||||
* 区域ID
|
||||
*/
|
||||
private Long areaId;
|
||||
|
||||
/**
|
||||
* 区域名称(冗余)
|
||||
*/
|
||||
private String areaName;
|
||||
|
||||
/**
|
||||
* 匹配的 Beacon MAC
|
||||
*/
|
||||
private String beaconMac;
|
||||
|
||||
/**
|
||||
* 进入时间
|
||||
*/
|
||||
private LocalDateTime enterTime;
|
||||
|
||||
/**
|
||||
* 离开时间
|
||||
*/
|
||||
private LocalDateTime leaveTime;
|
||||
|
||||
/**
|
||||
* 停留时长(秒)
|
||||
*/
|
||||
private Integer durationSeconds;
|
||||
|
||||
/**
|
||||
* 离开原因
|
||||
* <p>
|
||||
* SIGNAL_LOSS - 信号丢失
|
||||
* AREA_SWITCH - 切换到其他区域
|
||||
* DEVICE_OFFLINE - 设备离线
|
||||
*/
|
||||
private String leaveReason;
|
||||
|
||||
/**
|
||||
* 进入时 RSSI
|
||||
*/
|
||||
private Integer enterRssi;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.viewsh.module.ops.dal.mysql.trajectory;
|
||||
|
||||
import com.viewsh.framework.common.pojo.PageResult;
|
||||
import com.viewsh.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import com.viewsh.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import com.viewsh.module.ops.dal.dataobject.trajectory.OpsDeviceTrajectoryDO;
|
||||
import com.viewsh.module.ops.service.trajectory.dto.TrajectoryPageReqDTO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 设备轨迹记录 Mapper
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Mapper
|
||||
public interface OpsDeviceTrajectoryMapper extends BaseMapperX<OpsDeviceTrajectoryDO> {
|
||||
|
||||
/**
|
||||
* 查询设备在某区域最近一条未关闭的轨迹记录
|
||||
*/
|
||||
default OpsDeviceTrajectoryDO selectOpenRecord(Long deviceId, Long areaId) {
|
||||
return selectOne(new LambdaQueryWrapperX<OpsDeviceTrajectoryDO>()
|
||||
.eq(OpsDeviceTrajectoryDO::getDeviceId, deviceId)
|
||||
.eq(OpsDeviceTrajectoryDO::getAreaId, areaId)
|
||||
.isNull(OpsDeviceTrajectoryDO::getLeaveTime)
|
||||
.orderByDesc(OpsDeviceTrajectoryDO::getEnterTime)
|
||||
.last("LIMIT 1"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询设备在某区域未关闭的轨迹记录(加锁,防止并发竞态)
|
||||
*/
|
||||
default OpsDeviceTrajectoryDO selectOpenRecordForUpdate(Long deviceId, Long areaId) {
|
||||
return selectOne(new LambdaQueryWrapperX<OpsDeviceTrajectoryDO>()
|
||||
.eq(OpsDeviceTrajectoryDO::getDeviceId, deviceId)
|
||||
.eq(OpsDeviceTrajectoryDO::getAreaId, areaId)
|
||||
.isNull(OpsDeviceTrajectoryDO::getLeaveTime)
|
||||
.orderByDesc(OpsDeviceTrajectoryDO::getEnterTime)
|
||||
.last("LIMIT 1 FOR UPDATE"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询设备在某区域、按进入时间精确匹配的未关闭轨迹记录(加锁)
|
||||
*/
|
||||
default OpsDeviceTrajectoryDO selectOpenRecordByEnterTimeForUpdate(Long deviceId, Long areaId,
|
||||
LocalDateTime enterTime) {
|
||||
return selectOne(new LambdaQueryWrapperX<OpsDeviceTrajectoryDO>()
|
||||
.eq(OpsDeviceTrajectoryDO::getDeviceId, deviceId)
|
||||
.eq(OpsDeviceTrajectoryDO::getAreaId, areaId)
|
||||
.eq(OpsDeviceTrajectoryDO::getEnterTime, enterTime)
|
||||
.isNull(OpsDeviceTrajectoryDO::getLeaveTime)
|
||||
.last("LIMIT 1 FOR UPDATE"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询轨迹记录
|
||||
*/
|
||||
default PageResult<OpsDeviceTrajectoryDO> selectPage(TrajectoryPageReqDTO req) {
|
||||
return selectPage(req, new LambdaQueryWrapperX<OpsDeviceTrajectoryDO>()
|
||||
.eqIfPresent(OpsDeviceTrajectoryDO::getDeviceId, req.getDeviceId())
|
||||
.eqIfPresent(OpsDeviceTrajectoryDO::getAreaId, req.getAreaId())
|
||||
.betweenIfPresent(OpsDeviceTrajectoryDO::getEnterTime, req.getEnterTime())
|
||||
.orderByDesc(OpsDeviceTrajectoryDO::getEnterTime));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询某天的轨迹记录(deviceId 可选,为 null 则查全部设备)
|
||||
* <p>
|
||||
* 安全上限 5000 条,防止全量加载过多数据到内存
|
||||
*/
|
||||
default List<OpsDeviceTrajectoryDO> selectByDateAndDevice(LocalDate date, Long deviceId) {
|
||||
LocalDateTime start = date.atStartOfDay();
|
||||
LocalDateTime end = date.plusDays(1).atStartOfDay();
|
||||
return selectList(new LambdaQueryWrapperX<OpsDeviceTrajectoryDO>()
|
||||
.eqIfPresent(OpsDeviceTrajectoryDO::getDeviceId, deviceId)
|
||||
.ge(OpsDeviceTrajectoryDO::getEnterTime, start)
|
||||
.lt(OpsDeviceTrajectoryDO::getEnterTime, end)
|
||||
.orderByAsc(OpsDeviceTrajectoryDO::getEnterTime)
|
||||
.last("LIMIT 5000"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,10 +7,8 @@ import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
@@ -25,6 +23,8 @@ import java.util.stream.Collectors;
|
||||
@Component
|
||||
public class AreaPathBuilder {
|
||||
|
||||
private static final Pattern DIGITS = Pattern.compile("\\d+");
|
||||
|
||||
@Resource
|
||||
private OpsBusAreaMapper opsBusAreaMapper;
|
||||
|
||||
@@ -47,7 +47,7 @@ public class AreaPathBuilder {
|
||||
// 解析父级ID列表
|
||||
List<Long> parentIds = Arrays.stream(parentPath.split("/"))
|
||||
.filter(StrUtil::isNotBlank)
|
||||
.filter(pid -> pid.matches("\\d+"))
|
||||
.filter(pid -> DIGITS.matcher(pid).matches())
|
||||
.map(Long::parseLong)
|
||||
.filter(pid -> !pid.equals(area.getId()))
|
||||
.collect(Collectors.toList());
|
||||
@@ -105,4 +105,87 @@ public class AreaPathBuilder {
|
||||
return buildPath(area);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量构建区域路径(只执行一次父级查询)
|
||||
*
|
||||
* @param areas 区域对象集合
|
||||
* @return areaId → 完整路径 的映射
|
||||
*/
|
||||
public Map<Long, String> buildPaths(Collection<OpsBusAreaDO> areas) {
|
||||
if (areas == null || areas.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
// 1. 收集所有父级 ID
|
||||
Set<Long> allParentIds = new HashSet<>();
|
||||
for (OpsBusAreaDO area : areas) {
|
||||
if (StrUtil.isNotEmpty(area.getParentPath())) {
|
||||
for (String idStr : area.getParentPath().split("/")) {
|
||||
if (StrUtil.isNotBlank(idStr) && DIGITS.matcher(idStr).matches()) {
|
||||
allParentIds.add(Long.parseLong(idStr));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 排除自身已在集合中的
|
||||
Set<Long> areaIds = areas.stream().map(OpsBusAreaDO::getId).collect(Collectors.toSet());
|
||||
allParentIds.removeAll(areaIds);
|
||||
|
||||
// 2. 一次性查询所有父级
|
||||
Map<Long, String> parentNameMap = new HashMap<>();
|
||||
for (OpsBusAreaDO area : areas) {
|
||||
parentNameMap.put(area.getId(), area.getAreaName());
|
||||
}
|
||||
if (!allParentIds.isEmpty()) {
|
||||
List<OpsBusAreaDO> parents = opsBusAreaMapper.selectBatchIds(allParentIds);
|
||||
if (parents != null) {
|
||||
for (OpsBusAreaDO parent : parents) {
|
||||
parentNameMap.put(parent.getId(), parent.getAreaName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 纯内存拼路径
|
||||
Map<Long, String> result = new HashMap<>(areas.size());
|
||||
for (OpsBusAreaDO area : areas) {
|
||||
result.put(area.getId(), buildPathFromCache(area, parentNameMap));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用预加载的名称缓存构建路径(纯内存,无 DB 查询)
|
||||
*/
|
||||
private String buildPathFromCache(OpsBusAreaDO area, Map<Long, String> nameMap) {
|
||||
String parentPath = area.getParentPath();
|
||||
if (StrUtil.isEmpty(parentPath)) {
|
||||
return area.getAreaName();
|
||||
}
|
||||
|
||||
List<Long> parentIds = Arrays.stream(parentPath.split("/"))
|
||||
.filter(StrUtil::isNotBlank)
|
||||
.filter(pid -> DIGITS.matcher(pid).matches())
|
||||
.map(Long::parseLong)
|
||||
.filter(pid -> !pid.equals(area.getId()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 去重相邻重复
|
||||
List<String> segments = new ArrayList<>();
|
||||
Long lastId = null;
|
||||
for (Long pid : parentIds) {
|
||||
if (!pid.equals(lastId)) {
|
||||
String name = nameMap.get(pid);
|
||||
if (name != null) {
|
||||
segments.add(name);
|
||||
}
|
||||
lastId = pid;
|
||||
}
|
||||
}
|
||||
|
||||
if (segments.isEmpty()) {
|
||||
return area.getAreaName();
|
||||
}
|
||||
return String.join("/", segments) + "/" + area.getAreaName();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -82,6 +82,23 @@ public interface AreaDeviceService {
|
||||
*/
|
||||
List<Long> getDeviceIdsByAreaAndType(Long areaId, String relationType);
|
||||
|
||||
/**
|
||||
* 查询所有启用的指定类型设备关联
|
||||
*
|
||||
* @param relationType 关联类型(BADGE/BEACON/TRAFFIC_COUNTER)
|
||||
* @return 所有启用的指定类型关联关系
|
||||
*/
|
||||
List<OpsAreaDeviceRelationDO> listAllByType(String relationType);
|
||||
|
||||
/**
|
||||
* 查询所有启用的 Beacon 设备关联
|
||||
* <p>
|
||||
* 用于轨迹检测功能,获取全量 Beacon 注册表
|
||||
*
|
||||
* @return 所有启用的 Beacon 类型关联关系
|
||||
*/
|
||||
List<OpsAreaDeviceRelationDO> listAllEnabledBeacons();
|
||||
|
||||
/**
|
||||
* 初始化区域设备配置缓存
|
||||
* <p>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.viewsh.module.ops.service.area;
|
||||
|
||||
import com.viewsh.framework.common.util.json.JsonUtils;
|
||||
import com.viewsh.framework.tenant.core.aop.TenantIgnore;
|
||||
import com.viewsh.module.ops.api.area.AreaDeviceDTO;
|
||||
import com.viewsh.module.ops.dal.dataobject.area.OpsAreaDeviceRelationDO;
|
||||
import com.viewsh.module.ops.dal.mysql.area.OpsAreaDeviceRelationMapper;
|
||||
@@ -163,6 +164,19 @@ public class AreaDeviceServiceImpl implements AreaDeviceService, InitializingBea
|
||||
}
|
||||
|
||||
@Override
|
||||
@TenantIgnore // RPC 接口,跨租户查询全量设备
|
||||
public List<OpsAreaDeviceRelationDO> listAllByType(String relationType) {
|
||||
return relationMapper.selectListByAreaIdAndRelationType(null, relationType);
|
||||
}
|
||||
|
||||
@Override
|
||||
@TenantIgnore // RPC 接口,跨租户查询全量 Beacon
|
||||
public List<OpsAreaDeviceRelationDO> listAllEnabledBeacons() {
|
||||
return listAllByType("BEACON");
|
||||
}
|
||||
|
||||
@Override
|
||||
@TenantIgnore // 启动预热,跨租户加载全量配置
|
||||
public void initConfigCache() {
|
||||
log.info("[AreaDevice] 开始初始化区域设备配置缓存...");
|
||||
|
||||
|
||||
@@ -151,4 +151,14 @@ public interface UserDispatchStatusService {
|
||||
* @return 活跃工单数,无记录返回 0
|
||||
*/
|
||||
int getActiveOrderCount(Long userId);
|
||||
|
||||
/**
|
||||
* 启动校准:从 DB 重建人员调度状态
|
||||
* <p>
|
||||
* 扫描 Redis 中所有 ops:user:dispatch:* key,校验 currentOrderId 对应的工单是否仍在进行中,
|
||||
* 并根据 DB 中实际活跃工单数修正 activeOrderCount / waitingTaskCount / status。
|
||||
*
|
||||
* @return 校准的用户数
|
||||
*/
|
||||
int calibrateFromDb();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package com.viewsh.module.ops.service.dispatch;
|
||||
|
||||
import com.viewsh.module.ops.api.dispatch.UserDispatchStatusDTO;
|
||||
import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO;
|
||||
import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper;
|
||||
import com.viewsh.module.ops.enums.WorkOrderStatusEnum;
|
||||
import com.viewsh.module.ops.infrastructure.redis.OpsRedisKeyBuilder;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.Resource;
|
||||
@@ -10,6 +13,7 @@ import org.springframework.data.redis.core.script.DefaultRedisScript;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 通用人员调度状态服务 - Redis 实现
|
||||
@@ -31,6 +35,9 @@ public class UserDispatchStatusServiceImpl implements UserDispatchStatusService
|
||||
@Resource
|
||||
private StringRedisTemplate stringRedisTemplate;
|
||||
|
||||
@Resource
|
||||
private OpsOrderMapper opsOrderMapper;
|
||||
|
||||
// ==================== Lua 脚本 ====================
|
||||
|
||||
private DefaultRedisScript<Long> dispatchScript;
|
||||
@@ -297,6 +304,107 @@ public class UserDispatchStatusServiceImpl implements UserDispatchStatusService
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 启动校准 ====================
|
||||
|
||||
@Override
|
||||
public int calibrateFromDb() {
|
||||
// 1. SCAN 所有 user dispatch key
|
||||
Set<String> keys = stringRedisTemplate.keys(KEY_PREFIX + "*");
|
||||
if (keys == null || keys.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int calibrated = 0;
|
||||
|
||||
for (String key : keys) {
|
||||
try {
|
||||
String userIdStr = key.substring(KEY_PREFIX.length());
|
||||
Long userId = Long.parseLong(userIdStr);
|
||||
|
||||
// 2. 查 DB:该用户所有非终态、非保洁的活跃工单
|
||||
List<OpsOrderDO> activeOrders = opsOrderMapper.selectList(
|
||||
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<OpsOrderDO>()
|
||||
.eq(OpsOrderDO::getAssigneeId, userId)
|
||||
.ne(OpsOrderDO::getOrderType, "CLEAN")
|
||||
.notIn(OpsOrderDO::getStatus,
|
||||
WorkOrderStatusEnum.COMPLETED.getStatus(),
|
||||
WorkOrderStatusEnum.CANCELLED.getStatus())
|
||||
);
|
||||
|
||||
if (activeOrders.isEmpty()) {
|
||||
// 无活跃工单,但 Redis 中有记录 → 应该是 IDLE
|
||||
Map<Object, Object> current = stringRedisTemplate.opsForHash().entries(key);
|
||||
String currentStatus = current.get("status") != null ? current.get("status").toString() : null;
|
||||
if (!"IDLE".equals(currentStatus) || !"0".equals(Objects.toString(current.get("activeOrderCount"), "0"))) {
|
||||
// 重置为 IDLE
|
||||
stringRedisTemplate.opsForHash().putAll(key, Map.of(
|
||||
"status", "IDLE",
|
||||
"activeOrderCount", "0",
|
||||
"waitingTaskCount", "0",
|
||||
"lastUpdateTime", String.valueOf(System.currentTimeMillis())
|
||||
));
|
||||
stringRedisTemplate.opsForHash().delete(key, "currentOrderId", "currentOrderType", "currentOrderStatus");
|
||||
log.info("[校准] 人员状态重置为IDLE: userId={}, 原状态={}", userId, currentStatus);
|
||||
calibrated++;
|
||||
}
|
||||
} else {
|
||||
// 有活跃工单 → 重建计数
|
||||
int activeCount = activeOrders.size();
|
||||
int waitingCount = (int) activeOrders.stream()
|
||||
.filter(o -> WorkOrderStatusEnum.QUEUED.getStatus().equals(o.getStatus()))
|
||||
.count();
|
||||
|
||||
// 找当前正在执行的工单(DISPATCHED/CONFIRMED/ARRIVED 中最新的)
|
||||
OpsOrderDO currentOrder = activeOrders.stream()
|
||||
.filter(o -> {
|
||||
String s = o.getStatus();
|
||||
return WorkOrderStatusEnum.DISPATCHED.getStatus().equals(s)
|
||||
|| WorkOrderStatusEnum.CONFIRMED.getStatus().equals(s)
|
||||
|| WorkOrderStatusEnum.ARRIVED.getStatus().equals(s);
|
||||
})
|
||||
.max(Comparator.comparing(OpsOrderDO::getUpdateTime))
|
||||
.orElse(null);
|
||||
|
||||
// 判断是否有暂停的工单
|
||||
boolean hasPaused = activeOrders.stream()
|
||||
.anyMatch(o -> WorkOrderStatusEnum.PAUSED.getStatus().equals(o.getStatus()));
|
||||
|
||||
String status = hasPaused ? "PAUSED" : "BUSY";
|
||||
|
||||
Map<String, String> newHash = new HashMap<>();
|
||||
newHash.put("status", status);
|
||||
newHash.put("activeOrderCount", String.valueOf(activeCount));
|
||||
newHash.put("waitingTaskCount", String.valueOf(waitingCount));
|
||||
newHash.put("lastUpdateTime", String.valueOf(System.currentTimeMillis()));
|
||||
if (currentOrder != null) {
|
||||
newHash.put("currentOrderId", String.valueOf(currentOrder.getId()));
|
||||
newHash.put("currentOrderType", currentOrder.getOrderType());
|
||||
newHash.put("currentOrderStatus", currentOrder.getStatus());
|
||||
}
|
||||
|
||||
// 比对 Redis 当前值,有差异则修正
|
||||
Map<Object, Object> current = stringRedisTemplate.opsForHash().entries(key);
|
||||
boolean needUpdate = !String.valueOf(activeCount).equals(Objects.toString(current.get("activeOrderCount"), ""))
|
||||
|| !String.valueOf(waitingCount).equals(Objects.toString(current.get("waitingTaskCount"), ""))
|
||||
|| !status.equals(Objects.toString(current.get("status"), ""));
|
||||
|
||||
if (needUpdate) {
|
||||
stringRedisTemplate.delete(key);
|
||||
stringRedisTemplate.opsForHash().putAll(key, newHash);
|
||||
stringRedisTemplate.expire(key, java.time.Duration.ofSeconds(TTL_SECONDS));
|
||||
log.info("[校准] 人员状态已修正: userId={}, status={}, active={}, waiting={}",
|
||||
userId, status, activeCount, waitingCount);
|
||||
calibrated++;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[校准] 人员状态校准失败: key={}", key, e);
|
||||
}
|
||||
}
|
||||
|
||||
return calibrated;
|
||||
}
|
||||
|
||||
// ==================== Lua 脚本定义 ====================
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.viewsh.module.ops.service.trajectory.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 区域停留分布 Response DTO
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Schema(description = "管理后台 - 区域停留分布 Response DTO")
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AreaStayStatsDTO {
|
||||
|
||||
@Schema(description = "区域名称", example = "男卫")
|
||||
private String areaName;
|
||||
|
||||
@Schema(description = "完整区域路径", example = "A园区/A栋/3层/男卫")
|
||||
private String fullAreaName;
|
||||
|
||||
@Schema(description = "总停留时长(秒)", example = "3600")
|
||||
private Long totalStaySeconds;
|
||||
|
||||
@Schema(description = "访问次数", example = "12")
|
||||
private Long visitCount;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.viewsh.module.ops.service.trajectory.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 工牌设备精简 Response DTO(下拉列表用)
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Schema(description = "管理后台 - 工牌设备精简信息(下拉列表用)")
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class BadgeSimpleRespDTO {
|
||||
|
||||
@Schema(description = "设备ID", example = "31")
|
||||
private Long deviceId;
|
||||
|
||||
@Schema(description = "设备Key", example = "09207455611")
|
||||
private String deviceKey;
|
||||
|
||||
@Schema(description = "设备备注名称", example = "1号工牌")
|
||||
private String nickname;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.viewsh.module.ops.service.trajectory.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 设备实时位置 Response DTO
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Schema(description = "管理后台 - 设备实时位置 Response DTO")
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class DeviceCurrentLocationDTO {
|
||||
|
||||
@Schema(description = "设备ID", example = "31")
|
||||
private Long deviceId;
|
||||
|
||||
@Schema(description = "当前所在区域ID", example = "1301")
|
||||
private Long areaId;
|
||||
|
||||
@Schema(description = "区域名称", example = "A座2楼男卫")
|
||||
private String areaName;
|
||||
|
||||
@Schema(description = "进入时间(毫秒时间戳)", example = "1711872600000")
|
||||
private Long enterTime;
|
||||
|
||||
@Schema(description = "匹配的Beacon MAC", example = "F0:C8:60:1D:10:BB")
|
||||
private String beaconMac;
|
||||
|
||||
@Schema(description = "是否在某区域内")
|
||||
private Boolean inArea;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.viewsh.module.ops.service.trajectory.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 时段出入趋势 Response DTO
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Schema(description = "管理后台 - 时段出入趋势 Response DTO")
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class HourlyTrendDTO {
|
||||
|
||||
@Schema(description = "小时(0-23)", example = "8")
|
||||
private Integer hour;
|
||||
|
||||
@Schema(description = "进入次数", example = "8")
|
||||
private Long enterCount;
|
||||
|
||||
@Schema(description = "离开次数", example = "6")
|
||||
private Long leaveCount;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.viewsh.module.ops.service.trajectory.dto;
|
||||
|
||||
import com.viewsh.framework.common.pojo.PageParam;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static com.viewsh.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
|
||||
|
||||
/**
|
||||
* 轨迹分页查询 Request DTO
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Schema(description = "管理后台 - 设备轨迹分页查询 Request DTO")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class TrajectoryPageReqDTO extends PageParam {
|
||||
|
||||
@Schema(description = "设备ID", example = "31")
|
||||
private Long deviceId;
|
||||
|
||||
@Schema(description = "区域ID", example = "1301")
|
||||
private Long areaId;
|
||||
|
||||
@Schema(description = "进入时间范围(开始时间, 结束时间)")
|
||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||
@Size(min = 2, max = 2, message = "进入时间范围必须包含开始时间和结束时间")
|
||||
private LocalDateTime[] enterTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.viewsh.module.ops.service.trajectory.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 轨迹记录 Response DTO
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Schema(description = "管理后台 - 设备轨迹记录 Response DTO")
|
||||
@Data
|
||||
public class TrajectoryRespDTO {
|
||||
|
||||
@Schema(description = "记录ID", example = "1")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "设备ID", example = "31")
|
||||
private Long deviceId;
|
||||
|
||||
@Schema(description = "设备名称", example = "badge_001")
|
||||
private String deviceName;
|
||||
|
||||
@Schema(description = "设备备注名称", example = "1号工牌")
|
||||
private String nickname;
|
||||
|
||||
@Schema(description = "区域ID", example = "1301")
|
||||
private Long areaId;
|
||||
|
||||
@Schema(description = "区域名称", example = "A座2楼男卫")
|
||||
private String areaName;
|
||||
|
||||
@Schema(description = "完整区域路径", example = "A园区/A栋/3层/男卫")
|
||||
private String fullAreaName;
|
||||
|
||||
@Schema(description = "Beacon MAC", example = "F0:C8:60:1D:10:BB")
|
||||
private String beaconMac;
|
||||
|
||||
@Schema(description = "进入时间")
|
||||
private LocalDateTime enterTime;
|
||||
|
||||
@Schema(description = "离开时间")
|
||||
private LocalDateTime leaveTime;
|
||||
|
||||
@Schema(description = "停留时长(秒)", example = "300")
|
||||
private Integer durationSeconds;
|
||||
|
||||
@Schema(description = "离开原因", example = "SIGNAL_LOSS")
|
||||
private String leaveReason;
|
||||
|
||||
@Schema(description = "进入时RSSI", example = "-65")
|
||||
private Integer enterRssi;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.viewsh.module.ops.service.trajectory.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* 轨迹统计查询 Request DTO
|
||||
* <p>
|
||||
* 用于 summary / hourly-trend / area-stay-stats 三个统计接口
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Schema(description = "管理后台 - 轨迹统计查询 Request DTO")
|
||||
@Data
|
||||
public class TrajectoryStatsReqDTO {
|
||||
|
||||
@Schema(description = "查询日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-04-05")
|
||||
@NotNull(message = "查询日期不能为空")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd")
|
||||
private LocalDate date;
|
||||
|
||||
@Schema(description = "设备ID(不传=全部设备汇总)", example = "31")
|
||||
private Long deviceId;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.viewsh.module.ops.service.trajectory.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 轨迹统计摘要 Response DTO
|
||||
* <p>
|
||||
* 用于 KPI 卡片展示
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Schema(description = "管理后台 - 轨迹统计摘要 Response DTO")
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class TrajectorySummaryDTO {
|
||||
|
||||
@Schema(description = "总作业时长(秒)", example = "7200")
|
||||
private Long workDurationSeconds;
|
||||
|
||||
@Schema(description = "覆盖区域数", example = "5")
|
||||
private Long coveredAreaCount;
|
||||
|
||||
@Schema(description = "总出入事件数", example = "42")
|
||||
private Long totalEvents;
|
||||
|
||||
@Schema(description = "平均停留时长(秒)", example = "171")
|
||||
private Long avgStaySeconds;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.viewsh.module.ops.service.trajectory.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* 轨迹时间线查询 Request DTO
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Schema(description = "管理后台 - 设备轨迹时间线查询 Request DTO")
|
||||
@Data
|
||||
public class TrajectoryTimelineReqDTO {
|
||||
|
||||
@Schema(description = "设备ID(不传=全部设备)", example = "31")
|
||||
private Long deviceId;
|
||||
|
||||
@Schema(description = "查询日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-03-30")
|
||||
@NotNull(message = "查询日期不能为空")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd")
|
||||
private LocalDate date;
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user