Просмотр исходного кода

首页数据看板,库存管理等

skyline 3 месяцев назад
Родитель
Сommit
7dcdbd592f
27 измененных файлов с 1791 добавлено и 47 удалено
  1. 48 4
      haha-admin/src/main/java/com/haha/admin/config/RedisConfig.java
  2. 78 0
      haha-admin/src/main/java/com/haha/admin/controller/DashboardController.java
  3. 33 2
      haha-admin/src/main/java/com/haha/admin/controller/InventoryController.java
  4. 12 22
      haha-admin/src/main/java/com/haha/admin/controller/ProductController.java
  5. 163 0
      haha-admin/src/main/java/com/haha/admin/controller/ShopController.java
  6. 31 4
      haha-admin/src/main/resources/application.yml
  7. 16 0
      haha-admin/src/main/resources/sql/device.sql
  8. 103 0
      haha-admin/src/main/resources/sql/inventory.sql
  9. 138 0
      haha-admin/src/main/resources/sql/inventory_test_data.sql
  10. 48 0
      haha-admin/src/main/resources/sql/new_product_apply.sql
  11. 26 0
      haha-admin/src/main/resources/sql/product.sql
  12. 27 0
      haha-admin/src/main/resources/sql/shop.sql
  13. 27 0
      haha-admin/src/main/resources/sql/shop_test_data.sql
  14. 37 0
      haha-admin/src/main/resources/sql/sync.sql
  15. 0 6
      haha-entity/src/main/java/com/haha/entity/Product.java
  16. 81 0
      haha-entity/src/main/java/com/haha/entity/ShopReplenisher.java
  17. 58 0
      haha-mapper/src/main/java/com/haha/mapper/DeviceInventoryMapper.java
  18. 40 0
      haha-mapper/src/main/java/com/haha/mapper/ShopReplenisherMapper.java
  19. 47 2
      haha-miniapp/src/main/java/com/haha/miniapp/config/RedisConfig.java
  20. 19 4
      haha-miniapp/src/main/resources/application.yml
  21. 3 3
      haha-miniapp/src/main/resources/sql/order.sql
  22. 30 0
      haha-service/src/main/java/com/haha/service/DashboardService.java
  23. 10 0
      haha-service/src/main/java/com/haha/service/DeviceInventoryService.java
  24. 68 0
      haha-service/src/main/java/com/haha/service/ShopReplenisherService.java
  25. 436 0
      haha-service/src/main/java/com/haha/service/impl/DashboardServiceImpl.java
  26. 59 0
      haha-service/src/main/java/com/haha/service/impl/DeviceInventoryServiceImpl.java
  27. 153 0
      haha-service/src/main/java/com/haha/service/impl/ShopReplenisherServiceImpl.java

+ 48 - 4
haha-admin/src/main/java/com/haha/admin/config/RedisConfig.java

@@ -1,16 +1,60 @@
 package com.haha.admin.config;
 
