每年到毕设季节都会有一大批计算机专业的学生把目光放在“基于SpringBoot和Vue的XX信息管理系统”这类题目上。不少人嫌这类题目烂大街、没新意可真动手做才发现系统跑起来不难能不能过答辩、论文有没有东西写拼的全是那些看不见的细节。这篇文章就以“当代中国获奖知名作家信息管理系统”为例子把从选题定需求、数据库建模、后端接口、前端页面、数据采集清洗到论文撰写答辩的全过程拆开讲一遍。文章里涉及的SQL、Java代码、Vue代码都是从实际项目里摘出来的可运行版本。如果你正在做类似的管理系统直接照着自己的表结构改改就能用如果你还在犹豫要不要选这类题看完应该心里也有数了。1. 选题没那么“水”这个系统的需求边界和核心竞争力1.1 “获奖作家信息管理”到底管什么先说一个很多人对毕设题的误解信息管理系统不等于纯增删改查。纯增删改查确实没含金量但“获奖作家信息管理”这个题目有个天然优势——它的数据对象非常清晰而且有足够复杂的关联关系。这个系统管的东西主要有四块作家档案姓名、性别、出生日期、籍贯、民族、头像、人物简介、在世状态。作品信息每部作品的名称、体裁、出版社、出版时间、内容简介并关联到具体作家。获奖记录哪个奖项、哪一届、哪一年、凭借哪部作品获奖。这一块是整个系统最有价值的部分因为一位作家可能获得多个奖项一个奖项也可能同时授予多人和多部作品。普通用户浏览与管理员维护游客和注册用户可以浏览、搜索、查看详情和统计图表管理员负责对以上数据进行维护并对用户进行管理。我当时把系统的核心场景想成一句话一个文学研究者想查“第七届茅盾文学奖的获奖作品是哪几部”或者想查“获得过鲁迅文学奖的山东籍作家有哪些”系统要能在几秒内给出准确结果。1.2 功能清单拆到能直接写代码的粒度需求不能停留在脑子里面要落到功能列表否则后期编码和写论文都会乱。我最终整理成下面这个表功能模块具体功能使用者用户登录注册账号注册、登录、JWT鉴权、退出全部用户作家管理作家信息的增删改查、分页展示、条件搜索、头像上传管理员作品管理作品的增删改查、与作家关联管理员奖项管理奖项名称、主办单位、设立年份的维护管理员获奖记录管理维护作家、奖项、获奖作品、获奖年份的关联关系管理员前台浏览作家列表卡片展示、作家详情页、获奖时间线游客、注册用户数据统计奖项获奖人次统计、获奖年份趋势统计全部用户这个清单看着普通但它保证了项目的完整性既有常规CRUD又有“多表关联查询”和“数据统计可视化”论文里的需求分析、系统设计、系统实现、系统测试四大部分都有内容可写。1.3 为什么说这个题目适合做毕设从带过项目和被答辩折磨过的双重经验来看“获奖作家信息管理”这类题目的核心竞争力在于边界清楚、数据权威、工作量可控。边界清楚作家的获奖信息是公开的、可溯源的不需要像电商系统那样去虚构业务规则。数据权威茅盾文学奖、鲁迅文学奖等全国性重要文学奖项的获奖名单都有官方公示数据真实性问题在答辩时经得起追问。工作量可控数据库3到5张核心表后端十几个接口前端6到8个页面。认真做两个月完全能完成不会出现做不完的情况。但这也意味着如果只做基础的增删改查很容易被评委老师说“工作量不足”。所以这个课题的真正难点不在写代码而在把关联数据做明白把统计做出来把数据准备做足这三件事下文都会展开讲。2. 数据库是系统的第一份交付物作家、作品、奖项三张业务表的建模思路2.1 表结构设计从用户表到业务关联表数据库设计在论文里占的篇幅非常大ER图一画三张表变成五张表工作量立刻就不一样了。我的表结构设计如下sys_user系统用户表存储登录账号、密码、角色。writer获奖作家信息表。work文学作品表。prize文学奖项表。award_record获奖记录表也是核心关联表。用户表没什么好说的重点在业务表的关联关系上。最初我图省事只建了writer和award两张表把获奖作品名称、奖项名称都直接存成字符串字段结果写到一半发现一个很尴尬的问题第三届茅盾文学奖有《平凡的世界》等多部作品同时获奖如果我给作家A记了一条获奖记录正文里又要把作品名写一遍一旦作品信息需要修改所有相关记录都要手工维护这在论文答辩现场是致命的逻辑漏洞。2.2 核心表DDL可以直接复用的建表脚本下面这几个建表语句是我实测可用的版本字符集统一用utf8mb4避免存作家简介里的特殊标点符号时出现乱码。CREATE TABLE sys_user ( id bigint NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录账号, password varchar(255) NOT NULL COMMENT 密码BCrypt加密后存储, nickname varchar(50) DEFAULT NULL COMMENT 显示昵称, role tinyint NOT NULL DEFAULT 1 COMMENT 0管理员 1普通用户, avatar varchar(255) DEFAULT NULL COMMENT 头像URL, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT系统用户表;CREATE TABLE writer ( id bigint NOT NULL AUTO_INCREMENT, name varchar(50) NOT NULL COMMENT 作家姓名, gender tinyint DEFAULT NULL COMMENT 1男 2女, birth_date date DEFAULT NULL COMMENT 出生日期, death_date date DEFAULT NULL COMMENT 去世日期NULL表示在世, hometown varchar(100) DEFAULT NULL COMMENT 籍贯/出生地, nation varchar(30) DEFAULT NULL COMMENT 民族, avatar varchar(255) DEFAULT NULL COMMENT 头像图片URL, biography text COMMENT 人物简介与文学成就, status tinyint DEFAULT 1 COMMENT 1在世 0已故, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id), KEY idx_name (name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT获奖作家信息表;CREATE TABLE prize ( id bigint NOT NULL AUTO_INCREMENT, prize_name varchar(100) NOT NULL COMMENT 奖项名称如茅盾文学奖, organizer varchar(200) DEFAULT NULL COMMENT 主办单位, level tinyint DEFAULT 1 COMMENT 奖项级别 1国家级 2省部级, establish_year int DEFAULT NULL COMMENT 设立年份, create_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_prize_name (prize_name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT文学奖项表;CREATE TABLE work ( id bigint NOT NULL AUTO_INCREMENT, writer_id bigint NOT NULL COMMENT 所属作家id, title varchar(200) NOT NULL COMMENT 作品名称, work_type varchar(30) DEFAULT NULL COMMENT 体裁长篇小说/中篇小说/诗歌/散文/报告文学, publisher varchar(100) DEFAULT NULL COMMENT 出版社, publish_date date DEFAULT NULL COMMENT 出版日期, summary text COMMENT 内容简介, create_time datetime DEFAULT NULL, PRIMARY KEY (id), KEY idx_writer (writer_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT文学作品表;CREATE TABLE award_record ( id bigint NOT NULL AUTO_INCREMENT, writer_id bigint NOT NULL COMMENT 获奖作家id, prize_id bigint NOT NULL COMMENT 奖项id, work_id bigint DEFAULT NULL COMMENT 获奖作品id可空, award_year int NOT NULL COMMENT 获奖年份, session_no varchar(20) DEFAULT NULL COMMENT 届数如第三届, remark varchar(255) DEFAULT NULL COMMENT 备注如同一部作品获奖的并列情况, create_time datetime DEFAULT NULL, PRIMARY KEY (id), KEY idx_writer_prize (writer_id, prize_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT获奖记录表;2.3 五张表之间的关系一个“两两关联”的模型五张表的关系可以用一句话说清楚一个作家可以拥有多部作品一个奖项可以授予多人、多部作品获奖记录表把作家、奖项、作品两两关联在一起。给你画一个文字版逻辑说明writer1——Nwork一位作家有多部作品。writer1——Naward_recordN——1prize一位作家在获奖记录表里有多条记录每条记录对应一个奖项。work1——Naward_record一部作品可以被多条获奖记录引用对应不同奖项。这样设计的好处是查询“某个作家所有获奖情况”时只需要根据writer_id去award_record表里查再join出prize和work反过来查“茅盾文学奖历届获奖作家”也只需要根据prize_id去查逻辑非常清晰。2.4 索引、冗余和取舍的经验建表时我建议在下面几个位置加索引亲测查询速度提升明显writer表的name字段前台作家搜索会频繁按名字模糊查询加了普通索引后数据量到几百条时差异不大但作为规范值得保留。award_record表的writer_id和prize_id这是最高频的关联查询路径联合索引必须加。work表的writer_id作品列表和作家详情页都会用到。有同学可能会问hometown为什么不拆成省市县三张表说实话在毕设这个体量下完全没有必要反而会让代码复杂化。我的做法是直接在hometown字段里存“山东 高密”这样的文本前台展示时原样输出搜索时用like模糊匹配简单直接。另外有一条重要经验主键不要直接传给前端做Long类型使用。Java的Long主键在传递到JavaScript端时超过JavaScript安全整数范围会丢失精度。我当时第一次联调就遇到了这个问题作家详情页点进去显示Not Found查了半天才发现是ID精度丢了。解决方法是后端统一把ID转成字符串或者前端使用Number类型安全范围内的自增ID小数据量场景不会超边界稳妥起见还是转字符串最好。3. SpringBoot后端把CRUD做出含金量的三个关键接口3.1 效率工具MyBatis-Plus以及必须手动写的SQL后端框架上我选的是SpringBoot 2.7 MyBatis-Plus 3.5。选MyBatis-Plus的原因很简单单表CRUD不需要写SQLBaseMapper直接给到方法省下来的时间可以集中处理多表关联和统计查询。dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3/version /dependency引入依赖后Mapper接口只需要继承BaseMapperpublic interface WriterMapper extends BaseMapperWriter { }但有一点必须说清楚MyBatis-Plus解决不了多表查询和分组统计。这些SQL还是要自己写。我项目里有两个接口是手写的SQL一个是“获奖统计接口”一个是“作家详情的聚合查询”。下面重点讲。3.2 JWT登录认证代码结构清晰的实现方式登录部分我使用了JWTJSON Web Token配合SpringBoot拦截器实现接口鉴权。JWT的好处是服务端不需要存储会话状态登录成功后把用户id、姓名、角色编码进token客户端带着token访问受保护接口拦截器验证通过才放行。引入依赖dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency工具类生成tokenpublic class JwtUtil { Value(${jwt.secret}) private String secret; Value(${jwt.expire}) private Long expire; public String generateToken(Long userId, String username, Integer role) { return Jwts.builder() .setSubject(username) .claim(userId, userId) .claim(role, role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() expire)) .signWith(SignatureAlgorithm.HS256, secret) .compact(); } public Claims parseToken(String token) { return Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); } }登录接口的核心逻辑public LoginVO login(LoginDTO dto) { User user userMapper.selectOne( new LambdaQueryWrapperUser().eq(User::getUsername, dto.getUsername())); if (user null || !passwordEncoder.matches(dto.getPassword(), user.getPassword())) { throw new BusinessException(用户名或密码错误); } String token jwtUtil.generateToken(user.getId(), user.getUsername(), user.getRole()); return new LoginVO(token, user.getUsername(), user.getNickname(), user.getRole()); }密码必须加密存储我用的BCrypt算法Spring Security里自带的BCryptPasswordEncoder可以直接单拎出来用不用引入完整的安全框架否则自定义拦截器会和Spring Security的过滤链互相干扰那是另一个大坑。3.3 作家分页查询动态条件SQL的标准模板作家列表页是系统最核心的查询接口支持按姓名、籍贯、在世状态进行组合筛选。MyBatis-Plus的LambdaQueryWrapper对付这种场景非常顺手public IPageWriterVO pageWriter(WriterQuery query) { PageWriter page new Page(query.getPage(), query.getSize()); LambdaQueryWrapperWriter wrapper new LambdaQueryWrapper(); if (StringUtils.hasText(query.getName())) { wrapper.like(Writer::getName, query.getName()); } if (StringUtils.hasText(query.getHometown())) { wrapper.like(Writer::getHometown, query.getHometown()); } if (query.getStatus() ! null) { wrapper.eq(Writer::getStatus, query.getStatus()); } wrapper.orderByAsc(Writer::getBirthDate); return writerMapper.selectPage(page, wrapper); }注意一个细节status字段是tinyint前端传入的是数字。如果前端传了0MyBatis-Plus的eq方法也能正常匹配但如果你使用if (query.getStatus() ! null query.getStatus() ! 0)这种写法就会导致“已故作家”这个筛选项永远查不出来因为条件判断把0过滤掉了。这个问题我在开发时真实遇到过排查了很久。3.4 获奖统计接口给前端ECharts喂数据的SQL统计图表是系统里最能体现工作量的模块也往往是答辩老师问得最多的地方。我做了两个统计各奖项获奖人次统计柱状图获奖年份分布趋势折线图核心SQL可以放在Mapper接口里用Select注解直接写Select(SELECT p.prize_name AS name, COUNT(ar.id) AS value FROM award_record ar LEFT JOIN prize p ON ar.prize_id p.id GROUP BY ar.prize_id ORDER BY value DESC) ListMapString, Object countByPrize();Select(SELECT award_year AS year, COUNT(id) AS total FROM award_record GROUP BY award_year ORDER BY award_year) ListMapString, Object countByYear();这里踩过一个不大不小的坑MyBatis返回的Map里整数字段可能是Long类型也可能是Integer类型取决于数据库驱动和字段类型。前端如果直接把value当作Number处理问题不大但如果你用严格判断类型就会出错。稳妥做法是后端再包一层VO明确字段类型。3.5 跨域配置与开发联调前后端分离开发跨域是躲不掉的问题。我的方案是开发环境用Vite的代理把请求转发到后端端口生产环境在SpringBoot里配置CORS兜底。Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }注意allowCredentials(true)时allowedOrigins不能写成*必须用allowedOriginPatterns这是SpringBoot新版本的一个限制网上很多老教程就没写对照抄后请求一直报跨域错误。4. Vue前端一套符合毕设答辩审美的页面是怎么组织的4.1 技术选型和路由设计前端我用的Vue 3 Vite Element Plus。Vue 3的组合式API写起来逻辑更聚合Element Plus组件库的表格、表单、弹窗、分页组件齐全能大幅缩短开发时间。前端页面分为两个区域面向普通用户的展示区和面向管理员的后台区。路由设计如下路径页面说明/login登录页登录成功后跳转首页/首页作家卡片列表搜索栏/writer/:id作家详情页基本信息、作品列表、获奖记录/admin/dashboard后台首页统计图表/admin/writer作家管理页表格弹窗表单/admin/work作品管理页按作家筛选作品/admin/award获奖记录管理页关联作家、奖项、作品/admin/prize奖项管理页基础数据维护路由守卫在Vue Router里配置防止未登录用户进入后台管理页router.beforeEach((to, from, next) { const user JSON.parse(localStorage.getItem(user) || null) if (to.meta.requiresAuth !user) { next(/login) } else { next() } })4.2 axios封装与token携带前端和后端联调的第一步是把axios封装好所有请求自动携带token提高效率也方便处理401状态码。import axios from axios const request axios.create({ baseURL: import.meta.env.VITE_API_BASE || /api, timeout: 15000 }) request.interceptors.request.use(config { const user JSON.parse(localStorage.getItem(user) || null) if (user user.token) { config.headers.Authorization Bearer user.token } return config }) request.interceptors.response.use( response response.data, error { if (error.response error.response.status 401) { localStorage.removeItem(user) window.location.href /login } return Promise.reject(error) } ) export default request这段代码里最容易被忽略的是响应拦截器的返回值。后端返回的数据结构如果是{code, message, data}那你一定要在这里就返回response.data.data还是response.data统一好规范。我当时因为没有统一导致有的页面拿到的是一层包过的对象有的页面直接拿到了数据本体同一份接口在两个页面表现不一致Debug花了不少时间。4.3 后台表格页Element Plus的标准用法作家管理页是典型的“表格分页搜索弹窗表单”结构。核心模板代码el-table :datatableData border v-loadingloading el-table-column propname label姓名 width120 / el-table-column propgender label性别 width80 template #default{ row }{{ row.gender 1 ? 男 : 女 }}/template /el-table-column el-table-column prophometown label籍贯 width150 / el-table-column propbirthDate label出生日期 width120 / el-table-column label操作 width200 fixedright template #default{ row } el-button typeprimary link clickhandleEdit(row)编辑/el-button el-button typedanger link clickhandleDelete(row.id)删除/el-button /template /el-table-column /el-table el-pagination v-model:current-pagequery.page v-model:page-sizequery.size :totaltotal layouttotal, prev, pager, next changeloadData /表单校验这里有一个很容易被忽略的坑el-form的model对象中日期字段的绑定值格式必须和你编辑回显时赋值的格式一致否则日期选择器的值会显示不出来。比如后端返回的birthDate是2000-01-01字符串而Element Plus的el-date-picker在value-format未指定时默认是Date对象回显时会解析失败。解决方法是给日期选择器加上value-formatYYYY-MM-DD让v-model绑定的始终是字符串。4.4 作家详情页作品表格获奖时间线作家详情页是面向普通用户的核心页面也是最能展示数据关联关系的页面。我把它设计成三个区域顶部作家头像、姓名、生卒年份、籍贯、民族、简介卡片。中部作品列表用表格展示点击可查看简介。底部获奖记录时间线用Element Plus的el-timeline组件展示。获奖时间线组件代码el-timeline el-timeline-item v-foritem in awardList :keyitem.id :timestampitem.awardYear 年 placementtop el-card p{{ item.prizeName }} {{ item.sessionNo }}/p p v-ifitem.workTitle获奖作品《{{ item.workTitle }}》/p /el-card /el-timeline-item /el-timeline这个页面的数据来自后端一个聚合接口根据作家id查出基础信息、作品列表、获奖记录列表。如果分开三次请求前端就要用Promise.all处理反而更复杂。我认为后端聚合查询一次返回最合适这也算是我在后端多写一个自定义SQL的理由。4.5 ECharts统计图表的接入ECharts在Vue 3项目里的使用我采用的是按需模块引入避免打包体积过大。import * as echarts from echarts/core import { BarChart, LineChart } from echarts/charts import { GridComponent, TooltipComponent, LegendComponent } from echarts/components import { CanvasRenderer } from echarts/renderers echarts.use([BarChart, LineChart, GridComponent, TooltipComponent, LegendComponent, CanvasRenderer])初始化图表的代码放在组件的onMounted里onMounted(() { const chart echarts.init(document.getElementById(awardChart)) chart.setOption({ title: { text: 各奖项获奖人数统计 }, tooltip: { trigger: axis }, xAxis: { type: category, data: prizeNames }, yAxis: { type: value }, series: [{ type: bar, data: prizeCounts, barWidth: 30 }] }) })有两个注意点document.getElementById在组件里要先确保DOM已经渲染。如果是v-if控制显隐的页面要以v-if条件为true后再初始化否则拿不到容器节点。数据更新后要调用chart.setOption(newData, true)第二个参数true表示清空旧数据重新渲染否则多次查询时旧数据会叠加。4.6 图片上传和静态资源访问作家头像上传也是必做功能。后端需要提供一个接收MultipartFile的接口把文件保存到本地指定目录并返回可供前端访问的URL。PostMapping(/api/admin/upload) public ResultString upload(MultipartFile file) { String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.)); String fileName UUID.randomUUID().toString().replace(-, ) ext; File dest new File(uploadDir, fileName); file.transferTo(dest); return Result.success(/upload/ fileName); }关键是SpringBoot的静态资源映射配置。如果你把图片存到了D:/data/upload这样的目录必须把URL路径/upload/**映射到本地目录否则前端访问不到图片。Configuration public class WebConfig implements WebMvcConfigurer { Value(${file.upload-dir}) private String uploadDir; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceLocations(file: uploadDir); } }这里有一个血泪教训不要把图片的物理路径直接存到数据库里否则项目换台电脑运行路径全部失效。正确做法是数据库只存/upload/xxx.jpg这样的相对URL实际物理文件夹通过配置文件指定这样项目可以随时迁移。5. 获奖数据从哪来数据采集、校验和清洗的真实过程5.1 先定义清楚“当代获奖作家”的范围这是论文“需求分析”章节必须交代清楚的一步也是答辩时评委大概率会问的问题。“当代”这个词在文学界有界定讨论我采用的是最稳妥、最容易查证的口径本系统以1949年以后在中国大陆进行文学创作并获得全国性重要文学奖项的作家为数据主体同时收录了部分获得国际重要文学奖项的中国作家案例。奖项范围包括茅盾文学奖、鲁迅文学奖、老舍文学奖、全国优秀儿童文学奖以及诺贝尔文学奖等。这个定义把数据边界画得一目了然评委一听就知道你做过功课而不是网上随便抓了几条数据就填进去。5.2 权威数据源哪些渠道能拿到可靠的获奖名单数据真实性是这类系统最容易被追问的部分。我的数据来源分为三类中国作家协会官网茅盾文学奖、鲁迅文学奖的历届获奖名单公示这是最权威的渠道。官方公示页面提供了完整的届数、获奖作家、获奖作品列表。各奖项官方网站和成立文件比如老舍文学奖的相关资料用于确认主办单位和设立年份。出版社信息作品出版信息通过出版社官网或正规图书信息平台核对出版时间和出版社名称。我建议在论文的“数据来源”部分单独写一段数据采集过程和来源清单并附上数据量的统计表格这个细节在答辩时会加分。5.3 数据清洗阶段遇到的三类问题采集到的原始数据不能直接往数据库里塞一定要经过清洗。我遇到的最典型三个问题同名作家混淆文学界存在姓名相同的作家例如“李锐”就有多位。我的处理方案是用出生年份和籍贯辅助区分写入数据库时备注规范名展示时清楚标注。一届多人的并列获奖茅盾文学奖每届通常评选出多部获奖作品不是一人独享。在award_record表里多位作家可以拥有同一届同一年份的获奖记录分别关联自己的作品这就是当时坚持拆出关联表的价值所在。笔名和本名不一致有作家以笔名行世系统统一以作品署名作为作家姓名人物简介字段中说明本名保持展示一致性。另外数据库的日期字段格式要统一。老数据里年份经常是“1982年”这类带文字的文本入库前必须统一解析成标准日期格式或int类型的年份字段否则前端表格排序和时间线组件都会出问题。5.4 初始数据量多少合适我第一次录入数据时只录了8位作家后来发现前台页面空荡荡的统计图表只有可怜的一个柱子论文截图非常难看。后来按下面的数量重新整理作家30位左右覆盖茅盾文学奖、鲁迅文学奖等主要奖项的代表性获奖者。作品每位作家2到5部代表作总量80到150条。获奖记录每位作家1到4条获奖记录总量50到100条。这样数据量既能撑起前台列表的分页展示又能在统计图表上看到明显的分布趋势系统演示效果不会显得单薄。6. 论文、测试和答辩系统之外才是决定能不能过的一环6.1 测试用例怎么设计才不落俗套很多同学写测试章节只会写“登录测试”“查询测试”这种空泛的用例答辩老师一眼就看出来是在凑字数。我的做法是把测试用例和系统核心逻辑绑定让别人感受到你确实跑过完整流程。模块用例名称操作步骤预期结果登录模块管理员错误密码登录输入错误密码点击登录提示“用户名或密码错误”登录模块未登录访问后台接口不携带token调/upload接口返回401状态码前端跳转登录页作家模块按籍贯模糊搜索搜索“山东”正确返回籍贯包含山东的作家列表获奖模块为作家新增获奖记录选择作家和奖项填写届数年份列表展示完整获奖信息详情页时间线同步更新关联模块删除获奖记录关联操作删除作品查看获奖记录获奖记录保留作品信息显示为空数据不被级联删除统计模块刷新统计图表新增获奖记录后查看图表图表数字同步更新细节提示你设计的测试用例越具体越能体现你真正跑过系统。我当时还测了“连续快速点击分页按钮”和“搜索关键词为空时查全部”这种边界情况写进论文里工作量瞬间显得充实。6.2 论文各章节的写作重点和篇幅分配一篇系统的毕业设计论文我建议按下面的思路分配内容摘要与绪论重点写“为什么要做这个系统”和“国内外类似文学数据库的建设现状”。文学获奖数据公开是客观事实但“缺乏面向公众的整合型查询工具”这个切入点是可以讲的。需求分析画用例图把角色权限用表格列清楚。功能需求和非功能需求分开写。系统设计画系统架构图、数据库ER图、接口设计表。这部分是论文最核心的部分建议多花时间把ER图画规范。系统实现不能粘贴全部代码而是选关键模块的代码段配合截图。我的顺序是登录鉴权、作家分页查询、获奖关联保存、统计图表实现一个模块一张页面截图和一段核心代码。系统测试按照上面说的表格方式附上功能测试结果和部分界面截图。画图工具方面数据库ER图可以用专门的建模工具导出系统架构图用绘图工具画。需要注意图片的字体大小和配色统一很多同学直接把代码截图贴进论文字体大小不一给导师的印象很差。6.3 答辩高频问题与回答思路结合自身答辩和其他同学的经验下面这几个问题出现频率极高我直接把回答思路捋一遍为什么用JWT而不是Session回答思路前后端分离架构下Session需要服务端存储会话状态不利于水平扩展JWT无状态客户端保存token服务端只负责验证签名更契合前后端分离架构。如果token过期了怎么办回答思路前端在响应拦截器里统一捕获401状态码清空本地登录信息并跳转登录页。还可以多说一句“本项目未做token自动续期这是后续可以扩展的方向”主动暴露一个合理的小缺陷会让答辩更真实。数据库为什么拆成五张表回答思路作家、作品、奖项是三个独立实体获奖记录是多对多关系的关联表拆表可以避免数据冗余支持复杂查询。数据是怎么保证真实可靠的回答思路官方公示渠道采集按姓名、作者归属、作品出版信息多维校验遇到同名作家用出生年月和籍贯区分。系统有哪些不足回答思路采用静态token没有实现自动刷新图片上传保存在本地磁盘生产环境应接入对象存储未做数据导入导出功能。建议说两到三点即可不要把自己方案说成一无是处。6.4 最后的几个小建议代码全部做完后建议留出一周左右时间专门打磨论文和演示流程不要卡在答辩前一天还在改Bug。我做这个系统时最深的体会是真正拉开差距的不是技术栈有多新而是你愿不愿意把数据整理干净、把测试过程跑完整、把论文里每一张图做得规范。SpringBoot和Vue本身都是非常成熟的技术任何管理系统的骨架都一样最后能拿出来说的一定是你围绕这个特定业务做的那些细致功夫。如果时间充裕可以在基础功能上加一个“作家获奖数据年度动态报告”之类的醒目功能如果时间紧张就守住分页查询、多表关联、统计图表这几个核心点把每一步做扎实答辩不会出大问题。