☰
基于SpringBoot的古诗文研习平台:从架构设计到部署实践
2026/10/22 13:57:11 网站建设 项目流程

1. 为什么选型SpringBoot做古诗文学习平台

做Java毕设或者商业项目时,很多同学一听到“古文学习系统”这种带传统文化属性的题目,下意识就往简单了做——不就是个文章列表加个详情页嘛。这种判断大方向没错,但恰恰是“看起来简单”的题目,最容易在答辩和评阅环节翻车。为什么?因为评委老师们见过太多千篇一律的CRUD了,如果只做出一个带登录的文章浏览站,功能上虽然“没问题”,但技术含量和应用深度都撑不起一篇合格的研究生论文。

SpringBoot在这样一个偏内容展示型的项目里,真正的价值不在于它“能做什么”,而在于它能让你用极低的成本把系统架构撑得有模有样。我见过有人用Servlet+JSP硬扛古文系统的,页面写起来麻烦不说,前后端耦合得一塌糊涂。也有人嫌SpringBoot重,跑去用Node.js写,虽然后端开发速度确实快,但在国内高校的毕业设计评审语境下,Java技术栈显然更契合课程体系和应用背景。这里不是说技术栈有高下之分,而是说选型要匹配题目本身的气质——一个叫做“基于SpringBoot的中华经典诗词数字化研习平台”的项目,如果最终交付物只是一堆静态页面,那题目里“数字化研习”这几个字怎么都解释不通。

那么问题来了:什么是“研习平台”和“普通网站”的本质区别?我理解的核心在于“研习”二字——它不能只是拿给你看的,它得拿给你“用”。看一首诗谁都会,但理解一首诗的背景、意象、典故、平仄,需要一个交互式的学习闭环。所以这个系统要做的不是内容堆砌,而是把古文学习拆成几个可操作的学习行为:检索、阅读、标记、练习、自测、记录进度。这些行为对应的功能点,恰好都能在SpringBoot的生态里找到非常成熟的实现方案。

SpringBoot在接手这类项目时有一个肉眼可见的好处:起步快。一行SpringApplication.run就能把整个容器跑起来,不用像SSH时代那样配置一大堆XML。更关键是SpringBoot的自动配置思路非常适合内容管理型系统——你引入Thymeleaf就能做服务端渲染,引入MyBatis-Plus就能把SQL写到最少,引入Spring Security就能快速搞定登录鉴权。每一条依赖的引入都是即插即用的,改造成本低、试错成本低。选它做古文学习平台,本质上是选了一个“下限不低、上限很高”的技术底座。

第二个关键点是:诗词类数据天然适合结构化管理。一首诗包含标题、作者、朝代、体裁、原文、注解、译文、赏析、创作背景、关键词、标签等多维字段,如果不用后端框架去统一管理,这些数据的存储、检索、关联就会变得极其零散。SpringBoot配合MySQL,可以轻松将诗词拆成主表+扩展表,并通过JPA或MyBatis映射成清晰的对象模型。这一步做好了,后面前端展示、后台管理、学习记录追踪都会非常顺畅。

第三个点是团队协作和交付节奏。毕设项目通常时间紧、节奏快,单人开发的情况下更需要一个规整的项目结构。SpringBoot约定优于配置的理念在这时候特别省心——启动类放哪、配置文件叫什么、Mapper接口怎么扫描,多数的坑都帮你提前堵上了。对比之下,用原生的Servlet开发虽然能让你“了解底层”,但毕设的核心目标是交付一个完整、可演示、能答辩的系统,而不是重造一遍轮子。

我做了这些年项目,最大的体会就是:选技术栈不要举棋不定,想清楚项目类型和核心诉求之后,快刀斩乱麻选一个能让你顺利把系统从0做到1的框架,比反复纠结“有没有更酷的方案”重要得多。SpringBoot在古文学习系统这个场景里,走的就是一条稳中求进的路,它可以让你在同样的开发周期里,多挤出时间去打磨业务细节,而不是困在环境配置里动弹不得。

2. 功能拆解:把“研习”落成看得见的功能点

2.1 用户端的四大核心场景

先想想你打开这个平台最想做什么。是去查一首诗?是去读一篇赏析?还是想测一测自己背过多少?不同的学习动机会导致截然不同的页面路径,所以我在设计时没有简单把功能堆成“前端一个菜单,后端一张表”,而是按照学习场景来组织。

第一个场景是内容检索与发现。古文学习最大的痛点是“我知道有这首诗,但忘了具体内容”或“我脑子里有个模糊印象,但说不清出自哪里”。我做了一个支持多字段匹配的搜索模块,标题、作者、正文、注解、关键词都能作为搜索范围,用户输入“月落乌啼”也能精准定位到《枫桥夜泊》。搜索页同时提供朝代和体裁两个筛选维度,配合热门标签做辅助引导,基本能做到三步之内找到目标诗词。这个模块的逻辑不复杂,核心是把SQL的查询条件做细:既然用户可能从任何字段入手,那就别把LIKE条件写死在标题上,多开放几个查询入口的性价比极高。

