Quellcode durchsuchen

新品申请,库存管理

skyline vor 3 Monaten
Ursprung
Commit
08c17713ea
24 geänderte Dateien mit 2522 neuen und 0 gelöschten Zeilen
  1. 310 0
      haha-admin/src/main/java/com/haha/admin/controller/InventoryController.java
  2. 192 0
      haha-admin/src/main/java/com/haha/admin/controller/NewProductApplyController.java
  3. 75 0
      haha-entity/src/main/java/com/haha/entity/DeviceInventory.java
  4. 99 0
      haha-entity/src/main/java/com/haha/entity/InventoryLog.java
  5. 225 0
      haha-entity/src/main/java/com/haha/entity/NewProductApply.java
  6. 90 0
      haha-entity/src/main/java/com/haha/entity/StockRecord.java
  7. 70 0
      haha-entity/src/main/java/com/haha/entity/StockRecordItem.java
  8. 60 0
      haha-entity/src/main/java/com/haha/entity/Stocker.java
  9. 44 0
      haha-mapper/src/main/java/com/haha/mapper/DeviceInventoryMapper.java
  10. 40 0
      haha-mapper/src/main/java/com/haha/mapper/InventoryLogMapper.java
  11. 12 0
      haha-mapper/src/main/java/com/haha/mapper/NewProductApplyMapper.java
  12. 12 0
      haha-mapper/src/main/java/com/haha/mapper/StockRecordItemMapper.java
  13. 41 0
      haha-mapper/src/main/java/com/haha/mapper/StockRecordMapper.java
  14. 12 0
      haha-mapper/src/main/java/com/haha/mapper/StockerMapper.java
  15. 102 0
      haha-service/src/main/java/com/haha/service/DeviceInventoryService.java
  16. 67 0
      haha-service/src/main/java/com/haha/service/InventoryLogService.java
  17. 59 0
      haha-service/src/main/java/com/haha/service/NewProductApplyService.java
  18. 33 0
      haha-service/src/main/java/com/haha/service/OrderInventoryService.java
  19. 79 0
      haha-service/src/main/java/com/haha/service/StockRecordService.java
  20. 202 0
      haha-service/src/main/java/com/haha/service/impl/DeviceInventoryServiceImpl.java
  21. 113 0
      haha-service/src/main/java/com/haha/service/impl/InventoryLogServiceImpl.java
  22. 237 0
      haha-service/src/main/java/com/haha/service/impl/NewProductApplyServiceImpl.java
  23. 191 0
      haha-service/src/main/java/com/haha/service/impl/OrderInventoryServiceImpl.java
  24. 157 0
      haha-service/src/main/java/com/haha/service/impl/StockRecordServiceImpl.java

+ 310 - 0
haha-admin/src/main/java/com/haha/admin/controller/InventoryController.java

@@ -0,0 +1,310 @@
+package com.haha.admin.controller;
+
+import cn.dev33.satoken.stp.StpUtil;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.haha.common.vo.Result;
+import com.haha.entity.DeviceInventory;
+import com.haha.entity.InventoryLog;
+import com.haha.entity.StockRecord;
+import com.haha.service.DeviceInventoryService;
+import com.haha.service.InventoryLogService;
+import com.haha.service.StockRecordService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 库存管理控制器
+ */
+@Slf4j
+@RestController
+@RequestMapping("/inventory")
+public class InventoryController {
+
+    @Autowired
+    private DeviceInventoryService deviceInventoryService;
+
+    @Autowired
+    private InventoryLogService inventoryLogService;
+
+    @Autowired
+    private StockRecordService stockRecordService;
+
+    // ==================== 库存查询 ====================
+
+    /**
+     * 分页查询库存列表
+     */
+    @GetMapping("/list")
+    public Result<Map<String, Object>> list(@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());
+        }
+
+        // 处理低库存筛选参数
+        if ("true".equals(params.get("lowStock"))) {
+            params.put("lowStock", true);
+        }
+
+        IPage<DeviceInventory> pageResult = deviceInventoryService.getPage(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);
+    }
+
+    /**
+     * 查询设备库存详情
+     */
+    @GetMapping("/device/{deviceId}")
+    public Result<List<Map<String, Object>>> getDeviceInventory(@PathVariable String deviceId) {
+        List<Map<String, Object>> inventory = deviceInventoryService.getInventoryWithProduct(deviceId);
+        return Result.success(inventory);
+    }
+
+    /**
+     * 查询低库存商品
+     */
+    @GetMapping("/low-stock")
+    public Result<List<Map<String, Object>>> getLowStock() {
+        List<Map<String, Object>> lowStockList = deviceInventoryService.getLowStockList();
+        return Result.success(lowStockList);
+    }
+
+    /**
+     * 获取库存统计
+     */
+    @GetMapping("/statistics")
+    public Result<Map<String, Object>> getStatistics() {
+        Map<String, Object> stats = deviceInventoryService.getStatistics();
+        return Result.success(stats);
+    }
+
+    // ==================== 库存操作 ====================
+
+    /**
+     * 增加库存(上货)
+     */
+    @PostMapping("/increase")
+    public Result<DeviceInventory> increaseStock(@RequestBody Map<String, Object> params) {
+        try {
+            Long userId = StpUtil.getLoginIdAsLong();
+            String userName = StpUtil.getLoginIdAsString();
+
+            String deviceId = (String) params.get("deviceId");
+            Long productId = Long.parseLong(params.get("productId").toString());
+            String productCode = (String) params.get("productCode");
+            String productName = (String) params.get("productName");
+            Integer quantity = Integer.parseInt(params.get("quantity").toString());
+            Integer shelfNum = params.get("shelfNum") != null ? 
+                    Integer.parseInt(params.get("shelfNum").toString()) : null;
+            String position = (String) params.get("position");
+            String activityId = (String) params.get("activityId");
+
+            DeviceInventory inventory = deviceInventoryService.increaseStock(
+                    deviceId, productId, productCode, productName,
+                    quantity, shelfNum, position,
+                    userId, userName, activityId);
+
+            return Result.success("库存增加成功", inventory);
+        } catch (Exception e) {
+            log.error("增加库存失败", e);
+            return Result.error(e.getMessage());
+        }
+    }
+
+    /**
+     * 调整库存
+     */
+    @PostMapping("/adjust")
+    public Result<DeviceInventory> adjustStock(@RequestBody Map<String, Object> params) {
+        try {
+            Long userId = StpUtil.getLoginIdAsLong();
+            String userName = StpUtil.getLoginIdAsString();
+
+            String deviceId = (String) params.get("deviceId");
+            Long productId = Long.parseLong(params.get("productId").toString());
+            Integer newStock = Integer.parseInt(params.get("newStock").toString());
+            String remark = (String) params.get("remark");
+
+            DeviceInventory inventory = deviceInventoryService.adjustStock(
+                    deviceId, productId, newStock, remark, userId, userName);
+
+            return Result.success("库存调整成功", inventory);
+        } catch (Exception e) {
+            log.error("调整库存失败", e);
+            return Result.error(e.getMessage());
+        }
+    }
+
+    // ==================== 库存变动日志 ====================
+
+    /**
+     * 分页查询库存变动日志
+     */
+    @GetMapping("/logs")
+    public Result<Map<String, Object>> getLogList(@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<InventoryLog> pageResult = inventoryLogService.getPage(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);
+    }
+
+    /**
+     * 查询设备库存变动统计
+     */
+    @GetMapping("/logs/statistics")
+    public Result<List<Map<String, Object>>> getLogStatistics(
+            @RequestParam String deviceId,
+            @RequestParam(required = false) String startTime,
+            @RequestParam(required = false) String endTime) {
+        List<Map<String, Object>> stats = inventoryLogService.getChangeStatistics(deviceId, startTime, endTime);
+        return Result.success(stats);
+    }
+
+    // ==================== 上货记录 ====================
+
+    /**
+     * 分页查询上货记录
+     */
+    @GetMapping("/records")
+    public Result<Map<String, Object>> getRecordList(@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<StockRecord> pageResult = stockRecordService.getPage(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);
+    }
+
+    /**
+     * 获取上货记录详情
+     */
+    @GetMapping("/records/{id}")
+    public Result<Map<String, Object>> getRecordDetail(@PathVariable Long id) {
+        Map<String, Object> detail = stockRecordService.getDetailWithDeviceName(id);
+        return Result.success(detail);
+    }
+
+    /**
+     * 创建上货记录
+     */
+    @PostMapping("/records")
+    public Result<StockRecord> createRecord(@RequestBody Map<String, Object> params) {
+        try {
+            String deviceId = (String) params.get("deviceId");
+            Integer stockType = params.get("stockType") != null ?
+                    Integer.parseInt(params.get("stockType").toString()) : 1;
+            String remark = (String) params.get("remark");
+
+            // 获取当前用户作为上货员
+            Long stockerId = StpUtil.getLoginIdAsLong();
+            String stockerName = StpUtil.getLoginIdAsString();
+            String stockerPhone = (String) params.get("stockerPhone");
+
+            StockRecord record = stockRecordService.createRecord(
+                    deviceId, stockType, stockerId, stockerName, stockerPhone, remark);
+
+            return Result.success("上货记录创建成功", record);
+        } catch (Exception e) {
+            log.error("创建上货记录失败", e);
+            return Result.error(e.getMessage());
+        }
+    }
+
+    /**
+     * 完成上货记录
+     */
+    @PutMapping("/records/{id}/complete")
+    public Result<String> completeRecord(
+            @PathVariable Long id,
+            @RequestBody Map<String, Object> params) {
+        try {
+            Integer totalItems = params.get("totalItems") != null ?
+                    Integer.parseInt(params.get("totalItems").toString()) : 0;
+            Integer totalQuantity = params.get("totalQuantity") != null ?
+                    Integer.parseInt(params.get("totalQuantity").toString()) : 0;
+            String activityId = (String) params.get("activityId");
+
+            stockRecordService.completeActivity(id, totalItems, totalQuantity, activityId);
+            return Result.success("上货记录已完成");
+        } catch (Exception e) {
+            log.error("完成上货记录失败", e);
+            return Result.error(e.getMessage());
+        }
+    }
+
+    /**
+     * 取消上货记录
+     */
+    @PutMapping("/records/{id}/cancel")
+    public Result<String> cancelRecord(@PathVariable Long id) {
+        try {
+            stockRecordService.cancelActivity(id);
+            return Result.success("上货记录已取消");
+        } catch (Exception e) {
+            log.error("取消上货记录失败", e);
+            return Result.error(e.getMessage());
+        }
+    }
+
+    /**
+     * 获取上货员工作统计
+     */
+    @GetMapping("/records/stocker-statistics")
+    public Result<Map<String, Object>> getStockerStatistics(
+            @RequestParam Long stockerId,
+            @RequestParam(required = false) String startTime,
+            @RequestParam(required = false) String endTime) {
+        Map<String, Object> stats = stockRecordService.getStockerStatistics(stockerId, startTime, endTime);
+        return Result.success(stats);
+    }
+}

