diff --git a/wm-iot/pom.xml b/wm-iot/pom.xml
index 6ebe218a..81769730 100644
--- a/wm-iot/pom.xml
+++ b/wm-iot/pom.xml
@@ -13,5 +13,10 @@
org.springframework.bootspring-boot-starter-data-redis
org.postgresqlpostgresql
net.postgispostgis-jdbc
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
\ No newline at end of file
diff --git a/wm-iot/src/main/java/com/water/iot/config/RedisConfig.java b/wm-iot/src/main/java/com/water/iot/config/RedisConfig.java
new file mode 100644
index 00000000..0b25cde6
--- /dev/null
+++ b/wm-iot/src/main/java/com/water/iot/config/RedisConfig.java
@@ -0,0 +1,59 @@
+package com.water.iot.config;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+/**
+ * Redis配置
+ * - StringRedisTemplate: key/value均为String序列化(用于设备影子Hash存储)
+ * - RedisTemplate: key为String,value为JSON序列化(通用)
+ */
+@Configuration
+public class RedisConfig {
+
+ @Bean
+ public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {
+ StringRedisTemplate template = new StringRedisTemplate();
+ template.setConnectionFactory(factory);
+ return template;
+ }
+
+ @Bean
+ public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
+ RedisTemplate template = new RedisTemplate<>();
+ template.setConnectionFactory(factory);
+
+ // JSON序列化器
+ ObjectMapper mapper = new ObjectMapper();
+ mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
+ mapper.activateDefaultTyping(
+ mapper.getPolymorphicTypeValidator(),
+ ObjectMapper.DefaultTyping.NON_FINAL
+ );
+ mapper.registerModule(new JavaTimeModule());
+ mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
+
+ GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer(mapper);
+ StringRedisSerializer stringSerializer = new StringRedisSerializer();
+
+ // key序列化
+ template.setKeySerializer(stringSerializer);
+ template.setHashKeySerializer(stringSerializer);
+ // value序列化
+ template.setValueSerializer(jsonSerializer);
+ template.setHashValueSerializer(jsonSerializer);
+
+ template.afterPropertiesSet();
+ return template;
+ }
+}
diff --git a/wm-iot/src/main/java/com/water/iot/controller/DeviceShadowController.java b/wm-iot/src/main/java/com/water/iot/controller/DeviceShadowController.java
new file mode 100644
index 00000000..6886e24d
--- /dev/null
+++ b/wm-iot/src/main/java/com/water/iot/controller/DeviceShadowController.java
@@ -0,0 +1,107 @@
+package com.water.iot.controller;
+
+import com.water.common.core.result.R;
+import com.water.iot.entity.DeviceShadow;
+import com.water.iot.service.DeviceShadowService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+@Tag(name = "设备影子")
+@RestController
+@RequestMapping("/api/iot/shadow")
+@RequiredArgsConstructor
+public class DeviceShadowController {
+
+ private final DeviceShadowService shadowService;
+
+ @Operation(summary = "获取设备影子")
+ @GetMapping("/{deviceSn}")
+ public R getShadow(@PathVariable String deviceSn) {
+ DeviceShadow shadow = shadowService.getShadow(deviceSn);
+ if (shadow == null) {
+ return R.fail(404, "设备影子不存在: " + deviceSn);
+ }
+ return R.ok(shadow);
+ }
+
+ @Operation(summary = "更新设备上报状态")
+ @PostMapping("/{deviceSn}/reported")
+ public R updateReported(@PathVariable String deviceSn,
+ @RequestBody Map state) {
+ shadowService.updateReported(deviceSn, state);
+ return R.ok("上报状态已更新");
+ }
+
+ @Operation(summary = "更新期望状态(云端下发)")
+ @PostMapping("/{deviceSn}/desired")
+ public R updateDesired(@PathVariable String deviceSn,
+ @RequestBody Map desired) {
+ try {
+ com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
+ String json = mapper.writeValueAsString(desired);
+ shadowService.updateDesired(deviceSn, json);
+ return R.ok("期望状态已下发");
+ } catch (Exception e) {
+ return R.fail("序列化失败: " + e.getMessage());
+ }
+ }
+
+ @Operation(summary = "获取上报状态")
+ @GetMapping("/{deviceSn}/reported")
+ public R getReportedState(@PathVariable String deviceSn) {
+ String state = shadowService.getReportedState(deviceSn);
+ return R.ok(state);
+ }
+
+ @Operation(summary = "获取期望状态")
+ @GetMapping("/{deviceSn}/desired")
+ public R getDesiredState(@PathVariable String deviceSn) {
+ String state = shadowService.getDesiredState(deviceSn);
+ return R.ok(state);
+ }
+
+ @Operation(summary = "获取差异状态(delta)")
+ @GetMapping("/{deviceSn}/delta")
+ public R getDeltaState(@PathVariable String deviceSn) {
+ String state = shadowService.getDeltaState(deviceSn);
+ return R.ok(state);
+ }
+
+ @Operation(summary = "批量查询设备影子")
+ @PostMapping("/batch")
+ public R> batchGetShadows(@RequestBody List deviceSns) {
+ return R.ok(shadowService.batchGetShadows(deviceSns));
+ }
+
+ @Operation(summary = "检查设备在线状态")
+ @GetMapping("/{deviceSn}/online")
+ public R checkOnline(@PathVariable String deviceSn,
+ @RequestParam(defaultValue = "30") int thresholdMinutes) {
+ return R.ok(shadowService.checkOnline(deviceSn, thresholdMinutes));
+ }
+
+ @Operation(summary = "批量检测在线状态")
+ @PostMapping("/online/batch")
+ public R