skyline пре 4 месеци
родитељ
комит
1b6f1ac196
35 измењених фајлова са 1904 додато и 0 уклоњено
  1. 114 0
      haha-miniapp/pom.xml
  2. 50 0
      haha-miniapp/src/main/java/com/haha/miniapp/Application.java
  3. 48 0
      haha-miniapp/src/main/java/com/haha/miniapp/config/HahaSdkConfig.java
  4. 22 0
      haha-miniapp/src/main/java/com/haha/miniapp/config/SaTokenConfig.java
  5. 314 0
      haha-miniapp/src/main/java/com/haha/miniapp/controller/CallbackController.java
  6. 178 0
      haha-miniapp/src/main/java/com/haha/miniapp/controller/DeviceController.java
  7. 21 0
      haha-miniapp/src/main/java/com/haha/miniapp/controller/HealthController.java
  8. 53 0
      haha-miniapp/src/main/java/com/haha/miniapp/controller/LoginController.java
  9. 323 0
      haha-miniapp/src/main/java/com/haha/miniapp/controller/OrderController.java
  10. 34 0
      haha-miniapp/src/main/java/com/haha/miniapp/entity/Account.java
  11. 29 0
      haha-miniapp/src/main/java/com/haha/miniapp/entity/Device.java
  12. 37 0
      haha-miniapp/src/main/java/com/haha/miniapp/entity/FundLog.java
  13. 43 0
      haha-miniapp/src/main/java/com/haha/miniapp/entity/Order.java
  14. 33 0
      haha-miniapp/src/main/java/com/haha/miniapp/entity/Product.java
  15. 33 0
      haha-miniapp/src/main/java/com/haha/miniapp/entity/User.java
  16. 7 0
      haha-miniapp/src/main/java/com/haha/miniapp/mapper/AccountMapper.java
  17. 7 0
      haha-miniapp/src/main/java/com/haha/miniapp/mapper/DeviceMapper.java
  18. 7 0
      haha-miniapp/src/main/java/com/haha/miniapp/mapper/FundLogMapper.java
  19. 7 0
      haha-miniapp/src/main/java/com/haha/miniapp/mapper/OrderMapper.java
  20. 7 0
      haha-miniapp/src/main/java/com/haha/miniapp/mapper/ProductMapper.java
  21. 7 0
      haha-miniapp/src/main/java/com/haha/miniapp/mapper/UserMapper.java
  22. 10 0
      haha-miniapp/src/main/java/com/haha/miniapp/service/AccountService.java
  23. 13 0
      haha-miniapp/src/main/java/com/haha/miniapp/service/DeviceService.java
  24. 22 0
      haha-miniapp/src/main/java/com/haha/miniapp/service/LoginService.java
  25. 14 0
      haha-miniapp/src/main/java/com/haha/miniapp/service/OrderService.java
  26. 14 0
      haha-miniapp/src/main/java/com/haha/miniapp/service/ProductService.java
  27. 12 0
      haha-miniapp/src/main/java/com/haha/miniapp/service/UserService.java
  28. 27 0
      haha-miniapp/src/main/java/com/haha/miniapp/service/impl/AccountServiceImpl.java
  29. 40 0
      haha-miniapp/src/main/java/com/haha/miniapp/service/impl/DeviceServiceImpl.java
  30. 104 0
      haha-miniapp/src/main/java/com/haha/miniapp/service/impl/LoginServiceImpl.java
  31. 47 0
      haha-miniapp/src/main/java/com/haha/miniapp/service/impl/OrderServiceImpl.java
  32. 46 0
      haha-miniapp/src/main/java/com/haha/miniapp/service/impl/ProductServiceImpl.java
  33. 37 0
      haha-miniapp/src/main/java/com/haha/miniapp/service/impl/UserServiceImpl.java
  34. 91 0
      haha-miniapp/src/main/resources/application.yml
  35. 53 0
      haha-miniapp/src/main/resources/sql/user.sql

+ 114 - 0
haha-miniapp/pom.xml

@@ -0,0 +1,114 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-parent</artifactId>
+        <version>3.4.13</version>
+        <relativePath/>
+    </parent>
+
+    <groupId>com.haha</groupId>
+    <artifactId>haha-miniapp</artifactId>
+    <version>1.0.0</version>
+    <name>haha-miniapp</name>
+    <description>智能视觉售卖机系统 - 用户端小程序后端</description>
+
+    <properties>
+        <java.version>17</java.version>
+        <maven.compiler.source>${java.version}</maven.compiler.source>
+        <maven.compiler.target>${java.version}</maven.compiler.target>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <mybatis-plus.version>3.5.10.1</mybatis-plus.version>
+        <hutool.version>5.8.28</hutool.version>
+    </properties>
+
+    <dependencies>
+        <!-- Spring Boot 核心依赖 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <!-- 数据库相关 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-jdbc</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.mysql</groupId>
+            <artifactId>mysql-connector-j</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
+            <version>${mybatis-plus.version}</version>
+        </dependency>
+
+        <!-- 工具类 -->
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-all</artifactId>
+            <version>${hutool.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <optional>true</optional>
+        </dependency>
+
+        <!-- Redis 依赖 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis</artifactId>
+        </dependency>
+
+        <!-- 微信支付相关 (暂时注释,后续需要时再添加) -->
+        <!-- <dependency>
+            <groupId>com.github.wechatpay-apiv3</groupId>
+            <artifactId>wechatpay-java</artifactId>
+            <version>0.2.13</version>
+        </dependency> -->
+
+        <!-- 其他工具 -->
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>fastjson</artifactId>
+            <version>2.0.45</version>
+        </dependency>
+        
+        <!-- Sa-Token 认证框架 -->
+        <dependency>
+            <groupId>cn.dev33</groupId>
+            <artifactId>sa-token-spring-boot3-starter</artifactId>
+            <version>1.39.0</version>
+        </dependency>
+
+        <!-- 哈哈零售 SDK -->
+        <dependency>
+            <groupId>com.haha</groupId>
+            <artifactId>haha-sdk</artifactId>
+            <version>1.0.0</version>
+        </dependency>
+        
+
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>

+ 50 - 0
haha-miniapp/src/main/java/com/haha/miniapp/Application.java

@@ -0,0 +1,50 @@
+package com.haha.miniapp;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.context.annotation.Bean;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
+import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
+import org.springframework.core.PriorityOrdered;
+import org.springframework.core.Ordered;
+
+@SpringBootApplication
+@MapperScan("com.haha.miniapp.mapper")
+public class Application {
+
+    @Bean
+    public static BeanFactoryPostProcessor beanFactoryPostProcessor() {
+        return new PriorityOrderedBeanFactoryPostProcessor();
+    }
+
+    public static class PriorityOrderedBeanFactoryPostProcessor implements BeanFactoryPostProcessor, PriorityOrdered {
+        @Override
+        public int getOrder() {
+            return Ordered.HIGHEST_PRECEDENCE;
+        }
+
+        @Override
+        public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
+            for (String beanDefinitionName : beanFactory.getBeanDefinitionNames()) {
+                BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanDefinitionName);
+                Object attr = beanDefinition.getAttribute("factoryBeanObjectType");
+                if (attr instanceof String) {
+                    try {
+                        // 将 String 类型的类名转换为真正的 Class 对象,解决 Spring 6.2+ (Boot 3.4/3.5) 的类型检查问题
+                        beanDefinition.setAttribute("factoryBeanObjectType", Class.forName((String) attr));
+                    } catch (Throwable e) {
+                        // 转换失败则移除该属性,由 Spring 自行推断
+                        beanDefinition.removeAttribute("factoryBeanObjectType");
+                    }
+                }
+            }
+        }
+    }
+
+    public static void main(String[] args) {
+        SpringApplication.run(Application.class, args);
+    }
+}

+ 48 - 0
haha-miniapp/src/main/java/com/haha/miniapp/config/HahaSdkConfig.java

