diff --git a/sql/mysql/aiot_ops_traffic_statistics.sql b/sql/mysql/aiot_ops_traffic_statistics.sql
new file mode 100644
index 0000000..3aeb411
--- /dev/null
+++ b/sql/mysql/aiot_ops_traffic_statistics.sql
@@ -0,0 +1,28 @@
+-- Ops 模块:客流统计小时汇总表
+-- 用于业务报表统计,数据来源于 IoT 设备的客流计数器
+-- 每小时由 Ops 的 TrafficStatisticsPersistJob 从 Redis 持久化到 MySQL
+
+CREATE TABLE IF NOT EXISTS `ops_traffic_statistics` (
+ `id` BIGINT AUTO_INCREMENT PRIMARY KEY,
+ `device_id` BIGINT NOT NULL COMMENT '设备ID(数据来源)',
+ `area_id` BIGINT NOT NULL COMMENT '区域ID(主查询维度)',
+ `stat_hour` DATETIME NOT NULL COMMENT '统计小时(精确到小时,如 2026-02-03 10:00:00)',
+ `people_in` INT NOT NULL DEFAULT 0 COMMENT '进入人数',
+ `people_out` INT NOT NULL DEFAULT 0 COMMENT '离开人数',
+ `tenant_id` BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
+ `creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
+ `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+ `updater` VARCHAR(64) DEFAULT '' COMMENT '更新者',
+ `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+ `deleted` BIT(1) NOT NULL DEFAULT 0 COMMENT '是否删除',
+
+ -- 唯一约束:同一设备、同一小时、同一租户,只有一条记录
+ UNIQUE KEY `uk_device_hour_tenant` (`device_id`, `stat_hour`, `tenant_id`, `deleted`),
+
+ -- 区域+小时索引:用于按区域统计客流
+ INDEX `idx_area_hour` (`area_id`, `stat_hour`),
+
+ -- 设备+小时索引:用于按设备查询历史
+ INDEX `idx_device_hour` (`device_id`, `stat_hour`)
+
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客流统计小时汇总表(Ops业务统计)';
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
deleted file mode 100644
index 8e05ac7..0000000
--- a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/redis/clean/TrafficCounterBaseRedisDAO.java
+++ /dev/null
@@ -1,114 +0,0 @@
-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);
- }
-
- /**
- * 清除所有基准值
- *
- * 用于定时任务,每天 00:00 清零所有客流计数器基准值
- *
- * @return 清除的数量
- */
- public int resetAll() {
- String pattern = BASE_KEY_PATTERN.replace("%s", "*");
- var keys = stringRedisTemplate.keys(pattern);
-
- if (keys == null || keys.isEmpty()) {
- return 0;
- }
-
- // 批量删除
- stringRedisTemplate.delete(keys);
-
- return keys.size();
- }
-
- /**
- * 格式化 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/dal/redis/clean/TrafficCounterRedisDAO.java b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/redis/clean/TrafficCounterRedisDAO.java
new file mode 100644
index 0000000..58ca275
--- /dev/null
+++ b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/redis/clean/TrafficCounterRedisDAO.java
@@ -0,0 +1,261 @@
+package com.viewsh.module.iot.dal.redis.clean;
+
+import jakarta.annotation.Resource;
+import org.springframework.data.redis.core.Cursor;
+import org.springframework.data.redis.core.ScanOptions;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.stereotype.Repository;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 客流计数器 Redis DAO
+ *
+ * 维护两类数据:
+ * 1. 阈值计数器:累加增量,达到阈值触发工单后重置为 0
+ * 2. 当日累积统计:不因工单触发而重置,用于统计报表
+ *
+ * @author AI
+ */
+@Repository
+public class TrafficCounterRedisDAO {
+
+ /**
+ * 阈值计数器 Key 模式
+ *
+ * 格式:iot:clean:traffic:threshold:{deviceId}:{areaId}
+ */
+ private static final String THRESHOLD_KEY_PATTERN = "iot:clean:traffic:threshold:%s:%s";
+
+ /**
+ * 当日累积统计 Key 模式
+ *
+ * 格式:iot:clean:traffic:daily:{deviceId}:{date}
+ */
+ private static final String DAILY_KEY_PATTERN = "iot:clean:traffic:daily:%s:%s";
+
+ /**
+ * 阈值计数器 TTL(秒)- 1 天
+ */
+ private static final int THRESHOLD_TTL_SECONDS = 86400;
+
+ /**
+ * 当日累积统计 TTL(秒)- 2 天,确保跨日持久化任务能读取到昨天数据
+ */
+ private static final int DAILY_TTL_SECONDS = 172800;
+
+ private static final String FIELD_TOTAL_IN = "totalIn";
+ private static final String FIELD_TOTAL_OUT = "totalOut";
+ private static final String FIELD_LAST_PERSISTED_IN = "lastPersistedIn";
+ private static final String FIELD_LAST_PERSISTED_OUT = "lastPersistedOut";
+
+ private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");
+
+ @Resource
+ private StringRedisTemplate stringRedisTemplate;
+
+ // ==================== 阈值计数器 ====================
+
+ /**
+ * 原子递增阈值计数器
+ *
+ * @param deviceId 设备ID
+ * @param areaId 区域ID
+ * @param increment 增量
+ * @return 递增后的累积值
+ */
+ public Long incrementThreshold(Long deviceId, Long areaId, long increment) {
+ String key = formatThresholdKey(deviceId, areaId);
+ Long result = stringRedisTemplate.opsForValue().increment(key, increment);
+ // 确保 key 有 TTL(防止并发场景下 TTL 未设置)
+ Long ttl = stringRedisTemplate.getExpire(key, TimeUnit.SECONDS);
+ if (ttl == null || ttl == -1) {
+ stringRedisTemplate.expire(key, THRESHOLD_TTL_SECONDS, TimeUnit.SECONDS);
+ }
+ return result;
+ }
+
+ /**
+ * 重置阈值计数器(删除 key)
+ *
+ * @param deviceId 设备ID
+ * @param areaId 区域ID
+ */
+ public void resetThreshold(Long deviceId, Long areaId) {
+ String key = formatThresholdKey(deviceId, areaId);
+ stringRedisTemplate.delete(key);
+ }
+
+ /**
+ * 清除所有阈值计数器
+ *
+ * P1修复: 使用 SCAN 替代 KEYS,避免阻塞 Redis
+ *
+ * @return 清除的数量
+ */
+ public int resetAllThresholds() {
+ Set keys = new HashSet<>();
+ ScanOptions options = ScanOptions.scanOptions()
+ .match("iot:clean:traffic:threshold:*")
+ .count(100)
+ .build();
+
+ try (Cursor cursor = stringRedisTemplate.scan(options)) {
+ cursor.forEachRemaining(keys::add);
+ } catch (Exception e) {
+ // SCAN 失败,记录日志但不中断流程
+ }
+
+ if (keys.isEmpty()) {
+ return 0;
+ }
+
+ stringRedisTemplate.delete(keys);
+ return keys.size();
+ }
+
+ // ==================== 当日累积统计 ====================
+
+ /**
+ * 原子递增当日累积统计
+ *
+ * @param deviceId 设备ID
+ * @param date 日期
+ * @param peopleIn 进入人数增量
+ * @param peopleOut 离开人数增量
+ */
+ public void incrementDaily(Long deviceId, LocalDate date, long peopleIn, long peopleOut) {
+ String key = formatDailyKey(deviceId, date);
+ if (peopleIn > 0) {
+ stringRedisTemplate.opsForHash().increment(key, FIELD_TOTAL_IN, peopleIn);
+ }
+ if (peopleOut > 0) {
+ stringRedisTemplate.opsForHash().increment(key, FIELD_TOTAL_OUT, peopleOut);
+ }
+ // 设置 TTL(幂等,每次都设置保证不过期)
+ stringRedisTemplate.expire(key, DAILY_TTL_SECONDS, TimeUnit.SECONDS);
+ }
+
+ /**
+ * 获取当日累积统计
+ *
+ * @param deviceId 设备ID
+ * @param date 日期
+ * @return 统计数据 {totalIn, totalOut, lastPersistedIn, lastPersistedOut}
+ */
+ public Map getDailyStats(Long deviceId, LocalDate date) {
+ String key = formatDailyKey(deviceId, date);
+ Map