Ver Fonte

商品同步,商品列表,设备列表

skyline há 3 meses atrás
pai
commit
92ec313f1d

+ 33 - 0
haha-admin/src/main/java/com/haha/admin/controller/DataSyncController.java

@@ -54,6 +54,39 @@ public class DataSyncController {
         }
     }
 
+    /**
+     * 同步商品数据
+     * 从哈哈平台商家商品库拉取商品列表并存储到本地数据库
+     */
+    @PostMapping("/products")
+    public Result<SyncRecord> syncProducts() {
+        try {
+            // 获取当前登录用户信息
+            Long operatorId = StpUtil.getLoginIdAsLong();
+            String operatorName = StpUtil.getLoginIdAsString();
+            
+            // 尝试从Session中获取用户名
+            try {
+                Object nickname = StpUtil.getSession().get("nickname");
+                if (nickname != null) {
+                    operatorName = nickname.toString();
+                }
+            } catch (Exception e) {
+                log.debug("获取用户昵称失败,使用默认值");
+            }
+
+            SyncRecord record = dataSyncService.syncProducts(operatorId, operatorName);
+            
+            if (record.getStatus() == 3) {
+                return Result.error("同步失败: " + record.getErrorMessage());
+            }
+            return Result.success(record);
+        } catch (Exception e) {
+            log.error("同步商品数据失败", e);
+            return Result.error("同步失败: " + e.getMessage());
+        }
+    }
+
     /**
      * 获取同步记录列表
      */

+ 296 - 0
haha-admin/src/main/java/com/haha/admin/controller/DeviceController.java