@@ -0,0 +1,48 @@
+package com.haha.miniapp.config;
+
+import com.haha.sdk.HahaClient;
+import com.haha.sdk.config.HahaConfig;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 哈哈零售 SDK 配置类
+ * 基于新版SDK重新配置
+ */
+@Configuration
+public class HahaSdkConfig {
+
+    @Value("${haha.api.base-url}")
+    private String apiBaseUrl;
+
+    @Value("${haha.api.app-id}")
+    private String appId;
+
+    @Value("${haha.api.app-secret}")
+    private String appSecret;
+
+    /**
+     * 创建哈哈零售SDK客户端Bean
+     * 注意:新版SDK包路径已更改为 com.haha.sdk.HahaClient
+     */
+    @Bean
+    public HahaClient hahaClient() {
+        // 创建SDK配置
+        HahaConfig config = HahaConfig.builder()
+                .apiBaseUrl(apiBaseUrl)  // 新SDK使用apiBaseUrl
+                .appId(appId)
+                .appSecret(appSecret)
+                .connectTimeout(10)
+                .readTimeout(30)
+                .writeTimeout(30)
+                .enableLog(true)
+                .build();
+        
+        // 校验配置
+        config.validate();
+        
+        // 创建客户端
+        return new HahaClient(config);
+    }
+}

+ 22 - 0
haha-miniapp/src/main/java/com/haha/miniapp/config/SaTokenConfig.java

@@ -0,0 +1,22 @@
+package com.haha.miniapp.config;
+
+import cn.dev33.satoken.interceptor.SaInterceptor;
+import cn.dev33.satoken.stp.StpUtil;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Configuration
+public class SaTokenConfig implements WebMvcConfigurer {
+    
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        // 注册Sa-Token拦截器,拦截所有路径
+        registry.addInterceptor(new SaInterceptor(handle -> {
+            // 登录认证:除了登录接口和健康检查接口,其他都需要登录
+            StpUtil.checkLogin();
+        }))
+        .addPathPatterns("/**")
+        .excludePathPatterns("/api/login/**", "/api/health/**");
+    }
+}

+ 314 - 0
haha-miniapp/src/main/java/com/haha/miniapp/controller/CallbackController.java

@@ -0,0 +1,314 @@
+package com.haha.miniapp.controller;
+
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONArray;
+import com.alibaba.fastjson2.JSONObject;
+import com.haha.miniapp.entity.Order;
+import com.haha.miniapp.service.OrderService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.time.LocalDateTime;
+import java.util.Map;
+
+/**
+ * 哈哈平台回调通知接口
+ * 
+ * 接收哈哈平台推送的各类通知,所有通知都通过消息回调地址接收:
+ *    - DEVICE_STATUS: 开关门状态通知
+ *    - ONLINE_STATUS: 设备在线状态通知
+ *    - VOICE_RESULT: 音量调节结果通知
+ *    - CLIENT_NEW_PRODUCT: 新品审核结果回调
+ *    - MERGE_PRODUCT: 商品合并结果通知
+ *    - ORC_RESULT: AI识别结果通知(最重要)
+ * 
+ * 所有通知都通过 notify_type 字段区分具体类型
+ */
+@Slf4j
+@RestController
+@RequestMapping("/api/callback/haha")
+public class CallbackController {
+
+    @Autowired
+    private OrderService orderService;
+    
+    @Value("${haha.api.app-secret}")
+    private String appSecret;
+
+    /**
+     * 消息回调通知统一入口
+     * 
+     * 接收哈哈平台推送的所有类型通知,通过 notify_type 区分具体类型
+     * 
+     * @param params 回调参数
+     * @return 响应结果,必须返回 "success" 字符串
+     */
+    @PostMapping("/message")
+    public String handleMessageCallback(@RequestBody Map<String, Object> params) {
+        try {
+            // 记录回调日志
+            log.info("收到消息回调通知: {}", JSON.toJSONString(params));
+            
+            // 1. 验证签名(如果需要)
+            // validateSign(params);
+            
+            // 2. 获取通知类型
+            String notifyType = (String) params.get("notify_type");
+            
+            if (notifyType == null || notifyType.isEmpty()) {
+                log.warn("消息回调缺少 notify_type 参数");
+                return "success";
+            }
+            
+            // 3. 根据通知类型分发处理
+            switch (notifyType) {
+                case "DEVICE_STATUS":
+                    handleDeviceStatus(params);
+                    break;
+                case "ONLINE_STATUS":
+                    handleOnlineStatus(params);
+                    break;
+                case "VOICE_RESULT":
+                    handleVoiceResult(params);
+                    break;
+                case "CLIENT_NEW_PRODUCT":
+                    handleNewProductAudit(params);
+                    break;
+                case "MERGE_PRODUCT":
+                    handleMergeProduct(params);
+                    break;
+                case "ORC_RESULT":
+                    handleOrcResult(params);
+                    break;
+                default:
+                    log.warn("未知的消息通知类型: {}", notifyType);
+            }
+            
+            // 4. 返回成功响应(必须返回 "success" 字符串)
+            return "success";
+            
+        } catch (Exception e) {
+            log.error("处理消息回调通知失败", e);
+            // 即使处理失败,也返回成功,避免重复推送
+            return "success";
+        }
+    }
+
+    /**
+     * 处理开关门状态通知 (DEVICE_STATUS)
+     * 
+     * status取值:
+     * - 1=ERROR(开门失败)
+     * - 2=OPENED(门已开)
+     * - 3=CLOSED(门已关)
+     * - 4=ANOTHER(设备繁忙)
+     * 
+     * @param params 通知参数
+     */
+    private void handleDeviceStatus(Map<String, Object> params) {
+        try {
+            String activityId = (String) params.get("activity_id");
+            String deviceId = (String) params.get("device_id");
+            String status = (String) params.get("status");
+            String openType = (String) params.get("open_type");
+            String outUserId = (String) params.get("out_user_id");
+            
+            log.info("开关门状态通知 - 设备: {}, 状态: {}, 类型: {}, 用户: {}", 
+                deviceId, status, openType, outUserId);
+            
+            // 可以根据业务需要处理不同状态
+            switch (status) {
+                case "2": // OPENED
+                    log.info("设备 {} 开门成功", deviceId);
+                    break;
+                case "3": // CLOSED
+                    log.info("设备 {} 关门成功,等待AI识别", deviceId);
+                    break;
+                case "1": // ERROR
+                    log.error("设备 {} 开门失败", deviceId);
+                    break;
+                case "4": // ANOTHER
+                    log.warn("设备 {} 繁忙", deviceId);
+                    break;
+            }
+            
+        } catch (Exception e) {
+            log.error("处理开关门状态通知失败", e);
+        }
+    }
+
+    /**
+     * 处理设备在线状态通知 (ONLINE_STATUS)
+     * 
+     * @param params 通知参数
+     */
+    private void handleOnlineStatus(Map<String, Object> params) {
+        try {
+            String deviceId = (String) params.get("device_id");
+            Integer isOnline = (Integer) params.get("is_online");
+            
+            log.info("设备在线状态通知 - 设备: {}, 在线状态: {}", deviceId, isOnline == 1 ? "在线" : "离线");
+            
+            // 可以在这里更新设备在线状态到数据库
+            
+        } catch (Exception e) {
+            log.error("处理设备在线状态通知失败", e);
+        }
+    }
+
+    /**
+     * 处理音量调节结果通知 (VOICE_RESULT)
+     * 
+     * @param params 通知参数
+     */
+    private void handleVoiceResult(Map<String, Object> params) {
+        try {
+            String deviceId = (String) params.get("device_id");
+            Integer voice = (Integer) params.get("voice");
+            
+            log.info("音量调节结果通知 - 设备: {}, 音量值: {}", deviceId, voice);
+            
+        } catch (Exception e) {
+            log.error("处理音量调节结果通知失败", e);
+        }
+    }
+
+    /**
+     * 处理新品审核结果回调 (CLIENT_NEW_PRODUCT)
+     * 
+     * status取值:
+     * - 1=已通过
+     * - 2=已拒绝
+     * - 3=已上架
+     * 
+     * @param params 通知参数
+     */
+    private void handleNewProductAudit(Map<String, Object> params) {
+        try {
+            String id = (String) params.get("id");
+            Integer status = (Integer) params.get("status");
+            String name = (String) params.get("name");
+            String code = (String) params.get("code");
+            String rejectReason = (String) params.get("reject_reason");
+            
+            log.info("新品审核结果 - ID: {}, 商品名: {}, 状态: {}, code: {}", id, name, status, code);
+            
+            if (status == 2) {
+                log.warn("新品 {} 被拒绝,原因: {}", name, rejectReason);
+            }
+            
+        } catch (Exception e) {
+            log.error("处理新品审核结果通知失败", e);
+        }
+    }
+
+    /**
+     * 处理商品合并结果通知 (MERGE_PRODUCT)
+     * 
+     * @param params 通知参数
+     */
+    private void handleMergeProduct(Map<String, Object> params) {
+        try {
+            Object listObj = params.get("list");
+            
+            if (listObj instanceof JSONArray) {
+                JSONArray list = (JSONArray) listObj;
+                log.info("商品合并结果通知 - 合并数量: {}", list.size());
+                
+                for (Object item : list) {
+                    if (item instanceof JSONObject) {
+                        JSONObject merge = (JSONObject) item;
+                        String mainCode = merge.getString("main_code");
+                        String productCode = merge.getString("product_code");
+                        log.info("商品合并 - 主商品: {}, 合并商品: {}", mainCode, productCode);
+                    }
+                }
+            }
+            
+        } catch (Exception e) {
+            log.error("处理商品合并结果通知失败", e);
+        }
+    }
+
+    /**
+     * 处理AI识别结果通知 (ORC_RESULT) - 最重要的回调
+     * 
+     * 用户关门后,AI识别完成,哈哈平台推送识别结果
+     * 
+     * @param params 通知参数
+     */
+    private void handleOrcResult(Map<String, Object> params) {
+        try {
+            String activityId = (String) params.get("activity_id");
+            String deviceId = (String) params.get("device_id");
+            String outUserId = (String) params.get("out_user_id");
+            Integer nobuy = (Integer) params.get("nobuy");
+            
+            // 解析识别结果
+            String resultStr = (String) params.get("result");
+            String skuListStr = (String) params.get("sku_list");
+            String resourceInfoStr = (String) params.get("resource_info");
+            
+            log.info("AI识别结果通知 - 设备: {}, 活动: {}, 是否消费: {}", 
+                deviceId, activityId, nobuy == 1 ? "无消费" : "有消费");
+            
+            if (nobuy == 1) {
+                log.info("用户打开柜门但未消费,无需处理");
+                return;
+            }
+            
+            // 解析result字段
+            JSONObject result = JSON.parseObject(resultStr);
+            String type = result.getString("type"); // IN或OUT
+            JSONObject data = result.getJSONObject("data");
+            JSONArray excepts = result.getJSONArray("excepts");
+            
+            // 解析sku_list
+            JSONArray skuList = JSON.parseArray(skuListStr);
+            
+            // 解析resource_info获取视频URL
+            JSONObject resourceInfo = JSON.parseObject(resourceInfoStr);
+            String videoUrl = resourceInfo.getString("video_url");
+            
+            log.info("识别类型: {}, 商品数量: {}, 视频: {}", type, skuList.size(), videoUrl);
+            
+            // 检查是否有异常
+            if (excepts != null && !excepts.isEmpty()) {
+                log.warn("识别结果包含异常: {}", excepts.toJSONString());
+            }
+            
+            // TODO: 根据识别结果进行后续处理
+            // 1. 计算订单金额
+            // 2. 生成订单记录
+            // 3. 触发支付流程
+            
+        } catch (Exception e) {
+            log.error("处理AI识别结果通知失败", e);
+        }
+    }
+
+    /**
+     * 验证签名
+     * 
+     * @param params 回调参数
+     * @throws Exception 签名验证失败
+     */
+    private void validateSign(Map<String, Object> params) throws Exception {
+        String receivedSign = (String) params.get("sign");
+        if (receivedSign == null || receivedSign.isEmpty()) {
+            throw new Exception("签名参数为空");
+        }
+        
+        // TODO: 实现签名验证逻辑
+        // 1. 提取所有参数(除sign外)
+        // 2. 按字典序排序
+        // 3. 拼接 + AppSecret
+        // 4. MD5加密
+        // 5. 比对签名
+    }
+}

