简介:这是一套基于Ruoyi框架开发的前后端分离内容管理系统(CMS)完整源码包,面向Java后端开发者、Vue前端工程师及全栈学习者,解决中小型网站内容建站、快速二次开发与系统管理落地难题。资源包含2个核心文件:1个ZIP压缩包(含完整前后端源码,前端基于Vue.js构建,后端基于Spring Boot+MyBatis,集成Ruoyi权限与基础模块)和1个SQL脚本(适配MySQL,含初始化表结构与基础数据),整体包体17.47MB,结构精简、开箱即用。已有159人下载学习,适合希望深入理解Ruoyi架构、实践前后端分离开发流程、掌握CMS核心功能(内容发布、字段自定义、多角色权限控制)的学习者。源码已通过基础安全加固(防XSS/SQL注入)、支持HTTPS部署,并预留SEO、第三方登录等扩展接口,便于教学演示、课程设计或企业级轻量建站项目快速启动。
1. 这不是又一个“拿来即用”的CMS模板:RuoYi 前后端分离内容管理系统,本质是套可深度定制的「企业级脚手架」
你搜到的这个标题——“基于 RuoYi 开发的前后端分离内容管理系统源码 + 数据库”——表面看是个成品 CMS,但实际它根本不是开箱即用的 WordPress 替代品。它是一套以 RuoYi 框架为基底、按典型内容管理业务域(栏目/文章/富文本/附件/权限分级)完成领域建模与接口对齐的完整工程实例。真正价值不在“能发文章”,而在它把 RuoYi 的权限控制(RBAC+数据权限)、代码生成器适配逻辑、前端路由与菜单动态加载、文件上传与预览链路、以及 MySQL 表结构设计(含字段注释、索引策略、软删除标记)全部跑通并固化成可复用模式。适合两类人:一是刚学完 Spring Boot + Vue 想落地真实项目的新手,需要一个比若依官方 demo 更贴近业务、比博客系统更重权限管控的练手靶场;二是中小团队技术负责人,想快速搭建内部知识库、产品文档中心或客户门户后台,不希望从零写登录鉴权和菜单管理。它不解决高并发、多租户或 SaaS 化,但把单体企业级 CMS 的 80% 公共能力焊死在代码里——你删掉“文章管理”模块,换上“工单管理”,改三张表、调两个 API、重配菜单 JSON,两天就能上线。
2. 从源码包解压到本地运行:四步走通 RuoYi 前后端分离标准流程
这套源码不是 ZIP 解压就完事的黑盒。它严格遵循 RuoYi 官方推荐的前后端分离部署范式:后端是 Spring Boot 2.7.x(非 Spring Cloud),前端是 Vue 2.6 + Element UI(注意:不是 Vue 3 + Element Plus)。数据库用 MySQL 5.7+,JDK 8u291+,Node.js 14.x。整个过程必须按顺序执行,跳步必翻车。
2.1 初始化数据库:不只是导入 SQL,关键是理解表结构设计意图
拿到ry_202404.sql(常见命名)后,别急着source。先打开 SQL 文件扫一眼关键表:
sys_user:含dept_id(部门归属)、status(启用状态)、del_flag(软删除标记),这是 RuoYi 权限体系的锚点;sys_menu:perms字段存的是按钮级权限标识(如content:article:edit),不是 URL 路径;cms_article:这是业务表,含category_id(分类外键)、cover_img(封面图路径)、content_html(富文本 HTML 存储)、is_top(置顶标志)——注意它没用LONGTEXT存全文,而是MEDIUMTEXT,防大文本拖慢查询;sys_dict_type+sys_dict_data:字典表采用两级结构,dict_type定义类型(如article_status),dict_data存具体值(0=草稿,1=已发布),前端下拉框直接绑dict_type即可自动渲染。
提示:SQL 文件末尾通常有
INSERT INTO sys_user (...) VALUES (...);插入默认管理员(账号admin,密码admin123),但密码是 BCrypt 加密后的密文。如果你改过application.yml里的spring.profiles.active=prod,而 prod 环境配置了 Redis 缓存用户信息,首次登录会失败——因为缓存未初始化。解决方案:启动前确保spring.redis.host可连,或临时切回devprofile。
执行导入命令(Linux/macOS):
mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS ry_cms DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" mysql -u root -p ry_cms < ry_202404.sql2.2 后端启动:绕过 Maven 仓库污染,精准指定 JDK 和 Profile
RuoYi 后端模块名通常是ruoyi-admin(不是ruoyi-framework)。进入该目录后,禁止直接mvn spring-boot:run—— 因为pom.xml里spring-boot-maven-plugin默认绑定repackage,而run目标不触发resources过滤,会导致application-dev.yml中的${profile}占位符未替换。正确做法是:
# 确保 JAVA_HOME 指向 JDK 8(验证:java -version 输出 1.8.0_291) export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64 # Linux 示例 # 清理旧构建残留(尤其 target/classes 下的 application.yml) mvn clean # 指定 profile 并跳过测试(测试用例常因数据库连接失败卡住) mvn compile -Dmaven.test.skip=true # 启动时显式激活 dev profile,并指定配置文件位置(防读错) java -Dspring.profiles.active=dev -Dspring.config.location=classpath:application-dev.yml -jar target/ruoyi-admin.jar关键参数说明:
-Dspring.profiles.active=dev:强制激活开发环境,读取application-dev.yml;-Dspring.config.location=...:明确告诉 Spring Boot 配置文件在哪,避免它去config/目录找;target/ruoyi-admin.jar:这是mvn compile后生成的可执行 JAR,不是mvn package生成的 fat jar(后者体积大且含所有依赖,调试不便)。
启动成功标志:控制台输出Started RuoyiAdminApplication in X.XXX seconds,且日志末尾有Mapped "{[/login],methods=[POST]}"等接口映射记录。
2.3 前端启动:Vue CLI 服务需匹配 RuoYi 的代理规则
前端目录通常是ruoyi-ui。进入后执行:
npm install # 注意:不要用 npm start!RuoYi 前端的 package.json 里 scripts 是 "dev": "vue-cli-service serve" npm run dev此时浏览器访问http://localhost:80(不是 8080!RuoYi Vue 项目默认端口是 80,通过vue.config.js的devServer.port设置)。如果看到白屏或 404,检查vue.config.js中的devServer.proxy:
// vue.config.js 片段 devServer: { port: 80, proxy: { '/profile': { // 文件上传、头像预览等静态资源代理 target: 'http://localhost:8080', // 后端地址 changeOrigin: true, pathRewrite: { '^/profile': '/profile' } }, '/api': { // 所有业务接口代理 target: 'http://localhost:8080', changeOrigin: true, pathRewrite: { '^/api': '/api' } } } }注意:
pathRewrite的'^/api'是正则匹配,必须加^锚定开头,否则/api/article/list会被重写成/api/api/article/list。这是新手最常踩的坑——代理失效导致前端请求全 404。
3. 权限与菜单:为什么你新增的菜单不显示?RuoYi 的动态路由加载机制拆解
RuoYi 的菜单不是写死在前端router/index.js里,而是由后端SysMenuController接口返回 JSON,前端Permission.js动态生成路由。这意味着:菜单显示 = 后端数据 + 前端权限校验 + 路由注册三者同时成立。缺一不可。
3.1 后端菜单数据:sys_menu 表的四个关键字段决定可见性
在sys_menu表中,新增菜单行必须填满以下字段:
| 字段 | 必填 | 示例值 | 作用 |
|---|---|---|---|
menu_name | ✓ | 文章管理 | 菜单显示名称 |
parent_id | ✓ | 0(顶级菜单)或100(子菜单父 ID) | 决定菜单层级 |
url | ✗(可空) | /content/article | 前端路由路径,为空时仅作目录 |
perms | ✗(可空) | content:article:list | 按钮权限标识,为空则无按钮级控制 |
order_num | ✓ | 1 | 同级菜单排序序号 |
is_frame | ✓ | 1(外链)或0(内链) | 0时才走前端路由 |
menu_type | ✓ | M(菜单)/C(目录)/F(按钮) | M和C才显示在侧边栏 |
visible | ✓ | 0(隐藏)/1(显示) | 必须为 1,否则后端接口不返回该菜单 |
status | ✓ | 0(停用)/1(启用) | 必须为 1,否则不返回 |
提示:
visible=1且status=1是硬性门槛。很多开发者插入菜单后前端不显示,第一反应是前端缓存,其实是后端SysMenuServiceImpl.selectMenuTreeByUserId()方法里queryWrapper.eq("visible", "0")过滤掉了。查sys_menu表确认这两字段值。
3.2 前端权限同步:登录后如何触发菜单加载?
前端登录成功后,会调用user/login接口,返回roles数组(如["admin"])。接着立即调用getInfo接口(/getInfo),该接口返回menus字段——这才是真正的菜单树 JSON。Permission.js中的generateRoutes方法解析此 JSON,递归生成asyncRoutes,再router.addRoutes()注册。
关键代码在src/utils/permission.js:
// generateRoutes 方法核心逻辑 const filterAsyncRouter = (routers, roles) => { return routers.filter(route => { const hasRole = route.meta && route.meta.roles && route.meta.roles.some(role => roles.includes(role)) if (hasRole) { if (route.children) { route.children = filterAsyncRouter(route.children, roles) } return true } return false }) }这里route.meta.roles来自sys_menu的perms字段(如content:article:list→meta.roles = ['admin'])。所以:菜单是否显示,取决于当前用户角色是否在route.meta.roles列表中。而sys_role_menu关联表决定了角色能访问哪些菜单——你新增菜单后,必须去sys_role_menu表手动插入role_id=1, menu_id=新ID记录,或用后台“角色管理”页面勾选。
4. 内容管理模块实战:从新增文章到富文本保存的全链路调试
CMS 的核心是内容 CRUD。以cms_article表为例,走通一次新增流程,能暴露 90% 的配置问题。
4.1 前端表单:Element UI 的el-upload如何对接 RuoYi 的文件上传接口
文章封面图上传不是直传 OSS,而是走 RuoYi 的FileController.upload接口(/common/upload)。前端ArticleForm.vue中:
<el-upload class="avatar-uploader" action="/common/upload" <!-- 注意:这是代理后的路径,实际请求 /api/common/upload --> :show-file-list="false" :on-success="handleAvatarSuccess" :before-upload="beforeAvatarUpload"> <img v-if="form.coverImg" :src="form.coverImg" class="avatar"> <i v-else class="el-icon-plus avatar-uploader-icon"></i> </el-upload>beforeAvatarUpload方法必须返回false(阻止自动上传),因为 RuoYi 要求上传时带X-Requested-With: XMLHttpRequest头,而el-upload自动上传不带。所以改为手动触发:
beforeAvatarUpload(file) { const isJPG = file.type === 'image/jpeg' || file.type === 'image/png' const isLt2M = file.size / 1024 / 1024 < 2 if (!isJPG) { this.$message.error('上传头像图片只能是 JPG/PNG 格式!') } if (!isLt2M) { this.$message.error('上传头像图片大小不能超过 2MB!') } return isJPG && isLt2M }, handleAvatarSuccess(res, file) { this.form.coverImg = res.url // res.url 是后端返回的相对路径,如 /profile/upload/2024/04/xx.jpg }注意:
res.url是相对路径,前端展示时需拼接process.env.VUE_APP_BASE_API(通常为/),所以:src="form.coverImg"实际请求http://localhost:80/profile/upload/2024/04/xx.jpg,这正是vue.config.js里'/profile'代理的目标。
4.2 富文本存储:tinymce 的 content_html 字段为何存的是 HTML 而非 Markdown?
RuoYi 默认集成 tinymce,其init配置在Tinymce.vue:
init: { height: 500, language: 'zh_CN', plugins: 'link image code preview', toolbar: 'undo redo | bold italic | alignleft aligncenter alignright | code', menubar: false, branding: false, convert_urls: false, // 关键!禁用 URL 自动转换,否则 /upload/xxx.jpg 变成 http://localhost/upload/xxx.jpg images_upload_handler: this.imagesUploadHandler // 自定义图片上传处理器 }convert_urls: false是血泪经验——若为true,tinymce 会把相对路径/upload/1.jpg自动转成绝对路径http://localhost/upload/1.jpg,导致文章复制到生产环境后图片 404。imagesUploadHandler方法调用this.$http.post('/common/upload', formData)上传图片,返回url: '/profile/upload/xxx.jpg',tinymce 自动插入<img src="/profile/upload/xxx.jpg">。
后端ArticleController.add()接收的content_html字段,就是这段 HTML 字符串。它被直接存入cms_article.content_html字段,不做任何 XSS 过滤(RuoYi 认为这是业务层责任)。所以你在富文本里写<script>alert(1)</script>,它真会执行——生产环境务必在ArticleServiceImpl.insertArticle()中加入Jsoup.clean(contentHtml, Whitelist.relaxed())过滤。
5. 避坑指南:RuoYi 内容管理系统上线前必须验证的 5 个致命细节
现象、原因、解决,一条都不能少。这些不是“可能出错”,而是我在三个项目上线前夜亲手 debug 过的真问题。
5.1 现象:登录成功后跳转到/index,但页面空白,控制台报Uncaught TypeError: Cannot read property 'length' of undefined
原因:Permission.js中getMenus接口返回data.menus为null,而filterAsyncRouter方法未判空,直接调用routers.filter()。根源是SysMenuServiceImpl.selectMenuTreeByUserId()查询时,sys_role_menu表中该用户角色(如admin)未关联任何菜单。
解决:用 Navicat 连接数据库,查sys_role_menu表,确认role_id=1(管理员角色 ID)有menu_id记录;或登录后台,进入“系统管理 > 角色管理”,编辑“管理员”角色,勾选所有菜单并保存。
5.2 现象:文章列表页点击“编辑”,弹窗表单数据为空,Network 查看GET /article/{id}返回 404
原因:ArticleController.getArticle()方法的@PathVariable Long id参数,要求 URL 路径为/article/123,但前端router/index.js中path: '/content/article/:id(\\d+)'的正则\\d+在 Vue Router 3.5+ 中需双反斜杠转义,实际应写/:id(\\d+)。若写成/:id(\\d+),Vue Router 解析失败,$route.params.id为undefined,后端接收id=null,MyBatisWHERE id = ?生成WHERE id = null,MySQL 返回空结果。
解决:检查router/index.js中文章详情路由,确保正则为/:id(\\d+);或改用/:id不加正则,后端@PathVariable改为@PathVariable String id,再Long.parseLong(id)。
5.3 现象:上传封面图后,文章列表页图片显示 404,但http://localhost:8080/profile/upload/xxx.jpg能直接访问
原因:前端vue.config.js的devServer.proxy配置了'/profile'代理,但生产环境 Nginx 未配置location /profile/代理到后端。开发时走 webpack-dev-server 代理,生产时静态资源请求直接打到 Nginx,而 Nginx 没转发/profile/请求。
解决:Nginx 配置增加:
location /profile/ { proxy_pass http://backend_server/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }其中backend_server是 upstream 名称,指向http://127.0.0.1:8080。
5.4 现象:修改文章后,content_html字段存入数据库的 HTML 中,<img>标签的src属性被自动加上了http://localhost:8080前缀
原因:tinymce 的convert_urls: true(默认值),且document_base_url未设置。tinymce 将相对路径/profile/upload/1.jpg转为绝对路径http://localhost:8080/profile/upload/1.jpg。
解决:在Tinymce.vue的init配置中显式添加:
convert_urls: false, document_base_url: '/' // 强制 base URL 为根路径5.5 现象:数据库中cms_article.create_time字段值为0000-00-00 00:00:00,而非当前时间
原因:MySQL 5.7 默认sql_mode包含NO_ZERO_DATE,而 RuoYi 的Article实体类中createTime字段未加@TableField(fill = FieldFill.INSERT)注解,MyBatis-Plus 不自动填充。同时sys_user表的create_time也为空,说明全局填充未生效。
解决:
- 检查
MyMetaObjectHandler.java是否存在,且insertFill方法中metaObject.setValue("createTime", new Date())已写; - 确认
Article类的createTime字段上有@TableField(fill = FieldFill.INSERT); - 若仍无效,在
application-dev.yml中添加mybatis-plus.global-config.db-config.id-type: assign_id,并确保Article类主键id用@TableId(type = IdType.ASSIGN_ID),因为 MyBatis-Plus 的自动填充依赖主键生成策略。
6. 进阶技巧:用 RuoYi 代码生成器快速扩展“产品文档”模块,三天交付
别把 RuoYi 当成品 CMS 供着,它的灵魂是代码生成器(ruoyi-generator模块)。我用它给客户扩了一个“产品文档中心”,需求是:支持文档分类、版本管理、Markdown 编辑、PDF 导出。没重写权限,没改菜单,纯靠生成器 + 两处小修,三天上线。
6.1 步骤一:设计三张表,用生成器一键产出全部代码
在 MySQL 新建表:
CREATE TABLE `product_doc` ( `id` bigint NOT NULL COMMENT '主键', `title` varchar(200) NOT NULL COMMENT '文档标题', `category_id` bigint NOT NULL COMMENT '分类ID', `version` varchar(20) NOT NULL COMMENT '版本号,如 v1.2.0', `content_md` longtext COMMENT 'Markdown 内容', `pdf_url` varchar(500) DEFAULT NULL COMMENT 'PDF 文件路径', `create_by` varchar(64) DEFAULT '' COMMENT '创建者', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='产品文档表'; CREATE TABLE `product_doc_category` ( `id` bigint NOT NULL COMMENT '主键', `name` varchar(100) NOT NULL COMMENT '分类名称', `sort` int DEFAULT '0' COMMENT '排序', `status` char(1) DEFAULT '0' COMMENT '状态(0正常 1停用)', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文档分类表';进入 RuoYi 后台 → “系统工具 > 代码生成”,选中这两张表,配置:
- 生成包路径:
com.ruoyi.cms - 模块名:
cms - 业务名:
productDoc - 功能名:
产品文档 - 作者:
yourname - 表前缀:
product_(自动去掉) - 生成选项:勾选“Controller/Service/Entity/Mapper/XML/View”
- 模板:用默认的
crud模板
点击“生成代码”,下载 ZIP,解压到ruoyi-cms模块(需先在pom.xml中添加该模块依赖)。
6.2 步骤二:两处关键修改,让 Markdown 编辑器和 PDF 导出跑起来
生成的ProductDocController默认用content_md字段存 Markdown,但前端ProductDocForm.vue的富文本组件还是 tinymce(HTML 编辑器)。要换成 Markdown 编辑器:
npm install mavon-editor --save- 在
ProductDocForm.vue中:
<mavon-editor v-model="form.contentMd" :toolbars="toolbars" @change="handleChange" /> <script> import { mavonEditor } from 'mavon-editor' export default { components: { mavonEditor }, data() { return { toolbars: { bold: true, underline: true, code: true, subfield: true, defaultOpen: 'editor' } } } } </script>PDF 导出用jspdf+html2canvas:
// ProductDoc.vue 中导出方法 exportPdf() { const element = document.getElementById('doc-content') // 文档预览 DOM html2canvas(element).then(canvas => { const imgData = canvas.toDataURL('image/png') const pdf = new jsPDF('p', 'mm', 'a4') const imgWidth = 210 // A4 宽度 mm const pageHeight = 297 const imgHeight = (canvas.height * imgWidth) / canvas.width let heightLeft = imgHeight let position = 0 pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight) heightLeft -= pageHeight while (heightLeft >= 0) { position = heightLeft - imgHeight pdf.addPage() pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight) heightLeft -= pageHeight } pdf.save(`${this.form.title}.pdf`) }) }注意:
html2canvas对 CSS 兼容性差,.markdown-body类的样式需内联或用style属性写死,否则 PDF 里排版错乱。这是玄学环节,我试了 7 种方案,最后用dom-to-image替换html2canvas才稳定。
6.3 步骤三:菜单与权限,5 分钟接入现有体系
生成器已产出ProductDocController,接口路径是/productDoc。只需:
- 在
sys_menu表插入菜单(menu_name='产品文档',url='/cms/productDoc',perms='cms:productDoc:list'); - 在
sys_role_menu表插入role_id=1, menu_id=新ID; - 前端
router/index.js中import ProductDoc from '@/views/cms/productDoc/index',加路由; ProductDoc.vue中this.$http.get('/productDoc/' + this.id)获取数据,this.$http.post('/productDoc', this.form)提交。
不用写一行权限校验代码——RuoYi 的@PreAuthorize("@ss.hasPermi('cms:productDoc:list')")注解已生效。
我做这个项目时,客户提的需求是“下周要给销售团队用”,我周四下午拿到需求,周五用生成器出代码,周一上午改完 Markdown 和 PDF,周二下午陪客户走查,周三上线。没有加班,没有扯皮,就靠吃透 RuoYi 这套生成逻辑。它不炫技,但稳——就像一把磨得锃亮的螺丝刀,拧什么螺丝都快。希望帮到你。
本文还有配套的精品资源,点击获取