做这类管理系统项目的人应该不少,但能把一个主题做出完整闭环的并不多见。这次分享的项目是一个前后端分离的当代中国获奖知名作家信息管理系统,技术栈锁定在 SpringBoot + Vue + MyBatis + MySQL 这套非常经典的组合上。项目本身包含了完整源码和部署教程,属于那种拿来就能跑、跑起来就能学的类型。
这个系统的业务定位很明确:围绕“作家”这个核心实体,管理作家基本信息、获奖记录、代表作品、创作风格等数据,并提供检索、筛选、统计分析和后台管理能力。对于正在做课程设计、毕业设计,或者想系统性练习前后端分离开发的人来说,这是一个非常合适的练手项目。它不涉及复杂的分布式架构,也没有堆砌高深的技术名词,而是把工程化开发中最常见、最实用的能力都覆盖到了。
接下来我会从项目设计思路、数据库建模、后端接口实现、前端页面交互到最终部署,一步步拆解这个项目的完整细节。中途会把一些我自己实测踩过的坑和排查方法一并放进来,希望能让读到这篇文章的人少走一些弯路。
1. 项目整体设计与技术选型思路
1.1 为什么选“作家信息管理”这个业务场景
很多人在做管理系统时喜欢选“图书管理”“学生管理”“商品管理”这类主题,不是说不行,而是这类主题太常见了,答辩时很难做出差异化。作家信息管理这个选题有几个独特优势。
业务实体之间天然存在多对多关系。一位作家有多部作品,也有多个奖项,作品和奖项之间又有交叉,这种数据关系比单纯的“商品-分类”要复杂,能很好地体现数据库设计能力。作家信息本身有明显的筛选维度:按出生年代、按地区、按获奖类型、按创作领域,这些维度放进查询条件后,能让搜索功能做得很有层次感,而不是只有一个关键词搜索框。
更重要的是,这个主题有一定的人文属性。同样是增删改查,管理“作家”比管理“商品”天然多了一层谈话空间——可以聊数据模型怎么承载人物关系,怎么通过数据分析展示获奖趋势。在答辩或者文档汇报时,这种选题更容易展开来讲。
1.2 技术栈选型的真实考量
先说后端,SpringBoot 基本是这个量级项目的默认解。它的自动配置机制能省掉大量 XML 配置,内嵌的 Tomcat 让部署也变得简单。MyBatis 的选型可能有人会问:为什么不直接用 MyBatis-Plus?我的看法是,MyBatis 原生写法虽然啰嗦,但能把 SQL 控制权完全握在自己手里,对理解 ORM 底层逻辑帮助更大。这个项目用了 MyBatis,反而是一个学习加分项。
前端选择 Vue 而不是 React,核心原因是 Vue 的上手曲线更平缓。Vue 的模板语法对后端开发者非常友好,单文件组件(SFC)让代码组织清晰,配合 Element-UI 这类组件库,能在很短时间内搭建出界面规范的后台管理页面。
数据库用 MySQL 是顺理成章的选择。这个项目的数据量级在万级以内,MySQL 完全能胜任。有一点必须说清楚:不要在这个项目里引入 Redis、Elasticsearch 这类中间件,会显得技术选型脱离业务实际。把 MySQL 的表结构设计好、索引建对,性能完全不是问题。
1.3 前后端分离架构的协作方式
前后端分离的核心,是前后端通过接口文档约定的数据格式进行通信。这个项目里,后端只负责提供 JSON 格式的 RESTful API,前端负责渲染页面和调用接口。两边的开发可以完全并行推进。
我在设计接口时遵循了几个简单原则:统一返回结构,响应体包含 code、message、data 三个字段;资源路径用名词复数,比如 /api/authors、/api/awards;状态码尽量语义化,2xx 代表成功,401 代表未认证,403 代表无权限。这些约定看似基础,但很多人做项目时容易忽略,导致前后端联调时反复扯皮。
2. 数据库设计:一张作家表撑不起整个系统
2.1 核心表结构与关系拆解
这个系统的数据库一共设计了五张核心表,分别是作家表、奖项表、作品表、作家奖项关联表、用户表。前四张围绕业务展开,用户表负责后台登录认证。
作家表(author)是核心主表,字段包括:主键 id、作家姓名、性别、出生日期、逝世日期、籍贯、民族、创作领域、个人简介、头像地址、创建时间、更新时间。这里涉及一个关于“逝世日期”的设计细节:它不是必填字段,默认允许为空,只有状态为“已故”的作家才需要填写。如果把它设为 NOT NULL,录入在世作家数据时就会遇到麻烦。
奖项表(award)相对简单,存储的是奖项名称、主办方、设立时间等信息。作品表(work)则记录了作品名称、体裁、出版年份、出版社、作品简介、所属作家的外键。
作家和奖项是多对多关系——一位作家可以拿多个奖,一个奖也有多位获奖者。所以设计了作家奖项关联表(author_award),包含作家 id、奖项 id、获奖年份、获奖作品名称。这个字段“获奖年份”一定要放在关联表里,而不是放在作家表里,否则年代信息会丢失。
2.2 字段类型选择与索引设计
有几个字段类型我要特别提一下。
出生日期和逝世日期推荐使用 DATE 类型,不要用 VARCHAR。原因很简单,DATE 类型可以直接进行排序和范围查询,比如筛选“60后作家”可以用 BETWEEN 语句;如果存成字符串,这类查询会变得非常痛苦。
个人简介字段用 TEXT 类型,而不是 VARCHAR。VARCHAR 最长支持 65535 字节,但默认分配长度不宜过大。简介文本长度不确定,直接用 TEXT 更合理。
索引方面,作家姓名和出生日期都适合加普通索引。获奖年份、创作领域这类高频筛选条件也要建索引。一个容易犯的错误是在创建时间上建索引——这个项目完全没有按创建时间筛选的需求,建了索引只会浪费存储空间、拖慢写入速度。
2.3 从需求反推字段设计的思路
设计表结构时,我一直强调“先想查询需求,再设计字段”。举例来说,如果系统需要展示“某位作家的获奖时间线”,那获奖年份在数据库里必须是独立字段,而不能拼接在获奖描述文本里。如果列表页需要按创作领域快速筛选,那创作领域就必须是一个可枚举字段,而不是写在简介里的散落文本。
这个思路即使不做这个项目,在以后任何涉及数据库设计的场景中都很实用。数据表的设计不是凭感觉堆字段,而是从页面原型和接口需求一步步倒推出来的。
3. 后端开发:SpringBoot + MyBatis 的核心实现
3.1 项目分层结构与包组织
后端工程采用经典的三层架构:Controller 层负责接收请求和返回响应,Service 层负责业务逻辑处理,Mapper 层负责数据库访问。对应的包结构如下:
com.example.author ├── controller # 接收 HTTP 请求 ├── service # 业务逻辑 ├── mapper # MyBatis 数据访问接口 ├── entity # 实体类 ├── dto # 数据传输对象 ├── vo # 视图对象 ├── config # 配置类 ├── common # 统一返回体和异常处理 └── utils # 工具类Controller 层应该保持轻薄,只做参数接收和结果封装,不要写业务代码。我见过不少人把 SQL 逻辑写在 Controller 里,只能说是灾难。Service 层要处理的核心逻辑包括:作家数据校验、获奖记录的幂等校验、分页条件拼装等。
实体类与数据库表的映射关系,通过 MyBatis 的注解或 XML 文件维护。我建议 Mapper 层接口方法与 XML 中的 SQL id 保持一致命名,这样一个方法对应一条 SQL,排查问题会很直接。
3.2 统一返回体与全局异常处理
前后端分离项目最忌讳接口返回格式不一致。有的接口返回对象,有的接口返回字符串,前端处理起来极其痛苦。我在这个项目里定义了一个统一返回体 Result:
public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("操作成功"); result.setData(data); return result; } public static <T> Result<T> error(Integer code, String message) { Result<T> result = new Result<>(); result.setCode(code); result.setMessage(message); return result; } }全局异常处理用 @RestControllerAdvice 实现。业务异常、参数校验异常、系统异常分别捕获,返回对应的错误消息。这里我想强调一下消息设计的细节:不要直接向用户暴露 SQL 异常信息,要把 SQLException 包装成“系统繁忙,请稍后重试”,把真正的错误日志打印到服务端控制台。这样既保证了用户体验,也保留了排查问题的线索。
3.3 列表查询与分页接口的完整实现
分页查询是管理系统的核心功能之一,这个项目里作家列表页的请求参数包括:当前页码 pageNum、每页条数 pageSize、作家姓名 name、创作领域 field、起始出生年份 startYear、结束出生年份 endYear。后端需要根据这些条件动态拼接 SQL。
Mapper 接口定义如下:
public interface AuthorMapper { List<AuthorVO> selectAuthorList(@Param("authorQueryVO") AuthorQueryVO queryVO); Long countAuthorList(@Param("authorQueryVO") AuthorQueryVO queryVO); }注意这里把查询对象封装成了一个独立的 VO 类,而不是散落的多个参数。这样一个对象参数对应一个查询条件集合,后面增加筛选条件时只需修改 VO 字段。
对应的 XML 文件里使用动态 SQL 拼接查询条件:
<select id="selectAuthorList" resultType="com.example.author.vo.AuthorVO"> SELECT id, name, gender, birth_date, death_date, hometown, nation, field, introduction, avatar FROM author <where> <if test="authorQueryVO.name != null and authorQueryVO.name != ''"> AND name LIKE CONCAT('%', #{authorQueryVO.name}, '%') </if> <if test="authorQueryVO.field != null and authorQueryVO.field != ''"> AND field = #{authorQueryVO.field} </if> <if test="authorQueryVO.startYear != null"> AND birth_date >= CONCAT(#{authorQueryVO.startYear}, '-01-01') </if> <if test="authorQueryVO.endYear != null"> AND birth_date <= CONCAT(#{authorQueryVO.endYear}, '-12-31') </if> </where> ORDER BY birth_date DESC LIMIT #{authorQueryVO.offset}, #{authorQueryVO.pageSize} </select>LIKE 查询使用 CONCAT 拼接触发器而不是直接用 #{name},能防止 SQL 注入风险。MyBatis 的 #{ } 预编译机制本身就是安全的,但对模糊查询很多人会图省事写 ${ },这会直接导致注入漏洞。把 LIMIT 参数传成 offset 而不是 pageNum,也是分页查询的一个细节——很多新手会把页码直接传入 LIMIT,导致第一页和第二页数据错乱。
3.4 登录认证与 JWT 校验
管理系统必须做权限控制。这里用的是 JWT(JSON Web Token)方案,流程是:用户提交用户名密码,后端校验通过后签发一个 token 返回前端;前端把 token 存到 localStorage,在每次请求时通过 Authorization 请求头携带;后端拦截器校验 token 有效性,通过后才放行。
登录接口核心代码逻辑如下:
@Override public String login(LoginDTO loginDTO) { User user = userMapper.selectByUsername(loginDTO.getUsername()); if (user == null) { throw new BusinessException("用户不存在"); } String encryptedPassword = DigestUtils.md5DigestAsHex( loginDTO.getPassword().getBytes(StandardCharsets.UTF_8)); if (!user.getPassword().equals(encryptedPassword)) { throw new BusinessException("密码错误"); } Map<String, Object> claims = new HashMap<>(); claims.put("userId", user.getId()); claims.put("username", user.getUsername()); return JwtUtils.generateToken(claims); }密码存储要强调一下:数据库里不能存明文。这个项目用 MD5 做加密虽然简单,但实际生产环境更推荐 BCrypt 这类加盐哈希算法。MD5 的问题在于相同密码会得到相同摘要,容易被彩虹表破解。如果是自己学习,知道这一点并说明原因,反而比闷头用 BCrypt 更能体现对安全的思考。
拦截器代码中要注意对 OPTIONS 请求的特殊处理。跨域请求在发送正式请求前会先发一个 OPTIONS 预检请求,预检请求不会携带自定义请求头,所以拦截器必须放行 OPTIONS,否则前端浏览器的请求会直接报跨域错误。
4. 前端 Vue 实现:从搭建到接口对接
4.1 项目结构与核心依赖
前端工程基于 Vue CLI 创建,考虑到生态稳定性,选用了 Vue 2 搭配 Element-UI。虽然 Vue 3 + Element Plus 已经是新项目的选择,但 Vue 2 + Element-UI 的资料最多、踩坑记录最全,遇到问题最容易找到解决方案。
关键依赖包括:vue-router 负责路由管理,axios 负责 HTTP 请求,element-ui 负责 UI 组件,echarts 负责统计数据可视化。项目结构如下:
src ├── api # 接口请求封装 ├── assets # 静态资源 ├── components # 公共组件 ├── router # 路由配置 ├── store # 状态管理 ├── views # 页面组件 ├── utils # 工具函数 └── App.vue # 根组件4.2 axios 封装与请求拦截器配置
axios 不能直接拿来就用,必须做一层封装。统一配置 baseURL、请求超时时间、请求拦截器和响应拦截器。请求拦截器负责在每次请求时附加上 token;响应拦截器负责统一处理业务错误,比如 token 过期时跳转到登录页。
import axios from 'axios' import { Message } from 'element-ui' import router from '@/router' const service = axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 10000 }) service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config }) service.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { Message.error(res.message || '系统异常') if (res.code === 401) { localStorage.removeItem('token') router.push('/login') } return Promise.reject(new Error(res.message)) } return res.data }, error => { Message.error(error.message || '网络异常') return Promise.reject(error) } ) export default service这里有个细节值得注意:响应拦截器的成功分支同时处理 HTTP 状态码和业务状态码。即使 HTTP 状态码是 200,如果业务 code 不是 200,也要提示错误并进入异常流程。这个写法能避免很多联调时“接口明明返回了但前端没任何提示”的尴尬场景。
4.3 核心页面:列表查询与表单弹窗
作家列表页是这个系统最主要的工作台页面。页面结构是:顶部条件筛选卡片区,中间主体表格区,底部操作按钮。查询表单包含姓名输入框、创作领域下拉选择、出生年份范围输入,点击查询按钮后重新加载表格数据。
热门搜索关键词“条件筛选”在这个系统的实现思路值得展开说一说。一个常见误区是:每次点击查询都重新请求整个列表接口,然后把筛选条件放在后端处理。正确做法是:查询按钮触发的是“重置当前页为第一页并携带参数请求列表接口”,而分页组件切换页码时,则需要在原有筛选条件下拼接 pageNum 和 pageSize。这两个动作共享同一个查询参数对象,但触发的时机不同。
Element-UI 的 el-table 定义列时,超出字数限制的文本内容用 show-overflow-tooltip 属性处理,鼠标悬停展示完整内容;操作列放置“编辑”和“删除”按钮,分别绑定对应的处理函数。这套代码量不小,但对练习后台管理页面开发的人来说,这恰恰是最值得细看的部分。
4.4 统计数据可视化与图表展示
这个项目除了基础 CRUD,还加入了一个统计看板页面。使用 ECharts 展示两个图表:一个是作家出生年代分布饼图,另一个是获奖记录按年份趋势的折线图。数据来源是后端提供的统计接口。
这里涉及一个后端接口设计的知识点:统计接口返回的数据结构应当直接适配前端图表组件的格式,而不是把原始数据丢给前端做二次加工。以年代分布为例,后端直接返回 name-value 结构的列表:
[ { "name": "1940-1949", "value": 12 }, { "name": "1950-1959", "value": 23 }, { "name": "1960-1969", "value": 18 } ]前端拿到这个结构后,几乎不需要任何转换就能直接塞给 ECharts 的 pie 组件。设计接口时考虑消费方的数据格式偏好,是区分“能用”和“好用”的重要标志。
4.5 权限控制与页面路由守卫
前端路由可以通过全局前置守卫来控制访问权限。未登录用户访问后台页面时,直接重定向到登录页。这个逻辑放在 router.beforeEach 中实现:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.path === '/login') { next() } else { if (token) { next() } else { next('/login') } } })前端路由守卫只能提升体验,不能作为真正的安全屏障——真正的校验一定要在后端接口层面实现。也就是说,即使有人绕过了前端页面,直接调用后端接口获取数据,后端拦截器也会拦截未携带 token 的请求。前后端双层的权限校验才是完整方案。
5. 部署流程与全套实操记录
5.1 本地环境准备清单
在开始部署前,需要确认本机环境已经具备以下条件:JDK 1.8 或以上版本、Maven 3.6+、Node.js 14+、MySQL 5.7 或 8.0。这些环境版本要对照着选,比如 JDK 17 配 SpringBoot 2.7 没问题,但配 SpringBoot 2.1 就可能遇到兼容性问题。强烈建议按项目的 README 文档给出的版本安装,不要盲目追求最新版。
数据库初始化需要执行项目提供的 SQL 脚本。脚本会创建数据库、数据表,并往用户表写入一个默认管理员账号。这里会遇到比较常见的坑:MySQL 8.0 默认的认证插件是 caching_sha2_password,某些版本的数据库连接驱动可能不兼容,需要在连接串中显式指定 useSSL=false 等参数,或者在建用户时指定 mysql_native_password。这类问题在部署文档里通常会标注,但很多人喜欢跳过文档直接开跑,结果被卡在这一步。
5.2 后端打包含启动完整流程
后端项目的启动过程分两步:打包和运行。打包使用 Maven:
mvn clean package -DskipTests打包完成后,target 目录下会生成一个可执行的 jar 包。如果项目依赖的外部配置文件(比如数据库账号密码)想独立管理,可以把 application.yml 放在 jar 包同级目录,SpringBoot 会自动优先读取外部配置文件。
启动后端:
java -jar author-manage-server.jar稍微展开讲一下启动过程:SpringBoot 启动时会自动执行数据源初始化检查,如果数据库中缺少表结构就会直接报错。另外,启动端口默认是 8080,如果本机端口已被占用,可以在 application.yml 中修改 server.port。前端代码中 axios 封装的 baseURL 也需要和后端实际端口保持一致。
5.3 前端构建与 Nginx 部署配置
前端开发完成后需要执行构建命令,生成静态文件:
npm install npm run build构建完成后,dist 目录就是包含全部静态资源的产物。把 dist 目录上传到服务器上,用 Nginx 托管。
前后端分离项目中 Nginx 的配置需要注意两个核心点:静态资源路径指定为 dist 目录;前端所有以 /api 开头的请求通过反向代理转发到后端服务。
server { listen 80; server_name your-domain.com; root /path/to/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location / { try_files $uri $uri/ /index.html; } }这里有个细节经常被忽略:proxy_pass 配置的 http://127.0.0.1:8080/ 结尾是否带斜杠,行为完全不同。带斜杠时,Nginx 会把 URL 中匹配 location 的 /api 前缀去掉再转发到后端。也就是说,前端请求 /api/authors,后端实际收到的是 /authors。如果后端接口路径没有 /api 前缀,那这个写法就是正确的;如果后端接口本身就带 /api,这里的斜杠需要去掉。很多部署失败的案例,都栽在这个一字之差上。
try_files 配置对 history 路由模式至关重要,它让前端在刷新非首页路由时能正确返回 index.html,而不是出现 404。如果你用的是 hash 路由模式,这个配置可以不写,但 history 模式下少写了,刷新页面就会白屏或直接报错。
5.4 常见问题与排查方法速查
问题一:前端页面能打开,但列表数据加载失败。
排查顺序是先看浏览器 F12 的 Network 面板,确认请求是否发出;再确认请求 URL 是否正确;然后看响应状态码,401 表示 token 问题,404 表示接口路径与后端不一致,500 表示后端代码异常。这里有个很常见的后端排查指令:
# 查看后端实时日志 tail -f nohup.out问题二:跨域请求被拦截。
确认后端是否配置了 CORS,或者在 Nginx 这一层是否做了跨域处理。用 Nginx 反代方案时,前端和后端使用同一个域名,几乎不会出现跨域问题。如果前后端分别部署在不同的端口,比如前端 8080、后端 9090,那要么在后端配置 CORS,要么用 Nginx 统一入口。推荐后者,因为生产环境最终都建议统一域名。
问题三:MySQL 连接失败,报 Access denied for user。
优先检查密码是否正确,其次检查数据库是否允许远程连接。MySQL 默认配置下 root 用户可能只允许本机登录。学习环境建议单独创建一个业务用户,授予该用户指定数据库的全部权限,而不是直接用 root,这也是一个好习惯。
问题四:打包时 Maven 下载依赖失败。
优先检查本机 maven 仓库配置是否指向了可用的镜像源。国内网络环境下载 Maven 中央仓库的依赖速度可能很慢,可以在 settings.xml 中配置阿里云的公共仓库镜像。
5.5 实际操作中遇到的三个隐蔽坑
坑一:MyBatis 的 XML 文件中出现了“<”符号导致解析报错。
SQL 中小于等于符号不能直接写,必须用转义字符<=。我在一个年份筛选接口里写birth_date <= #{endYear},如果漏掉转义直接写“<”,MyBatis 解析 XML 时会直接抛异常。这种问题表现形式为“XML 文件解析失败”或“Error parsing SQL mapper configuration”。
坑二:前端 npm install 时出现 node-sass 安装失败。
node-sass 这个包经常在学校或公司网络中安装失败,要么等待时间长,要么直接编译报错。这两个招都能解决:第一,切换 npm 镜像源到国内源;第二,将 node-sass 替换为 sass(dart-sass),兼容性更好,安装速度也更快。如果项目里有 @vue/cli-plugin-babel 等依赖锁定 node-sass 版本,可以用 sass 替代并修改引用路径。
坑三:后端启动时报 “Consider defining a field of type 'XXXMapper' that could not be found”。
这说明 Mapper 接口没有注册到 Spring 容器中。解决办法是在启动类上添加 @MapperScan 注解并指定 mapper 包路径,或者在每个 Mapper 接口上加 @Mapper 注解。推荐前者,只需配置一次,全包生效。
6. 项目扩展方向与实际心得
做完这个基础版本之后,有几个扩展方向如果时间和精力允许,很值得尝试。第一个方向是加入数据导入导出功能,后端用 Apache POI 操作 Excel 文件,实现作家信息的批量导入和导出。这个功能在实际管理场景中极其高频,写在简历上也很加分。第二个方向是把统计报表进一步丰富,比如按省份展示作家分布地图、按获奖类型展示词云图,这会用到 ECharts 更高级的图表类型。第三个方向是在系统中引入角色权限,区分“管理员”和“访客”,访客只能浏览查询,不能新增、修改和删除。这一块涉及 Spring Security 或 Shiro,是很多面试官会比较关注的能力。
做这个项目过程中,我最大的感受是:一个系统做得完整,不在于功能多,而在于每个功能都做得闭环。拿搜索功能来说,不只是简单加一个关键词输入框,而是让搜索条件、列表展示、分页、重置、编辑回显这条链路完全打通,才算真正完成。如果只是机械地参照教程敲代码,代码量到了但思考量没到,那做完之后收获非常有限。这个项目的源码我也反复对照过好几遍,每次重新看,都能在新的细节里发现之前忽略掉的东西。这个项目本身就是一个很好的开发训练场。