第二个场景是深度阅读与赏析。打开一首诗之后,除了原文和作者,我额外拆了四个Tab:注释、译文、赏析、创作背景。这么做不是为了界面好看,而是为了把学习内容分层次——第一次读这首诗,可以只看原文和译文;准备背诵的时候,翻到注释理解字词;写读后感的时候,去赏析和背景里找素材。每个Tab对应内容表里的一个字段,前端切换时不重新请求页面,用Ajax拉取即可。这里有一个容易被忽略的细节:赏析类的长文本里包含大量古文引用,原始文本如果用普通换行符存,渲染到页面上格式经常乱掉。我的处理方案是前端用markdown渲染或富文本格式保留,后端存储时保留原始换行逻辑,保证展示效果和录入效果一致。

第三个场景是互动学习。这部分是“研习”二字的灵魂。我把互动拆成了四类功能:收藏、笔记、练习、打卡。收藏不是简单的“存书签”,而是支持用户给诗词打标签,比如“我喜欢的”“要背的”“考试重点”,方便后续在个人中心按标签复盘。笔记功能类似读书批注,用户可以在诗词详情页写下自己的理解和心得,系统自动关联时间和诗词ID,个人中心里可以按时间线回顾。练习功能是诗词知识自测,题库按诗词ID关联,题型设计成语境填空和作者匹配为主,答完题自动判分并给出解析。打卡则是一个轻量级的正向激励机制,每日登录或完成一次练习后自动记录,日历视图一目了然,帮助用户建立持续学习的习惯。这四类功能听起来零散,但在后端设计上有一个统一思路:都以诗词ID作为外键关联,归到用户行为表,整体非常规整。

第四个场景是个人学习档案。虽然这个模块看起来就是“查一下我收藏了什么、做了多少笔记”,但它的价值在于把你所有的学习行为聚合到了一张页面上,形成一个可视化的学习轨迹。用户可以看到自己学习了多少篇、收藏了多少首、写了多少笔记、最近七天练习的正确率变化。这个页面的数据来源比较杂,涉及多张表的聚合查询,我额外建了一个专门的学习统计Service,把零散的数据查询统一封装,避免Controller层写得又乱又长。

2.2 管理端:内容才是系统的生命线

很多毕设项目把管理端做成“凑数功能”——放一个登录入口,加一个用户列表,再放一个文章管理页面,能增删改查就交差了。但古文学习系统的管理端是整个平台的运营基石,如果不好好做,用户端再漂亮也是空中楼庙。因为诗词数据的来源不是天天都在变,录入、校对、审核天然需要一个更从容的编辑环境。

管理端的核心是诗词内容管理,我用的是典型的主表加关联表设计:主表存诗词的基本信息,如标题、作者、体裁、朝代、原文;关联表存扩展信息,如注释、译文、赏析、背景。为什么拆分?因为扩展信息的文本量远大于基本信息,拆开后接口按需加载,详情页打开速度更快,后台编辑时也可以按区块保存,避免一次提交超长文本导致体验卡顿。新增一首诗时,默认走“录入草稿”状态,待内容确认无误再点击“发布”。这个状态字段看着不起眼,但在实际使用中非常有用——运营人员在录错了半截内容时可以安心保存而不必担心影响前端展示。

标签管理是另一个容易被忽视的模块。诗词的标签关系到检索和推荐,我在设计标签时采用了人工标注优先的策略——系统不做自动打标签,而是由管理员在录入内容时手动选择,这样可以保证标签质量。标签表独立设计,诗词和标签用中间表维护多对多关系,前端展示时按关联查询取出。

用户管理模块包含用户信息查询、状态管理(正常/禁用)和学习行为概览。管理员可以看到每位用户的活跃度、收藏数量和练习次数,这个设计主要服务于系统运营的侧写需求。系统管理则承担菜单、部门和操作日志等功能,这部分沿用常规RBAC设计,核心是角色-菜单-权限三级结构。说实话,管理端这块技术上没什么新东西,但它的工程价值在于帮你把题目的完整性撑起来——评审老师看系统时,大概率会先打开管理端,看看数据是怎么进去的,再从前端看看数据是怎么出来的。循环闭合,题目才立得住。

2.3 推荐功能:让平台有“活了”的感觉

诗词平台的推荐不能照搬电商的复杂模型,一上来就设计协同过滤和深度学习,既没数据支撑也没训练成本,纯属给自己挖坑。我的建议是从基于标签的偏好推荐入手,做一个轻量级的算法:用户每首收藏的诗词都关联了标签,把用户收藏过的标签和频次汇总成一个偏好向量,然后去诗词库里找相同标签的基他内容,按照标签匹配度倒序输出。这个逻辑不涉及外部算法库,用Java的Map和List就能实现,跑起来效果却出奇的好——用户收藏过王维的诗,下次首页推荐里就会出现孟浩然和储光羲的作品,学习路径变得非常自然。

当然这种方案的局限性也很明显:新用户进来没有收藏行为,推荐模块完全失效。所以我在推荐策略里加了一个冷启动方案:新用户默认推荐系统自带的精选榜单和热门诗词Top10。这个榜单的数据来源于全局收藏和练习数据的实时聚合,至少保证用户一进来看到的不是空白页。我始终觉得,推荐算法不在多高深,在一个内容型平台里,能让用户觉得“系统懂我”,就已经成功了。

