资讯详情

资讯详情

SpringBoot+Vue前后端分离的作家信息管理系统设计与实践

做这类管理系统项目的人应该不少但能把一个主题做出完整闭环的并不多见。这次分享的项目是一个前后端分离的当代中国获奖知名作家信息管理系统技术栈锁定在 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 统一返回体与全局异常处理前后端分离项目最忌讳接口返回格式不一致。有的接口返回对象有的接口返回字符串前端处理起来极其痛苦。我在这个项目里定义了一个统一返回体 Resultpublic class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(操作成功); result.setData(data); return result; } public static T ResultT error(Integer code, String message) { ResultT 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 { ListAuthorVO selectAuthorList(Param(authorQueryVO) AuthorQueryVO queryVO); Long countAuthorList(Param(authorQueryVO) AuthorQueryVO queryVO); }注意这里把查询对象封装成了一个独立的 VO 类而不是散落的多个参数。这样一个对象参数对应一个查询条件集合后面增加筛选条件时只需修改 VO 字段。对应的 XML 文件里使用动态 SQL 拼接查询条件select idselectAuthorList resultTypecom.example.author.vo.AuthorVO SELECT id, name, gender, birth_date, death_date, hometown, nation, field, introduction, avatar FROM author where if testauthorQueryVO.name ! null and authorQueryVO.name ! AND name LIKE CONCAT(%, #{authorQueryVO.name}, %) /if if testauthorQueryVO.field ! null and authorQueryVO.field ! AND field #{authorQueryVO.field} /if if testauthorQueryVO.startYear ! null AND birth_date gt; CONCAT(#{authorQueryVO.startYear}, -01-01) /if if testauthorQueryVO.endYear ! null AND birth_date lt; CONCAT(#{authorQueryVO.endYear}, -12-31) /if /where ORDER BY birth_date DESC LIMIT #{authorQueryVO.offset}, #{authorQueryVO.pageSize} /selectLIKE 查询使用 CONCAT 拼接触发器而不是直接用 #{name}能防止 SQL 注入风险。MyBatis 的 #{ } 预编译机制本身就是安全的但对模糊查询很多人会图省事写 ${ }这会直接导致注入漏洞。把 LIMIT 参数传成 offset 而不是 pageNum也是分页查询的一个细节——很多新手会把页码直接传入 LIMIT导致第一页和第二页数据错乱。3.4 登录认证与 JWT 校验管理系统必须做权限控制。这里用的是 JWTJSON 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(密码错误); } MapString, 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某些版本的数据库连接驱动可能不兼容需要在连接串中显式指定 useSSLfalse 等参数或者在建用户时指定 mysql_native_password。这类问题在部署文档里通常会标注但很多人喜欢跳过文档直接开跑结果被卡在这一步。5.2 后端打包含启动完整流程后端项目的启动过程分两步打包和运行。打包使用 Mavenmvn 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 中小于等于符号不能直接写必须用转义字符lt;。我在一个年份筛选接口里写birth_date lt; #{endYear}如果漏掉转义直接写“”MyBatis 解析 XML 时会直接抛异常。这种问题表现形式为“XML 文件解析失败”或“Error parsing SQL mapper configuration”。坑二前端 npm install 时出现 node-sass 安装失败。node-sass 这个包经常在学校或公司网络中安装失败要么等待时间长要么直接编译报错。这两个招都能解决第一切换 npm 镜像源到国内源第二将 node-sass 替换为 sassdart-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是很多面试官会比较关注的能力。做这个项目过程中我最大的感受是一个系统做得完整不在于功能多而在于每个功能都做得闭环。拿搜索功能来说不只是简单加一个关键词输入框而是让搜索条件、列表展示、分页、重置、编辑回显这条链路完全打通才算真正完成。如果只是机械地参照教程敲代码代码量到了但思考量没到那做完之后收获非常有限。这个项目的源码我也反复对照过好几遍每次重新看都能在新的细节里发现之前忽略掉的东西。这个项目本身就是一个很好的开发训练场。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →