Parcourir la source

feat(mall): 添加页面分类和链接管理功能

- 在PlatformDataScopeInterceptor中新增mall_page_link和mall_page_category表的数据权限控制
- 扩展RedisUtils工具类,添加getCacheObject方法支持函数式获取缓存和deleteObjectPattern方法支持模式删除
- 添加CacheConstants缓存常量类,定义页面分类和链接相关的缓存键前缀和过期时间
- 实现CacheWarmupRunner应用启动器,在启动时预热页面分类和链接数据到Redis缓存
- 创建DataSyncEvent数据同步事件和DataSyncListener监听器,用于数据变更时清理相关缓存
- 实现IPageCategoryService和IPageLinkService接口,提供完整的分类和链接管理功能
- 定义PageCategory实体类和PageCategoryBo业务对象,映射mall_page_category数据库表
- 创建PageCategoryController控制器,提供分类管理的REST API接口
- 实现PageCategoryMapper数据访问层和对应的XML映射文件,支持分类树形查询和批量更新
- 完成PageCategoryServiceImpl业务逻辑实现,集成Redis缓存和数据同步事件机制
hurx il y a 1 mois
Parent
commit
5d577a056c
22 fichiers modifiés avec 2435 ajouts et 10 suppressions
  1. 3 1
      ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/interceptor/PlatformDataScopeInterceptor.java
  2. 41 9
      ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/utils/RedisUtils.java
  3. 45 0
      ruoyi-modules/ruoyi-mall/src/main/java/org/dromara/mall/config/CacheConstants.java
  4. 83 0
      ruoyi-modules/ruoyi-mall/src/main/java/org/dromara/mall/config/CacheWarmupRunner.java
  5. 285 0
      ruoyi-modules/ruoyi-mall/src/main/java/org/dromara/mall/controller/PageCategoryController.java
  6. 162 0
      ruoyi-modules/ruoyi-mall/src/main/java/org/dromara/mall/controller/PageLinkController.java
  7. 78 0
      ruoyi-modules/ruoyi-mall/src/main/java/org/dromara/mall/domain/PageCategory.java
  8. 80 0
      ruoyi-modules/ruoyi-mall/src/main/java/org/dromara/mall/domain/PageLink.java
  9. 80 0
      ruoyi-modules/ruoyi-mall/src/main/java/org/dromara/mall/domain/bo/PageCategoryBo.java
  10. 82 0
      ruoyi-modules/ruoyi-mall/src/main/java/org/dromara/mall/domain/bo/PageLinkBo.java
  11. 90 0
      ruoyi-modules/ruoyi-mall/src/main/java/org/dromara/mall/domain/vo/PageCategoryVo.java
  12. 79 0
      ruoyi-modules/ruoyi-mall/src/main/java/org/dromara/mall/domain/vo/PageLinkVo.java
  13. 61 0
      ruoyi-modules/ruoyi-mall/src/main/java/org/dromara/mall/event/DataSyncEvent.java
  14. 70 0
      ruoyi-modules/ruoyi-mall/src/main/java/org/dromara/mall/event/DataSyncListener.java
  15. 43 0
      ruoyi-modules/ruoyi-mall/src/main/java/org/dromara/mall/mapper/PageCategoryMapper.java
  16. 51 0
      ruoyi-modules/ruoyi-mall/src/main/java/org/dromara/mall/mapper/PageLinkMapper.java
  17. 89 0
      ruoyi-modules/ruoyi-mall/src/main/java/org/dromara/mall/service/IPageCategoryService.java
  18. 110 0
      ruoyi-modules/ruoyi-mall/src/main/java/org/dromara/mall/service/IPageLinkService.java
  19. 329 0
      ruoyi-modules/ruoyi-mall/src/main/java/org/dromara/mall/service/impl/PageCategoryServiceImpl.java
  20. 365 0
      ruoyi-modules/ruoyi-mall/src/main/java/org/dromara/mall/service/impl/PageLinkServiceImpl.java
  21. 121 0
      ruoyi-modules/ruoyi-mall/src/main/resources/mapper/mall/PageCategoryMapper.xml
  22. 88 0
      ruoyi-modules/ruoyi-mall/src/main/resources/mapper/mall/PageLinkMapper.xml

+ 3 - 1
ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/interceptor/PlatformDataScopeInterceptor.java