3. 数据库设计:一张表拆得好,胜过十行优化

3.1 诗词主表和扩展表的设计思路

先放一段核心建表语句,这是我做这类内容系统反复打磨后沉淀下来的稳定方案:

CREATE TABLE poem ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID', title VARCHAR(100) NOT NULL COMMENT '诗词标题', author VARCHAR(50) NOT NULL COMMENT '作者', dynasty VARCHAR(20) NOT NULL COMMENT '朝代', genre VARCHAR(20) DEFAULT NULL COMMENT '体裁:诗/词/曲/赋', content TEXT NOT NULL COMMENT '正文,换行分隔', dynastic_background TEXT COMMENT '时代背景', status TINYINT DEFAULT 0 COMMENT '0草稿 1发布 2下架', view_count INT DEFAULT 0 COMMENT '浏览数', favorite_count INT DEFAULT 0 COMMENT '收藏数', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_dynasty_genre (dynasty, genre), INDEX idx_author (author) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='诗词主表'; CREATE TABLE poem_detail ( id BIGINT PRIMARY KEY AUTO_INCREMENT, poem_id BIGINT NOT NULL COMMENT '关联诗词主表', annotation TEXT COMMENT '注释', translation TEXT COMMENT '译文', appreciation TEXT COMMENT '赏析', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_poem_id (poem_id), CONSTRAINT fk_poem_detail FOREIGN KEY (poem_id) REFERENCES poem(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='诗词扩展信息表';

为什么这么拆?我可以展开说一下。把赏析、译文、背景这种超长文本和原文放在一张表里,数据库存储没问题,但查询性能堪忧——详情页每次打开都要把几KB甚至几十KB的长文本一股脑查出来,当数据量上千条之后,页面响应体量会明显增大。拆成两份表之后,列表页只需要查主表,不需要带上任何长文本字段,查询效率快一个量级。关联表之间用外键约束保证数据完整性,这一点对内容录入场景特别重要——防止录入了赏析却找不到它属于哪首诗的脏数据出现。

另一个重点是朗读音频预留字段。现在很多古诗文学习平台都支持朗读功能,我在主表里预留了audio_url字段,前期可以存第三方平台的公开音频链接,后期有条件再转本地上传。毕设阶段不做实现没关系,但表结构预留好,答辩时导师问到“有没有考虑过音频方向”,你能马上给出方案腹稿,这是加分项。

3.2 用户行为表:收藏、笔记、练习和打卡

用户行为表和诗词表之间的关系看起来是一对多,实际操作时我强烈建议用一张通用型的用户行为表来承载多类行为。这里有一个设计取舍:一种方式是收藏表、笔记表、练习记录表、打卡表各建一张,分开管理;另一种方式是统一建一张user_behavior表,用behavior_type字段区分行为类型。这两种方案我都实际做过,最终选择了后者。

这张表的结构大致如此:

