diff --git a/viewsh-module-iot/viewsh-module-iot-api/src/main/java/com/viewsh/module/iot/api/trajectory/DeviceLocationDTO.java b/viewsh-module-iot/viewsh-module-iot-api/src/main/java/com/viewsh/module/iot/api/trajectory/DeviceLocationDTO.java
new file mode 100644
index 0000000..14ff1eb
--- /dev/null
+++ b/viewsh-module-iot/viewsh-module-iot-api/src/main/java/com/viewsh/module/iot/api/trajectory/DeviceLocationDTO.java
@@ -0,0 +1,36 @@
+package com.viewsh.module.iot.api.trajectory;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 设备实时位置 DTO
+ *
+ * @author lzh
+ */
+@Schema(description = "RPC - 设备实时位置")
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class DeviceLocationDTO {
+
+ @Schema(description = "设备ID", example = "31")
+ private Long deviceId;
+
+ @Schema(description = "当前所在区域ID", example = "1301")
+ private Long areaId;
+
+ @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;
+
+}
diff --git a/viewsh-module-iot/viewsh-module-iot-api/src/main/java/com/viewsh/module/iot/api/trajectory/TrajectoryStateApi.java b/viewsh-module-iot/viewsh-module-iot-api/src/main/java/com/viewsh/module/iot/api/trajectory/TrajectoryStateApi.java
new file mode 100644
index 0000000..40933e8
--- /dev/null
+++ b/viewsh-module-iot/viewsh-module-iot-api/src/main/java/com/viewsh/module/iot/api/trajectory/TrajectoryStateApi.java
@@ -0,0 +1,31 @@
+package com.viewsh.module.iot.api.trajectory;
+
+import com.viewsh.framework.common.pojo.CommonResult;
+import com.viewsh.module.iot.enums.ApiConstants;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+
+/**
+ * 轨迹实时状态 API
+ *
+ * 提供 RPC 接口供 Ops 模块查询设备当前位置
+ *
+ * @author lzh
+ */
+@FeignClient(name = ApiConstants.NAME)
+@Tag(name = "RPC 服务 - 轨迹实时状态")
+public interface TrajectoryStateApi {
+
+ String PREFIX = ApiConstants.PREFIX + "/trajectory";
+
+ @GetMapping(PREFIX + "/current-location")
+ @Operation(summary = "查询设备当前位置")
+ CommonResult getCurrentLocation(
+ @Parameter(description = "设备ID", required = true, example = "31")
+ @RequestParam("deviceId") Long deviceId);
+
+}
diff --git a/viewsh-module-iot/viewsh-module-iot-core/src/main/java/com/viewsh/module/iot/core/integration/constants/TrajectoryTopics.java b/viewsh-module-iot/viewsh-module-iot-core/src/main/java/com/viewsh/module/iot/core/integration/constants/TrajectoryTopics.java
new file mode 100644
index 0000000..2b849bb
--- /dev/null
+++ b/viewsh-module-iot/viewsh-module-iot-core/src/main/java/com/viewsh/module/iot/core/integration/constants/TrajectoryTopics.java
@@ -0,0 +1,22 @@
+package com.viewsh.module.iot.core.integration.constants;
+
+/**
+ * 轨迹事件 Topic 常量
+ *
+ * 定义 IoT → Ops 的轨迹事件 Topic
+ *
+ * @author lzh
+ */
+public interface TrajectoryTopics {
+
+ /**
+ * 进入区域事件
+ */
+ String TRAJECTORY_ENTER = "trajectory-enter";
+
+ /**
+ * 离开区域事件
+ */
+ String TRAJECTORY_LEAVE = "trajectory-leave";
+
+}
diff --git a/viewsh-module-iot/viewsh-module-iot-core/src/main/java/com/viewsh/module/iot/core/integration/event/trajectory/TrajectoryEnterEvent.java b/viewsh-module-iot/viewsh-module-iot-core/src/main/java/com/viewsh/module/iot/core/integration/event/trajectory/TrajectoryEnterEvent.java
new file mode 100644
index 0000000..228baea
--- /dev/null
+++ b/viewsh-module-iot/viewsh-module-iot-core/src/main/java/com/viewsh/module/iot/core/integration/event/trajectory/TrajectoryEnterEvent.java
@@ -0,0 +1,72 @@
+package com.viewsh.module.iot.core.integration.event.trajectory;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+/**
+ * 轨迹进入区域事件
+ *
+ * 当工牌蓝牙信号满足强进条件确认进入某区域时发布
+ * Topic: trajectory-enter
+ *
+ * @author lzh
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class TrajectoryEnterEvent {
+
+ /**
+ * 事件ID(唯一标识,用于幂等性处理)
+ */
+ @Builder.Default
+ private String eventId = UUID.randomUUID().toString();
+
+ /**
+ * 设备ID(工牌)
+ */
+ private Long deviceId;
+
+ /**
+ * 设备名称
+ */
+ private String deviceName;
+
+ /**
+ * 设备备注名称
+ */
+ private String nickname;
+
+ /**
+ * 区域ID
+ */
+ private Long areaId;
+
+ /**
+ * 匹配的 Beacon MAC 地址
+ */
+ private String beaconMac;
+
+ /**
+ * 进入时的 RSSI 值
+ */
+ private Integer enterRssi;
+
+ /**
+ * 事件时间(ISO-8601 字符串,跨模块序列化安全)
+ */
+ @Builder.Default
+ private String eventTime = LocalDateTime.now().toString();
+
+ /**
+ * 租户ID
+ */
+ private Long tenantId;
+
+}
diff --git a/viewsh-module-iot/viewsh-module-iot-core/src/main/java/com/viewsh/module/iot/core/integration/event/trajectory/TrajectoryLeaveEvent.java b/viewsh-module-iot/viewsh-module-iot-core/src/main/java/com/viewsh/module/iot/core/integration/event/trajectory/TrajectoryLeaveEvent.java
new file mode 100644
index 0000000..e501561
--- /dev/null
+++ b/viewsh-module-iot/viewsh-module-iot-core/src/main/java/com/viewsh/module/iot/core/integration/event/trajectory/TrajectoryLeaveEvent.java
@@ -0,0 +1,81 @@
+package com.viewsh.module.iot.core.integration.event.trajectory;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+/**
+ * 轨迹离开区域事件
+ *
+ * 当工牌蓝牙信号满足弱出条件确认离开某区域时发布
+ * Topic: trajectory-leave
+ *
+ * @author lzh
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class TrajectoryLeaveEvent {
+
+ /**
+ * 事件ID(唯一标识,用于幂等性处理)
+ */
+ @Builder.Default
+ private String eventId = UUID.randomUUID().toString();
+
+ /**
+ * 设备ID(工牌)
+ */
+ private Long deviceId;
+
+ /**
+ * 设备名称
+ */
+ private String deviceName;
+
+ /**
+ * 设备备注名称
+ */
+ private String nickname;
+
+ /**
+ * 区域ID
+ */
+ private Long areaId;
+
+ /**
+ * 匹配的 Beacon MAC 地址
+ */
+ private String beaconMac;
+
+ /**
+ * 离开原因
+ *
+ * SIGNAL_LOSS - 信号丢失(弱出条件满足)
+ * AREA_SWITCH - 切换到其他区域
+ * DEVICE_OFFLINE - 设备离线
+ */
+ private String leaveReason;
+
+ /**
+ * 进入时间戳(毫秒),用于 Ops 端匹配记录
+ */
+ private Long enterTimestamp;
+
+ /**
+ * 事件时间(ISO-8601 字符串,跨模块序列化安全)
+ */
+ @Builder.Default
+ private String eventTime = LocalDateTime.now().toString();
+
+ /**
+ * 租户ID
+ */
+ private Long tenantId;
+
+}
diff --git a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/controller/rpc/TrajectoryStateController.java b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/controller/rpc/TrajectoryStateController.java
new file mode 100644
index 0000000..9d4744f
--- /dev/null
+++ b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/controller/rpc/TrajectoryStateController.java
@@ -0,0 +1,46 @@
+package com.viewsh.module.iot.controller.rpc;
+
+import com.viewsh.framework.common.pojo.CommonResult;
+import com.viewsh.module.iot.api.trajectory.DeviceLocationDTO;
+import com.viewsh.module.iot.api.trajectory.TrajectoryStateApi;
+import com.viewsh.module.iot.dal.redis.clean.TrajectoryStateRedisDAO;
+import jakarta.annotation.Resource;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.RestController;
+
+import static com.viewsh.framework.common.pojo.CommonResult.success;
+
+/**
+ * 轨迹实时状态 RPC Controller
+ *
+ * 实现 {@link TrajectoryStateApi},从 Redis 读取设备当前位置
+ *
+ * @author lzh
+ */
+@RestController
+@Validated
+public class TrajectoryStateController implements TrajectoryStateApi {
+
+ @Resource
+ private TrajectoryStateRedisDAO trajectoryStateRedisDAO;
+
+ @Override
+ public CommonResult getCurrentLocation(Long deviceId) {
+ TrajectoryStateRedisDAO.CurrentAreaInfo currentArea = trajectoryStateRedisDAO.getCurrentArea(deviceId);
+
+ if (currentArea == null) {
+ return success(DeviceLocationDTO.builder()
+ .deviceId(deviceId)
+ .inArea(false)
+ .build());
+ }
+
+ return success(DeviceLocationDTO.builder()
+ .deviceId(deviceId)
+ .areaId(currentArea.getAreaId())
+ .enterTime(currentArea.getEnterTime())
+ .beaconMac(currentArea.getBeaconMac())
+ .inArea(true)
+ .build());
+ }
+}
diff --git a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/dataobject/integration/clean/TrajectoryTrackingConfig.java b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/dataobject/integration/clean/TrajectoryTrackingConfig.java
new file mode 100644
index 0000000..8df8c6a
--- /dev/null
+++ b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/dataobject/integration/clean/TrajectoryTrackingConfig.java
@@ -0,0 +1,26 @@
+package com.viewsh.module.iot.dal.dataobject.integration.clean;
+
+import lombok.Data;
+
+/**
+ * 轨迹记录功能配置
+ *
+ * 存储在 iot_device.config JSON 字段中,与 ButtonEventConfig 同级
+ *
+ * {
+ * "buttonEvent": { ... },
+ * "trajectoryTracking": { "enabled": true }
+ * }
+ *
+ *
+ * @author lzh
+ */
+@Data
+public class TrajectoryTrackingConfig {
+
+ /**
+ * 是否启用轨迹记录
+ */
+ private Boolean enabled;
+
+}
diff --git a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/redis/clean/TrajectoryRssiWindowRedisDAO.java b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/redis/clean/TrajectoryRssiWindowRedisDAO.java
new file mode 100644
index 0000000..518a67c
--- /dev/null
+++ b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/redis/clean/TrajectoryRssiWindowRedisDAO.java
@@ -0,0 +1,71 @@
+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
+ *
+ * 与 BeaconRssiWindowRedisDAO 功能相同,使用独立的 Key 前缀,
+ * 避免与保洁到岗检测的窗口数据互相干扰
+ *
+ * @author lzh
+ */
+@Repository
+public class TrajectoryRssiWindowRedisDAO {
+
+ private static final String WINDOW_KEY_PATTERN = "iot:trajectory:rssi:window:%s:%s";
+ private static final int WINDOW_TTL_SECONDS = 3600;
+
+ @Resource
+ private StringRedisTemplate stringRedisTemplate;
+
+ /**
+ * 更新滑动窗口(保留最近 N 个样本)
+ */
+ public void updateWindow(Long deviceId, Long areaId, Integer rssi, Integer maxSize) {
+ String key = formatKey(deviceId, areaId);
+ List 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(","));
+ stringRedisTemplate.opsForValue().set(key, windowStr, WINDOW_TTL_SECONDS, TimeUnit.SECONDS);
+ }
+
+ /**
+ * 获取窗口样本
+ */
+ public List 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());
+ }
+
+ /**
+ * 清除窗口
+ */
+ public void clearWindow(Long deviceId, Long areaId) {
+ String key = formatKey(deviceId, areaId);
+ stringRedisTemplate.delete(key);
+ }
+
+ private static String formatKey(Long deviceId, Long areaId) {
+ return String.format(WINDOW_KEY_PATTERN, deviceId, areaId);
+ }
+}
diff --git a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/redis/clean/TrajectoryStateRedisDAO.java b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/redis/clean/TrajectoryStateRedisDAO.java
new file mode 100644
index 0000000..c38ec37
--- /dev/null
+++ b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/redis/clean/TrajectoryStateRedisDAO.java
@@ -0,0 +1,109 @@
+package com.viewsh.module.iot.dal.redis.clean;
+
+import jakarta.annotation.Resource;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.stereotype.Repository;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 轨迹实时状态 Redis DAO
+ *
+ * 维护每个设备当前所在区域的实时状态
+ *
+ * Key: iot:trajectory:current:{deviceId}
+ * Type: Hash
+ * Fields: areaId, enterTime, beaconMac
+ * TTL: 24小时
+ *
+ * @author lzh
+ */
+@Slf4j
+@Repository
+public class TrajectoryStateRedisDAO {
+
+ private static final String CURRENT_KEY_PATTERN = "iot:trajectory:current:%s";
+ private static final int CURRENT_TTL_SECONDS = 86400; // 24小时
+
+ private static final String FIELD_AREA_ID = "areaId";
+ private static final String FIELD_ENTER_TIME = "enterTime";
+ private static final String FIELD_BEACON_MAC = "beaconMac";
+
+ @Resource
+ private StringRedisTemplate stringRedisTemplate;
+
+ /**
+ * 设置当前所在区域
+ */
+ public void setCurrentArea(Long deviceId, Long areaId, long enterTime, String beaconMac) {
+ String key = formatKey(deviceId);
+ // 先删后写,清除旧字段(如上次的 beaconMac),保证一致性
+ stringRedisTemplate.delete(key);
+ Map fields = new HashMap<>();
+ fields.put(FIELD_AREA_ID, String.valueOf(areaId));
+ fields.put(FIELD_ENTER_TIME, String.valueOf(enterTime));
+ if (beaconMac != null) {
+ fields.put(FIELD_BEACON_MAC, beaconMac);
+ }
+ stringRedisTemplate.opsForHash().putAll(key, fields);
+ stringRedisTemplate.expire(key, CURRENT_TTL_SECONDS, TimeUnit.SECONDS);
+ }
+
+ /**
+ * 获取当前所在区域信息
+ *
+ * @return 当前区域信息,如果不在任何区域则返回 null
+ */
+ public CurrentAreaInfo getCurrentArea(Long deviceId) {
+ String key = formatKey(deviceId);
+ Map