feat(iot): add-iot-clean-order-integration阶段一-基础设施

This commit is contained in:
lzh
2026-01-17 15:20:57 +08:00
parent 864b5da245
commit 471cd45162
13 changed files with 1403 additions and 0 deletions

View File

@@ -0,0 +1,165 @@
package com.viewsh.module.iot.dal.dataobject.integration.clean;
import lombok.Data;
/**
* 蓝牙信标检测配置
* <p>
* 用于配置基于工牌蓝牙信标检测自动确认到岗/离岗的规则
* 采用"强进弱出"双阈值算法,避免信号抖动导致的误判
*
* @author AI
*/
@Data
public class BeaconPresenceConfig {
/**
* 是否启用信标检测
*/
private Boolean enabled;
/**
* 目标信标 MAC 地址
* <p>
* 例如F0:C8:60:1D:10:BB
*/
private String beaconMac;
/**
* <20><>动窗口配置
*/
private WindowConfig window;
/**
* 进入(到岗)判定配置 - 强阈值
*/
private EnterConfig enter;
/**
* 退出(离岗)判定配置 - 弱阈值
*/
private ExitConfig exit;
/**
* 滑动窗口配置
*/
@Data
public static class WindowConfig {
/**
* 样本在 Redis 中的保留时间(秒)
* <p>
* 超过此时间的样本将被自动清理,防止 Redis 内存溢出
*/
private Integer sampleTtlSeconds;
/**
* 未检测到信标时的默认填充值
* <p>
* 当工牌未检测到目标信标时,使用此值填充窗口
* 建议设置为 -999明显低于正常 RSSI 值)
*/
private Integer missingValue;
}
/**
* 进入(到岗)判定配置 - 强阈值
* <p>
* 使用强阈值(如 -70dBm可以避免路过误判只有信号足够强才算到达
*/
@Data
public static class EnterConfig {
/**
* RSSI 强阈值dBm
* <p>
* 只有 RSSI >= 此值才算有效信号
* 例如:-70
*/
private Integer rssiThreshold;
/**
* 滑动窗口大小
* <p>
* 采样窗口中的样本数量
* 例如3 表示取最近 3 次采样
*/
private Integer windowSize;
/**
* 命中次数
* <p>
* 窗口中满足阈值的次数达到此值时,判定为到达
* 例如2 表示 3 次中有 2 次满足即判定到达
*/
private Integer hitCount;
/**
* 是否自动触发到岗事件
* <p>
* true = 检测到到达后自动发布 ops.order.arrive 事件
* false = 仅记录日志,不触发事件
*/
private Boolean autoArrival;
}
/**
* 退出(离岗)判定配置 - 弱阈值
* <p>
* 使用弱阈值(如 -85dBm和迟滞设计避免边缘抖动
* 只有信号足够弱或彻底消失才算离开
*/
@Data
public static class ExitConfig {
/**
* RSSI 弱阈值dBm
* <p>
* RSSI < 此值或信号丢失才算离开
* 例如:-85
*/
private Integer weakRssiThreshold;
/**
* 滑动窗口大小
*/
private Integer windowSize;
/**
* 命中次数
* <p>
* 窗口中满足弱阈值(丢失)的次数达到此值时,判定为离开
*/
private Integer hitCount;
/**
* 确认离开后多久发送警告(分钟)
* <p>
* 0 = 立即发送
*/
private Integer warningDelayMinutes;
/**
* 持续丢失多久后触发自动完成(分钟)
* <p>
* 从首次丢失开始计时,持续丢失超过此时间后自动完成工单
*/
private Integer lossTimeoutMinutes;
/**
* 最小有效作业时长(分钟)
* <p>
* 用于防止"打卡即走"作弊
* 如果作业时长(从到岗到离开)小于此值,则抑制自动完成
*/
private Integer minValidWorkMinutes;
/**
* 是否自动触发完成事件
* <p>
* true = 检测到离开超时后自动发布 ops.order.complete 事件
* false = 仅记录日志,不触发事件
*/
private Boolean autoComplete;
}
}

View File

@@ -0,0 +1,33 @@
package com.viewsh.module.iot.dal.dataobject.integration.clean;
import lombok.Data;
/**
* 按键事件配置
* <p>
* 用于配置工牌按键事件的处理规则
*
* @author AI
*/
@Data
public class ButtonEventConfig {
/**
* 是否启用按键事件处理
*/
private Boolean enabled;
/**
* 确认键 ID
* <p>
* 当保洁员按下此按键时,触发工单确认事件
*/
private Integer confirmKeyId;
/**
* 查询键 ID
* <p>
* 当保洁员按下此按键时,触发工单查询事件
*/
private Integer queryKeyId;
}