CREATE TABLE user_behavior ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT '用户ID', poem_id BIGINT NOT NULL COMMENT '诗词ID', behavior_type TINYINT NOT NULL COMMENT '1收藏 2笔记 3练习记录 4打卡', content TEXT COMMENT '扩展内容,笔记文本或练习答题记录', label VARCHAR(50) DEFAULT NULL COMMENT '用户自定义标签', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_user_type (user_id, behavior_type), INDEX idx_poem_user (poem_id, user_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户行为表';

统一表设计的好处立竿见影:统计“某位用户最近30天的学习活跃度”时,一个count查询全搞定,不需要union四张表。练习记录存在content字段里,我采用的是一种极简的JSON序列化方案——答题数据以一个JSON字符串存储,解析时用Hutool或Jackson随手就能完成,根本不需要单独建答案明细表。这算是经验之谈了:这类系统没那么高的并发写入压力,设计过度反而自己给自己添堵。

说到这是一个典型的“性价比优先”设计方案。如果目标是一套高并发的线上产品,我肯定拆成独立表,方便做缓存、做分区、做定向查询;但作为毕设项目和应用级平台,行为数据量和复杂度都没到那个量级,合并表带来的维护成本和查询便捷性收益是明显大于坏处的。学会区分项目规模,选择合适的复杂度,这本身就是工程能力的一部分。

3.3 题库与标签关联:别小看这几张表

古文学习系统的题库不是独立于诗词存在的,它必须和诗词主表管理起来。我做的是一诗多题的结构:

CREATE TABLE poem_question ( id BIGINT PRIMARY KEY AUTO_INCREMENT, poem_id BIGINT NOT NULL COMMENT '关联诗词', question_type TINYINT COMMENT '1填空 2选择 3判断', question_content VARCHAR(500) NOT NULL, options TEXT COMMENT '选择题选项,JSON格式', answer VARCHAR(200) NOT NULL, analysis TEXT COMMENT '答案解析', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_poem_question (poem_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='诗词题库表';

练习题和诗词用poem_id关联,用户在前端点击“练习”时按诗词拉取属于它的题目,一组大约5到8道,答完实时判分。这里有一个设计细节:选项为什么用JSON存而不拆成独立的选项表?因为选择题的选项只在答题场景使用,没有按选项维度的查询需求,拆表纯属徒增成本。全表按JSON处理,前端取出后直接JSON.parse填充到选项区域,工作量省了不止一半。

标签关联表我采用三表结构:tag表、poem_tag表。中间表存poem_id和tag_id,多对多关系清晰明了,查询时两端都能快速定位。

CREATE TABLE tag ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(30) NOT NULL UNIQUE COMMENT '标签名称', category VARCHAR(20) DEFAULT 'style' COMMENT '标签类型:style/content/dynasty', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='标签表'; CREATE TABLE poem_tag ( poem_id BIGINT NOT NULL, tag_id BIGINT NOT NULL, PRIMARY KEY (poem_id, tag_id), CONSTRAINT fk_pt_poem FOREIGN KEY (poem_id) REFERENCES poem(id), CONSTRAINT fk_pt_tag FOREIGN KEY (tag_id) REFERENCES tag(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='诗词标签关联表';

标签这里有一个推荐系统的延伸用法:给诗词打标签统一用规范词,比如“田园”“边塞”“怀古”“送别”“写景”“抒情”,后台录入时做成下拉选择,不允许自由输入脏数据。这样推荐系统的匹配精度才能得到保证。我试过开放自由录入,跑了一周就出现“田原”“天涯”“边寒”等各种同义或错误标签,最后不得不全表清洗。规范大于自由,这是数据治理的第一课。

4. 后端核心模块实现:从Controller到Service的边界划分

4.1 诗词检索接口的渐进式查询设计

诗词检索是用户端最高频的接口,我按“渐进式查询”的思路做了实现。所谓渐进式搜索,指的是用户在搜索框输入内容时,每敲一个字就调一次接口,后端基于前缀匹配和热词权重做一个轻量级补全接口;当用户敲击回车或点击搜索按钮时,才执行全文模糊匹配的精确检索接口。

补全接口实现如下:

@RestController @RequestMapping("/api/poem") public class PoemController { @Autowired private PoemService poemService; @GetMapping("/search/suggest") public Result<List<PoemSuggestVO>> suggest(@RequestParam String keyword) { if (StringUtils.isBlank(keyword)) { return Result.success(Collections.emptyList()); } return Result.success(poemService.suggest(keyword)); } @GetMapping("/search") public Result<PageResult<PoemVO>> search(PoemQueryDTO dto) { return Result.success(poemService.pageSearch(dto)); } }
public List<PoemSuggestVO> suggest(String keyword) { LambdaQueryWrapper<Poem> wrapper = new LambdaQueryWrapper<>(); wrapper.like(Poem::getTitle, keyword) .or().like(Poem::getAuthor, keyword) .orderByDesc(Poem::getViewCount) .last("LIMIT 8"); return poemMapper.selectList(wrapper).stream() .map(p -> new PoemSuggestVO(p.getId(), p.getTitle(), p.getAuthor(), p.getDynasty())) .collect(Collectors.toList()); }

这里就是MyBatis-Plus大展身手的地方。LambdaQueryWrapper让我不用手写XML里的动态SQL,用Java流式编程就把查询条件拼完了。分页搜索这里用了PageHelper或MyBatis-Plus的Page对象:

public PageResult<PoemVO> pageSearch(PoemQueryDTO dto) { Integer pageNum = dto.getPageNum() == null ? 1 : dto.getPageNum(); Integer pageSize = dto.getPageSize() == null ? 10 : dto.getPageSize(); LambdaQueryWrapper<Poem> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(StringUtils.isNotBlank(dto.getDynasty()), Poem::getDynasty, dto.getDynasty()) .eq(StringUtils.isNotBlank(dto.getGenre()), Poem::getGenre, dto.getGenre()) .like(StringUtils.isNotBlank(dto.getKeyword()), Poem::getTitle, dto.getKeyword()) .or(StringUtils.isNotBlank(dto.getKeyword())).like(StringUtils.isNotBlank(dto.getKeyword()), Poem::getAuthor, dto.getKeyword()); wrapper.eq(Poem::getStatus, 1).orderByDesc(Poem::getViewCount); Page<Poem> page = poemMapper.selectPage(new Page<>(pageNum, pageSize), wrapper); PageResult<PoemVO> result = new PageResult<>(); result.setTotal(page.getTotal()); result.setList(page.getRecords().stream().map(PoemConverter::toVO).collect(Collectors.toList())); return result; }

搜索逻辑并不复杂,但有一个值得注意的细节:我做了字段级的非空判断,dto里的参数能传空值进来时,调用eq或like前必须预先过滤条件,否则查询结果会变成“全表任意字段匹配空字符串”,结果集直接失控。MyBatis-Plus的条件构造器提供了eq(boolean condition, column, value)这种重载,条件判断放第一个参数,代码能简洁不少,我实际项目里更喜欢这种写法。

4.2 详情页的聚合查询与缓存策略

详情页是用户点进来最关心的页面,我聚合了主表基础信息、扩展表赏析注释、标签列表和作者其他作品四个数据源。如果不做任何处理,一次请求会触发四次数据库查询,对性能不友好。我处理的办法有两种,按场景切换。

第一种是Service层聚合:

@Transactional(readOnly = true) public PoemDetailVO getDetail(Long poemId, Long userId) { Poem poem = poemMapper.selectById(poemId); if (poem == null) { throw new BizException("诗词不存在"); } poemMapper.incrementViewCount(poemId); PoemDetail detail = poemDetailMapper.selectOne( new LambdaQueryWrapper<PoemDetail>().eq(PoemDetail::getPoemId, poemId)); List<TagVO> tags = tagMapper.selectByPoemId(poemId); List<PoemSimpleVO> sameAuthorPoems = poemMapper.selectList( new LambdaQueryWrapper<Poem>() .eq(Poem::getAuthor, poem.getAuthor()) .eq(Poem::getStatus, 1) .ne(Poem::getId, poemId) .last("LIMIT 5")); PoemDetailVO vo = new PoemDetailVO(); vo.setBasic(PoemConverter.toVO(poem)); vo.setDetail(PoemDetailConverter.toVO(detail)); vo.setTags(tags); vo.setAuthorPoems(sameAuthorPoems); if (userId != null) { vo.setUserFavorite(favoriteService.isFavorite(userId, poemId)); vo.setUserNote(noteService.getNoteSummary(userId, poemId)); } return vo; }

代码层面把主表、扩展表、标签表、作者作品四块数据组合在一起,返回一个完整的VO对象给前端渲染。写到这里就明白了为什么建议拆扩展表:selectById主表只有基础字段,走的是聚簇索引覆盖,查询性能极稳。

第二种是本地缓存优化。因为详情页的诗词内容是只读的,变更频率极低,非常适合Caffeine做本地缓存。我把“主表+扩展表+标签”的聚合结果封装成DetailCacheKey,缓存key由poemId构成,过期时间设为30分钟。用户每次查看详情时先查本地缓存,没有命中再走数据库。这样下来,热门前十的诗词在高峰期访问时几乎不会产生数据库压力。作为毕设,引入Caffeine这种轻量级缓存组件不算炫技,但它背后的缓存思想和技术选型说明能在答辩时讲出很多实在的内容。

4.3 用户行为记录:收藏、笔记、做题的并发安全

用户的行为记录接口涉及写操作,我最看重的是幂等性和防重复提交。以收藏为例,用户快速点击两次收藏按钮,正常情况下应该只生成一条收藏记录,而不是两条。我的处理方式是在Service层先查询再插入,并配合唯一索引做兜底:

ALTER TABLE user_behavior ADD UNIQUE KEY uk_user_poem_type (user_id, poem_id, behavior_type);

当然behavior_type=2的笔记场景允许多条,不考虑唯一索引;而行为类型=1收藏场景必须唯一。这个唯一索引设计我是靠踩坑换来的:最初只做了Service层的存在性判断,实际运行时收到过几次重复收藏的反馈,后来加了数据库唯一索引作为兜底,双保险才彻底解决。

做题提交的接口要考虑的是答案校验逻辑。我实现了一个策略模式,根据题目类型动态选择校验器:

@Component public class AnswerValidatorRegistry { @Autowired private List<AnswerValidator> validators; public boolean validate(Integer questionType, String userAnswer, String correctAnswer) { return validators.stream() .filter(v -> v.supports(questionType)) .findFirst() .orElseThrow(() -> new BizException("不支持的题目类型")) .validate(userAnswer, correctAnswer); } }

填空和判断是精确匹配,选择题需要解析JSON数组做比对。每次提交时记录用户答案、是否正确、题目ID和诗词ID,存到user_behavior表,前端拿到结果后立即刷新练习记录。

这里有个小难点:练习提交接口是高频写的,同一用户短时间内做10道题会产生10次插入请求。虽然数据量不大,但为了减少数据库连接开销,我使用了Spring的@Transactional将一次练习会话的多次提交合并为一个事务。前端每道题做完先暂存本地,最后统一提交整组题目,后端一次事务接收List ,解析、判分、存储一气呵成。这个设计既降低了数据库压力,也让练习流程的交互体验更自然。

4.4 推荐模块的Java实现:标签权重算法的具体落地

推荐模块是我最想在论文里展示的一个亮点,核心思路可以归纳为三点:统计用户偏好标签、计算标签相似度、推荐未读诗词。

第一步,从user_behavior表里筛选用户的收藏记录,根据poem_tag关联表抓取出标签统计:

public Map<String, Integer> calUserTagPreference(Long userId) { List<UserBehavior> behaviors = behaviorMapper.selectList( new LambdaQueryWrapper<UserBehavior>() .eq(UserBehavior::getUserId, userId) .eq(UserBehavior::getBehaviorType, 1) // 收藏行为 ); Map<String, Integer> tagFreq = new HashMap<>(); for (UserBehavior b : behaviors) { List<TagVO> tags = tagMapper.selectByPoemId(b.getPoemId()); for (TagVO tag : tags) { tagFreq.merge(tag.getName(), 1, Integer::sum); } } return tagFreq; }

第二步,对所有诗词做遍历评分。由于诗词总量规模可控(一般不超过几千条),全表扫描的代价完全可以接受,不涉及大规模分布式计算:

public List<PoemVO> recommendByTag(Long userId, int limit) { Map<String, Integer> pref = calUserTagPreference(userId); if (pref.isEmpty()) { return hotPoems(limit); } List<PoemVO> candidates = poemMapper.selectHotWithTag(); Map<Long, Double> scoreMap = new HashMap<>(); for (PoemVO p : candidates) { double score = 0; for (String tag : p.getTagNames()) { score += pref.getOrDefault(tag, 0) * 1.0; } scoreMap.put(p.getId(), score); } return scoreMap.entrySet().stream() .sorted(Map.Entry.<Long, Double>comparingByValue().reversed()) .limit(limit) .map(entry -> poemService.getSimpleById(entry.getKey())) .collect(Collectors.toList()); }

这里的推荐逻辑可以做个类比:就像你去图书馆借书,管理员发现你老借武侠小说,于是下次直接把书架上其他武侠作者的作品推荐给你。原理虽然朴素,但因为结合了每首诗词的标签权重累积,推荐粒度比“按作者推荐”细了很多——喜欢李商隐《无题》的人会被推荐晏几道的词,而不是苏轼的诗。数据跑起来之后,我实测给测试用户A推荐的结果确实呈现出多作者、同风格的丰富性,这种效果在答辩时极具说服力。

第三种是冷启动问题,这我上文提到过:新用户没收藏,偏好映射为空,直接退回热门榜单。我实现了一个hotPoems方法,统计近30天收藏数和阅读数,综合算出一个热度权重排Top10,这个榜单就是个不错的兜底。

4.5 登录认证与权限控制:Spring Security的轻量级实践

身份认证方案我选用了JWT+Spring Security组合。在毕设项目里,这是一个非常标准的方案:无状态Token部署简单,前端在登录成功后把Token存在本地,之后每次请求都带上Authorization头。

核心配置片段如下:

@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/api/auth/login", "/api/auth/register", "/api/poem/list", "/api/poem/detail/**").permitAll() .antMatchers("/api/admin/**").hasRole("ADMIN") .anyRequest().authenticated() .and() .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } }

这里有一个看起来简单但很容易出大错的地方:放行路径必须手动枚举全。我最初只放行了登录注册,结果用户点进诗词列表页报401,排查了很久才发现是静态资源路径没放行。建议前端所有静态资源(css/js/images)统一放到/static/**路径下,在后端一并permitAll,避免这类问题反复出现。

注册和登录接口我额外做了三个处理。第一,用户密码使用BCrypt加密,数据库里存的是加密后的哈希值,绝不存明文。第二,注册接口增加了用户名唯一性校验,查重逻辑放在事务内执行,防止并发注册同用户名而双双通过。第三,登录接口返回的数据结构里除了Token,还会带上用户的基础信息、角色编码和头像地址,前端拿到后可以直接渲染侧边栏头像,减少一次不必要的用户信息请求。

5. 前端交互与页面实现:从服务端渲染到前后端分离

前端选型上,我采用的是一个折中方案:不引入复杂的Vue全家桶工程链,而是用Thymeleaf做服务端渲染为主、局部交互用原生JS或少量Vue3 CDN方式嵌入。为什么这么选?毕设场景下,前后端完全分离意味着要维护两套工程,前端工程还要单独配置代理、构建流程,开发成本不可小觑。Thymeleaf的天然优势在于和SpringBoot集成极简,数据从后端ModelAndView直接渲染到HTML,语法类似HTML标签,门槛低得多。

到了诗词详情页这种交互密集的场景,再用服务端整页刷新就显得笨拙了。我在详情页的四个Tab切换场景里,引入了Vue的单组件模式——不是完整工程化,而是直接引入Vue3的全局构建版,通过createApp挂载到一个div容器上。这种方案的好处是既享受到了响应式数据绑定的便利,又不需要配置工程化链。

Tab切换的核心代码结构:

<div id="detailApp"> <ul class="tab-nav"> <li :class="{ active: activeTab === 'annotation' }" @click="switchTab('annotation')">注释</li> <li :class="{ active: activeTab === 'translation' }" @click="switchTab('translation')">译文</li> <li :class="{ active: activeTab === 'appreciation' }" @click="switchTab('appreciation')">赏析</li> <li :class="{ active: activeTab === 'background' }" @click="switchTab('background')">背景</li> </ul> <div class="tab-content" v-html="currentContent"></div> </div>

配合Vue的异步请求组件,点击Tab时发送Ajax请求获取对应字段,不刷整页,体验顺畅。这里我不得不提醒一个坑:Thymeleaf和Vue3同时使用时的变量插值冲突。两个模板都使用双花括号{{ }}作为插值语法,Thymeleaf解析到{{ }}时有时候会先入为主处理掉,导致Vue拿到的值不对。我的解决方式有两条——一是Thymeleaf模板里Vue插值改成[[ ]](通过配置修改Vue的插值分隔符),二是Thymeleaf生成页面的初始数据用th:text输出,Vue只管后续交互数据。实际项目里我用的是第二种,把页面初始数据(诗词ID、用户ID)直接放在HTML的data属性上,再用Vue读取,绕开了模板冲突。

整体前端页面结构我规划如下:

  • 首页:轮播图推荐位+精选榜单+热门标签入口
  • 诗词列表:搜索栏+朝代筛选+体裁筛选+分页结果
  • 详情页:标题头部+主表信息+四Tab区块+用户操作区
  • 个人中心:学习统计+我的收藏(按标签分组)+我的笔记+练习记录
  • 管理后台:侧边栏导航+内容管理+标签管理+用户管理+系统设置

这套页面布局主打“内容阅读理解”场景,没有花哨的元素,每个区块都在为同一个目标服务——用最少的学习成本,让一首诗的含义和背景被充分理解。

6. 部署与运行:从开发环境到可演示的完整流程

很多人在开发时跑得好好的,一到部署演示环境就各种翻车。我总结了一套从零开始的部署流程,照着走基本稳。

6.1 本地环境搭建

这个项目的本地环境依赖清单很常规:JDK1.8及以上、Maven3.6+、MySQL5.7+、Redis可选。我用的是阿里云服务器部署演示,操作系统是CentOS7,宝塔面板管理,也可以用Docker容器化部署。

启动后端前务必确认三件事:

  • MySQL中建好数据库,项目里配置好数据库名、用户、密码
  • 启动前执行项目里带的schema.sql建表初始化脚本
  • 确保Redis可选依赖已按需启动;如果项目集成了验证码功能,Redis是必需的

关键配置文件application.yml:

server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/poetry_study?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver thymeleaf: cache: false redis: host: localhost port: 6379 database: 0 mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true

map-underscore-to-camel-case这个配置项非常关键。数据库字段命名是下划线风格favorite_count,实体类属性是驼峰风格favoriteCount,这个字段开启后MyBatis-Plus自动映射,省去大量手写注解的功夫。很多新手项目跑不起来,十有八九是把这个默认false的配置漏开了,字段全部映射不上,所有查询全是null。

初始化数据我用的是SQL脚本一次性导入诗词内容,脚本直接从搜韵等公开数据源抓取后清洗导入。这一步工作量虽然繁琐,但做得越扎实,系统演示时的内容丰富度就越高。一般准备300首唐诗、200首宋词、50首先秦诗歌,加上对应注释、译文、赏析,已经足够撑起整个平台的观感了。

6.2 常见部署报错速查

整理几个我在部署过程中真实踩过的坑,方便对照自查:

端口被占用:报错信息常见Port 8080 was already in use,用lsof -i:8080或netstat -tunlp | grep 8080查占用进程,kill掉就行。

数据库连接失败:报错信息常见Access denied for user,三处地方需要核对:数据库里是否创建了对应的用户和密码,application.yml里的密码是否正确,MySQL是否允许远程连接(存在bind-address配置问题)。

中文乱码:这个问题出现概率极高。全文规划时我就用utf8mb4建表,若依然乱码,检查启动参数是否加了-Dfile.encoding=utf-8,以及MySQL连接串是否带了characterEncoding=utf8。

JWT过期异常:报错信息常见JWT expired,检查前端存的Token是否已过期,把前端的Token清掉重新登录即可。调试阶段建议把JWT过期时长暂时设长一些,比如7天,减少反复登录的干扰。

6.3 演示注意事项

毕设答辩或项目演示时,系统“跑得起来”只是基础门槛,更关键的是要“展示得有条理”。我一般会提前准备好三组演示数据:第一组是完整学习流程,从首页注册登录,搜索一首诗,打开详情页,写一条笔记,做一组练习,查看个人中心的统计;第二组是管理端操作流,从后台新增一首诗,录入信息并发布,回到前端刷新页面,看这条新数据如何呈现;第三组是推荐效果流:用一个已积累收藏的测试账号登录,打开首页,看推荐列表的内容变化。

这三组数据对应三层业务逻辑:用户体验闭环、数据管理闭环、算法推荐闭环。每个闭环都能单独讲三到五分钟,答辩时长完全够用,还能显得你对系统理解很深入。

7. 系统测试与性能优化:用数据说话

7.1 功能测试表

我整理了一份可直接用于测试的项目核对表,保证每一个功能点都被覆盖到,不至于在答辩演示时出现“点了没反应”的尴尬:

模块测试点预期结果
登录注册使用错误密码登录提示密码错误
登录注册注册重复用户名提示用户名已存在
诗词检索搜索“李白”返回李白名下多首诗词
诗词检索搜索“月落乌啼”精准定位对应诗目
详情页打开一首诗详情主表+四Tab扩展信息完整展示
收藏功能重复点击收藏仅生成一条收藏记录
笔记功能写笔记后个人中心查看按时间线排列笔记
练习功能完成一组练习题分数即时计算并展示解析
推荐功能冷启动新用户访问首页显示热门榜单
管理端新增一篇诗词草稿前端不展示草稿内容
管理端发布草稿后前端查看页面刷新后立即出现
权限控制普通用户访问管理端接口返回401或403

7.2 性能优化三板斧

内容型系统性能瓶颈不在并发而在数据查询和大字段传输,我的优化经验可以总结成三板斧。

首先是上文提到的主扩展表拆分,列表页不带长文本字段,一次查询的数据量减少80%以上,这个优化在数据量过千时效果特别明显。用EXPLAIN看执行计划,走主键索引和全表扫描的速度差距是数量级上的。

其次是详情页缓存。Caffeine本地缓存配过期时间,热门诗词的详情查询在缓存命中时延后到毫秒级,对比数据库直查的50-100ms,体验提升十分明显。我简单测过首页和详情页在未开启缓存时的响应时间约120ms,开启后压降到30ms左右,这种可量化的指标放在论文里非常有说服力。

再次是前端静态资源缓存。Thymeleaf模板开启浏览器缓存配置后,css/js在首次加载后再刷新就不会重复下载,整体白屏时间进一步缩短。部署时我还在Nginx层做了静态资源的gzip压缩和expires缓存配置,效果立竿见影。

7.3 接口性能实测

我在开发环境用JMeter做过一次简单的压力测试,结果大致如下:

接口并发数平均响应时间错误率
诗词列表分页5082ms0%
诗词详情(含聚合)50156ms0%
用户登录5064ms0%
收藏接口5053ms0%
首页推荐50203ms0%

这个数据虽然不是大规模压测,但用于毕设答辩已经够真实、够好看了。讲性能时切忌凭空口说“很快”,拿一组实测数据出来,再解释优化手段,说服力完全不是一个档次。

8. 项目延展方向:答辩加分和后续提升的思路

做完了基础功能之后,如果你想在这个项目上再拔高一些,有几个延展方向非常值得尝试。

第一是语音朗读模块。前端可以直接调用浏览器的SpeechSynthesis API,把后端返回的原文文本传给朗读器,实现原声诵读效果。这个功能对古文学习平台的使用价值极高——学习古诗文时聆听节奏和韵律至关重要,而且成本几乎为零,不需要额外的语音合成服务。答辩讲到这个功能时,能自然带出“多媒体学习”和“多感官记忆”的教学理念,阐述度一下子就不同了。

第二是深度学习算法替代标签推荐。如果你有余力,可以在现有标签偏好推荐的基础上,尝试引入Word2Vec或BERT做诗词文本向量化。把训练好的向量存储在向量数据库里,新用户浏览时实时计算诗文间的语义相似度。这个方向的开发量很大,但论文的理论层次能再上一个台阶。核心原理是——每首诗词经过预训练模型编码成一个高维向量,向量空间中的距离越近,说明诗词的语义风格越相似。然后在向量数据库里执行最近邻检索,高效召回“风格相似”的诗。这套方案的优势是推荐质量不再依赖人工打标签,而是直接来自语义理解层面,推荐结果更精准,论文的创新点也更有分量。

第三是学习社区功能:增加评论区和讨论区,用户可以对某一首诗发表见解和提问,其他人可以回复。这个方向的技术增量不大,主要是社会属性增补,但对古文学习平台的“互动学习”定位是一次很好的补全。

第四是微信小程序端适配。SpringBoot后端天然提供RESTful API,小程序端只需重新写一套前端页面,数据接口完全复用。这个方向可以帮助系统覆盖移动端学习场景,让“随时随地的古文研习”从概念落到实地。做小程序端还有一个隐藏好处:答辩时可以现场掏出手机演示,比只在电脑上点网页多一层真实感。

选择哪条延展路径,关键是看你的时间和精力,还有论文立意想要打哪张牌——要技术深度就搞向量化推荐,要应用广度就做小程序,要教育价值就加朗读模块。都不冲突,按优先级排序即可。

9. 写在最后的几段真心话

反复做这类毕设项目之后,我最深的体会是:一个古文学习系统的价值,不是因为技术有多前沿,而在于它能不能真的帮到使用者。如果你的平台设计出来,教师用它备课,学生用它预习,爱好者用它摘记,那这个系统就已经超出了“毕业设计”的范畴,有了真实的应用意义。

技术上能聊的其实就这些:SpringBoot做底座、MySQL做存储、MyBatis-Plus做数据访问、Thymeleaf做页面渲染、Vue做交互增强、JWT做认证、Caffeine做缓存。每一样都不是新技术,但组合起来能撑起一个逻辑清晰、功能饱满、可以对外演示的完整平台。很多时候我们过分追逐新框架、新工具,却忘了工程实践的本质——合理的选型和对业务的透彻理解,才是一个项目成功的主因。

再给后来者两个实用建议。第一,写代码前先画设计图,不需要多专业,手画一张用户流程图就能帮你少走太多弯路。第二,功能宁精勿滥,一个系统的完成度取决于每个按钮背后的逻辑是否经得起追问,而不是菜单数量堆得有多满。做项目管理时一定要给自己留足测试和写文档的时间,很多项目失败不是开发不出来,而是来不及打磨。

最后分享一个细节处理技巧:在这个系统里,我特意在诗词详情页底部加了一个“前一首”和“后一首”的翻页导航,按朝代内的时间顺序切换。这个小功能写起来就几行代码,但使用感受提升非常明显——很多用户会顺着某一朝代的脉络一路读下去,学习体验流畅度比单纯依赖搜索高得多。这种细节的堆叠,才让一个技术型系统有了人文的温度。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询