+ 178 - 0
haha-miniapp/src/main/java/com/haha/miniapp/controller/DeviceController.java

@@ -0,0 +1,178 @@
+package com.haha.miniapp.controller;
+
+import cn.dev33.satoken.stp.StpUtil;
+import com.haha.miniapp.entity.Order;
+import com.haha.miniapp.service.OrderService;
+import com.haha.sdk.HahaClient;
+import com.haha.sdk.exception.HahaException;
+import com.haha.sdk.model.DeviceOnlineStatus;
+import com.haha.sdk.model.OpenDoorResult;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.time.LocalDateTime;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 设备控制相关接口
+ * 基于新版哈哈零售SDK重新实现
+ */
+@Slf4j
+@RestController
+@RequestMapping("/api/device")
+public class DeviceController {
+
+    @Autowired
+    private HahaClient hahaClient;
+    
+    @Autowired
+    private OrderService orderService;
+
+    /**
+     * 处理小程序扫码开门
+     * 
+     * 业务流程:
+     * 1. 参数校验
+     * 2. 用户身份验证
+     * 3. 检查设备在线状态
+     * 4. 检查设备是否多门单开
+     * 5. 调用SDK开门接口
+     * 6. 返回开门结果
+     *
+     * @param params 包含 deviceId(设备ID)
+     * @return 操作结果
+     */
+    @PostMapping("/scan-open")
+    public Map<String, Object> scanOpen(@RequestBody Map<String, String> params) {
+        Map<String, Object> result = new HashMap<>();
+        
+        try {
+            // ========== 1. 参数校验 ==========
+            String deviceId = params.get("deviceId");
+            if (deviceId == null || deviceId.trim().isEmpty()) {
+                result.put("code", 400);
+                result.put("message", "参数错误:deviceId 不能为空");
+                return result;
+            }
+
+            // ========== 2. 获取用户信息 ==========
+            Long userId = StpUtil.getLoginIdAsLong();
+            log.info("用户 {} 请求打开设备 {}", userId, deviceId);
+
+            // ========== 3. 检查设备在线状态 ==========
+            DeviceOnlineStatus onlineStatus = hahaClient.getDeviceApi().getOnlineStatus(deviceId);
+            
+            if (onlineStatus.getOnlineStatus() != 1) {
+                log.warn("设备 {} 当前离线,最后在线时间: {}", deviceId, onlineStatus.getLastOnlineTime());
+                result.put("code", 400);
+                result.put("message", "设备当前离线,请稍后再试");
+                result.put("data", Map.of(
+                    "online", false,
+                    "lastOnlineTime", onlineStatus.getLastOnlineTime()
+                ));
+                return result;
+            }
+
+            // ========== 4. 检查是否多门单开 ==========
+            Integer multiDoorType = hahaClient.getDeviceApi().isMultiDoorUnique(deviceId);
+            Integer doorIndex = null;
+            
+            // 如果是多门单开设备,默认打开A门(索引0)
+            if (multiDoorType != null && multiDoorType > 0) {
+                doorIndex = 0; // 0-A门, 1-B门
+                log.info("设备 {} 是多门单开设备,类型: {},将打开A门", deviceId, multiDoorType);
+            }
+
+            // ========== 5. 生成商户订单号并开门 ==========
+            String outTradeNo = "ORDER_" + userId + "_" + System.currentTimeMillis();
+            
+            OpenDoorResult openResult = hahaClient.getDeviceApi()
+                .openDoor(deviceId, outTradeNo, doorIndex);
+
+            // ========== 6. 创建本地订单记录 ==========
+            Order order = new Order();
+            order.setOutTradeNo(outTradeNo);
+            order.setHahaOrderNo(openResult.getOrderNo());
+            order.setUserId(userId);
+            order.setDeviceSn(deviceId);
+            order.setStatus(0); // 0-待支付(等待识别结果)
+            order.setPayStatus("pending");
+            order.setCreateTime(LocalDateTime.now());
+            
+            boolean saved = orderService.save(order);
+            if (!saved) {
+                log.warn("订单 {} 保存失败,但开门已成功", outTradeNo);
+            }
+            
+            // ========== 7. 返回成功结果 ==========
+            log.info("开门成功 - 设备: {}, 商户订单号: {}, 哈哈订单号: {}", 
+                deviceId, outTradeNo, openResult.getOrderNo());
+            
+            result.put("code", 200);
+            result.put("message", "开门成功");
+            result.put("data", Map.of(
+                "doorOpened", true,
+                "outTradeNo", outTradeNo,
+                "orderNo", openResult.getOrderNo(),
+                "deviceId", deviceId,
+                "doorIndex", doorIndex != null ? doorIndex : 0
+            ));
+
+        } catch (HahaException e) {
+            // 处理SDK异常
+            Integer errorCode = e.getErrorCode();
+            String errorMsg = e.getMessage();
+            
+            log.error("开门失败 - 错误码: {}, 错误信息: {}", errorCode, errorMsg, e);
+            
+            // 根据错误码返回具体的错误信息
+            String message = getErrorMessage(errorCode, errorMsg);
+            
+            result.put("code", 500);
+            result.put("message", message);
+            result.put("errorCode", errorCode);
+            
+        } catch (Exception e) {
+            // 处理其他异常
+            log.error("开门异常", e);
+            result.put("code", 500);
+            result.put("message", "系统繁忙,请稍后重试");
+        }
+        
+        return result;
+    }
+
+    /**
+     * 根据错误码返回友好的错误信息
+     */
+    private String getErrorMessage(Integer errorCode, String defaultMsg) {
+        if (errorCode == null) {
+            return defaultMsg != null ? defaultMsg : "操作失败";
+        }
+        
+        switch (errorCode) {
+            case -1:
+                return "参数错误,请检查设备ID是否正确";
+            case -2:
+                return "签名错误,请联系技术支持";
+            case -3:
+            case -4:
+                return "认证失败,请稍后重试";
+            case -100:
+                return "设备不存在,请确认设备ID";
+            case -101:
+                return "设备离线,请稍后再试";
+            case -102:
+                return "设备故障,请联系设备维护人员";
+            case -103:
+                return "设备使用中,请稍后再试";
+            default:
+                return defaultMsg != null ? defaultMsg : "开门失败";
+        }
+    }
+}