+ 192 - 0
haha-admin/src/main/java/com/haha/admin/controller/NewProductApplyController.java

@@ -0,0 +1,192 @@
+package com.haha.admin.controller;
+
+import cn.dev33.satoken.stp.StpUtil;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.haha.common.vo.Result;
+import com.haha.entity.NewProductApply;
+import com.haha.service.NewProductApplyService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 新品申请控制器
+ */
+@Slf4j
+@RestController
+@RequestMapping("/new-product-apply")
+public class NewProductApplyController {
+
+    @Autowired
+    private NewProductApplyService newProductApplyService;
+
+    /**
+     * 分页查询新品申请列表
+     * @param params 查询参数
+     * @return 申请列表
+     */
+    @GetMapping("/list")
+    public Result<Map<String, Object>> list(@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<NewProductApply> pageResult = newProductApplyService.getPage(page, pageSize, params);
+        
+        Map<String, Object> result = new HashMap<>();
+        result.put("list", pageResult.getRecords());
+        result.put("total", pageResult.getTotal());
+        result.put("pageSize", pageResult.getSize());
+        result.put("currentPage", pageResult.getCurrent());
+        
+        return Result.success(result);
+    }
+
+    /**
+     * 获取申请详情
+     * @param id 申请ID
+     * @return 申请详情
+     */
+    @GetMapping("/{id}")
+    public Result<NewProductApply> getById(@PathVariable Long id) {
+        NewProductApply apply = newProductApplyService.getById(id);
+        return Result.success(apply);
+    }
+
+    /**
+     * 获取申请统计信息
+     * @return 统计信息
+     */
+    @GetMapping("/statistics")
+    public Result<Map<String, Object>> getStatistics() {
+        Map<String, Object> stats = newProductApplyService.getStatistics();
+        return Result.success(stats);
+    }
+
+    /**
+     * 提交新品申请
+     * @param apply 申请信息
+     * @return 申请ID
+     */
+    @PostMapping("/submit")
+    public Result<Long> submit(@RequestBody NewProductApply apply) {
+        try {
+            // 获取当前登录用户
+            Long userId = StpUtil.getLoginIdAsLong();
+            String userName = StpUtil.getLoginIdAsString();
+            
+            Long id = newProductApplyService.submitApply(apply, userId, userName);
+            return Result.success("新品申请已提交,请等待审核", id);
+        } catch (Exception e) {
+            log.error("提交新品申请失败", e);
+            return Result.error(e.getMessage());
+        }
+    }
+
+    /**
+     * 保存草稿(仅保存到本地数据库,不提交到哈哈平台)
+     * @param apply 申请信息
+     * @return 申请ID
+     */
+    @PostMapping("/save-draft")
+    public Result<Long> saveDraft(@RequestBody NewProductApply apply) {
+        try {
+            // 获取当前登录用户
+            Long userId = StpUtil.getLoginIdAsLong();
+            String userName = StpUtil.getLoginIdAsString();
+            
+            apply.setStatus(0); // 待提交状态
+            apply.setApplicantId(userId);
+            apply.setApplicantName(userName);
+            
+            newProductApplyService.save(apply);
+            return Result.success("草稿保存成功", apply.getId());
+        } catch (Exception e) {
+            log.error("保存草稿失败", e);
+            return Result.error(e.getMessage());
+        }
+    }
+
+    /**
+     * 更新申请信息
+     * @param id 申请ID
+     * @param apply 申请信息
+     * @return 是否成功
+     */
+    @PutMapping("/{id}")
+    public Result<Boolean> update(@PathVariable Long id, @RequestBody NewProductApply apply) {
+        try {
+            NewProductApply existing = newProductApplyService.getById(id);
+            if (existing == null) {
+                return Result.error("申请记录不存在");
+            }
+            
+            // 只有待提交状态才能修改
+            if (existing.getStatus() != 0) {
+                return Result.error("只有待提交状态的申请才能修改");
+            }
+            
+            apply.setId(id);
+            newProductApplyService.updateById(apply);
+            return Result.success("更新成功", true);
+        } catch (Exception e) {
+            log.error("更新申请失败", e);
+            return Result.error(e.getMessage());
+        }
+    }
+
+    /**
+     * 删除申请(仅待提交状态可删除)
+     * @param id 申请ID
+     * @return 是否成功
+     */
+    @DeleteMapping("/{id}")
+    public Result<Boolean> delete(@PathVariable Long id) {
+        try {
+            NewProductApply existing = newProductApplyService.getById(id);
+            if (existing == null) {
+                return Result.error("申请记录不存在");
+            }
+            
+            // 只有待提交状态才能删除
+            if (existing.getStatus() != 0) {
+                return Result.error("只有待提交状态的申请才能删除");
+            }
+            
+            newProductApplyService.removeById(id);
+            return Result.success("删除成功", true);
+        } catch (Exception e) {
+            log.error("删除申请失败", e);
+            return Result.error(e.getMessage());
+        }
+    }
+
+    /**
+     * 查询商品总库中是否存在该条码的商品
+     * @param barcode 商品条码
+     * @return 商品信息(如果存在)
+     */
+    @GetMapping("/check-barcode")
+    public Result<Map<String, Object>> checkBarcode(@RequestParam String barcode) {
+        try {
+            // 调用哈哈平台API查询商品总库
+            Map<String, Object> result = newProductApplyService.checkBarcodeInTotalList(barcode);
+            return Result.success(result);
+        } catch (Exception e) {
+            log.error("查询条码失败", e);
+            return Result.error(e.getMessage());
+        }
+    }
+}

+ 75 - 0
haha-entity/src/main/java/com/haha/entity/DeviceInventory.java

@@ -0,0 +1,75 @@
+package com.haha.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 设备商品库存实体类
+ */
+@Data
+@TableName("t_device_inventory")
+public class DeviceInventory implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 设备ID(SN号)
+     */
+    private String deviceId;
+
+    /**
+     * 商品ID(本地)
+     */
+    private Long productId;
+
+    /**
+     * 商品编码(哈哈平台code)
+     */
+    private String productCode;
+
+    /**
+     * 商品名称(冗余)
+     */
+    private String productName;
+
+    /**
+     * 当前库存数量
+     */
+    private Integer stock;
+
+    /**
+     * 所在货架层号
+     */
+    private Integer shelfNum;
+
+    /**
+     * 货道位置(left/right)
+     */
+    private String position;
+
+    /**
+     * 库存预警阈值
+     */
+    private Integer warningThreshold;
+
+    /**
+     * 最后补货时间
+     */
+    private LocalDateTime lastRestockTime;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    private LocalDateTime updateTime;
+}

+ 99 - 0
haha-entity/src/main/java/com/haha/entity/InventoryLog.java

