用若依框架快速开发一个CMS栏目管理模块(SpringBoot+Vue3实战)
若依框架实战:从零构建CMS栏目管理模块的全栈指南
在当今快节奏的软件开发领域,能够快速构建稳定、可扩展的业务系统是每个开发团队的刚需。若依(RuoYi)作为一款基于SpringBoot和Vue.js的前后端分离框架,凭借其丰富的功能组件和优雅的架构设计,已经成为众多企业级应用开发的首选。本文将带您深入探索如何利用若依框架高效开发一个完整的CMS栏目管理模块,涵盖从数据库设计到前后端联调的完整流程。
1. 环境准备与项目初始化
在开始编码之前,我们需要确保开发环境配置正确。若依框架对开发工具的要求相对灵活,您可以选择IntelliJ IDEA或Eclipse作为Java开发环境,VS Code或WebStorm作为前端开发工具。
关键环境依赖:
- JDK 1.8+
- Maven 3.5+
- Node.js 12+
- Redis 5.0+
项目初始化步骤:
- 从若依官方GitHub仓库克隆最新版本代码
- 使用Maven导入后端项目,重点关注以下核心模块:
ruoyi-admin:SpringBoot启动模块ruoyi-system:系统核心模块
- 前端项目位于
ruoyi-ui目录,使用npm安装依赖:npm install --registry=https://registry.npmmirror.com
提示:若依框架默认使用Redis作为缓存和会话存储,启动前请确保Redis服务已运行。
2. 数据库设计与实体建模
CMS栏目管理作为内容系统的核心模块,其数据结构设计直接影响系统的扩展性和性能。我们采用以下表结构设计:
CREATE TABLE `cms_channel` ( `id` varchar(32) NOT NULL COMMENT '栏目ID', `channel_name` varchar(100) NOT NULL COMMENT '栏目名称', `is_show` tinyint(1) DEFAULT '1' COMMENT '是否显示(0:隐藏 1:显示)', `create_by` varchar(64) DEFAULT '' COMMENT '创建者', `create_time` datetime DEFAULT NULL COMMENT '创建时间', `update_by` varchar(64) DEFAULT '' COMMENT '更新者', `update_time` datetime DEFAULT NULL COMMENT '更新时间', `remark` varchar(500) DEFAULT NULL COMMENT '备注', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CMS栏目表';对应的Java实体类继承若依提供的BaseEntity基类,自动获得创建时间、更新时间等公共字段:
public class Channel extends BaseEntity { private static final long serialVersionUID = 1L; private String id; private String channelName; private Integer isShow; // 省略getter/setter方法 }设计要点:
- 使用UUID而非自增ID,便于分布式环境部署
- 继承
BaseEntity获得审计字段支持 - 字段命名遵循若依规范,数据库使用下划线风格
3. 后端CRUD接口实现
若依框架深度集成了MyBatis-Plus,可以大幅简化数据持久层开发。我们按照三层架构实现栏目管理功能。
3.1 Mapper层实现
首先创建ChannelMapper接口,定义基本的CRUD操作:
public interface ChannelMapper { int insertChannel(Channel channel); int deleteChannelById(String id); int updateChannel(Channel channel); Channel selectChannelById(String id); List<Channel> selectChannelList(Channel channel); }对应的XML映射文件放置在resources/mapper/system/basic目录下:
<mapper namespace="com.ruoyi.system.mapper.ChannelMapper"> <resultMap id="ChannelResult" type="Channel"> <result property="id" column="id"/> <result property="channelName" column="channel_name"/> <result property="isShow" column="is_show"/> </resultMap> <sql id="selectChannelVo"> select id, channel_name, is_show, create_by, create_time from cms_channel </sql> <select id="selectChannelList" parameterType="Channel" resultMap="ChannelResult"> <include refid="selectChannelVo"/> <where> <if test="channelName != null and channelName != ''"> AND channel_name like concat('%', #{channelName}, '%') </if> <if test="isShow != null"> AND is_show = #{isShow} </if> </where> </select> </mapper>3.2 Service层实现
Service接口定义业务契约:
public interface IChannelService { int insertChannel(Channel channel); int deleteChannelById(String id); int updateChannel(Channel channel); Channel selectChannelById(String id); List<Channel> selectChannelList(Channel channel); }Service实现类中,我们利用若依提供的工具类简化开发:
@Service public class ChannelServiceImpl implements IChannelService { @Autowired private ChannelMapper channelMapper; @Override public int insertChannel(Channel channel) { channel.setId(IdUtils.fastUUID()); channel.setCreateBy(SecurityUtils.getUsername()); return channelMapper.insertChannel(channel); } @Override public List<Channel> selectChannelList(Channel channel) { return channelMapper.selectChannelList(channel); } // 其他方法实现... }关键点说明:
IdUtils.fastUUID()生成高性能UUIDSecurityUtils获取当前操作用户信息- 业务异常统一使用若依的
ServiceException
3.3 Controller层实现
控制器层继承若依的BaseController,获得统一响应处理能力:
@RestController @RequestMapping("/cms/channel") public class ChannelController extends BaseController { @Autowired private IChannelService channelService; @GetMapping("/list") public TableDataInfo list(Channel channel) { startPage(); List<Channel> list = channelService.selectChannelList(channel); return getDataTable(list); } @PostMapping public AjaxResult add(@Validated @RequestBody Channel channel) { return toAjax(channelService.insertChannel(channel)); } // 其他RESTful接口... }RESTful接口设计规范:
- GET
/cms/channel/list- 分页查询栏目列表 - POST
/cms/channel- 新增栏目 - PUT
/cms/channel- 修改栏目 - DELETE
/cms/channel/{id}- 删除栏目
4. 前端Vue3实现
若依前端采用Vue3+Element Plus技术栈,我们按照模块化方式实现栏目管理界面。
4.1 API接口封装
在src/api/cms目录下创建channel.js,封装后端接口调用:
import request from '@/utils/request' export function listChannel(query) { return request({ url: '/cms/channel/list', method: 'get', params: query }) } export function addChannel(data) { return request({ url: '/cms/channel', method: 'post', data: data }) } // 其他接口方法...4.2 页面组件开发
创建栏目管理页面src/views/cms/channel/index.vue:
<template> <div class="app-container"> <el-card shadow="never"> <div class="toolbar"> <el-button type="primary" @click="handleAdd">新增栏目</el-button> </div> <el-table v-loading="loading" :data="list" row-key="id" border > <el-table-column prop="channelName" label="栏目名称" /> <el-table-column prop="isShow" label="状态"> <template #default="{row}"> <el-tag :type="row.isShow ? 'success' : 'danger'"> {{ row.isShow ? '显示' : '隐藏' }} </el-tag> </template> </el-table-column> <el-table-column label="操作" width="200"> <template #default="{row}"> <el-button size="small" @click="handleEdit(row)">编辑</el-button> <el-button size="small" type="danger" @click="handleDelete(row)">删除</el-button> </template> </el-table-column> </el-table> <pagination v-show="total>0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" /> </el-card> <!-- 新增/编辑对话框 --> <channel-form ref="formRef" @success="getList" /> </div> </template> <script setup> import { listChannel, deleteChannel } from '@/api/cms/channel' import ChannelForm from './components/ChannelForm.vue' const state = reactive({ list: [], loading: false, total: 0, queryParams: { pageNum: 1, pageSize: 10, channelName: undefined, isShow: undefined } }) const { proxy } = getCurrentInstance() function getList() { state.loading = true listChannel(state.queryParams).then(response => { state.list = response.rows state.total = response.total state.loading = false }) } function handleAdd() { proxy.$refs.formRef.show() } // 其他方法... </script>4.3 表单组件开发
创建表单组件src/views/cms/channel/components/ChannelForm.vue:
<template> <el-dialog :title="title" v-model="visible" width="500px"> <el-form ref="formRef" :model="form" :rules="rules" label-width="80px"> <el-form-item label="栏目名称" prop="channelName"> <el-input v-model="form.channelName" placeholder="请输入栏目名称" /> </el-form-item> <el-form-item label="显示状态" prop="isShow"> <el-radio-group v-model="form.isShow"> <el-radio :label="1">显示</el-radio> <el-radio :label="0">隐藏</el-radio> </el-radio-group> </el-form-item> </el-form> <template #footer> <el-button @click="visible = false">取 消</el-button> <el-button type="primary" @click="submitForm">确 定</el-button> </template> </el-dialog> </template> <script setup> import { addChannel, updateChannel } from '@/api/cms/channel' const emit = defineEmits(['success']) const state = reactive({ visible: false, title: '', form: { id: undefined, channelName: '', isShow: 1 }, rules: { channelName: [ { required: true, message: '栏目名称不能为空', trigger: 'blur' } ] } }) function show(data) { state.visible = true if (data) { state.title = '修改栏目' state.form = Object.assign({}, data) } else { state.title = '新增栏目' state.form.id = undefined } } function submitForm() { proxy.$refs.formRef.validate(valid => { if (valid) { if (state.form.id) { updateChannel(state.form).then(() => { proxy.$modal.msgSuccess('修改成功') state.visible = false emit('success') }) } else { addChannel(state.form).then(() => { proxy.$modal.msgSuccess('新增成功') state.visible = false emit('success') }) } } }) } defineExpose({ show }) </script>5. 权限控制与系统集成
若依框架提供了完善的权限控制机制,我们需要为栏目管理模块配置适当的权限。
5.1 菜单配置
在系统管理→菜单管理中新增以下菜单项:
| 菜单名称 | 权限标识 | 路径 | 组件 | 类型 |
|---|---|---|---|---|
| 栏目管理 | cms:channel:list | /cms/channel | cms/channel/index | 目录 |
| 栏目查询 | cms:channel:query | 按钮 | ||
| 栏目新增 | cms:channel:add | 按钮 | ||
| 栏目编辑 | cms:channel:edit | 按钮 | ||
| 栏目删除 | cms:channel:remove | 按钮 |
5.2 按钮权限控制
在前端页面中,使用v-hasPermi指令控制按钮显示:
<el-button v-hasPermi="['cms:channel:add']" type="primary" @click="handleAdd" > 新增栏目 </el-button>5.3 后端权限校验
在Controller方法上添加@PreAuthorize注解:
@PreAuthorize("@ss.hasPermi('cms:channel:add')") @PostMapping public AjaxResult add(@Validated @RequestBody Channel channel) { return toAjax(channelService.insertChannel(channel)); }6. 常见问题与优化建议
在实际开发过程中,可能会遇到以下典型问题:
1. 分页查询失效
若依的分页插件需要确保startPage()方法与查询语句紧邻:
startPage(); // 这行必须紧跟在查询方法前 List<Channel> list = channelService.selectChannelList(channel);2. 事务管理
对于需要事务管理的业务方法,添加@Transactional注解:
@Transactional @Override public int deleteChannelById(String id) { // 先删除关联数据 articleService.deleteByChannelId(id); // 再删除栏目 return channelMapper.deleteChannelById(id); }3. 前端性能优化
对于大型数据列表,建议启用虚拟滚动:
<el-table v-loading="loading" :data="list" style="width: 100%" height="calc(100vh - 250px)" row-key="id" border lazy :load="loadChildren" :tree-props="{children: 'children', hasChildren: 'hasChildren'}" > <!-- 列定义 --> </el-table>4. 接口安全增强
敏感接口建议增加防重复提交和XSS过滤:
@RepeatSubmit @PreAuthorize("@ss.hasPermi('cms:channel:add')") @PostMapping public AjaxResult add(@Validated @RequestBody Channel channel) { channel.setChannelName(HtmlUtils.htmlEscape(channel.getChannelName())); return toAjax(channelService.insertChannel(channel)); }7. 扩展功能实现
一个完整的CMS栏目管理系统通常还需要以下增强功能:
7.1 栏目树形展示
修改Mapper查询,支持树形结构:
<select id="selectChannelTree" resultMap="ChannelResult"> <include refid="selectChannelVo"/> where parent_id = #{parentId} order by order_num </select>前端使用Element Plus的树形表格:
<el-table :data="list" row-key="id" border lazy :load="loadChildren" :tree-props="{children: 'children', hasChildren: 'hasChildren'}" > <el-table-column prop="channelName" label="栏目名称" /> </el-table>7.2 栏目排序功能
数据库添加排序字段:
ALTER TABLE `cms_channel` ADD COLUMN `order_num` int(4) DEFAULT 0 COMMENT '显示顺序';前端实现拖拽排序:
<el-table :data="list" row-key="id" border @row-dragend="handleDragEnd" > <el-table-column label="排序" width="80"> <template #default> <el-icon class="drag-handle"><el-icon-rank /></el-icon> </template> </el-table-column> </el-table>7.3 栏目模板关联
扩展栏目表,增加模板字段:
ALTER TABLE `cms_channel` ADD COLUMN `template_id` varchar(32) DEFAULT NULL COMMENT '模板ID';在栏目表单中添加模板选择器:
<el-form-item label="栏目模板" prop="templateId"> <el-select v-model="form.templateId" placeholder="请选择模板"> <el-option v-for="item in templateOptions" :key="item.id" :label="item.name" :value="item.id" /> </el-select> </el-form-item>8. 部署与监控
完成开发后,我们需要将系统部署到生产环境并设置适当的监控。
8.1 应用打包
后端打包:
mvn clean package -DskipTests前端打包:
npm run build:prod8.2 部署配置
若依支持多种部署方式,推荐使用Docker容器化部署:
FROM openjdk:8-jdk COPY target/ruoyi-admin.jar /app.jar ENTRYPOINT ["java","-jar","/app.jar"]8.3 性能监控
集成Spring Boot Actuator监控端点:
# application-prod.properties management.endpoints.web.exposure.include=health,info,metrics management.metrics.tags.application=${spring.application.name}配置Prometheus监控:
<dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> </dependency>9. 项目总结与经验分享
在实际项目中采用若依框架开发CMS栏目管理模块,最大的优势在于其开箱即用的基础功能和规范的代码结构。通过本次实践,我们验证了以下几点:
- 开发效率提升:相比从零开始搭建,使用若依框架节省了约60%的基础代码编写时间
- 统一规范:框架强制执行的编码规范使团队协作更加顺畅
- 可扩展性:清晰的模块划分使得后续功能扩展非常方便
遇到的典型问题及解决方案:
问题1:前端打包后路由失效
解决:检查vue.config.js中的publicPath配置,确保与部署环境匹配问题2:MyBatis-Plus分页失效
解决:确保在Mapper方法调用前调用startPage(),且中间不能有其他SQL查询问题3:权限注解不生效
解决:检查@EnableGlobalMethodSecurity注解是否启用,并确认权限字符串格式正确
对于需要进一步定制开发的团队,建议:
- 仔细阅读若依官方文档,理解框架设计思想
- 在修改核心组件前,先考虑通过扩展方式实现需求
- 定期同步官方更新,获取最新的安全补丁和功能增强
