Ver Fonte

feat(mall): 实现分类同步功能集成产品分类服务

- 集成 RemoteCategoryService 通过 Dubbo 引用远程分类服务
- 添加 CategoryDto 数据传输对象用于分类数据交换
- 实现分类树结构的查询、构建和展示功能
- 支持分类数据的新增、更新和同步操作
- 在 EpCategoryMainVo 中添加 subMenus 和 panelData 字段
- 实现分类数据的增量更新和树形结构转换
- 添加异常处理和日志记录机制
- 配置 ruoyi-api-product 依赖支持分类服务调用
hurx há 4 dias atrás
pai
commit
a9503cb983

+ 24 - 2
ruoyi-api/ruoyi-api-product/src/main/java/org/dromara/product/api/RemoteCategoryService.java

@@ -1,5 +1,8 @@
 package org.dromara.product.api;
 
+import org.dromara.product.api.domain.CategoryDto;
+
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
@@ -9,7 +12,26 @@ import java.util.Set;
  */
 public interface RemoteCategoryService {
 
-    Map<Long,Map<String,String>> getallCategoryNameById(Map<Long,Long> categoryMap);
+    Map<Long, Map<String, String>> getallCategoryNameById(Map<Long, Long> categoryMap);
+
+    Map<Long, String> getCategoryNamebyIds(Set<Long> ids);
+
+    Long insertCategory(List<CategoryDto> categoryDtos);
+
+    /**
+     * 根据分类ID查询分类树结构
+     *
+     * @param categoryId 分类ID
+     * @return 分类树结构列表(包含子分类)
+     */
+    List<CategoryDto> getCategoryTreeById(Long categoryId);
 
-    Map<Long,String> getCategoryNamebyIds(Set<Long> ids);
+    /**
+     * 智能更新分类树(增量更新,保留原有ID)
+     *
+     * @param rootCategoryId 根分类ID
+     * @param newCategoryTree 新的分类树数据
+     * @return 是否更新成功
+     */
+    Boolean updateCategoryTree(Long rootCategoryId, List<CategoryDto> newCategoryTree);
 }

+ 141 - 0
ruoyi-api/ruoyi-api-product/src/main/java/org/dromara/product/api/domain/CategoryDto.java

@@ -0,0 +1,141 @@
+package org.dromara.product.api.domain;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.List;
+
+@Data
+public class CategoryDto implements Serializable {
+
+    /**
+     * 主键
+     */
+    private Long id;
+
+    /**
+     * 分类编号
+     */
+    private String categoryNo;
+
+    /**
+     * 分类名称
+     */
+    private String categoryName;
+
+    /**
+     * 父级分类ID
+     */
+    private Long parentId;
+
+    /**
+     * 祖籍列表
+     */
+    private String ancestors;
+
+    /**
+     * 分类层级(1=一级,2=二级, 3三级)
+     */
+    private Long classLevel;
+
+    /**
+     * 是否显示(1=显示,0=隐藏)
+     */
+    private Long isShow;
+
+    /**
+     * 是否在GPS中显示
+     */
+    private Long isShowGps;
+
+    /**
+     * 折扣率(可能为JSON或文本)
+     */
+    private BigDecimal discountRate;
+
+    /**
+     * 拼音码(用于快速检索)
+     */
+    private String pyCode;
+
+    /**
+     * 分类描述
+     */
+    private String classDescription;
+
+    /**
+     * 数据来源
+     */
+    private String dataSource;
+
+    /**
+     * 自定义标签1
+     */
+    private String oneLable1;
+
+    /**
+     * 自定义标签2
+     */
+    private String oneLable2;
+
+    /**
+     * 自定义链接1
+     */
+    private String oneLink1;
+
+    /**
+     * 自定义链接2
+     */
+    private String oneLink2;
+
+    /**
+     * 排序值,默认为0
+     */
+    private Long sort;
+
+    /**
+     * 颜色(如CSS颜色值)
+     */
+    private String color;
+
+    /**
+     * 采购编号
+     */
+    private String purchaseNo;
+
+    /**
+     * 采购名称
+     */
+    private String purchaseName;
+
+    /**
+     * 采购负责人编号
+     */
+    private String purchaseManagerNo;
+
+    /**
+     * 采购负责人姓名
+     */
+    private String purchaseManagerName;
+
+    /**
+     * 所属平台 (0=平台,1=工业品,2=福利,3=企业购,4=大客户)
+     */
+    private Long platform;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+    /**
+     * 审核人
+     */
+    private Long reviewerId;
+
+    /**
+     * 子分类列表(用于树形结构)
+     */
+    private List<CategoryDto> children;
+}

+ 4 - 0
ruoyi-modules/ruoyi-mall/pom.xml

@@ -104,6 +104,10 @@
             <groupId>org.dromara</groupId>
             <artifactId>ruoyi-api-resource</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-api-product</artifactId>
+        </dependency>
 
 
     </dependencies>

+ 80 - 0
ruoyi-modules/ruoyi-mall/src/main/java/org/dromara/mall/domain/vo/EpCategoryMainVo.java

@@ -5,10 +5,12 @@ import cn.idev.excel.annotation.ExcelProperty;
 import io.github.linpeilie.annotations.AutoMapper;
 import lombok.Data;
 import org.dromara.mall.domain.EpCategoryMain;
