Explorar el Código

智能柜项目提交

skyline hace 4 meses
padre
commit
7b09efc9d0
Se han modificado 36 ficheros con 1375 adiciones y 72 borrados
  1. 15 1
      haha-miniapp/pom.xml
  2. 51 0
      haha-miniapp/src/main/java/com/haha/miniapp/common/RedisConstants.java
  3. 40 0
      haha-miniapp/src/main/java/com/haha/miniapp/common/Result.java
  4. 41 0
      haha-miniapp/src/main/java/com/haha/miniapp/config/GlobalExceptionHandler.java
  5. 28 0
      haha-miniapp/src/main/java/com/haha/miniapp/config/PageHelperConfig.java
  6. 16 0
      haha-miniapp/src/main/java/com/haha/miniapp/config/RedisConfig.java
  7. 6 2
      haha-miniapp/src/main/java/com/haha/miniapp/config/SaTokenConfig.java
  8. 20 0
      haha-miniapp/src/main/java/com/haha/miniapp/config/SaTokenSetupConfig.java
  9. 3 1
      haha-miniapp/src/main/java/com/haha/miniapp/controller/CallbackController.java
  10. 2 2
      haha-miniapp/src/main/java/com/haha/miniapp/controller/DeviceController.java
  11. 2 0
      haha-miniapp/src/main/java/com/haha/miniapp/controller/HealthController.java
  12. 21 5
      haha-miniapp/src/main/java/com/haha/miniapp/controller/LoginController.java
  13. 1 1
      haha-miniapp/src/main/java/com/haha/miniapp/controller/OrderController.java
  14. 51 0
      haha-miniapp/src/main/java/com/haha/miniapp/service/HahaCallbackService.java
  15. 13 2
      haha-miniapp/src/main/java/com/haha/miniapp/service/LoginService.java
  16. 96 0
      haha-miniapp/src/main/java/com/haha/miniapp/service/RedisService.java
  17. 174 0
      haha-miniapp/src/main/java/com/haha/miniapp/service/impl/HahaCallbackServiceImpl.java
  18. 66 32
      haha-miniapp/src/main/java/com/haha/miniapp/service/impl/LoginServiceImpl.java
  19. 14 0
      haha-miniapp/src/main/java/com/haha/miniapp/vo/LoginVO.java
  20. 17 0
      haha-miniapp/src/main/java/com/haha/miniapp/vo/OpenDoorVO.java
  21. 16 0
      haha-miniapp/src/main/java/com/haha/miniapp/vo/OrderItemVO.java
  22. 26 0
      haha-miniapp/src/main/java/com/haha/miniapp/vo/OrderVO.java
  23. 16 0
      haha-miniapp/src/main/java/com/haha/miniapp/vo/UserVO.java
  24. 15 2
      haha-miniapp/src/main/resources/application.yml
  25. 45 0
      haha-miniapp/src/main/resources/logback-spring.xml
  26. 81 0
      haha-miniapp/src/main/resources/sql/order.sql
  27. 34 0
      haha-mp/src/App.vue
  28. 52 0
      haha-mp/src/api/user.ts
  29. 3 3
      haha-mp/src/manifest.json
  30. 8 0
      haha-mp/src/pages.json
  31. 2 2
      haha-mp/src/pages/index/index.vue
  32. 236 0
      haha-mp/src/pages/login/login.vue
  33. 61 7
      haha-mp/src/pages/orders/orders.vue
  34. 95 0
      haha-mp/src/utils/auth.ts
  35. 6 10
      haha-mp/src/utils/request.ts
  36. 2 2
      haha-sdk/pom.xml

+ 15 - 1
haha-miniapp/pom.xml

@@ -18,7 +18,7 @@
     <description>智能视觉售卖机系统 - 用户端小程序后端</description>
 
     <properties>
-        <java.version>17</java.version>
+        <java.version>21</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>
@@ -91,6 +91,13 @@
             <artifactId>sa-token-spring-boot3-starter</artifactId>
             <version>1.39.0</version>
         </dependency>
+        
+        <!-- Sa-Token 整合 Redis(使用 jackson 序列化) -->
+        <dependency>
+            <groupId>cn.dev33</groupId>
+            <artifactId>sa-token-redis-jackson</artifactId>
+            <version>1.39.0</version>
+        </dependency>
 
         <!-- 哈哈零售 SDK -->
         <dependency>
@@ -98,6 +105,13 @@
             <artifactId>haha-sdk</artifactId>
             <version>1.0.0</version>
         </dependency>
+
+        <!-- PageHelper 分页插件 -->
+        <dependency>
+            <groupId>com.github.pagehelper</groupId>
+            <artifactId>pagehelper</artifactId>
+            <version>5.3.3</version>
+        </dependency>
         
 
     </dependencies>

+ 51 - 0
haha-miniapp/src/main/java/com/haha/miniapp/common/RedisConstants.java

@@ -0,0 +1,51 @@
+package com.haha.miniapp.common;
+
+/**
+ * Redis 缓存键常量类
+ * 统一管理所有 Redis 缓存的键名
+ */
+public class RedisConstants {
+
+    /**
+     * Sa-Token 用户会话相关
+     */
+    public static final String SA_TOKEN_USER_KEY_PREFIX = "satoken:user:";
+    public static final String SA_TOKEN_USER_LOGIN_ID_KEY = "satoken:loginId:%s";
+    public static final String SA_TOKEN_USER_TOKEN_KEY = "satoken:token:%s";
+
+    /**
+     * 设备状态相关
+     */
+    public static final String DEVICE_STATUS_KEY_PREFIX = "device_status:";
+    public static final String DEVICE_STATUS_KEY = "device_status:%s";
+
+    /**
+     * 分布式锁相关
+     */
+    public static final String LOCK_DOOR_KEY_PREFIX = "lock:door:";
+    public static final String LOCK_DOOR_KEY = "lock:door:%s";
+
+    /**
+     * 临时订单相关
+     */
+    public static final String ORDER_TEMP_KEY_PREFIX = "order:temp:";
+    public static final String ORDER_TEMP_KEY = "order:temp:%s";
+
+    /**
+     * 用户信息缓存相关
+     */
+    public static final String USER_INFO_KEY_PREFIX = "user:info:";
+    public static final String USER_INFO_KEY = "user:info:%s";
+
+    /**
+     * 商品信息缓存相关
+     */
+    public static final String PRODUCT_INFO_KEY_PREFIX = "product:info:";
+    public static final String PRODUCT_INFO_KEY = "product:info:%s";
+
+    /**
+     * 设备信息缓存相关
+     */
+    public static final String DEVICE_INFO_KEY_PREFIX = "device:info:";
+    public static final String DEVICE_INFO_KEY = "device:info:%s";
+}

+ 40 - 0
haha-miniapp/src/main/java/com/haha/miniapp/common/Result.java

@@ -0,0 +1,40 @@
+package com.haha.miniapp.common;
+
+import lombok.Data;
+
+/**
+ * 统一接口返回对象
+ */
+@Data
+public class Result<T> {
+    private Integer code;
+    private String message;
+    private T data;
+
+    public static <T> Result<T> success(T data) {
+        Result<T> result = new Result<>();
+        result.setCode(200);
+        result.setMessage("操作成功");
+        result.setData(data);
+        return result;
+    }
+
+    public static <T> Result<T> success(String message, T data) {
+        Result<T> result = new Result<>();
+        result.setCode(200);
+        result.setMessage(message);
+        result.setData(data);
+        return result;
+    }
+
+    public static <T> Result<T> error(Integer code, String message) {
+        Result<T> result = new Result<>();
+        result.setCode(code);
+        result.setMessage(message);
+        return result;
+    }
+
+    public static <T> Result<T> error(String message) {
+        return error(500, message);
+    }
+}

+ 41 - 0
haha-miniapp/src/main/java/com/haha/miniapp/config/GlobalExceptionHandler.java

@@ -0,0 +1,41 @@
+package com.haha.miniapp.config;
+
+import cn.dev33.satoken.exception.NotLoginException;
+import com.haha.miniapp.common.Result;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+@Slf4j
+@RestControllerAdvice
+public class GlobalExceptionHandler {
+
+    /**
+     * 处理 Sa-Token 未登录异常
+     */
+    @ExceptionHandler(NotLoginException.class)
+    public Result<Void> handleNotLoginException(NotLoginException e) {
+        log.warn("未登录异常: {}", e.getMessage());
+        // 根据异常类型返回不同的错误信息
+        String message;
+        if (e.getType().equals(NotLoginException.NOT_TOKEN)) {
+            message = "未提供token";
+        } else if (e.getType().equals(NotLoginException.INVALID_TOKEN)) {
+            message = "token无效";
+        } else if (e.getType().equals(NotLoginException.TOKEN_TIMEOUT)) {
+            message = "token已过期";
+        } else {
+            message = "未登录";
+        }
+        return Result.error(401, message);
+    }
+
+    /**
+     * 处理其他所有异常
+     */
+    @ExceptionHandler(Exception.class)
+    public Result<Void> handleException(Exception e) {
+        log.error("系统异常: {}", e.getMessage(), e);
+        return Result.error(500, "系统内部错误");
+    }
+}

+ 28 - 0
haha-miniapp/src/main/java/com/haha/miniapp/config/PageHelperConfig.java

@@ -0,0 +1,28 @@
+package com.haha.miniapp.config;
+
+import com.github.pagehelper.PageInterceptor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import java.util.Properties;
+
+@Configuration
+public class PageHelperConfig {
+
+    @Bean
+    public PageInterceptor pageInterceptor() {
+        PageInterceptor pageInterceptor = new PageInterceptor();
+        Properties properties = new Properties();
+        // 数据库方言
+        properties.setProperty("helperDialect", "mysql");
+        // 分页合理化参数,默认false。当设置为true时,pageNum<=0时会查询第一页,pageNum>pages时会查询最后一页
+        properties.setProperty("reasonable", "true");
+        // 支持通过Mapper接口参数来传递分页参数
+        properties.setProperty("supportMethodsArguments", "true");
+        // 为了支持startPage(Object params)方法,增加了一个"params"参数来配置参数映射
+        properties.setProperty("params", "count=countSql");
+        // 总是返回PageInfo类型
+        properties.setProperty("returnPageInfo", "check");
+        pageInterceptor.setProperties(properties);
+        return pageInterceptor;
+    }
+}

