毕业设计、课程项目里“书城”类系统不算罕见,但大多数作品其实都做成了“图书商城”,购物车、订单、支付一个不少,反而把真正的“阅读”给丢了。这个题目有意思的地方就在于“阅读器”这三个字,它明确告诉我们:核心不是卖书,而是让用户能打开一本书、翻页、记忆进度、做笔记。结合 Spring Boot 和 HTML 的组合,这是一个典型的轻量级应用——后端走接口,前端不用 Vue 全家桶,直接靠 HTML 页面加 Ajax 交互就能跑起来。整套系统非常适合拿来练手,也适合作为毕业设计落地,想快速上手一个完整全栈项目的同学,把这篇文读透应该能省不少弯路。
1. 项目概览:这个“书城阅读器”到底要解决什么问题
1.1 系统定位与目标用户
书城阅读器系统,本质上干两件事:一是“书城”的展示和检索,二是“阅读器”的沉浸式阅读体验。
书城部分,用户进来能看到书籍列表、按分类筛选、搜索书名、进入详情页看简介。这是信息展示层,难度不高,但数据结构和接口设计要留好扩展的余地。阅读器部分,用户点开某一本书,能进入阅读页面,看到章节内容,调整字号、记住读到哪一章哪一行、做书签、写笔记。这层才是系统的灵魂,也是答辩时能讲出东西的核心亮点。
系统面向的用户角色建议拆成两类:普通用户和网站管理员。普通用户干上面那些事,管理员负责上架书籍、维护章节内容、管理用户状态。管理员这块不用做太复杂,后台页面能操作就行,严格权限控制可以先放一放,但“管理员和普通用户看到的东西不一样”这个逻辑要成立。
1.2 功能边界:必须做的与不该碰的
很多同学一上来就想把系统做“大”,电子书支付、第三方登录、在线阅读计时、会员体系,全都规划进去。结果就是每个模块都做不深,代码堆得密密麻麻,一跑起来全是 bug。这个项目我的建议非常明确:阅读体验是核心,交易流程能砍就砍。
必须做扎实的功能,我按优先级排一下:
- 用户注册与登录(Session 或 JWT 二选一,后面细说)
- 书籍列表、分类筛选、关键词搜索、书籍详情
- 章节列表与正文阅读(HTML 渲染或接口返回 JSON,前端动态拼接)
- 阅读进度自动保存,下次打开直接续读
- 书架功能:收藏/移出书籍
- 阅读笔记:添加、查看、删除笔记
- 管理员登录与简单的书籍/章节维护界面
这些功能全部落地,系统已经很完整了。支付、优惠券、荐书算法这类内容,非核心场景,做了反而是累赘。哪怕你只是为了毕设凑工作量,我都不建议动支付,因为涉及账目和安全性问题,答辩时老师一问就容易露馅,而且确实吃力不讨好。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型与整体设计思路
2.1 为什么用 Spring Boot + HTML 这个组合
题目直接指定了 Spring Boot 和 HTML,很多人一看到 HTML 就觉得“low”,觉得现在不都是 Vue + Element UI 前后端分离吗?这里我分享一个实际判断:如果你的项目是毕设、课程实战,并且要在有限时间内把前后端都完成,那用服务端渲染或“静态 HTML + Ajax”其实是更聪明的选择。
原因有三点。第一,开发链路短。Spring Boot 搭后台,把页面模板放好,一套代码里前端后端联调,出错了好定位,不用开两个端口还要处理跨域。第二,依赖少、部署简单。包成一个 jar 直接跑,不用单独部署 Node 服务。第三,答辩的时候老师要的是“你能讲清楚系统的每一个环节”,传统 HTML 页面配合 Thymeleaf 模板引擎,没有前端构建那一层黑盒子,代码逻辑一眼能看穿,反而加分。
在实际落地时,我推荐用 Thymeleaf 来渲染需要动态数据的页面,静态资源(CSS、JS、图片)放 src/main/resources/static 下,需要动态渲染的页面放 src/main/resources/templates 下。如果有些页面想走纯静态 HTML + Ajax,也完全没问题,Spring Boot 本身就支持,只需要在 Controller 里返回 JSON,前端页面用 Fetch 或 Axios 调用接口。这种“混合模式”是这个题目最舒服的姿势,既要了一部分服务端渲染的省事,又保留了前后端交互的灵活性。
2.2 版本选择的坑:Spring Boot 2.x 还是 3.x
“springboot版本太高”这个热词最近在技术社区里刷得特别多,我猜很多同学踩过 3.x 的坑。这里重点提醒:如果你还在用 JDK 8,千万别直接上 Spring Boot 3.x,因为 3.x 强制要求 JDK 17,地下很多老教材、老代码全都是基于 2.x 写的,拿过来改代码时命名空间还是 javax,而 3.x 换成了 jakarta,结果一大堆 import javax.servlet.* 直接编译失败,心态瞬间崩掉。
我个人的推荐组合,如果你是跟着网上的资料学习或者毕设求稳:
- JDK 8 + Spring Boot 2.7.x,这个组合最成熟,网上资料最多,踩坑也最少
- 如果你的环境本身就是 JDK 17 或更高,那就上 Spring Boot 3.x,起步比 2.7 多争取点项目优势
创建项目时可以用 Spring Initializr,选择合适版本,依赖勾选 Spring Web、Thymeleaf、Spring Data JPA 或 MyBatis、MySQL Driver、Lombok、Validation。新手我建议用 MyBatis 或 Spring Data JPA 二选一,别贪多。JPA 写起来简洁,适合结构比较规矩的项目;MyBatis 的 SQL 控制力更强,适合你手写复杂查询。书城阅读器这类系统,SQL 不会特别复杂,用 JPA 会省很多事。
2.3 项目目录结构与分层设计
包结构建议这样分,清晰且后期好扩展:
code复制com.example.bookreader
├── controller # 控制层,接收前端请求
├── service # 业务层,放核心业务逻辑
│ └── impl # 业务实现类
├── mapper # 数据访问层,MyBatis 的 Mapper 接口(如果用 JPA 则放 repository)
├── entity # 实体类,对应数据库表
├── dto # 前端传入的数据对象,比如登录参数、搜索参数
├── vo # 返回给前端的视图对象,比如书籍详情 VO、章节内容 VO
├── config # 配置类,比如拦截器、跨域配置、静态资源配置
├── common # 通用返回结果、异常处理
└── util # 工具类,比如 JWT 工具、文本处理工具
这套分层是 Java 后端最经典的经验结构。Controller 不写业务代码,只做参数接收和返回封装;Service 里处理所有业务规则;Mapper/Repository 只做数据库交互。好处是排查问题时不用翻一个几百行的大类,分工明确,答辩时讲起来也容易。
3. 数据库设计与核心表结构
3.1 核心表关系梳理
这个系统涉及的核心数据模型包括用户、书籍、章节、书架、阅读进度、笔记。表与表之间的关系其实并不复杂,但有一个地方经常被忽略:章节内容字段怎么存。
真实书籍的章节内容动辄上万字,如果放在 chapter 表里用 TEXT 类型存,完全没问题。但要注意,列表页搜索图书时,不应该把章节正文也捞出来,否则 SQL 会变得很慢。查询时能只查元数据就只查元数据,需要正文时才查正文,这个点老师很喜欢问,问到了就是加分项。
书和章节的对应关系是典型的一对多。一个用户对应多条书架记录、多条笔记、一条阅读进度。阅读进度表强烈建议做成“一个用户对一本书只有一条记录”,每次读到时更新章节 ID 和偏移位置,而不是每次翻页都插入新记录。否则用不了几天,这张表的数据量就会爆炸。
3.2 建表 SQL 与索引设计
下面是一份可以直接用的建表 SQL,我把字段名都调成和实体类方便映射的格式:
sql复制CREATE TABLE `user` (
`id` bigint NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL COMMENT '用户名',
`password` varchar(100) NOT NULL COMMENT '密码(BCrypt加密)',
`nickname` varchar(50) DEFAULT NULL COMMENT '昵称',
`role` tinyint NOT NULL DEFAULT '0' COMMENT '角色:0普通用户,1管理员',
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `book` (
`id` bigint NOT NULL AUTO_INCREMENT,
`title` varchar(200) NOT NULL COMMENT '书名',
`author` varchar(100) DEFAULT NULL COMMENT '作者',
`category` varchar(50) DEFAULT NULL COMMENT '分类',
`cover_url` varchar(500) DEFAULT NULL COMMENT '封面图URL',
`description` text COMMENT '简介',
`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:0下架,1上架',
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `chapter` (
`id` bigint NOT NULL AUTO_INCREMENT,
`book_id` bigint NOT NULL COMMENT '所属书籍ID',
`chapter_number` int NOT NULL COMMENT '章节序号,从1开始',
`title` varchar(200) NOT NULL COMMENT '章节标题',
`content` mediumtext COMMENT '正文内容',
PRIMARY KEY (`id`),
KEY `idx_book_id` (`book_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `bookshelf` (
`id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL,
`book_id` bigint NOT NULL,
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_book` (`user_id`, `book_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `reading_progress` (
`id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL,
`book_id` bigint NOT NULL,
`chapter_id` bigint NOT NULL COMMENT '最后阅读章节ID',
`position_percent` int DEFAULT '0' COMMENT '阅读进度百分比,0-100',
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_book` (`user_id`, `book_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `note` (
`id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL,
`book_id` bigint NOT NULL,
`chapter_id` bigint NOT NULL,
`note_content` text NOT NULL COMMENT '笔记内容',
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
索引设计这里我说三点心得。第一,外键不一定要在数据库层面建,但查询频繁的字段必须建索引,比如 chapter.book_id;第二,复合唯一索引 uk_user_book 不仅保证数据不重复,还能让按用户查书架和按用户查进度这两个高频查询走索引,速度很快;第三,考虑之后做“继续阅读”时按 update_time 倒序排序,可以在 reading_progress 表的 update_time 上再加一个普通索引。
3.3 数据初始化与演示数据
项目跑起来需要几千字的正文内容,总不能一本本手敲。建议准备一个 data.sql 或启动时执行的初始化数据脚本,往书里塞几个章节。书可以选几本版权过期的名著,比如《小王子》《老人与海》这种,文字本身在公版领域,不用担心版权问题。章节正文可以在网上找纯文本内容,清洗一下格式,用脚本拆到对应的 chapter 表。可以在初始化时预留 30 本书、每本书 10-20 个章节,演示时数据量就够看了。
4. 后端核心功能实现要点
4.1 登录注册:Session 还是 JWT
登录这块,网络上大量教程都在推 JWT,但我不建议新手一上来就上 JWT。如果你是纯后端返回 JSON、前端用 HTML 加 Ajax,那用 Session + Cookie 其实更简单,浏览器自动带上会话状态,后端用拦截器校验登录就能搞定。
只有当你做的是前后端彻底分离、前端和后端分开部署时,JWT 才是更合适的方案。而且 JWT 有个小坑:注销和过期控制需要额外实现,否则 token 一旦签发,无法在到期前主动作废。
如果你决定用 JWT,需要注意 token 放在请求头 Authorization: Bearer <token>,前端每次请求都带上。如果前端是 Thymeleaf 页面,想省事的话,登录后将用户 ID 放入 Session,由拦截器统一拦截“需要登录才能访问”的路径,比如书架、阅读进度、笔记相关接口,这是最稳妥的方案。密码存储方面,无论选哪种方案,都不允许明文存密码,用 BCrypt 加密,Spring Security Crypto 库里的 BCryptPasswordEncoder 就够了,不过如果你不想引入整套 Spring Security,也可以只引这个类。
4.2 书籍列表、分类筛选与搜索分页
书籍列表是每个用户打开系统后第一眼看到的内容。接口建议这样设计:
code复制GET /api/books?page=1&size=10&category=文学&keyword=小王子
Controller 接收参数后,传给 Service。Service 层用 MyBatis 也好、JPA 也好,拼装查询条件。分页返回的内容建议用统一包装结构,比如 Result 对象,包括 code、message、data 三部分,前端拿到后好判断请求是否成功,也好统一处理错误状态。
页码从 1 开始还是从 0 开始,前端和后台必须约定好。很多联调时出现的翻页对不上问题,就是这里约定不一致导致的。我一般统一用“1 表示第一页”。搜索功能建议直接用数据库的 LIKE '%keyword%' 实现,数据量不大时完全够用,不用上 Elasticsearch。当然,如果后面书量大了,可以再考虑全文检索,但那是后话。
4.3 阅读器核心:章节内容、进度保存与翻页
阅读器页面是这个系统最核心的模块,接口设计上要分两块:内容接口和进度接口。
内容接口:
code复制GET /api/chapters/{chapterId}
返回章节标题、上一章 ID、下一章 ID、正文内容。前端拿到正文后,按 <p> 标签分段渲染,而不是显示成一大段死文字。这里有个细节:数据库里存的章节正文,建议按段落用双换行 \n\n 分隔存储,返回给前端时再按分隔符拆成段落数组,前端循环拼接 <p> 元素。这样做既保证了存储格式干净,又方便后续做字号调整、行高调整。
进度接口:
code复制POST /api/progress
Body: { bookId, chapterId, percent }
前端在用户翻页或离开页面时把阅读进度上报到后端。这里有个技巧:不要在滚动条每动一次就发一次请求,否则后端接口会被刷爆。正确做法是加节流,比如每 5 秒保存一次,或者在页面隐藏(visibilitychange 事件)时保存一次,也可以在用户点击翻页时保存。我们实际开发时通常把“滚动停止后 1 秒”作为保存点,体验最自然。
4.4 书架与笔记接口设计
书架其实就是收藏夹,接口相对简单:
code复制GET /api/bookshelf -> 获取我收藏的全部书籍
POST /api/bookshelf/{bookId} -> 添加收藏
DELETE /api/bookshelf/{bookId} -> 移除收藏
注意返回书架列表时,最好把这本书的最新阅读进度一起带上,这样前端能在书架卡片上显示“读到第 3 章 45%”,这个体验很像真实阅读 App 的“继续阅读”功能。实现上可以用一条 SQL 关联查询,或者后端把两个列表查出来在 Service 里做合并。数据量不大时,后一种方案代码更好写。
笔记接口:
code复制GET /api/notes?bookId=xxx -> 获取某本书下的全部笔记
POST /api/notes -> 新增笔记
DELETE /api/notes/{noteId} -> 删除笔记
笔记表里存 chapter_id,是为了在阅读器里按章节查看笔记时可以直接定位。实现时,阅读器页面每一段正文后面加一个“记笔记”按钮,点击弹出一个小输入框,保存后自动刷新该章节的笔记列表。这个功能交互上不复杂,但非常能体现系统“阅读器”的定位,是答辩展示时的加分点。
5. 前端页面组织与“基于 HTML”的落地方式
5.1 页面清单与导航结构
我建议整个系统的前端页面按以下清单来组织,既不啰嗦又能覆盖全部功能。
login.html—— 登录/注册页index.html—— 书城首页(书籍列表 + 分类 + 搜索)book-detail.html—— 书籍详情页reader.html—— 阅读器页面bookshelf.html—— 我的书架admin/books.html—— 管理员书籍管理页admin/chapters.html—— 管理员章节管理页
页面骨架可以用一段通用的导航栏,包含:首页、书架、分类下拉,以及右上角的用户菜单(登录/注册或者头像昵称、退出登录)。导航这块如果每个页面都复制粘贴 HTML,后面改样式会改到想哭。建议用 Thymeleaf 的模板片段 th:replace 把公共头部抽出来,所有页面引用同一个片段。
5.2 首页与书籍详情页的渲染方式
首页推荐用 Thymeleaf 服务端渲染,这样打开网页时 HTML 里直接就有书籍数据,对搜索引擎友好,用户也能立刻看到内容,不需要等 JS 请求。Controller 从 Service 取首页要展示的书籍列表,塞进 Model,页面里 th:each 遍历输出。
书籍详情页也类似,从数据库查出书名、作者、分类、简介、章节列表,渲染成静态 HTML。页面上“开始阅读”的按钮,链接到 /reader?bookId=xxx&chapterId=xxx,用户点击后进入阅读器页面。
有一点常被忽视:详情页展示的章节列表,最好只显示章节标题和序号,不要把每个章节的内容都查出来。数据库查询时,如果 JPA 直接用实体关联查询,很容易把全部章节正文查进去,等详情页打开后发现接口慢得离谱。这时候需要写一个投影查询,只拿 id、bookId、chapterNumber、title 这些字段。
5.3 阅读器页面的交互细节
阅读器页面是整个项目中前端难度最高的页面,别一上来就整轮播、动画,先把这些基础交互做扎实:
- 章节内容按段落渲染,默认字体大小 18px,用户可以通过“Aa- / Aa+”调整字号,调整结果存到
localStorage - 显示阅读进度条(
position: fixed定位在顶部或底部),滚动监听计算百分比 - 底部/顶部提供“上一章”“下一章”按钮
- 章节标题固定在页面顶部,沉浸阅读时可以隐藏
- 每段文字旁边放一个小图标按钮,点击后在下方展开一个输入框,用来写笔记
- 页面加载时调用进度接口,拿到上次阅读位置后直接滚动到指定位置
阅读器页面的内容建议用接口返回 JSON,前端 JS 动态渲染。如果整章内容也在服务端用 Thymeleaf 直接输出,页面源码会非常长,而且字号调整、段落拆分都不好做。
一个比较隐蔽的坑:章节内容如果包含特殊字符,比如 <script> 或 &,直接插进 HTML 会引起 XSS 问题。渲染时必须使用 textContent 而不是 innerHTML,或者做 HTML 转义。Thymeleaf 默认会转义,但如果是接口返回的 JSON,前端拿 innerHTML 拼接就危险了。我建议在数据库清洗数据时就把特殊字符处理一遍,前端渲染再用 textContent 兜底,双保险。
6. 常见问题排查与避坑清单
6.1 启动与配置类问题
这条最容易出现在环境不一致的机器上。springboot版本太高 导致的问题我在前面提过——如果你用的是 JDK 8,你新建项目时不小心选了 Spring Boot 3.2.x,启动时大概率会报:
code复制UnsupportedClassVersionError: xxx has been compiled by a more recent version of the Java Runtime
或者提示需要 Java 17。解决办法就是调整 JDK 版本,或者把 Spring Boot 版本降到 2.7.x。如果你的某个中间件依赖(比如 MyBatis 启动器)不支持 3.x,也大概率是版本兼容问题,优先检查依赖版本。另外创建项目时建议加上 spring-boot-starter-validation,否则 @Valid 注解不生效,参数校验全静默失败,你排查半天才发现是依赖没引。
端口占用也是常见问题。默认 8080 被占用时,Spring Boot 启动会直接报 “Port already in use”。临时解决办法是在 application.yml 里改端口:
yaml复制server:
port: 8081
排查占用端口可以用:
bash复制netstat -ano | findstr 8080 # Windows
lsof -i:8080 # macOS / Linux
6.2 页面与前端资源问题
“HTML 文件无法预览”这个情况很容易让人懵。如果你把 login.html 丢到浏览器直接双击打开,结果发现页面空白或样式不加载,那是因为页面上有相对路径资源,比如 /css/style.css,直接双击时走的是 file:// 协议,路径解析出了问题。正确姿势是通过浏览器访问 Spring Boot 应用地址,比如 http://localhost:8080/login,让页面由后端引擎来处理。
另一个高频问题:静态资源 404。Spring Boot 默认静态资源目录是 classpath:/static/,你的 CSS、JS、图片要放到 src/main/resources/static 下。如果页面在 templates 目录下,服务端渲染时确实能正常输出 HTML,但它引用的 CSS 和 JS 路径必须写成 /css/xxx.css 这种以 / 开头的绝对路径,否则嵌套路由下会拼错。我在很多项目里看到别人把资源放在 templates 目录里,然后怎么访问都 404,其实 Spring Boot 的默认配置并不直接暴露 templates 下的静态文件,别踩这个坑。
需要注意,如果把纯静态 HTML 页面(比如 reader.html)放在 static 目录下,又想用 fetch('/api/xxx') 请求后端接口,浏览器会直接发相对路径请求,如果前端页面部署在 http://localhost:8080/,接口也在同一个应用里,那没问题;如果前端部署在别的地方、后端单独部署,就需要配置跨域。Spring Boot 配置跨域很简单:
java复制@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowCredentials(true);
}
}
6.3 业务逻辑与数据问题
中文乱码问题在 Spring Boot 里很常见。数据库层面字符集用 utf8mb4,连接串加 characterEncoding=utf8,返回 JSON 时如果还乱码,可以在 application.yml 里加:
yaml复制server:
servlet:
encoding:
force: true
charset: UTF-8
书架重复收藏、进度记录不唯一这类问题,根本原因就是 SQL 里缺少唯一约束,或者添加前忘记查重。建表时加上 uk_user_book 唯一索引,再用 INSERT IGNORE 或先查后插,问题就解决了。
还有一个小问题很容易被忽略:章节序号排序。列表展示章节时,如果按 id 排序,有可能因为导入数据顺序问题导致章节顺序错乱。排序时一定要按 chapter_number 排,而不是按主键排。实际项目中我遇到过一次,数据是从 CSV 批量导入的,导入顺序被打乱,结果阅读器翻页逻辑全部错位,排查了很久才发现是这个问题。
7. 写在最后:如何让这个项目在答辩里真正加分
说到答辩,我说点实在的。技术点讲太多反而容易暴露短板,重点讲大家都能理解、但容易忽视的地方。你可以在项目亮点里强调这些细节:
- 阅读进度按用户和书籍唯一存储,节流保存,防止接口频繁请求
- 阅读器字号本地持久化,重新打开不用再调
- 章节内容分段存储,前端分段渲染,阅读体验优于整块文本输出
- 书架接口合并返回阅读进度,实现“继续阅读”的完整闭环
- 使用 Thymeleaf 服务端渲染首页保证首屏速度,静态资源走标准目录结构
最后分享一个小技巧,我在做这类系统时养成的习惯:开发过程中,每完成一个接口,就先用浏览器直接访问接口地址,看看返回的 JSON 是否正常,再联调前端页面。这样能快速隔离后端和前端的 bug,避免到最后联调时各种问题搅在一起,排查起来特别痛苦。
如果你学完这套内容,下一步还可以继续扩展——给系统加一个管理员编辑章节的富文本页面,或者把阅读器页面改成移动端适配的响应式布局,再进一步可以接上全文检索组件。不过这些都是锦上添花,先把基础跑通才是头等大事。