@@ -98,7 +98,9 @@ public class PlatformDataScopeInterceptor implements Interceptor {
         "authorize_type_level",
         "order_return",
         "order_return_item",
-        "customer_business_info"
+        "customer_business_info",
+        "mall_page_link",
+        "mall_page_category"
 
 
         // 注意:前缀匹配需特殊处理(如 qrtz_),见 isIgnoreTable 方法

+ 41 - 9
ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/utils/RedisUtils.java

@@ -225,6 +225,26 @@ public class RedisUtils {
         return rBucket.get();
     }
 
+    /**
+     * 获得缓存的基本对象,如果不存在则调用函数获取并设置缓存。
+     *
+     * @param key        缓存键值
+     * @param function   获取数据的函数
+     * @param expireTime 过期时间(秒)
+     * @return 缓存键值对应的数据
+     */
+    public static <T> T getCacheObject(final String key, final java.util.function.Supplier<T> function, final long expireTime) {
+        T result = getCacheObject(key);
+        if (result == null && function != null) {
+            result = function.get();
+            if (result != null) {
+                setCacheObject(key, result, Duration.ofSeconds(expireTime));
+            }
+        }
+        return result;
+    }
+
+
     /**
      * 获得key剩余存活时间
      *
@@ -236,6 +256,16 @@ public class RedisUtils {
         return rBucket.remainTimeToLive();
     }
 
+    /**
+     * 根据模式删除匹配的所有缓存对象
+     *
+     * @param pattern 模式,支持*和?通配符
+     */
+    public static void deleteObjectPattern(final String pattern) {
+        CLIENT.getKeys().deleteByPattern(pattern);
+    }
+
+
     /**
      * 删除单个对象
      *
@@ -533,29 +563,31 @@ public class RedisUtils {
 
     /**
      * 获得缓存的基本对象列表(全局匹配忽略租户 自行拼接租户id)
-     * <P>
+     * <p>
      * limit-设置扫描的限制数量(默认为0,查询全部)
      * pattern-设置键的匹配模式(默认为null)
      * chunkSize-设置每次扫描的块大小(默认为0,本方法设置为1000)
      * type-设置键的类型(默认为null,查询全部类型)
      * </P>
-     * @see KeysScanOptions
+     *
      * @param pattern 字符串前缀
      * @return 对象列表
+     * @see KeysScanOptions
      */
     public static Collection<String> keys(final String pattern) {
-        return  keys(KeysScanOptions.defaults().pattern(pattern).chunkSize(1000));
+        return keys(KeysScanOptions.defaults().pattern(pattern).chunkSize(1000));
     }
 
     /**
      * 通过扫描参数获取缓存的基本对象列表
+     *
      * @param keysScanOptions 扫描参数
-     * <P>
-     * limit-设置扫描的限制数量(默认为0,查询全部)
-     * pattern-设置键的匹配模式(默认为null)
-     * chunkSize-设置每次扫描的块大小(默认为0)
-     * type-设置键的类型(默认为null,查询全部类型)
-     * </P>
+     *                        <p>
+     *                        limit-设置扫描的限制数量(默认为0,查询全部)
+     *                        pattern-设置键的匹配模式(默认为null)
+     *                        chunkSize-设置每次扫描的块大小(默认为0)
+     *                        type-设置键的类型(默认为null,查询全部类型)
+     *                        </P>
      * @see KeysScanOptions
      */
     public static Collection<String> keys(final KeysScanOptions keysScanOptions) {

+ 45 - 0
ruoyi-modules/ruoyi-mall/src/main/java/org/dromara/mall/config/CacheConstants.java

@@ -0,0 +1,45 @@
+package org.dromara.mall.config;
+
+/**
+ * 缓存常量类
+ *
+ * @author ruoyi
+ */
+public class CacheConstants {
+
+    /**
+     * 页面分类缓存键前缀
+     */
+    public static final String PAGE_CATEGORY_CACHE_KEY = "mall:page:category:";
+
+    /**
+     * 页面链接缓存键前缀
+     */
+    public static final String PAGE_LINK_CACHE_KEY = "mall:page:link:";
+
+    /**
+     * 分类树形结构缓存键
+     */
+    public static final String CATEGORY_TREE_CACHE_KEY = "mall:page:category:tree";
+
+    /**
+     * 分类列表缓存键前缀
+     */
+    public static final String CATEGORY_LIST_CACHE_KEY = "mall:page:category:list:";
+
+    /**
+     * 链接列表缓存键前缀
+     */
+    public static final String LINK_LIST_CACHE_KEY = "mall:page:link:list:";
+
+    /**
+     * 缓存过期时间(秒)
+     */
+    public static final long CACHE_EXPIRE_TIME = 3600;
+
+    /**
+     * 短缓存过期时间(秒)
+     */
+    public static final long SHORT_CACHE_EXPIRE_TIME = 600;
+}
+

+ 83 - 0
ruoyi-modules/ruoyi-mall/src/main/java/org/dromara/mall/config/CacheWarmupRunner.java

@@ -0,0 +1,83 @@
+package org.dromara.mall.config;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.mall.service.IPageCategoryService;
+import org.dromara.mall.service.IPageLinkService;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+/**
+ * 缓存预热运行器
+ * 应用启动时预热页面分类和链接数据到Redis缓存
+ *
+ * @author system
+ */
+@Component
+@RequiredArgsConstructor
+@Slf4j
+@Order(100)
+public class CacheWarmupRunner implements ApplicationRunner {
+
+    private final IPageCategoryService pageCategoryService;
+    private final IPageLinkService pageLinkService;
+
+    @Override
+    public void run(ApplicationArguments args) throws Exception {
+        log.info("开始执行页面分类与链接缓存预热...");
+
+        try {
+            // 预热分类树形结构缓存
+            warmupCategoryTree();
+
+            // 预热顶级分类列表缓存
+            warmupTopCategories();
+
+            // 预热热门分类缓存
+            warmupHotCategories();
+
+            // 预热自定义链接缓存
+            warmupCustomLinks();
+
+            log.info("页面分类与链接缓存预热完成");
+        } catch (Exception e) {
+            log.error("页面分类与链接缓存预热失败", e);
+        }
+    }
+
+    /**
+     * 预热分类树形结构缓存
+     */
+    private void warmupCategoryTree() {
+        log.info("预热分类树形结构缓存...");
+        pageCategoryService.selectCategoryTree(null);
+    }
+
+    /**
+     * 预热顶级分类列表缓存
+     */
+    private void warmupTopCategories() {
+        log.info("预热顶级分类列表缓存...");
+        pageCategoryService.selectByParentId(0L);
+    }
+
+    /**
+     * 预热热门分类缓存
+     */
+    private void warmupHotCategories() {
+        log.info("预热热门分类缓存...");
+        pageCategoryService.selectByType("1"); // 1表示热门分类
+    }
+
+    /**
+     * 预热自定义链接缓存
+     */
+    private void warmupCustomLinks() {
+        log.info("预热自定义链接缓存...");
+        pageLinkService.selectByIsCustom(1); // 1表示自定义链接
+    }
+}
+
+

+ 285 - 0
ruoyi-modules/ruoyi-mall/src/main/java/org/dromara/mall/controller/PageCategoryController.java

@@ -0,0 +1,285 @@
+package org.dromara.mall.controller;
+
+import lombok.RequiredArgsConstructor;
+import jakarta.validation.constraints.*;
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import org.dromara.mall.mapper.PageCategoryMapper;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.validation.annotation.Validated;
+import org.dromara.common.log.annotation.Log;
+import org.dromara.common.web.core.BaseController;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.core.validate.AddGroup;
+import org.dromara.common.core.validate.EditGroup;
+import org.dromara.common.log.enums.BusinessType;
+import org.dromara.mall.domain.vo.PageCategoryVo;
+import org.dromara.mall.domain.bo.PageCategoryBo;
+import org.dromara.mall.service.IPageCategoryService;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 页面分类管理
+ * 前端访问路由地址为:/mall/pageCategory
+ *
+ * @author ruoyi
+ */
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/pageCategory")
+public class PageCategoryController extends BaseController {
+
+    private final IPageCategoryService pageCategoryService;
+    private final PageCategoryMapper baseMapper;
+
+    /**
+     * 获取页面链接分类列表
+     */
+    //@SaCheckPermission("mall:pageCategory:list")
+    @PostMapping("/list")
+    public R<List<PageCategoryVo>> list(PageCategoryBo bo) {
+        List<PageCategoryVo> list = pageCategoryService.queryList(bo);
+        return R.ok(list);
+    }
+
+    /**
+     * 获取分类树形结构(用于前端树形选择器)
+     */
+    //@SaCheckPermission("mall:pageCategory:query")
+    @PostMapping("/treeList")
+    public R<List<PageCategoryVo>> treeList(@RequestBody PageCategoryBo bo) {
+        return R.ok(pageCategoryService.selectCategoryTree(bo));
+    }
+
+    /**
+     * 获取分类树形结构
+     */
+    //@SaCheckPermission("mall:pageCategory:query")
+    @GetMapping("/tree")
+    public R<List<PageCategoryVo>> getCategoryTree() {
+        // 创建一个顶级父节点(id=0,名称为"顶级父类")
+        PageCategoryVo rootNode = new PageCategoryVo();
+        rootNode.setId(0L);
+        rootNode.setName("顶级父类");
+        rootNode.setParentId(-1L); // 设置一个不存在的父ID,表示这是真正的根节点
+        rootNode.setLevel(0);
+
+        // 获取所有分类的扁平列表(不分层)
+        PageCategoryBo bo = new PageCategoryBo();
+        bo.setStatus(1);
+        List<PageCategoryVo> allCategories = pageCategoryService.queryList(bo);
+
+        // 将所有parentId=0的分类作为顶级父节点的直接子节点
+        List<PageCategoryVo> rootChildren = new ArrayList<>();
+
+        // 为每个顶级分类(parentId=0)构建其子树
+        for (PageCategoryVo category : allCategories) {
+            if (category.getParentId() == 0) {
+                // 递归构建子分类树
+                buildCategoryChildren(category, allCategories);
+                rootChildren.add(category);
+            }
+        }
+
+        rootNode.setChildren(rootChildren);
+
+        // 构建返回的树结构,只包含这个顶级父节点
+        List<PageCategoryVo> resultTree = new ArrayList<>();
+        resultTree.add(rootNode);
+
+        return R.ok(resultTree);
+    }
+
+    /**
+     * 递归构建分类的子树
+     */
+    private void buildCategoryChildren(PageCategoryVo parent, List<PageCategoryVo> allCategories) {
+        List<PageCategoryVo> children = new ArrayList<>();
+
+        for (PageCategoryVo category : allCategories) {
+            if (category.getParentId().equals(parent.getId())) {
+                // 递归构建子分类
+                buildCategoryChildren(category, allCategories);
+                children.add(category);
+            }
+        }
+
+        if (!children.isEmpty()) {
+            parent.setChildren(children);
+        }
+    }
+
+    /**
+     * 获取分类详细信息
+     *
+     * @param id 主键
+     */
+    //@SaCheckPermission("mall:pageCategory:query")
+    @GetMapping("/{id}")
+    public R<PageCategoryVo> getInfo(@NotNull(message = "主键不能为空")
+                                     @PathVariable("id") Long id) {
+        return R.ok(baseMapper.selectVoById(id));
+    }
+
+    /**
+     * 根据父级分类ID查询子分类列表
+     *
+     * @param parentId 父级分类ID
+     */
+    //@SaCheckPermission("mall:pageCategory:query")
+    @GetMapping("/children/{parentId}")
+    public R<List<PageCategoryVo>> getChildren(@NotNull(message = "父级分类ID不能为空")
+                                               @PathVariable("parentId") Long parentId) {
+        return R.ok(pageCategoryService.selectByParentId(parentId).stream()
+            .map(baseMapper::selectVoById)
+            .toList());
+    }
+
+    /**
+     * 检查分类是否可作为父分类(排除自身及子分类)
+     *
+     * @param excludeId 排除的分类ID
+     */
+    //@SaCheckPermission("mall:pageCategory:query")
+    @GetMapping("/checkParentValid/{excludeId}")
+    public R<List<PageCategoryVo>> checkParentValid(@PathVariable("excludeId") Long excludeId) {
+        PageCategoryBo bo = new PageCategoryBo();
+        // 这里可以添加排除逻辑,确保不会出现循环引用
+        List<PageCategoryVo> allCategories = pageCategoryService.queryList(bo);
+        // 构建树并排除当前分类及其子分类
+        return R.ok(buildValidParentTree(allCategories, excludeId));
+    }
+
+    /**
+     * 构建有效的父分类树(排除指定分类及其子分类)
+     */
+    private List<PageCategoryVo> buildValidParentTree(List<PageCategoryVo> allCategories, Long excludeId) {
+        // 查找需要排除的所有分类ID(包括子分类)
+        List<Long> excludeIds = new ArrayList<>();
+        if (excludeId != null) {
+            excludeIds.add(excludeId);
+            findAllChildrenIds(allCategories, excludeId, excludeIds);
+        }
+
+        // 过滤掉需要排除的分类
+        List<PageCategoryVo> filteredCategories = allCategories.stream()
+            .filter(cat -> !excludeIds.contains(cat.getId()))
+            .collect(Collectors.toList());
+
+        // 构建树形结构
+        List<PageCategoryVo> result = new ArrayList<>();
+        List<PageCategoryVo> rootCategories = filteredCategories.stream()
+            .filter(cat -> cat.getParentId() == 0)
+            .toList();
+
+        rootCategories.forEach(root -> {
+            buildTreeChildren(root, filteredCategories);
+            result.add(root);
+        });
+
+        return result;
+    }
+
+    /**
+     * 递归查找所有子分类ID
+     */
+    private void findAllChildrenIds(List<PageCategoryVo> allCategories, Long parentId, List<Long> ids) {
+        List<PageCategoryVo> children = allCategories.stream()
+            .filter(cat -> cat.getParentId().equals(parentId))
+            .toList();
+
+        for (PageCategoryVo child : children) {
+            ids.add(child.getId());
+            findAllChildrenIds(allCategories, child.getId(), ids);
+        }
+    }
+
+    /**
+     * 递归构建树的子节点
+     */
+    private void buildTreeChildren(PageCategoryVo parent, List<PageCategoryVo> allCategories) {
+        List<PageCategoryVo> children = allCategories.stream()
+            .filter(cat -> cat.getParentId().equals(parent.getId()))
+            .collect(Collectors.toList());
+
+        if (!children.isEmpty()) {
+            parent.setChildren(children);
+            children.forEach(child -> buildTreeChildren(child, allCategories));
+        }
+    }
+
+    /**
+     * 根据分类类型查询分类列表
+     *
+     * @param type 分类类型
+     */
+    //@SaCheckPermission("mall:pageCategory:query")
+    @GetMapping("/byType/{type}")
+    public R<List<PageCategoryVo>> getByType(@NotNull(message = "分类类型不能为空")
+                                             @PathVariable("type") String type) {
+        return R.ok(pageCategoryService.selectByType(type).stream()
+            .map(baseMapper::selectVoById)
+            .toList());
+    }
+
+    /**
+     * 新增分类
+     */
+    //@SaCheckPermission("mall:pageCategory:add")
+    @Log(title = "分类管理", businessType = BusinessType.INSERT)
+    @PostMapping
+    public R<Void> add(@Validated(AddGroup.class) @RequestBody PageCategoryBo bo) {
+        return toAjax(pageCategoryService.insertByBo(bo));
+    }
+
+    /**
+     * 修改分类
+     */
+    //@SaCheckPermission("mall:pageCategory:edit")
+    @Log(title = "分类管理", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public R<Void> edit(@Validated(EditGroup.class) @RequestBody PageCategoryBo bo) {
+        return toAjax(pageCategoryService.updateByBo(bo));
+    }
+
+    /**
+     * 删除分类
+     *
+     * @param ids 主键串
+     */
+    //@SaCheckPermission("mall:pageCategory:remove")
+    @Log(title = "分类管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public R<Void> remove(@NotEmpty(message = "主键不能为空")
+                          @PathVariable List<Long> ids) {
+        return toAjax(pageCategoryService.deleteWithValidByIds(ids));
+    }
+
+    /**
+     * 检查分类是否有子分类
+     *
+     * @param categoryId 分类ID
+     */
+    //@SaCheckPermission("mall:pageCategory:query")
+    @GetMapping("/hasChild/{categoryId}")
+    public R<Boolean> hasChild(@NotNull(message = "分类ID不能为空")
+                               @PathVariable("categoryId") Long categoryId) {
+        return R.ok(pageCategoryService.hasChildByCategoryId(categoryId));
+    }
+
+    /**
+     * 检查分类是否关联链接
+     *
+     * @param categoryId 分类ID
+     */
+    //@SaCheckPermission("mall:pageCategory:query")
+    @GetMapping("/hasLink/{categoryId}")
+    public R<Boolean> hasLink(@NotNull(message = "分类ID不能为空")
+                              @PathVariable("categoryId") Long categoryId) {
+        return R.ok(pageCategoryService.checkCategoryExistLink(categoryId));
+    }
+}
+

+ 162 - 0
ruoyi-modules/ruoyi-mall/src/main/java/org/dromara/mall/controller/PageLinkController.java

@@ -0,0 +1,162 @@
+package org.dromara.mall.controller;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.core.validate.AddGroup;
+import org.dromara.common.core.validate.EditGroup;
+import org.dromara.common.log.annotation.Log;
+import org.dromara.common.log.enums.BusinessType;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.web.core.BaseController;
+import org.dromara.mall.domain.PageLink;
+import org.dromara.mall.domain.bo.PageLinkBo;
+import org.dromara.mall.domain.vo.PageLinkVo;
+import org.dromara.mall.mapper.PageLinkMapper;
+import org.dromara.mall.service.IPageLinkService;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 页面链接管理
+ * 前端访问路由地址为:/mall/pageLink
+ *
+ * @author ruoyi
+ */
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/pageLink")
+public class PageLinkController extends BaseController {
+
+    private final IPageLinkService pageLinkService;
+    private final PageLinkMapper baseMapper;
+
+    /**
+     * 查询页面链接列表
+     */
+    //@SaCheckPermission("system:pageLink:list")
+    @GetMapping("/list")
+    public TableDataInfo<PageLinkVo> list(PageLinkBo bo, PageQuery pageQuery) {
+        return pageLinkService.queryPageList(bo, pageQuery);
+    }
+
+    /**
+     * 根据分类ID查询链接列表
+     *
+     * @param categoryId 分类ID
+     */
+    //@SaCheckPermission("mall:pageLink:query")
+    @GetMapping("/byCategory/{categoryId}")
+    public R<List<PageLinkVo>> getByCategory(@NotNull(message = "分类ID不能为空")
+                                             @PathVariable("categoryId") Long categoryId) {
+        return R.ok(pageLinkService.selectByCategoryId(categoryId).stream()
+            .map(baseMapper::selectVoById)
+            .toList());
+    }
+
+    /**
+     * 根据是否自定义查询链接列表
+     *
+     * @param isCustom 是否自定义:0-系统预设,1-自定义
+     */
+    //@SaCheckPermission("mall:pageLink:query")
+    @GetMapping("/byIsCustom/{isCustom}")
+    public R<List<PageLinkVo>> getByIsCustom(@NotNull(message = "是否自定义不能为空")
+                                             @PathVariable("isCustom") Integer isCustom) {
+        return R.ok(pageLinkService.selectByIsCustom(isCustom).stream()
+            .map(baseMapper::selectVoById)
+            .toList());
+    }
+
+    /**
+     * 获取链接详细信息
+     *
+     * @param id 主键
+     */
+    //@SaCheckPermission("mall:pageLink:query")
+    @GetMapping("/detail/{id}")
+    public R<PageLinkVo> getDetail(@NotNull(message = "主键不能为空")
+                                   @PathVariable("id") Long id) {
+        return R.ok(pageLinkService.selectLinkDetail(id));
+    }
+
+    /**
+     * 根据链接关键字查询链接
+     *
+     * @param key 链接关键字
+     */
+    //@SaCheckPermission("mall:pageLink:query")
+    @GetMapping("/byKey/{key}")
+    public R<PageLinkVo> getByKey(@NotBlank(message = "链接关键字不能为空")
+                                  @PathVariable("key") String key) {
+        PageLink link = pageLinkService.selectByKey(key);
+        return link != null ? R.ok(baseMapper.selectVoById(link.getId())) : R.ok();
+    }
+
+    /**
+     * 新增链接
+     */
+    //@SaCheckPermission("mall:pageLink:add")
+    @Log(title = "链接管理", businessType = BusinessType.INSERT)
+    @PostMapping
+    public R<Void> add(@Validated(AddGroup.class) @RequestBody PageLinkBo bo) {
+        return toAjax(pageLinkService.insertByBo(bo));
+    }
+
+    /**
+     * 修改链接
+     */
+    //@SaCheckPermission("mall:pageLink:edit")
+    @Log(title = "链接管理", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public R<Void> edit(@Validated(EditGroup.class) @RequestBody PageLinkBo bo) {
+        return toAjax(pageLinkService.updateByBo(bo));
+    }
+
+    /**
+     * 删除链接
+     *
+     * @param ids 主键串
+     */
+    //@SaCheckPermission("mall:pageLink:remove")
+    @Log(title = "链接管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public R<Void> remove(@NotEmpty(message = "主键不能为空")
+                          @PathVariable List<Long> ids) {
+        return toAjax(pageLinkService.deleteWithValidByIds(ids));
+    }
+
+    /**
+     * 批量更新链接状态
+     *
+     * @param ids 主键串
+     * @param status 状态:0-启用,1-禁用
+     */
+//    //@SaCheckPermission("mall:pageLink:edit")
+//    @Log(title = "链接管理", businessType = BusinessType.UPDATE)
+//    @PutMapping("/status/{ids}")
+//    public R<Void> updateStatus(@NotEmpty(message = "主键不能为空")
+//                               @PathVariable List<Long> ids,
+//                               @NotBlank(message = "状态不能为空")
+//                               @RequestParam String status) {
+//        return toAjax(pageLinkService.updateBatchStatus(ids, status));
+//    }
+
+    /**
+     * 批量更新链接排序
+     */
+//    //@SaCheckPermission("mall:pageLink:edit")
+//    @Log(title = "链接管理", businessType = BusinessType.UPDATE)
+//    @PutMapping("/sort")
+//    public R<Void> updateSort(@RequestBody List<PageLink> links) {
+//        return toAjax(pageLinkService.updateBatchSort(links));
+//    }
+}
+
+

+ 78 - 0
ruoyi-modules/ruoyi-mall/src/main/java/org/dromara/mall/domain/PageCategory.java

@@ -0,0 +1,78 @@
+package org.dromara.mall.domain;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.mybatis.core.domain.BaseEntity;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 页面分类对象 c_page_category
+ *
+ * @author ruoyi
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName("mall_page_category")
+public class PageCategory extends BaseEntity {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键ID
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 父分类ID,用于构建树形结构
+     */
+    @TableField(value = "pid")
+    private Long parentId;
+
+    /**
+     * 层级
+     */
+    private Integer level;
+
+    /**
+     * 类型标识
+     */
+    private String type;
+
+    /**
+     * 分类名称
+     */
+    private String name;
+
+    /**
+     * 显示标题
+     */
+    private String title;
+
+    /**
+     * 唯一键值,用于前端组件匹配
+     */
+    private String pageKey;
+
+    /**
+     * 排序
+     */
+    private Integer sort;
+
+    /**
+     * 状态(0正常 1停用)
+     */
+    private Integer status;
+
+    /**
+     * 是否商户 0平台,1商户
+     */
+    private Integer isMer;
+
+}
+

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

@@ -0,0 +1,80 @@
+package org.dromara.mall.domain;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.mybatis.core.domain.BaseEntity;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 页面链接对象 c_page_link
+ *
+ * @author ruoyi
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName("mall_page_link")
+public class PageLink extends BaseEntity {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键ID
+     */
+    @TableId(value = "id")
+    private Long id;
+
+    /**
+     * 分类ID,关联c_page_category表
+     */
+    private Long cateId;
+
+    /**
+     * 类型标识
+     */
+    private Integer type;
+
+    /**
+     * 链接名称
+     */
+    private String name;
+
+    /**
+     * 唯一标识
+     */
+    private String linkKey;
+
+    /**
+     * 路由路径
+     */
+    private String url;
+
+    /**
+     * 是否自定义链接(0否 1是)
+     */
+    private Integer isCustom;
+
+    /**
+     * 状态(0正常 1停用)
+     */
+    private Integer status;
+
+    /**
+     * 排序
+     */
+    private Integer sort;
+
+    /**
+     * 是否商户 0平台,1商户
+     */
+    private Integer isMer;
+
+}
+

+ 80 - 0
ruoyi-modules/ruoyi-mall/src/main/java/org/dromara/mall/domain/bo/PageCategoryBo.java

@@ -0,0 +1,80 @@
+package org.dromara.mall.domain.bo;
+
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.mybatis.core.domain.BaseEntity;
+import org.dromara.mall.domain.PageCategory;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 页面分类业务对象 c_page_category
+ *
+ * @author ruoyi
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@AutoMapper(target = PageCategory.class, reverseConvertGenerate = false)
+public class PageCategoryBo extends BaseEntity {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键ID
+     */
+    private Long id;
+
+    /**
+     * 父级分类ID,0表示一级分类
+     */
+//    @NotNull(message = "父级分类ID不能为空", groups = {AddGroup.class, EditGroup.class})
+    private Long parentId;
+
+    /**
+     * 分类级别:1-一级分类,2-二级分类
+     */
+//    @NotNull(message = "分类级别不能为空", groups = {AddGroup.class, EditGroup.class})
+    private Integer level;
+
+    /**
+     * 分类类型:1-商城页面,2-商品页面,3-文章页面,4-自定义链接
+     */
+//    @NotNull(message = "分类类型不能为空", groups = {AddGroup.class, EditGroup.class})
+    private Integer type;
+
+    /**
+     * 分类名称
+     */
+//    @NotBlank(message = "分类名称不能为空", groups = {AddGroup.class, EditGroup.class})
+    private String name;
+
+    /**
+     * 分类标题(用于SEO)
+     */
+    private String title;
+
+    /**
+     * 分类关键字
+     */
+    private String pageKey;
+
+    /**
+     * 排序值
+     */
+    private Integer sort;
+
+    /**
+     * 状态:0-启用,1-禁用
+     */
+    private Integer status;
+
+    /**
+     * 是否商户 0平台,1商户
+     */
+    private Integer isMer;
+}
+

+ 82 - 0
ruoyi-modules/ruoyi-mall/src/main/java/org/dromara/mall/domain/bo/PageLinkBo.java

@@ -0,0 +1,82 @@
+package org.dromara.mall.domain.bo;
+
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.mybatis.core.domain.BaseEntity;
+import org.dromara.mall.domain.PageLink;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 页面链接业务对象 c_page_link
+ *
+ * @author ruoyi
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@AutoMapper(target = PageLink.class, reverseConvertGenerate = false)
+public class PageLinkBo extends BaseEntity {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键ID
+     */
+    private Long id;
+
+    /**
+     * 分类ID
+     */
+//    @NotNull(message = "分类ID不能为空", groups = {AddGroup.class, EditGroup.class})
+    private Long cateId;
+
+    /**
+     * 类型标识
+     */
+    private Integer type;
+
+    /**
+     * 链接名称
+     */
+//    @NotBlank(message = "链接名称不能为空", groups = {AddGroup.class, EditGroup.class})
+    private String name;
+
+    /**
+     * 链接关键字
+     */
+//    @NotBlank(message = "链接关键字不能为空", groups = {AddGroup.class, EditGroup.class})
+    private String linkKey;
+
+    /**
+     * 链接路径
+     */
+//    @NotBlank(message = "链接路径不能为空", groups = {AddGroup.class, EditGroup.class})
+    private String url;
+
+    /**
+     * 是否自定义链接:0-系统预设,1-自定义
+     */
+//    @NotNull(message = "是否自定义链接不能为空", groups = {AddGroup.class, EditGroup.class})
+    private Integer isCustom;
+
+    /**
+     * 排序值
+     */
+    private Integer sort;
+
+    /**
+     * 状态:0-启用,1-禁用
+     */
+    private Integer status;
+
+    /**
+     * 是否商户 0平台,1商户
+     */
+    private Integer isMer;
+}
+
+

+ 90 - 0
ruoyi-modules/ruoyi-mall/src/main/java/org/dromara/mall/domain/vo/PageCategoryVo.java

@@ -0,0 +1,90 @@
+package org.dromara.mall.domain.vo;
+
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+import org.dromara.mall.domain.PageCategory;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 页面分类视图对象
+ *
+ * @author ruoyi
+ */
+@Data
+@AutoMapper(target = PageCategory.class)
+public class PageCategoryVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键ID
+     */
+    private Long id;
+
+    /**
+     * 父分类ID,用于构建树形结构
+     */
+    private Long parentId;
+
+    /**
+     * 父部门名称
+     */
+    private String parentName;
+
+    /**
+     * 层级
+     */
+    private Integer level;
+
+    /**
+     * 类型标识
+     */
+    private String type;
+
+    /**
+     * 分类名称
+     */
+    private String name;
+
+    /**
+     * 显示标题
+     */
+    private String title;
+
+    /**
+     * 唯一键值,用于前端组件匹配
+     */
+    private String pageKey;
+
+    /**
+     * 排序
+     */
+    private Integer sort;
+
+    /**
+     * 状态(0正常 1停用)
+     */
+    private Integer status;
+
+    /**
+     * 添加时间
+     */
+    private Date createTime;
+
+    /**
+     * 是否商户 0平台,1商户
+     */
+    private Integer isMer;
+
+    /**
+     * 子分类列表
+     */
+    private List<PageCategoryVo> children;
+
+}
+

+ 79 - 0
ruoyi-modules/ruoyi-mall/src/main/java/org/dromara/mall/domain/vo/PageLinkVo.java

@@ -0,0 +1,79 @@
+package org.dromara.mall.domain.vo;
+
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+import org.dromara.mall.domain.PageLink;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 页面链接视图对象
+ *
+ * @author ruoyi
+ */
+@Data
+@AutoMapper(target = PageLink.class)
+public class PageLinkVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键ID
+     */
+    private Long id;
+
+    /**
+     * 分类ID,关联c_page_category表
+     */
+    private Long cateId;
+
+    /**
+     * 类型标识
+     */
+    private Integer type;
+
+    /**
+     * 链接名称
+     */
+    private String name;
+
+    /**
+     * 唯一标识
+     */
+    private String linkKey;
+
+    /**
+     * 路由路径
+     */
+    private String url;
+
+    /**
+     * 是否自定义链接(0否 1是)
+     */
+    private Integer isCustom;
+
+    /**
+     * 状态(0正常 1停用)
+     */
+    private Integer status;
+
+    /**
+     * 排序
+     */
+    private Integer sort;
+
+    /**
+     * 增加时间
+     */
+    private Date createTime;
+
+    /**
+     * 是否商户 0平台,1商户
+     */
+    private Integer isMer;
+
+}
+

+ 61 - 0
ruoyi-modules/ruoyi-mall/src/main/java/org/dromara/mall/event/DataSyncEvent.java

@@ -0,0 +1,61 @@
+package org.dromara.mall.event;
+
+import lombok.Getter;
+import org.springframework.context.ApplicationEvent;
+
+/**
+ * 数据同步事件
+ * 用于在数据变更时通知相关组件进行同步操作
+ *
+ * @author system
+ * @date 2024-01-18
+ */
+@Getter
+public class DataSyncEvent extends ApplicationEvent {
+
+    /**
+     * 数据类型
+     */
+    private final String dataType;
+
+    /**
+     * 操作类型
+     */
+    private final String operation;
+
+    /**
+     * 数据ID
+     */
+    private final Object dataId;
+
+    /**
+     * 构造函数
+     *
+     * @param source    事件源
+     * @param dataType  数据类型
+     * @param operation 操作类型
+     * @param dataId    数据ID
+     */
+    public DataSyncEvent(Object source, String dataType, String operation, Object dataId) {
+        super(source);
+        this.dataType = dataType;
+        this.operation = operation;
+        this.dataId = dataId;
+    }
+
+    /**
+     * 数据类型常量
+     */
+    public static final String DATA_TYPE_PAGE_CATEGORY = "PAGE_CATEGORY";
+    public static final String DATA_TYPE_PAGE_LINK = "PAGE_LINK";
+
+    /**
+     * 操作类型常量
+     */
+    public static final String OPERATION_CREATE = "CREATE";
+    public static final String OPERATION_UPDATE = "UPDATE";
+    public static final String OPERATION_DELETE = "DELETE";
+    public static final String OPERATION_BATCH_DELETE = "BATCH_DELETE";
+    public static final String OPERATION_STATUS_CHANGE = "STATUS_CHANGE";
+    public static final String OPERATION_SORT_CHANGE = "SORT_CHANGE";
+}

+ 70 - 0
ruoyi-modules/ruoyi-mall/src/main/java/org/dromara/mall/event/DataSyncListener.java

@@ -0,0 +1,70 @@
+package org.dromara.mall.event;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.redis.utils.RedisUtils;
+import org.dromara.mall.config.CacheConstants;
+import org.springframework.context.ApplicationListener;
+import org.springframework.stereotype.Component;
+
+/**
+ * 数据同步监听器
+ * 监听数据变更事件,清除相关缓存
+ *
+ * @author system
+ * @date 2024-01-18
+ */
+@Component
+@RequiredArgsConstructor
+@Slf4j
+public class DataSyncListener implements ApplicationListener<DataSyncEvent> {
+
+    @Override
+    public void onApplicationEvent(DataSyncEvent event) {
+        log.info("收到数据同步事件:dataType={}, operation={}, dataId={}",
+            event.getDataType(), event.getOperation(), event.getDataId());
+
+        // 根据数据类型和操作类型清除相关缓存
+        if (DataSyncEvent.DATA_TYPE_PAGE_CATEGORY.equals(event.getDataType())) {
+            clearCategoryCache();
+        } else if (DataSyncEvent.DATA_TYPE_PAGE_LINK.equals(event.getDataType())) {
+            clearLinkCache(event);
+        }
+    }
+
+    /**
+     * 清除分类相关缓存
+     */
+    private void clearCategoryCache() {
+        // 清除树形结构缓存
+        RedisUtils.deleteObject(CacheConstants.CATEGORY_TREE_CACHE_KEY);
+        // 清除分类列表缓存
+        RedisUtils.deleteObjectPattern(CacheConstants.CATEGORY_LIST_CACHE_KEY + "*");
+        log.info("分类缓存已清除");
+    }
+
+    /**
+     * 清除链接相关缓存
+     *
+     * @param event 数据同步事件
+     */
+    private void clearLinkCache(DataSyncEvent event) {
+        // 如果是批量操作或无法确定具体ID,清除所有链接缓存
+        if (DataSyncEvent.OPERATION_BATCH_DELETE.equals(event.getOperation()) ||
+            DataSyncEvent.OPERATION_STATUS_CHANGE.equals(event.getOperation()) ||
+            DataSyncEvent.OPERATION_SORT_CHANGE.equals(event.getOperation())) {
+            // 清除所有链接列表缓存
+            RedisUtils.deleteObjectPattern(CacheConstants.LINK_LIST_CACHE_KEY + "*");
+            // 清除所有链接详情缓存
+            RedisUtils.deleteObjectPattern(CacheConstants.PAGE_LINK_CACHE_KEY + "*");
+        } else if (event.getDataId() != null) {
+            // 清除特定链接的缓存
+            RedisUtils.deleteObject(CacheConstants.PAGE_LINK_CACHE_KEY + event.getDataId());
+            // 对于单个链接的修改,也需要清除相关分类的链接列表缓存
+            // 这里简化处理,清除所有分类的链接列表缓存
+            RedisUtils.deleteObjectPattern(CacheConstants.LINK_LIST_CACHE_KEY + "category:*");
+        }
+        log.info("链接缓存已清除");
+    }
+}
+

+ 43 - 0
ruoyi-modules/ruoyi-mall/src/main/java/org/dromara/mall/mapper/PageCategoryMapper.java

@@ -0,0 +1,43 @@
+package org.dromara.mall.mapper;
+
+import org.apache.ibatis.annotations.Mapper;
+import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
+import org.dromara.mall.domain.PageCategory;
+import org.dromara.mall.domain.vo.PageCategoryVo;
+
+import java.util.List;
+
+/**
+ * 页面分类Mapper接口
+ *
+ * @author ruoyi
+ */
+@Mapper
+public interface PageCategoryMapper extends BaseMapperPlus<PageCategory, PageCategoryVo> {
+
+    /**
+     * 查询分类树结构
+     *
+     * @return 分类树列表
+     */
+    List<PageCategoryVo> selectCategoryTree();
+
+    /**
+     * 根据父ID查询子分类
+     *
+     * @param parentId 父分类ID
+     * @return 子分类列表
+     */
+    List<PageCategory> selectByParentId(Long parentId);
+
+    /**
+     * 根据分类类型查询分类
+     *
+     * @param type 分类类型
+     * @return 分类列表
+     */
+    List<PageCategory> selectByType(String type);
+
+}
+
+

+ 51 - 0
ruoyi-modules/ruoyi-mall/src/main/java/org/dromara/mall/mapper/PageLinkMapper.java

@@ -0,0 +1,51 @@
+package org.dromara.mall.mapper;
+
+import org.apache.ibatis.annotations.Mapper;
+import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
+import org.dromara.mall.domain.PageLink;
+import org.dromara.mall.domain.vo.PageLinkVo;
+
+import java.util.List;
+
+/**
+ * 页面链接Mapper接口
+ *
+ * @author ruoyi
+ */
+@Mapper
+public interface PageLinkMapper extends BaseMapperPlus<PageLink, PageLinkVo> {
+
+    /**
+     * 根据分类ID查询链接列表
+     *
+     * @param categoryId 分类ID
+     * @return 链接列表
+     */
+    List<PageLink> selectByCategoryId(Long categoryId);
+
+    /**
+     * 根据链接类型查询链接列表
+     *
+     * @param isCustom 是否自定义链接
+     * @return 链接列表
+     */
+    List<PageLink> selectByIsCustom(String isCustom);
+
+    /**
+     * 查询链接详情,包含分类信息
+     *
+     * @param id 链接ID
+     * @return 链接详情
+     */
+    PageLinkVo selectLinkDetail(Long id);
+
+    /**
+     * 根据key查询链接
+     *
+     * @param key 唯一标识
+     * @return 链接对象
+     */
+    PageLink selectByKey(String key);
+
+}
+

+ 89 - 0
ruoyi-modules/ruoyi-mall/src/main/java/org/dromara/mall/service/IPageCategoryService.java

@@ -0,0 +1,89 @@
+package org.dromara.mall.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import org.dromara.mall.domain.PageCategory;
+import org.dromara.mall.domain.vo.PageCategoryVo;
+import org.dromara.mall.domain.bo.PageCategoryBo;
+
+import java.util.List;
+
+/**
+ * 页面分类Service接口
+ *
+ * @author ruoyi
+ */
+public interface IPageCategoryService extends IService<PageCategory> {
+
+    /**
+     * 查询分类列表
+     *
+     * @param bo 分类业务对象
+     * @return 分类列表
+     */
+    List<PageCategoryVo> queryList(PageCategoryBo bo);
+
+    /**
+     * 获取分类树形结构
+     *
+     * @return 分类树形结构
+     */
+    List<PageCategoryVo> selectCategoryTree(PageCategoryBo bo);
+
+    /**
+     * 根据父级分类ID查询子分类列表
+     *
+     * @param parentId 父级分类ID
+     * @return 子分类列表
+     */
+    List<PageCategory> selectByParentId(Long parentId);
+
+    /**
+     * 根据分类类型查询分类列表
+     *
+     * @param type 分类类型
+     * @return 分类列表
+     */
+    List<PageCategory> selectByType(String type);
+
+    /**
+     * 新增分类
+     *
+     * @param bo 分类业务对象
+     * @return 结果
+     */
+    Boolean insertByBo(PageCategoryBo bo);
+
+    /**
+     * 修改分类
+     *
+     * @param bo 分类业务对象
+     * @return 结果
+     */
+    Boolean updateByBo(PageCategoryBo bo);
+
+    /**
+     * 批量删除分类
+     *
+     * @param ids 分类ID列表
+     * @return 结果
+     */
+    Boolean deleteWithValidByIds(List<Long> ids);
+
+    /**
+     * 检查分类是否有子分类
+     *
+     * @param categoryId 分类ID
+     * @return 是否有子分类
+     */
+    Boolean hasChildByCategoryId(Long categoryId);
+
+    /**
+     * 检查分类是否关联链接
+     *
+     * @param categoryId 分类ID
+     * @return 是否关联链接
+     */
+    Boolean checkCategoryExistLink(Long categoryId);
+}
+
+

+ 110 - 0
ruoyi-modules/ruoyi-mall/src/main/java/org/dromara/mall/service/IPageLinkService.java

@@ -0,0 +1,110 @@
+package org.dromara.mall.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.mall.domain.PageLink;
+import org.dromara.mall.domain.vo.PageLinkVo;
+import org.dromara.mall.domain.bo.PageLinkBo;
+
+import java.util.List;
+
+/**
+ * 页面链接Service接口
+ *
+ * @author ruoyi
+ */
+public interface IPageLinkService extends IService<PageLink> {
+
+    /**
+     * 分页查询页面链接列表
+     *
+     * @param bo        查询条件
+     * @param pageQuery 分页参数
+     * @return 页面链接分页列表
+     */
+    TableDataInfo<PageLinkVo> queryPageList(PageLinkBo bo, PageQuery pageQuery);
+
+    /**
+     * 根据分类ID查询链接列表
+     *
+     * @param categoryId 分类ID
+     * @return 链接列表
+     */
+    List<PageLink> selectByCategoryId(Long categoryId);
+
+    /**
+     * 根据是否自定义查询链接列表
+     *
+     * @param isCustom 是否自定义:0-系统预设,1-自定义
+     * @return 链接列表
+     */
+    List<PageLink> selectByIsCustom(Integer isCustom);
+
+    /**
+     * 根据ID查询链接详情
+     *
+     * @param id 链接ID
+     * @return 链接详情
+     */
+    PageLinkVo selectLinkDetail(Long id);
+
+    /**
+     * 根据链接关键字查询链接
+     *
+     * @param key 链接关键字
+     * @return 链接信息
+     */
+    PageLink selectByKey(String key);
+
+    /**
+     * 根据分类ID列表查询链接
+     *
+     * @param categoryIds 分类ID列表
+     * @return 链接列表
+     */
+    List<PageLink> selectLinksByCategoryIds(List<Long> categoryIds);
+
+    /**
+     * 新增链接
+     *
+     * @param bo 链接业务对象
+     * @return 结果
+     */
+    Boolean insertByBo(PageLinkBo bo);
+
+    /**
+     * 修改链接
+     *
+     * @param bo 链接业务对象
+     * @return 结果
+     */
+    Boolean updateByBo(PageLinkBo bo);
+
+    /**
+     * 批量删除链接
+     *
+     * @param ids 链接ID列表
+     * @return 结果
+     */
+    Boolean deleteWithValidByIds(List<Long> ids);
+
+    /**
+     * 批量更新链接状态
+     *
+     * @param ids 链接ID列表
+     * @param status 状态:0-启用,1-禁用
+     * @return 结果
+     */
+//    Boolean updateBatchStatus(List<Long> ids, String status);
+
+    /**
+     * 批量更新链接排序
+     *
+     * @param links 链接列表
+     * @return 结果
+     */
+//    Boolean updateBatchSort(List<PageLink> links);
+}
+
+

+ 329 - 0
ruoyi-modules/ruoyi-mall/src/main/java/org/dromara/mall/service/impl/PageCategoryServiceImpl.java

@@ -0,0 +1,329 @@
+package org.dromara.mall.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.exception.base.BaseException;
+import org.dromara.common.core.utils.MapstructUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.redis.utils.RedisUtils;
+import org.dromara.mall.config.CacheConstants;
+import org.dromara.mall.domain.PageCategory;
+import org.dromara.mall.domain.PageLink;
+import org.dromara.mall.domain.bo.PageCategoryBo;
+import org.dromara.mall.domain.vo.PageCategoryVo;
+import org.dromara.mall.event.DataSyncEvent;
+import org.dromara.mall.mapper.PageCategoryMapper;
+import org.dromara.mall.mapper.PageLinkMapper;
+import org.dromara.mall.service.IPageCategoryService;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 页面分类Service业务层处理
+ *
+ * @author ruoyi
+ */
+@Slf4j
+@RequiredArgsConstructor
+@Service
+public class PageCategoryServiceImpl extends ServiceImpl<PageCategoryMapper, PageCategory> implements IPageCategoryService {
+
+    private final PageCategoryMapper baseMapper;
+    private final PageLinkMapper pageLinkMapper;
+    private final ApplicationEventPublisher eventPublisher;
+
+    @Override
+    public List<PageCategoryVo> queryList(PageCategoryBo bo) {
+        LambdaQueryWrapper<PageCategory> lqw = buildQueryWrapper(bo);
+        return baseMapper.selectVoList(lqw);
+    }
+
+    private LambdaQueryWrapper<PageCategory> buildQueryWrapper(PageCategoryBo bo) {
+        // 添加空值检查,防止空指针异常
+        if (bo == null) {
+            return new LambdaQueryWrapper<PageCategory>().orderByAsc(PageCategory::getSort);
+        }
+
+        return new LambdaQueryWrapper<PageCategory>()
+            .like(StringUtils.isNotBlank(bo.getTitle()), PageCategory::getTitle, bo.getTitle())
+            .like(StringUtils.isNotBlank(bo.getPageKey()), PageCategory::getPageKey, bo.getPageKey())
+            .eq(bo.getCreateTime() != null, PageCategory::getCreateTime, bo.getCreateTime())
+            .eq(bo.getParentId() != null && bo.getParentId() > 0, PageCategory::getParentId, bo.getParentId())
+            .eq(bo.getStatus() != null, PageCategory::getStatus, bo.getStatus())
+            .eq(bo.getType() != null, PageCategory::getType, bo.getType())
+            .eq(bo.getIsMer() != null, PageCategory::getIsMer, bo.getIsMer())
+            .like(StringUtils.isNotBlank(bo.getName()), PageCategory::getName, bo.getName())
+            .orderByAsc(PageCategory::getSort);
+    }
+
+    /**
+     * 获取分类树形结构
+     *
+     * @return 分类树形结构
+     */
+    @Override
+    public List<PageCategoryVo> selectCategoryTree(PageCategoryBo bo) {
+        LambdaQueryWrapper<PageCategory> lqw = buildQueryWrapper(bo);
+        List<PageCategoryVo> categoryList = baseMapper.selectVoList(lqw);
+        return buildCategoryTree(categoryList);
+
+    }
+
+    /**
+     * 构建分类树
+     *
+     * @param categoryList 分类列表
+     * @return 分类树
+     */
+    private List<PageCategoryVo> buildCategoryTree(List<PageCategoryVo> categoryList) {
+        List<PageCategoryVo> result = new ArrayList<>();
+        List<PageCategoryVo> mainCategories = categoryList.stream()
+            .filter(category -> category.getParentId() == 0)
+            .toList();
+
+        mainCategories.forEach(category -> {
+            buildChildren(category, categoryList);
+            result.add(category);
+        });
+
+        return result;
+    }
+
+    /**
+     * 递归构建子分类
+     *
+     * @param parent       父分类
+     * @param categoryList 所有分类
+     */
+    private void buildChildren(PageCategoryVo parent, List<PageCategoryVo> categoryList) {
+        List<PageCategoryVo> children = categoryList.stream()
+            .filter(category -> category.getParentId().equals(parent.getId()))
+            .collect(Collectors.toList());
+
+        if (!children.isEmpty()) {
+            parent.setChildren(children);
+            children.forEach(child -> buildChildren(child, categoryList));
+        }
+    }
+
+    /**
+     * 根据父级分类ID查询子分类列表
+     *
+     * @param parentId 父级分类ID
+     * @return 子分类列表
+     */
+    @Override
+    public List<PageCategory> selectByParentId(Long parentId) {
+        String cacheKey = CacheConstants.CATEGORY_LIST_CACHE_KEY + "parent:" + parentId;
+        return RedisUtils.getCacheObject(cacheKey, () -> baseMapper.selectByParentId(parentId), CacheConstants.CACHE_EXPIRE_TIME);
+    }
+
+    /**
+     * 根据分类类型查询分类列表
+     *
+     * @param type 分类类型
+     * @return 分类列表
+     */
+    @Override
+    public List<PageCategory> selectByType(String type) {
+        String cacheKey = CacheConstants.CATEGORY_LIST_CACHE_KEY + "type:" + type;
+        return RedisUtils.getCacheObject(cacheKey, () -> baseMapper.selectByType(type), CacheConstants.CACHE_EXPIRE_TIME);
+    }
+
+    /**
+     * 新增分类
+     *
+     * @param bo 分类业务对象
+     * @return 结果
+     */
+    @Override
+    @Transactional
+    public Boolean insertByBo(PageCategoryBo bo) {
+        PageCategory category = MapstructUtils.convert(bo, PageCategory.class);
+        boolean result = false;
+        if (category != null) {
+            if (StringUtils.isEmpty(String.valueOf(category.getSort()))) {
+                category.setSort(50); //默认排序50
+            }
+            if (category.getStatus() == null) {
+                category.setStatus(0); //默认启用
+            }
+            result = save(category);
+            if (result) {
+                // 发布数据同步事件
+                eventPublisher.publishEvent(new DataSyncEvent(
+                    this,
+                    DataSyncEvent.DATA_TYPE_PAGE_CATEGORY,
+                    DataSyncEvent.OPERATION_CREATE,
+                    category.getId()
+                ));
+                // 清除缓存
+                clearCategoryCache();
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * 修改分类
+     *
+     * @param bo 分类业务对象
+     * @return 结果
+     */
+    @Override
+    @Transactional
+    public Boolean updateByBo(PageCategoryBo bo) {
+        PageCategory category = MapstructUtils.convert(bo, PageCategory.class);
+        boolean result = false;
+        if (category != null) {
+            // 检查是否为子分类修改为自己的子分类
+            if (category.getId().equals(category.getParentId())) {
+                throw new BaseException("不能将分类设置为自己的子分类");
+            }
+            // 检查是否存在循环引用
+            if (checkCircularReference(category.getId(), category.getParentId())) {
+                throw new BaseException("存在循环引用,不能修改分类");
+            }
+            result = updateById(category);
+            if (result) {
+                // 发布数据同步事件
+                eventPublisher.publishEvent(new DataSyncEvent(
+                    this,
+                    DataSyncEvent.DATA_TYPE_PAGE_CATEGORY,
+                    DataSyncEvent.OPERATION_UPDATE,
+                    category.getId()
+                ));
+                // 清除缓存
+                clearCategoryCache();
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * 检查循环引用
+     *
+     * @param categoryId 当前分类ID
+     * @param parentId   父分类ID
+     * @return 是否存在循环引用
+     */
+    private Boolean checkCircularReference(Long categoryId, Long parentId) {
+        if (parentId == 0) {
+            return false;
+        }
+        PageCategory parent = getById(parentId);
+        if (parent == null) {
+            return false;
+        }
+        if (parent.getId().equals(categoryId)) {
+            return true;
+        }
+        return checkCircularReference(categoryId, parent.getParentId());
+    }
+
+    /**
+     * 批量删除分类
+     *
+     * @param ids 分类ID列表
+     * @return 结果
+     */
+    @Override
+    @Transactional
+    public Boolean deleteWithValidByIds(List<Long> ids) {
+        for (Long id : ids) {
+            // 检查是否有子分类
+            if (hasChildByCategoryId(id)) {
+                throw new BaseException("分类下存在子分类,无法删除");
+            }
+            // 检查是否关联链接
+            if (checkCategoryExistLink(id)) {
+                throw new BaseException("分类下存在链接,无法删除");
+            }
+        }
+        boolean result = removeByIds(ids);
+        if (result) {
+            // 发布数据同步事件
+            eventPublisher.publishEvent(new DataSyncEvent(
+                this,
+                DataSyncEvent.DATA_TYPE_PAGE_CATEGORY,
+                DataSyncEvent.OPERATION_BATCH_DELETE,
+                ids
+            ));
+            // 清除缓存
+            clearCategoryCache();
+        }
+        return result;
+    }
+
+    /**
+     * 检查分类是否有子分类
+     *
+     * @param categoryId 分类ID
+     * @return 是否有子分类
+     */
+    @Override
+    public Boolean hasChildByCategoryId(Long categoryId) {
+        LambdaQueryWrapper<PageCategory> lqw = Wrappers.lambdaQuery();
+        lqw.eq(PageCategory::getParentId, categoryId);
+        return baseMapper.exists(lqw);
+    }
+
+    /**
+     * 检查分类是否关联链接
+     *
+     * @param categoryId 分类ID
+     * @return 是否关联链接
+     */
+    @Override
+    public Boolean checkCategoryExistLink(Long categoryId) {
+        LambdaQueryWrapper<PageLink> lqw = Wrappers.lambdaQuery();
+        lqw.eq(PageLink::getCateId, categoryId);
+        return pageLinkMapper.exists(lqw);
+    }
+
+    /**
+     * 清除分类缓存
+     *
+     * @param category 分类对象,为null时清除所有缓存
+     */
+    private void clearCategoryCache(PageCategory category) {
+        if (category == null) {
+            // 清除所有分类缓存
+            RedisUtils.deleteObject(CacheConstants.CATEGORY_TREE_CACHE_KEY);
+            RedisUtils.deleteObjectPattern(CacheConstants.PAGE_CATEGORY_CACHE_KEY + "*");
+            RedisUtils.deleteObjectPattern(CacheConstants.CATEGORY_LIST_CACHE_KEY + "*");
+        } else {
+            // 清除分类详情缓存
+            RedisUtils.deleteObject(CacheConstants.PAGE_CATEGORY_CACHE_KEY + category.getId());
+
+            // 清除分类树形结构缓存
+            RedisUtils.deleteObject(CacheConstants.CATEGORY_TREE_CACHE_KEY);
+
+            // 清除分类列表缓存
+            RedisUtils.deleteObject(CacheConstants.CATEGORY_LIST_CACHE_KEY + "*");
+
+            // 如果有父分类,清除父分类的缓存
+            if (category.getParentId() != null && category.getParentId() > 0) {
+                RedisUtils.deleteObject(CacheConstants.PAGE_CATEGORY_CACHE_KEY + category.getParentId());
+            }
+        }
+    }
+
+    /**
+     * 清除所有分类缓存
+     */
+    private void clearCategoryCache() {
+        clearCategoryCache(null);
+    }
+}
+

+ 365 - 0
ruoyi-modules/ruoyi-mall/src/main/java/org/dromara/mall/service/impl/PageLinkServiceImpl.java

@@ -0,0 +1,365 @@
+package org.dromara.mall.service.impl;
+
+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 lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.exception.base.BaseException;
+import org.dromara.common.core.utils.MapstructUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.redis.utils.RedisUtils;
+import org.dromara.mall.config.CacheConstants;
+import org.dromara.mall.domain.PageLink;
+import org.dromara.mall.domain.vo.PageLinkVo;
+import org.dromara.mall.domain.bo.PageLinkBo;
+import org.dromara.mall.event.DataSyncEvent;
+import org.dromara.mall.mapper.PageLinkMapper;
+import org.dromara.mall.service.IPageLinkService;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * 页面链接Service业务层处理
+ *
+ * @author ruoyi
+ */
+@Slf4j
+@RequiredArgsConstructor
+@Service
+public class PageLinkServiceImpl extends ServiceImpl<PageLinkMapper, PageLink> implements IPageLinkService {
+
+    private final PageLinkMapper baseMapper;
+    private final ApplicationEventPublisher eventPublisher;
+
+    /**
+     * 分页查询页面链接列表
+     *
+     * @param bo        查询条件
+     * @param pageQuery 分页参数
+     * @return 页面链接分页列表
+     */
+    @Override
+    public TableDataInfo<PageLinkVo> queryPageList(PageLinkBo bo, PageQuery pageQuery) {
+        LambdaQueryWrapper<PageLink> lqw = buildQueryWrapper(bo);
+        Page<PageLinkVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
+        return TableDataInfo.build(result);
+    }
+
+    private LambdaQueryWrapper<PageLink> buildQueryWrapper(PageLinkBo bo) {
+        LambdaQueryWrapper<PageLink> lqw = Wrappers.lambdaQuery();
+        lqw.orderByAsc(PageLink::getId);
+        lqw.eq(bo.getCateId() != null, PageLink::getCateId, bo.getCateId());
+        lqw.eq(bo.getType() != null, PageLink::getType, bo.getType());
+        lqw.like(StringUtils.isNotBlank(bo.getName()), PageLink::getName, bo.getName());
+        lqw.eq(StringUtils.isNotBlank(bo.getLinkKey()), PageLink::getLinkKey, bo.getLinkKey());
+        lqw.eq(StringUtils.isNotBlank(bo.getUrl()), PageLink::getUrl, bo.getUrl());
+        lqw.eq(bo.getIsCustom() != null, PageLink::getIsCustom, bo.getIsCustom());
+        lqw.eq(bo.getStatus() != null, PageLink::getStatus, bo.getStatus());
+        lqw.eq(bo.getSort() != null, PageLink::getSort, bo.getSort());
+        lqw.eq(bo.getCreateTime() != null, PageLink::getCreateTime, bo.getCreateTime());
+        lqw.eq(bo.getIsMer() != null, PageLink::getIsMer, bo.getIsMer());
+        return lqw;
+    }
+
+    /**
+     * 根据分类ID查询链接列表
+     *
+     * @param categoryId 分类ID
+     * @return 链接列表
+     */
+    @Override
+    public List<PageLink> selectByCategoryId(Long categoryId) {
+        String cacheKey = CacheConstants.LINK_LIST_CACHE_KEY + "category:" + categoryId;
+        return RedisUtils.getCacheObject(cacheKey, () -> {
+            LambdaQueryWrapper<PageLink> lqw = Wrappers.lambdaQuery();
+            lqw.eq(PageLink::getCateId, categoryId);
+            lqw.orderByAsc(PageLink::getSort);
+            return baseMapper.selectList(lqw);
+        }, CacheConstants.CACHE_EXPIRE_TIME);
+    }
+
+    /**
+     * 根据是否自定义查询链接列表
+     *
+     * @param isCustom 是否自定义:0-系统预设,1-自定义
+     * @return 链接列表
+     */
+    @Override
+    public List<PageLink> selectByIsCustom(Integer isCustom) {
+        String cacheKey = CacheConstants.LINK_LIST_CACHE_KEY + "custom:" + isCustom;
+        return RedisUtils.getCacheObject(cacheKey, () -> {
+            LambdaQueryWrapper<PageLink> lqw = Wrappers.lambdaQuery();
+            lqw.eq(PageLink::getIsCustom, isCustom);
+            lqw.orderByAsc(PageLink::getSort);
+            return baseMapper.selectList(lqw);
+        }, CacheConstants.CACHE_EXPIRE_TIME);
+    }
+
+    /**
+     * 根据ID查询链接详情
+     *
+     * @param id 链接ID
+     * @return 链接详情
+     */
+    @Override
+    public PageLinkVo selectLinkDetail(Long id) {
+        String cacheKey = CacheConstants.PAGE_LINK_CACHE_KEY + id;
+        return baseMapper.selectLinkDetail(id);
+    }
+
+    /**
+     * 根据链接关键字查询链接
+     *
+     * @param key 链接关键字
+     * @return 链接信息
+     */
+    @Override
+    public PageLink selectByKey(String key) {
+        LambdaQueryWrapper<PageLink> lqw = Wrappers.lambdaQuery();
+        lqw.eq(PageLink::getLinkKey, key);
+        return baseMapper.selectOne(lqw);
+    }
+
+    /**
+     * 根据分类ID列表查询链接
+     *
+     * @param categoryIds 分类ID列表
+     * @return 链接列表
+     */
+    @Override
+    public List<PageLink> selectLinksByCategoryIds(List<Long> categoryIds) {
+        if (categoryIds == null || categoryIds.isEmpty()) {
+            return null;
+        }
+        // 简单实现,不缓存多ID查询
+        LambdaQueryWrapper<PageLink> lqw = Wrappers.lambdaQuery();
+        lqw.in(PageLink::getCateId, categoryIds);
+        lqw.orderByAsc(PageLink::getSort);
+        return baseMapper.selectList(lqw);
+    }
+
+    /**
+     * 新增链接
+     *
+     * @param bo 链接业务对象
+     * @return 结果
+     */
+    @Override
+    @Transactional
+    public Boolean insertByBo(PageLinkBo bo) {
+        boolean result = false;
+        // 检查链接关键字是否已存在
+        checkKeyUnique(bo.getLinkKey(), null);
+
+        PageLink link = MapstructUtils.convert(bo, PageLink.class);
+        if (link != null) {
+            if (link.getSort() == null) {
+                link.setSort(50);
+            }
+
+            result = save(link);
+            if (result) {
+                // 发布数据同步事件
+                eventPublisher.publishEvent(new DataSyncEvent(
+                    this,
+                    DataSyncEvent.DATA_TYPE_PAGE_LINK,
+                    DataSyncEvent.OPERATION_CREATE,
+                    link.getId()
+                ));
+                // 清除缓存
+                clearLinkCache(link);
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * 修改链接
+     *
+     * @param bo 链接业务对象
+     * @return 结果
+     */
+    @Override
+    @Transactional
+    public Boolean updateByBo(PageLinkBo bo) {
+        boolean result = false;
+        // 检查链接关键字是否已存在
+        checkKeyUnique(bo.getLinkKey(), bo.getId());
+
+        PageLink link = MapstructUtils.convert(bo, PageLink.class);
+        // 获取旧的链接信息,用于清除缓存
+        if (link != null) {
+            PageLink oldLink = getById(link.getId());
+            result = updateById(link);
+            if (result) {
+                // 发布数据同步事件
+                eventPublisher.publishEvent(new DataSyncEvent(
+                    this,
+                    DataSyncEvent.DATA_TYPE_PAGE_LINK,
+                    DataSyncEvent.OPERATION_UPDATE,
+                    link.getId()
+                ));
+                // 清除新旧链接的缓存
+                if (oldLink != null) {
+                    clearLinkCache(oldLink);
+                }
+                clearLinkCache(link);
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * 检查链接关键字是否唯一
+     *
+     * @param key 链接关键字
+     * @param id  链接ID,用于更新时排除自身
+     */
+    private void checkKeyUnique(String key, Long id) {
+        LambdaQueryWrapper<PageLink> lqw = Wrappers.lambdaQuery();
+        lqw.eq(PageLink::getLinkKey, key);
+        if (id != null) {
+            lqw.ne(PageLink::getId, id);
+        }
+        if (baseMapper.exists(lqw)) {
+            throw new BaseException("链接关键字已存在,请更换");
+        }
+    }
+
+    /**
+     * 批量删除链接
+     *
+     * @param ids 链接ID列表
+     * @return 结果
+     */
+    @Override
+    @Transactional
+    public Boolean deleteWithValidByIds(List<Long> ids) {
+        // 检查是否有系统预设的链接
+        LambdaQueryWrapper<PageLink> lqw = Wrappers.lambdaQuery();
+        lqw.in(PageLink::getId, ids);
+        lqw.eq(PageLink::getIsCustom, 0);
+        List<PageLink> systemLinks = baseMapper.selectList(lqw);
+        if (!systemLinks.isEmpty()) {
+            throw new BaseException("包含系统预设链接,无法删除");
+        }
+        // 获取要删除的链接信息,用于清除缓存
+        List<PageLink> links = listByIds(ids);
+        boolean result = removeByIds(ids);
+        if (result && links != null) {
+            // 发布数据同步事件
+            eventPublisher.publishEvent(new DataSyncEvent(
+                this,
+                DataSyncEvent.DATA_TYPE_PAGE_LINK,
+                DataSyncEvent.OPERATION_BATCH_DELETE,
+                ids
+            ));
+            // 清除缓存
+            for (PageLink link : links) {
+                clearLinkCache(link);
+            }
+        }
+        return result;
+    }
+
+    /**
+     * 批量更新链接状态
+     *
+     * @param ids 链接ID列表
+     * @param status 状态:0-启用,1-禁用
+     * @return 结果
+     */
+//    @Override
+//    @Transactional
+//    public Boolean updateBatchStatus(List<Long> ids, String status) {
+//        // 获取要更新的链接信息,用于清除缓存
+//        List<PageLink> links = listByIds(ids);
+//        boolean result = baseMapper.updateBatchStatus(ids, status) > 0;
+//        if (result && links != null) {
+//            // 发布数据同步事件
+//            eventPublisher.publishEvent(new DataSyncEvent(
+//                    this,
+//                    DataSyncEvent.DATA_TYPE_PAGE_LINK,
+//                    DataSyncEvent.OPERATION_STATUS_CHANGE,
+//                    ids
+//            ));
+//            // 清除缓存
+//            for (PageLink link : links) {
+//                clearLinkCache(link);
+//            }
+//        }
+//        return result;
+//    }
+
+    /**
+     * 批量更新链接排序
+     *
+     * @param links 链接列表
+     * @return 结果
+     */
+//    @Override
+//    @Transactional
+//    public Boolean updateBatchSort(List<PageLink> links) {
+//        if (links.isEmpty()) {
+//            return true;
+//        }
+//        // 验证分类一致性
+//        Map<Long, List<PageLink>> categoryMap = links.stream()
+//                .collect(Collectors.groupingBy(PageLink::getCategoryId));
+//        if (categoryMap.size() > 1) {
+//            throw new BaseException("批量排序只能处理同一分类下的链接");
+//        }
+//        boolean result = baseMapper.updateBatchSort(links) > 0;
+//        if (result) {
+//            // 发布数据同步事件
+//            eventPublisher.publishEvent(new DataSyncEvent(
+//                    this,
+//                    DataSyncEvent.DATA_TYPE_PAGE_LINK,
+//                    DataSyncEvent.OPERATION_SORT_CHANGE,
+//                    links.stream().map(PageLink::getId).collect(Collectors.toList())
+//            ));
+//            // 清除缓存
+//            for (PageLink link : links) {
+//                clearLinkCache(link);
+//            }
+//        }
+//        return result;
+//    }
+
+    /**
+     * 清除链接缓存
+     *
+     * @param link 链接对象
+     */
+    private void clearLinkCache(PageLink link) {
+        if (link == null) {
+            return;
+        }
+
+        // 清除链接详情缓存
+        RedisUtils.deleteObject(CacheConstants.PAGE_LINK_CACHE_KEY + link.getId());
+
+        // 清除分类链接列表缓存
+        RedisUtils.deleteObject(CacheConstants.LINK_LIST_CACHE_KEY + "category:" + link.getCateId());
+
+        // 清除自定义链接列表缓存
+        RedisUtils.deleteObject(CacheConstants.LINK_LIST_CACHE_KEY + "custom:" + link.getIsCustom());
+
+        // 清除关键字缓存
+        if (StringUtils.isNotEmpty(link.getLinkKey())) {
+            RedisUtils.deleteObject(CacheConstants.PAGE_LINK_CACHE_KEY + link.getLinkKey());
+        }
+    }
+}
+

+ 121 - 0
ruoyi-modules/ruoyi-mall/src/main/resources/mapper/mall/PageCategoryMapper.xml

@@ -0,0 +1,121 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="org.dromara.mall.mapper.PageCategoryMapper">
+
+    <resultMap type="org.dromara.mall.domain.vo.PageCategoryVo" id="PageCategoryVoResult">
+        <id property="id" column="id"/>
+        <result property="parentId" column="pid"/>
+        <result property="level" column="level"/>
+        <result property="type" column="type"/>
+        <result property="name" column="name"/>
+        <result property="title" column="title"/>
+        <result property="pageKey" column="page_key"/>
+        <result property="sort" column="sort"/>
+        <result property="status" column="status"/>
+        <result property="createTime" column="create_time"/>
+        <result property="isMer" column="is_mer"/>
+        <!-- 支持递归查询子分类 -->
+        <collection property="children" ofType="org.dromara.mall.domain.vo.PageCategoryVo"
+                    select="selectCategoryTreeByParentId" column="id"/>
+    </resultMap>
+
+    <!-- 查询分类树结构(顶级分类) -->
+    <select id="selectCategoryTree" resultMap="PageCategoryVoResult">
+        SELECT *
+        FROM mall_page_category
+        WHERE pid = 0
+          AND status = '0'
+        ORDER BY sort ASC
+    </select>
+
+    <!-- 根据父ID查询子分类树 -->
+    <select id="selectCategoryTreeByParentId" resultMap="PageCategoryVoResult">
+        SELECT *
+        FROM mall_page_category
+        WHERE pid = #{parentId}
+          AND status = '0'
+        ORDER BY sort ASC
+    </select>
+
+    <!-- 根据父ID查询子分类(不限制状态) -->
+    <select id="selectByParentId" resultType="org.dromara.mall.domain.PageCategory">
+        SELECT *
+        FROM mall_page_category
+        WHERE pid = #{parentId}
+        ORDER BY sort ASC
+    </select>
+
+    <!-- 根据分类类型查询分类 -->
+    <select id="selectByType" resultType="org.dromara.mall.domain.PageCategory">
+        SELECT *
+        FROM mall_page_category
+        WHERE type = #{type}
+        ORDER BY sort ASC
+    </select>
+
+    <!-- 批量更新 -->
+    <update id="updateBatch">
+        UPDATE mall_page_category
+        SET
+        name = CASE id
+        <foreach collection="list" item="item" separator="">
+            WHEN #{item.id} THEN #{item.name}
+        </foreach>
+        ELSE name
+        END,
+        title = CASE id
+        <foreach collection="list" item="item" separator="">
+            WHEN #{item.id} THEN #{item.title}
+        </foreach>
+        ELSE title
+        END,
+        sort = CASE id
+        <foreach collection="list" item="item" separator="">
+            WHEN #{item.id} THEN #{item.sort}
+        </foreach>
+        ELSE sort
+        END,
+        status = CASE id
+        <foreach collection="list" item="item" separator="">
+            WHEN #{item.id} THEN #{item.status}
+        </foreach>
+        ELSE status
+        END
+        WHERE id IN
+        <foreach collection="list" item="item" open="(" separator="," close=")">
+            #{item.id}
+        </foreach>
+    </update>
+
+    <!-- 查询指定分类的所有子分类ID(递归查询) -->
+    <select id="selectAllChildrenIds" resultType="java.lang.Long">
+        WITH RECURSIVE cte AS (SELECT id
+                               FROM mall_page_category
+                               WHERE pid = #{categoryId}
+                               UNION ALL
+                               SELECT c.id
+                               FROM mall_page_category c
+                                        INNER JOIN cte ON c.pid = cte.id)
+        SELECT id
+        FROM cte
+    </select>
+
+    <!-- 检查分类是否可作为父分类(排除自身及子分类) -->
+    <select id="selectValidParentTree" resultMap="PageCategoryVoResult">
+        WITH RECURSIVE invalid_ids AS (SELECT id
+                                       FROM mall_page_category
+                                       WHERE id = #{excludeId}
+                                       UNION ALL
+                                       SELECT c.id
+                                       FROM mall_page_category c
+                                                INNER JOIN invalid_ids ON c.pid = invalid_ids.id)
+        SELECT *
+        FROM mall_page_category
+        WHERE id NOT IN (SELECT id FROM invalid_ids)
+          AND pid = 0
+          AND status = '0'
+        ORDER BY sort ASC
+    </select>
+</mapper>

+ 88 - 0
ruoyi-modules/ruoyi-mall/src/main/resources/mapper/mall/PageLinkMapper.xml

@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="org.dromara.mall.mapper.PageLinkMapper">
+
+    <resultMap type="org.dromara.mall.domain.vo.PageLinkVo" id="PageLinkVoResult">
+        <id property="id" column="id"/>
+        <result property="cateId" column="cate_id"/>
+        <result property="type" column="type"/>
+        <result property="name" column="name"/>
+        <result property="linkKey" column="link_key"/>
+        <result property="url" column="url"/>
+        <result property="isCustom" column="is_custom"/>
+        <result property="status" column="status"/>
+        <result property="sort" column="sort"/>
+        <result property="createTime" column="create_time"/>
+        <result property="isMer" column="is_mer"/>
+    </resultMap>
+
+    <select id="selectByCategoryId" resultType="org.dromara.mall.domain.PageLink">
+        SELECT *
+        FROM mall_page_link
+        WHERE cate_id = #{categoryId}
+          AND status = 0
+        ORDER BY sort ASC
+    </select>
+
+    <select id="selectByIsCustom" resultType="org.dromara.mall.domain.PageLink">
+        SELECT *
+        FROM mall_page_link
+        WHERE is_custom = #{isCustom}
+          AND status = 0
+        ORDER BY sort ASC
+    </select>
+
+    <select id="selectLinkDetail" resultMap="PageLinkVoResult">
+        SELECT l.*,
+               c.name AS category_name
+        FROM mall_page_link l
+                 LEFT JOIN mall_page_category c ON l.cate_id = c.id
+        WHERE l.id = #{id}
+    </select>
+
+    <select id="selectByKey" resultType="org.dromara.mall.domain.PageLink">
+        SELECT *
+        FROM mall_page_link
+        WHERE link_key = #{key}
+    </select>
+
+    <!-- 查询分类下的所有链接(包含子分类) -->
+    <select id="selectLinksByCategoryIds" resultType="org.dromara.mall.domain.PageLink">
+        SELECT * FROM mall_page_link
+        WHERE cate_id IN
+        <foreach collection="categoryIds" item="id" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+        AND status = 0
+        ORDER BY sort ASC
+    </select>
+
+    <!-- 批量更新链接状态 -->
+    <update id="updateBatchStatus">
+        UPDATE mall_page_link
+        SET status = #{status}
+        WHERE id IN
+        <foreach collection="ids" item="id" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </update>
+
+    <!-- 批量更新排序 -->
+    <update id="updateBatchSort">
+        UPDATE mall_page_link
+        SET
+        sort = CASE id
+        <foreach collection="list" item="item" separator="">
+            WHEN #{item.id} THEN #{item.sort}
+        </foreach>
+        ELSE sort
+        END
+        WHERE id IN
+        <foreach collection="list" item="item" open="(" separator="," close=")">
+            #{item.id}
+        </foreach>
+    </update>
+</mapper>
+