+ 16 - 0
haha-miniapp/src/main/java/com/haha/miniapp/config/RedisConfig.java

@@ -0,0 +1,16 @@
+package com.haha.miniapp.config;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Redis 配置类
+ * Sa-Token 使用 Redis 作为持久化存储,通过 sa-token-redis-jackson 自动配置
+ * Redis 连接信息通过 application.yml 中的 spring.redis 配置
+ */
+@Configuration
+@Slf4j
+public class RedisConfig {
+    // Sa-Token 的 Redis 整合已由 sa-token-redis-jackson 依赖自动配置
+    // 无需额外配置,Spring Boot 会自动读取 application.yml 中的 Redis 配置
+}

+ 6 - 2
haha-miniapp/src/main/java/com/haha/miniapp/config/SaTokenConfig.java

@@ -13,10 +13,14 @@ public class SaTokenConfig implements WebMvcConfigurer {
     public void addInterceptors(InterceptorRegistry registry) {
         // 注册Sa-Token拦截器,拦截所有路径
         registry.addInterceptor(new SaInterceptor(handle -> {
-            // 登录认证:除了登录接口和健康检查接口,其他都需要登录
+            // 登录认证:除了指定的接口,其他都需要登录
             StpUtil.checkLogin();
         }))
         .addPathPatterns("/**")
-        .excludePathPatterns("/api/login/**", "/api/health/**");
+        .excludePathPatterns(
+            "/login/**",           // 登录接口(注意:不包含context-path)
+            "/health/**",          // 健康检查接口
+            "/callback/**"         // 第三方回调接口(包括微信支付、哈哈平台等)
+        );
     }
 }

+ 20 - 0
haha-miniapp/src/main/java/com/haha/miniapp/config/SaTokenSetupConfig.java

@@ -0,0 +1,20 @@
+package com.haha.miniapp.config;
+
+import cn.dev33.satoken.SaManager;
+import org.springframework.context.annotation.Configuration;
+
+import jakarta.annotation.PostConstruct;
+
+/**
+ * Sa-Token 初始化配置
+ * 确保Sa-Token配置正确加载
+ */
+@Configuration
+public class SaTokenSetupConfig {
+
+    @PostConstruct
+    public void initSaToken() {
+        // 输出Sa-Token版本等信息,用于确认配置生效
+        System.out.println("Sa-Token 配置已加载,当前配置超时时间为:" + SaManager.getConfig().getTimeout() + " 秒");
+    }
+}

+ 3 - 1
haha-miniapp/src/main/java/com/haha/miniapp/controller/CallbackController.java

@@ -1,5 +1,6 @@
 package com.haha.miniapp.controller;
 
+import cn.dev33.satoken.annotation.SaIgnore;
 import com.alibaba.fastjson2.JSON;
 import com.alibaba.fastjson2.JSONArray;
 import com.alibaba.fastjson2.JSONObject;
@@ -31,7 +32,8 @@ import java.util.Map;
  */
 @Slf4j
 @RestController