View File

@@ -0,0 +1,36 @@
package com.viewsh.module.iot.dal.dataobject.integration.clean;
import lombok.Data;
/**
* 保洁工单集成配置
* <p>
* 这是 IoT 设备与保洁工单集成的总配置类,包含所有子配置
* 存储在 {@link OpsAreaDeviceRelationDO#getConfigData()} 中
*
* @author AI
*/
@Data
public class CleanOrderIntegrationConfig {
/**
* 客流阈值配置
* <p>
* 用于基于客流计数器自动触发工单创建
*/
private TrafficThresholdConfig trafficThreshold;
/**
* 蓝牙信标检测配置
* <p>
* 用于基于工牌蓝牙信标检测自动确认到岗/离岗
*/
private BeaconPresenceConfig beaconPresence;
/**
* 按键事件配置
* <p>
* 用于工牌按键事件的处理
*/
private ButtonEventConfig buttonEvent;
}

View File

@@ -0,0 +1,92 @@
package com.viewsh.module.iot.dal.dataobject.integration.clean;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import com.viewsh.framework.mybatis.core.dataobject.BaseDO;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 运营区域设备关联 DO
* <p>
* 用于建立运营区域与 IoT 设备的绑定关系,并存储核心检测配置
* 注意:此表在 ops 库中创建,但在 IoT 模块中<E59D97><E4B8AD>通过 Feign 或直接访问)
*
* @author AI
*/
@TableName(value = "ops_area_device_relation", autoResultMap = true)
@KeySequence("ops_area_device_relation_seq")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OpsAreaDeviceRelationDO extends BaseDO {
/**
* 主键
*/
@TableId
private Long id;
/**
* 运营区域ID
* <p>
* 关联 ops_bus_area.id
*/
private Long areaId;
/**
* IoT设备ID
* <p>
* 关联 iot_device.id
*/
private Long deviceId;
/**
* 设备Key冗余字段便于查询
*/
private String deviceKey;
/**
* 产品ID
* <p>
* 关联 iot_product.id
*/
private Long productId;
/**
* 产品Key冗余字段便于查询
*/
private String productKey;
/**
* 关联类型
* <p>
* TRAFFIC_COUNTER - 客流计数器
* BEACON - 蓝牙信标
* BADGE - 工牌设备
*/
private String relationType;
/**
* 配置数据JSON格式
* <p>
* 存储保洁工单集成的各类配置
* 使用 {@link JacksonTypeHandler} 自动序列化/反序列化
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private CleanOrderIntegrationConfig configData;
/**
* 是否启用
* <p>
* true - 启用
* false - 禁用
*/
private Boolean enabled;
}

View File

@@ -0,0 +1,45 @@
package com.viewsh.module.iot.dal.dataobject.integration.clean;
import lombok.Data;
/**
* 客流阈值配置
* <p>
* 用于配置基于客流计数器自动触发工单创建的规则
*
* @author AI
*/
@Data
public class TrafficThresholdConfig {
/**
* 触发阈值
* <p>
* 当实际客流(当前值 - 基准值)达到此阈值时,触发工单创建
*/
private Integer threshold;
/**
* 统计时间窗口(秒)
* <p>
* 在此时间窗口内,同一设备只能触发一次工单创建,防止重复
*/
private Integer timeWindowSeconds;
/**
* 是否自动创建工单
* <p>
* true = 自动创建工单
* false = 仅记录日志,不创建工单
*/
private Boolean autoCreateOrder;
/**
* 工单优先级
* <p>
* P0 = 紧急
* P1 = 重要
* P2 = 普通
*/
private String orderPriority;
}

View File