@@ -0,0 +1,99 @@
+package com.haha.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 库存变动日志实体类
+ */
+@Data
+@TableName("t_inventory_log")
+public class InventoryLog implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 变动类型:1上货增加,2销售减少,3调整增加,4调整减少,5盘点调整
+     */
+    public static final int TYPE_RESTOCK = 1;
+    public static final int TYPE_SALE = 2;
+    public static final int TYPE_ADJUST_ADD = 3;
+    public static final int TYPE_ADJUST_SUB = 4;
+    public static final int TYPE_INVENTORY = 5;
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 设备ID
+     */
+    private String deviceId;
+
+    /**
+     * 商品ID
+     */
+    private Long productId;
+
+    /**
+     * 商品编码
+     */
+    private String productCode;
+
+    /**
+     * 商品名称
+     */
+    private String productName;
+
+    /**
+     * 变动类型
+     */
+    private Integer changeType;
+
+    /**
+     * 变动数量(正数增加,负数减少)
+     */
+    private Integer changeQuantity;
+
+    /**
+     * 变动前库存
+     */
+    private Integer beforeStock;
+
+    /**
+     * 变动后库存
+     */
+    private Integer afterStock;
+
+    /**
+     * 关联订单号(销售时)
+     */
+    private String orderId;
+
+    /**
+     * 关联活动号
+     */
+    private String activityId;
+
+    /**
+     * 操作人ID
+     */
+    private Long operatorId;
+
+    /**
+     * 操作人名称
+     */
+    private String operatorName;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+}

+ 225 - 0
haha-entity/src/main/java/com/haha/entity/NewProductApply.java

@@ -0,0 +1,225 @@
+package com.haha.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 新品申请实体类
+ * 用于记录商家向哈哈平台申请新商品的信息
+ */
+@Data
+@TableName("t_new_product_apply")
+public class NewProductApply implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键ID
+     */
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 哈哈平台工单编号(申请成功后返回)
+     */
+    private String hahaApplyId;
+
+    /**
+     * 商品条形码
+     */
+    private String barcode;
+
+    /**
+     * 商品名称
+     */
+    private String name;
+
+    /**
+     * 商品分类
+     */
+    private String category;
+
+    /**
+     * 商品规格
+     */
+    private String specification;
+
+    /**
+     * 建议售价
+     */
+    private BigDecimal price;
+
+    /**
+     * 成本价
+     */
+    private BigDecimal cost;
+
+    /**
+     * 商品图片URL(正面图)
+     */
+    private String imageUrl;
+
+    /**
+     * 商品背面图URL
+     */
+    private String imageUrl2;
+
+    /**
+     * 商品侧面图URL
+     */
+    private String imageUrl3;
+
+    /**
+     * 商品顶部图URL
+     */
+    private String imageUrl4;
+
+    /**
+     * 商品称重图URL
+     */
+    private String imageUrl5;
+
+    /**
+     * 正面图原图URL
+     */
+    private String originalImageUrl1;
+
+    /**
+     * 背面图原图URL
+     */
+    private String originalImageUrl2;
+
+    /**
+     * 侧面图原图URL
+     */
+    private String originalImageUrl3;
+
+    /**
+     * 顶部图原图URL
+     */
+    private String originalImageUrl4;
+
+    /**
+     * 供应商
+     */
+    private String supplier;
+
+    /**
+     * 保质期
+     */
+    private String shelfLife;
+
+    /**
+     * 存储条件
+     */
+    private String storageConditions;
+
+    /**
+     * 商品描述
+     */
+    private String description;
+
+    /**
+     * 申请原因
+     */
+    private String reason;
+
+    /**
+     * 商品类型:0静态,1动态
+     */
+    private Integer productType;
+
+    /**
+     * 学习机号
+     */
+    private String stickerNum;
+
+    /**
+     * 是否标品:1标品,2非标品
+     */
+    private Integer standard;
+
+    /**
+     * 商品长度(mm)
+     */
+    private Integer length;
+
+    /**
+     * 商品宽度(mm)
+     */
+    private Integer width;
+
+    /**
+     * 商品高度(mm)
+     */
+    private Integer height;
+
+    /**
+     * 陈列高度(mm)
+     */
+    private Integer exhibitHeight;
+
+    /**
+     * 商品占列
+     */
+    private Integer columns;
+
+    /**
+     * 一个货道放几个
+     */
+    private Integer numbers;
+
+    /**
+     * 申请状态:0待提交,1审核中,2已通过,3已拒绝
+     */
+    private Integer status;
+
+    /**
+     * 哈哈平台返回的商品code(审核通过后)
+     */
+    private String hahaCode;
+
+    /**
+     * 拒绝原因
+     */
+    private String rejectReason;
+
+    /**
+     * 扩展字段
+     */
+    private String productExtends;
+
+    /**
+     * 申请人ID
+     */
+    private Long applicantId;
+
+    /**
+     * 申请人名称
+     */
+    private String applicantName;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    private LocalDateTime updateTime;
+
+    /**
+     * 提交时间
+     */
+    private LocalDateTime submitTime;
+
+    /**
+     * 审核时间
+     */
+    private LocalDateTime auditTime;
+}

+ 90 - 0
haha-entity/src/main/java/com/haha/entity/StockRecord.java

@@ -0,0 +1,90 @@
+package com.haha.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 上货记录实体类
+ */
+@Data
+@TableName("t_stock_record")
+public class StockRecord implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 设备ID
+     */
+    private String deviceId;
+
+    /**
+     * 哈哈平台活动号
+     */
+    private String activityId;
+
+    /**
+     * 类型:1上货,2补货
+     */
+    private Integer stockType;
+
+    /**
+     * 上货员ID
+     */
+    private Long stockerId;
+
+    /**
+     * 上货员名称
+     */
+    private String stockerName;
+
+    /**
+     * 上货员电话
+     */
+    private String stockerPhone;
+
+    /**
+     * 上货商品种类数
+     */
+    private Integer totalItems;
+
+    /**
+     * 上货总数量
+     */
+    private Integer totalQuantity;
+
+    /**
+     * 状态:1进行中,2已完成,3已取消
+     */
+    private Integer status;
+
+    /**
+     * 开始时间
+     */
+    private LocalDateTime startTime;
+
+    /**
+     * 结束时间
+     */
+    private LocalDateTime endTime;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    private LocalDateTime updateTime;
+}

+ 70 - 0
haha-entity/src/main/java/com/haha/entity/StockRecordItem.java

@@ -0,0 +1,70 @@
+package com.haha.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 上货记录明细实体类
+ */
+@Data
+@TableName("t_stock_record_item")
+public class StockRecordItem implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 上货记录ID
+     */
+    private Long recordId;
+
+    /**
+     * 设备ID
+     */
+    private String deviceId;
+
+    /**
+     * 商品ID
+     */
+    private Long productId;
+
+    /**
+     * 商品编码
+     */
+    private String productCode;
+
+    /**
+     * 商品名称
+     */
+    private String productName;
+
+    /**
+     * 货架层号
+     */
+    private Integer shelfNum;
+
+    /**
+     * 上货数量
+     */
+    private Integer quantity;
+
+    /**
+     * 上货前库存
+     */
+    private Integer beforeStock;
+
+    /**
+     * 上货后库存
+     */
+    private Integer afterStock;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+}

+ 60 - 0
haha-entity/src/main/java/com/haha/entity/Stocker.java

@@ -0,0 +1,60 @@
+package com.haha.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 上货员实体类
+ */
+@Data
+@TableName("t_stocker")
+public class Stocker implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 姓名
+     */
+    private String name;
+
+    /**
+     * 电话
+     */
+    private String phone;
+
+    /**
+     * 工号
+     */
+    private String employeeId;
+
+    /**
+     * 状态:1启用,0禁用
+     */
+    private Integer status;
+
+    /**
+     * 累计任务数
+     */
+    private Integer totalTasks;
+
+    /**
+     * 最后任务时间
+     */
+    private LocalDateTime lastTaskTime;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    private LocalDateTime updateTime;
+}

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

@@ -0,0 +1,44 @@
+package com.haha.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.haha.entity.DeviceInventory;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 设备商品库存Mapper
+ */
+@Mapper
+public interface DeviceInventoryMapper extends BaseMapper<DeviceInventory> {
+
+    /**
+     * 根据设备ID查询库存列表(带商品详情)
+     */
+    @Select("SELECT di.*, p.barcode, p.pic as product_image, p.cost_price, p.retail_price " +
+            "FROM t_device_inventory di " +
+            "LEFT JOIN t_product p ON di.product_id = p.id " +
+            "WHERE di.device_id = #{deviceId} " +
+            "ORDER BY di.shelf_num, di.position")
+    List<Map<String, Object>> selectInventoryWithProduct(@Param("deviceId") String deviceId);
+
+    /**
+     * 根据设备和商品查询库存
+     */
+    @Select("SELECT * FROM t_device_inventory WHERE device_id = #{deviceId} AND product_id = #{productId}")
+    DeviceInventory selectByDeviceAndProduct(@Param("deviceId") String deviceId, @Param("productId") Long productId);
+
+    /**
+     * 查询低库存商品
+     */
+    @Select("SELECT di.*, d.name as device_name, p.barcode " +
+            "FROM t_device_inventory di " +
+            "LEFT JOIN t_device d ON di.device_id = d.device_id " +
+            "LEFT JOIN t_product p ON di.product_id = p.id " +
+            "WHERE di.stock <= di.warning_threshold " +
+            "ORDER BY di.stock ASC")
+    List<Map<String, Object>> selectLowStockInventories();
+}

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

