skyline 2 mesiacov pred
rodič
commit
52d553dcfb

+ 1 - 0
haha-admin/src/main/resources/sql/device.sql

@@ -6,6 +6,7 @@ CREATE TABLE IF NOT EXISTS `t_device` (
   `name` VARCHAR(128) DEFAULT NULL COMMENT '设备名称',
   `auth_token` VARCHAR(256) DEFAULT NULL COMMENT '认证令牌',
   `status` INT DEFAULT 1 COMMENT '状态:0-禁用,1-正常',
+  `door_status` VARCHAR(10) DEFAULT NULL COMMENT '门状态:1-开门失败(ERROR),2-门已开(OPENED),3-门已关(CLOSED),4-设备繁忙(BUSY)',
   `current_inventory_hash` VARCHAR(64) DEFAULT NULL COMMENT '当前库存哈希值',
   `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
   `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',

+ 5 - 0
haha-entity/src/main/java/com/haha/entity/Device.java

@@ -28,6 +28,11 @@ public class Device implements Serializable {
 
     private String currentInventoryHash;
 
+    /**
+     * 门状态:1-开门失败(ERROR),2-门已开(OPENED),3-门已关(CLOSED),4-设备繁忙(BUSY)
+     */
+    private String doorStatus;
+
     private LocalDateTime createTime;
 
     private LocalDateTime updateTime;

+ 31 - 0
haha-mapper/src/main/java/com/haha/mapper/DeviceMapper.java

@@ -5,6 +5,9 @@ import com.haha.entity.Device;
 import org.apache.ibatis.annotations.Mapper;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
+import org.apache.ibatis.annotations.Update;
+
+import java.util.List;
 
 @Mapper
 public interface DeviceMapper extends BaseMapper<Device> {
@@ -14,4 +17,32 @@ public interface DeviceMapper extends BaseMapper<Device> {
      */
     @Select("SELECT * FROM t_device WHERE device_id = #{deviceId}")
     Device selectByDeviceId(@Param("deviceId") String deviceId);
+    
+    /**
+     * 根据门状态查询设备列表
+     */
+    @Select("SELECT * FROM t_device WHERE door_status = #{doorStatus}")
+    List<Device> selectByDoorStatus(@Param("doorStatus") String doorStatus);
+    
+    /**
+     * 更新设备门状态
+     */
+    @Update("UPDATE t_device SET door_status = #{doorStatus} WHERE device_id = #{deviceId}")
+    int updateDoorStatus(@Param("deviceId") String deviceId, @Param("doorStatus") String doorStatus);
+    
+    /**
+     * 批量更新设备门状态
+     */
+    @Update("<script>" +
+            "UPDATE t_device SET door_status = CASE device_id " +
+            "<foreach collection='deviceStatusMap' item='status' index='deviceId'>" +
+            "WHEN #{deviceId} THEN #{status} " +
+            "</foreach>" +
+            "END " +
+            "WHERE device_id IN " +
+            "<foreach collection='deviceStatusMap.keySet()' item='deviceId' open='(' separator=',' close=')'>" +
+            "#{deviceId}" +
+            "</foreach>" +
+            "</script>")
+    int batchUpdateDoorStatus(@Param("deviceStatusMap") java.util.Map<String, String> deviceStatusMap);
 }

+ 65 - 10
haha-miniapp/src/main/java/com/haha/miniapp/config/SaTokenConfig.java

@@ -16,6 +16,7 @@ public class SaTokenConfig implements WebMvcConfigurer {
 
     /**
      * 注册 Sa-Token 全局过滤器,用于读取小程序的 access_token
+     * 支持多种token传递方式,提高兼容性
      */
     @Bean
     public SaServletFilter getSaServletFilter() {
@@ -23,33 +24,87 @@ public class SaTokenConfig implements WebMvcConfigurer {
             .addInclude("/**")
             .addExclude("/login/**", "/health/**", "/callback/**")
             .setAuth(obj -> {
-                // 从小程序的 access_token 请求头中获取token
-                String accessToken = SaHolder.getRequest().getHeader("access_token");
-                if (accessToken != null && !accessToken.isEmpty()) {
-                    // 将 access_token 设置为当前上下文的token
-                    StpUtil.setTokenValue(accessToken);
-                    log.debug("从小程序请求头中读取到token: {}", accessToken);
+                // 多种方式获取token,提高兼容性
+                String tokenValue = null;
+                
+                // 方式1: 从 access_token 请求头获取(小程序标准方式)
+                tokenValue = SaHolder.getRequest().getHeader("access_token");
+                if (isValidToken(tokenValue)) {
+                    log.info("[Sa-Token] 从 access_token 头读取到token: {}", maskToken(tokenValue));
+                }
+                
+                // 方式2: 从 Authorization 头获取(标准Bearer方式)
+                if (!isValidToken(tokenValue)) {
+                    String authHeader = SaHolder.getRequest().getHeader("Authorization");
+                    if (authHeader != null && authHeader.startsWith("Bearer ")) {
+                        tokenValue = authHeader.substring(7);
+                        if (isValidToken(tokenValue)) {
+                            log.info("[Sa-Token] 从 Authorization 头读取到token: {}", maskToken(tokenValue));
+                        }
+                    }
+                }
+                
+                // 方式3: 从请求参数获取(备用方式)
+                if (!isValidToken(tokenValue)) {
+                    // 注意:SaRequest 可能不直接支持 getParameter,这里先注释掉
+                    // tokenValue = SaHolder.getRequest().getParameter("access_token");
+                    // if (isValidToken(tokenValue)) {
+                    //     log.info("[Sa-Token] 从请求参数读取到token: {}", maskToken(tokenValue));
+                    // }
+                }
+                
+                // 如果获取到有效token,设置到上下文
+                if (isValidToken(tokenValue)) {
+                    StpUtil.setTokenValue(tokenValue);
+                } else {
+                    log.info("[Sa-Token] 未找到有效的token");
                 }
                 
                 // 执行登录校验
                 StpUtil.checkLogin();
             })
             .setError(e -> {
-                log.warn("Sa-Token认证失败: {}", e.getMessage());
+                log.warn("[Sa-Token] 认证失败: {}", e.getMessage());
                 return e;
             });
     }
+    
+    /**
+     * 验证token是否有效
+     */
+    private boolean isValidToken(String token) {
+        return token != null && !token.trim().isEmpty() && !"null".equals(token);
+    }
+    
+    /**
+     * 遮蔽token敏感信息,只显示前后几位
+     */
+    private String maskToken(String token) {
+        if (token == null || token.length() <= 8) {
+            return "***";
+        }
+        return token.substring(0, 4) + "****" + token.substring(token.length() - 4);
+    }
 
     @Override
     public void addInterceptors(InterceptorRegistry registry) {
         // 注册Sa-Token拦截器,拦截所有路径
         registry.addInterceptor(new SaInterceptor(handle -> {
-            // 记录token信息(用于调试)
+            // 记录详细的token信息(生产环境也启用INFO级别日志
             String tokenValue = StpUtil.getTokenValue();
+            boolean isLogin = StpUtil.isLogin();
+            
             if (tokenValue != null && !tokenValue.isEmpty()) {
-                log.debug("当前请求token: {}, 是否登录: {}", tokenValue, StpUtil.isLogin());
+                String loginId = "unknown";
+                try {
+                    loginId = StpUtil.getLoginIdAsString();
+                } catch (Exception e) {
+                    // 忽略异常,使用默认值
+                }
+                log.info("[Sa-Token Interceptor] 当前请求token: {}, 是否登录: {}, 登录账号: {}", 
+                    maskToken(tokenValue), isLogin, isLogin ? loginId : "未登录");
             } else {
-                log.debug("当前请求未携带有效token");
+                log.info("[Sa-Token Interceptor] 当前请求未携带有效token");
             }
 
             // 登录认证:除了指定的接口,其他都需要登录

+ 7 - 1
haha-miniapp/src/main/java/com/haha/miniapp/controller/StatusQueryController.java

@@ -56,7 +56,13 @@ public class StatusQueryController {
         }
         
         Map<String, String> result = new HashMap<>();
-            statusData.forEach((k, v) -> result.put(k.toString(), v != null ? v.toString() : ""));
+        // 只返回必要的字段,确保门状态字段名为doorStatus
+        result.put("deviceId", statusData.getOrDefault("deviceId", "").toString());
+        result.put("activityId", statusData.getOrDefault("activityId", "").toString());
+        result.put("doorStatus", statusData.getOrDefault("doorStatus", "unknown").toString());
+        result.put("openType", statusData.getOrDefault("openType", "").toString());
+        result.put("userId", statusData.getOrDefault("userId", "").toString());
+        result.put("timestamp", statusData.getOrDefault("timestamp", "").toString());
         
         return Result.success("查询成功", result);
     }

+ 108 - 0
haha-miniapp/src/main/resources/application-dev.yml

@@ -0,0 +1,108 @@
+# 开发测试环境配置文件
+spring:
+  application:
+    name: haha-miniapp
+    
+  # 数据库配置(开发测试环境)
+  datasource:
+    url: jdbc:mysql://server.kuaiyuman.cn:3306/haha?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=true&requireSSL=true
+    username: root
+    password: KuaiyuMan/*-
+    driver-class-name: com.mysql.cj.jdbc.Driver
+    # 生产环境连接池优化
+    hikari:
+      minimum-idle: 5
+      maximum-pool-size: 20
+      max-lifetime: 1800000
+      idle-timeout: 300000
+      connection-timeout: 30000
+      connection-test-query: SELECT 1
+      keepalive-time: 30000
+      
+  # Redis 配置(开发测试环境)
+  data:
+    redis:
+      host: server.kuaiyuman.cn
+      port: 6379
+      password: "KtXA^Zx!TZmLEy(@JjB@2(TVG0kdy5)&"
+      database: 8
+      timeout: 10000
+      lettuce:
+        pool:
+          max-active: 20
+          max-wait: 3000
+          max-idle: 15
+          min-idle: 5
+          time-between-eviction-runs: 30000
+        shutdown-timeout: 100
+
+# 服务器配置(开发测试环境)
+server:
+  port: 7077
+  servlet:
+    context-path: /api
+  # SSL 配置(如果需要HTTPS)
+  # ssl:
+  #   enabled: true
+  #   key-store: classpath:keystore.p12
+  #   key-store-password: your_keystore_password
+  #   key-store-type: PKCS12
+
+# Sa-Token 配置(开发测试环境优化)
+sa-token:
+  # token 名称
+  token-name: accessToken
+  # token 有效期(单位:秒)- 30天
+  timeout: 2592000
+  # token 临时有效期(单位:秒)
+  active-timeout: -1
+  # 是否允许同一账号多地同时登录
+  is-concurrent: true
+  # 同一账号最大登录数量
+  max-login-count: 100
+  # token 风格
+  token-style: uuid
+  # 是否输出操作日志
+  is-log: true
+  # 生产环境额外配置
+  is-share: true  # 不共享token
+  is-read-header: true
+  is-read-cookie: false  # 禁用cookie,只使用header
+  is-lasting-cookie: false
+
+# 日志配置(生产环境)
+logging:
+  level:
+    root: INFO
+    com.haha.miniapp: INFO
+    cn.dev33.satoken: INFO
+  file:
+    path: /var/log/haha-miniapp
+    name: /var/log/haha-miniapp/haha-miniapp.log
+  pattern:
+    file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
+    console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
+
+# 微信相关配置(生产环境)
+wechat:
+  miniapp:
+    app-id: your_production_wechat_miniapp_appid
+    secret: your_production_wechat_miniapp_secret
+    token: your_production_wechat_miniapp_token
+    aes-key: your_production_wechat_miniapp_aes_key
+
+# 哈哈零兽配置(生产环境)
+haha:
+  api:
+    app-id: 2601051549145878
+    app-secret: 06e1be59332b00de0baad82002cdbcb5
+    base-url: http://api.hahabianli.com/
+
+management:
+  endpoints:
+    web:
+      exposure:
+        include: health,info,metrics
+  endpoint:
+    health:
+      show-details: when_authorized

+ 108 - 0
haha-miniapp/src/main/resources/application-prod.yml

@@ -0,0 +1,108 @@
+# 生产环境配置文件
+spring:
+  application:
+    name: haha-miniapp
+    
+  # 数据库配置(生产环境)
+  datasource:
+    url: jdbc:mysql://server.kuaiyuman.cn:3306/haha?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=true&requireSSL=true
+    username: root
+    password: KuaiyuMan/*-
+    driver-class-name: com.mysql.cj.jdbc.Driver
+    # 生产环境连接池优化
+    hikari:
+      minimum-idle: 5
+      maximum-pool-size: 20
+      max-lifetime: 1800000
+      idle-timeout: 300000
+      connection-timeout: 30000
+      connection-test-query: SELECT 1
+      keepalive-time: 30000
+      
+  # Redis 配置(生产环境)
+  data:
+    redis:
+      host: server.kuaiyuman.cn
+      port: 6379
+      password: "KtXA^Zx!TZmLEy(@JjB@2(TVG0kdy5)&"
+      database: 8
+      timeout: 10000
+      lettuce:
+        pool:
+          max-active: 20
+          max-wait: 3000
+          max-idle: 15
+          min-idle: 5
+          time-between-eviction-runs: 30000
+        shutdown-timeout: 100
+
+# 服务器配置(生产环境)
+server:
+  port: 7077
+  servlet:
+    context-path: /api
+  # SSL 配置(如果需要HTTPS)
+  # ssl:
+  #   enabled: true
+  #   key-store: classpath:keystore.p12
+  #   key-store-password: your_keystore_password
+  #   key-store-type: PKCS12
+
+# Sa-Token 配置(生产环境优化)
+sa-token:
+  # token 名称
+  token-name: accessToken
+  # token 有效期(单位:秒)- 30天
+  timeout: 2592000
+  # token 临时有效期(单位:秒)
+  active-timeout: -1
+  # 是否允许同一账号多地同时登录
+  is-concurrent: true
+  # 同一账号最大登录数量
+  max-login-count: 100
+  # token 风格
+  token-style: uuid
+  # 是否输出操作日志
+  is-log: true
+  # 生产环境额外配置
+  is-share: true  # 不共享token
+  is-read-header: true
+  is-read-cookie: false  # 禁用cookie,只使用header
+  is-lasting-cookie: false
+
+# 日志配置(生产环境)
+logging:
+  level:
+    root: INFO
+    com.haha.miniapp: INFO
+    cn.dev33.satoken: INFO
+  file:
+    path: /var/log/haha-miniapp
+    name: /var/log/haha-miniapp/haha-miniapp.log
+  pattern:
+    file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
+    console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
+
+# 微信相关配置(生产环境)
+wechat:
+  miniapp:
+    app-id: your_production_wechat_miniapp_appid
+    secret: your_production_wechat_miniapp_secret
+    token: your_production_wechat_miniapp_token
+    aes-key: your_production_wechat_miniapp_aes_key
+
+# 哈哈零兽配置(生产环境)
+haha:
+  api:
+    app-id: 2601051549145878
+    app-secret: 06e1be59332b00de0baad82002cdbcb5
+    base-url: http://api.hahabianli.com/
+
+management:
+  endpoints:
+    web:
+      exposure:
+        include: health,info,metrics
+  endpoint:
+    health:
+      show-details: when_authorized

+ 1 - 0
haha-miniapp/src/main/resources/application.yml

@@ -1,6 +1,7 @@
 spring:
   application:
     name: haha-miniapp
+    active: dev
 
   # 数据库配置
   datasource:

+ 7 - 0
haha-mp/src/App.vue

@@ -21,6 +21,13 @@ onShow(() => {
  */
 onHide(() => {
   console.log("App Hide");
+  
+  // 当应用隐藏时,清理购物页面的轮询状态
+  const pollingActive = uni.getStorageSync('shoppingPollingActive');
+  if (pollingActive === 'true') {
+    console.log('应用隐藏,清理购物页面轮询状态');
+    uni.removeStorageSync('shoppingPollingActive');
+  }
 });
 
 /**

+ 3 - 0
haha-mp/src/pages/index/index.vue

@@ -108,6 +108,9 @@ const scanCode = () => {
         uni.setStorageSync('currentOutTradeNo', response.outTradeNo);
         uni.setStorageSync('currentOrderNo', response.orderNo);
         
+        // 清理可能存在的旧轮询状态标记
+        uni.removeStorageSync('shoppingPollingActive');
+        
         // 跳转到购物进行中页面
         setTimeout(() => {
           uni.navigateTo({

+ 95 - 56
haha-mp/src/pages/shopping/shopping.vue

@@ -62,8 +62,13 @@ const currentOrderNo = ref('');
 
 let countdownTimer: number | null = null;
 let statusCheckTimer: number | null = null;
+let isComponentActive = true; // 组件活跃状态标志
 
 onMounted(() => {
+  // 标记当前页面轮询为活跃状态
+  uni.setStorageSync('shoppingPollingActive', 'true');
+  isComponentActive = true;
+  
   // 获取存储的设备信息
   currentDeviceId.value = uni.getStorageSync('currentDeviceId') || '';
   currentOrderNo.value = uni.getStorageSync('currentOrderNo') || '';
@@ -82,11 +87,16 @@ onMounted(() => {
 });
 
 onUnmounted(() => {
+  // 设置组件为非活跃状态,停止所有轮询
+  isComponentActive = false;
+  
   if (countdownTimer) {
     clearInterval(countdownTimer);
+    countdownTimer = null;
   }
   if (statusCheckTimer) {
     clearTimeout(statusCheckTimer);
+    statusCheckTimer = null;
   }
 });
 
@@ -98,75 +108,104 @@ const startCountdown = () => {
   }, 1000);
 };
 
-const startStatusPolling = () => {
-  // 轮询设备状态,等待关门
-  pollDeviceStatus(currentDeviceId.value, 120000, 2000)
-    .then((status) => {
-      console.log('设备状态更新:', status);
+const startStatusPolling = async () => {
+  // 检查组件是否仍然活跃
+  if (!isComponentActive) {
+    return;
+  }
+  
+  try {
+    // 轮询设备状态,等待关门
+    const status = await pollDeviceStatus(currentDeviceId.value, 120000, 2000);
+    
+    // 再次检查组件是否仍然活跃
+    if (!isComponentActive) {
+      return;
+    }
+    
+    console.log('设备状态更新:', status);
+    
+    // 检查门是否关闭
+    if (status.doorStatus === 'close') {
+      // 门已关,进入识别阶段
+      doorStatus.value = 'closing';
+      currentActivityId.value = status.activityId;
+      
+      // 开始轮询识别结果
+      const recognizeResult = await pollRecognizeResult(status.activityId, 60000, 1000);
+      
+      // 再次检查组件是否仍然活跃
+      if (!isComponentActive) {
+        return;
+      }
       
-      // 检查门是否关闭
-            if (status.doorStatus === 'close') {
-                // 门已关,进入识别阶段
-                doorStatus.value = 'closing';
-                currentActivityId.value = status.activityId;
-                
-                // 开始轮询识别结果
-                return pollRecognizeResult(status.activityId, 60000, 1000);
-            } else {
-                // 继续等待关门
-                return new Promise<RecognizeResultResponse>((_, reject) => {
-                    startStatusPolling();
-                    setTimeout(() => reject(new Error('继续轮询')), 1000);
-                });
-            }
-    })
-    .then((recognizeResult) => {
       if (recognizeResult) {
         console.log('识别结果:', recognizeResult);
         
         // 开始轮询订单信息
-        return pollOrderInfo(currentActivityId.value, 30000, 1000);
-      }
-    })
-    .then((orderInfo) => {
-      if (orderInfo) {
-        console.log('订单信息:', orderInfo);
+        const orderInfo = await pollOrderInfo(currentActivityId.value, 30000, 1000);
         
-        // 解析商品列表
-        if (orderInfo.products) {
-          try {
-            purchasedProducts.value = JSON.parse(orderInfo.products);
-          } catch (e) {
-            console.error('解析商品列表失败:', e);
-            purchasedProducts.value = [];
-          }
+        // 再次检查组件是否仍然活跃
+        if (!isComponentActive) {
+          return;
         }
         
-        // 设置总价
-        totalPrice.value = parseFloat(orderInfo.totalAmount) || 0;
-        
-        // 进入结算完成阶段
-        doorStatus.value = 'closed';
-        
-        // 停止倒计时
-        if (countdownTimer) {
-          clearInterval(countdownTimer);
+        if (orderInfo) {
+          console.log('订单信息:', orderInfo);
+          
+          // 解析商品列表
+          if (orderInfo.products) {
+            try {
+              purchasedProducts.value = JSON.parse(orderInfo.products);
+            } catch (e) {
+              console.error('解析商品列表失败:', e);
+              purchasedProducts.value = [];
+            }
+          }
+          
+          // 设置总价
+          totalPrice.value = parseFloat(orderInfo.totalAmount) || 0;
+          
+          // 进入结算完成阶段
+          doorStatus.value = 'closed';
+          
+          // 停止倒计时
+          if (countdownTimer) {
+            clearInterval(countdownTimer);
+            countdownTimer = null;
+          }
         }
       }
-    })
-    .catch((error) => {
-      console.error('状态轮询失败:', error);
-      
-      // 如果还在开门状态,继续轮询
-      if (doorStatus.value === 'opened') {
+    } else {
+      // 继续等待关门,设置延迟后重新轮询
+      if (isComponentActive && doorStatus.value === 'opened') {
         statusCheckTimer = setTimeout(() => {
-          startStatusPolling();
+          if (isComponentActive) {
+            startStatusPolling();
+          }
         }, 3000);
-      } else {
-        doorStatus.value = 'error';
-        errorMessage.value = error.message || '获取状态失败';
       }
-    });
+    }
+  } catch (error: any) {
+    console.error('状态轮询失败:', error);
+    
+    // 再次检查组件是否仍然活跃
+    if (!isComponentActive) {
+      return;
+    }
+    
+    // 如果还在开门状态,继续轮询
+    if (doorStatus.value === 'opened') {
+      statusCheckTimer = setTimeout(() => {
+        if (isComponentActive) {
+          startStatusPolling();
+        }
+      }, 3000);
+    } else {
+      doorStatus.value = 'error';
+      errorMessage.value = error.message || '获取状态失败';
+    }
+  }
 };
 
 const showProblem = () => {

+ 25 - 0
haha-service/src/main/java/com/haha/service/DeviceService.java

@@ -105,4 +105,29 @@ public interface DeviceService extends IService<Device> {
      * @return 设备列表
      */
     List<Device> getUnlinkedDevices();
+    
+    /**
+     * 根据门状态查询设备列表
+     *
+     * @param doorStatus 门状态
+     * @return 设备列表
+     */
+    List<Device> getDevicesByDoorStatus(String doorStatus);
+    
+    /**
+     * 更新设备门状态
+     *
+     * @param deviceId 设备ID
+     * @param doorStatus 门状态
+     * @return 是否成功
+     */
+    boolean updateDeviceDoorStatus(String deviceId, String doorStatus);
+    
+    /**
+     * 批量更新设备门状态
+     *
+     * @param deviceStatusMap 设备ID和门状态的映射
+     * @return 成功更新的数量
+     */
+    int batchUpdateDeviceDoorStatus(Map<String, String> deviceStatusMap);
 }

+ 45 - 0
haha-service/src/main/java/com/haha/service/impl/DeviceServiceImpl.java

@@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.haha.common.constant.DeviceConstants;
 import com.haha.common.constant.OrderConstants;
+import com.haha.common.enums.DeviceDoorStatus;
 import com.haha.common.exception.BusinessException;
 import com.haha.common.utils.EntityLabelUtils;
 import com.haha.entity.Device;
@@ -38,6 +39,7 @@ public class DeviceServiceImpl extends ServiceImpl<DeviceMapper, Device> impleme
     private final HahaClient hahaClient;
     private final OrderService orderService;
     private final ShopMapper shopMapper;
+    private final DeviceMapper deviceMapper;
 
     @Override
     public IPage<Device> getPage(int page, int pageSize, String deviceId, Long shopId, Integer status) {
@@ -320,4 +322,47 @@ public class DeviceServiceImpl extends ServiceImpl<DeviceMapper, Device> impleme
             device.setOrderCount(0);
         }
     }
+    
+    @Override
+    public List<Device> getDevicesByDoorStatus(String doorStatus) {
+        try {
+            return deviceMapper.selectByDoorStatus(doorStatus);
+        } catch (Exception e) {
+            log.error("根据门状态查询设备列表失败 - 门状态: {}", doorStatus, e);
+            return java.util.Collections.emptyList();
+        }
+    }
+    
+    @Override
+    public boolean updateDeviceDoorStatus(String deviceId, String doorStatus) {
+        try {
+            int result = deviceMapper.updateDoorStatus(deviceId, doorStatus);
+            if (result > 0) {
+                log.info("更新设备 {} 门状态成功: {}", deviceId, doorStatus);
+                return true;
+            } else {
+                log.warn("更新设备 {} 门状态失败: 找不到设备或状态未改变", deviceId);
+                return false;
+            }
+        } catch (Exception e) {
+            log.error("更新设备 {} 门状态异常 - 状态: {}", deviceId, doorStatus, e);
+            return false;
+        }
+    }
+    
+    @Override
+    public int batchUpdateDeviceDoorStatus(java.util.Map<String, String> deviceStatusMap) {
+        if (deviceStatusMap == null || deviceStatusMap.isEmpty()) {
+            return 0;
+        }
+        
+        try {
+            int result = deviceMapper.batchUpdateDoorStatus(deviceStatusMap);
+            log.info("批量更新设备门状态成功 - 更新数量: {}", result);
+            return result;
+        } catch (Exception e) {
+            log.error("批量更新设备门状态异常", e);
+            return 0;
+        }
+    }
 }

+ 250 - 4
haha-service/src/main/java/com/haha/service/impl/HahaCallbackServiceImpl.java

@@ -3,14 +3,22 @@ package com.haha.service.impl;
 import com.alibaba.fastjson2.JSON;
 import com.alibaba.fastjson2.JSONArray;
 import com.alibaba.fastjson2.JSONObject;
+import com.haha.common.constant.OrderConstants;
+import com.haha.entity.Device;
+import com.haha.entity.Order;
+import com.haha.mapper.DeviceMapper;
 import com.haha.service.HahaCallbackService;
 import com.haha.service.OrderService;
+import org.springframework.data.redis.core.StringRedisTemplate;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 
+import java.math.BigDecimal;
 import java.util.Map;
+import java.util.concurrent.TimeUnit;
 
 @Slf4j
 @Service
@@ -18,9 +26,19 @@ public class HahaCallbackServiceImpl implements HahaCallbackService {
 
     @Autowired
     private OrderService orderService;
+    
+    @Autowired
+    private DeviceMapper deviceMapper;
+    
+    @Autowired
+    private StringRedisTemplate stringRedisTemplate;
 
     @Value("${haha.api.app-secret}")
     private String appSecret;
+    
+    private static final String DEVICE_STATUS_KEY = "haha:device:status:";
+    private static final String RECOGNIZE_RESULT_KEY = "haha:recognize:result:";
+    private static final String ORDER_INFO_KEY = "haha:order:info:";
 
     @Override
     public void handleMessage(Map<String, Object> params) {
@@ -55,27 +73,44 @@ public class HahaCallbackServiceImpl implements HahaCallbackService {
     }
 
     @Override
+    @Transactional(rollbackFor = Exception.class)
     public void handleDeviceStatus(Map<String, Object> params) {
         try {
             String deviceId = (String) params.get("device_id");
             String status = (String) params.get("status");
             String openType = (String) params.get("open_type");
+            String activityId = (String) params.get("activity_id");
+            String userId = (String) params.get("user_id");
+            
+            log.info("开关门状态通知 - 设备: {}, 状态: {}, 类型: {}, 活动: {}, 用户: {}", 
+                    deviceId, status, openType, activityId, userId);
             
-            log.info("开关门状态通知 - 设备: {}, 状态: {}, 类型: {}", deviceId, status, openType);
+            // 保存设备状态到Redis,供小程序轮询
+            String statusKey = DEVICE_STATUS_KEY + deviceId;
+            stringRedisTemplate.opsForValue().set(statusKey, JSON.toJSONString(params), 30, TimeUnit.MINUTES);
+            
+            // 更新设备表中的门状态字段
+            updateDeviceDoorStatus(deviceId, status);
             
             switch (status) {
                 case "2": // OPENED
                     log.info("设备 {} 开门成功", deviceId);
+                    handleDeviceOpened(deviceId, activityId, userId, openType);
                     break;
                 case "3": // CLOSED
                     log.info("设备 {} 关门成功,等待AI识别", deviceId);
+                    handleDeviceClosed(deviceId, activityId, userId);
                     break;
                 case "1": // ERROR
                     log.error("设备 {} 开门失败", deviceId);
+                    handleDeviceError(deviceId, activityId, userId);
                     break;
                 case "4": // ANOTHER
                     log.warn("设备 {} 繁忙", deviceId);
+                    handleDeviceBusy(deviceId, activityId, userId);
                     break;
+                default:
+                    log.warn("未知的设备状态: {}", status);
             }
         } catch (Exception e) {
             log.error("处理开关门状态通知逻辑失败", e);
@@ -90,6 +125,11 @@ public class HahaCallbackServiceImpl implements HahaCallbackService {
             Integer isOnline = onlineObj instanceof Integer ? (Integer) onlineObj : Integer.valueOf(onlineObj.toString());
             
             log.info("设备在线状态通知 - 设备: {}, 在线状态: {}", deviceId, isOnline == 1 ? "在线" : "离线");
+            
+            // 更新设备在线状态到缓存
+            String onlineKey = "device:online:" + deviceId;
+            stringRedisTemplate.opsForValue().set(onlineKey, isOnline.toString(), 10, TimeUnit.MINUTES);
+            
         } catch (Exception e) {
             log.error("处理设备在线状态通知逻辑失败", e);
         }
@@ -103,6 +143,11 @@ public class HahaCallbackServiceImpl implements HahaCallbackService {
             Integer voice = voiceObj instanceof Integer ? (Integer) voiceObj : Integer.valueOf(voiceObj.toString());
             
             log.info("音量调节结果通知 - 设备: {}, 音量值: {}", deviceId, voice);
+            
+            // 保存音量设置到缓存
+            String voiceKey = "device:voice:" + deviceId;
+            stringRedisTemplate.opsForValue().set(voiceKey, voice.toString(), 30, TimeUnit.DAYS);
+            
         } catch (Exception e) {
             log.error("处理音量调节结果通知逻辑失败", e);
         }
@@ -115,8 +160,16 @@ public class HahaCallbackServiceImpl implements HahaCallbackService {
             Object statusObj = params.get("status");
             Integer status = statusObj instanceof Integer ? (Integer) statusObj : Integer.valueOf(statusObj.toString());
             String name = (String) params.get("name");
+            String auditRemark = (String) params.get("audit_remark");
+            
+            log.info("新品审核结果 - ID: {}, 商品名: {}, 状态: {}, 备注: {}", id, name, status, auditRemark);
+            
+            // 保存审核结果到缓存
+            String auditKey = "product:audit:" + id;
+            stringRedisTemplate.opsForValue().set(auditKey, JSON.toJSONString(params), 7, TimeUnit.DAYS);
+            
+            // TODO: 根据审核状态更新商品信息或通知相关人员
             
-            log.info("新品审核结果 - ID: {}, 商品名: {}, 状态: {}", id, name, status);
         } catch (Exception e) {
             log.error("处理新品审核结果通知逻辑失败", e);
         }
@@ -129,6 +182,13 @@ public class HahaCallbackServiceImpl implements HahaCallbackService {
             if (listObj instanceof JSONArray) {
                 JSONArray list = (JSONArray) listObj;
                 log.info("商品合并结果通知 - 合并数量: {}", list.size());
+                
+                // 保存合并结果到缓存
+                String mergeKey = "product:merge:" + System.currentTimeMillis();
+                stringRedisTemplate.opsForValue().set(mergeKey, list.toJSONString(), 1, TimeUnit.DAYS);
+                
+                // TODO: 处理商品合并逻辑,如更新商品关联关系
+                
             }
         } catch (Exception e) {
             log.error("处理商品合并结果通知逻辑失败", e);
@@ -136,20 +196,31 @@ public class HahaCallbackServiceImpl implements HahaCallbackService {
     }
 
     @Override
+    @Transactional(rollbackFor = Exception.class)
     public void handleOrcResult(Map<String, Object> params) {
         try {
             String activityId = (String) params.get("activity_id");
             String deviceId = (String) params.get("device_id");
+            String userId = (String) params.get("user_id");
             Object nobuyObj = params.get("nobuy");
             Integer nobuy = nobuyObj instanceof Integer ? (Integer) nobuyObj : Integer.valueOf(nobuyObj.toString());
+            String resourceInfoStr = (String) params.get("resource_info");
+            Object confidenceObj = params.get("confidence");
+            BigDecimal confidence = confidenceObj != null ? new BigDecimal(confidenceObj.toString()) : null;
+            
+            // 保存识别结果到Redis
+            String recognizeKey = RECOGNIZE_RESULT_KEY + activityId;
+            stringRedisTemplate.opsForValue().set(recognizeKey, JSON.toJSONString(params), 30, TimeUnit.MINUTES);
             
             if (nobuy == 1) {
                 log.info("AI识别结果: 用户未消费 (activityId: {})", activityId);
+                handleNoPurchase(activityId, deviceId, userId);
                 return;
             }
 
             // 解析识别结果
             String resultStr = (String) params.get("result");
+            String skuListStr = (String) params.get("skuList");
             JSONObject result = JSON.parseObject(resultStr);
             JSONArray excepts = result.getJSONArray("excepts");
             
@@ -157,18 +228,193 @@ public class HahaCallbackServiceImpl implements HahaCallbackService {
                 log.warn("识别结果包含异常 (activityId: {}): {}", activityId, excepts.toJSONString());
             }
 
-            log.info("AI识别结果通知处理完成 - 设备: {}, 活动: {}", deviceId, activityId);
+            log.info("AI识别结果通知处理完成 - 设备: {}, 活动: {}, 用户: {}", deviceId, activityId, userId);
+            
+            // 创建订单
+            Order order = orderService.createOrderFromRecognition(
+                activityId, deviceId, userId, skuListStr, resourceInfoStr, confidence);
             
-            // TODO: 调用 orderService 计算金额并生成订单
+            if (order != null) {
+                log.info("AI识别订单创建成功 - 订单ID: {}, 金额: {}", order.getId(), order.getTotalAmount());
+                
+                // 保存订单信息到Redis供小程序查询
+                saveOrderInfoToRedis(order, activityId);
+            }
             
         } catch (Exception e) {
             log.error("处理AI识别结果逻辑失败", e);
         }
     }
 
+    /**
+     * 处理设备开门成功
+     */
+    private void handleDeviceOpened(String deviceId, String activityId, String userId, String openType) {
+        try {
+            log.info("处理设备开门成功 - 设备: {}, 活动: {}, 用户: {}, 类型: {}", 
+                    deviceId, activityId, userId, openType);
+            
+            // 更新相关订单状态为进行中
+            if (activityId != null && userId != null) {
+                Order order = orderService.lambdaQuery()
+                    .eq(Order::getActivityId, activityId)
+                    .eq(Order::getUserId, Long.parseLong(userId))
+                    .eq(Order::getStatus, OrderConstants.STATUS_PENDING_PAYMENT)
+                    .orderByDesc(Order::getCreateTime)
+                    .last("LIMIT 1")
+                    .one();
+                
+                if (order != null) {
+                    order.setStatus(OrderConstants.STATUS_PENDING_PAYMENT);
+                    order.setPayStatus(OrderConstants.PAY_STATUS_UNPAID);
+                    orderService.updateById(order);
+                    log.info("更新订单状态为待支付: orderNo={}", order.getOrderNo());
+                }
+            }
+            
+        } catch (Exception e) {
+            log.error("处理设备开门成功逻辑失败", e);
+        }
+    }
+    
+    /**
+     * 处理设备关门
+     */
+    private void handleDeviceClosed(String deviceId, String activityId, String userId) {
+        try {
+            log.info("处理设备关门 - 设备: {}, 活动: {}, 用户: {}", deviceId, activityId, userId);
+            
+            // 触发AI识别流程
+            // 实际的AI识别由哈哈平台处理,我们只需等待ORC_RESULT回调
+            
+        } catch (Exception e) {
+            log.error("处理设备关门逻辑失败", e);
+        }
+    }
+    
+    /**
+     * 处理设备错误
+     */
+    private void handleDeviceError(String deviceId, String activityId, String userId) {
+        try {
+            log.error("处理设备错误 - 设备: {}, 活动: {}, 用户: {}", deviceId, activityId, userId);
+            
+            // 取消相关订单
+            if (activityId != null && userId != null) {
+                Order order = orderService.lambdaQuery()
+                    .eq(Order::getActivityId, activityId)
+                    .eq(Order::getUserId, Long.parseLong(userId))
+                    .eq(Order::getStatus, OrderConstants.STATUS_PENDING_PAYMENT)
+                    .orderByDesc(Order::getCreateTime)
+                    .last("LIMIT 1")
+                    .one();
+                
+                if (order != null) {
+                    order.setStatus(OrderConstants.STATUS_CANCELLED);
+                    order.setPayStatus(OrderConstants.PAY_STATUS_UNPAID);
+                    orderService.updateById(order);
+                    log.info("取消订单due to设备错误: orderNo={}", order.getOrderNo());
+                }
+            }
+            
+        } catch (Exception e) {
+            log.error("处理设备错误逻辑失败", e);
+        }
+    }
+    
+    /**
+     * 处理设备繁忙
+     */
+    private void handleDeviceBusy(String deviceId, String activityId, String userId) {
+        try {
+            log.warn("处理设备繁忙 - 设备: {}, 活动: {}, 用户: {}", deviceId, activityId, userId);
+            
+            // 通知用户设备繁忙,建议重试
+            // TODO: 发送消息通知给用户
+            
+        } catch (Exception e) {
+            log.error("处理设备繁忙逻辑失败", e);
+        }
+    }
+    
+    /**
+     * 处理用户未消费情况
+     */
+    private void handleNoPurchase(String activityId, String deviceId, String userId) {
+        try {
+            log.info("处理用户未消费 - 活动: {}, 设备: {}, 用户: {}", activityId, deviceId, userId);
+            
+            // 取消相关订单
+            if (activityId != null && userId != null) {
+                Order order = orderService.lambdaQuery()
+                    .eq(Order::getActivityId, activityId)
+                    .eq(Order::getUserId, Long.parseLong(userId))
+                    .eq(Order::getStatus, OrderConstants.STATUS_PENDING_PAYMENT)
+                    .orderByDesc(Order::getCreateTime)
+                    .last("LIMIT 1")
+                    .one();
+                
+                if (order != null) {
+                    order.setStatus(OrderConstants.STATUS_CANCELLED);
+                    order.setPayStatus(OrderConstants.PAY_STATUS_UNPAID);
+                    orderService.updateById(order);
+                    log.info("取消订单due to用户未消费: orderNo={}", order.getOrderNo());
+                }
+            }
+            
+        } catch (Exception e) {
+            log.error("处理用户未消费逻辑失败", e);
+        }
+    }
+    
+    /**
+     * 保存订单信息到Redis供小程序查询
+     */
+    private void saveOrderInfoToRedis(Order order, String activityId) {
+        try {
+            String orderKey = ORDER_INFO_KEY + activityId;
+            JSONObject orderJson = new JSONObject();
+            orderJson.put("orderId", order.getId());
+            orderJson.put("orderNo", order.getOrderNo());
+            orderJson.put("activityId", activityId);
+            orderJson.put("deviceId", order.getDeviceId());
+            orderJson.put("userId", order.getUserId());
+            orderJson.put("totalAmount", order.getTotalAmount());
+            orderJson.put("items", order.getItems());
+            orderJson.put("status", order.getStatus());
+            orderJson.put("payStatus", order.getPayStatus());
+            orderJson.put("createTime", order.getCreateTime());
+            orderJson.put("timestamp", System.currentTimeMillis());
+            
+            stringRedisTemplate.opsForValue().set(orderKey, orderJson.toJSONString(), 30, TimeUnit.MINUTES);
+            
+        } catch (Exception e) {
+            log.error("保存订单信息到Redis失败", e);
+        }
+    }
+    
+    /**
+     * 更新设备门状态
+     */
+    private void updateDeviceDoorStatus(String deviceId, String doorStatus) {
+        try {
+            Device device = deviceMapper.selectByDeviceId(deviceId);
+            if (device != null) {
+                device.setDoorStatus(doorStatus);
+                deviceMapper.updateById(device);
+                log.info("更新设备 {} 门状态为: {}", deviceId, doorStatus);
+            } else {
+                log.warn("未找到设备: {}", deviceId);
+            }
+        } catch (Exception e) {
+            log.error("更新设备门状态失败 - 设备: {}, 状态: {}", deviceId, doorStatus, e);
+        }
+    }
+    
     @Override
     public boolean validateSign(Map<String, Object> params) {
         // TODO: 实现具体的签名验证逻辑
+        // 建议使用HMAC-SHA256算法,结合appSecret进行签名验证
         return true;
     }
 }