+ 21 - 0
haha-miniapp/src/main/java/com/haha/miniapp/controller/HealthController.java

@@ -0,0 +1,21 @@
+package com.haha.miniapp.controller;
+
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/health")
+public class HealthController {
+
+    @GetMapping("/check")
+    public Map<String, Object> healthCheck() {
+        Map<String, Object> result = new HashMap<>();
+        result.put("status", "ok");
+        result.put("message", "服务运行正常");
+        return result;
+    }
+}

+ 53 - 0
haha-miniapp/src/main/java/com/haha/miniapp/controller/LoginController.java

@@ -0,0 +1,53 @@
+package com.haha.miniapp.controller;
+
+import cn.dev33.satoken.stp.StpUtil;
+import com.haha.miniapp.service.LoginService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/login")
+public class LoginController {
+    
+    @Autowired
+    private LoginService loginService;
+    
+    /**
+     * 微信手机号快捷登录
+     * @param params 包含code和encryptedData、iv的参数
+     * @return 登录结果,包含token和用户信息
+     */
+    @PostMapping("/wechat-phone")
+    public Map<String, Object> wechatPhoneLogin(@RequestBody Map<String, String> params) {
+        String code = params.get("code");
+        String encryptedData = params.get("encryptedData");
+        String iv = params.get("iv");
+        
+        return loginService.wechatPhoneLogin(code, encryptedData, iv);
+    }
+    
+    /**
+     * 退出登录
+     * @return 退出结果
+     */
+    @PostMapping("/logout")
+    public Map<String, Object> logout() {
+        StpUtil.logout();
+        return Map.of("code", 200, "message", "退出登录成功");
+    }
+    
+    /**
+     * 获取当前登录用户信息
+     * @return 用户信息
+     */
+    @PostMapping("/user-info")
+    public Map<String, Object> getUserInfo() {
+        Object userId = StpUtil.getLoginId();
+        return loginService.getUserInfo(userId.toString());
+    }
+}

+ 323 - 0
haha-miniapp/src/main/java/com/haha/miniapp/controller/OrderController.java