-@RequestMapping("/api/callback/haha")
+@RequestMapping("/callback/haha")
+@SaIgnore // 第三方回调接口,不需要token验证
 public class CallbackController {
 
     @Autowired

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

@@ -20,11 +20,11 @@ import java.util.Map;
 
 /**
  * 设备控制相关接口
- * 基于新版哈哈零售SDK重新实现
+ * 基于哈哈SDK实现
  */
 @Slf4j
 @RestController
-@RequestMapping("/api/device")
+@RequestMapping("/device")
 public class DeviceController {
 
     @Autowired

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

@@ -1,5 +1,6 @@
 package com.haha.miniapp.controller;
 
+import cn.dev33.satoken.annotation.SaIgnore;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
@@ -9,6 +10,7 @@ import java.util.Map;
 
 @RestController
 @RequestMapping("/health")
+@SaIgnore // 健康检查接口,不需要token验证
 public class HealthController {
 
     @GetMapping("/check")

+ 21 - 5
haha-miniapp/src/main/java/com/haha/miniapp/controller/LoginController.java

@@ -1,7 +1,10 @@
 package com.haha.miniapp.controller;
 
+import cn.dev33.satoken.annotation.SaIgnore;
 import cn.dev33.satoken.stp.StpUtil;
+import com.haha.miniapp.common.Result;
 import com.haha.miniapp.service.LoginService;
+import com.haha.miniapp.vo.LoginVO;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
@@ -11,19 +14,32 @@ import org.springframework.web.bind.annotation.RestController;
 import java.util.Map;
 
 @RestController
-@RequestMapping("/api/login")
+@RequestMapping("/login")
+@SaIgnore // 忽略当前类所有方法的登录校验
 public class LoginController {
     
     @Autowired
     private LoginService loginService;
     
+    /**
+     * 账号密码登录(临时方案)
+     * @param params 包含phone和password的参数
+     * @return 登录结果,包含token和用户信息
+     */
+    @PostMapping("/password")
+    public Result<LoginVO> loginByPassword(@RequestBody Map<String, String> params) {
+        String phone = params.get("phone");
+        String password = params.get("password");
+        return loginService.loginByPassword(phone, password);
+    }
+    
     /**
      * 微信手机号快捷登录
      * @param params 包含code和encryptedData、iv的参数
      * @return 登录结果,包含token和用户信息
      */
     @PostMapping("/wechat-phone")
-    public Map<String, Object> wechatPhoneLogin(@RequestBody Map<String, String> params) {
+    public Result<LoginVO> wechatPhoneLogin(@RequestBody Map<String, String> params) {
         String code = params.get("code");
         String encryptedData = params.get("encryptedData");
         String iv = params.get("iv");
@@ -36,9 +52,9 @@ public class LoginController {
      * @return 退出结果
      */
     @PostMapping("/logout")
-    public Map<String, Object> logout() {
+    public Result<Void> logout() {
         StpUtil.logout();
-        return Map.of("code", 200, "message", "退出登录成功");
+        return Result.success("退出登录成功", null);
     }
     
     /**
@@ -46,7 +62,7 @@ public class LoginController {
      * @return 用户信息
      */
     @PostMapping("/user-info")
-    public Map<String, Object> getUserInfo() {
+    public Result<Map<String, Object>> getUserInfo() {
         Object userId = StpUtil.getLoginId();
         return loginService.getUserInfo(userId.toString());
     }

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

@@ -20,7 +20,7 @@ import java.util.Map;
  */
 @Slf4j
 @RestController
-@RequestMapping("/api/order")
+@RequestMapping("/order")
 public class OrderController {
 
     @Autowired

+ 51 - 0
haha-miniapp/src/main/java/com/haha/miniapp/service/HahaCallbackService.java

@@ -0,0 +1,51 @@
+package com.haha.miniapp.service;
+
+import java.util.Map;
+
+/**
+ * 哈哈平台回调业务逻辑接口
+ */
+public interface HahaCallbackService {
+
+    /**
+     * 处理统一消息回调
+     * 
+     * @param params 回调原始参数
+     */
+    void handleMessage(Map<String, Object> params);
+
+    /**
+     * 处理开关门状态通知 (DEVICE_STATUS)
+     */
+    void handleDeviceStatus(Map<String, Object> params);
+
+    /**
+     * 处理设备在线状态通知 (ONLINE_STATUS)
+     */
+    void handleOnlineStatus(Map<String, Object> params);
+
+    /**
+     * 处理音量调节结果通知 (VOICE_RESULT)
+     */
+    void handleVoiceResult(Map<String, Object> params);
+
+    /**
+     * 处理新品审核结果回调 (CLIENT_NEW_PRODUCT)
+     */
+    void handleNewProductAudit(Map<String, Object> params);
+
+    /**
+     * 处理商品合并结果通知 (MERGE_PRODUCT)
+     */
+    void handleMergeProduct(Map<String, Object> params);
+
+    /**
+     * 处理AI识别结果通知 (ORC_RESULT)
+     */
+    void handleOrcResult(Map<String, Object> params);
+    
+    /**
+     * 验证回调签名
+     */
+    boolean validateSign(Map<String, Object> params);
+}

+ 13 - 2
haha-miniapp/src/main/java/com/haha/miniapp/service/LoginService.java

@@ -1,9 +1,20 @@
 package com.haha.miniapp.service;
 
+import com.haha.miniapp.common.Result;
+import com.haha.miniapp.vo.LoginVO;
+
 import java.util.Map;
 
 public interface LoginService {
     
+    /**
+     * 账号密码登录(临时方案)
+     * @param phone 手机号
+     * @param password 密码
+     * @return 登录结果,包含token和用户信息
+     */
+    Result<LoginVO> loginByPassword(String phone, String password);
+    
     /**
      * 微信手机号快捷登录
      * @param code 微信登录凭证code
@@ -11,12 +22,12 @@ public interface LoginService {
      * @param iv 加密算法的初始向量
      * @return 登录结果,包含token和用户信息
      */
-    Map<String, Object> wechatPhoneLogin(String code, String encryptedData, String iv);
+    Result<LoginVO> wechatPhoneLogin(String code, String encryptedData, String iv);
     
     /**
      * 根据用户ID获取用户信息
      * @param userId 用户ID
      * @return 用户信息
      */
-    Map<String, Object> getUserInfo(String userId);
+    Result<Map<String, Object>> getUserInfo(String userId);
 }

+ 96 - 0
haha-miniapp/src/main/java/com/haha/miniapp/service/RedisService.java

@@ -0,0 +1,96 @@
+package com.haha.miniapp.service;
+
+import com.haha.miniapp.common.RedisConstants;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.stereotype.Service;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Redis 服务类
+ * 提供基于统一Redis键常量的缓存操作
+ */
+@Service
+public class RedisService {
+
+    @Autowired
+    private StringRedisTemplate stringRedisTemplate;
+
+    /**
+     * 存储Sa-Token相关信息
+     */
+    public void setSaTokenInfo(String token, String loginId, long timeout) {
+        stringRedisTemplate.opsForValue().set(
+            String.format(RedisConstants.SA_TOKEN_USER_TOKEN_KEY, token), 
+            loginId, 
+            timeout, 
+            TimeUnit.SECONDS
+        );
+        stringRedisTemplate.opsForValue().set(
+            String.format(RedisConstants.SA_TOKEN_USER_LOGIN_ID_KEY, loginId), 
+            token, 
+            timeout, 
+            TimeUnit.SECONDS
+        );
+    }
+
+    /**
+     * 获取Sa-Token相关信息
+     */
+    public String getSaTokenByLoginId(String loginId) {
+        return stringRedisTemplate.opsForValue().get(
+            String.format(RedisConstants.SA_TOKEN_USER_LOGIN_ID_KEY, loginId)
+        );
+    }
+
+    /**
+     * 删除Sa-Token相关信息
+     */
+    public void deleteSaTokenInfo(String token, String loginId) {
+        stringRedisTemplate.delete(String.format(RedisConstants.SA_TOKEN_USER_TOKEN_KEY, token));
+        stringRedisTemplate.delete(String.format(RedisConstants.SA_TOKEN_USER_LOGIN_ID_KEY, loginId));
+    }
+
+    /**
+     * 存储设备状态
+     */
+    public void setDeviceStatus(String deviceSn, String status, long timeout) {
+        stringRedisTemplate.opsForValue().set(
+            String.format(RedisConstants.DEVICE_STATUS_KEY, deviceSn),
+            status,
+            timeout,
+            TimeUnit.SECONDS
+        );
+    }
+
+    /**
+     * 获取设备状态
+     */
+    public String getDeviceStatus(String deviceSn) {
+        return stringRedisTemplate.opsForValue().get(
+            String.format(RedisConstants.DEVICE_STATUS_KEY, deviceSn)
+        );
+    }
+
+    /**
+     * 设置用户信息缓存
+     */
+    public void setUserInfo(Long userId, String userInfo, long timeout) {
+        stringRedisTemplate.opsForValue().set(
+            String.format(RedisConstants.USER_INFO_KEY, userId),
+            userInfo,
+            timeout,
+            TimeUnit.SECONDS
+        );
+    }
+
+    /**
+     * 获取用户信息缓存
+     */
+    public String getUserInfo(Long userId) {
+        return stringRedisTemplate.opsForValue().get(
+            String.format(RedisConstants.USER_INFO_KEY, userId)
+        );
+    }
+}

+ 174 - 0
haha-miniapp/src/main/java/com/haha/miniapp/service/impl/HahaCallbackServiceImpl.java

@@ -0,0 +1,174 @@
+package com.haha.miniapp.service.impl;
+
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONArray;
+import com.alibaba.fastjson2.JSONObject;
+import com.haha.miniapp.service.HahaCallbackService;
+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.stereotype.Service;
+
+import java.util.Map;
+
+@Slf4j
+@Service
+public class HahaCallbackServiceImpl implements HahaCallbackService {
+
+    @Autowired
+    private OrderService orderService;
+
+    @Value("${haha.api.app-secret}")
+    private String appSecret;
+
+    @Override
+    public void handleMessage(Map<String, Object> params) {
+        String notifyType = (String) params.get("notify_type");
+        if (notifyType == null || notifyType.isEmpty()) {
+            log.warn("消息回调缺少 notify_type 参数");
+            return;
+        }
+
+        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);
+        }
+    }
+
+    @Override
+    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");
+            
+            log.info("开关门状态通知 - 设备: {}, 状态: {}, 类型: {}", deviceId, status, openType);
+            
+            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);
+        }
+    }
+
+    @Override
+    public void handleOnlineStatus(Map<String, Object> params) {
+        try {
+            String deviceId = (String) params.get("device_id");
+            Object onlineObj = params.get("is_online");
+            Integer isOnline = onlineObj instanceof Integer ? (Integer) onlineObj : Integer.valueOf(onlineObj.toString());
+            
+            log.info("设备在线状态通知 - 设备: {}, 在线状态: {}", deviceId, isOnline == 1 ? "在线" : "离线");
+        } catch (Exception e) {
+            log.error("处理设备在线状态通知逻辑失败", e);
+        }
+    }
+
+    @Override
+    public void handleVoiceResult(Map<String, Object> params) {
+        try {
+            String deviceId = (String) params.get("device_id");
+            Object voiceObj = params.get("voice");
+            Integer voice = voiceObj instanceof Integer ? (Integer) voiceObj : Integer.valueOf(voiceObj.toString());
+            
+            log.info("音量调节结果通知 - 设备: {}, 音量值: {}", deviceId, voice);
+        } catch (Exception e) {
+            log.error("处理音量调节结果通知逻辑失败", e);
+        }
+    }
+
+    @Override
+    public void handleNewProductAudit(Map<String, Object> params) {
+        try {
+            String id = (String) params.get("id");
+            Object statusObj = params.get("status");
+            Integer status = statusObj instanceof Integer ? (Integer) statusObj : Integer.valueOf(statusObj.toString());
+            String name = (String) params.get("name");
+            
+            log.info("新品审核结果 - ID: {}, 商品名: {}, 状态: {}", id, name, status);
+        } catch (Exception e) {
+            log.error("处理新品审核结果通知逻辑失败", e);
+        }
+    }
+
+    @Override
+    public void handleMergeProduct(Map<String, Object> params) {
+        try {
+            Object listObj = params.get("list");
+            if (listObj instanceof JSONArray) {
+                JSONArray list = (JSONArray) listObj;
+                log.info("商品合并结果通知 - 合并数量: {}", list.size());
+            }
+        } catch (Exception e) {
+            log.error("处理商品合并结果通知逻辑失败", e);
+        }
+    }
+
+    @Override
+    public void handleOrcResult(Map<String, Object> params) {
+        try {
+            String activityId = (String) params.get("activity_id");
+            String deviceId = (String) params.get("device_id");
+            Object nobuyObj = params.get("nobuy");
+            Integer nobuy = nobuyObj instanceof Integer ? (Integer) nobuyObj : Integer.valueOf(nobuyObj.toString());
+            
+            if (nobuy == 1) {
+                log.info("AI识别结果: 用户未消费 (activityId: {})", activityId);
+                return;
+            }
+
+            // 解析识别结果
+            String resultStr = (String) params.get("result");
+            JSONObject result = JSON.parseObject(resultStr);
+            JSONArray excepts = result.getJSONArray("excepts");
+            
+            if (excepts != null && !excepts.isEmpty()) {
+                log.warn("识别结果包含异常 (activityId: {}): {}", activityId, excepts.toJSONString());
+            }
+
+            log.info("AI识别结果通知处理完成 - 设备: {}, 活动: {}", deviceId, activityId);
+            
+            // TODO: 调用 orderService 计算金额并生成订单
+            
+        } catch (Exception e) {
+            log.error("处理AI识别结果逻辑失败", e);
+        }
+    }
+
+    @Override
+    public boolean validateSign(Map<String, Object> params) {
+        // TODO: 实现具体的签名验证逻辑
+        return true;
+    }
+}

+ 66 - 32
haha-miniapp/src/main/java/com/haha/miniapp/service/impl/LoginServiceImpl.java

@@ -1,9 +1,12 @@
 package com.haha.miniapp.service.impl;
 
 import cn.dev33.satoken.stp.StpUtil;
+import com.haha.miniapp.common.Result;
 import com.haha.miniapp.entity.User;
 import com.haha.miniapp.service.LoginService;
 import com.haha.miniapp.service.UserService;
+import com.haha.miniapp.vo.LoginVO;
+import com.haha.miniapp.vo.UserVO;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
@@ -17,7 +20,53 @@ public class LoginServiceImpl implements LoginService {
     private UserService userService;
     
     @Override
-    public Map<String, Object> wechatPhoneLogin(String code, String encryptedData, String iv) {
+    public Result<LoginVO> loginByPassword(String phone, String password) {
+        try {
+            // 临时方案:硬编码测试账号
+            // 注意:生产环境必须使用数据库存储的加密密码,并使用BCrypt等安全算法验证
+            final String TEST_PHONE = "13018061579";
+            final String TEST_PASSWORD = "abc123";
+            
+            if (!TEST_PHONE.equals(phone) || !TEST_PASSWORD.equals(password)) {
+                return Result.error(401, "手机号或密码错误");
+            }
+            
+            // 根据手机号查找或创建用户
+            User user = userService.getUserByPhone(phone);
+            if (user == null) {
+                // 创建新用户
+                user = new User();
+                user.setPhone(phone);
+                user.setNickname("用户" + phone.substring(7));
+                userService.save(user);
+            }
+            
+            // 使用Sa-Token进行登录,生成token
+            StpUtil.login(user.getId());
+            String token = StpUtil.getTokenValue();
+            
+            // 构建返回数据
+            UserVO userVO = UserVO.builder()
+                    .id(user.getId())
+                    .phone(user.getPhone())
+                    .nickname(user.getNickname())
+                    .build();
+            
+            LoginVO loginVO = LoginVO.builder()
+                    .token(token)
+                    .userInfo(userVO)
+                    .build();
+            
+            return Result.success("登录成功", loginVO);
+            
+        } catch (Exception e) {
+            e.printStackTrace();
+            return Result.error(500, "登录失败:" + e.getMessage());
+        }
+    }
+    
+    @Override
+    public Result<LoginVO> wechatPhoneLogin(String code, String encryptedData, String iv) {
         try {
             // 暂时简化实现,模拟微信手机号登录
             // 实际项目中需要使用微信小程序SDK解密获取手机号
@@ -45,60 +94,45 @@ public class LoginServiceImpl implements LoginService {
             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);
+            // 构建返回数据
+            UserVO userVO = UserVO.builder()
+                    .id(user.getId())
+                    .phone(user.getPhone())
+                    .nickname(user.getNickname())
+                    .build();
             
-            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);
+            LoginVO loginVO = LoginVO.builder()
+                    .token(token)
+                    .userInfo(userVO)
+                    .build();
             
-            return result;
+            return Result.success("登录成功", loginVO);
             
         } catch (Exception e) {
             e.printStackTrace();
-            Map<String, Object> result = new HashMap<>();
-            result.put("code", 500);
-            result.put("message", "登录失败:" + e.getMessage());
-            return result;
+            return Result.error(500, "登录失败:" + e.getMessage());
         }
     }
     
     @Override
-    public Map<String, Object> getUserInfo(String userId) {
+    public Result<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;
+                return Result.error(404, "用户不存在");
             }
             
-            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;
+            return Result.success("获取用户信息成功", userInfo);
             
         } catch (Exception e) {
             e.printStackTrace();
-            Map<String, Object> result = new HashMap<>();
-            result.put("code", 500);
-            result.put("message", "获取用户信息失败:" + e.getMessage());
-            return result;
+            return Result.error(500, "获取用户信息失败:" + e.getMessage());
         }
     }
 }

+ 14 - 0
haha-miniapp/src/main/java/com/haha/miniapp/vo/LoginVO.java

@@ -0,0 +1,14 @@
+package com.haha.miniapp.vo;
+
+import lombok.Builder;
+import lombok.Data;
+
+/**
+ * 登录结果展示对象
+ */
+@Data
+@Builder
+public class LoginVO {
+    private String token;
+    private UserVO userInfo;
+}

+ 17 - 0
haha-miniapp/src/main/java/com/haha/miniapp/vo/OpenDoorVO.java

@@ -0,0 +1,17 @@
+package com.haha.miniapp.vo;
+
+import lombok.Builder;
+import lombok.Data;
+
+/**
+ * 开门结果展示对象
+ */
+@Data
+@Builder
+public class OpenDoorVO {
+    private boolean doorOpened;
+    private String outTradeNo;
+    private String orderNo;
+    private String deviceId;
+    private Integer doorIndex;
+}

+ 16 - 0
haha-miniapp/src/main/java/com/haha/miniapp/vo/OrderItemVO.java

@@ -0,0 +1,16 @@
+package com.haha.miniapp.vo;
+
+import lombok.Data;
+
+/**
+ * 订单商品展示对象
+ */
+@Data
+public class OrderItemVO {
+    private String id;
+    private String name;
+    private Double price;
+    private Integer quantity;
+    private String image;
+    private Double subtotal;
+}

+ 26 - 0
haha-miniapp/src/main/java/com/haha/miniapp/vo/OrderVO.java

@@ -0,0 +1,26 @@
+package com.haha.miniapp.vo;
+
+import lombok.Data;
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 订单详情展示对象
+ */
+@Data
+public class OrderVO {
+    private Long id;
+    private String orderNo;
+    private String outTradeNo;
+    private String hahaOrderNo;
+    private String deviceSn;
+    private Double totalAmount;
+    private Integer payStatus;
+    private Integer status;
+    private String statusText;
+    private LocalDateTime createTime;
+    private LocalDateTime payTime;
+    private String videoUrl;
+    private Double confidence;
+    private List<OrderItemVO> products;
+}

+ 16 - 0
haha-miniapp/src/main/java/com/haha/miniapp/vo/UserVO.java

@@ -0,0 +1,16 @@
+package com.haha.miniapp.vo;
+
+import lombok.Builder;
+import lombok.Data;
+
+/**
+ * 用户信息展示对象
+ */
+@Data
+@Builder
+public class UserVO {
+    private Long id;
+    private String nickname;
+    private String avatar;
+    private String phone;
+}

+ 15 - 2
haha-miniapp/src/main/resources/application.yml

@@ -38,6 +38,19 @@ mybatis-plus:
   global-config:
     enable-sql-runner: false
 
+# PageHelper 分页插件配置
+pagehelper:
+  # 指定数据库方言
+  helper-dialect: mysql
+  # 分页合理化参数,默认false。当设置为true时,pageNum<=0时会查询第一页,pageNum>pages(超过总数时),会查询最后一页
+  reasonable: true
+  # 支持通过Mapper接口参数来传递分页参数,默认false
+  support-methods-arguments: true
+  # 为了支持startPage(Object params)方法,增加了一个"params"参数来配置参数映射,用于从Map或ServletRequest中取值
+  params: count=countSql
+  # 总是返回PageInfo类型,默认false
+  return-page-info: check
+
 # 服务器配置
 server:
   port: 8080
@@ -71,8 +84,8 @@ haha:
 sa-token:
   # token 名称(同时也是 cookie 名称)
   token-name: haha-token-KuaiyuMan
-  # token 有效期(单位:秒)
-  timeout: 86400
+  # token 有效期(单位:秒)- 7天
+  timeout: 604800
   # token 临时有效期(单位:秒)
   activity-timeout: -1
   # 是否允许同一账号多地同时登录

+ 45 - 0
haha-miniapp/src/main/resources/logback-spring.xml

@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+    <!-- 控制台输出 -->
+    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
+        <!-- Windows控制台编码设置为GBK,解决中文乱码问题 -->
+        <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
+            <layout class="ch.qos.logback.classic.PatternLayout">
+                <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
+            </layout>
+            <!-- Windows系统使用GBK编码 -->
+            <charset>GBK</charset>
+        </encoder>
+    </appender>
+
+    <!-- 文件输出 -->
+    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>logs/haha-miniapp.log</file>
+        <encoder>
+            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
+            <!-- 日志文件使用UTF-8编码 -->
+            <charset>UTF-8</charset>
+        </encoder>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <fileNamePattern>logs/haha-miniapp.%d{yyyy-MM-dd}.log</fileNamePattern>
+            <maxHistory>30</maxHistory>
+        </rollingPolicy>
+    </appender>
+
+    <!-- 开发环境:控制台输出 -->
+    <springProfile name="dev,default">
+        <root level="INFO">
+            <appender-ref ref="CONSOLE"/>
+        </root>
+        <logger name="com.haha.miniapp" level="DEBUG"/>
+        <logger name="cn.dev33.satoken" level="DEBUG"/>
+    </springProfile>
+
+    <!-- 生产环境:文件输出 -->
+    <springProfile name="prod">
+        <root level="INFO">
+            <appender-ref ref="FILE"/>
+        </root>
+        <logger name="com.haha.miniapp" level="INFO"/>
+    </springProfile>
+</configuration>

+ 81 - 0
haha-miniapp/src/main/resources/sql/order.sql

@@ -0,0 +1,81 @@
+-- 创建订单表
+CREATE TABLE IF NOT EXISTS `t_order` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '订单ID',
+  `order_no` VARCHAR(64) NOT NULL COMMENT '订单号(系统内部订单号)',
+  `out_trade_no` VARCHAR(64) DEFAULT NULL COMMENT '商户订单号(支付订单号)',
+  `haha_order_no` VARCHAR(64) DEFAULT NULL COMMENT '哈哈平台订单号',
+  `user_id` BIGINT NOT NULL COMMENT '用户ID',
+  `device_sn` VARCHAR(64) NOT NULL COMMENT '设备序列号',
+  `total_amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '订单总金额',
+  `pay_status` VARCHAR(20) NOT NULL DEFAULT 'UNPAID' COMMENT '支付状态:UNPAID-未支付,PAID-已支付,REFUND-已退款',
+  `video_url` VARCHAR(500) DEFAULT NULL COMMENT '购物视频URL',
+  `confidence` DECIMAL(5,2) DEFAULT NULL COMMENT '识别置信度(0-100)',
+  `items_json` TEXT DEFAULT NULL COMMENT '订单商品明细JSON',
+  `status` INT NOT NULL DEFAULT 1 COMMENT '订单状态:0-已取消,1-待支付,2-已完成,3-已关闭',
+  `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `pay_time` DATETIME DEFAULT NULL COMMENT '支付时间',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_order_no` (`order_no`),
+  KEY `idx_user_id` (`user_id`),
+  KEY `idx_device_sn` (`device_sn`),
+  KEY `idx_create_time` (`create_time`),
+  KEY `idx_haha_order_no` (`haha_order_no`)
+) ENGINE=InnoDB AUTO_INCREMENT=100000 DEFAULT CHARSET=utf8mb4 COMMENT='订单表';
+
+-- 插入订单表测试数据
+INSERT IGNORE INTO `t_order` (`order_no`, `out_trade_no`, `haha_order_no`, `user_id`, `device_sn`, `total_amount`, `pay_status`, `video_url`, `confidence`, `items_json`, `status`, `create_time`, `pay_time`) VALUES
+    ('ORD202601252110345920', 'OUT202601252110347833', 'HAHA202601252110346621', 10000, 'DEVICE_001', 43.0, 'REFUND', NULL, 84.1, '[{"name": "口香糖", "price": 3.0, "quantity": 2}, {"name": "可乐", "price": 3.5, "quantity": 2}, {"name": "面包", "price": 7.5, "quantity": 1}, {"name": "面包", "price": 7.5, "quantity": 3}]', 3, '2026-01-17 14:52:34', NULL),
+    ('ORD202601252110342389', 'OUT202601252110343653', 'HAHA202601252110348775', 10000, 'DEVICE_003', 46.0, 'PAID', NULL, NULL, '[{"name": "口香糖", "price": 3.0, "quantity": 1}, {"name": "薯片", "price": 6.0, "quantity": 3}, {"name": "饮料", "price": 5.0, "quantity": 3}, {"name": "口香糖", "price": 3.0, "quantity": 1}, {"name": "雪碧", "price": 3.5, "quantity": 2}]', 2, '2026-01-06 03:14:34', '2026-01-06 03:30:34'),
+    ('ORD202601252110342567', NULL, 'HAHA202601252110341393', 10000, 'DEVICE_006', 62.5, 'UNPAID', 'https://example.com/video/ORD202601252110342567.mp4', 92.56, '[{"name": "坚果", "price": 12.0, "quantity": 3}, {"name": "矿泉水", "price": 2.0, "quantity": 3}, {"name": "饼干", "price": 4.5, "quantity": 1}, {"name": "巧克力", "price": 8.0, "quantity": 2}]', 0, '2026-01-06 10:01:34', NULL),
+    ('ORD202601252110346048', NULL, 'HAHA202601252110345459', 10000, 'DEVICE_001', 14.5, 'UNPAID', 'https://example.com/video/ORD202601252110346048.mp4', NULL, '[{"name": "矿泉水", "price": 2.0, "quantity": 2}, {"name": "可乐", "price": 3.5, "quantity": 1}, {"name": "矿泉水", "price": 2.0, "quantity": 1}, {"name": "饮料", "price": 5.0, "quantity": 1}]', 1, '2026-01-21 13:45:34', NULL),
+    ('ORD202601252110341086', 'OUT202601252110341077', 'HAHA202601252110345389', 10000, 'DEVICE_006', 6.0, 'UNPAID', 'https://example.com/video/ORD202601252110341086.mp4', NULL, '[{"name": "矿泉水", "price": 2.0, "quantity": 3}]', 1, '2025-12-29 22:14:34', NULL),
+    ('ORD202601252110343968', NULL, NULL, 10000, 'DEVICE_007', 43.0, 'PAID', 'https://example.com/video/ORD202601252110343968.mp4', 88.96, '[{"name": "坚果", "price": 12.0, "quantity": 3}, {"name": "可乐", "price": 3.5, "quantity": 2}]', 2, '2025-12-27 05:30:34', '2025-12-27 06:00:34'),
+    ('ORD202601252110344836', 'OUT202601252110343564', NULL, 10000, 'DEVICE_001', 22.5, 'PAID', 'https://example.com/video/ORD202601252110344836.mp4', NULL, '[{"name": "饼干", "price": 4.5, "quantity": 1}, {"name": "薯片", "price": 6.0, "quantity": 3}]', 2, '2026-01-11 19:29:34', '2026-01-11 19:35:34'),
+    ('ORD202601252110341177', 'OUT202601252110345719', 'HAHA202601252110345877', 10000, 'DEVICE_005', 36.0, 'REFUND', 'https://example.com/video/ORD202601252110341177.mp4', NULL, '[{"name": "薯片", "price": 6.0, "quantity": 2}, {"name": "坚果", "price": 12.0, "quantity": 2}]', 3, '2026-01-08 09:21:34', NULL),
+    ('ORD202601252110348307', 'OUT202601252110342226', 'HAHA202601252110345370', 10000, 'DEVICE_002', 7.5, 'PAID', 'https://example.com/video/ORD202601252110348307.mp4', NULL, '[{"name": "可乐", "price": 3.5, "quantity": 1}, {"name": "矿泉水", "price": 2.0, "quantity": 2}]', 2, '2026-01-20 05:21:34', '2026-01-20 05:42:34'),
+    ('ORD202601252110348755', 'OUT202601252110345810', 'HAHA202601252110344532', 10000, 'DEVICE_005', 22.0, 'UNPAID', NULL, 99.68, '[{"name": "口香糖", "price": 3.0, "quantity": 2}, {"name": "巧克力", "price": 8.0, "quantity": 2}]', 0, '2026-01-07 20:30:34', NULL),
+    ('ORD202601252110344404', 'OUT202601252110348190', NULL, 10000, 'DEVICE_009', 25.5, 'PAID', 'https://example.com/video/ORD202601252110344404.mp4', NULL, '[{"name": "口香糖", "price": 3.0, "quantity": 1}, {"name": "面包", "price": 7.5, "quantity": 3}]', 2, '2025-12-27 00:02:34', '2025-12-27 00:15:34'),
+    ('ORD202601252110343825', NULL, NULL, 10000, 'DEVICE_009', 9.0, 'REFUND', NULL, 94.52, '[{"name": "饼干", "price": 4.5, "quantity": 2}]', 3, '2025-12-31 14:02:34', NULL),
+    ('ORD202601252110348464', 'OUT202601252110343933', 'HAHA202601252110341123', 10000, 'DEVICE_002', 15.0, 'PAID', 'https://example.com/video/ORD202601252110348464.mp4', 92.73, '[{"name": "面包", "price": 7.5, "quantity": 2}]', 2, '2025-12-26 05:36:34', '2025-12-26 05:57:34'),
+    ('ORD202601252110342408', 'OUT202601252110341810', 'HAHA202601252110344480', 10000, 'DEVICE_001', 16.0, 'UNPAID', 'https://example.com/video/ORD202601252110342408.mp4', NULL, '[{"name": "巧克力", "price": 8.0, "quantity": 2}]', 1, '2026-01-14 03:19:34', NULL),
+    ('ORD202601252110345569', 'OUT202601252110344489', 'HAHA202601252110343588', 10000, 'DEVICE_010', 12.0, 'PAID', 'https://example.com/video/ORD202601252110345569.mp4', 85.1, '[{"name": "坚果", "price": 12.0, "quantity": 1}]', 2, '2025-12-27 12:51:34', '2025-12-27 13:16:34'),
+    ('ORD202601252110348594', 'OUT202601252110348455', NULL, 10000, 'DEVICE_009', 19.0, 'PAID', NULL, 90.45, '[{"name": "饮料", "price": 5.0, "quantity": 1}, {"name": "雪碧", "price": 3.5, "quantity": 1}, {"name": "雪碧", "price": 3.5, "quantity": 3}]', 2, '2025-12-31 14:31:34', '2025-12-31 14:45:34'),
+    ('ORD202601252110348085', 'OUT202601252110346505', NULL, 10000, 'DEVICE_010', 8.0, 'PAID', NULL, NULL, '[{"name": "饼干", "price": 4.5, "quantity": 1}, {"name": "雪碧", "price": 3.5, "quantity": 1}]', 2, '2026-01-17 22:09:34', '2026-01-17 22:19:34'),
+    ('ORD202601252110343688', 'OUT202601252110342550', 'HAHA202601252110343176', 10000, 'DEVICE_010', 24.0, 'REFUND', NULL, NULL, '[{"name": "巧克力", "price": 8.0, "quantity": 3}]', 3, '2026-01-25 14:49:34', NULL),
+    ('ORD202601252110341919', 'OUT202601252110341806', 'HAHA202601252110343116', 10000, 'DEVICE_001', 9.0, 'PAID', 'https://example.com/video/ORD202601252110341919.mp4', NULL, '[{"name": "雪碧", "price": 3.5, "quantity": 2}, {"name": "矿泉水", "price": 2.0, "quantity": 1}]', 2, '2026-01-23 05:13:34', '2026-01-23 05:26:34'),
+    ('ORD202601252110346035', NULL, 'HAHA202601252110342496', 10000, 'DEVICE_007', 65.0, 'UNPAID', 'https://example.com/video/ORD202601252110346035.mp4', 80.24, '[{"name": "饼干", "price": 4.5, "quantity": 3}, {"name": "雪碧", "price": 3.5, "quantity": 3}, {"name": "巧克力", "price": 8.0, "quantity": 1}, {"name": "口香糖", "price": 3.0, "quantity": 3}, {"name": "坚果", "price": 12.0, "quantity": 2}]', 1, '2026-01-09 14:03:34', NULL),
+    ('ORD202601252110343091', 'OUT202601252110349313', NULL, 10000, 'DEVICE_005', 7.5, 'UNPAID', 'https://example.com/video/ORD202601252110343091.mp4', 81.26, '[{"name": "口香糖", "price": 3.0, "quantity": 1}, {"name": "饼干", "price": 4.5, "quantity": 1}]', 1, '2026-01-23 02:35:34', NULL),
+    ('ORD202601252110345610', NULL, 'HAHA202601252110341193', 10000, 'DEVICE_006', 10.5, 'UNPAID', 'https://example.com/video/ORD202601252110345610.mp4', NULL, '[{"name": "雪碧", "price": 3.5, "quantity": 3}]', 0, '2026-01-25 05:00:34', NULL),
+    ('ORD202601252110345955', 'OUT202601252110347834', NULL, 10000, 'DEVICE_001', 9.0, 'PAID', 'https://example.com/video/ORD202601252110345955.mp4', NULL, '[{"name": "口香糖", "price": 3.0, "quantity": 3}]', 2, '2026-01-20 05:10:34', '2026-01-20 05:33:34'),
+    ('ORD202601252110345190', NULL, 'HAHA202601252110349146', 10000, 'DEVICE_007', 49.0, 'PAID', 'https://example.com/video/ORD202601252110345190.mp4', NULL, '[{"name": "口香糖", "price": 3.0, "quantity": 2}, {"name": "巧克力", "price": 8.0, "quantity": 2}, {"name": "口香糖", "price": 3.0, "quantity": 1}, {"name": "面包", "price": 7.5, "quantity": 2}, {"name": "口香糖", "price": 3.0, "quantity": 3}]', 2, '2026-01-03 05:10:34', '2026-01-03 05:22:34'),
+    ('ORD202601252110349920', 'OUT202601252110342403', 'HAHA202601252110344363', 10000, 'DEVICE_007', 5.0, 'PAID', NULL, NULL, '[{"name": "饮料", "price": 5.0, "quantity": 1}]', 2, '2026-01-22 13:01:34', '2026-01-22 13:19:34'),
+    ('ORD202601252110345236', 'OUT202601252110342498', 'HAHA202601252110343272', 10000, 'DEVICE_008', 25.5, 'UNPAID', NULL, NULL, '[{"name": "雪碧", "price": 3.5, "quantity": 2}, {"name": "坚果", "price": 12.0, "quantity": 1}, {"name": "矿泉水", "price": 2.0, "quantity": 1}, {"name": "饼干", "price": 4.5, "quantity": 1}]', 1, '2026-01-14 19:56:34', NULL),
+    ('ORD202601252110341423', NULL, 'HAHA202601252110341823', 10000, 'DEVICE_002', 33.0, 'UNPAID', NULL, 85.44, '[{"name": "坚果", "price": 12.0, "quantity": 2}, {"name": "口香糖", "price": 3.0, "quantity": 3}]', 1, '2026-01-06 19:34:34', NULL),
+    ('ORD202601252110341533', NULL, 'HAHA202601252110342284', 10000, 'DEVICE_009', 7.0, 'UNPAID', 'https://example.com/video/ORD202601252110341533.mp4', NULL, '[{"name": "可乐", "price": 3.5, "quantity": 2}]', 1, '2026-01-12 17:53:34', NULL),
+    ('ORD202601252110345888', 'OUT202601252110341261', NULL, 10000, 'DEVICE_006', 69.5, 'REFUND', NULL, NULL, '[{"name": "坚果", "price": 12.0, "quantity": 2}, {"name": "雪碧", "price": 3.5, "quantity": 1}, {"name": "薯片", "price": 6.0, "quantity": 3}, {"name": "薯片", "price": 6.0, "quantity": 1}, {"name": "薯片", "price": 6.0, "quantity": 3}]', 3, '2026-01-20 09:26:34', NULL),
+    ('ORD202601252110343952', 'OUT202601252110347635', 'HAHA202601252110343170', 10000, 'DEVICE_004', 29.0, 'PAID', 'https://example.com/video/ORD202601252110343952.mp4', 82.29, '[{"name": "饼干", "price": 4.5, "quantity": 1}, {"name": "可乐", "price": 3.5, "quantity": 2}, {"name": "饮料", "price": 5.0, "quantity": 2}, {"name": "面包", "price": 7.5, "quantity": 1}]', 2, '2026-01-10 00:28:34', '2026-01-10 00:48:34'),
+    ('ORD202601252110344665', 'OUT202601252110342457', 'HAHA202601252110343046', 10000, 'DEVICE_008', 24.5, 'REFUND', NULL, 86.67, '[{"name": "可乐", "price": 3.5, "quantity": 2}, {"name": "可乐", "price": 3.5, "quantity": 3}, {"name": "雪碧", "price": 3.5, "quantity": 2}]', 3, '2026-01-17 07:02:34', NULL),
+    ('ORD202601252110341837', 'OUT202601252110347753', 'HAHA202601252110349386', 10000, 'DEVICE_010', 35.0, 'REFUND', NULL, NULL, '[{"name": "面包", "price": 7.5, "quantity": 3}, {"name": "口香糖", "price": 3.0, "quantity": 3}, {"name": "雪碧", "price": 3.5, "quantity": 1}]', 3, '2026-01-22 16:50:34', NULL),
+    ('ORD202601252110342223', 'OUT202601252110342591', NULL, 10000, 'DEVICE_006', 34.0, 'UNPAID', NULL, 86.49, '[{"name": "巧克力", "price": 8.0, "quantity": 2}, {"name": "薯片", "price": 6.0, "quantity": 3}]', 1, '2025-12-30 11:49:34', NULL),
+    ('ORD202601252110343815', 'OUT202601252110346369', 'HAHA202601252110342340', 10000, 'DEVICE_002', 3.5, 'REFUND', 'https://example.com/video/ORD202601252110343815.mp4', 92.58, '[{"name": "雪碧", "price": 3.5, "quantity": 1}]', 3, '2026-01-10 15:53:34', NULL),
+    ('ORD202601252110344588', 'OUT202601252110349889', NULL, 10000, 'DEVICE_008', 48.0, 'UNPAID', 'https://example.com/video/ORD202601252110344588.mp4', 99.93, '[{"name": "薯片", "price": 6.0, "quantity": 2}, {"name": "坚果", "price": 12.0, "quantity": 3}]', 1, '2026-01-12 23:14:34', NULL),
+    ('ORD202601252110342958', NULL, 'HAHA202601252110342690', 10000, 'DEVICE_003', 2.0, 'REFUND', NULL, 97.48, '[{"name": "矿泉水", "price": 2.0, "quantity": 1}]', 3, '2026-01-04 04:41:34', NULL),
+    ('ORD202601252110341689', 'OUT202601252110346847', 'HAHA202601252110348888', 10000, 'DEVICE_001', 35.0, 'PAID', 'https://example.com/video/ORD202601252110341689.mp4', NULL, '[{"name": "面包", "price": 7.5, "quantity": 3}, {"name": "饼干", "price": 4.5, "quantity": 1}, {"name": "矿泉水", "price": 2.0, "quantity": 3}, {"name": "矿泉水", "price": 2.0, "quantity": 1}]', 2, '2025-12-26 10:15:34', '2025-12-26 10:22:34'),
+    ('ORD202601252110348189', 'OUT202601252110341430', 'HAHA202601252110343407', 10000, 'DEVICE_009', 74.5, 'REFUND', 'https://example.com/video/ORD202601252110348189.mp4', 89.09, '[{"name": "饼干", "price": 4.5, "quantity": 1}, {"name": "巧克力", "price": 8.0, "quantity": 2}, {"name": "口香糖", "price": 3.0, "quantity": 3}, {"name": "口香糖", "price": 3.0, "quantity": 3}, {"name": "坚果", "price": 12.0, "quantity": 3}]', 3, '2026-01-23 13:21:34', NULL),
+    ('ORD202601252110346432', NULL, NULL, 10000, 'DEVICE_009', 3.5, 'REFUND', NULL, NULL, '[{"name": "可乐", "price": 3.5, "quantity": 1}]', 3, '2026-01-23 05:54:34', NULL),
+    ('ORD202601252110341485', NULL, 'HAHA202601252110345095', 10000, 'DEVICE_004', 19.5, 'REFUND', NULL, 86.07, '[{"name": "口香糖", "price": 3.0, "quantity": 1}, {"name": "雪碧", "price": 3.5, "quantity": 1}, {"name": "饼干", "price": 4.5, "quantity": 2}, {"name": "矿泉水", "price": 2.0, "quantity": 2}]', 3, '2025-12-30 17:10:34', NULL),
+    ('ORD202601252110341615', 'OUT202601252110344250', NULL, 10000, 'DEVICE_004', 32.5, 'REFUND', NULL, 91.46, '[{"name": "可乐", "price": 3.5, "quantity": 2}, {"name": "口香糖", "price": 3.0, "quantity": 1}, {"name": "面包", "price": 7.5, "quantity": 2}, {"name": "面包", "price": 7.5, "quantity": 1}]', 3, '2025-12-27 10:31:34', NULL),
+    ('ORD202601252110346188', 'OUT202601252110343359', NULL, 10000, 'DEVICE_005', 48.5, 'REFUND', 'https://example.com/video/ORD202601252110346188.mp4', 92.71, '[{"name": "可乐", "price": 3.5, "quantity": 2}, {"name": "巧克力", "price": 8.0, "quantity": 3}, {"name": "口香糖", "price": 3.0, "quantity": 2}, {"name": "面包", "price": 7.5, "quantity": 1}, {"name": "矿泉水", "price": 2.0, "quantity": 2}]', 3, '2026-01-17 02:02:34', NULL),
+    ('ORD202601252110344579', 'OUT202601252110349271', 'HAHA202601252110344919', 10000, 'DEVICE_010', 22.5, 'UNPAID', 'https://example.com/video/ORD202601252110344579.mp4', NULL, '[{"name": "饼干", "price": 4.5, "quantity": 3}, {"name": "口香糖", "price": 3.0, "quantity": 3}]', 1, '2026-01-03 04:44:34', NULL),
+    ('ORD202601252110348101', NULL, NULL, 10000, 'DEVICE_003', 43.0, 'PAID', 'https://example.com/video/ORD202601252110348101.mp4', 82.97, '[{"name": "可乐", "price": 3.5, "quantity": 2}, {"name": "坚果", "price": 12.0, "quantity": 1}, {"name": "薯片", "price": 6.0, "quantity": 2}, {"name": "坚果", "price": 12.0, "quantity": 1}]', 2, '2025-12-30 07:45:34', '2025-12-30 07:56:34'),
+    ('ORD202601252110341881', 'OUT202601252110347746', 'HAHA202601252110348053', 10000, 'DEVICE_003', 28.5, 'REFUND', NULL, NULL, '[{"name": "面包", "price": 7.5, "quantity": 3}, {"name": "矿泉水", "price": 2.0, "quantity": 3}]', 3, '2026-01-08 18:39:34', NULL),
+    ('ORD202601252110346673', 'OUT202601252110347179', 'HAHA202601252110342567', 10000, 'DEVICE_010', 40.0, 'UNPAID', NULL, NULL, '[{"name": "口香糖", "price": 3.0, "quantity": 3}, {"name": "薯片", "price": 6.0, "quantity": 2}, {"name": "矿泉水", "price": 2.0, "quantity": 2}, {"name": "饮料", "price": 5.0, "quantity": 3}]', 1, '2025-12-26 12:01:34', NULL),
+    ('ORD202601252110343535', 'OUT202601252110342752', NULL, 10000, 'DEVICE_009', 13.5, 'REFUND', NULL, NULL, '[{"name": "饼干", "price": 4.5, "quantity": 1}, {"name": "饼干", "price": 4.5, "quantity": 2}]', 3, '2026-01-19 13:32:34', NULL),
+    ('ORD202601252110342391', 'OUT202601252110345772', 'HAHA202601252110348319', 10000, 'DEVICE_001', 18.5, 'PAID', 'https://example.com/video/ORD202601252110342391.mp4', 98.39, '[{"name": "饮料", "price": 5.0, "quantity": 3}, {"name": "雪碧", "price": 3.5, "quantity": 1}]', 2, '2026-01-05 02:46:34', '2026-01-05 03:02:34'),
+    ('ORD202601252110342174', 'OUT202601252110342891', 'HAHA202601252110346685', 10000, 'DEVICE_005', 22.5, 'REFUND', 'https://example.com/video/ORD202601252110342174.mp4', NULL, '[{"name": "可乐", "price": 3.5, "quantity": 3}, {"name": "薯片", "price": 6.0, "quantity": 2}]', 3, '2026-01-01 02:28:34', NULL),
+    ('ORD202601252110349995', 'OUT202601252110345175', 'HAHA202601252110346279', 10000, 'DEVICE_003', 53.0, 'UNPAID', NULL, 80.66, '[{"name": "雪碧", "price": 3.5, "quantity": 1}, {"name": "饼干", "price": 4.5, "quantity": 1}, {"name": "口香糖", "price": 3.0, "quantity": 1}, {"name": "薯片", "price": 6.0, "quantity": 3}, {"name": "巧克力", "price": 8.0, "quantity": 3}]', 1, '2026-01-13 23:39:34', NULL),
+    ('ORD202601252110347484', 'OUT202601252110349012', 'HAHA202601252110347571', 10000, 'DEVICE_010', 15.0, 'REFUND', NULL, 93.72, '[{"name": "巧克力", "price": 8.0, "quantity": 1}, {"name": "雪碧", "price": 3.5, "quantity": 2}]', 3, '2026-01-11 19:49:34', NULL),
+    ('ORD202601252110345218', NULL, 'HAHA202601252110343189', 10000, 'DEVICE_003', 39.0, 'UNPAID', NULL, NULL, '[{"name": "薯片", "price": 6.0, "quantity": 1}, {"name": "薯片", "price": 6.0, "quantity": 3}, {"name": "面包", "price": 7.5, "quantity": 2}]', 1, '2026-01-11 16:11:34', NULL),
+    ('ORD202601252110347510', 'OUT202601252110347258', 'HAHA202601252110347241', 10000, 'DEVICE_004', 88.0, 'REFUND', NULL, NULL, '[{"name": "雪碧", "price": 3.5, "quantity": 3}, {"name": "坚果", "price": 12.0, "quantity": 3}, {"name": "面包", "price": 7.5, "quantity": 3}, {"name": "饮料", "price": 5.0, "quantity": 2}, {"name": "口香糖", "price": 3.0, "quantity": 3}]', 3, '2026-01-23 22:28:34', NULL),
+    ('ORD202601252110342013', 'OUT202601252110348634', 'HAHA202601252110345165', 10000, 'DEVICE_003', 54.5, 'UNPAID', 'https://example.com/video/ORD202601252110342013.mp4', 80.9, '[{"name": "巧克力", "price": 8.0, "quantity": 3}, {"name": "可乐", "price": 3.5, "quantity": 1}, {"name": "饮料", "price": 5.0, "quantity": 3}, {"name": "薯片", "price": 6.0, "quantity": 2}]', 1, '2026-01-02 17:53:34', NULL),
+    ('ORD202601252110346533', 'OUT202601252110341183', NULL, 10000, 'DEVICE_003', 58.0, 'REFUND', NULL, NULL, '[{"name": "巧克力", "price": 8.0, "quantity": 1}, {"name": "巧克力", "price": 8.0, "quantity": 1}, {"name": "矿泉水", "price": 2.0, "quantity": 3}, {"name": "坚果", "price": 12.0, "quantity": 3}]', 3, '2026-01-12 07:50:34', NULL);

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

@@ -1,13 +1,47 @@
 <script setup lang="ts">
 import { onLaunch, onShow, onHide } from "@dcloudio/uni-app";
+
+/**
+ * 应用启动
+ */
 onLaunch(() => {
   console.log("App Launch");
+  checkLoginStatus();
 });
+
+/**
+ * 应用显示
+ */
 onShow(() => {
   console.log("App Show");
 });
+
+/**
+ * 应用隐藏
+ */
 onHide(() => {
   console.log("App Hide");
 });
+
+/**
+ * 检查登录状态
+ * 如果未登录且当前不在登录页,则跳转到登录页
+ */
+const checkLoginStatus = () => {
+  const token = uni.getStorageSync('haha-token-KuaiyuMan');
+  
+  // 获取当前页面路径
+  const pages = getCurrentPages();
+  const currentPage = pages[pages.length - 1];
+  const currentPath = currentPage ? currentPage.route : '';
+  
+  // 如果没有token且不在登录页,跳转到登录页
+  if (!token && currentPath !== 'pages/login/login') {
+    console.log('未登录,跳转到登录页');
+    uni.reLaunch({
+      url: '/pages/login/login'
+    });
+  }
+};
 </script>
 <style></style>

+ 52 - 0
haha-mp/src/api/user.ts

@@ -0,0 +1,52 @@
+import { post } from '../utils/request';
+
+/**
+ * 用户相关接口
+ */
+
+/**
+ * 登录结果接口
+ */
+export interface LoginResult {
+  token: string;
+  userInfo: UserInfo;
+}
+
+/**
+ * 用户信息接口
+ */
+export interface UserInfo {
+  id: number;
+  nickname: string;
+  phone: string;
+  avatar: string;
+}
+
+/**
+ * 账号密码登录(临时方案)
+ */
+export const loginByPassword = (params: { phone: string; password: string }): Promise<LoginResult> => {
+  return post<LoginResult>('/login/password', params);
+};
+
+/**
+ * 微信手机号一键登录
+ * @param params { code, encryptedData, iv }
+ */
+export const loginByWechatPhone = (params: { code: string; encryptedData?: string; iv?: string }): Promise<LoginResult> => {
+  return post<LoginResult>('/login/wechat-phone', params);
+};
+
+/**
+ * 获取用户信息
+ */
+export const getUserInfo = (): Promise<UserInfo> => {
+  return post<UserInfo>('/login/user-info');
+};
+
+/**
+ * 退出登录
+ */
+export const logout = (): Promise<void> => {
+  return post<void>('/login/logout');
+};

+ 3 - 3
haha-mp/src/manifest.json

@@ -50,7 +50,7 @@
     "quickapp" : {},
     /* 小程序特有相关 */
     "mp-weixin" : {
-        "appid" : "",
+        "appid" : "wxef6ffc2591d04b1b",
         "setting" : {
             "urlCheck" : false
         },
@@ -65,8 +65,8 @@
     "mp-toutiao" : {
         "usingComponents" : true
     },
-    "uniStatistics": {  
-        "enable": false
+    "uniStatistics" : {
+        "enable" : false
     },
     "vueVersion" : "3"
 }

+ 8 - 0
haha-mp/src/pages.json

@@ -9,6 +9,14 @@
 				"navigationBarIconColor": "black"
 			}
 		},
+		{
+			"path": "pages/login/login",
+			"style": {
+				"navigationBarTitleText": "登录",
+				"navigationBarBackgroundColor": "#FFD700",
+				"navigationBarTextStyle": "black"
+			}
+		},
 		{
 			"path": "pages/my/my",
 			"style": {

+ 2 - 2
haha-mp/src/pages/index/index.vue

@@ -115,9 +115,9 @@ const goToMy = () => {
 };
 
 const goToRefund = () => {
-  // 跳转到退款页面
+  // 跳转到订单列表页面
   uni.navigateTo({
-    url: '/pages/refund/refund'
+    url: '/pages/orders/orders'
   });
 };
 </script>

+ 236 - 0
haha-mp/src/pages/login/login.vue

@@ -0,0 +1,236 @@
+<template>
+  <view class="container">
+    <view class="login-header">
+      <image class="logo" src="/static/logo.png" mode="aspectFit"></image>
+      <text class="title">快与慢充电桩</text>
+      <text class="subtitle">智能视觉零售柜</text>
+    </view>
+
+    <view class="login-content">
+      <!-- 账号密码登录表单 -->
+      <view class="form-item">
+        <input 
+          class="input" 
+          type="number" 
+          placeholder="请输入手机号" 
+          v-model="loginForm.phone"
+          maxlength="11"
+        />
+      </view>
+      <view class="form-item">
+        <input 
+          class="input" 
+          type="password" 
+          placeholder="请输入密码" 
+          v-model="loginForm.password"
+        />
+      </view>
+      
+      <button class="login-btn" @click="onPasswordLogin">
+        立即登录
+      </button>
+
+      <!-- 微信手机号一键登录 (个人版小程序暂不可用,已注释) -->
+      <!-- 
+      <button 
+        class="login-btn wechat-btn" 
+        open-type="getPhoneNumber" 
+        @getphonenumber="onGetPhoneNumber"
+      >
+        微信手机号一键登录
+      </button>
+      -->
+      
+      <view class="agreement">
+        登录即代表您同意<text class="link">《用户协议》</text>和<text class="link">《隐私政策》</text>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive } from 'vue';
+import { onLoad } from '@dcloudio/uni-app';
+import { loginByPassword } from '@/api/user';
+import { setToken, setUserInfo } from '@/utils/auth';
+// import { loginByWechatPhone } from '@/api/user';
+
+const loginForm = reactive({
+  phone: '',
+  password: ''
+});
+
+// 登录后跳转的页面
+const redirectUrl = ref('');
+
+// 页面加载时获取redirect参数
+onLoad((options: any) => {
+  if (options.redirect) {
+    redirectUrl.value = decodeURIComponent(options.redirect);
+  }
+});
+
+const onPasswordLogin = async () => {
+  if (!loginForm.phone || loginForm.phone.length !== 11) {
+    uni.showToast({ title: '请输入正确的手机号', icon: 'none' });
+    return;
+  }
+  if (!loginForm.password) {
+    uni.showToast({ title: '请输入密码', icon: 'none' });
+    return;
+  }
+
+  uni.showLoading({ title: '登录中...' });
+  try {
+    const res = await loginByPassword({
+      phone: loginForm.phone,
+      password: loginForm.password
+    });
+    handleLoginSuccess(res);
+  } catch (error) {
+    console.error('登录失败', error);
+  } finally {
+    uni.hideLoading();
+  }
+};
+
+/**
+ * 微信手机号快捷登录 (已禁用)
+ */
+/*
+const onGetPhoneNumber = async (e: any) => {
+  if (e.detail.errMsg !== 'getPhoneNumber:ok') {
+    uni.showToast({
+      title: '登录失败,请重试',
+      icon: 'none'
+    });
+    return;
+  }
+  const { code, encryptedData, iv } = e.detail;
+  uni.showLoading({ title: '登录中...' });
+  try {
+    const res = await loginByWechatPhone({ code, encryptedData, iv });
+    handleLoginSuccess(res);
+  } catch (error) {
+    console.error('登录失败', error);
+  } finally {
+    uni.hideLoading();
+  }
+};
+*/
+
+const handleLoginSuccess = (res: any) => {
+  // 使用auth工具保存token和用户信息
+  setToken(res.token);
+  setUserInfo(res.userInfo);
+  
+  uni.showToast({
+    title: '登录成功',
+    icon: 'success'
+  });
+  
+  // 根据redirect参数跳转
+  setTimeout(() => {
+    if (redirectUrl.value) {
+      uni.reLaunch({ url: redirectUrl.value });
+    } else {
+      // 返回上一页或首页
+      const pages = getCurrentPages();
+      if (pages.length > 1) {
+        uni.navigateBack();
+      } else {
+        uni.reLaunch({ url: '/pages/index/index' });
+      }
+    }
+  }, 1000);
+};
+</script>
+
+<style lang="scss">
+.container {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 100rpx 40rpx;
+  min-height: 100vh;
+  background-color: #fff;
+}
+
+.login-header {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  margin-top: 100rpx;
+  margin-bottom: 150rpx;
+  
+  .logo {
+    width: 160rpx;
+    height: 160rpx;
+    margin-bottom: 30rpx;
+  }
+  
+  .title {
+    font-size: 40rpx;
+    font-weight: bold;
+    color: #333;
+    margin-bottom: 10rpx;
+  }
+  
+  .subtitle {
+    font-size: 28rpx;
+    color: #999;
+  }
+}
+
+.login-content {
+  width: 100%;
+  
+  .form-item {
+    width: 100%;
+    margin-bottom: 30rpx;
+    
+    .input {
+      width: 100%;
+      height: 90rpx;
+      padding: 0 30rpx;
+      background-color: #f5f5f5;
+      border-radius: 45rpx;
+      font-size: 28rpx;
+      box-sizing: border-box;
+    }
+  }
+  
+  .login-btn {
+    width: 100%;
+    height: 90rpx;
+    line-height: 90rpx;
+    background-color: #FFD700;
+    color: #333;
+    border-radius: 45rpx;
+    font-size: 32rpx;
+    font-weight: bold;
+    margin-top: 40rpx;
+    margin-bottom: 40rpx;
+    border: none;
+    
+    &::after {
+      border: none;
+    }
+    
+    &.wechat-btn {
+      background-color: #07c160;
+      color: #fff;
+    }
+  }
+  
+  .agreement {
+    font-size: 24rpx;
+    color: #999;
+    text-align: center;
+    
+    .link {
+      color: #576b95;
+    }
+  }
+}
+</style>

+ 61 - 7
haha-mp/src/pages/orders/orders.vue

@@ -1,7 +1,27 @@
 <template>
   <view class="container">
+    <!-- 无数据提示 -->
+    <view v-if="!loading && orders.length === 0" class="empty-container">
+      <view class="empty-icon">
+        <svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
+          <!-- 空箱子 -->
+          <rect x="60" y="80" width="80" height="70" rx="4" fill="#E8E8E8" stroke="#CCCCCC" stroke-width="2"/>
+          <!-- 箱子盖子 -->
+          <path d="M 55 80 L 100 60 L 145 80" fill="#F5F5F5" stroke="#CCCCCC" stroke-width="2"/>
+          <!-- 箱子细节线 -->
+          <line x1="100" y1="80" x2="100" y2="150" stroke="#CCCCCC" stroke-width="1" stroke-dasharray="3,3"/>
+          <!-- 表情 -->
+          <circle cx="85" cy="110" r="3" fill="#999999"/>
+          <circle cx="115" cy="110" r="3" fill="#999999"/>
+          <path d="M 85 125 Q 100 120 115 125" stroke="#999999" stroke-width="2" fill="none" stroke-linecap="round"/>
+        </svg>
+      </view>
+      <text class="empty-text">暂无订单记录</text>
+      <text class="empty-tip">快去购物吧~</text>
+    </view>
+    
     <!-- 订单列表 -->
-    <view class="order-list">
+    <view v-else class="order-list">
       <view v-for="order in orders" :key="order.id" class="order-item">
         <view class="order-header">
           <view class="time-invoice-container">
@@ -90,12 +110,13 @@ const loadOrders = async () => {
     
     uni.hideLoading();
     
-    if (orderList.length === 0) {
-      uni.showToast({
-        title: '暂无订单',
-        icon: 'none'
-      });
-    }
+    // 无数据时不显示toast,由空状态提示展示
+    // if (orderList.length === 0) {
+    //   uni.showToast({
+    //     title: '暂无订单',
+    //     icon: 'none'
+    //   });
+    // }
   } catch (error: any) {
     uni.hideLoading();
     console.error('加载订单列表失败:', error);
@@ -139,6 +160,39 @@ const viewOrderDetail = (order: OrderInfo) => {
   padding-top: 0;
 }
 
+/* 无数据提示 */
+.empty-container {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  min-height: 80vh;
+  padding: 40rpx;
+}
+
+.empty-icon {
+  width: 300rpx;
+  height: 300rpx;
+  margin-bottom: 40rpx;
+}
+
+.empty-icon svg {
+  width: 100%;
+  height: 100%;
+}
+
+.empty-text {
+  font-size: 32rpx;
+  color: #666666;
+  margin-bottom: 16rpx;
+  font-weight: 500;
+}
+
+.empty-tip {
+  font-size: 28rpx;
+  color: #999999;
+}
+
 /* 订单列表 */
 .order-list {
   padding: 10rpx 0 30rpx;

+ 95 - 0
haha-mp/src/utils/auth.ts

@@ -0,0 +1,95 @@
+/**
+ * 认证工具函数
+ * 提供登录状态检查、token管理等功能
+ */
+
+/**
+ * 获取本地存储的token
+ */
+export const getToken = (): string => {
+  return uni.getStorageSync('haha-token-KuaiyuMan') || '';
+};
+
+/**
+ * 保存token到本地存储
+ */
+export const setToken = (token: string): void => {
+  uni.setStorageSync('haha-token-KuaiyuMan', token);
+};
+
+/**
+ * 清除本地存储的token
+ */
+export const removeToken = (): void => {
+  uni.removeStorageSync('haha-token-KuaiyuMan');
+};
+
+/**
+ * 检查是否已登录
+ */
+export const isLoggedIn = (): boolean => {
+  return !!getToken();
+};
+
+/**
+ * 获取用户信息
+ */
+export const getUserInfo = (): any => {
+  return uni.getStorageSync('haha-user-info') || null;
+};
+
+/**
+ * 保存用户信息
+ */
+export const setUserInfo = (userInfo: any): void => {
+  uni.setStorageSync('haha-user-info', userInfo);
+};
+
+/**
+ * 清除用户信息
+ */
+export const removeUserInfo = (): void => {
+  uni.removeStorageSync('haha-user-info');
+};
+
+/**
+ * 清除所有登录信息
+ */
+export const clearAuth = (): void => {
+  removeToken();
+  removeUserInfo();
+};
+
+/**
+ * 路由守卫:检查登录状态
+ * 如果未登录,则跳转到登录页
+ * @param redirectUrl 登录后要跳转的页面(可选)
+ * @returns 是否已登录
+ */
+export const checkAuth = (redirectUrl?: string): boolean => {
+  if (!isLoggedIn()) {
+    // 构建登录页URL,带上redirect参数
+    let loginUrl = '/pages/login/login';
+    if (redirectUrl) {
+      loginUrl += `?redirect=${encodeURIComponent(redirectUrl)}`;
+    }
+    
+    uni.reLaunch({
+      url: loginUrl
+    });
+    
+    return false;
+  }
+  
+  return true;
+};
+
+/**
+ * 退出登录
+ */
+export const logout = (): void => {
+  clearAuth();
+  uni.reLaunch({
+    url: '/pages/login/login'
+  });
+};

+ 6 - 10
haha-mp/src/utils/request.ts

@@ -4,6 +4,7 @@
  */
 
 import { API_CONFIG } from './config';
+import { getToken, removeToken } from './auth';
 
 /**
  * 请求配置接口
@@ -25,13 +26,6 @@ interface ResponseData<T = any> {
   data?: T;
 }
 
-/**
- * 从本地存储获取token
- */
-const getToken = (): string => {
-  return uni.getStorageSync('haha-token-KuaiyuMan') || '';
-};
-
 /**
  * 发起HTTP请求
  * @param config 请求配置
@@ -97,9 +91,11 @@ export const request = <T = any>(config: RequestConfig): Promise<T> => {
             icon: 'none'
           });
           // 清除token
-          uni.removeStorageSync('haha-token-KuaiyuMan');
-          // 跳转到登录页(如果有)
-          // uni.navigateTo({ url: '/pages/login/login' });
+          removeToken();
+          // 跳转到登录页
+          setTimeout(() => {
+            uni.reLaunch({ url: '/pages/login/login' });
+          }, 1500);
           reject(new Error(responseData.message || '未登录'));
         } else {
           // 业务错误

+ 2 - 2
haha-sdk/pom.xml

@@ -13,8 +13,8 @@
     <description>哈哈零售系统 Java SDK</description>
 
     <properties>
-        <maven.compiler.source>17</maven.compiler.source>
-        <maven.compiler.target>17</maven.compiler.target>
+        <maven.compiler.source>21</maven.compiler.source>
+        <maven.compiler.target>21</maven.compiler.target>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
         <okhttp.version>4.12.0</okhttp.version>
         <fastjson2.version>2.0.53</fastjson2.version>