@@ -0,0 +1,296 @@
+package com.haha.admin.controller;
+
+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.haha.common.vo.Result;
+import com.haha.entity.Device;
+import com.haha.service.DeviceService;
+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("/devices")
+public class DeviceController {
+
+    @Autowired
+    private DeviceService deviceService;
+
+    /**
+     * 分页查询设备列表
+     * @param params 查询参数
+     * @return 设备列表
+     */
+    @GetMapping("/list")
+    public Result<Map<String, Object>> list(@RequestParam Map<String, Object> params) {
+        try {
+            // 分页参数
+            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());
+            }
+
+            // 查询条件
+            String deviceId = (String) params.get("deviceId");
+            String storeName = (String) params.get("storeName");
+            Integer status = null;
+            Object statusObj = params.get("status");
+            if (statusObj != null && !statusObj.toString().isEmpty()) {
+                status = Integer.valueOf(statusObj.toString());
+            }
+
+            // 构建查询条件
+            LambdaQueryWrapper<Device> wrapper = new LambdaQueryWrapper<>();
+            wrapper.like(deviceId != null && !deviceId.isEmpty(), Device::getDeviceId, deviceId)
+                   .eq(status != null, Device::getStatus, status);
+            
+            // 按创建时间倒序
+            wrapper.orderByDesc(Device::getCreateTime);
+
+            // 分页查询
+            Page<Device> pageResult = new Page<>(page, pageSize);
+            IPage<Device> devicePage = deviceService.page(pageResult, wrapper);
+
+            // 填充额外字段
+            for (Device device : devicePage.getRecords()) {
+                fillDeviceLabels(device);
+            }
+
+            Map<String, Object> data = new HashMap<>();
+            data.put("list", devicePage.getRecords());
+            data.put("total", devicePage.getTotal());
+            data.put("pageSize", devicePage.getSize());
+            data.put("currentPage", devicePage.getCurrent());
+
+            return Result.success("查询成功", data);
+
+        } catch (Exception e) {
+            log.error("查询设备列表失败: {}", e.getMessage(), e);
+            return Result.error(500, "查询设备列表失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 获取设备详情
+     * @param id 设备ID
+     * @return 设备详情
+     */
+    @GetMapping("/{id}")
+    public Result<Device> getById(@PathVariable Long id) {
+        try {
+            Device device = deviceService.getById(id);
+            if (device == null) {
+                return Result.error(404, "设备不存在");
+            }
+            fillDeviceLabels(device);
+            return Result.success("查询成功", device);
+        } catch (Exception e) {
+            log.error("查询设备详情失败: deviceId={}, error={}", id, e.getMessage(), e);
+            return Result.error(500, "查询设备详情失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 获取设备统计数据
+     * @return 统计数据
+     */
+    @GetMapping("/statistics")
+    public Result<Map<String, Object>> getStatistics() {
+        try {
+            Map<String, Object> statistics = new HashMap<>();
+
+            // 总设备数
+            long total = deviceService.count();
+            statistics.put("total", total);
+
+            // 在线设备数 (status = 1 为正常/在线)
+            long online = deviceService.lambdaQuery()
+                    .eq(Device::getStatus, 1)
+                    .count();
+            statistics.put("online", online);
+
+            // 离线设备数
+            long offline = total - online;
+            statistics.put("offline", offline);
+
+            // TODO: 后续从订单服务获取销售额数据
+            statistics.put("totalSales", "0.00");
+            statistics.put("todaySales", "0.00");
+            statistics.put("avgTemperature", "0.0");
+
+            return Result.success("查询成功", statistics);
+
+        } catch (Exception e) {
+            log.error("查询设备统计失败: {}", e.getMessage(), e);
+            return Result.error(500, "查询设备统计失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 远程开门
+     * @param id 设备ID
+     * @param params 开门参数
+     * @return 操作结果
+     */
+    @PostMapping("/{id}/open")
+    public Result<Void> openDoor(@PathVariable Long id, @RequestBody Map<String, Object> params) {
+        try {
+            Device device = deviceService.getById(id);
+            if (device == null) {
+                return Result.error(404, "设备不存在");
+            }
+
+            String deviceId = device.getDeviceId();
+            String doorIndex = (String) params.getOrDefault("doorIndex", "A");
+            
+            log.info("远程开门: deviceId={}, doorIndex={}", deviceId, doorIndex);
+            
+            // TODO: 调用SDK发送开门指令
+            // hahaClient.getDeviceApi().openDoor(deviceId, ...);
+            
+            return Result.success("开门指令已发送", null);
+
+        } catch (Exception e) {
+            log.error("远程开门失败: deviceId={}, error={}", id, e.getMessage(), e);
+            return Result.error(500, "远程开门失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 设置温度
+     * @param id 设备ID
+     * @param params 温度参数
+     * @return 操作结果
+     */
+    @PutMapping("/{id}/temperature")
+    public Result<Void> setTemperature(@PathVariable Long id, @RequestBody Map<String, Object> params) {
+        try {
+            Device device = deviceService.getById(id);
+            if (device == null) {
+                return Result.error(404, "设备不存在");
+            }
+
+            Double temperature = Double.valueOf(params.get("temperature").toString());
+            log.info("设置温度: deviceId={}, temperature={}", device.getDeviceId(), temperature);
+            
+            // TODO: 调用SDK设置温度
+            
+            return Result.success("温度设置成功", null);
+
+        } catch (Exception e) {
+            log.error("设置温度失败: deviceId={}, error={}", id, e.getMessage(), e);
+            return Result.error(500, "设置温度失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 设置音量
+     * @param id 设备ID
+     * @param params 音量参数
+     * @return 操作结果
+     */
+    @PutMapping("/{id}/volume")
+    public Result<Void> setVolume(@PathVariable Long id, @RequestBody Map<String, Object> params) {
+        try {
+            Device device = deviceService.getById(id);
+            if (device == null) {
+                return Result.error(404, "设备不存在");
+            }
+
+            Integer volume = Integer.valueOf(params.get("volume").toString());
+            log.info("设置音量: deviceId={}, volume={}", device.getDeviceId(), volume);
+            
+            // TODO: 调用SDK设置音量
+            
+            return Result.success("音量设置成功", null);
+
+        } catch (Exception e) {
+            log.error("设置音量失败: deviceId={}, error={}", id, e.getMessage(), e);
+            return Result.error(500, "设置音量失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 填充设备标签字段
+     * @param device 设备对象
+     */
+    private void fillDeviceLabels(Device device) {
+        // 设备状态标签
+        if (device.getStatus() != null) {
+            switch (device.getStatus()) {
+                case 1:
+                    device.setStatusLabel("正常");
+                    device.setStatusColor("success");
+                    device.setIsOnline(true);
+                    break;
+                case 2:
+                    device.setStatusLabel("冻结");
+                    device.setStatusColor("warning");
+                    device.setIsOnline(false);
+                    break;
+                case 3:
+                    device.setStatusLabel("离线");
+                    device.setStatusColor("info");
+                    device.setIsOnline(false);
+                    break;
+                case 4:
+                    device.setStatusLabel("维护中");
+                    device.setStatusColor("danger");
+                    device.setIsOnline(false);
+                    break;
+                default:
+                    device.setStatusLabel("未知");
+                    device.setStatusColor("info");
+                    device.setIsOnline(false);
+            }
+        }
+        
+        // 默认值
+        if (device.getShopName() == null) {
+            device.setShopName("默认门店");
+        }
+        if (device.getAddress() == null) {
+            device.setAddress("-");
+        }
+        if (device.getDeviceTypeLabel() == null) {
+            device.setDeviceTypeLabel("智能柜");
+        }
+        if (device.getTemperature() == null) {
+            device.setTemperature("0.0");
+        }
+        if (device.getVolume() == null) {
+            device.setVolume(8);
+        }
+        if (device.getDoorCount() == null) {
+            device.setDoorCount(1);
+        }
+        if (device.getLayerCount() == null) {
+            device.setLayerCount(5);
+        }
+        if (device.getTotalSales() == null) {
+            device.setTotalSales("0.00");
+        }
+        if (device.getTodaySales() == null) {
+            device.setTodaySales("0.00");
+        }
+        if (device.getOrderCount() == null) {
+            device.setOrderCount(0);
+        }
+    }
+}

+ 306 - 0
haha-admin/src/main/java/com/haha/admin/controller/ProductController.java

@@ -0,0 +1,306 @@
+package com.haha.admin.controller;
+
+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.haha.common.vo.Result;
+import com.haha.entity.Product;
+import com.haha.service.ProductService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.time.LocalDateTime;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 商品管理控制器
+ */
+@Slf4j
+@RestController
+@RequestMapping("/products")
+public class ProductController {
+
+    @Autowired
+    private ProductService productService;
+
+    /**
+     * 分页查询商品列表
+     * @param params 查询参数
+     * @return 商品列表
+     */
+    @GetMapping("/list")
+    public Result<Map<String, Object>> list(@RequestParam Map<String, Object> params) {
+        try {
+            // 分页参数
+            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());
+            }
+
+            // 查询条件
+            String name = (String) params.get("name");
+            String barcode = (String) params.get("barcode");
+            String category = (String) params.get("category");
+            Integer syncStatus = null;
+            Object syncStatusObj = params.get("syncStatus");
+            if (syncStatusObj != null && !syncStatusObj.toString().isEmpty()) {
+                syncStatus = Integer.valueOf(syncStatusObj.toString());
+            }
+
+            // 构建查询条件
+            LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<>();
+            wrapper.like(name != null && !name.isEmpty(), Product::getName, name)
+                   .like(barcode != null && !barcode.isEmpty(), Product::getBarcode, barcode)
+                   .eq(category != null && !category.isEmpty(), Product::getType, category)
+                   .eq(syncStatus != null, Product::getSyncStatus, syncStatus)
+                   .eq(Product::getIsDeleted, 0);  // 只查询未删除的
+            
+            // 按创建时间倒序
+            wrapper.orderByDesc(Product::getCreateTime);
+
+            // 分页查询
+            Page<Product> pageResult = new Page<>(page, pageSize);
+            IPage<Product> productPage = productService.page(pageResult, wrapper);
+
+            // 填充额外字段
+            for (Product product : productPage.getRecords()) {
+                fillProductLabels(product);
+            }
+
+            Map<String, Object> data = new HashMap<>();
+            data.put("list", productPage.getRecords());
+            data.put("total", productPage.getTotal());
+            data.put("pageSize", productPage.getSize());
+            data.put("currentPage", productPage.getCurrent());
+
+            return Result.success("查询成功", data);
+
+        } catch (Exception e) {
+            log.error("查询商品列表失败: {}", e.getMessage(), e);
+            return Result.error(500, "查询商品列表失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 获取商品详情
+     * @param id 商品ID
+     * @return 商品详情
+     */
+    @GetMapping("/{id}")
+    public Result<Product> getById(@PathVariable Long id) {
+        try {
+            Product product = productService.getById(id);
+            if (product == null) {
+                return Result.error(404, "商品不存在");
+            }
+            fillProductLabels(product);
+            return Result.success("查询成功", product);
+        } catch (Exception e) {
+            log.error("查询商品详情失败: productId={}, error={}", id, e.getMessage(), e);
+            return Result.error(500, "查询商品详情失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 获取商品统计数据
+     * @return 统计数据
+     */
+    @GetMapping("/statistics")
+    public Result<Map<String, Object>> getStatistics() {
+        try {
+            Map<String, Object> statistics = new HashMap<>();
+
+            // 总商品数(未删除)
+            long totalProducts = productService.lambdaQuery()
+                    .eq(Product::getIsDeleted, 0)
+                    .count();
+            statistics.put("totalProducts", totalProducts);
+
+            // 已同步商品数
+            long syncedProducts = productService.lambdaQuery()
+                    .eq(Product::getIsDeleted, 0)
+                    .eq(Product::getSyncStatus, 2)
+                    .count();
+            statistics.put("syncedProducts", syncedProducts);
+
+            // TODO: 库存和库存总值需要从其他服务获取
+            statistics.put("totalStock", 0);
+            statistics.put("totalValue", "0.00");
+
+            return Result.success("查询成功", statistics);
+
+        } catch (Exception e) {
+            log.error("查询商品统计失败: {}", e.getMessage(), e);
+            return Result.error(500, "查询商品统计失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 添加商品
+     * @param product 商品信息
+     * @return 添加结果
+     */
+    @PostMapping
+    public Result<Product> add(@RequestBody Product product) {
+        try {
+            product.setIsDeleted(0);
+            product.setSyncStatus(0);
+            product.setCreateTime(LocalDateTime.now());
+            product.setUpdateTime(LocalDateTime.now());
+            
+            boolean success = productService.save(product);
+            if (success) {
+                return Result.success("添加成功", product);
+            } else {
+                return Result.error(500, "添加失败");
+            }
+        } catch (Exception e) {
+            log.error("添加商品失败: {}", e.getMessage(), e);
+            return Result.error(500, "添加商品失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 编辑商品
+     * @param id 商品ID
+     * @param product 商品信息
+     * @return 编辑结果
+     */
+    @PutMapping("/{id}")
+    public Result<Product> update(@PathVariable Long id, @RequestBody Product product) {
+        try {
+            Product existing = productService.getById(id);
+            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);
+            } else {
+                return Result.error(500, "更新失败");
+            }
+        } catch (Exception e) {
+            log.error("更新商品失败: productId={}, error={}", id, e.getMessage(), e);
+            return Result.error(500, "更新商品失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 删除商品(软删除)
+     * @param id 商品ID
+     * @return 删除结果
+     */
+    @DeleteMapping("/{id}")
+    public Result<Void> delete(@PathVariable Long id) {
+        try {
+            Product product = productService.getById(id);
+            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);
+            return Result.error(500, "删除商品失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 同步商品到哈哈平台
+     * @param id 商品ID
+     * @return 同步结果
+     */
+    @PostMapping("/{id}/sync")
+    public Result<Void> sync(@PathVariable Long id) {
+        try {
+            Product product = productService.getById(id);
+            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);
+            return Result.error(500, "同步商品失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 填充商品标签字段
+     * @param product 商品对象
+     */
+    private void fillProductLabels(Product product) {
+        // 同步状态标签
+        if (product.getSyncStatus() != null) {
+            switch (product.getSyncStatus()) {
+                case 0:
+                    product.setSyncStatusLabel("未同步");
+                    product.setSyncStatusColor("info");
+                    break;
+                case 1:
+                    product.setSyncStatusLabel("同步中");
+                    product.setSyncStatusColor("warning");
+                    break;
+                case 2:
+                    product.setSyncStatusLabel("已同步");
+                    product.setSyncStatusColor("success");
+                    break;
+                case 3:
+                    product.setSyncStatusLabel("同步失败");
+                    product.setSyncStatusColor("danger");
+                    break;
+                default:
+                    product.setSyncStatusLabel("未知");
+                    product.setSyncStatusColor("info");
+            }
+        }
+        
+        // 图片URL
+        if (product.getImageUrl() == null && product.getPic() != null) {
+            product.setImageUrl(product.getPic());
+        }
+        
+        // 默认值
+        if (product.getCategory() == null) {
+            product.setCategory(product.getType());
+        }
+        if (product.getPrice() == null && product.getRetailPrice() != null) {
+            product.setPrice(product.getRetailPrice());
+        }
+        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);
+        }
+    }
+}

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

@@ -1,6 +1,7 @@
 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;
@@ -30,4 +31,51 @@ public class Device implements Serializable {
     private LocalDateTime createTime;
 
     private LocalDateTime updateTime;
+
+    // ========== 以下为非数据库字段,用于前端展示 ==========
+
+    @TableField(exist = false)
+    private String shopName;
+
+    @TableField(exist = false)
+    private String address;
+
+    @TableField(exist = false)
+    private String deviceTypeLabel;
+
+    @TableField(exist = false)
+    private String statusLabel;
+
+    @TableField(exist = false)
+    private String statusColor;
+
+    @TableField(exist = false)
+    private Boolean isOnline;
+
+    @TableField(exist = false)
+    private String temperature;
+
+    @TableField(exist = false)
+    private Integer volume;
+
+    @TableField(exist = false)
+    private Integer doorCount;
+
+    @TableField(exist = false)
+    private Integer layerCount;
+
+    @TableField(exist = false)
+    private Boolean hasScreen;
+
+    @TableField(exist = false)
+    private String totalSales;
+
+    @TableField(exist = false)
+    private String todaySales;
+
+    @TableField(exist = false)
+    private Integer orderCount;
+
+    @TableField(exist = false)
+    private LocalDateTime lastOnlineTime;
 }

+ 123 - 4
haha-entity/src/main/java/com/haha/entity/Product.java

@@ -1,33 +1,152 @@
 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_product")
 public class Product implements Serializable {
     private static final long serialVersionUID = 1L;
 
+    /**
+     * 主键ID
+     */
     @TableId(type = IdType.AUTO)
     private Long id;
 
+    /**
+     * 哈哈平台商品ID
+     */
+    private String productId;
+
+    /**
+     * 商品编码(哈哈平台code)
+     */
+    private String code;
+
+    /**
+     * 商品条码
+     */
     private String barcode;
 
+    /**
+     * 商品名称
+     */
     private String name;
 
-    private Double price;
+    /**
+     * 商品分类(如:饮料、零食等)
+     */
+    private String type;
 
-    private String imageUrl;
+    /**
+     * 商品图片URL
+     */
+    private String pic;
 
-    private Integer hahaSyncStatus;
+    /**
+     * 商品陈列图URL
+     */
+    private String planogram;
 
-    private String hahaModelId;
+    /**
+     * 适用设备:全部柜、静态柜、动态柜
+     */
+    private String applyStatus;
+
+    /**
+     * 成本价
+     */
+    private Double costPrice;
+
+    /**
+     * 零售价(门店统一售价)
+     */
+    private Double retailPrice;
+
+    /**
+     * 商家自定义商品ID
+     */
+    private String customerId;
 
+    /**
+     * 同步状态:0-未同步,1-同步中,2-已同步,3-同步失败
+     */
+    private Integer syncStatus;
+
+    /**
+     * 最后同步时间
+     */
+    private LocalDateTime syncTime;
+
+    /**
+     * 是否删除:0-正常,1-已删除
+     */
     private Integer isDeleted;
 
+    /**
+     * 创建时间
+     */
     private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    private LocalDateTime updateTime;
+
+    // ========== 以下为非数据库字段,用于前端展示 ==========
+
+    @TableField(exist = false)
+    private String category;
+
+    @TableField(exist = false)
+    private String imageUrl;
+
+    @TableField(exist = false)
+    private Double price;
+
+    @TableField(exist = false)
+    private Double cost;
+
+    @TableField(exist = false)
+    private Integer stock;
+
+    @TableField(exist = false)
+    private Integer soldCount;
+
+    @TableField(exist = false)
+    private String syncStatusLabel;
+
+    @TableField(exist = false)
+    private String syncStatusColor;
+
+    @TableField(exist = false)
+    private String hahaModelId;
+
+    @TableField(exist = false)
+    private String specification;
+
+    @TableField(exist = false)
+    private String supplier;
+
+    @TableField(exist = false)
+    private String shelfLife;
+
+    @TableField(exist = false)
+    private String storageConditions;
+
+    @TableField(exist = false)
+    private String description;
+
+    @TableField(exist = false)
+    private LocalDateTime lastSyncTime;
 }

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

@@ -22,6 +22,16 @@ public interface DataSyncService extends IService<SyncRecord> {
      */
     SyncRecord syncDevices(Long operatorId, String operatorName);
 
+    /**
+     * 同步商品列表
+     * 从哈哈平台商家商品库拉取商品数据并存储到本地数据库
+     * 
+     * @param operatorId 操作人ID
+     * @param operatorName 操作人名称
+     * @return 同步记录
+     */
+    SyncRecord syncProducts(Long operatorId, String operatorName);
+
     /**
      * 获取同步记录列表
      * 

+ 25 - 1
haha-service/src/main/java/com/haha/service/ProductService.java

@@ -5,10 +5,34 @@ import com.haha.entity.Product;
 import java.util.List;
 
 public interface ProductService extends IService<Product> {
+    
+    /**
+     * 根据设备ID获取商品列表
+     */
     List<Product> getProductListBydeviceId(String deviceId);
+    
+    /**
+     * 更新商品库存
+     */
     boolean updateProductStock(String deviceId, Long productId, Integer stock);
+    
+    /**
+     * 根据分类ID获取商品列表
+     */
     List<Product> getProductListByCategory(Integer categoryId);
+    
+    /**
+     * 根据条码获取商品
+     */
     Product getProductByBarcode(String barcode);
+    
+    /**
+     * 根据商品编码获取商品
+     */
+    Product getProductByCode(String code);
+    
+    /**
+     * 更新同步状态
+     */
     boolean updateSyncStatus(Long productId, Integer syncStatus);
-    boolean updateHahaModelId(Long productId, String modelId);
 }

+ 184 - 0
haha-service/src/main/java/com/haha/service/impl/DataSyncServiceImpl.java

@@ -6,6 +6,7 @@ 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.Device;
+import com.haha.entity.Product;
 import com.haha.entity.SyncLog;
 import com.haha.entity.SyncRecord;
 import com.haha.mapper.SyncLogMapper;
@@ -15,6 +16,7 @@ import com.haha.sdk.exception.HahaException;
 import com.haha.sdk.model.DeviceInfo;
 import com.haha.service.DataSyncService;
 import com.haha.service.DeviceService;
+import com.haha.service.ProductService;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
@@ -38,6 +40,9 @@ public class DataSyncServiceImpl extends ServiceImpl<SyncRecordMapper, SyncRecor
     @Autowired
     private DeviceService deviceService;
 
+    @Autowired
+    private ProductService productService;
+
     @Autowired
     private SyncLogMapper syncLogMapper;
 
@@ -53,6 +58,7 @@ public class DataSyncServiceImpl extends ServiceImpl<SyncRecordMapper, SyncRecor
      * 同步类型
      */
     private static final String SYNC_TYPE_DEVICE = "DEVICE";
+    private static final String SYNC_TYPE_PRODUCT = "PRODUCT";
 
     @Override
     @Transactional(rollbackFor = Exception.class)
@@ -248,4 +254,182 @@ public class DataSyncServiceImpl extends ServiceImpl<SyncRecordMapper, SyncRecor
 
         return stats;
     }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public SyncRecord syncProducts(Long operatorId, String operatorName) {
+        log.info("开始同步商品数据,操作人: {}", operatorName);
+
+        // 1. 创建同步记录
+        SyncRecord record = new SyncRecord();
+        record.setSyncType(SYNC_TYPE_PRODUCT);
+        record.setStatus(STATUS_SYNCING);
+        record.setStartTime(LocalDateTime.now());
+        record.setOperatorId(operatorId);
+        record.setOperatorName(operatorName);
+        record.setCreateTime(LocalDateTime.now());
+        record.setTotalCount(0);
+        record.setSuccessCount(0);
+        record.setFailCount(0);
+        save(record);
+
+        int successCount = 0;
+        int failCount = 0;
+        int totalCount = 0;
+        StringBuilder errorMsg = new StringBuilder();
+
+        try {
+            // 2. 分页从哈哈平台商家商品库获取商品列表
+            int pageNo = 1;
+            int pageSize = 50;
+            boolean hasMore = true;
+
+            while (hasMore) {
+                // 调用哈哈平台API获取商家商品库列表
+                Map<String, Object> result = hahaClient.getGoodsApi().getMerchantList(pageNo, pageSize, null, null, null, null);
+                
+                if (result == null || !result.containsKey("listData")) {
+                    log.warn("获取商品列表返回数据为空");
+                    break;
+                }
+
+                List<Map<String, Object>> listData = (List<Map<String, Object>>) result.get("listData");
+                if (listData == null || listData.isEmpty()) {
+                    hasMore = false;
+                    break;
+                }
+
+                // 3. 遍历商品列表,同步到本地数据库
+                for (Map<String, Object> productData : listData) {
+                    totalCount++;
+                    try {
+                        syncSingleProduct(record.getId(), productData);
+                        successCount++;
+                    } catch (Exception e) {
+                        failCount++;
+                        String code = productData.get("code") != null ? productData.get("code").toString() : "unknown";
+                        String name = productData.get("name") != null ? productData.get("name").toString() : "unknown";
+                        String errMsg = String.format("商品 %s(%s) 同步失败: %s", code, name, e.getMessage());
+                        errorMsg.append(errMsg).append("; ");
+                        log.error(errMsg, e);
+
+                        // 记录失败日志
+                        saveSyncLog(record.getId(), SYNC_TYPE_PRODUCT, code, name, 0, errMsg, JSON.toJSONString(productData));
+                    }
+                }
+
+                // 检查是否还有更多数据
+                Object totals = result.get("totals");
+                int total = totals != null ? Integer.parseInt(totals.toString()) : 0;
+                if (pageNo * pageSize >= total) {
+                    hasMore = false;
+                } else {
+                    pageNo++;
+                }
+            }
+
+            // 4. 更新同步记录
+            record.setTotalCount(totalCount);
+            record.setSuccessCount(successCount);
+            record.setFailCount(failCount);
+            record.setStatus(failCount == 0 ? STATUS_SUCCESS : (successCount > 0 ? STATUS_SUCCESS : STATUS_FAILED));
+            record.setEndTime(LocalDateTime.now());
+            record.setDuration(java.time.Duration.between(record.getStartTime(), record.getEndTime()).toMillis());
+            if (errorMsg.length() > 0) {
+                record.setErrorMessage(errorMsg.toString());
+            }
+            updateById(record);
+
+            log.info("商品数据同步完成,总数: {}, 成功: {}, 失败: {}", totalCount, successCount, failCount);
+
+        } catch (HahaException e) {
+            log.error("调用哈哈平台API失败", e);
+            record.setStatus(STATUS_FAILED);
+            record.setEndTime(LocalDateTime.now());
+            record.setDuration(java.time.Duration.between(record.getStartTime(), record.getEndTime()).toMillis());
+            record.setErrorMessage("调用哈哈平台API失败: " + e.getMessage());
+            updateById(record);
+        } catch (Exception e) {
+            log.error("商品数据同步异常", e);
+            record.setStatus(STATUS_FAILED);
+            record.setEndTime(LocalDateTime.now());
+            record.setDuration(java.time.Duration.between(record.getStartTime(), record.getEndTime()).toMillis());
+            record.setErrorMessage("同步异常: " + e.getMessage());
+            updateById(record);
+        }
+
+        return record;
+    }
+
+    /**
+     * 同步单个商品
+     */
+    private void syncSingleProduct(Long recordId, Map<String, Object> productData) {
+        String code = productData.get("code") != null ? productData.get("code").toString() : null;
+        String name = productData.get("name") != null ? productData.get("name").toString() : "";
+        
+        log.debug("同步商品: {} - {}", code, name);
+
+        if (code == null || code.isEmpty()) {
+            throw new RuntimeException("商品编码为空");
+        }
+
+        // 查询本地是否已存在该商品
+        Product existProduct = productService.getProductByCode(code);
+
+        if (existProduct != null) {
+            // 更新商品信息
+            existProduct.setName(name);
+            existProduct.setBarcode(productData.get("bar_code") != null ? productData.get("bar_code").toString() : null);
+            existProduct.setType(productData.get("type") != null ? productData.get("type").toString() : null);
+            existProduct.setPic(productData.get("pic") != null ? productData.get("pic").toString() : null);
+            existProduct.setPlanogram(productData.get("planogram") != null ? productData.get("planogram").toString() : null);
+            existProduct.setApplyStatus(productData.get("apply_status") != null ? productData.get("apply_status").toString() : null);
+            existProduct.setCostPrice(parseDouble(productData.get("cost_price")));
+            existProduct.setRetailPrice(parseDouble(productData.get("cprice")));
+            existProduct.setCustomerId(productData.get("customer_id") != null ? productData.get("customer_id").toString() : null);
+            existProduct.setSyncStatus(STATUS_SUCCESS);
+            existProduct.setSyncTime(LocalDateTime.now());
+            existProduct.setUpdateTime(LocalDateTime.now());
+            productService.updateById(existProduct);
+
+            // 记录成功日志
+            saveSyncLog(recordId, SYNC_TYPE_PRODUCT, code, name, 1, "商品信息更新成功", null);
+        } else {
+            // 新增商品
+            Product newProduct = new Product();
+            newProduct.setProductId(productData.get("product_id") != null ? productData.get("product_id").toString() : null);
+            newProduct.setCode(code);
+            newProduct.setName(name);
+            newProduct.setBarcode(productData.get("bar_code") != null ? productData.get("bar_code").toString() : null);
+            newProduct.setType(productData.get("type") != null ? productData.get("type").toString() : null);
+            newProduct.setPic(productData.get("pic") != null ? productData.get("pic").toString() : null);
+            newProduct.setPlanogram(productData.get("planogram") != null ? productData.get("planogram").toString() : null);
+            newProduct.setApplyStatus(productData.get("apply_status") != null ? productData.get("apply_status").toString() : null);
+            newProduct.setCostPrice(parseDouble(productData.get("cost_price")));
+            newProduct.setRetailPrice(parseDouble(productData.get("cprice")));
+            newProduct.setCustomerId(productData.get("customer_id") != null ? productData.get("customer_id").toString() : null);
+            newProduct.setSyncStatus(STATUS_SUCCESS);
+            newProduct.setSyncTime(LocalDateTime.now());
+            newProduct.setIsDeleted(0);
+            newProduct.setCreateTime(LocalDateTime.now());
+            newProduct.setUpdateTime(LocalDateTime.now());
+            productService.save(newProduct);
+
+            // 记录成功日志
+            saveSyncLog(recordId, SYNC_TYPE_PRODUCT, code, name, 1, "商品新增成功", null);
+        }
+    }
+
+    /**
+     * 解析Double值
+     */
+    private Double parseDouble(Object value) {
+        if (value == null) return null;
+        try {
+            return Double.parseDouble(value.toString());
+        } catch (NumberFormatException e) {
+            return null;
+        }
+    }
 }

+ 9 - 4
haha-service/src/main/java/com/haha/service/impl/ProductServiceImpl.java

@@ -6,6 +6,7 @@ import com.haha.mapper.ProductMapper;
 import com.haha.service.ProductService;
 import org.springframework.stereotype.Service;
 
+import java.time.LocalDateTime;
 import java.util.List;
 
 @Service
@@ -35,12 +36,16 @@ public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> impl
     }
 
     @Override
-    public boolean updateSyncStatus(Long productId, Integer syncStatus) {
-        return lambdaUpdate().eq(Product::getId, productId).set(Product::getHahaSyncStatus, syncStatus).update();
+    public Product getProductByCode(String code) {
+        return lambdaQuery().eq(Product::getCode, code).one();
     }
 
     @Override
-    public boolean updateHahaModelId(Long productId, String modelId) {
-        return lambdaUpdate().eq(Product::getId, productId).set(Product::getHahaModelId, modelId).update();
+    public boolean updateSyncStatus(Long productId, Integer syncStatus) {
+        return lambdaUpdate()
+                .eq(Product::getId, productId)
+                .set(Product::getSyncStatus, syncStatus)
+                .set(Product::getSyncTime, LocalDateTime.now())
+                .update();
     }
 }