@@ -0,0 +1,323 @@
+package com.haha.miniapp.controller;
+
+import cn.dev33.satoken.stp.StpUtil;
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONArray;
+import com.alibaba.fastjson2.JSONObject;
+import com.haha.miniapp.entity.Order;
+import com.haha.miniapp.service.OrderService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 订单相关接口
+ */
+@Slf4j
+@RestController
+@RequestMapping("/api/order")
+public class OrderController {
+
+    @Autowired
+    private OrderService orderService;
+
+    /**
+     * 获取订单列表
+     * 
+     * 业务流程:
+     * 1. 获取当前登录用户ID
+     * 2. 根据状态筛选查询订单列表
+     * 3. 解析订单商品信息
+     * 4. 返回订单列表
+     *
+     * @param params 包含 status(可选,订单状态:0-待支付,1-已完成,2-已取消)
+     * @return 订单列表
+     */
+    @PostMapping("/list")
+    public Map<String, Object> getOrderList(@RequestBody(required = false) Map<String, Object> params) {
+        Map<String, Object> result = new HashMap<>();
+        
+        try {
+            // 获取当前登录用户ID
+            Long userId = StpUtil.getLoginIdAsLong();
+            log.info("用户 {} 请求订单列表", userId);
+            
+            // 获取状态参数
+            Integer status = null;
+            if (params != null && params.containsKey("status")) {
+                status = Integer.valueOf(params.get("status").toString());
+            }
+            
+            // 查询订单列表
+            List<Order> orders = orderService.getOrderListByUserId(userId, status);
+            
+            // 转换为前端需要的格式
+            List<Map<String, Object>> orderList = new ArrayList<>();
+            for (Order order : orders) {
+                Map<String, Object> orderMap = new HashMap<>();
+                orderMap.put("id", order.getId());
+                orderMap.put("orderNo", order.getOrderNo());
+                orderMap.put("outTradeNo", order.getOutTradeNo());
+                orderMap.put("hahaOrderNo", order.getHahaOrderNo());
+                orderMap.put("deviceSn", order.getDeviceSn());
+                orderMap.put("totalAmount", order.getTotalAmount());
+                orderMap.put("payStatus", order.getPayStatus());
+                orderMap.put("status", order.getStatus());
+                orderMap.put("createTime", order.getCreateTime());
+                orderMap.put("payTime", order.getPayTime());
+                orderMap.put("videoUrl", order.getVideoUrl());
+                orderMap.put("confidence", order.getConfidence());
+                
+                // 解析商品信息
+                if (order.getItemsJson() != null && !order.getItemsJson().isEmpty()) {
+                    try {
+                        JSONArray items = JSON.parseArray(order.getItemsJson());
+                        List<Map<String, Object>> products = new ArrayList<>();
+                        for (int i = 0; i < items.size(); i++) {
+                            JSONObject item = items.getJSONObject(i);
+                            Map<String, Object> product = new HashMap<>();
+                            product.put("id", item.getString("goodsId"));
+                            product.put("name", item.getString("goodsName"));
+                            product.put("price", item.getDouble("price"));
+                            product.put("quantity", item.getInteger("quantity"));
+                            product.put("image", item.getString("image"));
+                            products.add(product);
+                        }
+                        orderMap.put("products", products);
+                    } catch (Exception e) {
+                        log.warn("解析订单 {} 的商品信息失败: {}", order.getId(), e.getMessage());
+                        orderMap.put("products", new ArrayList<>());
+                    }
+                } else {
+                    orderMap.put("products", new ArrayList<>());
+                }
+                
+                orderList.add(orderMap);
+            }
+            
+            result.put("code", 200);
+            result.put("message", "查询成功");
+            result.put("data", orderList);
+            
+            log.info("用户 {} 查询到 {} 条订单", userId, orderList.size());
+            
+        } catch (Exception e) {
+            log.error("查询订单列表失败", e);
+            result.put("code", 500);
+            result.put("message", "查询订单列表失败");
+        }
+        
+        return result;
+    }
+
+    /**
+     * 获取订单详情
+     * 
+     * 业务流程:
+     * 1. 参数校验
+     * 2. 获取当前登录用户ID
+     * 3. 查询订单详情
+     * 4. 验证订单归属
+     * 5. 解析订单商品信息
+     * 6. 返回订单详情
+     *
+     * @param params 包含 orderId(订单ID)或 orderNo(订单号)或 outTradeNo(商户订单号)
+     * @return 订单详情
+     */
+    @PostMapping("/detail")
+    public Map<String, Object> getOrderDetail(@RequestBody Map<String, Object> params) {
+        Map<String, Object> result = new HashMap<>();
+        
+        try {
+            // 获取当前登录用户ID
+            Long userId = StpUtil.getLoginIdAsLong();
+            
+            // 查询订单
+            Order order = null;
+            
+            // 支持三种查询方式
+            if (params.containsKey("orderId")) {
+                Long orderId = Long.valueOf(params.get("orderId").toString());
+                order = orderService.getById(orderId);
+                log.info("用户 {} 通过订单ID {} 查询订单详情", userId, orderId);
+            } else if (params.containsKey("orderNo")) {
+                String orderNo = params.get("orderNo").toString();
+                order = orderService.lambdaQuery()
+                    .eq(Order::getOrderNo, orderNo)
+                    .one();
+                log.info("用户 {} 通过订单号 {} 查询订单详情", userId, orderNo);
+            } else if (params.containsKey("outTradeNo")) {
+                String outTradeNo = params.get("outTradeNo").toString();
+                order = orderService.getOrderByOutTradeNo(outTradeNo);
+                log.info("用户 {} 通过商户订单号 {} 查询订单详情", userId, outTradeNo);
+            } else {
+                result.put("code", 400);
+                result.put("message", "参数错误:必须提供 orderId、orderNo 或 outTradeNo");
+                return result;
+            }
+            
+            // 检查订单是否存在
+            if (order == null) {
+                result.put("code", 404);
+                result.put("message", "订单不存在");
+                return result;
+            }
+            
+            // 验证订单归属
+            if (!order.getUserId().equals(userId)) {
+                log.warn("用户 {} 尝试访问不属于自己的订单 {}", userId, order.getId());
+                result.put("code", 403);
+                result.put("message", "无权访问该订单");
+                return result;
+            }
+            
+            // 构建订单详情
+            Map<String, Object> orderDetail = new HashMap<>();
+            orderDetail.put("id", order.getId());
+            orderDetail.put("orderNo", order.getOrderNo());
+            orderDetail.put("outTradeNo", order.getOutTradeNo());
+            orderDetail.put("hahaOrderNo", order.getHahaOrderNo());
+            orderDetail.put("deviceSn", order.getDeviceSn());
+            orderDetail.put("totalAmount", order.getTotalAmount());
+            orderDetail.put("payStatus", order.getPayStatus());
+            orderDetail.put("status", order.getStatus());
+            orderDetail.put("createTime", order.getCreateTime());
+            orderDetail.put("payTime", order.getPayTime());
+            orderDetail.put("videoUrl", order.getVideoUrl());
+            orderDetail.put("confidence", order.getConfidence());
+            
+            // 解析商品信息
+            if (order.getItemsJson() != null && !order.getItemsJson().isEmpty()) {
+                try {
+                    JSONArray items = JSON.parseArray(order.getItemsJson());
+                    List<Map<String, Object>> products = new ArrayList<>();
+                    for (int i = 0; i < items.size(); i++) {
+                        JSONObject item = items.getJSONObject(i);
+                        Map<String, Object> product = new HashMap<>();
+                        product.put("id", item.getString("goodsId"));
+                        product.put("name", item.getString("goodsName"));
+                        product.put("price", item.getDouble("price"));
+                        product.put("quantity", item.getInteger("quantity"));
+                        product.put("image", item.getString("image"));
+                        product.put("subtotal", item.getDouble("price") * item.getInteger("quantity"));
+                        products.add(product);
+                    }
+                    orderDetail.put("products", products);
+                } catch (Exception e) {
+                    log.warn("解析订单 {} 的商品信息失败: {}", order.getId(), e.getMessage());
+                    orderDetail.put("products", new ArrayList<>());
+                }
+            } else {
+                orderDetail.put("products", new ArrayList<>());
+            }
+            
+            // 返回状态文本
+            String statusText = getStatusText(order.getStatus());
+            orderDetail.put("statusText", statusText);
+            
+            result.put("code", 200);
+            result.put("message", "查询成功");
+            result.put("data", orderDetail);
+            
+            log.info("用户 {} 查询订单详情成功: {}", userId, order.getOrderNo());
+            
+        } catch (Exception e) {
+            log.error("查询订单详情失败", e);
+            result.put("code", 500);
+            result.put("message", "查询订单详情失败");
+        }
+        
+        return result;
+    }
+
+    /**
+     * 取消订单
+     * 
+     * @param params 包含 orderId(订单ID)
+     * @return 操作结果
+     */
+    @PostMapping("/cancel")
+    public Map<String, Object> cancelOrder(@RequestBody Map<String, Object> params) {
+        Map<String, Object> result = new HashMap<>();
+        
+        try {
+            // 获取当前登录用户ID
+            Long userId = StpUtil.getLoginIdAsLong();
+            
+            // 参数校验
+            if (!params.containsKey("orderId")) {
+                result.put("code", 400);
+                result.put("message", "参数错误:orderId 不能为空");
+                return result;
+            }
+            
+            Long orderId = Long.valueOf(params.get("orderId").toString());
+            
+            // 查询订单
+            Order order = orderService.getById(orderId);
+            if (order == null) {
+                result.put("code", 404);
+                result.put("message", "订单不存在");
+                return result;
+            }
+            
+            // 验证订单归属
+            if (!order.getUserId().equals(userId)) {
+                log.warn("用户 {} 尝试取消不属于自己的订单 {}", userId, orderId);
+                result.put("code", 403);
+                result.put("message", "无权操作该订单");
+                return result;
+            }
+            
+            // 检查订单状态(只能取消待支付的订单)
+            if (order.getStatus() != 0) {
+                result.put("code", 400);
+                result.put("message", "该订单不能取消");
+                return result;
+            }
+            
+            // 取消订单
+            boolean success = orderService.cancelOrder(orderId);
+            
+            if (success) {
+                result.put("code", 200);
+                result.put("message", "订单已取消");
+                log.info("用户 {} 取消订单 {} 成功", userId, orderId);
+            } else {
+                result.put("code", 500);
+                result.put("message", "取消订单失败");
+            }
+            
+        } catch (Exception e) {
+            log.error("取消订单失败", e);
+            result.put("code", 500);
+            result.put("message", "取消订单失败");
+        }
+        
+        return result;
+    }
+
+    /**
+     * 获取订单状态文本
+     */
+    private String getStatusText(Integer status) {
+        if (status == null) {
+            return "未知";
+        }
+        switch (status) {
+            case 0:
+                return "待支付";
+            case 1:
+                return "已完成";
+            case 2:
+                return "已取消";
+            default:
+                return "未知";
+        }
+    }
+}

+ 34 - 0
haha-miniapp/src/main/java/com/haha/miniapp/entity/Account.java