+import org.dromara.product.api.domain.CategoryDto;
 
 import java.io.Serial;
 import java.io.Serializable;
 import java.util.List;
+import java.util.Map;
 
 
 /**
@@ -96,5 +98,83 @@ public class EpCategoryMainVo implements Serializable {
 
     private String categoryThemeColor;
 
+    List<CategoryDto>categoryDtoList;
+
+    /**
+     * 子菜单列表(分类树结构)
+     */
+    private List<Map<String, Object>> subMenus;
+
+    /**
+     * 面板数据(用于前端展示)
+     */
+    private PanelDataVo panelData;
+
+    /**
+     * 面板数据内部类
+     */
+    @Data
+    public static class PanelDataVo implements Serializable {
+        @Serial
+        private static final long serialVersionUID = 1L;
+
+        /**
+         * 主标题
+         */
+        private String mainTitle;
+
+        /**
+         * 副标题
+         */
+        private String subTitle;
+
+        /**
+         * 便签列表
+         */
+        private List<NoteItem> notes;
+
+        /**
+         * 分组列表
+         */
+        private List<GroupItem> groups;
+    }
+
+    /**
+     * 便签项
+     */
+    @Data
+    public static class NoteItem implements Serializable {
+        @Serial
+        private static final long serialVersionUID = 1L;
+
+        /**
+         * 名称
+         */
+        private String name;
+
+        /**
+         * 链接
+         */
+        private String link;
+    }
+
+    /**
+     * 分组项
+     */
+    @Data
+    public static class GroupItem implements Serializable {
+        @Serial
+        private static final long serialVersionUID = 1L;
+
+        /**
+         * 分组标题
+         */
+        private String title;
+
+        /**
+         * 分组下的分类项列表
+         */
+        private List<String> items;
+    }
 
 }

+ 447 - 27
ruoyi-modules/ruoyi-mall/src/main/java/org/dromara/mall/service/impl/EpCategoryMainServiceImpl.java

@@ -4,10 +4,12 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.dubbo.config.annotation.DubboReference;
 import org.dromara.common.core.utils.MapstructUtils;
 import org.dromara.common.core.utils.StringUtils;
 import org.dromara.common.mybatis.core.page.PageQuery;
@@ -24,11 +26,16 @@ import org.dromara.mall.mapper.EpCategoryItemMapper;
 import org.dromara.mall.mapper.EpCategoryMainMapper;
 import org.dromara.mall.service.IEpCategoryMainService;
 import org.dromara.mall.service.IEpSearchConfigService;
+import org.dromara.product.api.RemoteCategoryService;
+import org.dromara.product.api.domain.CategoryDto;
 import org.springframework.stereotype.Service;
 
+import java.util.ArrayList;
 import java.util.Collection;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
 
 /**
  * 分类设置主Service业务层处理
@@ -47,6 +54,9 @@ public class EpCategoryMainServiceImpl extends ServiceImpl<EpCategoryMainMapper,
 
     private final IEpSearchConfigService searchConfigService;
 
+    @DubboReference
+    private RemoteCategoryService remoteCategoryService;
+
     private static final ObjectMapper objectMapper = new ObjectMapper();
 
     /**
@@ -58,7 +68,7 @@ public class EpCategoryMainServiceImpl extends ServiceImpl<EpCategoryMainMapper,
     @Override
     public EpCategoryMainVo queryById(Long id) {
         // 查询主表数据
-        EpCategoryMainVo mainVo = baseMapper.selectVoById(id);
+      /*  EpCategoryMainVo mainVo = baseMapper.selectVoById(id);
         if (mainVo == null) {
             return null;
         }
@@ -111,11 +121,164 @@ public class EpCategoryMainServiceImpl extends ServiceImpl<EpCategoryMainMapper,
 
             responseVo.setTags(tags);
             panelData.setNotes(notes);
-        }
+        }*/
         EpCategoryMainVo vo = baseMapper.selectVoById(id);
+        if (vo == null) {
+            return null;
+        }
+
+        // 如果有同步分类ID,查询分类树结构
+        if (vo.getSyncCategoryId() != null) {
+            try {
+                List<CategoryDto> categoryTree = remoteCategoryService.getCategoryTreeById(vo.getSyncCategoryId());
+                
+                // 将分类树转换为 subMenus 格式
+                if (categoryTree != null && !categoryTree.isEmpty()) {
+                    // 构建分类树结构
+                    List<Map<String, Object>> subMenus = buildCategoryTree(categoryTree);
+                    
+                    // 将 subMenus 设置到 vo 中
+                    vo.setSubMenus(subMenus);
+                    
+                    // 构建 panelData(面板展示数据)
+                    EpCategoryMainVo.PanelDataVo panelData = buildPanelData(vo, categoryTree);
+                    vo.setPanelData(panelData);
+                }
+            } catch (Exception e) {
+                log.error("查询分类树失败", e);
+            }
+        }
+
         return vo;
     }
 