@@ -0,0 +1,40 @@
+package com.haha.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.haha.entity.InventoryLog;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 库存变动日志Mapper
+ */
+@Mapper
+public interface InventoryLogMapper extends BaseMapper<InventoryLog> {
+
+    /**
+     * 查询设备库存变动统计
+     */
+    @Select("SELECT change_type, SUM(ABS(change_quantity)) as total_quantity, COUNT(*) as count " +
+            "FROM t_inventory_log " +
+            "WHERE device_id = #{deviceId} " +
+            "AND create_time BETWEEN #{startTime} AND #{endTime} " +
+            "GROUP BY change_type")
+    List<Map<String, Object>> selectChangeStatistics(@Param("deviceId") String deviceId,
+                                                      @Param("startTime") String startTime,
+                                                      @Param("endTime") String endTime);
+
+    /**
+     * 查询商品变动记录(带详情)
+     */
+    @Select("SELECT il.*, d.name as device_name " +
+            "FROM t_inventory_log il " +
+            "LEFT JOIN t_device d ON il.device_id = d.device_id " +
+            "WHERE il.product_code = #{productCode} " +
+            "ORDER BY il.create_time DESC " +
+            "LIMIT #{limit}")
+    List<Map<String, Object>> selectByProductCode(@Param("productCode") String productCode, @Param("limit") int limit);
+}

+ 12 - 0
haha-mapper/src/main/java/com/haha/mapper/NewProductApplyMapper.java

@@ -0,0 +1,12 @@
+package com.haha.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.haha.entity.NewProductApply;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 新品申请Mapper
+ */
+@Mapper
+public interface NewProductApplyMapper extends BaseMapper<NewProductApply> {
+}

+ 12 - 0
haha-mapper/src/main/java/com/haha/mapper/StockRecordItemMapper.java

@@ -0,0 +1,12 @@
+package com.haha.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.haha.entity.StockRecordItem;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 上货记录明细Mapper
+ */
+@Mapper
+public interface StockRecordItemMapper extends BaseMapper<StockRecordItem> {
+}

+ 41 - 0
haha-mapper/src/main/java/com/haha/mapper/StockRecordMapper.java

@@ -0,0 +1,41 @@
+package com.haha.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.haha.entity.StockRecord;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 上货记录Mapper
+ */
+@Mapper
+public interface StockRecordMapper extends BaseMapper<StockRecord> {
+
+    /**
+     * 查询上货记录(带设备名称)
+     */
+    @Select("SELECT sr.*, d.name as device_name " +
+            "FROM t_stock_record sr " +
+            "LEFT JOIN t_device d ON sr.device_id = d.device_id " +
+            "WHERE sr.id = #{id}")
+    Map<String, Object> selectWithDeviceName(@Param("id") Long id);
+
+    /**
+     * 统计上货员工作数据
+     */
+    @Select("SELECT stocker_id, stocker_name, " +
+            "COUNT(*) as total_activities, " +
+            "SUM(total_quantity) as total_quantity, " +
+            "SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) as completed_count " +
+            "FROM t_stock_record " +
+            "WHERE stocker_id = #{stockerId} " +
+            "AND create_time BETWEEN #{startTime} AND #{endTime} " +
+            "GROUP BY stocker_id, stocker_name")
+    Map<String, Object> selectStockerStatistics(@Param("stockerId") Long stockerId,
+                                                 @Param("startTime") String startTime,
+                                                 @Param("endTime") String endTime);
+}

+ 12 - 0
haha-mapper/src/main/java/com/haha/mapper/StockerMapper.java

@@ -0,0 +1,12 @@
+package com.haha.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.haha.entity.Stocker;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 上货员Mapper
+ */
+@Mapper
+public interface StockerMapper extends BaseMapper<Stocker> {
+}

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

@@ -0,0 +1,102 @@
+package com.haha.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.haha.entity.DeviceInventory;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 设备库存服务接口
+ */
+public interface DeviceInventoryService extends IService<DeviceInventory> {
+
+    /**
+     * 分页查询设备库存列表
+     *
+     * @param page     页码
+     * @param pageSize 每页条数
+     * @param params   查询参数(deviceId, productName, lowStock等)
+     * @return 分页结果
+     */
+    IPage<DeviceInventory> getPage(int page, int pageSize, Map<String, Object> params);
+
+    /**
+     * 查询设备的库存列表(带商品详情)
+     *
+     * @param deviceId 设备ID
+     * @return 库存列表
+     */
+    List<Map<String, Object>> getInventoryWithProduct(String deviceId);
+
+    /**
+     * 查询低库存商品列表
+     *
+     * @return 低库存商品列表
+     */
+    List<Map<String, Object>> getLowStockList();
+
+    /**
+     * 增加库存(上货)
+     *
+     * @param deviceId     设备ID
+     * @param productId    商品ID
+     * @param productCode  商品编码
+     * @param productName  商品名称
+     * @param quantity     增加数量
+     * @param shelfNum     货架层号
+     * @param position     货道位置
+     * @param operatorId   操作人ID
+     * @param operatorName 操作人名称
+     * @param activityId   活动号
+     * @return 库存记录
+     */
+    DeviceInventory increaseStock(String deviceId, Long productId, String productCode, String productName,
+                                  Integer quantity, Integer shelfNum, String position,
+                                  Long operatorId, String operatorName, String activityId);
+
+    /**
+     * 减少库存(销售)
+     *
+     * @param deviceId     设备ID
+     * @param productId    商品ID
+     * @param quantity     减少数量
+     * @param orderId      订单号
+     * @param operatorId   操作人ID
+     * @param operatorName 操作人名称
+     * @return 库存记录
+     */
+    DeviceInventory decreaseStock(String deviceId, Long productId, Integer quantity,
+                                  String orderId, Long operatorId, String operatorName);
+
+    /**
+     * 调整库存
+     *
+     * @param deviceId     设备ID
+     * @param productId    商品ID
+     * @param newStock     新库存数量
+     * @param remark       备注
+     * @param operatorId   操作人ID
+     * @param operatorName 操作人名称
+     * @return 库存记录
+     */
+    DeviceInventory adjustStock(String deviceId, Long productId, Integer newStock,
+                                String remark, Long operatorId, String operatorName);
+
+    /**
+     * 获取库存统计信息
+     *
+     * @return 统计信息
+     */
+    Map<String, Object> getStatistics();
+
+    /**
+     * 根据设备和商品查询库存
+     *
+     * @param deviceId  设备ID
+     * @param productId 商品ID
+     * @return 库存记录
+     */
+    DeviceInventory getByDeviceAndProduct(String deviceId, Long productId);
+}

+ 67 - 0
haha-service/src/main/java/com/haha/service/InventoryLogService.java

@@ -0,0 +1,67 @@
+package com.haha.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.haha.entity.InventoryLog;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 库存变动日志服务接口
+ */
+public interface InventoryLogService extends IService<InventoryLog> {
+
+    /**
+     * 分页查询库存变动日志
+     *
+     * @param page     页码
+     * @param pageSize 每页条数
+     * @param params   查询参数
+     * @return 分页结果
+     */
+    IPage<InventoryLog> getPage(int page, int pageSize, Map<String, Object> params);
+
+    /**
+     * 记录库存变动
+     *
+     * @param deviceId        设备ID
+     * @param productId       商品ID
+     * @param productCode     商品编码
+     * @param productName     商品名称
+     * @param changeType      变动类型
+     * @param changeQuantity  变动数量
+     * @param beforeStock     变动前库存
+     * @param afterStock      变动后库存
+     * @param orderId         关联订单号
+     * @param activityId      关联活动号
+     * @param operatorId      操作人ID
+     * @param operatorName    操作人名称
+     * @param remark          备注
+     * @return 日志记录
+     */
+    InventoryLog logChange(String deviceId, Long productId, String productCode, String productName,
+                           Integer changeType, Integer changeQuantity,
+                           Integer beforeStock, Integer afterStock,
+                           String orderId, String activityId,
+                           Long operatorId, String operatorName, String remark);
+
+    /**
+     * 查询设备的变动统计
+     *
+     * @param deviceId  设备ID
+     * @param startTime 开始时间
+     * @param endTime   结束时间
+     * @return 统计结果
+     */
+    List<Map<String, Object>> getChangeStatistics(String deviceId, String startTime, String endTime);
+
+    /**
+     * 查询商品的变动记录
+     *
+     * @param productCode 商品编码
+     * @param limit       限制条数
+     * @return 变动记录列表
+     */
+    List<Map<String, Object>> getByProductCode(String productCode, int limit);
+}

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