@@ -0,0 +1,34 @@
+package com.haha.miniapp.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Data
+@TableName("t_account")
+public class Account implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long userId;
+
+    private BigDecimal balance;
+
+    private BigDecimal frozenAmount;
+
+    private BigDecimal totalRecharge;
+
+    private BigDecimal totalConsume;
+
+    private Integer points;
+
+    private LocalDateTime createTime;
+
+    private LocalDateTime updateTime;
+}

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

@@ -0,0 +1,29 @@
+package com.haha.miniapp.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+@Data
+@TableName("t_device")
+public class Device implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private String deviceSn;
+
+    private Long shopId;
+
+    private String name;
+
+    private String authToken;
+
+    private Integer status;
+
+    private String currentInventoryHash;
+}

+ 37 - 0
haha-miniapp/src/main/java/com/haha/miniapp/entity/FundLog.java

@@ -0,0 +1,37 @@
+package com.haha.miniapp.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Data
+@TableName("t_fund_log")
+public class FundLog implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long userId;
+
+    /**
+     * 交易类型:1-充值,2-消费,3-退款,4-提现
+     */
+    private Integer type;
+
+    private BigDecimal amount;
+
+    private BigDecimal balanceBefore;
+
+    private BigDecimal balanceAfter;
+
+    private String orderNo;
+
+    private String remark;
+
+    private LocalDateTime createTime;
+}

+ 43 - 0
haha-miniapp/src/main/java/com/haha/miniapp/entity/Order.java

@@ -0,0 +1,43 @@
+package com.haha.miniapp.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+@Data
+@TableName("t_order")
+public class Order implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private String orderNo;
+
+    private String outTradeNo;
+
+    private String hahaOrderNo;
+
+    private Long userId;
+
+    private String deviceSn;
+
+    private Double totalAmount;
+
+    private String payStatus;
+
+    private String videoUrl;
+
+    private Double confidence;
+
+    private String itemsJson;
+
+    private LocalDateTime createTime;
+
+    private LocalDateTime payTime;
+
+    private Integer status;
+}

+ 33 - 0
haha-miniapp/src/main/java/com/haha/miniapp/entity/Product.java

@@ -0,0 +1,33 @@
+package com.haha.miniapp.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+@Data
+@TableName("t_product")
+public class Product implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private String barcode;
+
+    private String name;
+
+    private Double price;
+
+    private String imageUrl;
+
+    private Integer hahaSyncStatus;
+
+    private String hahaModelId;
+
+    private Integer isDeleted;
+
+    private LocalDateTime createTime;
+}

+ 33 - 0
haha-miniapp/src/main/java/com/haha/miniapp/entity/User.java

@@ -0,0 +1,33 @@
+package com.haha.miniapp.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+@Data
+@TableName("t_user")
+public class User implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private String openid;
+
+    private String phone;
+
+    private String nickname;
+
+    private String avatar;
+
+    private Integer creditScore;
+
+    private Integer status;
+
+    private LocalDateTime createTime;
+
+    private LocalDateTime updateTime;
+}

+ 7 - 0
haha-miniapp/src/main/java/com/haha/miniapp/mapper/AccountMapper.java

@@ -0,0 +1,7 @@
+package com.haha.miniapp.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.haha.miniapp.entity.Account;
+
+public interface AccountMapper extends BaseMapper<Account> {
+}

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

@@ -0,0 +1,7 @@
+package com.haha.miniapp.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.haha.miniapp.entity.Device;
+
+public interface DeviceMapper extends BaseMapper<Device> {
+}

+ 7 - 0
haha-miniapp/src/main/java/com/haha/miniapp/mapper/FundLogMapper.java

@@ -0,0 +1,7 @@
+package com.haha.miniapp.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.haha.miniapp.entity.FundLog;
+
+public interface FundLogMapper extends BaseMapper<FundLog> {
+}

+ 7 - 0
haha-miniapp/src/main/java/com/haha/miniapp/mapper/OrderMapper.java

@@ -0,0 +1,7 @@
+package com.haha.miniapp.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.haha.miniapp.entity.Order;
+
+public interface OrderMapper extends BaseMapper<Order> {
+}

+ 7 - 0
haha-miniapp/src/main/java/com/haha/miniapp/mapper/ProductMapper.java

@@ -0,0 +1,7 @@
+package com.haha.miniapp.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.haha.miniapp.entity.Product;
+
+public interface ProductMapper extends BaseMapper<Product> {
+}

+ 7 - 0
haha-miniapp/src/main/java/com/haha/miniapp/mapper/UserMapper.java

@@ -0,0 +1,7 @@
+package com.haha.miniapp.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.haha.miniapp.entity.User;
+
+public interface UserMapper extends BaseMapper<User> {
+}

+ 10 - 0
haha-miniapp/src/main/java/com/haha/miniapp/service/AccountService.java

@@ -0,0 +1,10 @@
+package com.haha.miniapp.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.haha.miniapp.entity.Account;
+import java.math.BigDecimal;
+
+public interface AccountService extends IService<Account> {
+    Account getByUserId(Long userId);
+    boolean updateBalance(Long userId, BigDecimal amount);
+}

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

@@ -0,0 +1,13 @@
+package com.haha.miniapp.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.haha.miniapp.entity.Device;
+import java.util.List;
+
+public interface DeviceService extends IService<Device> {
+    List<Device> getNearbyDevices(Double longitude, Double latitude, Double distance);
+    Device getDeviceBySn(String deviceSn);
+    boolean updateDeviceStatus(String deviceSn, Integer status);
+    List<Device> getDeviceListByArea(String province, String city, String district);
+    boolean updateInventoryHash(String deviceSn, String inventoryHash);
+}

+ 22 - 0
haha-miniapp/src/main/java/com/haha/miniapp/service/LoginService.java

@@ -0,0 +1,22 @@
+package com.haha.miniapp.service;
+
+import java.util.Map;
+
+public interface LoginService {
+    
+    /**
+     * 微信手机号快捷登录
+     * @param code 微信登录凭证code
+     * @param encryptedData 加密的手机号数据
+     * @param iv 加密算法的初始向量
+     * @return 登录结果,包含token和用户信息
+     */
+    Map<String, Object> wechatPhoneLogin(String code, String encryptedData, String iv);
+    
+    /**
+     * 根据用户ID获取用户信息
+     * @param userId 用户ID
+     * @return 用户信息
+     */
+    Map<String, Object> getUserInfo(String userId);
+}

+ 14 - 0
haha-miniapp/src/main/java/com/haha/miniapp/service/OrderService.java

@@ -0,0 +1,14 @@
+package com.haha.miniapp.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.haha.miniapp.entity.Order;
+import java.util.List;
+
+public interface OrderService extends IService<Order> {
+    List<Order> getOrderListByUserId(Long userId, Integer status);
+    boolean cancelOrder(Long orderId);
+    boolean updateOrderStatus(Long orderId, Integer status);
+    Order getOrderByOutTradeNo(String outTradeNo);
+    Order getOrderByDeviceSn(String deviceSn);
+    boolean updatePayStatus(String orderNo, String payStatus);
+}

+ 14 - 0
haha-miniapp/src/main/java/com/haha/miniapp/service/ProductService.java

@@ -0,0 +1,14 @@
+package com.haha.miniapp.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.haha.miniapp.entity.Product;
+import java.util.List;
+
+public interface ProductService extends IService<Product> {
+    List<Product> getProductListByDeviceSn(String deviceSn);
+    boolean updateProductStock(String deviceSn, Long productId, Integer stock);
+    List<Product> getProductListByCategory(Integer categoryId);
+    Product getProductByBarcode(String barcode);
+    boolean updateSyncStatus(Long productId, Integer syncStatus);
+    boolean updateHahaModelId(Long productId, String modelId);
+}

+ 12 - 0
haha-miniapp/src/main/java/com/haha/miniapp/service/UserService.java