@@ -0,0 +1,71 @@
package com.viewsh.module.iot.dal.mysql.integration.clean;
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.iot.dal.dataobject.integration.clean.OpsAreaDeviceRelationDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* 运营区域设备关联 Mapper
*
* @author AI
*/
@Mapper
public interface OpsAreaDeviceRelationMapper extends BaseMapperX<OpsAreaDeviceRelationDO> {
/**
* 根据设备ID查询关联关系仅查询启用的
*
* @param deviceId 设备ID
* @return 关联关系
*/
default OpsAreaDeviceRelationDO selectByDeviceId(Long deviceId) {
return selectOne(new LambdaQueryWrapperX<OpsAreaDeviceRelationDO>()
.eq(OpsAreaDeviceRelationDO::getDeviceId, deviceId)
.eq(OpsAreaDeviceRelationDO::getEnabled, true)
.orderByDesc(OpsAreaDeviceRelationDO::getCreateTime)
.last("LIMIT 1"));
}
/**
* 根据区域ID查询所有启用的关联关系
*
* @param areaId 区域ID
* @return 关联关系列表
*/
default List<OpsAreaDeviceRelationDO> selectListByAreaId(Long areaId) {
return selectList(new LambdaQueryWrapperX<OpsAreaDeviceRelationDO>()
.eq(OpsAreaDeviceRelationDO::getAreaId, areaId)
.eq(OpsAreaDeviceRelationDO::getEnabled, true)
.orderByDesc(OpsAreaDeviceRelationDO::getCreateTime));
}
/**
* 根据区域ID和关联类型查询关联关系
*
* @param areaId 区域ID
* @param relationType 关联类型TRAFFIC_COUNTER/BEACON/BADGE
* @return 关联关系列表
*/
default List<OpsAreaDeviceRelationDO> selectListByAreaIdAndRelationType(Long areaId, String relationType) {
return selectList(new LambdaQueryWrapperX<OpsAreaDeviceRelationDO>()
.eq(OpsAreaDeviceRelationDO::getAreaId, areaId)
.eq(OpsAreaDeviceRelationDO::getRelationType, relationType)
.eq(OpsAreaDeviceRelationDO::getEnabled, true));
}
/**
* 根据产品Key查询所有关联关系
*
* @param productKey 产品Key
* @return 关联关系列表
*/
default List<OpsAreaDeviceRelationDO> selectListByProductKey(String productKey) {
return selectList(new LambdaQueryWrapperX<OpsAreaDeviceRelationDO>()
.eq(OpsAreaDeviceRelationDO::getProductKey, productKey)
.eq(OpsAreaDeviceRelationDO::getEnabled, true));
}
}

View File

@@ -0,0 +1,83 @@
package com.viewsh.module.iot.dal.redis.clean;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Repository;
/**
* 蓝牙信标到达时间 Redis DAO
* <p>
* 用于记录保洁员首次进入区域的时间戳
* 用于计算作业时长Duration = 离开时间 - 到达时间)
*
* @author AI
*/
@Repository
public class BeaconArrivedTimeRedisDAO {
/**
* 到达时间 Key 模式
* <p>
* 格式iot:clean:beacon:arrivedAt:{deviceId}:{areaId}
*/
private static final String ARRIVED_AT_KEY_PATTERN = "iot:clean:beacon:arrivedAt:%s:%s";
/**
* 到达时间的 TTL
* <p>
* 默认保留 24 小时
*/
private static final int ARRIVED_AT_TTL_SECONDS = 86400;
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* 记录到达时间
*
* @param deviceId 设备ID
* @param areaId 区域ID
* @param timestamp 时间戳(毫秒)
*/
public void recordArrivedTime(Long deviceId, Long areaId, Long timestamp) {
String key = formatKey(deviceId, areaId);
stringRedisTemplate.opsForValue().set(key, String.valueOf(timestamp),
ARRIVED_AT_TTL_SECONDS, java.util.concurrent.TimeUnit.SECONDS);
}
/**
* 获取到达时间
*
* @param deviceId 设备ID
* @param areaId 区域ID
* @return 时间戳(毫秒),如果不存在返回 null
*/
public Long getArrivedTime(Long deviceId, Long areaId) {
String key = formatKey(deviceId, areaId);
String timestampStr = stringRedisTemplate.opsForValue().get(key);
if (timestampStr == null) {
return null;
}
return Long.parseLong(timestampStr);
}
/**
* 清除到达时间
*
* @param deviceId 设备ID
* @param areaId 区域ID
*/
public void clearArrivedTime(Long deviceId, Long areaId) {
String key = formatKey(deviceId, areaId);
stringRedisTemplate.delete(key);
}
/**
* 格式化 Redis Key
*/
private static String formatKey(Long deviceId, Long areaId) {
return String.format(ARRIVED_AT_KEY_PATTERN, deviceId, areaId);
}
}

View File

