1. 这个毕设选题真正要解决的需求与整体技术架构前一阵有同学问我说想做一个“有内容、不空洞”的Java Web毕设既不想纯堆CRUD又不想一上来就搞微服务把毕业设计做成灾难现场。聊来聊去我建议他看看“当代获奖作家信息管理”这类方向后来我们又一起把整个项目从零到一跑通了。今天这篇文章就把这个完整项目的落地过程、关键代码逻辑、建库思路和联调经验一次性说清楚。很多同学看到“作家信息管理系统”第一反应是这不就是换个壳的图书管理吗实际上差别很大。图书管理管的是物书核心字段是书名、ISBN、出版社、库存。而作家管理系统管的是“人人的成就人的作品”它是典型的“人物档案型”业务需要围绕作者维度展开把获奖经历、代表作、获奖类型、年度分布、出版社关联等信息串起来。这种业务模型在企业里对应的是“专家库”“人才档案系统”在文化单位里对应的是“创作人才管理系统”所以用来做毕设不仅能讲清楚还能往行业场景上延展。当时定下的需求是这样的系统分管理员和普通用户两种角色管理员对作家信息、获奖信息、作品信息做增删改查普通用户只能浏览和检索。支持按作家姓名、获奖年度、获奖类型比如“茅盾文学奖”“鲁迅文学奖”等多类奖项筛选检索。每位作家维护一份档案页包含基本信息、获奖记录列表和代表作列表。提供数据统计看板比如每年获奖数量趋势、奖项类型占比让系统不止是“表格增删改”还有分析价值。技术选型上用的是当前Java Web毕设最稳的组合Spring Boot作为后端主体框架MyBatis-Plus作为数据访问层MySQL存储数据Spring Security JWT做认证授权前端使用Vue 2 Element UI通过Axios调用后端RESTful接口。这套选型没有盲目追新原因很简单毕设周期有限这套方案的资料密度高、问题排查容易、答辩时又能往“工程化”“安全认证”“前后端分离”等多个方向延展。整个系统的架构分层是典型的“前后端分离 后端分层”结构层级技术/组件职责前端展示层Vue 2 Element UI Axios页面渲染、交互控制、调用后端接口后端控制层Spring Boot Controller接收请求、参数校验、统一响应封装业务逻辑层Service 接口 实现核心业务规则、数据组装、事务控制数据访问层MyBatis-Plus Mapper单表CRUD、条件查询、分页安全认证Spring Security JWT登录认证、接口访问控制数据层MySQL含初始化SQL脚本持久化存储下面我按实际开发顺序——从数据库建模到后端接口到前端页面再到联调部署——把关键部分拆开讲。每个部分都会解释“为什么这么做”而不只是贴代码。2. 数据库设计与SQL脚本里的关键决策数据库设计是整个项目的地基。这块如果偷懒后面写后端代码会一直返工。我们当时反复改了好几版最终沉淀下来一套比较成熟的建表方案5张核心业务表 2张系统表。2.1 作家信息表的设计思路作家基本信息表是最核心的表但它不能只是“姓名性别出生日期”。信息管理系统最有价值的地方在于档案的完整性和独立扩展性。所以我把作家表拆成了两部分一部分是常规的字段在作家表里直接放另一部分是“获奖记录”和“作品档案”分别独立成表用外键关联。作家基础表author的结构字段类型说明idbigint主键自增author_namevarchar(50)作家姓名gendertinyint性别1男 2女 0未知birth_datedate出生日期death_datedate逝世日期空表示在世nationalityvarchar(50)国籍/民族native_placevarchar(100)籍贯biographytext生平简介长文本portrait_urlvarchar(255)作家照片存储路径create_timedatetime创建时间update_timedatetime更新时间deletedtinyint逻辑删除标记0未删 1已删这里有两个很容易被忽略但答辩时很好讲的设计点第一逻辑删除字段 deleted。毕设里很多同学做删除就是物理DELETE这个不是不行但如果想体现“工程化意识”逻辑删除是加分项。MyBatis-Plus对逻辑删除支持得很好配置一下全局逻辑删除字段所有删除操作自动变成UPDATE。第二death_date允许为空。很多作家还在世“逝世日期”本来就是可空字段。这个细节虽然小但直接体现了是否做过真实业务的分析而不是机械地给每个字段都设成NOT NULL。2.2 获奖记录表和作品表怎么关联获奖记录表award_record字段类型说明idbigint主键author_idbigint关联作家ID索引award_namevarchar(100)奖项名称如茅盾文学奖award_typevarchar(50)奖项类型小说类/诗歌类/散文类等award_yearint获奖年份award_workvarchar(200)获奖作品名称create_timedatetime创建时间作品表work字段类型说明idbigint主键author_idbigint关联作家IDwork_namevarchar(200)作品名称work_typevarchar(50)体裁长篇小说/短篇/诗集/散文集publishervarchar(100)出版社publish_yearint出版年份isbnvarchar(30)ISBN编号introductiontext内容简介为什么单独拆两张表而不直接在作者表里用逗号分隔存“奖项集合”或者“作品集合”因为一对多关系必须用独立表存储才能支持高效的查询和统计。举个具体场景管理员想知道“2023年获得过鲁迅文学奖的作家有哪几位”如果奖项存在作者表的一个拼接字段里SQL写起来极其痛苦只能先查出来再在Java里分割字符串拆成独立表之后一句SQL就能解决SELECT a.author_name, a.nationality, r.award_work FROM author a INNER JOIN award_record r ON a.id r.author_id WHERE r.award_name 鲁迅文学奖 AND r.award_year 2023;这就是关系型数据库范式设计的价值也是答辩时老师一定会问的点“为什么这么拆表”这个答案足够清晰。用户表sys_user和角色表sys_role就比较常规了一个用户一个角色用user_id和role_id关联。密码存储用的是BCrypt加密后的密文不是明文。这里强调一下Spring Security自带的BCryptPasswordEncoder可以直接用不需要额外引包安全性足够撑起毕设答辩。补充一个建表时容易踩的坑不要给每个表都加一个“备注”字段然后全填“暂无”。这种为了凑字段而凑出来的列答辩时老师随便一问就会露馅。真正的业务字段应该来自“未来这个系统会被谁用、用的时候需要看什么信息”从这个角度倒推字段设计。3. 后端核心接口设计从RESTful规范到实现细节后端是整个系统的中枢。当初设计接口的时候定了一个原则所有接口返回统一响应结构包括状态码、消息和数据。这个设计在联调阶段省了非常多事前端解析逻辑可以完全一致不需要为每个接口单独处理异常分支。3.1 统一响应结构与分页接口统一响应类我封装为Result核心就是三个字段public class ResultT { private Integer code; private String message; private T data; // 静态方法 success() / error() 快速构造 }code200表示成功code500表示业务异常code401表示未登录或token过期。前端拿到code之后统一判断然后从data里取数据这个模式写起来非常顺。分页查询接口是管理系统的重头戏参考了MyBatis-Plus官方推荐的方式后端接收pageNum和pageSize返回总记录数和当前页数据列表。以作家列表为例GetMapping(/author/page) public ResultIPageAuthorVO pageAuthors(RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, RequestParam(required false) String keyword, RequestParam(required false) Integer awardYear) { PageAuthor page new Page(pageNum, pageSize); LambdaQueryWrapperAuthor wrapper new LambdaQueryWrapper(); if (StringUtils.hasText(keyword)) { wrapper.like(Author::getAuthorName, keyword) .or().like(Author::getNationality, keyword); } // awardYear 有值时关联 award_record 表过滤 IPageAuthorVO result authorService.pageAuthorsWithAwards(page, wrapper, awardYear); return Result.success(result); }这里有个细节关键字检索用的是LIKE而不是因为在管理系统的真实使用场景里用户更习惯“输入一个姓氏或片段”来找人而不是必须输入全名。这个操作虽然简单但是贴近真实需求。为了避免前端拿到的JSON数据里出现MyBatis-Plus的IPage内部字段比如records、total、size之类不好看的结构我在Service层做了一个转换把分页对象里的实体列表转换成对应的VO对象列表再塞回一个新的分页对象里。这样前端拿到的就是干净的{ records: [...], total: 100, current: 1, size: 10 }。3.2 作家详情页的聚合查询作家详情页是用户进入系统后看得最多的页面。它需要同时展示作家基本信息、获奖记录列表、作品列表。这个需求如果写三个接口让前端分别调也不是不行但会产生三次网络请求而且聚合逻辑放前端会让前端代码变重。更合理的做法是后端提供一个聚合接口一次返回所有详情数据。GetMapping(/author/detail/{id}) public ResultAuthorDetailVO getAuthorDetail(PathVariable Long id) { AuthorDetailVO vo new AuthorDetailVO(); Author author authorService.getById(id); ListAwardRecord awards awardService.list( new LambdaQueryWrapperAwardRecord() .eq(AwardRecord::getAuthorId, id) .orderByDesc(AwardRecord::getAwardYear)); ListWork works workService.list( new LambdaQueryWrapperWork() .eq(Work::getAuthorId, id) .orderByDesc(Work::getPublishYear)); BeanUtils.copyProperties(author, vo); vo.setAwardList(awards); vo.setWorkList(works); return Result.success(vo); }这个接口的实现技术上不难但有一个面向对象设计的考量点为什么把聚合放在Service而不是直接在Controller里写因为如果直接在Controller里注入三个Mapper然后拼装数据Controller会越来越臃肿而且这个拼装逻辑没法被复用的Service方法替代。业务逻辑一层、控制器一层各司其职代码就长期保持清爽。3.3 JWT认证与接口权限控制系统不是所有接口都对游客开放的比如新增作家必须管理员权限。我们用Spring Security JWT实现了一个轻量的认证授权流程。登录成功之后后端生成一个JWT令牌返回给前端。前端把token存在localStorage里之后每次调用Axios请求时在请求头里带上Authorization: Bearer token。后端通过一个过滤器OncePerRequestFilter解析token把用户信息和角色信息放进SecurityContextHolder这样后续的接口就可以通过Spring Security的注解做权限控制。具体到接口上管理员专属的操作加上PreAuthorize(hasRole(ADMIN))用户查询接口则允许登录用户访问。这样做还有一个隐藏好处普通用户即使猜到了新增接口的URL也无法调用成功——因为Spring Security在到达Controller之前就会拦截并返回403。这个设计在答辩演示时效果很好老师如果追问“系统安全怎么做的”就能用这个链路来完整回答。这里给一个开发时的实践建议JWT的密钥不要硬编码在代码里放在application.yml配置文件中并通过Value注入。如果代码要传到公开的Git仓库密钥一旦泄露对方就可以伪造任意用户身份这是个很直接的安全隐患。4. 前端页面实现组件化拆分与数据流管理前端用的是Vue 2 Element UI整体是标准的中间布局左侧导航菜单右侧内容区。项目结构上比较规整按模块拆分了views目录和components目录。很多同学的毕设前端是找一个模板然后改文字结果答辩时老师随便点一个页面问“这个数据是从哪来的”完全答不上来。所以我的建议是哪怕用模板也要自己把每个页面的数据流彻底理清楚——数据从哪个接口来保存在哪个变量里通过什么事件触发刷新这些都要能脱口而出。4.1 登录页与路由守卫登录页的逻辑很简单表单校验通过后调用/auth/login接口把返回的token和用户信息存储起来然后跳转到首页。重点在路由守卫这块// 前端路由守卫未登录用户强制跳转登录页 router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.public) { next() // 公开页面如登录页 } else if (!token) { next({ path: /login }) } else { next() } })这个逻辑不复杂但它配合后端接口的JWT校验就形成了一个完整的前后端闭环防线。即使有人绕过前端直接调接口后端也一样能把未认证请求拦下来。这里可以顺便介绍一个小知识点登录页接口本身必须是公开的否则用户连登录都做不到所以在登录接口的权限配置里要放行。4.2 作家管理页面的表格与弹窗表单作家管理页面是后台最核心的页面主要功能包括分页表格展示作家列表含照片、姓名、国籍、生卒年顶部搜索栏关键词 获奖年度下拉新增/编辑弹窗表单删除操作二次确认点击“详情”跳转到作家的完整档案页表格部分用el-table绑定从接口拿到的records数组操作列里放置“编辑”“删除”“详情”三个按钮。新增和编辑共用一个弹窗表单组件AuthorFormDialog通过父子组件传值控制显示// 父组件中控制弹窗打开 openEditDialog(row) { this.dialogVisible true this.$refs.authorFormDialog.init(row) // row为null表示新增有值表示编辑 }这里用ref调用子组件方法而不是用props传数据再监听变化原因是当编辑和新增复用同一个表单时需要主动重置表单数据和校验规则用ref调用子组件暴露的init方法语义上更清晰。4.3 数据统计分析模块的前端呈现统计看板我选了ECharts来做图表渲染。后端提供两个统计接口一个是获奖年度趋势按年统计获奖数量一个是奖项类型分布按奖项分类聚合计数。前端用ECharts的柱状图和饼图来展示。这里的核心逻辑在于把后端返回的平铺列表转成ECharts需要的data结构。比如后端返回的是[{year: 2018, count: 3}, {year: 2019, count: 5}]柱状图的xAxis需要的是[2018, 2019]series需要的是[3, 5]这中间只需要一次简单的map转换。图表不是装饰品。我在图表下方加了一个点击事件点击某一年的柱子页面跳转到作家列表页并且自动在该年的筛选条件下展示那年获奖的作家列表。这个交互算是一个亮点让“统计图表”和“实际明细数据”联动起来而不只是停留在“画个图看看”。5. 前后端联调、部署验证与毕设答辩加分技巧项目写完到最后能跑起来中间还会遇到几个典型的坑。这里把最常踩的三类问题拿出来说。5.1 跨域配置与Axios拦截器前端开发服务器默认在localhost:8080Vue CLI默认端口后端在localhost:8081端口不同必然产生跨域问题。解决办法是我在后端里加了一个全局CORS配置类允许指定的前端地址跨域访问。同时在前端Axios封装里统一处理请求头// axios 请求拦截器自动附加token service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }) // 响应拦截器统一处理code401时跳转登录 service.interceptors.response.use( response { const res response.data if (res.code 401) { router.push(/login) } return res }, error { return Promise.reject(error) } )这个拦截器一加全站所有接口自动带token不需要每个组件单独处理。联调时测试登录后能否正常访问受保护接口就是通过这个机制验证的。5.2 SQL脚本的导入顺序与数据初始化项目交付里带了一份完整的SQL脚本按顺序包含建库语句、建表语句先用户表、角色表再业务表、初始化数据管理员账号、示例作家、获奖记录、作品数据。初始化数据是很多同学容易忽略的——如果系统打开是一张空表演示起来非常干瘪答辩观感差很多。我往初始化脚本里放了大约10位作家信息每位作家配2-4条获奖记录和2-3条代表作品确保前端列表页、详情页、统计页都有真实数据可展示。统计页的数据量虽然不多但画出来的趋势图已经有起伏足够说明功能跑通了。这块看情况还可以多填充一些数据越多图表越好看。5.3 部署顺序与演示前的确认事项课设项目通常是在本地演示但部署步骤还是要清楚。生产部署的话需要先把前端执行npm run build产出dist目录用Nginx托管静态文件并做反向代理/api开头的请求转发到后端Spring Boot服务。后端直接打成jar包用java -jar启动。为了演示不翻车我总结了一个“演示前检查清单”确保MySQL服务启动且已导入最新的SQL脚本。后端jar包启动时注意application.yml里的数据库账号密码是否匹配本地环境。前端通过Nginx部署时确保反向代理配置正确刷新子路由页面不会404。演示登录用管理员账号密码尽量简单好记比如admin123避免现场因为密码复杂输错。如果演示过程中需要新增数据最好提前准备好一条“不会出错”的测试数据现场填入最关键字段即可。5.4 答辩时怎么把项目讲出深度很多同学答辩时习惯从头到尾把代码念一遍这个效果很差。更好的方式是从“实际需求痛点我的解决方案”入手。我这里准备了一套说辞供参考“我在做这个项目之前调研了文化艺术管理领域的实际需求发现作家档案的数据往往是分散的获奖信息和作品信息很难统一检索和管理。所以我的系统重点解决三个问题一是作家档案的集中管理二是基于获奖维度的多条件检索三是基础的数据统计分析。技术上后端采用Spring Boot构建RESTful接口前端用Vue实现前后端分离认证方面通过JWT保证接口安全数据库设计上把一对多关系拆分成独立表保证查询效率和数据一致性。整套系统已经跑通所有核心操作按钮均可正常使用。”这套话术既不空洞又覆盖了需求分析、技术方案、工程实现三个层次不管老师从哪个角度追问都能接得住。6. 跑通Demo后我还额外做了哪些改进毕设如果止步于“能跑”答辩通过没问题但距离“有亮点”还有一段路。我后来又在基础功能之上加了三个小改进每个的投入都不大但对项目整体观感提升明显第一个改进是作家头像上传。Element UI自带Upload组件后端提供了文件上传接口把图片保存到服务器本地磁盘目录数据库里存URL路径。前端表格里就能显示头像缩略图详情页显示大头像视觉感受立刻从“管理系统”升级到“富媒体档案系统”。第二个改进是获奖记录的批量录入。一个作家可能获得过七八个奖项如果让管理员一条条新增效率太差。我在获奖记录编辑弹窗里支持了动态添加多条记录一次性提交保存。后端接收List类型参数遍历写入数据库。这个体验优化很实用演示的时候也能拉好感。第三个改进是全文检索的简化实现。本来想引入Elasticsearch但毕设阶段引入它有点头重脚轻。后来我用MySQL的LIKE %keyword%对作家姓名、国籍、生平简介做多字段模糊搜索效果完全OK。如果接的是MySQL 5.7以上版本还可以顺手试试全文索引但普通场景LIKE就够了。这段经历让我意识到技术选型不是越重越好“够用且能讲清原理”才是毕设的正确姿势。7. 我踩过的坑希望你绕开最后分享几个实际开发过程中让我印象深刻的坑每个都是真实遇到过并且花了时间排查的。第一个坑是日期时间字段的格式化问题。MySQL的datetime类型传到前端变成了2025-01-06T16:00:00.00000:00这种带T的格式显示在表格里非常丑。解决方法是后端在application.yml中统一配置spring.jackson.date-formatyyyy-MM-dd HH:mm:ss同时注意时区设置为GMT8。如果前后端用的是JavaScript的Date对象用dayjs处理一下也行。第二个坑是MyBatis-Plus逻辑删除和唯一索引的冲突。我给作家表的author_name建了唯一索引结果逻辑删除后再次插入同名作家时会报重复错误。解决方式是去掉数据库层的唯一索引在Service层先做一次“未删除且同名”的校验。这个坑很有代表性——唯一索引加在逻辑删除表上几乎必然出问题。第三个坑是前端表单校验字段名和后端实体字段名不一致。前端传的是author_name后端实体用的是authorName如果没注意下划线驼峰转换提交后永远拿不到值。这个最笨但也最稳妥的解决办法是统一规范后端传JSON时保持驼峰命名前端直接使用驼峰字段数据库字段如果习惯下划线就依赖MyBatis-Plus的map-underscore-to-camel-case自动转换。这三个坑每一个在搜索引擎上都有一堆人问我当时是逐个踩完才完全搞明白。如果你正在复现这个项目遇到同类问题可以直接按上面的思路处理能省至少一个晚上。