@@ -0,0 +1,12 @@
+package com.haha.miniapp.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.haha.miniapp.entity.User;
+import java.math.BigDecimal;
+
+public interface UserService extends IService<User> {
+    User getUserByOpenId(String openId);
+    User getUserByPhone(String phone);
+    boolean updateCreditScore(Long userId, Integer score);
+    boolean updateBalance(Long userId, BigDecimal amount);
+}

+ 27 - 0
haha-miniapp/src/main/java/com/haha/miniapp/service/impl/AccountServiceImpl.java

@@ -0,0 +1,27 @@
+package com.haha.miniapp.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.haha.miniapp.entity.Account;
+import com.haha.miniapp.mapper.AccountMapper;
+import com.haha.miniapp.service.AccountService;
+import org.springframework.stereotype.Service;
+import java.math.BigDecimal;
+
+@Service
+public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> implements AccountService {
+
+    @Override
+    public Account getByUserId(Long userId) {
+        return lambdaQuery().eq(Account::getUserId, userId).one();
+    }
+
+    @Override
+    public boolean updateBalance(Long userId, BigDecimal amount) {
+        Account account = getByUserId(userId);
+        if (account != null) {
+            BigDecimal newBalance = account.getBalance().add(amount);
+            return lambdaUpdate().eq(Account::getUserId, userId).set(Account::getBalance, newBalance).update();
+        }
+        return false;
+    }
+}

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

@@ -0,0 +1,40 @@
+package com.haha.miniapp.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.haha.miniapp.entity.Device;
+import com.haha.miniapp.mapper.DeviceMapper;
+import com.haha.miniapp.service.DeviceService;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+public class DeviceServiceImpl extends ServiceImpl<DeviceMapper, Device> implements DeviceService {
+
+    @Override
+    public List<Device> getNearbyDevices(Double longitude, Double latitude, Double distance) {
+        // 这里需要实现获取附近设备的逻辑,暂时返回所有设备
+        return list();
+    }
+
+    @Override
+    public Device getDeviceBySn(String deviceSn) {
+        return lambdaQuery().eq(Device::getDeviceSn, deviceSn).one();
+    }
+
+    @Override
+    public boolean updateDeviceStatus(String deviceSn, Integer status) {
+        return lambdaUpdate().eq(Device::getDeviceSn, deviceSn).set(Device::getStatus, status).update();
+    }
+
+    @Override
+    public List<Device> getDeviceListByArea(String province, String city, String district) {
+        // 这里需要实现根据区域获取设备列表的逻辑,暂时返回所有设备
+        return list();
+    }
+
+    @Override
+    public boolean updateInventoryHash(String deviceSn, String inventoryHash) {
+        return lambdaUpdate().eq(Device::getDeviceSn, deviceSn).set(Device::getCurrentInventoryHash, inventoryHash).update();
+    }
+}

+ 104 - 0
haha-miniapp/src/main/java/com/haha/miniapp/service/impl/LoginServiceImpl.java

@@ -0,0 +1,104 @@
+package com.haha.miniapp.service.impl;
+
+import cn.dev33.satoken.stp.StpUtil;
+import com.haha.miniapp.entity.User;
+import com.haha.miniapp.service.LoginService;
+import com.haha.miniapp.service.UserService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@Service
+public class LoginServiceImpl implements LoginService {
+    
+    @Autowired
+    private UserService userService;
+    
+    @Override
+    public Map<String, Object> wechatPhoneLogin(String code, String encryptedData, String iv) {
+        try {
+            // 暂时简化实现,模拟微信手机号登录
+            // 实际项目中需要使用微信小程序SDK解密获取手机号
+            String phoneNumber = "13800138000";
+            String openid = "mock_openid_" + System.currentTimeMillis();
+            
+            // 根据手机号查找或创建用户
+            User user = userService.getUserByPhone(phoneNumber);
+            if (user == null) {
+                // 创建新用户
+                user = new User();
+                user.setPhone(phoneNumber);
+                user.setOpenid(openid);
+                user.setNickname("用户" + phoneNumber.substring(7));
+                userService.save(user);
+            } else {
+                // 更新用户的openid
+                if (!openid.equals(user.getOpenid())) {
+                    user.setOpenid(openid);
+                    userService.updateById(user);
+                }
+            }
+            
+            // 使用Sa-Token进行登录,生成token
+            StpUtil.login(user.getId());
+            String token = StpUtil.getTokenValue();
+            
+            // 返回登录结果
+            Map<String, Object> result = new HashMap<>();
+            result.put("code", 200);
+            result.put("message", "登录成功");
+            result.put("token", token);
+            
+            Map<String, Object> userInfo = new HashMap<>();
+            userInfo.put("id", user.getId());
+            userInfo.put("phone", user.getPhone());
+            userInfo.put("nickname", user.getNickname());
+            userInfo.put("openid", user.getOpenid());
+            result.put("userInfo", userInfo);
+            
+            return result;
+            
+        } catch (Exception e) {
+            e.printStackTrace();
+            Map<String, Object> result = new HashMap<>();
+            result.put("code", 500);
+            result.put("message", "登录失败:" + e.getMessage());
+            return result;
+        }
+    }
+    
+    @Override
+    public Map<String, Object> getUserInfo(String userId) {
+        try {
+            User user = userService.getById(userId);
+            if (user == null) {
+                Map<String, Object> result = new HashMap<>();
+                result.put("code", 404);
+                result.put("message", "用户不存在");
+                return result;
+            }
+            
+            Map<String, Object> result = new HashMap<>();
+            result.put("code", 200);
+            result.put("message", "获取用户信息成功");
+            
+            Map<String, Object> userInfo = new HashMap<>();
+            userInfo.put("id", user.getId());
+            userInfo.put("phone", user.getPhone());
+            userInfo.put("nickname", user.getNickname());
+            userInfo.put("openid", user.getOpenid());
+            result.put("userInfo", userInfo);
+            
+            return result;
+            
+        } catch (Exception e) {
+            e.printStackTrace();
+            Map<String, Object> result = new HashMap<>();
+            result.put("code", 500);
+            result.put("message", "获取用户信息失败:" + e.getMessage());
+            return result;
+        }
+    }
+}

+ 47 - 0
haha-miniapp/src/main/java/com/haha/miniapp/service/impl/OrderServiceImpl.java

@@ -0,0 +1,47 @@
+package com.haha.miniapp.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.haha.miniapp.entity.Order;
+import com.haha.miniapp.mapper.OrderMapper;
+import com.haha.miniapp.service.OrderService;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {
+
+    @Override
+    public List<Order> getOrderListByUserId(Long userId, Integer status) {
+        if (status != null) {
+            return lambdaQuery().eq(Order::getUserId, userId).eq(Order::getStatus, status).list();
+        } else {
+            return lambdaQuery().eq(Order::getUserId, userId).list();
+        }
+    }
+
+    @Override
+    public boolean cancelOrder(Long orderId) {
+        return lambdaUpdate().eq(Order::getId, orderId).set(Order::getStatus, 2).update();
+    }
+
+    @Override
+    public boolean updateOrderStatus(Long orderId, Integer status) {
+        return lambdaUpdate().eq(Order::getId, orderId).set(Order::getStatus, status).update();
+    }
+
+    @Override
+    public Order getOrderByOutTradeNo(String outTradeNo) {
+        return lambdaQuery().eq(Order::getOutTradeNo, outTradeNo).one();
+    }
+
+    @Override
+    public Order getOrderByDeviceSn(String deviceSn) {
+        return lambdaQuery().eq(Order::getDeviceSn, deviceSn).orderByDesc(Order::getCreateTime).one();
+    }
+
+    @Override
+    public boolean updatePayStatus(String orderNo, String payStatus) {
+        return lambdaUpdate().eq(Order::getOrderNo, orderNo).set(Order::getPayStatus, payStatus).update();
+    }
+}

+ 46 - 0
haha-miniapp/src/main/java/com/haha/miniapp/service/impl/ProductServiceImpl.java