+    /**
+     * 构建分类树结构(只返回二级及以后的分类,不包含一级分类)
+     *
+     * @param categories 扁平的分类列表
+     * @return 树形结构的 subMenus(二级分类列表)
+     */
+    private List<Map<String, Object>> buildCategoryTree(List<CategoryDto> categories) {
+        if (categories == null || categories.isEmpty()) {
+            return new ArrayList<>();
+        }
+
+        log.info("开始构建分类树 - 总分类数量: {}", categories.size());
+
+        // 按层级分组
+        Map<Long, List<CategoryDto>> levelMap = new HashMap<>();
+        for (CategoryDto category : categories) {
+            Long level = category.getClassLevel();
+            if (level != null) {
+                levelMap.computeIfAbsent(level, k -> new ArrayList<>()).add(category);
+            }
+        }
+
+        log.info("按层级分组 - levelMap: {}", levelMap.keySet());
+
+        // 获取一级分类(用于查找其子分类)
+        List<CategoryDto> level1Categories = levelMap.getOrDefault(1L, new ArrayList<>());
+        log.info("一级分类数量: {}, IDs: {}", level1Categories.size(), level1Categories.stream().map(CategoryDto::getId).collect(Collectors.toList()));
+        
+        // 获取二级分类(作为 subMenus 的根节点)
+        List<CategoryDto> level2Categories = levelMap.getOrDefault(2L, new ArrayList<>());
+        log.info("二级分类数量: {}", level2Categories.size());
+        
+        List<Map<String, Object>> subMenus = new ArrayList<>();
+
+        for (CategoryDto level2 : level2Categories) {
+            Map<String, Object> level2Map = new HashMap<>();
+            level2Map.put("id", level2.getId());
+            level2Map.put("name", level2.getCategoryName());
+            level2Map.put("sortOrder", level2.getSort());
+            level2Map.put("status", level2.getIsShow());
+            level2Map.put("level", level2.getClassLevel());
+
+            // 查找三级分类
+            List<CategoryDto> level3Categories = categories.stream()
+                .filter(c -> c.getParentId() != null && c.getParentId().equals(level2.getId()))
+                .collect(Collectors.toList());
+            
+            log.info("二级分类 [{}] 的三级分类数量: {}", level2.getCategoryName(), level3Categories.size());
+
+            List<Map<String, Object>> children = new ArrayList<>();
+            for (CategoryDto level3 : level3Categories) {
+                Map<String, Object> level3Map = new HashMap<>();
+                level3Map.put("id", level3.getId());
+                level3Map.put("name", level3.getCategoryName());
+                level3Map.put("sortOrder", level3.getSort());
+                level3Map.put("status", level3.getIsShow());
+                level3Map.put("level", level3.getClassLevel());
+                children.add(level3Map);
+            }
+
+            level2Map.put("children", children);
+            subMenus.add(level2Map);
+        }
+
+        log.info("构建完成 - subMenus 数量: {}", subMenus.size());
+        return subMenus;
+    }
+
+    /**
+     * 构建面板数据(panelData)
+     *
+     * @param vo 分类主表 VO
+     * @param categories 分类树列表
+     * @return PanelDataVo
+     */
+    private EpCategoryMainVo.PanelDataVo buildPanelData(EpCategoryMainVo vo, List<CategoryDto> categories) {
+        EpCategoryMainVo.PanelDataVo panelData = new EpCategoryMainVo.PanelDataVo();
+        
+        // 设置主标题和副标题
+        panelData.setMainTitle(vo.getPanelMainTitle());
+        panelData.setSubTitle(vo.getPanelSubTitle());
+        
+        // 从数据库中查询 notes 标签项
+        List<EpCategoryMainVo.NoteItem> notes = new ArrayList<>();
+        if (vo.getNotes() != null) {
+            for (EpCategoryItemVo itemVo : vo.getNotes()) {
+                EpCategoryMainVo.NoteItem noteItem = new EpCategoryMainVo.NoteItem();
+                noteItem.setName(itemVo.getName());
+                noteItem.setLink(itemVo.getLink());
+                notes.add(noteItem);
+            }
+        }
+        panelData.setNotes(notes);
+        
+        // 从分类树中构建 groups(二级分类作为分组标题,三级分类作为 items)
+        List<EpCategoryMainVo.GroupItem> groups = new ArrayList<>();
+        if (categories != null && !categories.isEmpty()) {
+            // 按层级分组
+            Map<Long, List<CategoryDto>> levelMap = new HashMap<>();
+            for (CategoryDto category : categories) {
+                Long level = category.getClassLevel();
+                if (level != null) {
+                    levelMap.computeIfAbsent(level, k -> new ArrayList<>()).add(category);
+                }
+            }
+            
+            // 获取二级分类作为分组
+            List<CategoryDto> level2Categories = levelMap.getOrDefault(2L, new ArrayList<>());
+            for (CategoryDto level2 : level2Categories) {
+                EpCategoryMainVo.GroupItem group = new EpCategoryMainVo.GroupItem();
+                group.setTitle(level2.getCategoryName());
+                
+                // 获取该二级分类下的所有三级分类名称
+                List<String> items = categories.stream()
+                    .filter(c -> c.getParentId() != null && c.getParentId().equals(level2.getId()))
+                    .map(CategoryDto::getCategoryName)
+                    .collect(Collectors.toList());
+                
+                group.setItems(items);
+                groups.add(group);
+            }
+        }
+        panelData.setGroups(groups);
+        
+        return panelData;
+    }
+
 
     /**
      * 分页查询分类设置主列表
@@ -130,12 +293,32 @@ public class EpCategoryMainServiceImpl extends ServiceImpl<EpCategoryMainMapper,
         Page<EpCategoryMainVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
         List<EpCategoryMainVo> records = result.getRecords();
         EpSearchConfigVo currentSearchConfig = searchConfigService.getCurrentSearchConfig();
-        // 为每个分类填充对应的标签项
+        
+        // 为每个分类填充对应的标签项和分类树
         for (EpCategoryMainVo record : records) {
             fillCategoryItems(record);
-            if (null!=currentSearchConfig){
+            if (null != currentSearchConfig) {
                 record.setCategoryThemeColor(currentSearchConfig.getCategoryThemeColor());
             }
+            
+            // 如果有同步分类ID,查询分类树结构
+            if (record.getSyncCategoryId() != null) {
+                try {
+                    List<CategoryDto> categoryTree = remoteCategoryService.getCategoryTreeById(record.getSyncCategoryId());
+                    
+                    // 将分类树转换为 subMenus 格式
+                    if (categoryTree != null && !categoryTree.isEmpty()) {
+                        List<Map<String, Object>> subMenus = buildCategoryTree(categoryTree);
+                        record.setSubMenus(subMenus);
+                        
+                        // 构建 panelData(面板展示数据)
+                        EpCategoryMainVo.PanelDataVo panelData = buildPanelData(record, categoryTree);
+                        record.setPanelData(panelData);
+                    }
+                } catch (Exception e) {
+                    log.error("查询分类树失败, categoryId: {}", record.getSyncCategoryId(), e);
+                }
+            }
         }
 
         return TableDataInfo.build(result);
@@ -150,7 +333,30 @@ public class EpCategoryMainServiceImpl extends ServiceImpl<EpCategoryMainMapper,
     @Override
     public List<EpCategoryMainVo> queryList(EpCategoryMainBo bo) {
         LambdaQueryWrapper<EpCategoryMain> lqw = buildQueryWrapper(bo);
-        return baseMapper.selectVoList(lqw);
+        List<EpCategoryMainVo> voList = baseMapper.selectVoList(lqw);
+        
+        // 为每个分类填充分类树和 panelData
+        for (EpCategoryMainVo vo : voList) {
+            if (vo.getSyncCategoryId() != null) {
+                try {
+                    List<CategoryDto> categoryTree = remoteCategoryService.getCategoryTreeById(vo.getSyncCategoryId());
+                    
+                    if (categoryTree != null && !categoryTree.isEmpty()) {
+                        // 构建 subMenus(二级及以后的分类)
+                        List<Map<String, Object>> subMenus = buildCategoryTree(categoryTree);
+                        vo.setSubMenus(subMenus);
+                        
+                        // 构建 panelData(面板展示数据)
+                        EpCategoryMainVo.PanelDataVo panelData = buildPanelData(vo, categoryTree);
+                        vo.setPanelData(panelData);
+                    }
+                } catch (Exception e) {
+                    log.error("查询分类树失败, categoryId: {}", vo.getSyncCategoryId(), e);
+                }
+            }
+        }
+        
+        return voList;
     }
 
     private LambdaQueryWrapper<EpCategoryMain> buildQueryWrapper(EpCategoryMainBo bo) {
@@ -178,14 +384,129 @@ public class EpCategoryMainServiceImpl extends ServiceImpl<EpCategoryMainMapper,
     public Boolean insertByBo(EpCategoryMainBo bo) {
         EpCategoryMain add = MapstructUtils.convert(bo, EpCategoryMain.class);
         validEntityBeforeSave(add);
+
+        // 先插入主表数据
         boolean flag = baseMapper.insert(add) > 0;
-        if (flag) {
-            bo.setId(add.getId());
+        if (!flag) {
+            return false;
+        }
 
-            // 保存分类项列表
-            saveCategoryItems(add.getId(), bo);
+        // 解析 remark 中的 subMenus 并同步到分类表
+        try {
+            if (StringUtils.isNotBlank(bo.getRemark())) {
+                JsonNode rootNode = objectMapper.readTree(bo.getRemark());
+                JsonNode subMenus = rootNode.get("subMenus");
+
+                if (subMenus != null && subMenus.isArray()) {
+                    // 使用 bo.name 创建一级分类
+                    CategoryDto level1Dto = new CategoryDto();
+                    level1Dto.setCategoryName(bo.getName()); // 使用 bo.name 作为一级分类名称
+                    level1Dto.setParentId(0L); // 一级分类的父ID为0
+                    level1Dto.setClassLevel(1L); // 一级分类层级为1
+                    level1Dto.setIsShow(1L); // 默认显示
+                    level1Dto.setSort(bo.getSortOrder() != null ? bo.getSortOrder() : 0L);
+                    level1Dto.setDataSource("youyi"); // 数据来源设置为 youyi
+                    level1Dto.setPlatform(3L); // 平台设置为 3(企业购)
+                    level1Dto.setAncestors("0"); // 一级分类的祖籍为0
+
+                    // 插入一级分类
+                    List<CategoryDto> level1List = new ArrayList<>();
+                    level1List.add(level1Dto);
+                    Long level1Id = remoteCategoryService.insertCategory(level1List);
+
+                    if (level1Id != null) {
+                        // 将一级分类的 ID 绑定到 add.syncCategoryId
+                        add.setSyncCategoryId(level1Id);
+                        baseMapper.updateById(add);
+
+                        String level1Ancestors = "0," + level1Id;
+
+                        // 处理 subMenus 作为二级分类
+                        if (subMenus.size() > 0) {
+                            List<Long> level2Ids = new ArrayList<>();
+
+                            for (JsonNode level2Node : subMenus) {
+                                CategoryDto level2Dto = new CategoryDto();
+                                level2Dto.setCategoryName(level2Node.has("name") ? level2Node.get("name").asText() : null);
+                                level2Dto.setParentId(level1Id); // 二级分类的父ID为一级分类ID
+                                level2Dto.setClassLevel(2L); // 二级分类层级为2
+                                level2Dto.setIsShow(level2Node.has("status") ? level2Node.get("status").asLong() : 1L);
+                                level2Dto.setSort(level2Node.has("sortOrder") ? level2Node.get("sortOrder").asLong() : 0L);
+                                level2Dto.setDataSource("youyi");
+                                level2Dto.setPlatform(3L); // 平台设置为 3(企业购)
+                                level2Dto.setAncestors(level1Ancestors); // 二级分类的祖籍为 0,一级ID
+
+                                // 逐个插入二级分类以获取 ID
+                                List<CategoryDto> level2List = new ArrayList<>();
+                                level2List.add(level2Dto);
+                                Long level2Id = remoteCategoryService.insertCategory(level2List);
+                                if (level2Id != null) {
+                                    level2Ids.add(level2Id);
+
+                                    // 处理三级分类(当前二级分类的 children)
+                                    JsonNode level3Nodes = level2Node.get("children");
+                                    if (level3Nodes != null && level3Nodes.isArray() && !level3Nodes.isEmpty()) {
+                                        String level2Ancestors = level1Ancestors + "," + level2Id;
+
+                                        for (JsonNode level3Node : level3Nodes) {
+                                            CategoryDto level3Dto = new CategoryDto();
+                                            level3Dto.setCategoryName(level3Node.has("name") ? level3Node.get("name").asText() : null);
+                                            level3Dto.setParentId(level2Id); // 三级分类的父ID为二级分类ID
+                                            level3Dto.setClassLevel(3L); // 三级分类层级为3
+                                            level3Dto.setIsShow(level3Node.has("status") ? level3Node.get("status").asLong() : 1L);
+                                            level3Dto.setSort(level3Node.has("sortOrder") ? level3Node.get("sortOrder").asLong() : 0L);
+                                            level3Dto.setDataSource("youyi");
+                                            level3Dto.setPlatform(3L); // 平台设置为 3(企业购)
+                                            level3Dto.setAncestors(level2Ancestors); // 三级分类的祖籍为 0,一级ID,二级ID
+
+                                            // 逐个插入三级分类
+                                            List<CategoryDto> level3List = new ArrayList<>();
+                                            level3List.add(level3Dto);
+                                            remoteCategoryService.insertCategory(level3List);
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        } catch (JsonProcessingException e) {
+            log.error("解析 remark JSON 数据失败", e);
+            throw new RuntimeException("解析分类数据失败", e);
         }
-        return flag;
+
+        bo.setId(add.getId());
+
+        // 保存分类项列表
+        saveCategoryItems(add.getId(), bo);
+
+        return true;
+    }
+
+    /**
+     * 批量插入分类并返回 ID 列表
+     *
+     * @param categoryDtos 分类 DTO 列表
+     * @return 插入后的 ID 列表
+     */
+    private List<Long> batchInsertCategories(List<CategoryDto> categoryDtos) {
+        if (categoryDtos == null || categoryDtos.isEmpty()) {
+            return new ArrayList<>();
+        }
+
+        // 调用远程服务批量插入(会回填 ID 到 DTO 中)
+        remoteCategoryService.insertCategory(categoryDtos);
+
+        // 从 DTO 中收集 ID
+        List<Long> ids = new ArrayList<>();
+        for (CategoryDto dto : categoryDtos) {
+            if (dto.getId() != null) {
+                ids.add(dto.getId());
+            }
+        }
+
+        return ids;
     }
 
     /**
@@ -198,16 +519,127 @@ public class EpCategoryMainServiceImpl extends ServiceImpl<EpCategoryMainMapper,
     public Boolean updateByBo(EpCategoryMainBo bo) {
         EpCategoryMain update = MapstructUtils.convert(bo, EpCategoryMain.class);
         validEntityBeforeSave(update);
+        
+        // 先更新主表数据
         boolean flag = baseMapper.updateById(update) > 0;
         if (flag) {
-            // 先根据categoryId删除原有的子项
+            // 处理分类树的增量更新
+            if (bo.getSyncCategoryId() != null && StringUtils.isNotBlank(bo.getRemark())) {
+                try {
+                    JsonNode rootNode = objectMapper.readTree(bo.getRemark());
+                    JsonNode subMenus = rootNode.get("subMenus");
+                    
+                    if (subMenus != null && subMenus.isArray()) {
+                        // 构建新的分类树数据
+                        List<CategoryDto> newCategoryTree = buildCategoryTreeFromSubMenus(
+                            bo.getName(), 
+                            subMenus, 
+                            bo.getSyncCategoryId()
+                        );
+                        
+                        // 调用远程服务进行增量更新
+                        remoteCategoryService.updateCategoryTree(bo.getSyncCategoryId(), newCategoryTree);
+                    }
+                } catch (Exception e) {
+                    log.error("更新分类树失败", e);
+                    // 分类树更新失败不影响主表更新,只记录日志
+                }
+            }
+            
+            // 处理标签项(tags/notes)- 这部分可以删除重建
             categoryItemMapper.delete(Wrappers.<EpCategoryItem>lambdaQuery().eq(EpCategoryItem::getCategoryId, update.getId()));
-
-            // 再重新添加新的子项
             saveCategoryItems(update.getId(), bo);
         }
         return flag;
     }
+    
+    /**
+     * 从 subMenus 构建分类树 DTO 列表(树形结构)
+     *
+     * @param categoryName 一级分类名称
+     * @param subMenus 子菜单 JSON 节点
+     * @param rootCategoryId 根分类ID
+     * @return 分类树 DTO 列表(只包含一级分类,带 children)
+     */
+    private List<CategoryDto> buildCategoryTreeFromSubMenus(String categoryName, JsonNode subMenus, Long rootCategoryId) {
+        List<CategoryDto> result = new ArrayList<>();
+        
+        // 添加一级分类(根分类)
+        CategoryDto rootDto = new CategoryDto();
+        rootDto.setId(rootCategoryId); // 使用现有的 ID
+        rootDto.setCategoryName(categoryName);
+        rootDto.setParentId(0L);
+        rootDto.setClassLevel(1L);
+        rootDto.setDataSource("youyi");
+        rootDto.setPlatform(3L);
+        
+        // 处理二级分类
+        List<CategoryDto> level2Children = new ArrayList<>();
+        if (subMenus != null && subMenus.isArray()) {
+            for (JsonNode level2Node : subMenus) {
+                // 如果前端传来的数据包含 level=1 的一级分类,需要跳过(因为已经在 bo.name 中)
+                if (level2Node.has("level") && level2Node.get("level").asInt() == 1) {
+                    // 这是错误的数据结构,应该直接使用 bo.name 作为一级分类
+                    // 递归处理这个一级分类的 children 作为二级分类
+                    JsonNode nestedChildren = level2Node.get("children");
+                    if (nestedChildren != null && nestedChildren.isArray()) {
+                        for (JsonNode nestedChild : nestedChildren) {
+                            CategoryDto level2Dto = buildCategoryDtoFromNode(nestedChild, rootCategoryId, 2L);
+                            level2Children.add(level2Dto);
+                        }
+                    }
+                    continue; // 跳过一级分类节点
+                }
+                
+                // 正常的二级分类
+                CategoryDto level2Dto = buildCategoryDtoFromNode(level2Node, rootCategoryId, 2L);
+                level2Children.add(level2Dto);
+            }
+        }
+        
+        rootDto.setChildren(level2Children);
+        result.add(rootDto);
+        
+        return result;
+    }
+    
+    /**
+     * 从 JSON 节点构建 CategoryDto
+     *
+     * @param node JSON 节点
+     * @param parentId 父分类ID
+     * @param classLevel 分类层级
+     * @return CategoryDto
+     */
+    private CategoryDto buildCategoryDtoFromNode(JsonNode node, Long parentId, Long classLevel) {
+        CategoryDto dto = new CategoryDto();
+        
+        // 如果有 ID,说明是已存在的分类
+        if (node.has("id") && node.get("id").asLong() > 0) {
+            dto.setId(node.get("id").asLong());
+        }
+        
+        dto.setCategoryName(node.has("name") ? node.get("name").asText() : null);
+        dto.setParentId(parentId);
+        dto.setClassLevel(classLevel);
+        dto.setIsShow(node.has("status") ? node.get("status").asLong() : 1L);
+        dto.setSort(node.has("sortOrder") ? node.get("sortOrder").asLong() : 0L);
+        dto.setDataSource("youyi");
+        dto.setPlatform(3L);
+        
+        // 递归处理子分类
+        JsonNode childrenNode = node.get("children");
+        if (childrenNode != null && childrenNode.isArray() && !childrenNode.isEmpty()) {
+            List<CategoryDto> children = new ArrayList<>();
+            for (JsonNode childNode : childrenNode) {
+                CategoryDto childDto = buildCategoryDtoFromNode(childNode, dto.getId() != null ? dto.getId() : parentId, classLevel + 1);
+                children.add(childDto);
+            }
+            dto.setChildren(children);
+        }
+        
+        return dto;
+    }
 
     /**
      * 保存分类项(从 remark 和 categoryItemList 中解析)
@@ -254,20 +686,8 @@ public class EpCategoryMainServiceImpl extends ServiceImpl<EpCategoryMainMapper,
                     }
                 }
 
-                // 处理 groups(如果需要)
-                JsonNode groupsNode = rootNode.get("groups");
-                if (groupsNode != null && groupsNode.isArray()) {
-                    int sortOrder = items.size();
-                    for (JsonNode groupNode : groupsNode) {
-                        EpCategoryItem item = new EpCategoryItem();
-                        item.setCategoryId(categoryId);
-                        item.setItemType("group");
-                        item.setName(groupNode.has("name") ? groupNode.get("name").asText() : null);
-                        item.setLink(groupNode.has("link") ? groupNode.get("link").asText() : null);
-                        item.setSortOrder((long) sortOrder++);
-                        items.add(item);
-                    }
-                }
+                // 注意:groups 数据是从分类树动态生成的,不需要保存到 ep_category_item 表
+                // 所以这里不处理 groups 节点
             } catch (Exception e) {
                 log.error("解析 remark JSON 数据失败", e);
             }

+ 225 - 0
ruoyi-modules/ruoyi-product/src/main/java/org/dromara/product/dubbo/RemoteCategoryServiceImpl.java

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import lombok.RequiredArgsConstructor;
 import org.apache.dubbo.config.annotation.DubboService;
 import org.dromara.product.api.RemoteCategoryService;
+import org.dromara.product.api.domain.CategoryDto;
 import org.dromara.product.domain.ProductCategory;
 import org.dromara.product.mapper.ProductCategoryMapper;
 import org.springframework.stereotype.Service;
@@ -133,4 +134,228 @@ public class RemoteCategoryServiceImpl implements RemoteCategoryService {
 
         return categoryIdToNameMap;
     }
+
+    @Override
+    public Long insertCategory(List<CategoryDto> categoryDtos) {
+        if (categoryDtos == null || categoryDtos.isEmpty()) {
+            return null;
+        }
+
+        // 转换为 ProductCategory 实体
+        List<ProductCategory> categories = new ArrayList<>();
+        for (CategoryDto dto : categoryDtos) {
+            ProductCategory category = new ProductCategory();
+            category.setCategoryName(dto.getCategoryName());
+            category.setParentId(dto.getParentId());
+            category.setAncestors(dto.getAncestors());
+            category.setClassLevel(dto.getClassLevel());
+            category.setIsShow(dto.getIsShow());
+            category.setIsShowGps(dto.getIsShowGps());
+            category.setDiscountRate(dto.getDiscountRate());
+            category.setPyCode(dto.getPyCode());
+            category.setClassDescription(dto.getClassDescription());
+            category.setDataSource(dto.getDataSource());
+            category.setOneLable1(dto.getOneLable1());
+            category.setOneLable2(dto.getOneLable2());
+            category.setOneLink1(dto.getOneLink1());
+            category.setOneLink2(dto.getOneLink2());
+            category.setSort(dto.getSort());
+            category.setColor(dto.getColor());
+            category.setPurchaseNo(dto.getPurchaseNo());
+            category.setPurchaseName(dto.getPurchaseName());
+            category.setPurchaseManagerNo(dto.getPurchaseManagerNo());
+            category.setPurchaseManagerName(dto.getPurchaseManagerName());
+            category.setPlatform(dto.getPlatform());
+            category.setRemark(dto.getRemark());
+            category.setReviewerId(dto.getReviewerId());
+            categories.add(category);
+        }
+
+        // 批量插入
+        boolean success = categoryMapper.insertBatch(categories);
+        if (!success || categories.isEmpty()) {
+            return null;
+        }
+
+        // 将插入后的 ID 回填到 DTO 中
+        for (int i = 0; i < categoryDtos.size() && i < categories.size(); i++) {
+            categoryDtos.get(i).setId(categories.get(i).getId());
+        }
+
+        // 返回第一个(一级分类)的 ID
+        return categories.get(0).getId();
+    }
+
+    @Override
+    public List<CategoryDto> getCategoryTreeById(Long categoryId) {
+        if (categoryId == null) {
+            return new ArrayList<>();
+        }
+
+        // 查询当前分类
+        ProductCategory currentCategory = categoryMapper.selectById(categoryId);
+        if (currentCategory == null) {
+            return new ArrayList<>();
+        }
+
+        // 查询所有子分类(根据 ancestors 和 parentId 查询)
+        // ancestors 字段可能使用逗号或竖线分隔,需要同时支持
+        List<ProductCategory> childCategories = categoryMapper.selectList(
+            new LambdaQueryWrapper<ProductCategory>()
+                .apply("ancestors LIKE CONCAT('%', {0}, '%')", categoryId)
+                .or()
+                .eq(ProductCategory::getParentId, categoryId)
+                .orderByAsc(ProductCategory::getSort)
+                .orderByAsc(ProductCategory::getId)
+        );
+
+        // 去重:使用 Set 来避免重复
+        Set<Long> seenIds = new HashSet<>();
+        seenIds.add(currentCategory.getId());
+
+        List<ProductCategory> allCategories = new ArrayList<>();
+        allCategories.add(currentCategory);
+
+        for (ProductCategory child : childCategories) {
+            if (seenIds.add(child.getId())) { // 如果 ID 不存在则添加
+                allCategories.add(child);
+            }
+        }
+
+//        log.info("查询分类树 - categoryId: {}, 当前分类名称: {}, 查询到子分类数量: {}, 总数量: {}",
+//            categoryId, currentCategory.getCategoryName(), childCategories.size(), allCategories.size());
+
+        // 转换为 DTO
+        List<CategoryDto> result = new ArrayList<>();
+        for (ProductCategory category : allCategories) {
+            CategoryDto dto = new CategoryDto();
+            dto.setId(category.getId());
+            dto.setCategoryName(category.getCategoryName());
+            dto.setParentId(category.getParentId());
+            dto.setAncestors(category.getAncestors());
+            dto.setClassLevel(category.getClassLevel());
+            dto.setIsShow(category.getIsShow());
+            dto.setIsShowGps(category.getIsShowGps());
+            dto.setDiscountRate(category.getDiscountRate());
+            dto.setPyCode(category.getPyCode());
+            dto.setClassDescription(category.getClassDescription());
+            dto.setDataSource(category.getDataSource());
+            dto.setOneLable1(category.getOneLable1());
+            dto.setOneLable2(category.getOneLable2());
+            dto.setOneLink1(category.getOneLink1());
+            dto.setOneLink2(category.getOneLink2());
+            dto.setSort(category.getSort());
+            dto.setColor(category.getColor());
+            dto.setPurchaseNo(category.getPurchaseNo());
+            dto.setPurchaseName(category.getPurchaseName());
+            dto.setPurchaseManagerNo(category.getPurchaseManagerNo());
+            dto.setPurchaseManagerName(category.getPurchaseManagerName());
+            dto.setPlatform(category.getPlatform());
+            dto.setRemark(category.getRemark());
+            dto.setReviewerId(category.getReviewerId());
+            result.add(dto);
+        }
+
+        return result;
+    }
+
+    @Override
+    public Boolean updateCategoryTree(Long rootCategoryId, List<CategoryDto> newCategoryTree) {
+        if (rootCategoryId == null || newCategoryTree == null || newCategoryTree.isEmpty()) {
+            return false;
+        }
+
+        try {
+            // 1. 查询现有的分类树
+            List<CategoryDto> existingCategories = getCategoryTreeById(rootCategoryId);
+
+            // 2. 构建现有分类的 ID 映射
+            Map<Long, CategoryDto> existingMap = new HashMap<>();
+            for (CategoryDto existing : existingCategories) {
+                if (existing.getId() != null) {
+                    existingMap.put(existing.getId(), existing);
+                }
+            }
+
+            // 3. 递归处理分类树的更新
+            updateCategoryTreeRecursive(newCategoryTree, rootCategoryId, "0", existingMap);
+
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 递归更新分类树
+     *
+     * @param categories 当前层级的分类列表
+     * @param parentId 父分类ID
+     * @param ancestors 祖籍路径
+     * @param existingMap 现有分类映射
+     */
+    private void updateCategoryTreeRecursive(List<CategoryDto> categories, Long parentId, String ancestors, Map<Long, CategoryDto> existingMap) {
+        if (categories == null || categories.isEmpty()) {
+            return;
+        }
+
+        for (CategoryDto category : categories) {
+            Long categoryId = category.getId();
+
+            if (categoryId != null && existingMap.containsKey(categoryId)) {
+                // 已存在的分类,执行更新
+                CategoryDto existing = existingMap.get(categoryId);
+                ProductCategory updateEntity = new ProductCategory();
+                updateEntity.setId(categoryId);
+
+                // 只更新变化的字段
+                boolean needUpdate = false;
+                if (!Objects.equals(existing.getCategoryName(), category.getCategoryName())) {
+                    updateEntity.setCategoryName(category.getCategoryName());
+                    needUpdate = true;
+                }
+                if (!Objects.equals(existing.getSort(), category.getSort())) {
+                    updateEntity.setSort(category.getSort());
+                    needUpdate = true;
+                }
+                if (!Objects.equals(existing.getIsShow(), category.getIsShow())) {
+                    updateEntity.setIsShow(category.getIsShow());
+                    needUpdate = true;
+                }
+
+                if (needUpdate) {
+                    categoryMapper.updateById(updateEntity);
+                }
+
+                // 递归处理子分类
+                if (category.getChildren() != null && !category.getChildren().isEmpty()) {
+                    String newAncestors = ancestors + "," + categoryId;
+                    updateCategoryTreeRecursive(category.getChildren(), categoryId, newAncestors, existingMap);
+                }
+            } else if (categoryId == null) {
+                // 新分类,执行插入
+                ProductCategory newCategory = new ProductCategory();
+                newCategory.setCategoryName(category.getCategoryName());
+                newCategory.setParentId(parentId);
+                newCategory.setAncestors(ancestors);
+                newCategory.setClassLevel(category.getClassLevel());
+                newCategory.setIsShow(category.getIsShow() != null ? category.getIsShow() : 1L);
+                newCategory.setSort(category.getSort() != null ? category.getSort() : 0L);
+                newCategory.setDataSource(category.getDataSource());
+                newCategory.setPlatform(category.getPlatform());
+
+                categoryMapper.insert(newCategory);
+
+                // 更新 DTO 的 ID
+                category.setId(newCategory.getId());
+
+                // 递归处理子分类
+                if (category.getChildren() != null && !category.getChildren().isEmpty()) {
+                    String newAncestors = ancestors + "," + newCategory.getId();
+                    updateCategoryTreeRecursive(category.getChildren(), newCategory.getId(), newAncestors, existingMap);
+                }
+            }
+        }
+    }
 }