很多准备做Java毕业设计的同学,一开始盯上的都是“学生管理系统”“图书管理系统”这类经典题目,写完才发现答辩老师根本提不起兴趣,问两句就没了下文。我个人更建议试试带“数据味道”的题目,比如Spring Boot + 大数据技术的诗词信息系统。这个题目一方面不脱离Spring Boot的主线,另一方面又能把数据采集、清洗、统计分析、可视化串成一条完整链路,答辩时讲起来有层次,写在简历上也能体现项目沉淀。
这套系统本质上是一个BS架构的诗词信息管理平台,核心业务包括用户登录注册、后台信息维护、诗词与诗人的检索筛选,以及基于数据统计生成的各类可视化看板。“基于大数据技术”主要体现在数据预处理、中文分词、词频统计、排行榜聚合、图表展示这几层,并不是说要真的去搭一套分布式集群。适合已经学过Spring Boot基础、想通过一个完整项目把后端、数据库、前后端联调、数据可视化全部过一遍的同学,也适合需要参加毕业设计答辩、想让技术栈更完整的人。
我最近正好把一个类似的模拟项目X从零搭到了可验收状态,过程中踩了不少坑。这篇就把完整的设计思路、技术选型原因、核心模块实现、大数据流程落地和排错过程都整理出来,换一个业务场景,绝大部分思路也能复用。
1. 选题价值与课题边界:为什么说诗词数据自带“大数据”属性
1.1 诗词数据和传统的“图书管理”有什么本质区别
很多同学选管理类系统,是因为结构清楚、照着课本就能做。但这类项目有个通病:数据太“干”,只有增删改查,没有分析挖掘的余地。诗词信息系统的特点在于,它的数据天生带有文本密度高、分类维度多、统计分析价值大的属性。
以一套收录六千首左右诗词的演示数据集为例,至少能拆出四个维度:朝代、作者、题材类型、诗词内容。朝代和作者可以做排行榜,内容可以做高频词统计、意象提取,甚至可以做同一朝代的风格对比。这些都不是靠两张表、三个CRUD接口能糊弄过去的,必须引入清洗、分词、聚合、缓存、可视化这一整套处理思路,所以“大数据技术”这个前缀是站得住的。
大多数人听到“大数据”会觉得要上Hadoop搭集群,其实在毕业设计这个场景里,老师更看重的是你有没有数据思维,能不能把一条完整的数据处理链路跑通。我个人经验是:与其堆一套跑不动的Spark任务,不如把清洗、分析、可视化的每个环节都做实。
1.2 课题边界怎么划才不容易烂尾
毕设最怕贪大,需求一多,功能就散。我给这套系统定的边界很简单:一条数据流转主线“采集入库、管理维护、检索展示、统计分析、可视化呈现”,所有功能都必须挂在这条主线上,挂不上的先砍掉。
功能模块规划大概是这样:
| 模块 | 核心功能 | 技术点 |
|---|---|---|
| 用户与权限 | 注册、登录、角色区分 | JWT、拦截器 |
| 诗词信息管理 | 诗词/作者/朝代维护、批量导入、数据清洗 | MyBatis、Excel解析 |
| 检索筛选 | 关键词搜索、多条件组合筛选 | 索引、分词 |
| 数据统计分析 | 诗人排行榜、朝代词频、意象统计 | 定时任务、Redis |
| 可视化看板 | 柱状图、饼图、词云、趋势图 | ECharts、接口聚合 |
| 基础运维 | 缓存清理、数据统计、登录日志 | Redis、定时任务 |
不要做的内容也要提前想清楚。比如不做移动端、不做在线创作社区、不做复杂协同过滤推荐。诗词推荐可以做简单的规则推荐,比如“同作者”“同题材”,但不要去碰基于用户行为的大规模协同过滤,那个方向要的样本量和时间成本,毕设扛不住。把需要做的模块控制在六个以内,每个模块深挖细节,效果比堆十几个半成品要好得多。
1.3 开题前先盘好技能点和环境
在做任何编码之前,先把下面这些东西准备好,后面能省很多事:
- Java开发环境:JDK 8或11、Maven 3.6+、IDEA
- MySQL 5.7或8.0,Redis 6.x
- Node环境(做前后端分离时用,配Vue和打包工具)
- Postman或Apifox,用于调试接口
- 中文分词库的依赖,比如基于HanLP的轻量封装,或IK分词器的Java版本
- 一个公开的诗词语料数据集,建议提前确认授权方式和格式
这里多说一句数据源。网络上有不少公开的诗词语料,优先选字段清晰的数据集,也就是有“题目、作者、朝代、正文”这种结构化字段的;如果只有纯文本,后面清洗成本会高很多。我用的模拟项目X就是一份JSON格式的语料,约六千余首,字段相对齐整,省了很大力气。
2. 技术选型的底层逻辑:每种选择都要讲得出理由
2.1 后端为什么用Spring Boot + MyBatis + MySQL
先谈后端。Spring Boot在这个场景几乎是必选项,不是因为别的,而是它的生态成熟度太高,社区里能找到的参考代码最多,遇到问题你能靠搜索引擎解决。对毕设来说,“问题能不能被快速解决”比“技术是否最新”重要得多。
MyBatis的选择理由有三点。第一,它足够轻,SQL写起来直观,对于多条件动态查询特别友好。第二,MyBatis的<where>、<if>标签天然适合做组合筛选,比JPAL乱拼SQL要直观。第三,答辩时问“你是怎么查数据的”,你直接把XML里的SQL贴出来讲,比讲一遍ORM的映射原理更容易让老师听明白。
数据库用MySQL是主流选择。表结构方面,核心表我建议这么设计:
- 用户表:id、用户名、密码(加盐存储)、角色、创建时间
- 诗人表:id、姓名、朝代、生卒年、简介
- 诗词表:id、标题、作者id、朝代、正文、译文、赏析、题材类型
- 分类表:id、分类名称、父级分类
- 评论/收藏表:id、用户id、诗词id、操作类型、时间
诗词表是数据量最大的一张表,一定要给作者的索引、朝代索引、标题索引建好。这里有个容易被忽略的细节:索引不是建得越多越好,检索场景才是主导。比如只按“标题关键字”搜,就建title字段的索引;组合查询时优先选择区分度最高的单列索引,MySQL优化器能自己挑,但前提是索引列类型不要被函数包裹。
2.2 “大数据”相关组件:检索、分词、缓存、可视化怎么取舍
大数据方向在这一类项目里通常不做分布式存储,而是做单机处理。我把它拆成四个子选型,每个都做过对比。
第一个是检索。如果数据集只有几千条,MySQL自带的全文索引够用;如果数据量去到几万条以上,可以上Elasticsearch,但代价是要多维护一套集群,部署成本高,答辩也不好讲。我最终选择的是MySQL全文索引配合分词插件,数据量在这个范围内完全跑得动,还能给老师讲清楚“你为什么不选ES”。
方案对比可以参考这个表:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| LIKE模糊查询 | 实现最简单 | 全表扫描,字段大了很慢 | 演示用的超小样本 |
| MySQL全文索引 | 单机部署,索引可控 | 中文分词支持较弱 | 万级以内文本数据 |
| Elasticsearch | 分词强、查询快 | 部署重、维护成本高 | 海量日志、全文检索专项 |
第二个是中文分词。诗词和普通文本不一样,句子短、文言词汇多,“床前明月光”这句话如果按通用词典切,可能被切得乱七八糟。所以不要选单纯的通用分词器,建议用带自定义词典扩展能力的分词库,把诗人名、常用意象词、常见古语词加进自定义词典,分词质量才会有质的提升。
第三个是缓存。Redis在这个项目里用来扛两个点:一是登录态管理,二是统计看板的热点数据。尤其是统计看板,如果每次刷新都去MySQL跑一次GROUP BY聚合,数据少的时候还好,数据量一上来,响应时间立刻崩。把聚合结果缓存起来,设置合理的过期时间,性能问题基本消失。
第四个是可视化。ECharts是首选,原因很简单:图表形态多,中文文档全,而且后端只需要输出标准结构的数据,前端配置一下就能渲染。不要自己手写Canvas图表,那是给自己挖坑。
2.3 前端方案:前后端分离还是服务端渲染
毕设里前端路线通常有两派:一是用Vue + Element UI做前后端分离,二是用Thymeleaf直接渲染模板。
有基础的同学我建议选前后端分离。原因不是潮流,而是“接口”这个边界会逼着你把后端做得更规范。前后端分离以后,你必须有清晰的接口定义、统一的返回体和错误码,这些东西都是答辩时能拿出来的实际工程细节。如果没有Node环境、不想维护前端工程,退一步用Thymeleaf也不影响功能,但是交互体验和可视化效果会打折扣。
接口文档我用Swagger自动生成,对接的时候直接看页面,省去了写联调文档的时间。该依赖加上,配置一开,所有Controller的接口和参数说明自动生成,这在答辩演示时也是个加分项。
后端返回结构我统一用了一个Result对象,包含code、message、data三个字段。所有接口不管成功失败都返回这个结构,前端只根据code判断。一开始觉得麻烦,后期联调才知道统一结构有多香,不然每个接口都要处理一遍各种异常格式。
3. 核心功能模块拆解:从管理到检索再到统计
3.1 用户登录、注册与权限控制
用户模块是大部分系统的入口,处理方式比较成熟:注册时密码用加盐后哈希保存,登录成功后签发JWT,前端把Token存在本地,每次请求放到请求头里。后端写一个拦截器,放行登录、注册、诗词列表等公开接口,其余接口校验Token有效性。
这里有一个容易被忽略的细节:JWT的密钥不要写死在代码里,放到配置文件用占位符读取;过期时间也不要设太长,演示环境建议设30分钟,不然有人截到Token,整个后台等于裸奔。后台管理功能要增加角色判断,普通用户只能看和收藏,管理员才能增删改,这个判断在拦截器里做掉,不要在业务代码里到处散落。
3.2 诗词信息管理:批量导入、清洗、去重
诗词管理模块是系统的数据底座。数据入库的方式有两种:一种是通过管理界面手动录入,另一种是批量导入。毕设时间宝贵,手动录几千首不现实,一定是走批量导入。我做的是一个支持JSON格式导入的工具页面,上传以后后端解析并落库。
清洗逻辑重点处理三类脏数据:
- 作者名不统一,比如“李白”和“李太白”同时出现。此时要做名称映射,统一归到规范主ID。
- 朝代别名问题,比如“唐”和“唐代”混用。归一化以后才能保证按朝代统计时不出妖。
- 正文中的换行符、多余空格、标点全角半角混乱。这些字符不清理,分词和统计都会被干扰。
去重逻辑用的是“标题+作者ID”联合维度。同一作者下同名诗大概率是重复的,重复数据不清理,后面统计“诗人作品数”时会虚高。
3.3 检索与筛选:从LIKE到全文索引
诗词检索是核心功能,检索方式从入口到实现都分两层。
第一层是基础检索,就是输入关键词搜标题或作者。这一层用SQL查询,条件动态拼接,关键代码逻辑类似:
<select id="searchPoem" resultType="map"> SELECT id, title, author_name, dynasty, content FROM poem <where> <if test="keyword != null and keyword != ''"> AND (title LIKE CONCAT('%', #{keyword}, '%') OR author_name LIKE CONCAT('%', #{keyword}, '%')) </if> <if test="dynasty != null and dynasty != ''"> AND dynasty = #{dynasty} </if> <if test="category != null and category != ''"> AND category = #{category} </if> </where> </select>第二层是全文搜索,搜的是诗词正文。正文是长文本,LIKE直接扫描非常慢,所以要走全文索引。做法是在MySQL里对content字段建FULLTEXT索引,配合分词插件,查询时用MATCH...AGAINST。这个路线比ES轻得多,但演示时要把“为什么用全文索引”这个点讲清楚。
3.4 统计看板:用图表把数据讲清楚
整个系统最容易出彩的模块是统计看板。我把它设计成五个图表页面:
- 诗人作品Top10:柱状图,直观展示高产诗人
- 朝代词作数量分布:饼图或柱状图,看各朝代数据量差异
- 诗词高频词词云:体现文本分析能力
- 常见意象统计:例如“月”“风”“花”等高频意象词的出现次数
- 各朝代作品趋势线:折线图看诗歌数量随时间的变化
每个页面背后是一个统计接口,接口里跑聚合查询,然后返回前端可渲染的结构。以“作者作品Top10”为例,SQL大致是:
SELECT author_name, COUNT(*) AS cnt FROM poem GROUP BY author_name ORDER BY cnt DESC LIMIT 10;这类聚合查询看似简单,但表数据一多,频繁执行会很吃力。因此我在聚合外层加了Redis缓存。第一次请求时计算结果并写入缓存,过期时间设为30分钟。缓存生效以后,看板接口的响应时间从几百毫秒直接降到十几毫秒,这个优化点值得每个做毕设的同学都学会,因为它是“性能优化”这个关键词最直观的落地。
4. 大数据流程落地:采集、清洗、分析与可视化的完整链路
4.1 数据来源与清洗细节
模拟项目X的数据来源是一份公开的诗词语料JSON,整体质量尚可,但脏数据依然存在。清洗流程我做成了可重跑的幂等任务,每一步都有日志输出,方便追踪。
清洗步骤依次是:版本校验、字段解析、朝代归一化、作者归一化、正文格式化、重复数据标注。每完成一步,把清洗后的临时表更新一步,最后再一次性写入正式表。这样做的好处是如果某一步规则写错了,不需要重新导入原始文件,只要回退到对应步骤重跑就行。
清洗逻辑里最磨人的是朝代归一化。同样是唐代诗,语料里可能记作“唐”“唐代”“唐朝”,不统一,GROUP BY出来的结果就是错乱的。我最终维护了一张别名映射表,把全部别名映射到标准名称,跑完以后数据分布才干净。
4.2 中文分词与关键词提取怎么做才不跑偏
词云和高频词统计都依赖分词质量。分词流程大概是:先把正文中的标点符号全部替换为空格,然后按自定义词典进行分词,分词结果过滤掉停用词,最后统计词频。
停用词表特别重要。诗词里常见的“之”“乎”“者”“也”“兮”“以”这类虚词要全部滤掉,不然统计出来的高频词全是文言虚词,根本没法看。然后还要处理的一个问题是叠字和单字。古诗词里“月”“风”“花”这些单字意象非常关键,所以自定义词典里我把它们作为单字词保留,避免被通用词典并进其他词组里。
分词工具的具体实现我用的是轻量封装方案:把诗句按标点切碎成短句,每个短句再做词法分析。实战下来,短句切分的效果比整诗直接切分好很多,因为整首诗有对仗、有平仄,整体分词容易上下文串味。
4.3 统计任务的触发方式:实时查还是定时跑
统计看板的实现有两种思路:实时聚合和定时聚合。
实时聚合的好处是数据绝对新鲜,问题是每次看板刷新都要跑GROUP BY,如果用户量一多,数据库压力会很大。定时聚合的好处是查询性能好,缺点是数据不是实时更新。毕设场景下,我强烈建议用“定时任务 + 缓存”的组合。具体做法是:在系统启动或每天固定时刻,用Spring的@Scheduled注解触发一次统计任务,把统计结果写进Redis,接口只负责读Redis。
核心逻辑大致是这样:
@Scheduled(cron = "0 0 2 * * ?") public void refreshStatCache() { List<AuthorRankVO> rankList = poemMapper.selectAuthorRank(); redisTemplate.opsForValue().set("stat:authorRank", rankList, 24, TimeUnit.HOURS); }定时任务有两个额外好处。一是数据预热,用户第一次打开看板时不用等查询。二是答辩演示时可以手动触发一次重建缓存,让数据“现场刷新”,这个操作在答辩时能起到很好的加分效果。
4.4 可视化看板的接口设计与前端渲染
可视化的后端接口不需要做得花哨,关键是输出结构要便于前端解析。以词频统计接口为例,我设计的返回结构是一组词和频次:
{ "code": 200, "data": [ { "name": "月", "value": 823 }, { "name": "风", "value": 671 }, { "name": "花", "value": 588 } ] }前端用ECharts的wordCloud系列直接就能渲染词云。柱状图接口则是返回排行榜数组。前端页面只做一件事:调用接口,把数据塞给图表组件,每个图表页面控制在两百行代码以内,逻辑很干净。
这里有个数据格式的坑:前端图表的字段名和后端实体字段名如果对不上,图表会静默空白,连报错都没有。我吃过一次亏,后来直接在接口层用DTO转换,统一字段命名,问题立刻消失。做前后端分离的同学,一定要建立一个专门的VO层,不要直接把数据库实体丢给前端。
懒加载也是桌面看板的一个细节,图表进入视口区域才开始请求数据,避免首屏同时发五六个请求。加一个浏览器IntersectionObserver的监听,几十行代码的事,但能明显提升演示时的流畅度。
5. 踩坑实录:完整的排查链路与修复方案
5.1 检索慢:LIKE带头、索引失效的完整排查过程
模拟项目X第一次做全文搜索测试时,检索“月”字,响应时间到了三秒多,肉眼可见地卡顿。这个问题的排查很有代表性,完整过程如下。
第一步,打开MySQL慢查询日志,把阈值设为1秒,重新执行检索,确认查询时间确实严重超标。
第二步,对被查的表执行EXPLAIN,看执行计划。结果里type是ALL,rows是全表行数,Extra里是Using where,这说明查询压根没走索引,就是全表扫描。
第三步,检查SQL,发现当时全部用的LIKE '%keyword%'。这种写法有个致命问题:百分号在关键词前后,字符串匹配无法使用普通B树索引,只能全表扫。即使我在content字段建了普通索引也没有意义,因为索引失效了。
第四步,对比解决方案。方案A是改成LIKE 'keyword%',从字段头部匹配,能用索引,但语义变了,搜不到诗词正文中间的词。方案B是MySQL FULLTEXT索引,支持中文分词后做全文检索,语义正确,单机部署成本低。方案C是上Elasticsearch,成本高,对这个数据量属于杀鸡用牛刀。
最终选择方案B。把content字段改成全文索引以后,查询语法改为MATCH(content) AGAINST('月' IN NATURAL LANGUAGE MODE),再跑一遍EXPLAIN,type变成了fulltext,响应时间降到几十毫秒。这里要提醒一点:MySQL全文索引默认的最小分词长度对中文单词不友好,需要根据分词插件调整配置,这个细节不调好,中文检索还是会出现奇怪结果。
5.2 批量导入几千首诗词时内存溢出
第一次导入语料时,我写了一个最简单的解析逻辑:读取整个JSON文件到内存,然后循环插入。文件不算大,大概十几MB,但是解析出的对象列表加上映射关系占用的堆内存,直接把默认启动参数压垮,报了一次OutOfMemoryError。
排查思路是:把文件读取改成流式处理,每次只读一部分内容,边读边映射边入库。JSON解析改成流式解析后,内存占用立刻降下来。更稳妥的做法是加一个多线程导入的任务队列,每次固定处理两百条,处理完一批再拉下一批。
这个经验也给导入接口的返回设计提了个醒:大批量导入一定要做成异步任务。前端提交文件后立刻返回“导入中”,后台通过定时任务轮询导入进度,而不是让前端一直挂着等结果。演示的时候,这个异步进度条能讲出很多工程细节。
5.3 前后端联调阶段的跨域和接口文档问题
前后端分离以后,第一件事就是跨域报错,Access-Control-Allow-Origin的经典问题。排查方法很简单,看浏览器控制台完整报错信息,然后在后端写一个WebMvcConfigurer的配置类,允许指定域名跨域访问:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(Registry registry) { registry.addMapping("/**") .allowedOriginPatterns("http://localhost:*") .allowedMethods("GET", "POST", "PUT", "DELETE") .allowedHeaders("*") .allowCredentials(true); } }配置完跨域,联调时另一个问题是接口参数对不上。有的接口前端以为是POST,后端写成GET,还有字段名大小写不一致。这个问题最佳解法是用Swagger做接口文档自动生成,前端每个接口移进去看参数示例就行。自己写文档既费时又容易过期,Swagger能生成和代码同步的文档,省心很多。
远程帮同学排查这一类问题时,十有八九都是跨域配置、端口冲突、依赖版本不匹配。先把这三个地方过一遍,大部分联调问题都能解决,不要一上来就怀疑业务逻辑。
5.4 演示数据准备与答辩演示的细节
数据显示是答辩最容易出问题的环节。很多同学平时用几行测试数据开发,到了答辩现场一开看板,柱状图就一两根柱子,词云稀稀拉拉,老师根本看不出系统价值。我的做法是准备一份6000余首的演示数据集,并单独写一个初始化SQL脚本,一键导入,保证论文里写的统计结果能在现场复现。
演示时有几个操作提前演练:如何切换用户角色、如何触发一次缓存重建、如何展示导入过程的进度条。我还在系统里做了一个统计页面的手动刷新按钮,答辩时点一下,看板数据重新聚合,图表明显变一下,这个细节比嘴上讲一百遍”我做了缓存与定时任务“更有说服力。
顺带说一句,市面上有不少渠道提供源码加文档加远程调试的打包服务,但我不建议完全依赖这种方式。答辩老师会抽查项目里的关键代码逻辑,如果核心模块不是你写的,三五个细节追问下来就穿帮了。自己做一遍,哪怕照着公开教程做,你能回答出“为什么这么设计”,这才是毕设的根本目的。
最后说点实操体会
做完这个项目再回头看,最大的体会是毕业设计拼的从来不是技术名词堆砌,而是把一条核心链路彻底跑透。诗词信息系统真正的价值点,不在登录注册和增删改查,而在检索、统计、可视化这条数据处理链路。把这几件事做实,老师看到的是一个能完整运行、有分析结果、有优化措施的系统,而不是一堆代码碎片。
另外分享一个小技巧:开题报告和技术文档里的架构图、流程图不必画得过于复杂,把“数据从原始语料进入系统,经过清洗入库,再到检索和统计,最后通过看板展示”这条主线画清楚,就已经超过大多数人。毕竟大多数老师想看到的是你的思路,不是一张花花绿绿的网络拓扑图。
最后再补充一个操作细节:演示前务必清空Redis缓存,再手动触发一次统计任务,确保看板展示的是最新数据。这个动作练熟了,答辩现场你就能从容点开图表、展示数据结果,告诉老师“我现在重新跑一次统计,看一下聚合结果”,这是最有说服力的收尾。