@@ -0,0 +1,59 @@
+package com.haha.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.haha.entity.NewProductApply;
+
+import java.util.Map;
+
+/**
+ * 新品申请服务接口
+ */
+public interface NewProductApplyService extends IService<NewProductApply> {
+
+    /**
+     * 分页查询新品申请列表
+     *
+     * @param page     页码
+     * @param pageSize 每页条数
+     * @param params   查询参数
+     * @return 分页结果
+     */
+    IPage<NewProductApply> getPage(int page, int pageSize, Map<String, Object> params);
+
+    /**
+     * 提交新品申请
+     * 将申请信息保存到本地数据库,并调用哈哈平台API提交申请
+     *
+     * @param apply           申请信息
+     * @param applicantId     申请人ID
+     * @param applicantName   申请人名称
+     * @return 申请记录ID
+     */
+    Long submitApply(NewProductApply apply, Long applicantId, String applicantName);
+
+    /**
+     * 根据哈哈平台回调更新申请状态
+     *
+     * @param hahaApplyId 哈哈平台工单编号
+     * @param status      状态:2已通过,3已拒绝
+     * @param hahaCode    哈哈平台商品code(审核通过时)
+     * @param rejectReason 拒绝原因
+     */
+    void updateByCallback(String hahaApplyId, Integer status, String hahaCode, String rejectReason);
+
+    /**
+     * 获取申请统计信息
+     *
+     * @return 统计信息
+     */
+    Map<String, Object> getStatistics();
+
+    /**
+     * 查询商品总库中是否存在该条码的商品
+     *
+     * @param barcode 商品条码
+     * @return 商品信息(如果存在返回商品信息,否则返回null)
+     */
+    Map<String, Object> checkBarcodeInTotalList(String barcode);
+}

+ 33 - 0
haha-service/src/main/java/com/haha/service/OrderInventoryService.java

@@ -0,0 +1,33 @@
+package com.haha.service;
+
+import com.haha.entity.Order;
+
+/**
+ * 订单库存服务接口
+ * 处理订单相关的库存操作
+ */
+public interface OrderInventoryService {
+
+    /**
+     * 订单支付成功,扣减库存
+     *
+     * @param order 订单
+     */
+    void decreaseStockOnPaid(Order order);
+
+    /**
+     * 订单退款,恢复库存
+     *
+     * @param order 订单
+     */
+    void restoreStockOnRefund(Order order);
+
+    /**
+     * 检查订单商品库存是否充足
+     *
+     * @param deviceId 设备ID
+     * @param items    商品明细JSON
+     * @return 是否充足
+     */
+    boolean checkStockAvailable(String deviceId, String items);
+}

+ 79 - 0
haha-service/src/main/java/com/haha/service/StockRecordService.java

@@ -0,0 +1,79 @@
+package com.haha.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.haha.entity.StockRecord;
+
+import java.util.Map;
+
+/**
+ * 上货记录服务接口
+ */
+public interface StockRecordService extends IService<StockRecord> {
+
+    /**
+     * 分页查询上货记录
+     *
+     * @param page     页码
+     * @param pageSize 每页条数
+     * @param params   查询参数
+     * @return 分页结果
+     */
+    IPage<StockRecord> getPage(int page, int pageSize, Map<String, Object> params);
+
+    /**
+     * 创建上货记录
+     *
+     * @param deviceId    设备ID
+     * @param stockType   类型:1上货,2补货
+     * @param stockerId   上货员ID
+     * @param stockerName 上货员名称
+     * @param stockerPhone 上货员电话
+     * @param remark      备注
+     * @return 上货记录
+     */
+    StockRecord createRecord(String deviceId, Integer stockType, Long stockerId,
+                             String stockerName, String stockerPhone, String remark);
+
+    /**
+     * 开始上货活动
+     *
+     * @param recordId 记录ID
+     */
+    void startActivity(Long recordId);
+
+    /**
+     * 完成上货活动
+     *
+     * @param recordId    记录ID
+     * @param totalItems  商品种类数
+     * @param totalQuantity 总数量
+     * @param activityId  哈哈平台活动号
+     */
+    void completeActivity(Long recordId, Integer totalItems, Integer totalQuantity, String activityId);
+
+    /**
+     * 取消上货活动
+     *
+     * @param recordId 记录ID
+     */
+    void cancelActivity(Long recordId);
+
+    /**
+     * 获取上货员工作统计
+     *
+     * @param stockerId 上货员ID
+     * @param startTime 开始时间
+     * @param endTime   结束时间
+     * @return 统计数据
+     */
+    Map<String, Object> getStockerStatistics(Long stockerId, String startTime, String endTime);
+
+    /**
+     * 获取上货记录详情(带设备名称)
+     *
+     * @param id 记录ID
+     * @return 记录详情
+     */
+    Map<String, Object> getDetailWithDeviceName(Long id);
+}

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