@@ -0,0 +1,112 @@
package com.viewsh.module.iot.dal.redis.clean;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Repository;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* 蓝牙信标 RSSI 滑动窗口 Redis DAO
* <p>
* 用于存储工牌检测到的信标 RSSI 值的滑动窗口样本
* 支持抗抖动检测(基于滑动窗口的"强进弱出"算法)
*
* @author AI
*/
@Repository
public class BeaconRssiWindowRedisDAO {
/**
* 滑动窗口 Key 模式
* <p>
* 格式iot:clean:beacon:rssi:window:{deviceId}:{areaId}
*/
private static final String WINDOW_KEY_PATTERN = "iot:clean:beacon:rssi:window:%s:%s";
/**
* 窗口样本的 TTL
* <p>
* 默认保留 1 小时,防止 Redis 内存溢出
*/
private static final int WINDOW_TTL_SECONDS = 3600;
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* 更新滑动窗口(保留最近 N 个样本)
* <p>
* 如果窗口大小超过 maxSize则移除最旧的样本
*
* @param deviceId 设备ID
* @param areaId 区域ID
* @param rssi RSSI 值
* @param maxSize 窗口最大大小
*/
public void updateWindow(Long deviceId, Long areaId, Integer rssi, Integer maxSize) {
String key = formatKey(deviceId, areaId);
// 获取当前窗口
List<Integer> window = getWindow(deviceId, areaId);
// 添加新样本
window.add(rssi);
// 如果超过最大大小,移除最旧的样本
if (window.size() > maxSize) {
window = window.subList(window.size() - maxSize, window.size());
}
// 转换为逗号分隔的字符串存储
String windowStr = window.stream()
.map(String::valueOf)
.collect(Collectors.joining(","));
// 存储到 Redis
stringRedisTemplate.opsForValue().set(key, windowStr, WINDOW_TTL_SECONDS, TimeUnit.SECONDS);
}
/**
* 获取窗口样本
*
* @param deviceId 设备ID
* @param areaId 区域ID
* @return 窗口样本列表(按时间顺序,从旧到新)
*/
public List<Integer> getWindow(Long deviceId, Long areaId) {
String key = formatKey(deviceId, areaId);
String windowStr = stringRedisTemplate.opsForValue().get(key);
if (windowStr == null || windowStr.isEmpty()) {
return new ArrayList<>();
}
// 解析逗号分隔的字符串
return List.of(windowStr.split(","))
.stream()
.map(Integer::parseInt)
.collect(Collectors.toList());
}
/**
* 清除窗口
*
* @param deviceId 设备ID
* @param areaId 区域ID
*/
public void clearWindow(Long deviceId, Long areaId) {
String key = formatKey(deviceId, areaId);
stringRedisTemplate.delete(key);
}
/**
* 格式化 Redis Key
*/
private static String formatKey(Long deviceId, Long areaId) {
return String.format(WINDOW_KEY_PATTERN, deviceId, areaId);
}
}

View File