+import io.lettuce.core.ClientOptions;
+import io.lettuce.core.SocketOptions;
+import io.lettuce.core.protocol.ProtocolVersion;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
+import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
+import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
+import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
+
+import java.time.Duration;
 
 /**
  * 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 配置
+
+    @Bean
+    public RedisConnectionFactory redisConnectionFactory(RedisProperties redisProperties) {
+        // 配置 Socket 选项 - 连接超时和保持连接
+        SocketOptions socketOptions = SocketOptions.builder()
+                .connectTimeout(Duration.ofMillis(redisProperties.getTimeout().toMillis()))
+                .keepAlive(true)
+                .build();
+
+        // 配置客户端选项 - 自动重连,使用RESP2协议避免HELLO认证问题
+        ClientOptions clientOptions = ClientOptions.builder()
+                .socketOptions(socketOptions)
+                .autoReconnect(true)
+                .protocolVersion(ProtocolVersion.RESP2)
+                .disconnectedBehavior(ClientOptions.DisconnectedBehavior.REJECT_COMMANDS)
+                .build();
+
+        // 配置连接池
+        LettucePoolingClientConfiguration poolConfig = LettucePoolingClientConfiguration.builder()
+                .commandTimeout(redisProperties.getTimeout())
+                .clientOptions(clientOptions)
+                .build();
+
+        // 创建Redis单机配置,设置host、port、password和database
+        RedisStandaloneConfiguration redisConfig = new RedisStandaloneConfiguration();
+        redisConfig.setHostName(redisProperties.getHost());
+        redisConfig.setPort(redisProperties.getPort());
+        redisConfig.setPassword(redisProperties.getPassword());
+        redisConfig.setDatabase(redisProperties.getDatabase());
+
+        log.info("Redis 增强配置已加载: host={}, port={}, database={}, 连接超时={}ms, 协议版本=RESP2", 
+                redisProperties.getHost(), redisProperties.getPort(), 
+                redisProperties.getDatabase(), redisProperties.getTimeout().toMillis());
+
+        return new LettuceConnectionFactory(redisConfig, poolConfig);
+    }
 }

+ 78 - 0
haha-admin/src/main/java/com/haha/admin/controller/DashboardController.java

@@ -0,0 +1,78 @@
+package com.haha.admin.controller;
+
+import com.haha.common.vo.Result;
+import com.haha.service.DashboardService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Map;
+
+/**
+ * 首页数据看板控制器
+ */
+@Slf4j
+@RestController
+@RequestMapping("/dashboard")
+public class DashboardController {
+
+    @Autowired
+    private DashboardService dashboardService;
+
+    /**
+     * 获取概览数据(今日实时数据)
+     */
+    @GetMapping("/overview")
+    public Result<Map<String, Object>> getOverview() {
+        try {
+            Map<String, Object> data = dashboardService.getOverviewData();
+            return Result.success("查询成功", data);
+        } catch (Exception e) {
+            log.error("获取概览数据失败: {}", e.getMessage(), e);
+            return Result.error(500, "获取概览数据失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 获取统计数据(按时间维度)
+     * @param type 时间类型:yesterday/week/month/days30
+     */
+    @GetMapping("/statistics")
+    public Result<Map<String, Object>> getStatistics(@RequestParam(defaultValue = "week") String type) {
+        try {
+            Map<String, Object> data = dashboardService.getStatisticsData(type);
+            return Result.success("查询成功", data);
+        } catch (Exception e) {
+            log.error("获取统计数据失败: {}", e.getMessage(), e);
+            return Result.error(500, "获取统计数据失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 获取快捷入口数据
+     */
+    @GetMapping("/quick-links")
+    public Result<Map<String, Object>> getQuickLinks() {
+        try {
+            Map<String, Object> data = dashboardService.getQuickLinksData();
+            return Result.success("查询成功", data);
+        } catch (Exception e) {
+            log.error("获取快捷入口数据失败: {}", e.getMessage(), e);
+            return Result.error(500, "获取快捷入口数据失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 获取设备状态分布
+     */
+    @GetMapping("/device-status")
+    public Result<Object> getDeviceStatus() {
+        try {
+            Object data = dashboardService.getDeviceStatusData();
+            return Result.success("查询成功", data);
+        } catch (Exception e) {
+            log.error("获取设备状态分布失败: {}", e.getMessage(), e);
+            return Result.error(500, "获取设备状态分布失败: " + e.getMessage());
+        }
+    }
+}

+ 33 - 2
haha-admin/src/main/java/com/haha/admin/controller/InventoryController.java

@@ -36,6 +36,35 @@ public class InventoryController {
 
     // ==================== 库存查询 ====================
 
+    /**
+     * 分页查询设备库存统计列表
+     * 按设备分组展示库存汇总信息
+     */
+    @GetMapping("/device-stats")
+    public Result<Map<String, Object>> deviceStats(@RequestParam Map<String, Object> params) {
+        int page = 1;
+        int pageSize = 10;
+
+        Object pageObj = params.get("page");
+        if (pageObj != null && !pageObj.toString().isEmpty()) {
+            page = Integer.parseInt(pageObj.toString());
+        }
+        Object pageSizeObj = params.get("pageSize");
+        if (pageSizeObj != null && !pageSizeObj.toString().isEmpty()) {
+            pageSize = Integer.parseInt(pageSizeObj.toString());
+        }
+
+        IPage<Map<String, Object>> pageResult = deviceInventoryService.getDeviceInventoryStats(page, pageSize, params);
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("list", pageResult.getRecords());
+        result.put("total", pageResult.getTotal());
+        result.put("page", page);
+        result.put("pageSize", pageSize);
+
+        return Result.success(result);
+    }
+
     /**
      * 分页查询库存列表
      */
@@ -73,9 +102,11 @@ public class InventoryController {
      * 查询设备库存详情
      */
     @GetMapping("/device/{deviceId}")
-    public Result<List<Map<String, Object>>> getDeviceInventory(@PathVariable String deviceId) {
+    public Result<Map<String, Object>> getDeviceInventory(@PathVariable String deviceId) {
         List<Map<String, Object>> inventory = deviceInventoryService.getInventoryWithProduct(deviceId);
-        return Result.success(inventory);
+        Map<String, Object> result = new HashMap<>();
+        result.put("list", inventory);
+        return Result.success(result);
     }
 
     /**

+ 12 - 22
haha-admin/src/main/java/com/haha/admin/controller/ProductController.java

@@ -64,7 +64,7 @@ public class ProductController {
                    .eq(category != null && !category.isEmpty(), Product::getType, category)
                    .eq(syncStatus != null, Product::getSyncStatus, syncStatus)
                    .eq(Product::getIsDeleted, 0);  // 只查询未删除的
-            
+
             // 按创建时间倒序
             wrapper.orderByDesc(Product::getCreateTime);
 
@@ -133,10 +133,6 @@ public class ProductController {
                     .count();
             statistics.put("syncedProducts", syncedProducts);
 
-            // TODO: 库存和库存总值需要从其他服务获取
-            statistics.put("totalStock", 0);
-            statistics.put("totalValue", "0.00");
-
             return Result.success("查询成功", statistics);
 
         } catch (Exception e) {
@@ -157,7 +153,7 @@ public class ProductController {
             product.setSyncStatus(0);
             product.setCreateTime(LocalDateTime.now());
             product.setUpdateTime(LocalDateTime.now());
-            
+
             boolean success = productService.save(product);
             if (success) {
                 return Result.success("添加成功", product);
@@ -183,10 +179,10 @@ public class ProductController {
             if (existing == null) {
                 return Result.error(404, "商品不存在");
             }
-            
+
             product.setId(id);
             product.setUpdateTime(LocalDateTime.now());
-            
+
             boolean success = productService.updateById(product);
             if (success) {
                 return Result.success("更新成功", product);
@@ -211,12 +207,12 @@ public class ProductController {
             if (product == null) {
                 return Result.error(404, "商品不存在");
             }
-            
+
             // 软删除
             product.setIsDeleted(1);
             product.setUpdateTime(LocalDateTime.now());
             productService.updateById(product);
-            
+
             return Result.success("删除成功", null);
         } catch (Exception e) {
             log.error("删除商品失败: productId={}, error={}", id, e.getMessage(), e);
@@ -236,14 +232,14 @@ public class ProductController {
             if (product == null) {
                 return Result.error(404, "商品不存在");
             }
-            
+
             log.info("同步商品到哈哈平台: productId={}, name={}", id, product.getName());
-            
+
             // TODO: 调用SDK同步商品到哈哈平台
-            
+
             // 更新同步状态
             productService.updateSyncStatus(id, 2);
-            
+
             return Result.success("同步成功", null);
         } catch (Exception e) {
             log.error("同步商品失败: productId={}, error={}", id, e.getMessage(), e);
@@ -280,12 +276,12 @@ public class ProductController {
                     product.setSyncStatusColor("info");
             }
         }
-        
+
         // 图片URL
         if (product.getImageUrl() == null && product.getPic() != null) {
             product.setImageUrl(product.getPic());
         }
-        
+
         // 默认值
         if (product.getCategory() == null) {
             product.setCategory(product.getType());
@@ -296,11 +292,5 @@ public class ProductController {
         if (product.getCost() == null && product.getCostPrice() != null) {
             product.setCost(product.getCostPrice());
         }
-        if (product.getStock() == null) {
-            product.setStock(0);
-        }
-        if (product.getSoldCount() == null) {
-            product.setSoldCount(0);
-        }
     }
 }

+ 163 - 0
haha-admin/src/main/java/com/haha/admin/controller/ShopController.java

@@ -1,11 +1,16 @@
 package com.haha.admin.controller;
 
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.haha.common.vo.Result;
 import com.haha.entity.Shop;
 import com.haha.entity.Device;
+import com.haha.entity.ShopReplenisher;
+import com.haha.entity.Admin;
 import com.haha.service.ShopService;
 import com.haha.service.DeviceService;
+import com.haha.service.ShopReplenisherService;
+import com.haha.service.AdminService;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
@@ -28,6 +33,12 @@ public class ShopController {
     @Autowired
     private DeviceService deviceService;
 
+    @Autowired
+    private ShopReplenisherService shopReplenisherService;
+
+    @Autowired
+    private AdminService adminService;
+
     /**
      * 分页查询门店列表
      * @param params 查询参数
@@ -326,4 +337,156 @@ public class ShopController {
             return Result.error(500, "查询失败: " + e.getMessage());
         }
     }
+
+    // ==================== 补货员管理 ====================
+
+    /**
+     * 获取门店补货员列表
+     * @param id 门店ID
+     * @return 补货员列表
+     */
+    @GetMapping("/{id}/replenishers")
+    public Result<List<ShopReplenisher>> getReplenishers(@PathVariable Long id) {
+        try {
+            List<ShopReplenisher> replenishers = shopReplenisherService.getReplenishersByShopId(id);
+            return Result.success("查询成功", replenishers);
+        } catch (Exception e) {
+            log.error("查询补货员失败: shopId={}, error={}", id, e.getMessage(), e);
+            return Result.error(500, "查询失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 添加补货员
+     * @param id 门店ID
+     * @param params 管理员ID参数
+     * @return 操作结果
+     */
+    @PostMapping("/{id}/replenishers")
+    public Result<String> addReplenisher(@PathVariable Long id, @RequestBody Map<String, Object> params) {
+        try {
+            Long adminId = Long.valueOf(params.get("adminId").toString());
+            return shopReplenisherService.addReplenisher(id, adminId);
+        } catch (Exception e) {
+            log.error("添加补货员失败: shopId={}, error={}", id, e.getMessage(), e);
+            return Result.error(500, "添加失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 批量添加补货员
+     * @param id 门店ID
+     * @param params 管理员ID列表参数
+     * @return 操作结果
+     */
+    @PostMapping("/{id}/replenishers/batch")
+    public Result<Map<String, Object>> batchAddReplenishers(@PathVariable Long id, @RequestBody Map<String, Object> params) {
+        try {
+            @SuppressWarnings("unchecked")
+            List<Integer> adminIdInts = (List<Integer>) params.get("adminIds");
+            List<Long> adminIds = adminIdInts.stream()
+                    .map(Integer::longValue)
+                    .toList();
+
+            int count = shopReplenisherService.batchAddReplenishers(id, adminIds);
+
+            Map<String, Object> result = new HashMap<>();
+            result.put("success", count);
+            result.put("total", adminIds.size());
+
+            return Result.success("批量添加完成", result);
+        } catch (Exception e) {
+            log.error("批量添加补货员失败: shopId={}, error={}", id, e.getMessage(), e);
+            return Result.error(500, "批量添加失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 移除补货员
+     * @param id 门店ID
+     * @param adminId 管理员ID
+     * @return 操作结果
+     */
+    @DeleteMapping("/{id}/replenishers/{adminId}")
+    public Result<String> removeReplenisher(@PathVariable Long id, @PathVariable Long adminId) {
+        try {
+            return shopReplenisherService.removeReplenisher(id, adminId);
+        } catch (Exception e) {
+            log.error("移除补货员失败: shopId={}, adminId={}, error={}", id, adminId, e.getMessage(), e);
+            return Result.error(500, "移除失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 批量移除补货员
+     * @param id 门店ID
+     * @param params 管理员ID列表参数
+     * @return 操作结果
+     */
+    @DeleteMapping("/{id}/replenishers/batch")
+    public Result<Map<String, Object>> batchRemoveReplenishers(@PathVariable Long id, @RequestBody Map<String, Object> params) {
+        try {
+            @SuppressWarnings("unchecked")
+            List<Integer> adminIdInts = (List<Integer>) params.get("adminIds");
+            List<Long> adminIds = adminIdInts.stream()
+                    .map(Integer::longValue)
+                    .toList();
+
+            // 执行批量删除
+            LambdaQueryWrapper<ShopReplenisher> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(ShopReplenisher::getShopId, id)
+                    .in(ShopReplenisher::getAdminId, adminIds);
+            int count = (int) shopReplenisherService.count(wrapper);
+            shopReplenisherService.remove(wrapper);
+
+            Map<String, Object> result = new HashMap<>();
+            result.put("success", count);
+            result.put("total", adminIds.size());
+
+            return Result.success("批量移除完成", result);
+        } catch (Exception e) {
+            log.error("批量移除补货员失败: shopId={}, error={}", id, e.getMessage(), e);
+            return Result.error(500, "批量移除失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 更新补货员状态
+     * @param id 补货员关联ID
+     * @param params 状态参数
+     * @return 操作结果
+     */
+    @PutMapping("/replenishers/{id}/status")
+    public Result<String> updateReplenisherStatus(@PathVariable Long id, @RequestBody Map<String, Object> params) {
+        try {
+            Integer status = Integer.valueOf(params.get("status").toString());
+            return shopReplenisherService.updateStatus(id, status);
+        } catch (Exception e) {
+            log.error("更新补货员状态失败: id={}, error={}", id, e.getMessage(), e);
+            return Result.error(500, "更新失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 获取可选的管理员列表(用于添加补货员)
+     * @return 管理员列表
+     */
+    @GetMapping("/available-users")
+    public Result<List<Admin>> getAvailableUsers(@RequestParam(required = false) String keyword) {
+        try {
+            LambdaQueryWrapper<Admin> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(Admin::getStatus, 1); // 只查询正常状态的管理员
+            if (keyword != null && !keyword.isEmpty()) {
+                wrapper.and(w -> w.like(Admin::getRealName, keyword)
+                        .or().like(Admin::getPhone, keyword)
+                        .or().like(Admin::getUsername, keyword));
+            }
+            wrapper.orderByDesc(Admin::getCreateTime);
+            List<Admin> admins = adminService.list(wrapper);
+            return Result.success("查询成功", admins);
+        } catch (Exception e) {
+            log.error("查询可选管理员失败: error={}", e.getMessage(), e);
+            return Result.error(500, "查询失败: " + e.getMessage());
+        }
+    }
 }

+ 31 - 4
haha-admin/src/main/resources/application.yml

@@ -4,10 +4,26 @@ spring:
 
   # 数据库配置
   datasource:
-    url: jdbc:mysql://server.kuaiyuman.cn:3306/haha?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false
+    url: jdbc:mysql://server.kuaiyuman.cn:3306/haha?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&useServerPrepStmts=true&cachePrepStmts=true&prepStmtCacheSize=250&prepStmtCacheSqlLimit=2048
     username: root
     password: KuaiyuMan/*-
     driver-class-name: com.mysql.cj.jdbc.Driver
+    # HikariCP 连接池配置
+    hikari:
+      # 最小空闲连接数
+      minimum-idle: 3
+      # 最大连接池大小
+      maximum-pool-size: 10
+      # 连接最大存活时间(毫秒),建议小于数据库的 wait_timeout
+      max-lifetime: 1800000
+      # 空闲连接超时时间
+      idle-timeout: 300000
+      # 连接超时时间
+      connection-timeout: 30000
+      # 连接测试查询
+      connection-test-query: SELECT 1
+      # 保持连接活跃的频率(毫秒)
+      keepalive-time: 30000
 
   # Redis 配置
   data:
@@ -17,12 +33,23 @@ spring:
       password: KtXA^Zx!TZmLEy(@JjB@2(TVG0kdy5)&
       database: 9
       timeout: 10000
+      # 连接配置
       lettuce:
         pool:
-          max-active: 8
-          max-wait: -1
+          max-active: 10
+          max-wait: 3000
           max-idle: 8
-          min-idle: 0
+          min-idle: 2
+          # 连接耗尽时等待时间(毫秒)
+          time-between-eviction-runs: 30000
+        # 关闭超时时间
+        shutdown-timeout: 100
+        # 刷新配置
+        refresh:
+          # 刷新周期(毫秒)
+          period: 10000
+          # 最小刷新间隔
+          min: 5000
 
 # MyBatis-Plus 配置
 mybatis-plus:

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

@@ -0,0 +1,16 @@
+-- 创建设备表
+CREATE TABLE IF NOT EXISTS `t_device` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `device_id` VARCHAR(64) NOT NULL COMMENT '设备编号',
+  `shop_id` BIGINT DEFAULT NULL COMMENT '门店ID',
+  `name` VARCHAR(128) DEFAULT NULL COMMENT '设备名称',
+  `auth_token` VARCHAR(256) DEFAULT NULL COMMENT '认证令牌',
+  `status` INT DEFAULT 1 COMMENT '状态:0-禁用,1-正常',
+  `current_inventory_hash` VARCHAR(64) DEFAULT NULL COMMENT '当前库存哈希值',
+  `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_device_id` (`device_id`),
+  KEY `idx_shop_id` (`shop_id`),
+  KEY `idx_status` (`status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='设备表';

+ 103 - 0
haha-admin/src/main/resources/sql/inventory.sql

@@ -0,0 +1,103 @@
+-- 设备商品库存表
+CREATE TABLE IF NOT EXISTS `t_device_inventory` (
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+    `device_id` VARCHAR(50) NOT NULL COMMENT '设备ID(SN号)',
+    `product_id` BIGINT NOT NULL COMMENT '商品ID(本地)',
+    `product_code` VARCHAR(50) DEFAULT NULL COMMENT '商品编码(哈哈平台code)',
+    `product_name` VARCHAR(200) DEFAULT NULL COMMENT '商品名称(冗余)',
+    `stock` INT NOT NULL DEFAULT 0 COMMENT '当前库存数量',
+    `shelf_num` INT DEFAULT NULL COMMENT '所在货架层号',
+    `position` VARCHAR(20) DEFAULT NULL COMMENT '货道位置(left/right)',
+    `warning_threshold` INT DEFAULT 5 COMMENT '库存预警阈值',
+    `last_restock_time` DATETIME DEFAULT NULL 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_device_product` (`device_id`, `product_id`),
+    INDEX `idx_device_id` (`device_id`),
+    INDEX `idx_product_id` (`product_id`),
+    INDEX `idx_stock` (`stock`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='设备商品库存表';
+
+-- 库存变动日志表
+CREATE TABLE IF NOT EXISTS `t_inventory_log` (
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+    `device_id` VARCHAR(50) NOT NULL COMMENT '设备ID',
+    `product_id` BIGINT NOT NULL COMMENT '商品ID',
+    `product_code` VARCHAR(50) DEFAULT NULL COMMENT '商品编码',
+    `product_name` VARCHAR(200) DEFAULT NULL COMMENT '商品名称',
+    `change_type` TINYINT NOT NULL COMMENT '变动类型:1上货增加,2销售减少,3调整增加,4调整减少,5盘点调整',
+    `change_quantity` INT NOT NULL COMMENT '变动数量(正数增加,负数减少)',
+    `before_stock` INT NOT NULL COMMENT '变动前库存',
+    `after_stock` INT NOT NULL COMMENT '变动后库存',
+    `order_id` VARCHAR(50) DEFAULT NULL COMMENT '关联订单号(销售时)',
+    `activity_id` VARCHAR(50) DEFAULT NULL COMMENT '关联活动号',
+    `operator_id` BIGINT DEFAULT NULL COMMENT '操作人ID',
+    `operator_name` VARCHAR(100) DEFAULT NULL COMMENT '操作人名称',
+    `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
+    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    PRIMARY KEY (`id`),
+    INDEX `idx_device_id` (`device_id`),
+    INDEX `idx_product_id` (`product_id`),
+    INDEX `idx_change_type` (`change_type`),
+    INDEX `idx_create_time` (`create_time`),
+    INDEX `idx_order_id` (`order_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='库存变动日志表';
+
+-- 上货记录表
+CREATE TABLE IF NOT EXISTS `t_stock_record` (
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+    `device_id` VARCHAR(50) NOT NULL COMMENT '设备ID',
+    `activity_id` VARCHAR(50) DEFAULT NULL COMMENT '哈哈平台活动号',
+    `stock_type` TINYINT DEFAULT 1 COMMENT '类型:1上货,2补货',
+    `stocker_id` BIGINT DEFAULT NULL COMMENT '上货员ID',
+    `stocker_name` VARCHAR(100) DEFAULT NULL COMMENT '上货员名称',
+    `stocker_phone` VARCHAR(20) DEFAULT NULL COMMENT '上货员电话',
+    `total_items` INT DEFAULT 0 COMMENT '上货商品种类数',
+    `total_quantity` INT DEFAULT 0 COMMENT '上货总数量',
+    `status` TINYINT DEFAULT 1 COMMENT '状态:1进行中,2已完成,3已取消',
+    `start_time` DATETIME DEFAULT NULL COMMENT '开始时间',
+    `end_time` DATETIME DEFAULT NULL COMMENT '结束时间',
+    `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
+    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    PRIMARY KEY (`id`),
+    INDEX `idx_device_id` (`device_id`),
+    INDEX `idx_stocker_id` (`stocker_id`),
+    INDEX `idx_create_time` (`create_time`),
+    INDEX `idx_activity_id` (`activity_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='上货记录表';
+
+-- 上货记录明细表
+CREATE TABLE IF NOT EXISTS `t_stock_record_item` (
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+    `record_id` BIGINT NOT NULL COMMENT '上货记录ID',
+    `device_id` VARCHAR(50) NOT NULL COMMENT '设备ID',
+    `product_id` BIGINT NOT NULL COMMENT '商品ID',
+    `product_code` VARCHAR(50) DEFAULT NULL COMMENT '商品编码',
+    `product_name` VARCHAR(200) DEFAULT NULL COMMENT '商品名称',
+    `shelf_num` INT DEFAULT NULL COMMENT '货架层号',
+    `quantity` INT NOT NULL COMMENT '上货数量',
+    `before_stock` INT DEFAULT 0 COMMENT '上货前库存',
+    `after_stock` INT DEFAULT 0 COMMENT '上货后库存',
+    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    PRIMARY KEY (`id`),
+    INDEX `idx_record_id` (`record_id`),
+    INDEX `idx_device_product` (`device_id`, `product_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='上货记录明细表';
+
+-- 上货员表
+CREATE TABLE IF NOT EXISTS `t_stocker` (
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+    `name` VARCHAR(100) NOT NULL COMMENT '姓名',
+    `phone` VARCHAR(20) DEFAULT NULL COMMENT '电话',
+    `employee_id` VARCHAR(50) DEFAULT NULL COMMENT '工号',
+    `status` TINYINT DEFAULT 1 COMMENT '状态:1启用,0禁用',
+    `total_tasks` INT DEFAULT 0 COMMENT '累计任务数',
+    `last_task_time` DATETIME DEFAULT NULL COMMENT '最后任务时间',
+    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    PRIMARY KEY (`id`),
+    INDEX `idx_phone` (`phone`),
+    INDEX `idx_status` (`status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='上货员表';

+ 138 - 0
haha-admin/src/main/resources/sql/inventory_test_data.sql

@@ -0,0 +1,138 @@
+-- ============================================
+-- 库存管理测试数据
+-- ============================================
+
+-- 清空现有数据(按外键依赖顺序删除)
+DELETE FROM t_inventory_log WHERE id > 0;
+DELETE FROM t_stock_record_item WHERE id > 0;
+DELETE FROM t_stock_record WHERE id > 0;
+DELETE FROM t_device_inventory WHERE id > 0;
+DELETE FROM t_stocker WHERE id > 0;
+
+-- ============================================
+-- 1. 上货员测试数据
+-- ============================================
+INSERT INTO t_stocker (id, name, phone, employee_id, status, total_tasks, last_task_time, create_time) VALUES
+(1, '张三', '13800138001', 'ST001', 1, 15, '2026-02-10 14:30:00', '2025-12-01 08:00:00'),
+(2, '李四', '13800138002', 'ST002', 1, 12, '2026-02-10 16:45:00', '2025-12-05 09:00:00'),
+(3, '王五', '13800138003', 'ST003', 1, 8, '2026-02-09 10:20:00', '2025-12-10 10:00:00'),
+(4, '赵六', '13800138004', 'ST004', 0, 5, '2026-01-15 11:00:00', '2025-12-15 08:30:00'),
+(5, '钱七', '13800138005', 'ST005', 1, 20, '2026-02-10 17:30:00', '2025-11-20 09:00:00');
+
+-- ============================================
+-- 2. 设备库存测试数据
+-- 假设设备ID为:DEV001, DEV002, DEV003
+-- 假设商品ID为:1-10
+-- ============================================
+
+-- 设备DEV001的库存
+INSERT INTO t_device_inventory (device_id, product_id, product_code, product_name, stock, shelf_num, position, warning_threshold, last_restock_time, create_time, update_time) VALUES
+('DEV001', 1, 'P001', '可口可乐330ml', 15, 1, 'left', 5, '2026-02-10 10:00:00', '2026-01-15 08:00:00', '2026-02-10 10:00:00'),
+('DEV001', 2, 'P002', '百事可乐330ml', 3, 1, 'right', 5, '2026-02-08 14:00:00', '2026-01-15 08:00:00', '2026-02-08 14:00:00'),
+('DEV001', 3, 'P003', '农夫山泉550ml', 20, 2, 'left', 8, '2026-02-10 10:00:00', '2026-01-15 08:00:00', '2026-02-10 10:00:00'),
+('DEV001', 4, 'P004', '康师傅红烧牛肉面', 8, 2, 'right', 5, '2026-02-09 11:00:00', '2026-01-15 08:00:00', '2026-02-09 11:00:00'),
+('DEV001', 5, 'P005', '奥利奥饼干', 0, 3, 'left', 3, '2026-02-05 09:00:00', '2026-01-15 08:00:00', '2026-02-05 09:00:00'),
+('DEV001', 6, 'P006', '乐事薯片原味', 12, 3, 'right', 5, '2026-02-10 10:00:00', '2026-01-15 08:00:00', '2026-02-10 10:00:00'),
+('DEV001', 7, 'P007', '德芙巧克力', 6, 4, 'left', 3, '2026-02-09 15:00:00', '2026-01-15 08:00:00', '2026-02-09 15:00:00'),
+('DEV001', 8, 'P008', '脉动维生素饮料', 10, 4, 'right', 5, '2026-02-10 10:00:00', '2026-01-15 08:00:00', '2026-02-10 10:00:00');
+
+-- 设备DEV002的库存
+INSERT INTO t_device_inventory (device_id, product_id, product_code, product_name, stock, shelf_num, position, warning_threshold, last_restock_time, create_time, update_time) VALUES
+('DEV002', 1, 'P001', '可口可乐330ml', 25, 1, 'left', 5, '2026-02-10 09:00:00', '2026-01-16 09:00:00', '2026-02-10 09:00:00'),
+('DEV002', 2, 'P002', '百事可乐330ml', 18, 1, 'right', 5, '2026-02-09 16:00:00', '2026-01-16 09:00:00', '2026-02-09 16:00:00'),
+('DEV002', 3, 'P003', '农夫山泉550ml', 30, 2, 'left', 8, '2026-02-10 09:00:00', '2026-01-16 09:00:00', '2026-02-10 09:00:00'),
+('DEV002', 9, 'P009', '旺旺雪饼', 4, 2, 'right', 5, '2026-02-07 10:00:00', '2026-01-16 09:00:00', '2026-02-07 10:00:00'),
+('DEV002', 10, 'P010', '红牛功能饮料', 8, 3, 'left', 5, '2026-02-08 14:00:00', '2026-01-16 09:00:00', '2026-02-08 14:00:00');
+
+-- 设备DEV003的库存
+INSERT INTO t_device_inventory (device_id, product_id, product_code, product_name, stock, shelf_num, position, warning_threshold, last_restock_time, create_time, update_time) VALUES
+('DEV003', 4, 'P004', '康师傅红烧牛肉面', 22, 1, 'left', 5, '2026-02-10 11:00:00', '2026-01-17 10:00:00', '2026-02-10 11:00:00'),
+('DEV003', 5, 'P005', '奥利奥饼干', 2, 1, 'right', 3, '2026-02-06 13:00:00', '2026-01-17 10:00:00', '2026-02-06 13:00:00'),
+('DEV003', 6, 'P006', '乐事薯片原味', 15, 2, 'left', 5, '2026-02-10 11:00:00', '2026-01-17 10:00:00', '2026-02-10 11:00:00'),
+('DEV003', 7, 'P007', '德芙巧克力', 1, 2, 'right', 3, '2026-02-08 17:00:00', '2026-01-17 10:00:00', '2026-02-08 17:00:00'),
+('DEV003', 8, 'P008', '脉动维生素饮料', 20, 3, 'left', 5, '2026-02-10 11:00:00', '2026-01-17 10:00:00', '2026-02-10 11:00:00'),
+('DEV003', 9, 'P009', '旺旺雪饼', 0, 3, 'right', 5, '2026-02-04 09:00:00', '2026-01-17 10:00:00', '2026-02-04 09:00:00'),
+('DEV003', 10, 'P010', '红牛功能饮料', 3, 4, 'left', 5, '2026-02-09 12:00:00', '2026-01-17 10:00:00', '2026-02-09 12:00:00');
+
+-- ============================================
+-- 3. 上货记录测试数据
+-- ============================================
+INSERT INTO t_stock_record (id, device_id, activity_id, stock_type, stocker_id, stocker_name, stocker_phone, total_items, total_quantity, status, start_time, end_time, remark, create_time, update_time) VALUES
+(1, 'DEV001', 'ACT2026021001', 1, 1, '张三', '13800138001', 6, 48, 2, '2026-02-10 09:00:00', '2026-02-10 10:00:00', '日常上货', '2026-02-10 08:30:00', '2026-02-10 10:00:00'),
+(2, 'DEV002', 'ACT2026021002', 1, 2, '李四', '13800138002', 5, 35, 2, '2026-02-10 08:00:00', '2026-02-10 09:00:00', '日常上货', '2026-02-10 07:45:00', '2026-02-10 09:00:00'),
+(3, 'DEV003', 'ACT2026021003', 2, 3, '王五', '13800138003', 4, 25, 2, '2026-02-10 10:30:00', '2026-02-10 11:00:00', '补货', '2026-02-10 10:15:00', '2026-02-10 11:00:00'),
+(4, 'DEV001', 'ACT2026020901', 1, 5, '钱七', '13800138005', 3, 20, 2, '2026-02-09 10:30:00', '2026-02-09 11:30:00', '紧急补货', '2026-02-09 10:00:00', '2026-02-09 11:30:00'),
+(5, 'DEV002', 'ACT2026020902', 2, 1, '张三', '13800138001', 2, 15, 2, '2026-02-09 15:00:00', '2026-02-09 15:45:00', '补货', '2026-02-09 14:30:00', '2026-02-09 15:45:00'),
+(6, 'DEV003', NULL, 1, 4, '赵六', '13800138004', 5, 30, 3, '2026-02-08 14:00:00', NULL, '已取消', '2026-02-08 13:30:00', '2026-02-08 16:00:00'),
+(7, 'DEV001', 'ACT2026020801', 1, 2, '李四', '13800138002', 4, 28, 2, '2026-02-08 13:00:00', '2026-02-08 14:00:00', '日常上货', '2026-02-08 12:45:00', '2026-02-08 14:00:00'),
+(8, 'DEV003', NULL, 2, 3, '王五', '13800138003', 3, 18, 1, '2026-02-11 08:00:00', NULL, '进行中', '2026-02-11 07:45:00', '2026-02-11 08:00:00');
+
+-- ============================================
+-- 4. 上货记录明细测试数据
+-- ============================================
+INSERT INTO t_stock_record_item (record_id, device_id, product_id, product_code, product_name, shelf_num, quantity, before_stock, after_stock, create_time) VALUES
+-- 记录1的明细
+(1, 'DEV001', 1, 'P001', '可口可乐330ml', 1, 10, 5, 15, '2026-02-10 09:10:00'),
+(1, 'DEV001', 3, 'P003', '农夫山泉550ml', 2, 12, 8, 20, '2026-02-10 09:20:00'),
+(1, 'DEV001', 4, 'P004', '康师傅红烧牛肉面', 2, 8, 0, 8, '2026-02-10 09:30:00'),
+(1, 'DEV001', 6, 'P006', '乐事薯片原味', 3, 6, 6, 12, '2026-02-10 09:40:00'),
+(1, 'DEV001', 7, 'P007', '德芙巧克力', 4, 6, 0, 6, '2026-02-10 09:50:00'),
+(1, 'DEV001', 8, 'P008', '脉动维生素饮料', 4, 6, 4, 10, '2026-02-10 09:55:00'),
+
+-- 记录2的明细
+(2, 'DEV002', 1, 'P001', '可口可乐330ml', 1, 10, 15, 25, '2026-02-10 08:10:00'),
+(2, 'DEV002', 2, 'P002', '百事可乐330ml', 1, 8, 10, 18, '2026-02-10 08:20:00'),
+(2, 'DEV002', 3, 'P003', '农夫山泉550ml', 2, 15, 15, 30, '2026-02-10 08:30:00'),
+(2, 'DEV002', 9, 'P009', '旺旺雪饼', 2, 5, 0, 5, '2026-02-10 08:40:00'),
+(2, 'DEV002', 10, 'P010', '红牛功能饮料', 3, 8, 0, 8, '2026-02-10 08:50:00'),
+
+-- 记录3的明细
+(3, 'DEV003', 4, 'P004', '康师傅红烧牛肉面', 1, 10, 12, 22, '2026-02-10 10:35:00'),
+(3, 'DEV003', 6, 'P006', '乐事薯片原味', 2, 8, 7, 15, '2026-02-10 10:42:00'),
+(3, 'DEV003', 8, 'P008', '脉动维生素饮料', 3, 10, 10, 20, '2026-02-10 10:48:00'),
+(3, 'DEV003', 10, 'P010', '红牛功能饮料', 4, 5, 0, 5, '2026-02-10 10:55:00');
+
+-- ============================================
+-- 5. 库存变动日志测试数据
+-- ============================================
+INSERT INTO t_inventory_log (device_id, product_id, product_code, product_name, change_type, change_quantity, before_stock, after_stock, order_id, activity_id, operator_id, operator_name, remark, create_time) VALUES
+-- 上货记录
+('DEV001', 1, 'P001', '可口可乐330ml', 1, 10, 5, 15, NULL, 'ACT2026021001', 1, '张三', '上货增加库存', '2026-02-10 09:10:00'),
+('DEV001', 3, 'P003', '农夫山泉550ml', 1, 12, 8, 20, NULL, 'ACT2026021001', 1, '张三', '上货增加库存', '2026-02-10 09:20:00'),
+('DEV001', 4, 'P004', '康师傅红烧牛肉面', 1, 8, 0, 8, NULL, 'ACT2026021001', 1, '张三', '上货增加库存', '2026-02-10 09:30:00'),
+('DEV002', 1, 'P001', '可口可乐330ml', 1, 10, 15, 25, NULL, 'ACT2026021002', 2, '李四', '上货增加库存', '2026-02-10 08:10:00'),
+('DEV002', 3, 'P003', '农夫山泉550ml', 1, 15, 15, 30, NULL, 'ACT2026021002', 2, '李四', '上货增加库存', '2026-02-10 08:30:00'),
+
+-- 销售记录
+('DEV001', 1, 'P001', '可口可乐330ml', 2, -2, 15, 13, 'ORD20260210001', NULL, NULL, '系统', '销售扣减库存', '2026-02-10 12:30:00'),
+('DEV001', 3, 'P003', '农夫山泉550ml', 2, -1, 20, 19, 'ORD20260210001', NULL, NULL, '系统', '销售扣减库存', '2026-02-10 12:30:00'),
+('DEV001', 6, 'P006', '乐事薯片原味', 2, -1, 12, 11, 'ORD20260210002', NULL, NULL, '系统', '销售扣减库存', '2026-02-10 14:15:00'),
+('DEV002', 2, 'P002', '百事可乐330ml', 2, -3, 18, 15, 'ORD20260210003', NULL, NULL, '系统', '销售扣减库存', '2026-02-10 15:00:00'),
+('DEV002', 10, 'P010', '红牛功能饮料', 2, -2, 8, 6, 'ORD20260210004', NULL, NULL, '系统', '销售扣减库存', '2026-02-10 16:30:00'),
+('DEV003', 4, 'P004', '康师傅红烧牛肉面', 2, -1, 22, 21, 'ORD20260210005', NULL, NULL, '系统', '销售扣减库存', '2026-02-10 17:45:00'),
+('DEV003', 7, 'P007', '德芙巧克力', 2, -1, 2, 1, 'ORD20260210006', NULL, NULL, '系统', '销售扣减库存', '2026-02-10 18:20:00'),
+
+-- 调整记录
+('DEV001', 2, 'P002', '百事可乐330ml', 4, -2, 5, 3, NULL, NULL, 1, '张三', '盘点调整', '2026-02-09 11:00:00'),
+('DEV003', 5, 'P005', '奥利奥饼干', 4, -1, 3, 2, NULL, NULL, 3, '王五', '损坏报废', '2026-02-08 15:00:00'),
+
+-- 补货记录
+('DEV003', 4, 'P004', '康师傅红烧牛肉面', 1, 10, 12, 22, NULL, 'ACT2026021003', 3, '王五', '补货增加库存', '2026-02-10 10:35:00'),
+('DEV003', 6, 'P006', '乐事薯片原味', 1, 8, 7, 15, NULL, 'ACT2026021003', 3, '王五', '补货增加库存', '2026-02-10 10:42:00'),
+
+-- 昨天的记录
+('DEV001', 1, 'P001', '可口可乐330ml', 2, -3, 18, 15, 'ORD20260209001', NULL, NULL, '系统', '销售扣减库存', '2026-02-09 10:00:00'),
+('DEV001', 8, 'P008', '脉动维生素饮料', 2, -2, 12, 10, 'ORD20260209002', NULL, NULL, '系统', '销售扣减库存', '2026-02-09 11:30:00'),
+('DEV002', 3, 'P003', '农夫山泉550ml', 2, -5, 35, 30, 'ORD20260209003', NULL, NULL, '系统', '销售扣减库存', '2026-02-09 14:00:00'),
+('DEV003', 8, 'P008', '脉动维生素饮料', 2, -1, 21, 20, 'ORD20260209004', NULL, NULL, '系统', '销售扣减库存', '2026-02-09 16:45:00');
+
+-- ============================================
+-- 完成
+-- ============================================
+SELECT '库存管理测试数据导入完成!' AS message;
+SELECT 
+    (SELECT COUNT(*) FROM t_stocker) AS stocker_count,
+    (SELECT COUNT(*) FROM t_device_inventory) AS inventory_count,
+    (SELECT COUNT(*) FROM t_stock_record) AS record_count,
+    (SELECT COUNT(*) FROM t_stock_record_item) AS record_item_count,
+    (SELECT COUNT(*) FROM t_inventory_log) AS log_count;

+ 48 - 0
haha-admin/src/main/resources/sql/new_product_apply.sql

@@ -0,0 +1,48 @@
+-- 新品申请表
+CREATE TABLE IF NOT EXISTS `t_new_product_apply` (
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+    `haha_apply_id` VARCHAR(64) DEFAULT NULL COMMENT '哈哈平台工单编号',
+    `barcode` VARCHAR(50) DEFAULT NULL COMMENT '商品条形码',
+    `name` VARCHAR(200) NOT NULL COMMENT '商品名称',
+    `category` VARCHAR(50) DEFAULT NULL COMMENT '商品分类',
+    `specification` VARCHAR(100) DEFAULT NULL COMMENT '商品规格',
+    `price` DECIMAL(10, 2) DEFAULT NULL COMMENT '建议售价',
+    `cost` DECIMAL(10, 2) DEFAULT NULL COMMENT '成本价',
+    `image_url` VARCHAR(500) DEFAULT NULL COMMENT '商品图片URL(正面图)',
+    `image_url2` VARCHAR(500) DEFAULT NULL COMMENT '商品背面图URL',
+    `image_url3` VARCHAR(500) DEFAULT NULL COMMENT '商品侧面图URL',
+    `image_url4` VARCHAR(500) DEFAULT NULL COMMENT '商品顶部图URL',
+    `image_url5` VARCHAR(500) DEFAULT NULL COMMENT '商品称重图URL',
+    `original_image_url1` VARCHAR(500) DEFAULT NULL COMMENT '正面图原图URL',
+    `original_image_url2` VARCHAR(500) DEFAULT NULL COMMENT '背面图原图URL',
+    `original_image_url3` VARCHAR(500) DEFAULT NULL COMMENT '侧面图原图URL',
+    `original_image_url4` VARCHAR(500) DEFAULT NULL COMMENT '顶部图原图URL',
+    `supplier` VARCHAR(200) DEFAULT NULL COMMENT '供应商',
+    `shelf_life` VARCHAR(50) DEFAULT NULL COMMENT '保质期',
+    `storage_conditions` VARCHAR(100) DEFAULT NULL COMMENT '存储条件',
+    `description` VARCHAR(1000) DEFAULT NULL COMMENT '商品描述',
+    `reason` VARCHAR(500) DEFAULT NULL COMMENT '申请原因',
+    `product_type` TINYINT DEFAULT 0 COMMENT '商品类型:0静态,1动态',
+    `sticker_num` VARCHAR(20) DEFAULT NULL COMMENT '学习机号',
+    `standard` TINYINT DEFAULT 1 COMMENT '是否标品:1标品,2非标品',
+    `length` INT DEFAULT NULL COMMENT '商品长度(mm)',
+    `width` INT DEFAULT NULL COMMENT '商品宽度(mm)',
+    `height` INT DEFAULT NULL COMMENT '商品高度(mm)',
+    `exhibit_height` INT DEFAULT NULL COMMENT '陈列高度(mm)',
+    `columns` INT DEFAULT NULL COMMENT '商品占列',
+    `numbers` INT DEFAULT NULL COMMENT '一个货道放几个',
+    `status` TINYINT DEFAULT 0 COMMENT '申请状态:0待提交,1审核中,2已通过,3已拒绝',
+    `haha_code` VARCHAR(50) DEFAULT NULL COMMENT '哈哈平台返回的商品code',
+    `reject_reason` VARCHAR(500) DEFAULT NULL COMMENT '拒绝原因',
+    `product_extends` VARCHAR(500) DEFAULT NULL COMMENT '扩展字段',
+    `applicant_id` BIGINT DEFAULT NULL COMMENT '申请人ID',
+    `applicant_name` VARCHAR(100) DEFAULT NULL COMMENT '申请人名称',
+    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    `submit_time` DATETIME DEFAULT NULL COMMENT '提交时间',
+    `audit_time` DATETIME DEFAULT NULL COMMENT '审核时间',
+    PRIMARY KEY (`id`),
+    INDEX `idx_barcode` (`barcode`),
+    INDEX `idx_status` (`status`),
+    INDEX `idx_create_time` (`create_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='新品申请表';

+ 26 - 0
haha-admin/src/main/resources/sql/product.sql

@@ -0,0 +1,26 @@
+-- 创建商品表
+CREATE TABLE IF NOT EXISTS `t_product` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `product_id` VARCHAR(64) DEFAULT NULL COMMENT '哈哈平台商品ID',
+  `code` VARCHAR(128) DEFAULT NULL COMMENT '商品编码(哈哈平台code)',
+  `barcode` VARCHAR(64) DEFAULT NULL COMMENT '商品条码',
+  `name` VARCHAR(256) NOT NULL COMMENT '商品名称',
+  `type` VARCHAR(64) DEFAULT NULL COMMENT '商品分类(如:饮料、零食等)',
+  `pic` VARCHAR(512) DEFAULT NULL COMMENT '商品图片URL',
+  `planogram` VARCHAR(512) DEFAULT NULL COMMENT '商品陈列图URL',
+  `apply_status` VARCHAR(32) DEFAULT NULL COMMENT '适用设备:全部柜、静态柜、动态柜',
+  `cost_price` DECIMAL(10,2) DEFAULT NULL COMMENT '成本价',
+  `retail_price` DECIMAL(10,2) DEFAULT NULL COMMENT '零售价(门店统一售价)',
+  `customer_id` VARCHAR(128) DEFAULT NULL COMMENT '商家自定义商品ID',
+  `sync_status` INT DEFAULT 0 COMMENT '同步状态:0-未同步,1-同步中,2-已同步,3-同步失败',
+  `sync_time` DATETIME DEFAULT NULL COMMENT '最后同步时间',
+  `is_deleted` INT DEFAULT 0 COMMENT '是否删除:0-正常,1-已删除',
+  `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_code` (`code`),
+  KEY `idx_barcode` (`barcode`),
+  KEY `idx_product_id` (`product_id`),
+  KEY `idx_type` (`type`),
+  KEY `idx_sync_status` (`sync_status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表(商家商品库)';

+ 27 - 0
haha-admin/src/main/resources/sql/shop.sql

@@ -0,0 +1,27 @@
+-- 门店表
+CREATE TABLE IF NOT EXISTS `t_shop` (
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+    `shop_code` VARCHAR(50) DEFAULT NULL COMMENT '门店编码',
+    `name` VARCHAR(100) NOT NULL COMMENT '门店名称',
+    `address` VARCHAR(255) DEFAULT NULL COMMENT '门店地址',
+    `province` VARCHAR(50) DEFAULT NULL COMMENT '省份',
+    `city` VARCHAR(50) DEFAULT NULL COMMENT '城市',
+    `district` VARCHAR(50) DEFAULT NULL COMMENT '区域',
+    `longitude` DECIMAL(10, 6) DEFAULT NULL COMMENT '经度',
+    `latitude` DECIMAL(10, 6) DEFAULT NULL COMMENT '纬度',
+    `contact_name` VARCHAR(50) DEFAULT NULL COMMENT '联系人',
+    `contact_phone` VARCHAR(20) DEFAULT NULL COMMENT '联系电话',
+    `business_hours` VARCHAR(100) DEFAULT NULL COMMENT '营业时间',
+    `status` TINYINT DEFAULT 1 COMMENT '状态:0-禁用,1-启用',
+    `remark` VARCHAR(500) DEFAULT NULL 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_shop_code` (`shop_code`),
+    KEY `idx_name` (`name`),
+    KEY `idx_city` (`city`),
+    KEY `idx_status` (`status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='门店表';
+
+-- 更新设备表添加门店外键索引(如果shopId字段已存在则只需添加索引)
+ALTER TABLE `t_device` ADD INDEX `idx_shop_id` (`shop_id`);

+ 27 - 0
haha-admin/src/main/resources/sql/shop_test_data.sql

@@ -0,0 +1,27 @@
+-- ============================================
+-- 门店测试数据
+-- ============================================
+
+-- 清空现有数据
+DELETE FROM t_shop WHERE id > 0;
+
+-- ============================================
+-- 门店数据
+-- ============================================
+INSERT INTO t_shop (id, shop_code, name, address, province, city, district, longitude, latitude, contact_name, contact_phone, business_hours, status, remark, create_time, update_time) VALUES
+(1, 'SH001', '航城店', '深圳市宝安区航城街道大道1号', '广东省', '深圳市', '宝安区', 113.8546, 22.5678, '张三', '13800138001', '09:00-22:00', 1, '航城主要门店', NOW(), NOW()),
+(2, 'SH002', '长田店', '深圳市宝安区长田路88号', '广东省', '深圳市', '宝安区', 113.8234, 22.5891, '李四', '13800138002', '08:30-22:30', 1, '长田商圈门店', NOW(), NOW()),
+(3, 'SH003', '西乡店', '深圳市宝安区西乡街道商业街66号', '广东省', '深圳市', '宝安区', 113.8712, 22.5745, '王五', '13800138003', '09:00-21:30', 1, '西乡地铁站附近', NOW(), NOW()),
+(4, 'SH004', '新安店', '深圳市宝安区新安街道创业路123号', '广东省', '深圳市', '宝安区', 113.9023, 22.5612, '赵六', '13800138004', '08:00-23:00', 1, '新安商业中心', NOW(), NOW()),
+(5, 'SH005', '福永店', '深圳市宝安区福永街道怀德路55号', '广东省', '深圳市', '宝安区', 113.8123, 22.6234, '钱七', '13800138005', '09:00-22:00', 1, '福永商圈', NOW(), NOW()),
+(6, 'SH006', '沙井店', '深圳市宝安区沙井街道沙井路168号', '广东省', '深圳市', '宝安区', 113.8345, 22.7123, '孙八', '13800138006', '09:00-21:00', 0, '装修中', NOW(), NOW()),
+(7, 'SH007', '松岗店', '深圳市宝安区松岗街道松岗大道78号', '广东省', '深圳市', '宝安区', 113.8567, 22.7456, '周九', '13800138007', '10:00-22:00', 1, '松岗中心区', NOW(), NOW()),
+(8, 'SH008', '石岩店', '深圳市宝安区石岩街道宝石路268号', '广东省', '深圳市', '宝安区', 113.9234, 22.6891, '吴十', '13800138008', '09:00-22:00', 1, '石岩中心', NOW(), NOW());
+
+-- ============================================
+-- 更新设备表的门店关联
+-- ============================================
+UPDATE t_device SET shop_id = 1 WHERE shop_id IS NULL LIMIT 2;
+UPDATE t_device SET shop_id = 2 WHERE shop_id IS NULL LIMIT 2;
+UPDATE t_device SET shop_id = 3 WHERE shop_id IS NULL LIMIT 2;
+UPDATE t_device SET shop_id = 4 WHERE shop_id IS NULL LIMIT 2;

+ 37 - 0
haha-admin/src/main/resources/sql/sync.sql

@@ -0,0 +1,37 @@
+-- 创建数据同步记录表
+CREATE TABLE IF NOT EXISTS `t_sync_record` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `sync_type` VARCHAR(32) NOT NULL COMMENT '同步类型:DEVICE-设备,PRODUCT-商品,ORDER-订单',
+  `status` INT NOT NULL DEFAULT 0 COMMENT '同步状态:0-待同步,1-同步中,2-同步成功,3-同步失败',
+  `total_count` INT DEFAULT 0 COMMENT '同步数据总数',
+  `success_count` INT DEFAULT 0 COMMENT '同步成功数量',
+  `fail_count` INT DEFAULT 0 COMMENT '同步失败数量',
+  `start_time` DATETIME DEFAULT NULL COMMENT '开始时间',
+  `end_time` DATETIME DEFAULT NULL COMMENT '结束时间',
+  `duration` BIGINT DEFAULT NULL COMMENT '同步耗时(毫秒)',
+  `error_message` TEXT DEFAULT NULL COMMENT '错误信息',
+  `operator_id` BIGINT DEFAULT NULL COMMENT '操作人ID',
+  `operator_name` VARCHAR(64) DEFAULT NULL COMMENT '操作人名称',
+  `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_sync_type` (`sync_type`),
+  KEY `idx_status` (`status`),
+  KEY `idx_create_time` (`create_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据同步记录表';
+
+-- 创建数据同步日志表
+CREATE TABLE IF NOT EXISTS `t_sync_log` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `record_id` BIGINT NOT NULL COMMENT '同步记录ID',
+  `sync_type` VARCHAR(32) NOT NULL COMMENT '同步类型',
+  `data_id` VARCHAR(64) DEFAULT NULL COMMENT '数据ID',
+  `data_name` VARCHAR(128) DEFAULT NULL COMMENT '数据名称',
+  `status` INT NOT NULL DEFAULT 0 COMMENT '同步状态:0-失败,1-成功',
+  `message` VARCHAR(512) DEFAULT NULL COMMENT '同步结果消息',
+  `detail` TEXT DEFAULT NULL COMMENT '同步详情(JSON格式)',
+  `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_record_id` (`record_id`),
+  KEY `idx_sync_type` (`sync_type`),
+  KEY `idx_create_time` (`create_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据同步日志表';

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

@@ -117,12 +117,6 @@ public class Product implements Serializable {
     @TableField(exist = false)
     private Double cost;
 
-    @TableField(exist = false)
-    private Integer stock;
-
-    @TableField(exist = false)
-    private Integer soldCount;
-
     @TableField(exist = false)
     private String syncStatusLabel;
 

+ 81 - 0
haha-entity/src/main/java/com/haha/entity/ShopReplenisher.java

@@ -0,0 +1,81 @@
+package com.haha.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+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_shop_replenisher")
+public class ShopReplenisher implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键ID
+     */
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 门店ID
+     */
+    private Long shopId;
+
+    /**
+     * 管理员ID(补货员)
+     */
+    private Long adminId;
+
+    /**
+     * 状态:0-禁用,1-启用
+     */
+    private Integer status;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    private LocalDateTime updateTime;
+
+    // ========== 以下为非数据库字段,用于前端展示 ==========
+
+    /**
+     * 管理员姓名
+     */
+    @TableField(exist = false)
+    private String nickname;
+
+    /**
+     * 管理员手机号
+     */
+    @TableField(exist = false)
+    private String phone;
+
+    /**
+     * 管理员头像
+     */
+    @TableField(exist = false)
+    private String avatar;
+
+    /**
+     * 状态标签
+     */
+    @TableField(exist = false)
+    private String statusLabel;
+
+    /**
+     * 状态颜色
+     */
+    @TableField(exist = false)
+    private String statusColor;
+}

+ 58 - 0
haha-mapper/src/main/java/com/haha/mapper/DeviceInventoryMapper.java

@@ -1,6 +1,8 @@
 package com.haha.mapper;
 
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.haha.entity.DeviceInventory;
 import org.apache.ibatis.annotations.Mapper;
 import org.apache.ibatis.annotations.Param;
@@ -41,4 +43,60 @@ public interface DeviceInventoryMapper extends BaseMapper<DeviceInventory> {
             "WHERE di.stock <= di.warning_threshold " +
             "ORDER BY di.stock ASC")
     List<Map<String, Object>> selectLowStockInventories();
+
+    /**
+     * 分页查询设备库存统计列表
+     * 按设备分组统计库存信息
+     */
+    @Select("<script>" +
+            "SELECT " +
+            "  d.id as device_id_primary, " +
+            "  d.device_id, " +
+            "  d.name as device_name, " +
+            "  s.id as shop_id, " +
+            "  s.name as shop_name, " +
+            "  s.address as shop_address, " +
+            "  COALESCE(SUM(di.stock), 0) as remaining_stock, " +
+            "  COALESCE(SUM(di.warning_threshold), 0) as standard_stock, " +
+            "  COUNT(di.id) as product_count, " +
+            "  MAX(di.last_restock_time) as last_restock_time, " +
+            "  SUM(CASE WHEN di.stock = 0 THEN 1 ELSE 0 END) as zero_stock_count, " +
+            "  SUM(CASE WHEN di.stock &lt;= di.warning_threshold AND di.stock > 0 THEN 1 ELSE 0 END) as low_stock_count, " +
+            "  SUM(CASE WHEN di.product_id IS NULL OR di.product_id = 0 THEN 1 ELSE 0 END) as unknown_product_count " +
+            "FROM t_device d " +
+            "LEFT JOIN t_shop s ON d.shop_id = s.id " +
+            "LEFT JOIN t_device_inventory di ON d.device_id = di.device_id " +
+            "WHERE 1=1 " +
+            "<if test='shopName != null and shopName != \"\"'> " +
+            "  AND s.name LIKE CONCAT('%', #{shopName}, '%') " +
+            "</if> " +
+            "<if test='deviceName != null and deviceName != \"\"'> " +
+            "  AND d.name LIKE CONCAT('%', #{deviceName}, '%') " +
+            "</if> " +
+            "<if test='deviceId != null and deviceId != \"\"'> " +
+            "  AND d.device_id LIKE CONCAT('%', #{deviceId}, '%') " +
+            "</if> " +
+            "<if test='hasUnknownProduct != null and hasUnknownProduct == true'> " +
+            "  AND EXISTS (SELECT 1 FROM t_device_inventory di2 WHERE di2.device_id = d.device_id AND (di2.product_id IS NULL OR di2.product_id = 0)) " +
+            "</if> " +
+            "GROUP BY d.id, d.device_id, d.name, s.id, s.name, s.address " +
+            "ORDER BY d.update_time DESC " +
+            "</script>")
+    IPage<Map<String, Object>> selectDeviceInventoryStats(
+            Page<Map<String, Object>> page,
+            @Param("shopName") String shopName,
+            @Param("deviceName") String deviceName,
+            @Param("deviceId") String deviceId,
+            @Param("hasUnknownProduct") Boolean hasUnknownProduct,
+            @Param("stockStatus") Integer stockStatus);
+
+    /**
+     * 查询设备的最后一次补货后总库存
+     */
+    @Select("SELECT COALESCE(SUM(stock), 0) as total_stock_after_restock " +
+            "FROM t_inventory_log " +
+            "WHERE device_id = #{deviceId} " +
+            "AND change_type = 1 " +
+            "AND create_time = (SELECT MAX(create_time) FROM t_inventory_log WHERE device_id = #{deviceId} AND change_type = 1)")
+    Integer selectLastRestockTotalStock(@Param("deviceId") String deviceId);
 }

+ 40 - 0
haha-mapper/src/main/java/com/haha/mapper/ShopReplenisherMapper.java

@@ -0,0 +1,40 @@
+package com.haha.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.haha.entity.ShopReplenisher;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+/**
+ * 门店补货员关联Mapper
+ */
+@Mapper
+public interface ShopReplenisherMapper extends BaseMapper<ShopReplenisher> {
+
+    /**
+     * 获取门店补货员列表(带管理员信息)
+     */
+    @Select("SELECT sr.*, a.real_name as nickname, a.phone, a.avatar " +
+            "FROM t_shop_replenisher sr " +
+            "LEFT JOIN t_admin a ON sr.admin_id = a.id " +
+            "WHERE sr.shop_id = #{shopId} " +
+            "ORDER BY sr.create_time DESC")
+    List<ShopReplenisher> selectByShopId(@Param("shopId") Long shopId);
+
+    /**
+     * 检查管理员是否已是某门店的补货员
+     */
+    @Select("SELECT COUNT(*) FROM t_shop_replenisher WHERE shop_id = #{shopId} AND admin_id = #{adminId}")
+    int countByShopIdAndAdminId(@Param("shopId") Long shopId, @Param("adminId") Long adminId);
+
+    /**
+     * 获取管理员关联的所有门店补货员记录
+     */
+    @Select("SELECT sr.*, s.name as shop_name FROM t_shop_replenisher sr " +
+            "LEFT JOIN t_shop s ON sr.shop_id = s.id " +
+            "WHERE sr.admin_id = #{adminId}")
+    List<ShopReplenisher> selectByAdminId(@Param("adminId") Long adminId);
+}

+ 47 - 2
haha-miniapp/src/main/java/com/haha/miniapp/config/RedisConfig.java

@@ -1,7 +1,18 @@
 package com.haha.miniapp.config;
 
+import io.lettuce.core.ClientOptions;
+import io.lettuce.core.SocketOptions;
+import io.lettuce.core.protocol.ProtocolVersion;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
+import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
+import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
+import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
+
+import java.time.Duration;
 
 /**
  * Redis 配置类
@@ -11,6 +22,40 @@ import org.springframework.context.annotation.Configuration;
 @Configuration
 @Slf4j
 public class RedisConfig {
-    // Sa-Token 的 Redis 整合已由 sa-token-redis-jackson 依赖自动配置
-    // 无需额外配置,Spring Boot 会自动读取 application.yml 中的 Redis 配置
+    
+    @Bean
+    public RedisConnectionFactory redisConnectionFactory(RedisProperties redisProperties) {
+        // 配置 Socket 选项 - 连接超时和保持连接
+        SocketOptions socketOptions = SocketOptions.builder()
+                .connectTimeout(Duration.ofMillis(redisProperties.getTimeout().toMillis()))
+                .keepAlive(true)
+                .build();
+
+        // 配置客户端选项 - 自动重连,使用RESP2协议避免HELLO认证问题
+        ClientOptions clientOptions = ClientOptions.builder()
+                .socketOptions(socketOptions)
+                .autoReconnect(true)
+                .protocolVersion(ProtocolVersion.RESP2)
+                .disconnectedBehavior(ClientOptions.DisconnectedBehavior.REJECT_COMMANDS)
+                .build();
+
+        // 配置连接池
+        LettucePoolingClientConfiguration poolConfig = LettucePoolingClientConfiguration.builder()
+                .commandTimeout(redisProperties.getTimeout())
+                .clientOptions(clientOptions)
+                .build();
+
+        // 创建Redis单机配置,设置host、port、password和database
+        RedisStandaloneConfiguration redisConfig = new RedisStandaloneConfiguration();
+        redisConfig.setHostName(redisProperties.getHost());
+        redisConfig.setPort(redisProperties.getPort());
+        redisConfig.setPassword(redisProperties.getPassword());
+        redisConfig.setDatabase(redisProperties.getDatabase());
+
+        log.info("Redis 增强配置已加载: host={}, port={}, database={}, 连接超时={}ms, 协议版本=RESP2", 
+                redisProperties.getHost(), redisProperties.getPort(), 
+                redisProperties.getDatabase(), redisProperties.getTimeout().toMillis());
+
+        return new LettuceConnectionFactory(redisConfig, poolConfig);
+    }
 }

+ 19 - 4
haha-miniapp/src/main/resources/application.yml

@@ -4,10 +4,19 @@ spring:
 
   # 数据库配置
   datasource:
-    url: jdbc:mysql://server.kuaiyuman.cn:3306/haha?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false
+    url: jdbc:mysql://server.kuaiyuman.cn:3306/haha?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&useServerPrepStmts=true&cachePrepStmts=true&prepStmtCacheSize=250&prepStmtCacheSqlLimit=2048
     username: root
     password: KuaiyuMan/*-
     driver-class-name: com.mysql.cj.jdbc.Driver
+    # HikariCP 连接池配置
+    hikari:
+      minimum-idle: 3
+      maximum-pool-size: 10
+      max-lifetime: 1800000
+      idle-timeout: 300000
+      connection-timeout: 30000
+      connection-test-query: SELECT 1
+      keepalive-time: 30000
 
   # Redis 配置
   data:
@@ -17,12 +26,18 @@ spring:
       password: KtXA^Zx!TZmLEy(@JjB@2(TVG0kdy5)&
       database: 8
       timeout: 10000
+      # 连接配置
       lettuce:
         pool:
-          max-active: 8
-          max-wait: -1
+          max-active: 10
+          max-wait: 3000
           max-idle: 8
-          min-idle: 0
+          min-idle: 2
+          time-between-eviction-runs: 30000
+        shutdown-timeout: 100
+        refresh:
+          period: 10000
+          min: 5000
 
   # 数据库初始化
   sql:

+ 3 - 3
haha-miniapp/src/main/resources/sql/order.sql

@@ -5,7 +5,7 @@ CREATE TABLE IF NOT EXISTS `t_order` (
   `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 '设备序列号',
+  `device_id` 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',
@@ -17,13 +17,13 @@ CREATE TABLE IF NOT EXISTS `t_order` (
   PRIMARY KEY (`id`),
   UNIQUE KEY `uk_order_no` (`order_no`),
   KEY `idx_user_id` (`user_id`),
-  KEY `idx_device_sn` (`device_sn`),
+  KEY `idx_device_id` (`device_id`),
   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
+INSERT IGNORE INTO `t_order` (`order_no`, `out_trade_no`, `haha_order_no`, `user_id`, `device_id`, `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),

+ 30 - 0
haha-service/src/main/java/com/haha/service/DashboardService.java

@@ -0,0 +1,30 @@
+package com.haha.service;
+
+import java.util.Map;
+
+/**
+ * 首页数据看板服务接口
+ */
+public interface DashboardService {
+
+    /**
+     * 获取概览数据(今日实时数据)
+     */
+    Map<String, Object> getOverviewData();
+
+    /**
+     * 获取统计数据(按时间维度)
+     * @param type 时间类型:yesterday/week/month/days30
+     */
+    Map<String, Object> getStatisticsData(String type);
+
+    /**
+     * 获取快捷入口数据
+     */
+    Map<String, Object> getQuickLinksData();
+
+    /**
+     * 获取设备状态分布
+     */
+    Object getDeviceStatusData();
+}

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

@@ -99,4 +99,14 @@ public interface DeviceInventoryService extends IService<DeviceInventory> {
      * @return 库存记录
      */
     DeviceInventory getByDeviceAndProduct(String deviceId, Long productId);
+
+    /**
+     * 分页查询设备库存统计列表
+     *
+     * @param page     页码
+     * @param pageSize 每页条数
+     * @param params   查询参数(shopName, deviceName, deviceId, hasUnknownProduct, stockStatus)
+     * @return 分页结果
+     */
+    IPage<Map<String, Object>> getDeviceInventoryStats(int page, int pageSize, Map<String, Object> params);
 }

+ 68 - 0
haha-service/src/main/java/com/haha/service/ShopReplenisherService.java

@@ -0,0 +1,68 @@
+package com.haha.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.haha.common.vo.Result;
+import com.haha.entity.ShopReplenisher;
+
+import java.util.List;
+
+/**
+ * 门店补货员服务接口
+ */
+public interface ShopReplenisherService extends IService<ShopReplenisher> {
+
+    /**
+     * 获取门店补货员列表
+     * @param shopId 门店ID
+     * @return 补货员列表
+     */
+    List<ShopReplenisher> getReplenishersByShopId(Long shopId);
+
+    /**
+     * 添加补货员
+     * @param shopId 门店ID
+     * @param adminId 管理员ID
+     * @return 操作结果
+     */
+    Result<String> addReplenisher(Long shopId, Long adminId);
+
+    /**
+     * 批量添加补货员
+     * @param shopId 门店ID
+     * @param adminIds 管理员ID列表
+     * @return 成功添加数量
+     */
+    int batchAddReplenishers(Long shopId, List<Long> adminIds);
+
+    /**
+     * 移除补货员
+     * @param shopId 门店ID
+     * @param adminId 管理员ID
+     * @return 操作结果
+     */
+    Result<String> removeReplenisher(Long shopId, Long adminId);
+
+    /**
+     * 批量移除补货员
+     * @param shopId 门店ID
+     * @param adminIds 管理员ID列表
+     * @return 成功移除数量
+     */
+    int batchRemoveReplenishers(Long shopId, List<Long> adminIds);
+
+    /**
+     * 更新补货员状态
+     * @param id 补货员关联ID
+     * @param status 状态
+     * @return 操作结果
+     */
+    Result<String> updateStatus(Long id, Integer status);
+
+    /**
+     * 检查管理员是否是某门店的补货员
+     * @param shopId 门店ID
+     * @param adminId 管理员ID
+     * @return 是否是补货员
+     */
+    boolean isReplenisher(Long shopId, Long adminId);
+}

+ 436 - 0
haha-service/src/main/java/com/haha/service/impl/DashboardServiceImpl.java

@@ -0,0 +1,436 @@
+package com.haha.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.haha.entity.Device;
+import com.haha.entity.Order;
+import com.haha.entity.User;
+import com.haha.mapper.DeviceInventoryMapper;
+import com.haha.service.*;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+
+/**
+ * 首页数据看板服务实现类
+ */
+@Slf4j
+@Service
+public class DashboardServiceImpl implements DashboardService {
+
+    @Autowired
+    private OrderService orderService;
+
+    @Autowired
+    private DeviceService deviceService;
+
+    @Autowired
+    private UserService userService;
+
+    @Autowired
+    private DeviceInventoryMapper deviceInventoryMapper;
+
+    @Override
+    public Map<String, Object> getOverviewData() {
+        Map<String, Object> result = new HashMap<>();
+
+        // 今日开始时间
+        LocalDateTime todayStart = LocalDateTime.of(LocalDate.now(), LocalTime.MIN);
+        LocalDateTime todayEnd = LocalDateTime.of(LocalDate.now(), LocalTime.MAX);
+        
+        // 昨日同时段时间(当前时间往前推一天)
+        LocalDateTime yesterdayStart = todayStart.minusDays(1);
+        LocalDateTime yesterdayEnd = LocalDateTime.now().minusDays(1);
+
+        // 今日销售额和订单量
+        LambdaQueryWrapper<Order> todayWrapper = new LambdaQueryWrapper<>();
+        todayWrapper.eq(Order::getPayStatus, Order.PAY_STATUS_已支付)
+                     .ge(Order::getPayTime, todayStart)
+                     .le(Order::getPayTime, todayEnd);
+        
+        List<Order> todayOrders = orderService.list(todayWrapper);
+        
+        // 今日销售额
+        double todaySales = todayOrders.stream()
+                .mapToDouble(o -> o.getTotalAmount() != null ? o.getTotalAmount() : 0)
+                .sum();
+        result.put("todaySales", String.format("%.2f", todaySales));
+
+        // 今日订单量
+        int todayOrderCount = todayOrders.size();
+        result.put("todayOrders", todayOrderCount);
+
+        // 今日客单价
+        String todayAvgPrice = todayOrderCount > 0 
+                ? String.format("%.2f", todaySales / todayOrderCount) 
+                : "0.00";
+        result.put("todayAvgPrice", todayAvgPrice);
+
+        // 昨日同时段销售额和订单量
+        LambdaQueryWrapper<Order> yesterdayWrapper = new LambdaQueryWrapper<>();
+        yesterdayWrapper.eq(Order::getPayStatus, Order.PAY_STATUS_已支付)
+                        .ge(Order::getPayTime, yesterdayStart)
+                        .le(Order::getPayTime, yesterdayEnd);
+        
+        List<Order> yesterdayOrders = orderService.list(yesterdayWrapper);
+        
+        double yesterdaySales = yesterdayOrders.stream()
+                .mapToDouble(o -> o.getTotalAmount() != null ? o.getTotalAmount() : 0)
+                .sum();
+        int yesterdayOrderCount = yesterdayOrders.size();
+        double yesterdayAvgPrice = yesterdayOrderCount > 0 ? yesterdaySales / yesterdayOrderCount : 0;
+
+        // 与昨日同时段对比
+        result.put("todaySalesCompare", formatCompare(todaySales - yesterdaySales));
+        result.put("todayOrdersCompare", formatCompare(todayOrderCount - yesterdayOrderCount));
+        result.put("todayAvgPriceCompare", formatCompare(todayOrderCount > 0 ? 
+                (todaySales / todayOrderCount) - yesterdayAvgPrice : 0));
+
+        // 设备状态统计
+        long totalDevices = deviceService.count();
+        long onlineDevices = deviceService.lambdaQuery()
+                .eq(Device::getStatus, 1) // status=1 为正常/在线
+                .count();
+        long offlineDevices = totalDevices - onlineDevices;
+        
+        result.put("onlineDevices", onlineDevices);
+        result.put("offlineDevices", offlineDevices);
+
+        // 待处理事项统计
+        // 未支付订单数
+        long unpaidOrders = orderService.lambdaQuery()
+                .eq(Order::getStatus, Order.ORDER_STATUS_待支付)
+                .count();
+        result.put("unpaidOrders", unpaidOrders);
+
+        // 计算总待处理事项
+        int pendingTasks = (int) unpaidOrders; // 可添加更多待处理事项
+        result.put("pendingTasks", pendingTasks);
+        result.put("abnormalActivities", 0); // TODO: 异常活动数
+        result.put("pendingAfterSale", 0); // TODO: 售后待处理数
+
+        return result;
+    }
+
+    @Override
+    public Map<String, Object> getStatisticsData(String type) {
+        Map<String, Object> result = new HashMap<>();
+
+        // 计算时间范围
+        LocalDateTime startTime;
+        LocalDateTime endTime = LocalDateTime.now();
+        LocalDateTime compareStartTime;
+        LocalDateTime compareEndTime;
+
+        int days;
+        switch (type) {
+            case "yesterday":
+                startTime = LocalDateTime.of(LocalDate.now().minusDays(1), LocalTime.MIN);
+                endTime = LocalDateTime.of(LocalDate.now().minusDays(1), LocalTime.MAX);
+                compareStartTime = startTime.minusDays(1);
+                compareEndTime = endTime.minusDays(1);
+                days = 1;
+                break;
+            case "month":
+                startTime = LocalDateTime.of(LocalDate.now().withDayOfMonth(1), LocalTime.MIN);
+                compareStartTime = startTime.minusMonths(1);
+                compareEndTime = startTime.minusDays(1);
+                days = LocalDate.now().getDayOfMonth();
+                break;
+            case "days30":
+                startTime = LocalDateTime.now().minusDays(30);
+                compareStartTime = startTime.minusDays(30);
+                compareEndTime = startTime.minusDays(1);
+                days = 30;
+                break;
+            case "week":
+            default:
+                startTime = LocalDateTime.now().minusDays(7);
+                compareStartTime = startTime.minusDays(7);
+                compareEndTime = startTime.minusDays(1);
+                days = 7;
+                break;
+        }
+
+        // 查询订单数据
+        LambdaQueryWrapper<Order> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(Order::getPayStatus, Order.PAY_STATUS_已支付)
+               .ge(Order::getPayTime, startTime)
+               .le(Order::getPayTime, endTime);
+        
+        List<Order> orders = orderService.list(wrapper);
+
+        // 销售额
+        double totalSales = orders.stream()
+                .mapToDouble(o -> o.getTotalAmount() != null ? o.getTotalAmount() : 0)
+                .sum();
+        result.put("totalSales", String.format("%.2f", totalSales));
+
+        // 订单量
+        int totalOrders = orders.size();
+        result.put("totalOrders", totalOrders);
+
+        // 单均价
+        double avgOrderPrice = totalOrders > 0 ? totalSales / totalOrders : 0;
+        result.put("avgOrderPrice", String.format("%.2f", avgOrderPrice));
+
+        // 查询对比期数据
+        LambdaQueryWrapper<Order> compareWrapper = new LambdaQueryWrapper<>();
+        compareWrapper.eq(Order::getPayStatus, Order.PAY_STATUS_已支付)
+                      .ge(Order::getPayTime, compareStartTime)
+                      .le(Order::getPayTime, compareEndTime);
+        
+        List<Order> compareOrders = orderService.list(compareWrapper);
+        
+        double compareSales = compareOrders.stream()
+                .mapToDouble(o -> o.getTotalAmount() != null ? o.getTotalAmount() : 0)
+                .sum();
+        int compareOrderCount = compareOrders.size();
+        double compareAvgPrice = compareOrderCount > 0 ? compareSales / compareOrderCount : 0;
+
+        // 环比增长率
+        result.put("salesGrowth", formatGrowth(totalSales, compareSales));
+        result.put("ordersGrowth", formatGrowth(totalOrders, compareOrderCount));
+        result.put("avgOrderGrowth", formatGrowth(avgOrderPrice, compareAvgPrice));
+
+        // 毛利和毛利率(假设毛利为销售额的30%,实际应从成本数据计算)
+        double grossProfit = totalSales * 0.3;
+        double profitMargin = totalSales > 0 ? (grossProfit / totalSales) * 100 : 0;
+        double compareGrossProfit = compareSales * 0.3;
+        
+        result.put("grossProfit", String.format("%.2f", grossProfit));
+        result.put("profitMargin", String.format("%.0f", profitMargin) + "%");
+        result.put("profitGrowth", formatGrowth(grossProfit, compareGrossProfit));
+        result.put("marginGrowth", "+0%"); // 毛利率变化
+
+        // 用户统计
+        // 新增用户数
+        long newUsers = userService.lambdaQuery()
+                .ge(User::getCreateTime, startTime)
+                .le(User::getCreateTime, endTime)
+                .count();
+        result.put("newUsers", newUsers);
+
+        // 成交用户数(去重)
+        Set<Long> transactUserIds = new HashSet<>();
+        for (Order order : orders) {
+            if (order.getUserId() != null) {
+                transactUserIds.add(order.getUserId());
+            }
+        }
+        result.put("transactUsers", transactUserIds.size());
+
+        // 客单价(已付款用户的人均消费)
+        double customerPrice = transactUserIds.size() > 0 ? totalSales / transactUserIds.size() : 0;
+        result.put("customerPrice", String.format("%.2f", customerPrice));
+
+        // 对比期用户统计
+        long compareNewUsers = userService.lambdaQuery()
+                .ge(User::getCreateTime, compareStartTime)
+                .le(User::getCreateTime, compareEndTime)
+                .count();
+        
+        Set<Long> compareTransactUserIds = new HashSet<>();
+        for (Order order : compareOrders) {
+            if (order.getUserId() != null) {
+                compareTransactUserIds.add(order.getUserId());
+            }
+        }
+        double compareCustomerPrice = compareTransactUserIds.size() > 0 
+                ? compareSales / compareTransactUserIds.size() : 0;
+
+        result.put("newUsersGrowth", formatGrowth(newUsers, compareNewUsers));
+        result.put("transactGrowth", formatGrowth(transactUserIds.size(), compareTransactUserIds.size()));
+        result.put("customerGrowth", formatGrowth(customerPrice, compareCustomerPrice));
+
+        // 图表数据
+        List<String> dates = new ArrayList<>();
+        List<Double> salesData = new ArrayList<>();
+        List<Integer> ordersData = new ArrayList<>();
+
+        DateTimeFormatter formatter = type.equals("yesterday") 
+                ? DateTimeFormatter.ofPattern("H:00")
+                : DateTimeFormatter.ofPattern("MM-dd");
+
+        if ("yesterday".equals(type)) {
+            // 按小时统计
+            for (int i = 0; i < 24; i++) {
+                LocalDateTime hourStart = startTime.withHour(i);
+                LocalDateTime hourEnd = hourStart.withMinute(59).withSecond(59);
+                
+                dates.add(String.format("%d:00", i));
+                
+                final int hour = i;
+                double hourSales = orders.stream()
+                        .filter(o -> o.getPayTime() != null && o.getPayTime().getHour() == hour)
+                        .mapToDouble(o -> o.getTotalAmount() != null ? o.getTotalAmount() : 0)
+                        .sum();
+                long hourOrders = orders.stream()
+                        .filter(o -> o.getPayTime() != null && o.getPayTime().getHour() == hour)
+                        .count();
+                
+                salesData.add(hourSales);
+                ordersData.add((int) hourOrders);
+            }
+        } else {
+            // 按天统计
+            for (int i = 0; i < days; i++) {
+                LocalDate date = startTime.toLocalDate().plusDays(i);
+                LocalDateTime dayStart = date.atStartOfDay();
+                LocalDateTime dayEnd = date.atTime(LocalTime.MAX);
+                
+                dates.add(date.format(DateTimeFormatter.ofPattern("MM-dd")));
+                
+                double daySales = orders.stream()
+                        .filter(o -> o.getPayTime() != null 
+                                && !o.getPayTime().isBefore(dayStart) 
+                                && !o.getPayTime().isAfter(dayEnd))
+                        .mapToDouble(o -> o.getTotalAmount() != null ? o.getTotalAmount() : 0)
+                        .sum();
+                long dayOrders = orders.stream()
+                        .filter(o -> o.getPayTime() != null 
+                                && !o.getPayTime().isBefore(dayStart) 
+                                && !o.getPayTime().isAfter(dayEnd))
+                        .count();
+                
+                salesData.add(daySales);
+                ordersData.add((int) dayOrders);
+            }
+        }
+
+        result.put("dates", dates);
+        result.put("salesData", salesData);
+        result.put("ordersData", ordersData);
+
+        return result;
+    }
+
+    @Override
+    public Map<String, Object> getQuickLinksData() {
+        Map<String, Object> result = new HashMap<>();
+
+        // 设备不在线数
+        long offlineDevices = deviceService.lambdaQuery()
+                .ne(Device::getStatus, 1)
+                .count();
+        result.put("offlineDevices", offlineDevices);
+
+        // 未支付订单数
+        long unpaidOrders = orderService.lambdaQuery()
+                .eq(Order::getStatus, Order.ORDER_STATUS_待支付)
+                .count();
+        result.put("unpaidOrders", unpaidOrders);
+
+        // 需补货设备数(库存低于预警阈值)
+        try {
+            List<Map<String, Object>> lowStockList = deviceInventoryMapper.selectLowStockInventories();
+            Set<String> needRestockDevices = new HashSet<>();
+            for (Map<String, Object> item : lowStockList) {
+                Object deviceId = item.get("device_id");
+                if (deviceId != null) {
+                    needRestockDevices.add(deviceId.toString());
+                }
+            }
+            result.put("needRestock", needRestockDevices.size());
+        } catch (Exception e) {
+            result.put("needRestock", 0);
+        }
+
+        // 其他待处理项(暂返回0,待后续实现)
+        result.put("unknownProducts", 0);   // 未知商品设备数
+        result.put("pendingAfterSale", 0);  // 售后待审核数
+        result.put("abnormalActivities", 0); // 异常待处理数
+        result.put("complaints", 0);         // 投诉建议数
+        result.put("skuSuggestions", 0);     // SKU建议数
+
+        return result;
+    }
+
+    @Override
+    public Object getDeviceStatusData() {
+        List<Map<String, Object>> result = new ArrayList<>();
+
+        long onlineDevices = deviceService.lambdaQuery()
+                .eq(Device::getStatus, 1)
+                .count();
+        long totalDevices = deviceService.count();
+        long offlineDevices = totalDevices - onlineDevices;
+
+        Map<String, Object> online = new HashMap<>();
+        online.put("name", "在线");
+        online.put("value", onlineDevices);
+        online.put("color", "#67C23A");
+        result.add(online);
+
+        Map<String, Object> offline = new HashMap<>();
+        offline.put("name", "不在线");
+        offline.put("value", offlineDevices);
+        offline.put("color", "#909399");
+        result.add(offline);
+
+        return result;
+    }
+
+    /**
+     * 格式化对比数值(正负号)
+     */
+    private String formatCompare(double value) {
+        if (value > 0) {
+            return "+" + String.format("%.2f", value);
+        } else if (value < 0) {
+            return String.format("%.2f", value);
+        }
+        return "+0.00";
+    }
+
+    /**
+     * 格式化对比数值(整数)
+     */
+    private String formatCompare(int value) {
+        if (value > 0) {
+            return "+" + value;
+        } else if (value < 0) {
+            return String.valueOf(value);
+        }
+        return "+0";
+    }
+
+    /**
+     * 格式化增长率
+     */
+    private String formatGrowth(double current, double previous) {
+        if (previous == 0) {
+            return current > 0 ? "+100%" : "+0%";
+        }
+        double growth = ((current - previous) / previous) * 100;
+        if (growth > 0) {
+            return "+" + String.format("%.0f", growth) + "%";
+        } else if (growth < 0) {
+            return String.format("%.0f", growth) + "%";
+        }
+        return "+0%";
+    }
+
+    /**
+     * 格式化增长率(整数)
+     */
+    private String formatGrowth(long current, long previous) {
+        if (previous == 0) {
+            return current > 0 ? "+100%" : "+0%";
+        }
+        double growth = ((double)(current - previous) / previous) * 100;
+        if (growth > 0) {
+            return "+" + String.format("%.0f", growth) + "%";
+        } else if (growth < 0) {
+            return String.format("%.0f", growth) + "%";
+        }
+        return "+0%";
+    }
+}

+ 59 - 0
haha-service/src/main/java/com/haha/service/impl/DeviceInventoryServiceImpl.java

@@ -199,4 +199,63 @@ public class DeviceInventoryServiceImpl extends ServiceImpl<DeviceInventoryMappe
     public DeviceInventory getByDeviceAndProduct(String deviceId, Long productId) {
         return deviceInventoryMapper.selectByDeviceAndProduct(deviceId, productId);
     }
+
+    @Override
+    public IPage<Map<String, Object>> getDeviceInventoryStats(int page, int pageSize, Map<String, Object> params) {
+        Page<Map<String, Object>> pageParam = new Page<>(page, pageSize);
+
+        String shopName = (String) params.get("shopName");
+        String deviceName = (String) params.get("deviceName");
+        String deviceId = (String) params.get("deviceId");
+        Boolean hasUnknownProduct = null;
+        Object hasUnknownObj = params.get("hasUnknownProduct");
+        if (hasUnknownObj != null) {
+            hasUnknownProduct = Boolean.valueOf(hasUnknownObj.toString());
+        }
+        Integer stockStatus = null;
+        Object stockStatusObj = params.get("stockStatus");
+        if (stockStatusObj != null && !stockStatusObj.toString().isEmpty()) {
+            stockStatus = Integer.valueOf(stockStatusObj.toString());
+        }
+
+        IPage<Map<String, Object>> result = deviceInventoryMapper.selectDeviceInventoryStats(
+                pageParam, shopName, deviceName, deviceId, hasUnknownProduct, stockStatus);
+
+        // 处理每条记录,添加计算字段
+        for (Map<String, Object> record : result.getRecords()) {
+            // 计算库存占比
+            Integer remainingStock = record.get("remaining_stock") != null ?
+                    ((Number) record.get("remaining_stock")).intValue() : 0;
+            Integer standardStock = record.get("standard_stock") != null ?
+                    ((Number) record.get("standard_stock")).intValue() : 0;
+
+            int stockRatio = standardStock > 0 ? (remainingStock * 100 / standardStock) : 0;
+            record.put("stock_ratio", stockRatio);
+
+            // 判断库存状态
+            String stockStatusStr;
+            Integer zeroStockCount = record.get("zero_stock_count") != null ?
+                    ((Number) record.get("zero_stock_count")).intValue() : 0;
+            Integer lowStockCount = record.get("low_stock_count") != null ?
+                    ((Number) record.get("low_stock_count")).intValue() : 0;
+            Integer productCount = record.get("product_count") != null ?
+                    ((Number) record.get("product_count")).intValue() : 0;
+
+            if (productCount == 0 || remainingStock == 0) {
+                stockStatusStr = "缺货";
+            } else if (lowStockCount > 0 || stockRatio < 50) {
+                stockStatusStr = "低库存";
+            } else {
+                stockStatusStr = "正常";
+            }
+            record.put("stock_status", stockStatusStr);
+
+            // 是否有未知商品
+            Integer unknownProductCount = record.get("unknown_product_count") != null ?
+                    ((Number) record.get("unknown_product_count")).intValue() : 0;
+            record.put("has_unknown_product", unknownProductCount > 0);
+        }
+
+        return result;
+    }
 }

+ 153 - 0
haha-service/src/main/java/com/haha/service/impl/ShopReplenisherServiceImpl.java

@@ -0,0 +1,153 @@
+package com.haha.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.haha.common.vo.Result;
+import com.haha.entity.ShopReplenisher;
+import com.haha.entity.Admin;
+import com.haha.mapper.ShopReplenisherMapper;
+import com.haha.service.ShopReplenisherService;
+import com.haha.service.AdminService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 门店补货员服务实现类
+ */
+@Slf4j
+@Service
+public class ShopReplenisherServiceImpl extends ServiceImpl<ShopReplenisherMapper, ShopReplenisher> implements ShopReplenisherService {
+
+    @Autowired
+    private ShopReplenisherMapper shopReplenisherMapper;
+
+    @Autowired
+    private AdminService adminService;
+
+    @Override
+    public List<ShopReplenisher> getReplenishersByShopId(Long shopId) {
+        List<ShopReplenisher> list = shopReplenisherMapper.selectByShopId(shopId);
+        // 填充状态标签
+        for (ShopReplenisher sr : list) {
+            if (sr.getStatus() != null) {
+                if (sr.getStatus() == 1) {
+                    sr.setStatusLabel("正常");
+                    sr.setStatusColor("success");
+                } else {
+                    sr.setStatusLabel("禁用");
+                    sr.setStatusColor("danger");
+                }
+            }
+        }
+        return list;
+    }
+
+    @Override
+    @Transactional
+    public Result<String> addReplenisher(Long shopId, Long adminId) {
+        try {
+            // 检查管理员是否存在
+            Admin admin = adminService.getById(adminId);
+            if (admin == null) {
+                return Result.error(404, "管理员不存在");
+            }
+
+            // 检查是否已存在
+            int count = shopReplenisherMapper.countByShopIdAndAdminId(shopId, adminId);
+            if (count > 0) {
+                return Result.error(400, "该管理员已是此门店的补货员");
+            }
+
+            // 创建关联
+            ShopReplenisher replenisher = new ShopReplenisher();
+            replenisher.setShopId(shopId);
+            replenisher.setAdminId(adminId);
+            replenisher.setStatus(1);
+            replenisher.setCreateTime(LocalDateTime.now());
+            replenisher.setUpdateTime(LocalDateTime.now());
+
+            boolean success = this.save(replenisher);
+            return success ? Result.success("添加成功") : Result.error(500, "添加失败");
+
+        } catch (Exception e) {
+            log.error("添加补货员失败: shopId={}, adminId={}, error={}", shopId, adminId, e.getMessage(), e);
+            return Result.error(500, "添加失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    @Transactional
+    public int batchAddReplenishers(Long shopId, List<Long> adminIds) {
+        int successCount = 0;
+        for (Long adminId : adminIds) {
+            Result<String> result = addReplenisher(shopId, adminId);
+            if (result.getCode() == 200) {
+                successCount++;
+            }
+        }
+        return successCount;
+    }
+
+    @Override
+    @Transactional
+    public Result<String> removeReplenisher(Long shopId, Long adminId) {
+        try {
+            LambdaQueryWrapper<ShopReplenisher> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(ShopReplenisher::getShopId, shopId)
+                    .eq(ShopReplenisher::getAdminId, adminId);
+
+            boolean success = this.remove(wrapper);
+            return success ? Result.success("移除成功") : Result.error(500, "移除失败");
+
+        } catch (Exception e) {
+            log.error("移除补货员失败: shopId={}, adminId={}, error={}", shopId, adminId, e.getMessage(), e);
+            return Result.error(500, "移除失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    @Transactional
+    public int batchRemoveReplenishers(Long shopId, List<Long> adminIds) {
+        if (adminIds == null || adminIds.isEmpty()) {
+            return 0;
+        }
+
+        LambdaQueryWrapper<ShopReplenisher> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(ShopReplenisher::getShopId, shopId)
+                .in(ShopReplenisher::getAdminId, adminIds);
+
+        return (int) this.count(wrapper);
+    }
+
+    @Override
+    @Transactional
+    public Result<String> updateStatus(Long id, Integer status) {
+        try {
+            ShopReplenisher replenisher = this.getById(id);
+            if (replenisher == null) {
+                return Result.error(404, "记录不存在");
+            }
+
+            replenisher.setStatus(status);
+            replenisher.setUpdateTime(LocalDateTime.now());
+
+            boolean success = this.updateById(replenisher);
+            return success ? Result.success("状态更新成功") : Result.error(500, "状态更新失败");
+
+        } catch (Exception e) {
+            log.error("更新补货员状态失败: id={}, error={}", id, e.getMessage(), e);
+            return Result.error(500, "更新失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    public boolean isReplenisher(Long shopId, Long adminId) {
+        return shopReplenisherMapper.countByShopIdAndAdminId(shopId, adminId) > 0;
+    }
+}