@@ -0,0 +1,46 @@
+package com.haha.miniapp.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.haha.miniapp.entity.Product;
+import com.haha.miniapp.mapper.ProductMapper;
+import com.haha.miniapp.service.ProductService;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements ProductService {
+
+    @Override
+    public List<Product> getProductListByDeviceSn(String deviceSn) {
+        // 这里需要实现根据设备SN获取商品列表的逻辑,暂时返回所有商品
+        return list();
+    }
+
+    @Override
+    public boolean updateProductStock(String deviceSn, Long productId, Integer stock) {
+        // 这里需要实现更新商品库存的逻辑
+        return true;
+    }
+
+    @Override
+    public List<Product> getProductListByCategory(Integer categoryId) {
+        // 这里需要实现根据分类ID获取商品列表的逻辑,暂时返回所有商品
+        return list();
+    }
+
+    @Override
+    public Product getProductByBarcode(String barcode) {
+        return lambdaQuery().eq(Product::getBarcode, barcode).one();
+    }
+
+    @Override
+    public boolean updateSyncStatus(Long productId, Integer syncStatus) {
+        return lambdaUpdate().eq(Product::getId, productId).set(Product::getHahaSyncStatus, syncStatus).update();
+    }
+
+    @Override
+    public boolean updateHahaModelId(Long productId, String modelId) {
+        return lambdaUpdate().eq(Product::getId, productId).set(Product::getHahaModelId, modelId).update();
+    }
+}

+ 37 - 0
haha-miniapp/src/main/java/com/haha/miniapp/service/impl/UserServiceImpl.java

@@ -0,0 +1,37 @@
+package com.haha.miniapp.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.haha.miniapp.entity.User;
+import com.haha.miniapp.mapper.UserMapper;
+import com.haha.miniapp.service.UserService;
+import com.haha.miniapp.service.AccountService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import java.math.BigDecimal;
+
+@Service
+public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
+
+    @Autowired
+    private AccountService accountService;
+
+    @Override
+    public User getUserByOpenId(String openId) {
+        return lambdaQuery().eq(User::getOpenid, openId).one();
+    }
+
+    @Override
+    public User getUserByPhone(String phone) {
+        return lambdaQuery().eq(User::getPhone, phone).one();
+    }
+
+    @Override
+    public boolean updateCreditScore(Long userId, Integer score) {
+        return lambdaUpdate().eq(User::getId, userId).set(User::getCreditScore, score).update();
+    }
+
+    @Override
+    public boolean updateBalance(Long userId, BigDecimal amount) {
+        return accountService.updateBalance(userId, amount);
+    }
+}

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

@@ -0,0 +1,91 @@
+spring:
+  application:
+    name: haha-miniapp
+  
+  # 数据库配置
+  datasource:
+    url: jdbc:mysql://server.kuaiyuman.cn:3306/haha?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false
+    username: root
+    password: KuaiyuMan/*-
+    driver-class-name: com.mysql.cj.jdbc.Driver
+  
+  # Redis 配置
+  redis:
+    host: server.kuaiyuman.cn
+    port: 6379
+    password: KtXA^Zx!TZmLEy(@JjB@2(TVG0kdy5)&
+    database: 8
+    timeout: 10000
+    lettuce:
+      pool:
+        max-active: 8
+        max-wait: -1
+        max-idle: 8
+        min-idle: 0
+  
+  # 数据库初始化
+  sql:
+    init:
+      enabled: true
+      mode: always
+      schema-locations: classpath:sql/user.sql
+      encoding: UTF-8
+
+# MyBatis-Plus 配置
+mybatis-plus:
+  configuration:
+    map-underscore-to-camel-case: true
+  global-config:
+    enable-sql-runner: false
+
+# 服务器配置
+server:
+  port: 8080
+  servlet:
+    context-path: /api
+
+# 微信相关配置
+wechat:
+  # 支付配置
+  pay:
+    app-id: wx8888888888888888
+    mch-id: 1888888888
+    mch-key: 88888888888888888888888888888888
+    v3-api-key: 88888888888888888888888888888888
+    notify-url: http://localhost:8080/api/callback/wechat/pay
+  # 小程序配置
+  miniapp:
+    app-id: your_wechat_miniapp_appid
+    secret: your_wechat_miniapp_secret
+    token: your_wechat_miniapp_token
+    aes-key: your_wechat_miniapp_aes_key
+
+# 哈哈零兽配置
+haha:
+  api:
+    app-id: 2601051549145878
+    app-secret: 06e1be59332b00de0baad82002cdbcb5
+    base-url: https://api.hahalingshou.com/v1
+
+# Sa-Token 配置
+sa-token:
+  # token 名称(同时也是 cookie 名称)
+  token-name: haha-token-KuaiyuMan
+  # token 有效期(单位:秒)
+  timeout: 86400
+  # token 临时有效期(单位:秒)
+  activity-timeout: -1
+  # 是否允许同一账号多地同时登录
+  is-concurrent: true
+  # 同一账号最大登录数量
+  max-login-count: 10
+  # token 风格
+  token-style: uuid
+  # 是否输出操作日志
+  is-log: true
+
+# 日志配置
+logging:
+  level:
+    root: info
+    com.haha.miniapp: debug

+ 53 - 0
haha-miniapp/src/main/resources/sql/user.sql

@@ -0,0 +1,53 @@
+-- 创建用户表
+CREATE TABLE IF NOT EXISTS `t_user` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '用户ID',
+  `openid` VARCHAR(100) DEFAULT NULL COMMENT '微信小程序OpenID',
+  `phone` VARCHAR(20) DEFAULT NULL COMMENT '手机号',
+  `nickname` VARCHAR(50) DEFAULT NULL COMMENT '昵称',
+  `avatar` VARCHAR(255) DEFAULT NULL COMMENT '头像',
+  `credit_score` INT DEFAULT 100 COMMENT '信用分',
+  `status` INT DEFAULT 1 COMMENT '状态:1-正常,0-禁用',
+  `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_phone` (`phone`),
+  UNIQUE KEY `uk_openid` (`openid`)
+) ENGINE=InnoDB AUTO_INCREMENT=10000 DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
+
+-- 创建账户表
+CREATE TABLE IF NOT EXISTS `t_account` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `user_id` BIGINT NOT NULL COMMENT '用户ID',
+  `balance` DECIMAL(10,2) DEFAULT 0.00 COMMENT '可用余额',
+  `frozen_amount` DECIMAL(10,2) DEFAULT 0.00 COMMENT '冻结金额',
+  `total_recharge` DECIMAL(10,2) DEFAULT 0.00 COMMENT '累计充值',
+  `total_consume` DECIMAL(10,2) DEFAULT 0.00 COMMENT '累计消费',
+  `points` INT DEFAULT 0 COMMENT '积分',
+  `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_user_id` (`user_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='账户表';
+
+-- 创建资金流水表
+CREATE TABLE IF NOT EXISTS `t_fund_log` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `user_id` BIGINT NOT NULL COMMENT '用户ID',
+  `type` TINYINT NOT NULL COMMENT '交易类型:1-充值,2-消费,3-退款,4-提现',
+  `amount` DECIMAL(10,2) NOT NULL COMMENT '交易金额',
+  `balance_before` DECIMAL(10,2) NOT NULL COMMENT '交易前余额',
+  `balance_after` DECIMAL(10,2) NOT NULL COMMENT '交易后余额',
+  `order_no` VARCHAR(64) DEFAULT NULL COMMENT '关联订单号',
+  `remark` VARCHAR(255) DEFAULT NULL COMMENT '备注',
+  `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_user_id` (`user_id`),
+  KEY `idx_create_time` (`create_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='资金流水表';
+
+-- 插入测试数据
+INSERT IGNORE INTO `t_user` (`id`, `openid`, `phone`, `nickname`, `avatar`, `credit_score`, `status`) VALUES
+(10000, 'test_openid_1', '13800138001', '测试用户1', 'https://wx.qlogo.cn/mmopen/vi_32/default_avatar.png', 100, 1);
+
+INSERT IGNORE INTO `t_account` (`user_id`, `balance`, `points`) VALUES
+(10000, 100.00, 0);