@@ -0,0 +1,178 @@
package com.viewsh.module.iot.dal.redis.clean;
import com.viewsh.framework.common.util.json.JsonUtils;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Repository;
import java.util.concurrent.TimeUnit;
/**
* 设备当前工单缓存 Redis DAO
* <p>
* 用于缓存设备当前执行的工单信息(由 Ops 下发)
* 减少物联网模块查询数据库的频率
*
* @author AI
*/
@Repository
public class DeviceCurrentOrderRedisDAO {
/**
* 工单缓存 Key 模式
* <p>
* 格式ops:clean:device:order:{deviceId}
*/
private static final String ORDER_KEY_PATTERN = "ops:clean:device:order:%s";
/**
* 工单缓存的 TTL
* <p>
* 默认保留 1 小时
*/
private static final int ORDER_TTL_SECONDS = 3600;
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* 缓存设备当前工单
*
* @param deviceId 设备ID
* @param orderInfo 工单缓存信息
*/
public void cacheCurrentOrder(Long deviceId, OrderCacheInfo orderInfo) {
String key = formatKey(deviceId);
String json = JsonUtils.toJsonString(orderInfo);
stringRedisTemplate.opsForValue().set(key, json, ORDER_TTL_SECONDS, TimeUnit.SECONDS);
}
/**
* 获取当前工单
*
* @param deviceId 设备ID
* @return 工单缓存信息,如果不存在返回 null
*/
public OrderCacheInfo getCurrentOrder(Long deviceId) {
String key = formatKey(deviceId);
String json = stringRedisTemplate.opsForValue().get(key);
if (json == null) {
return null;
}
return JsonUtils.parseObject(json, OrderCacheInfo.class);
}
/**
* 清除当前工单缓存
*
* @param deviceId 设备ID
*/
public void clearCurrentOrder(Long deviceId) {
String key = formatKey(deviceId);
stringRedisTemplate.delete(key);
}
/**
* 缓存被暂停的工单ID
* <p>
* 用于 P0 插队场景,恢复被暂停的工单
*
* @param deviceId 设备ID
* @param pausedOrderId 被暂停的工单ID
*/
public void cachePausedOrder(Long deviceId, Long pausedOrderId) {
String key = formatKey(deviceId) + ":paused";
stringRedisTemplate.opsForValue().set(key, String.valueOf(pausedOrderId),
ORDER_TTL_SECONDS, TimeUnit.SECONDS);
}
/**
* 获取被暂停的工单ID
*
* @param deviceId 设备ID
* @return 被暂停的工单ID如果不存在返回 null
*/
public Long getPausedOrderId(Long deviceId) {
String key = formatKey(deviceId) + ":paused";
String orderIdStr = stringRedisTemplate.opsForValue().get(key);
return orderIdStr != null ? Long.parseLong(orderIdStr) : null;
}
/**
* 清除被暂停的工单缓存
*
* @param deviceId 设备ID
*/
public void clearPausedOrder(Long deviceId) {
String key = formatKey(deviceId) + ":paused";
stringRedisTemplate.delete(key);
}
/**
* 格式化 Redis Key
*/
private static String formatKey(Long deviceId) {
return String.format(ORDER_KEY_PATTERN, deviceId);
}
/**
* 工单缓存信息
*/
public static class OrderCacheInfo {
/**
* 工单ID
*/
private Long orderId;
/**
* 工单状态
* <p>
* DISPATCHED/ARRIVED/PAUSED/COMPLETED
*/
private String status;
/**
* 区域ID
*/
private Long areaId;
/**
* 信标 MAC 地址
*/
private String beaconMac;
public Long getOrderId() {
return orderId;
}
public void setOrderId(Long orderId) {
this.orderId = orderId;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public Long getAreaId() {
return areaId;
}
public void setAreaId(Long areaId) {
this.areaId = areaId;
}
public String getBeaconMac() {
return beaconMac;
}
public void setBeaconMac(String beaconMac) {
this.beaconMac = beaconMac;
}
}
}

View File

@@ -0,0 +1,168 @@
package com.viewsh.module.iot.dal.redis.clean;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Repository;
import java.util.concurrent.TimeUnit;
/**
* 信号丢失状态记录 Redis DAO
* <p>
* 用于记录保洁员离岗状态,支持离岗警告防抖、超时判断及无效作业拦截
*
* @author AI
*/
@Repository
public class SignalLossRedisDAO {
/**
* 信号丢失 Key 模式
* <p>
* 格式iot:clean:signal:loss:{deviceId}:{areaId}
* 使用 Hash 存储多个字段
*/
private static final String LOSS_KEY_PATTERN = "iot:clean:signal:loss:%s:%s";
/**
* Hash 字段名
*/
private static final String FIELD_FIRST_LOSS_TIME = "firstLossTime";
private static final String FIELD_LAST_LOSS_TIME = "lastLossTime";
private static final String FIELD_WARNING_SENT = "warningSent";
private static final String FIELD_INVALID_WORK_NOTIFIED = "invalidWorkNotified";
/**
* 丢失记录的 TTL
* <p>
* 默认保留 1 小时
*/
private static final int LOSS_TTL_SECONDS = 3600;
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* 记录首次丢失
*
* @param deviceId 设备ID
* @param areaId 区域ID
* @param timestamp 时间戳(毫秒)
*/
public void recordFirstLoss(Long deviceId, Long areaId, Long timestamp) {
String key = formatKey(deviceId, areaId);
// 使用 HSETNX仅当字段不存在时才设置防止覆盖首次丢失时间
stringRedisTemplate.opsForHash().putIfAbsent(key, FIELD_FIRST_LOSS_TIME, String.valueOf(timestamp));
// 更新最后丢失时间
stringRedisTemplate.opsForHash().put(key, FIELD_LAST_LOSS_TIME, String.valueOf(timestamp));
// 设置 TTL
stringRedisTemplate.expire(key, LOSS_TTL_SECONDS, TimeUnit.SECONDS);
}
/**
* 获取首次丢失时间
*
* @param deviceId 设备ID
* @param areaId 区域ID
* @return 时间戳(毫秒),如果不存在返回 null
*/
public Long getFirstLossTime(Long deviceId, Long areaId) {
String key = formatKey(deviceId, areaId);
Object value = stringRedisTemplate.opsForHash().get(key, FIELD_FIRST_LOSS_TIME);
return value != null ? Long.parseLong(value.toString()) : null;
}
/**
* 更新最后丢失时间
*
* @param deviceId 设备ID
* @param areaId 区域ID
* @param timestamp 时间戳(毫秒)
*/
public void updateLastLossTime(Long deviceId, Long areaId, Long timestamp) {
String key = formatKey(deviceId, areaId);
stringRedisTemplate.opsForHash().put(key, FIELD_LAST_LOSS_TIME, String.valueOf(timestamp));
}
/**
* 获取最后丢失时间
*
* @param deviceId 设备ID
* @param areaId 区域ID
* @return 时间戳(毫秒),如果不存在返回 null
*/
public Long getLastLossTime(Long deviceId, Long areaId) {
String key = formatKey(deviceId, areaId);
Object value = stringRedisTemplate.opsForHash().get(key, FIELD_LAST_LOSS_TIME);
return value != null ? Long.parseLong(value.toString()) : null;
}
/**
* 检查是否已发送警告
*
* @param deviceId 设备ID
* @param areaId 区域ID
* @return true - 已发送false - 未发送
*/
public Boolean isWarningSent(Long deviceId, Long areaId) {
String key = formatKey(deviceId, areaId);
Object value = stringRedisTemplate.opsForHash().get(key, FIELD_WARNING_SENT);
return value != null && Boolean.parseBoolean(value.toString());
}
/**
* 标记警告已发送
*
* @param deviceId 设备ID
* @param areaId 区域ID
*/
public void markWarningSent(Long deviceId, Long areaId) {
String key = formatKey(deviceId, areaId);
stringRedisTemplate.opsForHash().put(key, FIELD_WARNING_SENT, "true");
}
/**
* 检查是否已通知无效作业
*
* @param deviceId 设备ID
* @param areaId 区域ID
* @return true - 已通知false - 未通知
*/
public Boolean isInvalidWorkNotified(Long deviceId, Long areaId) {
String key = formatKey(deviceId, areaId);
Object value = stringRedisTemplate.opsForHash().get(key, FIELD_INVALID_WORK_NOTIFIED);
return value != null && Boolean.parseBoolean(value.toString());
}
/**
* 标记无效作业已通知
*
* @param deviceId 设备ID
* @param areaId 区域ID
*/
public void markInvalidWorkNotified(Long deviceId, Long areaId) {
String key = formatKey(deviceId, areaId);
stringRedisTemplate.opsForHash().put(key, FIELD_INVALID_WORK_NOTIFIED, "true");
}
/**
* 清除丢失记录
*
* @param deviceId 设备ID
* @param areaId 区域ID
*/
public void clearLossRecord(Long deviceId, Long areaId) {
String key = formatKey(deviceId, areaId);
stringRedisTemplate.delete(key);
}
/**
* 格式化 Redis Key
*/
private static String formatKey(Long deviceId, Long areaId) {
return String.format(LOSS_KEY_PATTERN, deviceId, areaId);
}
}

View File

@@ -0,0 +1,93 @@
package com.viewsh.module.iot.dal.redis.clean;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Repository;
import java.util.concurrent.TimeUnit;
/**
* 客流计数器基准值 Redis DAO
* <p>
* 用于维护客流计数器的基准值,支持逻辑清零和每日自动校准
* 实际客流 = 当前计数值 - 基准值
*
* @author AI
*/
@Repository
public class TrafficCounterBaseRedisDAO {
/**
* 基准值 Key 模式
* <p>
* 格式iot:clean:traffic:base:{deviceId}
*/
private static final String BASE_KEY_PATTERN = "iot:clean:traffic:base:%s";
/**
* 基准值的 TTL
* <p>
* 默认保留 7 天
*/
private static final int BASE_TTL_SECONDS = 604800;
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* 设置基准值
*
* @param deviceId 设备ID
* @param baseValue 基准值
*/
public void setBaseValue(Long deviceId, Long baseValue) {
String key = formatKey(deviceId);
stringRedisTemplate.opsForValue().set(key, String.valueOf(baseValue),
BASE_TTL_SECONDS, TimeUnit.SECONDS);
}
/**
* 获取基准值
*
* @param deviceId 设备ID
* @return 基准值,如果不存在返回 0
*/
public Long getBaseValue(Long deviceId) {
String key = formatKey(deviceId);
String baseValueStr = stringRedisTemplate.opsForValue().get(key);
if (baseValueStr == null) {
return 0L;
}
return Long.parseLong(baseValueStr);
}
/**
* 重置基准值
* <p>
* 将基准值设置为 0
*
* @param deviceId 设备ID
*/
public void resetBaseValue(Long deviceId) {
setBaseValue(deviceId, 0L);
}
/**
* 删除基准值
*
* @param deviceId 设备ID
*/
public void deleteBaseValue(Long deviceId) {
String key = formatKey(deviceId);
stringRedisTemplate.delete(key);
}
/**
* 格式化 Redis Key
*/
private static String formatKey(Long deviceId) {
return String.format(BASE_KEY_PATTERN, deviceId);
}
}

View File

@@ -0,0 +1,175 @@
package com.viewsh.module.iot.service.integration.clean;
import com.viewsh.module.iot.dal.dataobject.integration.clean.CleanOrderIntegrationConfig;
/**
* 保洁工单集成配置 Service
* <p>
* 提供运营区域设备关联配置的查询和管理能力
* 支持本地 Redis 缓存,减少数据库查询压力
*
* @author AI
*/
public interface CleanOrderIntegrationConfigService {
/**
* 根据设备ID查询配置带缓存
* <p>
* 优先从 Redis 缓存读取,缓存未命中时从数据库查询并写入缓存
* 缓存 TTL: 5 分钟
*
* @param deviceId 设备ID
* @return 集成配置,如果不存在或未启用返回 null
*/
CleanOrderIntegrationConfig getConfigByDeviceId(Long deviceId);
/**
* 根据区域ID查询所有启用的配置带缓存
* <p>
* 优先从 Redis 缓存读取
*
* @param areaId 区域ID
* @return 集成配置列表
*/
java.util.List<AreaDeviceConfigWrapper> getConfigsByAreaId(Long areaId);
/**
* 根据区域ID和关联类型查询配置
* <p>
* 例如:查询某个区域的所有客流计数器配置
*
* @param areaId 区域ID
* @param relationType 关联类型TRAFFIC_COUNTER/BEACON/BADGE
* @return 集成配置列表
*/
java.util.List<AreaDeviceConfigWrapper> getConfigsByAreaIdAndRelationType(Long areaId, String relationType);
/**
* 清除设备配置缓存
* <p>
* 配置更新后调用此方法清除缓存,确保下次查询能获取最新配置
*
* @param deviceId 设备ID
*/
void evictCache(Long deviceId);
/**
* 清除区域配置缓存
*
* @param areaId 区域ID
*/
void evictAreaCache(Long areaId);
/**
* 区域设备配置包装类
* <p>
* 包含配置数据和关联的基础信息设备ID、区域ID、关联类型等
*/
class AreaDeviceConfigWrapper {
/**
* 设备ID
*/
private Long deviceId;
/**
* 设备Key
*/
private String deviceKey;
/**
* 产品ID
*/
private Long productId;
/**
* 产品Key
*/
private String productKey;
/**
* 区域ID
*/
private Long areaId;
/**
* 关联类型
*/
private String relationType;
/**
* 集成配置
*/
private CleanOrderIntegrationConfig config;
public AreaDeviceConfigWrapper() {
}
public AreaDeviceConfigWrapper(Long deviceId, String deviceKey, Long productId,
String productKey, Long areaId, String relationType,
CleanOrderIntegrationConfig config) {
this.deviceId = deviceId;
this.deviceKey = deviceKey;
this.productId = productId;
this.productKey = productKey;
this.areaId = areaId;
this.relationType = relationType;
this.config = config;
}
public Long getDeviceId() {
return deviceId;
}
public void setDeviceId(Long deviceId) {
this.deviceId = deviceId;
}
public String getDeviceKey() {
return deviceKey;
}
public void setDeviceKey(String deviceKey) {
this.deviceKey = deviceKey;
}
public Long getProductId() {
return productId;
}
public void setProductId(Long productId) {
this.productId = productId;
}
public String getProductKey() {
return productKey;
}
public void setProductKey(String productKey) {
this.productKey = productKey;
}
public Long getAreaId() {
return areaId;
}
public void setAreaId(Long areaId) {
this.areaId = areaId;
}
public String getRelationType() {
return relationType;
}
public void setRelationType(String relationType) {
this.relationType = relationType;
}
public CleanOrderIntegrationConfig getConfig() {
return config;
}
public void setConfig(CleanOrderIntegrationConfig config) {
this.config = config;
}
}
}

View File

@@ -0,0 +1,152 @@
package com.viewsh.module.iot.service.integration.clean;
import com.viewsh.framework.common.util.json.JsonUtils;
import com.viewsh.module.iot.dal.dataobject.integration.clean.CleanOrderIntegrationConfig;
import com.viewsh.module.iot.dal.dataobject.integration.clean.OpsAreaDeviceRelationDO;
import com.viewsh.module.iot.dal.mysql.integration.clean.OpsAreaDeviceRelationMapper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* 保洁工单集成配置 Service 实现类
*
* @author AI
*/
@Service
@Slf4j
public class CleanOrderIntegrationConfigServiceImpl implements CleanOrderIntegrationConfigService {
/**
* 配置缓存 Key 模式
*/
private static final String CONFIG_DEVICE_KEY_PATTERN = "iot:clean:config:device:%s";
private static final String CONFIG_AREA_KEY_PATTERN = "iot:clean:config:area:%s";
/**
* 配置缓存 TTL
* <p>
* 5 分钟自动过期
*/
private static final int CONFIG_CACHE_TTL_SECONDS = 300;
@Resource
private OpsAreaDeviceRelationMapper relationMapper;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public CleanOrderIntegrationConfig getConfigByDeviceId(Long deviceId) {
log.debug("[CleanOrderConfig] 查询设备配置deviceId={}", deviceId);
// 1. 尝试从 Redis 缓存读取
String cacheKey = formatDeviceKey(deviceId);
String cachedConfig = stringRedisTemplate.opsForValue().get(cacheKey);
if (cachedConfig != null) {
log.debug("[CleanOrderConfig] 命中缓存deviceId={}", deviceId);
return JsonUtils.parseObject(cachedConfig, CleanOrderIntegrationConfig.class);
}
// 2. 从数据库查询
OpsAreaDeviceRelationDO relation = relationMapper.selectByDeviceId(deviceId);
if (relation == null) {
log.debug("[CleanOrderConfig] 设备无关联配置deviceId={}", deviceId);
return null;
}
if (!relation.getEnabled()) {
log.debug("[CleanOrderConfig] 设备配置已禁用deviceId={}", deviceId);
return null;
}
CleanOrderIntegrationConfig config = relation.getConfigData();
// 3. 写入缓存
if (config != null) {
stringRedisTemplate.opsForValue().set(
cacheKey,
JsonUtils.toJsonString(config),
CONFIG_CACHE_TTL_SECONDS,
TimeUnit.SECONDS
);
}
log.debug("[CleanOrderConfig] 查询到配置deviceId={}, areaId={}", deviceId, relation.getAreaId());
return config;
}
@Override
public List<AreaDeviceConfigWrapper> getConfigsByAreaId(Long areaId) {
log.debug("[CleanOrderConfig] 查询区域配置areaId={}", areaId);
// 区域配置暂不缓存,直接从数据库查询
List<OpsAreaDeviceRelationDO> relations = relationMapper.selectListByAreaId(areaId);
return relations.stream()
.map(this::wrapConfig)
.collect(Collectors.toList());
}
@Override
public List<AreaDeviceConfigWrapper> getConfigsByAreaIdAndRelationType(Long areaId, String relationType) {
log.debug("[CleanOrderConfig] 查询区域配置areaId={}, relationType={}", areaId, relationType);
List<OpsAreaDeviceRelationDO> relations = relationMapper.selectListByAreaIdAndRelationType(areaId, relationType);
return relations.stream()
.map(this::wrapConfig)
.collect(Collectors.toList());
}
@Override
public void evictCache(Long deviceId) {
String cacheKey = formatDeviceKey(deviceId);
stringRedisTemplate.delete(cacheKey);
log.info("[CleanOrderConfig] 清除设备配置缓存deviceId={}", deviceId);
}
@Override
public void evictAreaCache(Long areaId) {
String cacheKey = formatAreaKey(areaId);
stringRedisTemplate.delete(cacheKey);
log.info("[CleanOrderConfig] 清除区域配置缓存areaId={}", areaId);
}
/**
* 包装配置数据
*/
private AreaDeviceConfigWrapper wrapConfig(OpsAreaDeviceRelationDO relation) {
return new AreaDeviceConfigWrapper(
relation.getDeviceId(),
relation.getDeviceKey(),
relation.getProductId(),
relation.getProductKey(),
relation.getAreaId(),
relation.getRelationType(),
relation.getConfigData()
);
}
/**
* 格式化设备配置缓存 Key
*/
private static String formatDeviceKey(Long deviceId) {
return String.format(CONFIG_DEVICE_KEY_PATTERN, deviceId);
}
/**
* 格式化区域配置缓存 Key
*/
private static String formatAreaKey(Long areaId) {
return String.format(CONFIG_AREA_KEY_PATTERN, areaId);
}
}