1. 起盘这个项目前:民俗网站管理的真实痛点与选型思路
做民俗文化类网站管理系统,听起来不算多稀罕,但真正接过这类需求的人都知道,它和普通的企业官网后台差别很大。民俗内容通常包含大量图片、文字介绍、分类栏目、特色专题,还经常要按地域、按民俗门类做归档和检索。一个纯静态页面顶不住内容更新频率,纯模板渲染的单体项目又会把前端跟后端耦合得太死,改个页面风格都得动服务器。
这套“基于SpringBoot+Vue的中国陕西民俗网管理系统”能作为一个比较完整的参考样本拿出来说,是因为它正好踩中了这类项目最典型的几个诉求:区域性文化内容展示、后台内容管理、用户端阅读浏览、管理员权限隔离,以及将来可能接入的申报、投稿、活动报名等扩展能力。源码层面用SpringBoot做后端主体,Vue做前端界面,MyBatis负责数据持久化,MySQL作存储,是当前中小型信息管理类系统最主流也最稳妥的一套组合。
我帮朋友工作室接过一个类似的文化类站点,刚开始他们给的需求就是“做一个某某民俗的网站”,听着简单,实际一拆就是四五张表起步、十几个页面,再往后还要加轮播图管理、栏目分类、文章多图上传。如果没有一套前台展示和后台管理分开的结构,开发到一半就会想推翻重来。这套系统最大的参考价值不在于代码多炫,而在于它把文化站点的内容治理逻辑理顺了:谁在后台发内容,内容挂在哪个分类下,访客在前台怎么看到,管理员怎么审核,角色怎么区分——这些链路一旦清晰,后续改版也只是换皮,不用动筋骨。
适合参考这套系统的人,大概有三种:一是接文化类外包项目、需要快速搭底子的开发团队;二是高校里做信息系统课程设计或毕业设计的学生,这类题材好答辩、技术栈也主流;三是想把自己县域、村镇的民俗资料数字化的运营人员,虽然不一定自己改代码,但看懂这个系统的结构,再去提需求会精准很多。
2. 技术选型与整体架构:SpringBoot+Vue+MyBatis+MySQL怎么分工
2.1 前后端分离的职责边界
先说为什么不是传统的JSP+SpringMVC那一套。老方案在十年前确实够用,但放到现在,前端希望有更好的组件化能力,比如轮播、富文本编辑器、图片裁剪、极验验证码这些,用原生HTML+CSS写起来非常痛苦。Vue这套生态里,Element UI或者Element Plus直接提供现成的表格、弹窗、表单、上传组件,后台管理界面的开发效率能提升好几倍。
后端用SpringBoot,不光是注解式开发省事,更重要的是它内嵌Tomcat,打成一个jar包就能跑,不像传统SSH还要单独装Tomcat、维护web.xml。配合Spring家族的AOP事务、参数校验、统一异常处理,写起接口来干净利落。MyBatis在这个组合里负责SQL操作,没有用全自动ORM工具去替代,是因为民俗这种内容系统的查询条件实在太灵活:按标题模糊搜、按分类筛选、按地区聚合、按时间倒序,每一条SQL都不一样。MyBatis把SQL和控制逻辑分开,改查询条件直接改Mapper XML,不用重新编译整个业务层。
MySQL的选择就不用多说了,千万行以下的数据量,免费、稳定、生态成熟,配合Navicat或DBeaver这类图形工具,导出导入数据都方便,特别适合文化类站点这种以文字和图片路径为主、数据量不会爆炸的应用场景。
2.2 工程结构怎么摆才不乱
我按这套系统的普遍做法给一个推荐目录,后面说代码也基于这个结构:
├── src/main/java │ ├── com.demo.folklore │ │ ├── config // 跨域、拦截器、静态资源映射 │ │ ├── controller // 前台展示、后台管理接口 │ │ ├── entity // 数据表实体 │ │ ├── mapper // MyBatis接口 + XML │ │ ├── service // 业务逻辑 │ │ ├── utils // JWT工具、文件上传工具 │ │ └── common // 统一返回结果、异常处理 ├── src/main/resources │ ├── mapper // Mapper XML │ └── application.yml └── frontend ├── src/views // Vue页面 ├── src/api // 接口封装 └── src/router // 路由后台接口和前台接口分开写Controller,比如AdminArticleController负责管理端的文章增删改,ArticleController只管前台展示列表和详情。这样做的原因是权限粒度完全不同:前台接口可以放行,后台接口必须校验管理员Token。分开了,后面加权限配置不会乱。
3. 数据库设计:民俗内容系统最核心的表结构拆解
3.1 四张基础表怎么建
民俗网站管理系统的数据表不用设计得很复杂,但几张主表的关系必须理清楚。整个系统最核心的有四张:管理员表、文章内容表、文章分类表、系统配置表。以我这边的整理为例,表结构大致是下面的样子:
CREATE TABLE `admin_user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '登录账号', `password` varchar(255) NOT NULL COMMENT 'BCrypt加密密码', `nickname` varchar(50) DEFAULT NULL COMMENT '管理员昵称', `status` tinyint(1) DEFAULT '1' COMMENT '1启用 0禁用', `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='管理员表';密码字段这里特别提醒一下,草根项目很多直接用MD5加密,甚至明文存储。这种系统一旦泄露,攻击者可以直接进后台发垃圾文章。Spring Security自带BCryptPasswordEncoder,使用成本极低,不要为了图省事放弃它。
文章表是所有表里字段最多的,也是前台展示的核心:
CREATE TABLE `article` ( `id` int(11) NOT NULL AUTO_INCREMENT, `category_id` int(11) NOT NULL COMMENT '所属分类ID', `title` varchar(200) NOT NULL COMMENT '文章标题', `content` longtext COMMENT '富文本内容', `cover_image` varchar(500) DEFAULT NULL COMMENT '封面图路径', `images` text COMMENT '多图路径,逗号分隔', `author` varchar(100) DEFAULT NULL COMMENT '整理人', `source` varchar(100) DEFAULT NULL COMMENT '资料来源', `province` varchar(50) DEFAULT NULL COMMENT '所属地区', `view_count` int(11) DEFAULT '0' COMMENT '浏览量', `is_top` tinyint(1) DEFAULT '0' COMMENT '是否置顶', `status` tinyint(1) DEFAULT '1' COMMENT '1已发布 0草稿', `create_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_category` (`category_id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章内容表';这里province字段是我在实际需求里遇到过的一个点:民俗内容有很强的地域属性,比如用户想知道“陕西有哪些民俗”,单纯靠标题模糊搜不准,按地区字段筛才准确。别小看这个字段,后期做地域专题页全靠它。
分类表其实可以做成无限级,但个人不建议一上来就递归。多数民俗站分类深度两层就够:大类比如“传统技艺”“民俗节庆”“民间美食”,子类比如“皮影”“剪纸”。设计成parent_id字段就行,查询时做两步:先查大类,再按大类ID查子类,不必写递归SQL。
3.2 多对多场景:首页轮播和精选专题
除了基础表,很多系统还会加一张轮播图表,用来控制首页的大图广告位。这块用banner表加上article_id外键,比直接在前台写死要好。运营人员换图不用改代码,后台传一张新图、选一个跳转文章,前台刷新即生效。
还有一个容易忽略的需求是“专题聚合”。比如“2025陕西非遗名录”这种专题页面,它其实是由多个分类下的多条文章拼出来的。做法是在文章表里加一个topic_id字段,或者单独建一张文章和专题的多对多关联表。推荐关联表,后期灵活性更强,但这要看系统做的规模。这套参考系统里如果暂不打算做专题,那就不要提前建表,避免过度设计。
3.3 MyBatis查询中的条件拼接
MyBatis最有用的地方就是写动态SQL。这里分享一个常用的条件查询写法:
<select id="selectArticleList" resultType="com.demo.folklore.entity.Article"> SELECT a.*, c.name AS category_name FROM article a LEFT JOIN category c ON a.category_id = c.id <where> <if test="title != null and title != ''"> AND a.title LIKE CONCAT('%', #{title}, '%') </if> <if test="categoryId != null"> AND a.category_id = #{categoryId} </if> <if test="province != null and province != ''"> AND a.province = #{province} </if> <if test="status != null"> AND a.status = #{status} </if> </where> ORDER BY a.is_top DESC, a.create_time DESC </select><where>标签会自动去掉多余的AND,<if>标签按条件拼SQL。这套写法就是MyBatis项目里后台文章列表、前台分类浏览共用同一个Mapper的原因——参数不同,SQL自动变化。如果换成JPA,这种动态查询要么写Specification,要么写JPQL拼接,阅读成本反而高。
3.4 分页查询怎么设计
分页这里推荐用PageHelper插件,不要自己手写limit计算。只需要在Service层调用PageHelper.startPage(pageNum, pageSize),紧接着执行的查询就会自动带上分页。关键点在于startPage后面必须紧跟第一条查询语句,中间不能插入别的逻辑,否则分页会失效。
PageHelper还有一个比较坑的地方:如果查询里有子查询或者SQL_CALC_FOUND_ROWS,分页的count统计可能出现偏差。这种时候建议直接写一个selectCount方法,把总条数查出来,前端分页组件才能正确算页数。
4. 后端核心逻辑:认证鉴权、文件上传、内容发布的完整链路
4.1 后台接口的Token鉴权
民俗网后台并不需要像电商系统那样精细到按钮级权限,一般到“登录者是不是管理员”这一层就够了。这里用JWT是最常见的选择:登录成功后,后端签发一个带有效期的Token返回前端;前端每次调用后台接口,在请求头带上Authorization: Bearer xxx;后端通过拦截器校验Token有效性和过期时间。
核心配置大致是这样:
@Component public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String auth = request.getHeader("Authorization"); if (auth == null || !auth.startsWith("Bearer ")) { throw new RuntimeException("请先登录"); } String token = auth.substring(7); Claims claims = JwtUtil.parseToken(token); if (claims == null) { throw new RuntimeException("登录状态已过期"); } request.setAttribute("adminId", claims.get("adminId")); return true; } }注意这里只做了Token校验,没有做DB层面的会话状态管理。优点是接口天然支持无状态,缺点是无法在后台一键踢人下线。对民俗内容网站来说,管理员通常就两三个人,这个代价可以接受。
拦截器注册时,注意要放行前台接口、登录接口、静态图片资源,只拦截/admin/**开头的路径。否则部署上线后,前台页面图片全部因为Token原因加载不出来,排查起来非常隐蔽。
4.2 统一返回结果和全局异常
后端接口我建议从第一天起就统一返回格式:
{ "code": 200, "message": "操作成功", "data": {} }前端axios拿到响应后,先判断code === 200再进入业务逻辑,否则弹出message提示。这样做的好处是,无论接口处出现参数校验失败、Token过期,还是数据库异常,前端只用处理一个数据结构。很多半路项目前后端联调慢,就是栽在这种地方:有人返回String,有人返回Map,有人成功时无data字段,前端解析时各种判空。
配合@RestControllerAdvice统一异常处理器,把BusinessException、MethodArgumentNotValidException、全局异常分别处理,返回对应的code和message。这样代码里不需要到处try-catch,往里抛异常就行。
4.3 富文本内容与图片上传
民俗文章正文几乎不可能没有图片,所以图片上传接口可以说是这套系统的生命线。两种方案比较常见:一是传到本地服务器,配一个静态资源映射;二是传到OSS之类的云存储。本地方案做课程设计和中小项目足够,部署到一台云服务器上,图片和jar包放在一起,记得把服务器磁盘空间预留够。
本地存储的映射配置:
spring: servlet: multipart: max-file-size: 10MB max-request-size: 50MB file: upload-dir: ./upload access-path: /upload/**然后用一个WebMvcConfigurer把磁盘路径映射为URL访问地址:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourcePath("file:" + fileUploadDir + "/"); } }上传接口里要做两件事:一是限制文件类型,别把.exe、.jsp之类传上来;二是给文件重命名,推荐UUID + 原扩展名,避免中文文件名或者重复文件名造成的路径问题。还有一点是图片压缩:民俗文章经常一次传好几张单反拍的图,每张五六MB,不压缩的话页面加载非常慢。可以在后端用Thumbnailator插件做一下压缩,长边缩到1200像素左右,保存一份缩略图路径到cover_image字段。
4.4 浏览量统计接口
详情页的浏览量统计,不要真去写一个update语句做view_count = view_count + 1,每次请求都更新数据库会造成不必要的压力。简单的做法是前端做一些精简化处理,或者后端用Redis的HINCRBY定期刷盘。但很多小项目根本没有Redis,我就是把这个更新逻辑做成了一个单独的Mapper方法,页面每次加载异步调一下,不做缓存,内容站量级下其实没有任何问题。
5. Vue前端:页面组织、接口封装与联调细节
5.1 前台页面结构
前端分两个大门类:面向访客的展示页面和面向管理员的后台页面。Vue Router里可以用嵌套路由来区分,也可以直接用两个布局组件。
前台核心页面有这些:
- 首页:轮播图+Banner + 分类入口 + 最新文章列表 + 推荐文章
- 分类页:按分类加载文章列表,带分页
- 搜索页:按关键词模糊搜索,展示搜索结果
- 详情页:展示文章标题、封面、正文、来源,底部放相关推荐
- 关于页:网站介绍
后台核心页面:
- 登录页
- 仪表盘/统计页
- 文章管理列表页
- 文章编辑页(富文本)
- 分类管理页
- Banner管理页
- 管理员账号页
这里重点说一下编辑页。富文本编辑器推荐用wangEditor或者Quill,前者对中文系统更友好,后者更轻量。上传图片时是在编辑器内部的工具栏上传,此时要注意,编辑器组件请求上传接口走的是后端接口,同样需要在请求头带上Token。若你的上传接口放在了/admin/**路径下,不处理这个细节的话,编辑图片上传永远报401,非常容易踩坑。
5.2 axios封装的关键细节
前端接口调用的统一封装不用复杂,但有几处细节一定要处理到位:
const request = axios.create({ baseURL: '/api', timeout: 10000, }); request.interceptors.request.use(config => { const token = localStorage.getItem('admin_token'); if (token) { config.headers.Authorization = 'Bearer ' + token; } return config; }); request.interceptors.response.use( res => { if (res.data.code === 401) { localStorage.removeItem('admin_token'); router.push('/login'); } return res.data; }, err => { return Promise.reject(err); } );响应拦截器里判断401跳转登录,是后台管理界面常见的标准做法,不要省略。此外要处理好跨域问题:开发环境下Vue的运行端口是5173,后端接口是8080,浏览器会跨域。后端可以加CORS配置允许跨域,也可以让Vue的vite配置代理转发请求。
推荐用代理方案,这样前端请求地址直接写/api,后端不需要加跨域配置,上线后部署在同一个域名下也不会出问题。vite.config.js配置:
server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, rewrite: path => path.replace(/^\/api/, '') } } }5.3 列表页参数与分页组件
后台列表页最常见的问题是“搜索条件和分页参数的联动”。搜索关键词变了,当前页要重置成第一页;删除当前页最后一条数据后,页码要往回退一页,否则会出现空列表的情况。这些细节在写代码时就要考虑进去,不要等测试提bug再改。
具体做法很简单,在搜索方法里先重置pageNum = 1再调接口;删除方法里判断,如果当前页只剩一条且页码大于1,删除后pageNum--再刷新。这些逻辑总代码量不到二十行,但能把使用体验拉高一个档次。
6. 从开发到上线:配置、打包、以及我实际踩过的坑
6.1 开发环境的配置写法
后端application.yml一定要把开发环境和生产环境拆开。最简单的做法是用application-dev.yml和application-prod.yml两个文件,再在启动命令中指定profile。开发环境可以打印SQL日志、关闭某些校验,生产环境要关掉这些。
MyBatis的SQL日志用如下配置:
mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.demo.folklore.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case必须打开,否则数据库的create_time对应不上实体类的createTime,查出来全是null。这类问题不会报错,但页面数据莫名其妙显示不全,排查起来相当恼人。
数据库连接串这里也值得提一句:时区参数serverTimezone=Asia/Shanghai一定要加,否则MySQL 8.x连上来后时间会偏差8小时。另外建议加上characterEncoding=utf8mb4,不然富文本里保存生僻字或emoji,写入数据库会报“Incorrect string value”,这是民俗内容系统最容易遇到的一个坑。
6.2 前端打包与后端整合
前后端分离项目的部署有两种常见姿势。一种是前端Nginx跑,后端jar包跑,Nginx把/api请求反向代理到8080端口。这种适合已经有Nginx服务器的团队。
另一种是把Vue构建后的dist目录拷贝到SpringBoot的src/main/resources/static里,然后直接把jar包丢到服务器,一个服务搞定所有请求。这种方式配置很少,适合单机部署的小项目,也是很多课程设计答辩时演示最省事的方式。
我用第二种方式时碰到过一个坑:前端路由用的history模式,用户直接刷新某个子页面路径,比如/admin/article/edit/3,后端的SpringBoot不认识这个路径,会直接404。光在刷新场景下就够让人抓狂的。解决方法是把viewController配置好,或者做一个简单的路由回退,把非/api且非静态文件的所有路径都转发到index.html。这个问题在本地联调时根本发现不了,因为Vite开发服务器自己会处理history回退,等上线后就瞬间暴露。
6.3 部署中常见的真实问题
按我实际碰过的部署问题,把频率最高的几条列成下表,后面接手类似项目的人直接对照检查:
| 现象 | 根因 | 解决办法 |
|---|---|---|
| 前端能打开,接口全部报404 | 部署时把前后端放在了不同源,代理未配置或后端路径不对 | 检查Nginx的/api转发规则或静态资源路径 |
| 图片上传成功但无法访问 | 静态资源映射没配或图片目录权限不对 | 确认WebConfig映射的file:路径与实际目录一致 |
| 数据库中文乱码 | 连接串缺少characterEncoding=utf8mb4 | 修改JDBC URL,注意数据库表也要是utf8mb4 |
| 刷新页面404 | 前端history路由没有fallback | 配置路由回退,指向index.html |
| 文章保存报错但接口正常 | 富文本传的是带HTML标签的字符串,参数校验没放行 | 检查后端拦截器或全局异常对<script>标签的处理 |
| 后台修改密码后所有Token失效 | JWT没做踢人机制,但期望的是全局下线 | 重新设计Token方案,引入版本号或短期过期策略 |
6.4 内容安全与运营边界
最后说一个稍微容易被忽视但很重要的点:民俗网站是文化内容平台,后台能发什么内容、不能发什么内容,在技术上要有初步的把关。系统至少应该做一个敏感词过滤工具,发布文章时对标题和正文做校验,命中词条直接拦截,这是对运营方的基本保护。哪怕只是简单的遍历替换,也比什么都不做的强。
对于用户端,如果后续要开放评论和投稿功能,务必把审核机制设计进去,默认新提交内容状态设为待审核。不要图省事直接对外展示所有评论,否则网站内容质量会急速失控。这套系统目前的架构里,把评论表拆出来加一个status字段就能实现,后期扩展的压力并木有太大。
7. 我在实际使用中的几点体会
这套SpringBoot+Vue+MyBatis+MySQL的组合,我陆陆续续用在做文化站、内容管理这类系统上好几轮了。最大的体会是:这类系统真正的复杂度不在技术,而在内容结构的安排和权限逻辑的梳理。很多初学者一上来就研究SpringSecurity配置类怎么写、Shiro怎么集成,折腾一周还没看到登录页。
正确的做法应该是先画清楚页面和接口的对应关系,再把数据表建模做完,最后才动手写代码。民俗网站管理系统的表关系相对简单,花两天时间把分类、文章、轮播、管理员四张表理清楚,后面的编码就是在做“增删改查的排列组合”。
选型方面,如果再让我做一次,我依然会用这套组合。SpringBoot的自动配置大幅降低了起步成本,Vue的组件化让后台管理界面整洁可控,MyBatis保住了SQL的灵活性,MySQL作为存储永远有兜底方案。对从零开始学系统开发的人来说,把这套系统的每一个接口都手写一遍,比刷一百道八股文都有用。
最后分享一个小技巧:给项目加一个定时备份MySQL数据库的脚本,每天凌晨把.sql文件压缩归档到另一个目录。内容系统的数据一旦丢了,比丢代码难受得多,代码还可以从git拉,数据没了就真的没了。运维层面的活儿做细一点,项目才能长久跑下去。