diff --git a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/dataobject/integration/clean/BeaconPresenceConfig.java b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/dataobject/integration/clean/BeaconPresenceConfig.java new file mode 100644 index 0000000..94044d8 --- /dev/null +++ b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/dataobject/integration/clean/BeaconPresenceConfig.java @@ -0,0 +1,165 @@ +package com.viewsh.module.iot.dal.dataobject.integration.clean; + +import lombok.Data; + +/** + * 蓝牙信标检测配置 + *
+ * 用于配置基于工牌蓝牙信标检测自动确认到岗/离岗的规则 + * 采用"强进弱出"双阈值算法,避免信号抖动导致的误判 + * + * @author AI + */ +@Data +public class BeaconPresenceConfig { + + /** + * 是否启用信标检测 + */ + private Boolean enabled; + + /** + * 目标信标 MAC 地址 + *
+ * 例如:F0:C8:60:1D:10:BB + */ + private String beaconMac; + + /** + * ��动窗口配置 + */ + private WindowConfig window; + + /** + * 进入(到岗)判定配置 - 强阈值 + */ + private EnterConfig enter; + + /** + * 退出(离岗)判定配置 - 弱阈值 + */ + private ExitConfig exit; + + /** + * 滑动窗口配置 + */ + @Data + public static class WindowConfig { + + /** + * 样本在 Redis 中的保留时间(秒) + *
+ * 超过此时间的样本将被自动清理,防止 Redis 内存溢出 + */ + private Integer sampleTtlSeconds; + + /** + * 未检测到信标时的默认填充值 + *
+ * 当工牌未检测到目标信标时,使用此值填充窗口 + * 建议设置为 -999(明显低于正常 RSSI 值) + */ + private Integer missingValue; + } + + /** + * 进入(到岗)判定配置 - 强阈值 + *
+ * 使用强阈值(如 -70dBm)可以避免路过误判,只有信号足够强才算到达 + */ + @Data + public static class EnterConfig { + + /** + * RSSI 强阈值(dBm) + *
+ * 只有 RSSI >= 此值才算有效信号 + * 例如:-70 + */ + private Integer rssiThreshold; + + /** + * 滑动窗口大小 + *
+ * 采样窗口中的样本数量 + * 例如:3 表示取最近 3 次采样 + */ + private Integer windowSize; + + /** + * 命中次数 + *
+ * 窗口中满足阈值的次数达到此值时,判定为到达 + * 例如:2 表示 3 次中有 2 次满足即判定到达 + */ + private Integer hitCount; + + /** + * 是否自动触发到岗事件 + *
+ * true = 检测到到达后自动发布 ops.order.arrive 事件 + * false = 仅记录日志,不触发事件 + */ + private Boolean autoArrival; + } + + /** + * 退出(离岗)判定配置 - 弱阈值 + *
+ * 使用弱阈值(如 -85dBm)和迟滞设计,避免边缘抖动 + * 只有信号足够弱或彻底消失才算离开 + */ + @Data + public static class ExitConfig { + + /** + * RSSI 弱阈值(dBm) + *
+ * RSSI < 此值或信号丢失才算离开 + * 例如:-85 + */ + private Integer weakRssiThreshold; + + /** + * 滑动窗口大小 + */ + private Integer windowSize; + + /** + * 命中次数 + *
+ * 窗口中满足弱阈值(丢失)的次数达到此值时,判定为离开 + */ + private Integer hitCount; + + /** + * 确认离开后多久发送警告(分钟) + *
+ * 0 = 立即发送 + */ + private Integer warningDelayMinutes; + + /** + * 持续丢失多久后触发自动完成(分钟) + *
+ * 从首次丢失开始计时,持续丢失超过此时间后自动完成工单 + */ + private Integer lossTimeoutMinutes; + + /** + * 最小有效作业时长(分钟) + *
+ * 用于防止"打卡即走"作弊 + * 如果作业时长(从到岗到离开)小于此值,则抑制自动完成 + */ + private Integer minValidWorkMinutes; + + /** + * 是否自动触发完成事件 + *
+ * true = 检测到离开超时后自动发布 ops.order.complete 事件 + * false = 仅记录日志,不触发事件 + */ + private Boolean autoComplete; + } +} diff --git a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/dataobject/integration/clean/ButtonEventConfig.java b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/dataobject/integration/clean/ButtonEventConfig.java new file mode 100644 index 0000000..04838d2 --- /dev/null +++ b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/dataobject/integration/clean/ButtonEventConfig.java @@ -0,0 +1,33 @@ +package com.viewsh.module.iot.dal.dataobject.integration.clean; + +import lombok.Data; + +/** + * 按键事件配置 + *
+ * 用于配置工牌按键事件的处理规则 + * + * @author AI + */ +@Data +public class ButtonEventConfig { + + /** + * 是否启用按键事件处理 + */ + private Boolean enabled; + + /** + * 确认键 ID + *
+ * 当保洁员按下此按键时,触发工单确认事件 + */ + private Integer confirmKeyId; + + /** + * 查询键 ID + *
+ * 当保洁员按下此按键时,触发工单查询事件 + */ + private Integer queryKeyId; +} diff --git a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/dataobject/integration/clean/CleanOrderIntegrationConfig.java b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/dataobject/integration/clean/CleanOrderIntegrationConfig.java new file mode 100644 index 0000000..741060d --- /dev/null +++ b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/dataobject/integration/clean/CleanOrderIntegrationConfig.java @@ -0,0 +1,36 @@ +package com.viewsh.module.iot.dal.dataobject.integration.clean; + +import lombok.Data; + +/** + * 保洁工单集成配置 + *
+ * 这是 IoT 设备与保洁工单集成的总配置类,包含所有子配置 + * 存储在 {@link OpsAreaDeviceRelationDO#getConfigData()} 中 + * + * @author AI + */ +@Data +public class CleanOrderIntegrationConfig { + + /** + * 客流阈值配置 + *
+ * 用于基于客流计数器自动触发工单创建 + */ + private TrafficThresholdConfig trafficThreshold; + + /** + * 蓝牙信标检测配置 + *
+ * 用于基于工牌蓝牙信标检测自动确认到岗/离岗 + */ + private BeaconPresenceConfig beaconPresence; + + /** + * 按键事件配置 + *
+ * 用于工牌按键事件的处理 + */ + private ButtonEventConfig buttonEvent; +} diff --git a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/dataobject/integration/clean/OpsAreaDeviceRelationDO.java b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/dataobject/integration/clean/OpsAreaDeviceRelationDO.java new file mode 100644 index 0000000..e2490cb --- /dev/null +++ b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/dataobject/integration/clean/OpsAreaDeviceRelationDO.java @@ -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 + *
+ * 用于建立运营区域与 IoT 设备的绑定关系,并存储核心检测配置 + * 注意:此表在 ops 库中创建,但在 IoT 模块中��问(通过 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 + *
+ * 关联 ops_bus_area.id + */ + private Long areaId; + + /** + * IoT设备ID + *
+ * 关联 iot_device.id + */ + private Long deviceId; + + /** + * 设备Key(冗余字段,便于查询) + */ + private String deviceKey; + + /** + * 产品ID + *
+ * 关联 iot_product.id + */ + private Long productId; + + /** + * 产品Key(冗余字段,便于查询) + */ + private String productKey; + + /** + * 关联类型 + *
+ * TRAFFIC_COUNTER - 客流计数器 + * BEACON - 蓝牙信标 + * BADGE - 工牌设备 + */ + private String relationType; + + /** + * 配置数据(JSON格式) + *
+ * 存储保洁工单集成的各类配置 + * 使用 {@link JacksonTypeHandler} 自动序列化/反序列化 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private CleanOrderIntegrationConfig configData; + + /** + * 是否启用 + *
+ * true - 启用 + * false - 禁用 + */ + private Boolean enabled; +} diff --git a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/dataobject/integration/clean/TrafficThresholdConfig.java b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/dataobject/integration/clean/TrafficThresholdConfig.java new file mode 100644 index 0000000..54f56d4 --- /dev/null +++ b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/dataobject/integration/clean/TrafficThresholdConfig.java @@ -0,0 +1,45 @@ +package com.viewsh.module.iot.dal.dataobject.integration.clean; + +import lombok.Data; + +/** + * 客流阈值配置 + *
+ * 用于配置基于客流计数器自动触发工单创建的规则 + * + * @author AI + */ +@Data +public class TrafficThresholdConfig { + + /** + * 触发阈值 + *
+ * 当实际客流(当前值 - 基准值)达到此阈值时,触发工单创建 + */ + private Integer threshold; + + /** + * 统计时间窗口(秒) + *
+ * 在此时间窗口内,同一设备只能触发一次工单创建,防止重复 + */ + private Integer timeWindowSeconds; + + /** + * 是否自动创建工单 + *
+ * true = 自动创建工单 + * false = 仅记录日志,不创建工单 + */ + private Boolean autoCreateOrder; + + /** + * 工单优先级 + *
+ * P0 = 紧急
+ * P1 = 重要
+ * P2 = 普通
+ */
+ private String orderPriority;
+}
diff --git a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/mysql/integration/clean/OpsAreaDeviceRelationMapper.java b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/mysql/integration/clean/OpsAreaDeviceRelationMapper.java
new file mode 100644
index 0000000..1acae0d
--- /dev/null
+++ b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/mysql/integration/clean/OpsAreaDeviceRelationMapper.java
@@ -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
+ * 用于记录保洁员首次进入区域的时间戳
+ * 用于计算作业时长(Duration = 离开时间 - 到达时间)
+ *
+ * @author AI
+ */
+@Repository
+public class BeaconArrivedTimeRedisDAO {
+
+ /**
+ * 到达时间 Key 模式
+ *
+ * 格式:iot:clean:beacon:arrivedAt:{deviceId}:{areaId}
+ */
+ private static final String ARRIVED_AT_KEY_PATTERN = "iot:clean:beacon:arrivedAt:%s:%s";
+
+ /**
+ * 到达时间的 TTL(秒)
+ *
+ * 默认保留 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);
+ }
+}
diff --git a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/redis/clean/BeaconRssiWindowRedisDAO.java b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/redis/clean/BeaconRssiWindowRedisDAO.java
new file mode 100644
index 0000000..22aba3f
--- /dev/null
+++ b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/redis/clean/BeaconRssiWindowRedisDAO.java
@@ -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
+ *
+ * 用于存储工牌检测到的信标 RSSI 值的滑动窗口样本
+ * 支持抗抖动检测(基于滑动窗口的"强进弱出"算法)
+ *
+ * @author AI
+ */
+@Repository
+public class BeaconRssiWindowRedisDAO {
+
+ /**
+ * 滑动窗口 Key 模式
+ *
+ * 格式:iot:clean:beacon:rssi:window:{deviceId}:{areaId}
+ */
+ private static final String WINDOW_KEY_PATTERN = "iot:clean:beacon:rssi:window:%s:%s";
+
+ /**
+ * 窗口样本的 TTL(秒)
+ *
+ * 默认保留 1 小时,防止 Redis 内存溢出
+ */
+ private static final int WINDOW_TTL_SECONDS = 3600;
+
+ @Resource
+ private StringRedisTemplate stringRedisTemplate;
+
+ /**
+ * 更新滑动窗口(保留最近 N 个样本)
+ *
+ * 如果窗口大小超过 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
+ * 用于缓存设备当前执行的工单信息(由 Ops 下发)
+ * 减少物联网模块查询数据库的频率
+ *
+ * @author AI
+ */
+@Repository
+public class DeviceCurrentOrderRedisDAO {
+
+ /**
+ * 工单缓存 Key 模式
+ *
+ * 格式:ops:clean:device:order:{deviceId}
+ */
+ private static final String ORDER_KEY_PATTERN = "ops:clean:device:order:%s";
+
+ /**
+ * 工单缓存的 TTL(秒)
+ *
+ * 默认保留 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
+ *
+ * 用于 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;
+
+ /**
+ * 工单状态
+ *
+ * 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;
+ }
+ }
+}
diff --git a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/redis/clean/SignalLossRedisDAO.java b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/redis/clean/SignalLossRedisDAO.java
new file mode 100644
index 0000000..7afe8c3
--- /dev/null
+++ b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/redis/clean/SignalLossRedisDAO.java
@@ -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
+ *
+ * 用于记录保洁员离岗状态,支持离岗警告防抖、超时判断及无效作业拦截
+ *
+ * @author AI
+ */
+@Repository
+public class SignalLossRedisDAO {
+
+ /**
+ * 信号丢失 Key 模式
+ *
+ * 格式: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(秒)
+ *
+ * 默认保留 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);
+ }
+}
diff --git a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/redis/clean/TrafficCounterBaseRedisDAO.java b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/redis/clean/TrafficCounterBaseRedisDAO.java
new file mode 100644
index 0000000..2cc9a78
--- /dev/null
+++ b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/redis/clean/TrafficCounterBaseRedisDAO.java
@@ -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
+ *
+ * 用于维护客流计数器的基准值,支持逻辑清零和每日自动校准
+ * 实际客流 = 当前计数值 - 基准值
+ *
+ * @author AI
+ */
+@Repository
+public class TrafficCounterBaseRedisDAO {
+
+ /**
+ * 基准值 Key 模式
+ *
+ * 格式:iot:clean:traffic:base:{deviceId}
+ */
+ private static final String BASE_KEY_PATTERN = "iot:clean:traffic:base:%s";
+
+ /**
+ * 基准值的 TTL(秒)
+ *
+ * 默认保留 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);
+ }
+
+ /**
+ * 重置基准值
+ *
+ * 将基准值设置为 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);
+ }
+}
diff --git a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/integration/clean/CleanOrderIntegrationConfigService.java b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/integration/clean/CleanOrderIntegrationConfigService.java
new file mode 100644
index 0000000..4f58291
--- /dev/null
+++ b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/integration/clean/CleanOrderIntegrationConfigService.java
@@ -0,0 +1,175 @@
+package com.viewsh.module.iot.service.integration.clean;
+
+import com.viewsh.module.iot.dal.dataobject.integration.clean.CleanOrderIntegrationConfig;
+
+/**
+ * 保洁工单集成配置 Service
+ *
+ * 提供运营区域设备关联配置的查询和管理能力
+ * 支持本地 Redis 缓存,减少数据库查询压力
+ *
+ * @author AI
+ */
+public interface CleanOrderIntegrationConfigService {
+
+ /**
+ * 根据设备ID查询配置(带缓存)
+ *
+ * 优先从 Redis 缓存读取,缓存未命中时从数据库查询并写入缓存
+ * 缓存 TTL: 5 分钟
+ *
+ * @param deviceId 设备ID
+ * @return 集成配置,如果不存在或未启用返回 null
+ */
+ CleanOrderIntegrationConfig getConfigByDeviceId(Long deviceId);
+
+ /**
+ * 根据区域ID查询所有启用的配置(带缓存)
+ *
+ * 优先从 Redis 缓存读取
+ *
+ * @param areaId 区域ID
+ * @return 集成配置列表
+ */
+ java.util.List
+ * 例如:查询某个区域的所有客流计数器配置
+ *
+ * @param areaId 区域ID
+ * @param relationType 关联类型(TRAFFIC_COUNTER/BEACON/BADGE)
+ * @return 集成配置列表
+ */
+ java.util.List
+ * 配置更新后调用此方法清除缓存,确保下次查询能获取最新配置
+ *
+ * @param deviceId 设备ID
+ */
+ void evictCache(Long deviceId);
+
+ /**
+ * 清除区域配置缓存
+ *
+ * @param areaId 区域ID
+ */
+ void evictAreaCache(Long areaId);
+
+ /**
+ * 区域设备配置包装类
+ *
+ * 包含配置数据和关联的基础信息(设备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;
+ }
+ }
+}
diff --git a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/integration/clean/CleanOrderIntegrationConfigServiceImpl.java b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/integration/clean/CleanOrderIntegrationConfigServiceImpl.java
new file mode 100644
index 0000000..2d96da9
--- /dev/null
+++ b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/integration/clean/CleanOrderIntegrationConfigServiceImpl.java
@@ -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(秒)
+ *
+ * 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