@@ -0,0 +1,202 @@
+package com.haha.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.haha.entity.DeviceInventory;
+import com.haha.entity.InventoryLog;
+import com.haha.mapper.DeviceInventoryMapper;
+import com.haha.service.DeviceInventoryService;
+import com.haha.service.InventoryLogService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+
+import java.time.LocalDateTime;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 设备库存服务实现类
+ */
+@Slf4j
+@Service
+public class DeviceInventoryServiceImpl extends ServiceImpl<DeviceInventoryMapper, DeviceInventory>
+        implements DeviceInventoryService {
+
+    @Autowired
+    private DeviceInventoryMapper deviceInventoryMapper;
+
+    @Autowired
+    private InventoryLogService inventoryLogService;
+
+    @Override
+    public IPage<DeviceInventory> getPage(int page, int pageSize, Map<String, Object> params) {
+        LambdaQueryWrapper<DeviceInventory> wrapper = new LambdaQueryWrapper<>();
+
+        // 设备ID筛选
+        String deviceId = (String) params.get("deviceId");
+        if (StringUtils.hasText(deviceId)) {
+            wrapper.eq(DeviceInventory::getDeviceId, deviceId);
+        }
+
+        // 商品名称模糊搜索
+        String productName = (String) params.get("productName");
+        if (StringUtils.hasText(productName)) {
+            wrapper.like(DeviceInventory::getProductName, productName);
+        }
+
+        // 低库存筛选
+        Object lowStock = params.get("lowStock");
+        if (lowStock != null && Boolean.TRUE.equals(lowStock)) {
+            wrapper.apply("stock <= warning_threshold");
+        }
+
+        // 按更新时间倒序
+        wrapper.orderByDesc(DeviceInventory::getUpdateTime);
+
+        return page(new Page<>(page, pageSize), wrapper);
+    }
+
+    @Override
+    public List<Map<String, Object>> getInventoryWithProduct(String deviceId) {
+        return deviceInventoryMapper.selectInventoryWithProduct(deviceId);
+    }
+
+    @Override
+    public List<Map<String, Object>> getLowStockList() {
+        return deviceInventoryMapper.selectLowStockInventories();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public DeviceInventory increaseStock(String deviceId, Long productId, String productCode, String productName,
+                                         Integer quantity, Integer shelfNum, String position,
+                                         Long operatorId, String operatorName, String activityId) {
+        // 查询现有库存
+        DeviceInventory inventory = deviceInventoryMapper.selectByDeviceAndProduct(deviceId, productId);
+
+        int beforeStock = 0;
+        if (inventory == null) {
+            // 新建库存记录
+            inventory = new DeviceInventory();
+            inventory.setDeviceId(deviceId);
+            inventory.setProductId(productId);
+            inventory.setProductCode(productCode);
+            inventory.setProductName(productName);
+            inventory.setStock(quantity);
+            inventory.setShelfNum(shelfNum);
+            inventory.setPosition(position);
+            inventory.setWarningThreshold(5); // 默认预警阈值
+            inventory.setLastRestockTime(LocalDateTime.now());
+            inventory.setCreateTime(LocalDateTime.now());
+            inventory.setUpdateTime(LocalDateTime.now());
+            save(inventory);
+            beforeStock = 0;
+        } else {
+            // 更新库存
+            beforeStock = inventory.getStock();
+            inventory.setStock(beforeStock + quantity);
+            inventory.setShelfNum(shelfNum);
+            inventory.setPosition(position);
+            inventory.setLastRestockTime(LocalDateTime.now());
+            inventory.setUpdateTime(LocalDateTime.now());
+            updateById(inventory);
+        }
+
+        // 记录日志
+        inventoryLogService.logChange(deviceId, productId, productCode, productName,
+                InventoryLog.TYPE_RESTOCK, quantity, beforeStock, inventory.getStock(),
+                null, activityId, operatorId, operatorName, "上货增加库存");
+
+        return inventory;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public DeviceInventory decreaseStock(String deviceId, Long productId, Integer quantity,
+                                         String orderId, Long operatorId, String operatorName) {
+        DeviceInventory inventory = deviceInventoryMapper.selectByDeviceAndProduct(deviceId, productId);
+
+        if (inventory == null) {
+            log.warn("库存记录不存在: deviceId={}, productId={}", deviceId, productId);
+            return null;
+        }
+
+        int beforeStock = inventory.getStock();
+        int newStock = Math.max(0, beforeStock - quantity);
+        inventory.setStock(newStock);
+        inventory.setUpdateTime(LocalDateTime.now());
+        updateById(inventory);
+
+        // 记录日志
+        inventoryLogService.logChange(deviceId, productId, inventory.getProductCode(), inventory.getProductName(),
+                InventoryLog.TYPE_SALE, -quantity, beforeStock, newStock,
+                orderId, null, operatorId, operatorName, "销售扣减库存");
+
+        return inventory;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public DeviceInventory adjustStock(String deviceId, Long productId, Integer newStock,
+                                       String remark, Long operatorId, String operatorName) {
+        DeviceInventory inventory = deviceInventoryMapper.selectByDeviceAndProduct(deviceId, productId);
+
+        if (inventory == null) {
+            log.warn("库存记录不存在: deviceId={}, productId={}", deviceId, productId);
+            return null;
+        }
+
+        int beforeStock = inventory.getStock();
+        int changeQuantity = newStock - beforeStock;
+        int changeType = changeQuantity >= 0 ? InventoryLog.TYPE_ADJUST_ADD : InventoryLog.TYPE_ADJUST_SUB;
+
+        inventory.setStock(newStock);
+        inventory.setUpdateTime(LocalDateTime.now());
+        updateById(inventory);
+
+        // 记录日志
+        inventoryLogService.logChange(deviceId, productId, inventory.getProductCode(), inventory.getProductName(),
+                changeType, changeQuantity, beforeStock, newStock,
+                null, null, operatorId, operatorName, remark);
+
+        return inventory;
+    }
+
+    @Override
+    public Map<String, Object> getStatistics() {
+        Map<String, Object> stats = new HashMap<>();
+
+        // 总库存记录数
+        stats.put("totalRecords", count());
+
+        // 总库存数量
+        LambdaQueryWrapper<DeviceInventory> wrapper = new LambdaQueryWrapper<>();
+        wrapper.select(DeviceInventory::getStock);
+        List<DeviceInventory> allInventory = list(wrapper);
+        int totalStock = allInventory.stream().mapToInt(i -> i.getStock() != null ? i.getStock() : 0).sum();
+        stats.put("totalStock", totalStock);
+
+        // 低库存数量
+        long lowStockCount = count(new LambdaQueryWrapper<DeviceInventory>()
+                .apply("stock <= warning_threshold"));
+        stats.put("lowStockCount", lowStockCount);
+
+        // 零库存数量
+        long zeroStockCount = count(new LambdaQueryWrapper<DeviceInventory>()
+                .eq(DeviceInventory::getStock, 0));
+        stats.put("zeroStockCount", zeroStockCount);
+
+        return stats;
+    }
+
+    @Override
+    public DeviceInventory getByDeviceAndProduct(String deviceId, Long productId) {
+        return deviceInventoryMapper.selectByDeviceAndProduct(deviceId, productId);
+    }
+}

+ 113 - 0
haha-service/src/main/java/com/haha/service/impl/InventoryLogServiceImpl.java

@@ -0,0 +1,113 @@
+package com.haha.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.haha.entity.InventoryLog;
+import com.haha.mapper.InventoryLogMapper;
+import com.haha.service.InventoryLogService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 库存变动日志服务实现类
+ */
+@Slf4j
+@Service
+public class InventoryLogServiceImpl extends ServiceImpl<InventoryLogMapper, InventoryLog>
+        implements InventoryLogService {
+
+    @Autowired
+    private InventoryLogMapper inventoryLogMapper;
+
+    @Override
+    public IPage<InventoryLog> getPage(int page, int pageSize, Map<String, Object> params) {
+        LambdaQueryWrapper<InventoryLog> wrapper = new LambdaQueryWrapper<>();
+
+        // 设备ID筛选
+        String deviceId = (String) params.get("deviceId");
+        if (StringUtils.hasText(deviceId)) {
+            wrapper.eq(InventoryLog::getDeviceId, deviceId);
+        }
+
+        // 商品编码筛选
+        String productCode = (String) params.get("productCode");
+        if (StringUtils.hasText(productCode)) {
+            wrapper.eq(InventoryLog::getProductCode, productCode);
+        }
+
+        // 变动类型筛选
+        Object changeType = params.get("changeType");
+        if (changeType != null) {
+            wrapper.eq(InventoryLog::getChangeType, changeType);
+        }
+
+        // 订单号筛选
+        String orderId = (String) params.get("orderId");
+        if (StringUtils.hasText(orderId)) {
+            wrapper.eq(InventoryLog::getOrderId, orderId);
+        }
+
+        // 时间范围
+        String startTime = (String) params.get("startTime");
+        String endTime = (String) params.get("endTime");
+        if (StringUtils.hasText(startTime)) {
+            wrapper.ge(InventoryLog::getCreateTime, LocalDateTime.parse(startTime + "T00:00:00"));
+        }
+        if (StringUtils.hasText(endTime)) {
+            wrapper.le(InventoryLog::getCreateTime, LocalDateTime.parse(endTime + "T23:59:59"));
+        }
+
+        // 按创建时间倒序
+        wrapper.orderByDesc(InventoryLog::getCreateTime);
+
+        return page(new Page<>(page, pageSize), wrapper);
+    }
+
+    @Override
+    public InventoryLog logChange(String deviceId, Long productId, String productCode, String productName,
+                                  Integer changeType, Integer changeQuantity,
+                                  Integer beforeStock, Integer afterStock,
+                                  String orderId, String activityId,
+                                  Long operatorId, String operatorName, String remark) {
+        InventoryLog logRecord = new InventoryLog();
+        logRecord.setDeviceId(deviceId);
+        logRecord.setProductId(productId);
+        logRecord.setProductCode(productCode);
+        logRecord.setProductName(productName);
+        logRecord.setChangeType(changeType);
+        logRecord.setChangeQuantity(changeQuantity);
+        logRecord.setBeforeStock(beforeStock);
+        logRecord.setAfterStock(afterStock);
+        logRecord.setOrderId(orderId);
+        logRecord.setActivityId(activityId);
+        logRecord.setOperatorId(operatorId);
+        logRecord.setOperatorName(operatorName);
+        logRecord.setRemark(remark);
+        logRecord.setCreateTime(LocalDateTime.now());
+
+        save(logRecord);
+
+        log.info("库存变动日志: deviceId={}, productCode={}, type={}, quantity={}, before={}, after={}",
+                deviceId, productCode, changeType, changeQuantity, beforeStock, afterStock);
+
+        return logRecord;
+    }
+
+    @Override
+    public List<Map<String, Object>> getChangeStatistics(String deviceId, String startTime, String endTime) {
+        return inventoryLogMapper.selectChangeStatistics(deviceId, startTime, endTime);
+    }
+
+    @Override
+    public List<Map<String, Object>> getByProductCode(String productCode, int limit) {
+        return inventoryLogMapper.selectByProductCode(productCode, limit);
+    }
+}

+ 237 - 0
haha-service/src/main/java/com/haha/service/impl/NewProductApplyServiceImpl.java

@@ -0,0 +1,237 @@
+package com.haha.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.haha.entity.NewProductApply;
+import com.haha.mapper.NewProductApplyMapper;
+import com.haha.sdk.HahaClient;
+import com.haha.sdk.exception.HahaException;
+import com.haha.service.NewProductApplyService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+
+import java.time.LocalDateTime;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 新品申请服务实现类
+ */
+@Slf4j
+@Service
+public class NewProductApplyServiceImpl extends ServiceImpl<NewProductApplyMapper, NewProductApply> implements NewProductApplyService {
+
+    @Autowired
+    private HahaClient hahaClient;
+
+    @Override
+    public IPage<NewProductApply> getPage(int page, int pageSize, Map<String, Object> params) {
+        LambdaQueryWrapper<NewProductApply> wrapper = new LambdaQueryWrapper<>();
+        
+        // 条形码模糊查询
+        Object barcode = params.get("barcode");
+        if (barcode != null && StringUtils.hasText(barcode.toString())) {
+            wrapper.like(NewProductApply::getBarcode, barcode.toString());
+        }
+        
+        // 商品名称模糊查询
+        Object name = params.get("name");
+        if (name != null && StringUtils.hasText(name.toString())) {
+            wrapper.like(NewProductApply::getName, name.toString());
+        }
+        
+        // 状态筛选
+        Object status = params.get("status");
+        if (status != null && StringUtils.hasText(status.toString())) {
+            wrapper.eq(NewProductApply::getStatus, Integer.parseInt(status.toString()));
+        }
+        
+        // 分类筛选
+        Object category = params.get("category");
+        if (category != null && StringUtils.hasText(category.toString())) {
+            wrapper.eq(NewProductApply::getCategory, category.toString());
+        }
+        
+        // 按创建时间倒序
+        wrapper.orderByDesc(NewProductApply::getCreateTime);
+        
+        return page(new Page<>(page, pageSize), wrapper);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Long submitApply(NewProductApply apply, Long applicantId, String applicantName) {
+        // 设置申请人信息
+        apply.setApplicantId(applicantId);
+        apply.setApplicantName(applicantName);
+        apply.setStatus(1); // 审核中
+        apply.setCreateTime(LocalDateTime.now());
+        apply.setSubmitTime(LocalDateTime.now());
+        
+        // 先保存到本地数据库
+        save(apply);
+        
+        try {
+            // 构建哈哈平台API请求参数
+            Map<String, String> params = new HashMap<>();
+            params.put("type", apply.getProductType() != null ? String.valueOf(apply.getProductType()) : "0");
+            params.put("name", apply.getName());
+            params.put("bar_code", apply.getBarcode() != null ? apply.getBarcode() : "");
+            params.put("standard", apply.getStandard() != null ? String.valueOf(apply.getStandard()) : "1");
+            
+            // 设置图片,使用URL方式
+            params.put("img_type", "url");
+            params.put("pic_1", apply.getImageUrl() != null ? apply.getImageUrl() : "");
+            params.put("pic_2", apply.getImageUrl2() != null ? apply.getImageUrl2() : "");
+            params.put("pic_3", apply.getImageUrl3() != null ? apply.getImageUrl3() : "");
+            params.put("pic_4", apply.getImageUrl4() != null ? apply.getImageUrl4() : "");
+            params.put("pic_5", apply.getImageUrl5() != null ? apply.getImageUrl5() : "");
+            params.put("ori_pic_1", apply.getOriginalImageUrl1() != null ? apply.getOriginalImageUrl1() : "");
+            params.put("ori_pic_2", apply.getOriginalImageUrl2() != null ? apply.getOriginalImageUrl2() : "");
+            params.put("ori_pic_3", apply.getOriginalImageUrl3() != null ? apply.getOriginalImageUrl3() : "");
+            params.put("ori_pic_4", apply.getOriginalImageUrl4() != null ? apply.getOriginalImageUrl4() : "");
+            
+            // 规格信息
+            if (StringUtils.hasText(apply.getSpecification())) {
+                params.put("specification", apply.getSpecification());
+            }
+            
+            // 学习机号
+            if (StringUtils.hasText(apply.getStickerNum())) {
+                params.put("sticker_num", apply.getStickerNum());
+            }
+            
+            // 尺寸信息
+            if (apply.getLength() != null) {
+                params.put("length", String.valueOf(apply.getLength()));
+            }
+            if (apply.getWidth() != null) {
+                params.put("width", String.valueOf(apply.getWidth()));
+            }
+            if (apply.getHeight() != null) {
+                params.put("height", String.valueOf(apply.getHeight()));
+            }
+            if (apply.getExhibitHeight() != null) {
+                params.put("exhibit_height", String.valueOf(apply.getExhibitHeight()));
+            }
+            if (apply.getColumns() != null) {
+                params.put("columns", String.valueOf(apply.getColumns()));
+            }
+            if (apply.getNumbers() != null) {
+                params.put("numbers", String.valueOf(apply.getNumbers()));
+            }
+            
+            // 价格信息
+            if (apply.getPrice() != null) {
+                params.put("retail_price", apply.getPrice().toString());
+            }
+            if (apply.getCost() != null) {
+                params.put("cost_price", apply.getCost().toString());
+            }
+            
+            // 备注
+            if (StringUtils.hasText(apply.getReason())) {
+                params.put("remark", apply.getReason());
+            }
+            
+            // 扩展字段
+            if (StringUtils.hasText(apply.getProductExtends())) {
+                params.put("product_extends", apply.getProductExtends());
+            }
+            
+            // 调用哈哈平台新品申请API
+            Map<String, Object> result = hahaClient.getGoodsApi().applyNew(params);
+            
+            // 更新工单编号
+            String hahaApplyId = (String) result.get("id");
+            apply.setHahaApplyId(hahaApplyId);
+            apply.setUpdateTime(LocalDateTime.now());
+            updateById(apply);
+            
+            log.info("新品申请提交成功,本地ID: {}, 哈哈工单ID: {}", apply.getId(), hahaApplyId);
+            
+        } catch (HahaException e) {
+            log.error("新品申请提交到哈哈平台失败: {}", e.getMessage());
+            // 提交失败,更新状态为待提交
+            apply.setStatus(0);
+            apply.setUpdateTime(LocalDateTime.now());
+            updateById(apply);
+            throw new RuntimeException("新品申请提交失败: " + e.getMessage());
+        }
+        
+        return apply.getId();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateByCallback(String hahaApplyId, Integer status, String hahaCode, String rejectReason) {
+        LambdaUpdateWrapper<NewProductApply> wrapper = new LambdaUpdateWrapper<>();
+        wrapper.eq(NewProductApply::getHahaApplyId, hahaApplyId)
+               .set(NewProductApply::getStatus, status)
+               .set(NewProductApply::getAuditTime, LocalDateTime.now())
+               .set(NewProductApply::getUpdateTime, LocalDateTime.now());
+        
+        if (StringUtils.hasText(hahaCode)) {
+            wrapper.set(NewProductApply::getHahaCode, hahaCode);
+        }
+        
+        if (StringUtils.hasText(rejectReason)) {
+            wrapper.set(NewProductApply::getRejectReason, rejectReason);
+        }
+        
+        update(wrapper);
+        log.info("新品申请状态更新,工单ID: {}, 状态: {}", hahaApplyId, status);
+    }
+
+    @Override
+    public Map<String, Object> getStatistics() {
+        Map<String, Object> stats = new HashMap<>();
+        
+        // 总申请数
+        long total = count();
+        stats.put("total", total);
+        
+        // 审核中
+        long pending = count(new LambdaQueryWrapper<NewProductApply>()
+                .eq(NewProductApply::getStatus, 1));
+        stats.put("pending", pending);
+        
+        // 已通过
+        long passed = count(new LambdaQueryWrapper<NewProductApply>()
+                .eq(NewProductApply::getStatus, 2));
+        stats.put("passed", passed);
+        
+        // 已拒绝
+        long rejected = count(new LambdaQueryWrapper<NewProductApply>()
+                .eq(NewProductApply::getStatus, 3));
+        stats.put("rejected", rejected);
+        
+        return stats;
+    }
+
+    @Override
+    public Map<String, Object> checkBarcodeInTotalList(String barcode) {
+        try {
+            // 调用哈哈平台API查询商品总库
+            Map<String, Object> result = hahaClient.getGoodsApi().getTotalList(1, 1, barcode, null, null, null);
+            
+            if (result != null) {
+                List<Map<String, Object>> list = (List<Map<String, Object>>) result.get("list");
+                if (list != null && !list.isEmpty()) {
+                    return list.get(0);
+                }
+            }
+            return null;
+        } catch (HahaException e) {
+            log.error("查询商品总库失败: {}", e.getMessage());
+            return null;
+        }
+    }
+}

+ 191 - 0
haha-service/src/main/java/com/haha/service/impl/OrderInventoryServiceImpl.java

@@ -0,0 +1,191 @@
+package com.haha.service.impl;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.haha.entity.DeviceInventory;
+import com.haha.entity.InventoryLog;
+import com.haha.entity.Order;
+import com.haha.service.DeviceInventoryService;
+import com.haha.service.OrderInventoryService;
+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.util.List;
+import java.util.Map;
+
+/**
+ * 订单库存服务实现类
+ */
+@Slf4j
+@Service
+public class OrderInventoryServiceImpl implements OrderInventoryService {
+
+    @Autowired
+    private DeviceInventoryService deviceInventoryService;
+
+    @Autowired
+    private ObjectMapper objectMapper;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void decreaseStockOnPaid(Order order) {
+        if (order.getItems() == null || order.getItems().isEmpty()) {
+            log.warn("订单商品明细为空,跳过库存扣减: orderNo={}", order.getOrderNo());
+            return;
+        }
+
+        try {
+            // 解析商品明细
+            List<Map<String, Object>> items = objectMapper.readValue(
+                    order.getItems(),
+                    new TypeReference<List<Map<String, Object>>>() {}
+            );
+
+            String deviceId = order.getDeviceId();
+            String orderNo = order.getOrderNo();
+
+            for (Map<String, Object> item : items) {
+                String productCode = (String) item.get("productCode");
+                if (productCode == null) {
+                    productCode = (String) item.get("code");
+                }
+                String productName = (String) item.get("productName");
+                if (productName == null) {
+                    productName = (String) item.get("name");
+                }
+
+                Integer quantity = 1;
+                Object qtyObj = item.get("quantity");
+                if (qtyObj != null) {
+                    quantity = Integer.parseInt(qtyObj.toString());
+                }
+
+                Long productId = null;
+                Object productIdObj = item.get("productId");
+                if (productIdObj != null) {
+                    productId = Long.parseLong(productIdObj.toString());
+                }
+
+                // 扣减库存
+                if (productId != null) {
+                    deviceInventoryService.decreaseStock(
+                            deviceId,
+                            productId,
+                            quantity,
+                            orderNo,
+                            null, // 系统自动操作
+                            "系统"
+                    );
+                    log.info("订单扣减库存: orderNo={}, deviceId={}, productCode={}, quantity={}",
+                            orderNo, deviceId, productCode, quantity);
+                }
+            }
+        } catch (Exception e) {
+            log.error("订单库存扣减失败: orderNo={}", order.getOrderNo(), e);
+            // 不抛出异常,避免影响订单流程
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void restoreStockOnRefund(Order order) {
+        if (order.getItems() == null || order.getItems().isEmpty()) {
+            log.warn("订单商品明细为空,跳过库存恢复: orderNo={}", order.getOrderNo());
+            return;
+        }
+
+        try {
+            // 解析商品明细
+            List<Map<String, Object>> items = objectMapper.readValue(
+                    order.getItems(),
+                    new TypeReference<List<Map<String, Object>>>() {}
+            );
+
+            String deviceId = order.getDeviceId();
+            String orderNo = order.getOrderNo();
+
+            for (Map<String, Object> item : items) {
+                Long productId = null;
+                Object productIdObj = item.get("productId");
+                if (productIdObj != null) {
+                    productId = Long.parseLong(productIdObj.toString());
+                }
+
+                String productCode = (String) item.get("productCode");
+                if (productCode == null) {
+                    productCode = (String) item.get("code");
+                }
+                String productName = (String) item.get("productName");
+                if (productName == null) {
+                    productName = (String) item.get("name");
+                }
+
+                Integer quantity = 1;
+                Object qtyObj = item.get("quantity");
+                if (qtyObj != null) {
+                    quantity = Integer.parseInt(qtyObj.toString());
+                }
+
+                // 恢复库存(使用调整方式)
+                if (productId != null) {
+                    DeviceInventory inventory = deviceInventoryService.getByDeviceAndProduct(deviceId, productId);
+                    if (inventory != null) {
+                        deviceInventoryService.adjustStock(
+                                deviceId,
+                                productId,
+                                inventory.getStock() + quantity,
+                                "订单退款恢复库存: " + orderNo,
+                                null,
+                                "系统"
+                        );
+                        log.info("退款恢复库存: orderNo={}, deviceId={}, productCode={}, quantity={}",
+                                orderNo, deviceId, productCode, quantity);
+                    }
+                }
+            }
+        } catch (Exception e) {
+            log.error("订单库存恢复失败: orderNo={}", order.getOrderNo(), e);
+        }
+    }
+
+    @Override
+    public boolean checkStockAvailable(String deviceId, String items) {
+        if (items == null || items.isEmpty()) {
+            return true;
+        }
+
+        try {
+            List<Map<String, Object>> itemsList = objectMapper.readValue(
+                    items,
+                    new TypeReference<List<Map<String, Object>>>() {}
+            );
+
+            for (Map<String, Object> item : itemsList) {
+                Long productId = null;
+                Object productIdObj = item.get("productId");
+                if (productIdObj != null) {
+                    productId = Long.parseLong(productIdObj.toString());
+                }
+
+                Integer quantity = 1;
+                Object qtyObj = item.get("quantity");
+                if (qtyObj != null) {
+                    quantity = Integer.parseInt(qtyObj.toString());
+                }
+
+                if (productId != null) {
+                    DeviceInventory inventory = deviceInventoryService.getByDeviceAndProduct(deviceId, productId);
+                    if (inventory == null || inventory.getStock() < quantity) {
+                        return false;
+                    }
+                }
+            }
+            return true;
+        } catch (Exception e) {
+            log.error("检查库存可用性失败", e);
+            return true; // 解析失败默认可用,避免影响正常流程
+        }
+    }
+}

+ 157 - 0
haha-service/src/main/java/com/haha/service/impl/StockRecordServiceImpl.java

@@ -0,0 +1,157 @@
+package com.haha.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.haha.entity.StockRecord;
+import com.haha.mapper.StockRecordMapper;
+import com.haha.service.StockRecordService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+
+import java.time.LocalDateTime;
+import java.util.Map;
+
+/**
+ * 上货记录服务实现类
+ */
+@Slf4j
+@Service
+public class StockRecordServiceImpl extends ServiceImpl<StockRecordMapper, StockRecord>
+        implements StockRecordService {
+
+    @Autowired
+    private StockRecordMapper stockRecordMapper;
+
+    @Override
+    public IPage<StockRecord> getPage(int page, int pageSize, Map<String, Object> params) {
+        LambdaQueryWrapper<StockRecord> wrapper = new LambdaQueryWrapper<>();
+
+        // 设备ID筛选
+        String deviceId = (String) params.get("deviceId");
+        if (StringUtils.hasText(deviceId)) {
+            wrapper.eq(StockRecord::getDeviceId, deviceId);
+        }
+
+        // 上货员筛选
+        Object stockerId = params.get("stockerId");
+        if (stockerId != null) {
+            wrapper.eq(StockRecord::getStockerId, stockerId);
+        }
+
+        // 类型筛选
+        Object stockType = params.get("stockType");
+        if (stockType != null) {
+            wrapper.eq(StockRecord::getStockType, stockType);
+        }
+
+        // 状态筛选
+        Object status = params.get("status");
+        if (status != null) {
+            wrapper.eq(StockRecord::getStatus, status);
+        }
+
+        // 时间范围
+        String startTime = (String) params.get("startTime");
+        String endTime = (String) params.get("endTime");
+        if (StringUtils.hasText(startTime)) {
+            wrapper.ge(StockRecord::getCreateTime, LocalDateTime.parse(startTime + "T00:00:00"));
+        }
+        if (StringUtils.hasText(endTime)) {
+            wrapper.le(StockRecord::getCreateTime, LocalDateTime.parse(endTime + "T23:59:59"));
+        }
+
+        // 按创建时间倒序
+        wrapper.orderByDesc(StockRecord::getCreateTime);
+
+        return page(new Page<>(page, pageSize), wrapper);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public StockRecord createRecord(String deviceId, Integer stockType, Long stockerId,
+                                    String stockerName, String stockerPhone, String remark) {
+        StockRecord record = new StockRecord();
+        record.setDeviceId(deviceId);
+        record.setStockType(stockType);
+        record.setStockerId(stockerId);
+        record.setStockerName(stockerName);
+        record.setStockerPhone(stockerPhone);
+        record.setStatus(1); // 进行中
+        record.setTotalItems(0);
+        record.setTotalQuantity(0);
+        record.setRemark(remark);
+        record.setCreateTime(LocalDateTime.now());
+        record.setUpdateTime(LocalDateTime.now());
+
+        save(record);
+
+        log.info("创建上货记录: id={}, deviceId={}, stockerName={}", record.getId(), deviceId, stockerName);
+
+        return record;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void startActivity(Long recordId) {
+        StockRecord record = getById(recordId);
+        if (record == null) {
+            throw new RuntimeException("上货记录不存在");
+        }
+
+        record.setStatus(1); // 进行中
+        record.setStartTime(LocalDateTime.now());
+        record.setUpdateTime(LocalDateTime.now());
+        updateById(record);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void completeActivity(Long recordId, Integer totalItems, Integer totalQuantity, String activityId) {
+        StockRecord record = getById(recordId);
+        if (record == null) {
+            throw new RuntimeException("上货记录不存在");
+        }
+
+        record.setStatus(2); // 已完成
+        record.setActivityId(activityId);
+        record.setTotalItems(totalItems);
+        record.setTotalQuantity(totalQuantity);
+        record.setEndTime(LocalDateTime.now());
+        record.setUpdateTime(LocalDateTime.now());
+        updateById(record);
+
+        log.info("完成上货记录: id={}, totalItems={}, totalQuantity={}", recordId, totalItems, totalQuantity);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void cancelActivity(Long recordId) {
+        StockRecord record = getById(recordId);
+        if (record == null) {
+            throw new RuntimeException("上货记录不存在");
+        }
+
+        record.setStatus(3); // 已取消
+        record.setEndTime(LocalDateTime.now());
+        record.setUpdateTime(LocalDateTime.now());
+        updateById(record);
+
+        log.info("取消上货记录: id={}", recordId);
+    }
+
+    @Override
+    public Map<String, Object> getStockerStatistics(Long stockerId, String startTime, String endTime) {
+        return stockRecordMapper.selectStockerStatistics(stockerId, startTime, endTime);
+    }
+
+    @Override
+    public Map<String, Object> getDetailWithDeviceName(Long id) {
+        return stockRecordMapper.selectWithDeviceName(id);
+    }
+}