首页 / 资讯中心 / 文章详情

Spring Boot小说阅读器系统实战:TXT解析、Redis缓存与进度同步

Spring Boot小说阅读器系统实战:TXT解析、Redis缓存与进度同步 ★ FEATURED ARTICLE
1. 先想清楚这套阅读器到底要解决什么问题做基于 javaspringboot 的小说阅读器系统很多人第一反应是“网上开源的一大把为什么还要自己写”。我最初也是这个想法直到真正拿几个现成项目跑起来才发现能跑的 Demo 和能用的系统之间中间隔着一条挺宽的沟。Demo 通常只保证“能打开一本书、能翻页”但真到自己想长期用问题就会一个接一个冒出来书架里塞了上百本书之后列表加载变慢、读到一半换台设备进度没了、几千章的长篇加载目录要等好几秒、TXT 文件里夹着各种奇怪编码导致整章乱码。所以我后来给自己定了个目标不追求做成商业级产品但要做成一个自己愿意天天打开、也能扛住几百本书和几十万字单本长篇的系统。核心功能就四块——书籍导入与管理、章节目录解析、正文阅读与排版、阅读进度与书签同步。技术栈定在 Java Spring Boot前端用最轻的模板或者纯静态页面数据库 MySQL缓存用 Redis文件存储先放本地磁盘。这个组合对个人项目来说性价比很高。Spring Boot 的自动装配省掉了大量 XML 配置一个SpringBootApplication就能把 Web 容器、数据源、事务管理器全部拉起来MyBatis-Plus 或者 Spring Data JPA 二选一都能把 CRUD 的工作量压到极低Redis 用来扛章节正文这种“读多写少、体积中等、要求毫秒级返回”的数据非常合适。整套东西跑在一台 2 核 4G 的机器上绰绰有余甚至不需要额外的前端构建流程。适合读这篇内容的人大概有三类一是想拿一个完整项目练手 Spring Boot 全套技能的同学这个场景涉及文件上传、文本解析、分页查询、缓存设计、并发更新几乎是后端面试知识点的实战场二是想给自己搭个私人书库的普通用户照着步骤能跑起来三是在做类似内容管理系统、需要处理“长文本 目录结构 阅读位置”这类问题的开发者里面的分章正则、进度存储模型、缓存粒度选择都能直接搬。我要提前说明一点下面所有代码和参数都是我自己跑通过的版本但具体数值比如缓存过期时间、分页字符数要根据你的实际数据量调我会把调整的依据一并写出来你照着改就行。2. 整体架构与数据模型设计2.1 分层结构怎么切才不别扭我见过不少个人项目把所有逻辑塞进 Controller一个方法两百行改一个字段要翻半天。这套系统虽然不大但我坚持按controller / service / mapper / entity / dto / util六层来切理由很实际文本解析这块逻辑最脏最杂必须隔离出去。具体目录大概是这样src/main/java/com/example/reader ├── controller 接口层只做参数校验和结果包装 ├── service 业务层书籍/章节/进度三大服务 ├── service/impl 实现类 ├── mapper 数据访问 ├── entity 数据库实体 ├── dto 出入参对象 ├── common 统一返回、异常、常量 └── util 编码识别、章节切分、分页计算把“章节切分”单独放进util包是我踩过坑之后的决定。一开始我把它写在 Service 里后来发现同一个切分逻辑在“上传解析”和“重新解析”两个入口都要用而且规则会随着遇到的新书格式不断调整——有的书章节标题是“第一章 xxx”有的是“第1章 xxx”有的干脆是“Chapter 1”还有的用“正文 第一章”。这类规则会反复迭代单独抽出来之后改一个正则只需要动一个文件测试也好写。另一条经验是DTO 和 Entity 一定要分开。书籍列表接口返回的字段和数据库表字段差别很大列表页只需要 id、书名、作者、封面、章节数、最后阅读时间而正文表里可能存着几 MB 的 content 字段。如果直接把 Entity 扔给前端序列化的时候会把整个正文查出来再传过去一本书几兆流量列表页直接卡死。2.2 六张核心表的设计取舍表结构我反复调整过三版最后定下来六张表。这里把关键字段和设计理由摊开讲。表名作用关键字段设计考量book书籍主信息id, title, author, cover_url, source_type, chapter_count, word_count, status冗余存章节数和字数避免列表页 count 查询chapter章节目录id, book_id, chapter_index, title, word_count只存目录不存正文目录查询快chapter_content章节正文chapter_id, content, content_hash与目录拆表正文按需加载bookshelf用户书架id, user_id, book_id, sort_order独立表支持排序和分组reading_progress阅读进度id, user_id, book_id, chapter_id, char_offset, percent, update_time记录字符偏移而非页码bookmark书签与笔记id, user_id, book_id, chapter_id, char_offset, note, create_time偏移量定位与字号无关为什么要把章节正文单独拆表这是整个设计里最关键的一步。假设一本 3000 章的小说每章平均 3000 字正文总量接近 900 万字符。如果目录和正文放一张表select * from chapter where book_id ?会把几百兆数据拉进内存MySQL 的max_allowed_packet都可能扛不住。拆表之后目录查询只扫chapter表一页 100 条数据可能只有几十 KB响应时间从秒级降到毫秒级。为什么进度要存字符偏移而不是页码这个问题我一开始想错了。最早我用“第几页第几行”来存进度结果用户把字号从 16px 调到 20px页面重新排版页码全变了进度直接错位。改成存“章节 id 该章内的字符偏移量”之后无论字号、行距、屏幕宽度怎么变都能精确定位到同一个字的位置。具体做法是前端在渲染时把字符位置映射到 DOM 节点或者简单粗暴一点按固定字符数分段浏览器自适应换行。为什么chapter_content要存content_hash用来做去重和增量更新。同一本书重复导入时通过 hash 对比可以跳过没变化的章节避免全量重写。我做二次导入优化时3000 章的书从原来耗时 40 秒降到 3 秒左右靠的就是这个字段。2.3 三条主链路的流程梳理不用画图我用文字把三条最核心的链路说清楚你照着就能在脑子里建立起完整的数据流向。链路一书籍上传到可阅读。用户上传 TXT 或 EPUB → 后端接收文件存到临时目录 → 读取文件头若干字节做编码探测 → 流式读取全文做章节正则切分 → 生成章节列表并批量插入chapter表 → 章节正文分批插入chapter_content表 → 更新book表的章节数和字数 → 删除临时文件 → 返回书籍 id。这条链路的关键是全程流式处理绝不能把整个文件读成一个 String否则一本 50MB 的书直接 OOM。链路二打开书进入阅读。请求书籍详情 → 查book表拿基础信息 → 查reading_progress拿上次读到的章节和偏移 → 查chapter表拿目录分页或全量3000 章以内可以全量超过就分页→ 前端跳到对应章节 → 请求章节正文接口 → 先查 Redis命中直接返回未命中查chapter_content并回写缓存 → 前端排版渲染。链路三阅读过程中的进度同步。前端滚动或翻页时节流计算当前所在章节和字符偏移 → 每 5 秒或每切换章节时上报一次 → 后端 upsert 到reading_progress→ 同时更新bookshelf的last_read_time。这里要注意不能用 POST 每次都 insert得用ON DUPLICATE KEY UPDATE或者先查后更新配合唯一索引(user_id, book_id)保证一人一书只有一条进度记录。3. 书籍解析从一堆 TXT 到结构化章节3.1 编码识别是第一道坎这一块绝对是整个项目里最容易翻车的地方。中文 TXT 小说常见的编码有 UTF-8、GBK、GB2312、UTF-8 with BOM还有一些老书是 BIG5。如果统一用 UTF-8 读GBK 文件会变成一堆“锟斤拷”和问号如果用 GBK 读 UTF-8中文会变成“测试”这种乱码。我的处理方案是这样一个顺序先看 BOM再试 UTF-8 严格解码失败则回退 GBK。JDK 自带的CharsetDecoder可以设置CodingErrorAction.REPORT遇到非法字节序列就抛异常这就成了一个天然的探测手段——UTF-8 对字节序列有严格约束GBK 文件几乎不可能通过 UTF-8 严格解码。public static Charset detectCharset(byte[] head) { // 1. BOM 判断 if (head.length 3 (head[0] 0xFF) 0xEF (head[1] 0xFF) 0xBB (head[2] 0xFF) 0xBF) { return StandardCharsets.UTF_8; } // 2. 严格 UTF-8 试探 CharsetDecoder decoder StandardCharsets.UTF_8.newDecoder() .onMalformedInput(CodingErrorAction.REPORT) .onUnmappableCharacter(CodingErrorAction.REPORT); try { decoder.decode(ByteBuffer.wrap(head)); return StandardCharsets.UTF_8; } catch (CharacterCodingException e) { // 3. 回退 GBK return Charset.forName(GBK); } }注意探测用的head不要只取 3 个字节建议取前 8KB 到 16KB。取太少会误判——比如文件前几十个字节恰好都是 ASCIIUTF-8 严格解码必然通过但后面全是 GBK 中文结果就是前半段正常后半段乱码。我实测 8KB 是个比较稳的阈值。做了这一步之后还有个小坑有些文件是“混合编码”前面几章是 GBK后面被人用工具追加了 UTF-8 内容。这种文件严格来说无解我的做法是逐行解码单行失败就单独回退虽然会慢一点但能救回 95% 的内容。3.2 章节正则的编写与调优章节切分规则决定了目录质量。我统计过手头两百多本书标题格式大致分布如下格式类型示例占比第X章第一章 初见 / 第1章 初见约 62%第X节/回/卷第三节 / 第五回约 15%数字开头001 起风了约 10%英文 ChapterChapter 12 The End约 5%无规则散文集直接用空行分隔约 8%正则我最终写成了这样一条组合式private static final Pattern CHAPTER_PATTERN Pattern.compile( ^\\s*(?:第\\s*[0-9一二三四五六七八九十百千万零两]\\s*[章节回卷集篇部] |Chapter\\s\\d |\\d{1,4}[、.\\s]\\s*\\S) , Pattern.MULTILINE);几个细节值得说。第一^配合MULTILINE保证只匹配行首正文里出现的“他说第三章的内容很精彩”不会被误判成章节。第二数字部分必须同时覆盖阿拉伯数字和中文数字中文数字里“两”和“零”容易漏。第三章节标题行长度要限制我加了一条后置校验匹配到的行如果超过 40 个字符直接丢弃因为正常标题不会那么长超长的多半是正文里恰好以“第一章”开头的句子。实操心得正则调完之后一定要拿真实书籍回归测试不要只测自己造的两三行样例。我当时用二十本书跑回归发现有一本《XX游记》每章标题是“第一日”“第二日”完全匹配不上最后又补了一条第[一二三四五六七八九十][日天夜]的分支。这类长尾规则只能靠真实数据喂出来。3.3 大文件流式解析与分批入库前面说过不能整个读进内存具体做法是BufferedReader逐行读边读边累积当前章节内容遇到下一个章节标题就把上一章落库。这里有个内存控制的技巧单章内容也要设上限我设的是 20 万字符。有些盗版书排版混乱整本书没有一个章节标题如果不设上限StringBuilder会一路涨到几百兆。入库环节用批量插入。3000 章如果一条一条 insert即使本地数据库也要几十秒。改成每 500 条一批的insertBatchSomeColumn配合rewriteBatchedStatementstrue的 JDBC 参数实测能压到 3 秒左右。spring: datasource: url: jdbc:mysql://localhost:3306/reader?useUnicodetruecharacterEncodingutf8rewriteBatchedStatementstrueserverTimezoneAsia/Shanghai这个rewriteBatchedStatementstrue是必须加的不加的话 MySQL 驱动会把批量插入拆成一条条独立 SQL 发过去性能差距能有十倍以上。这也是很多新手觉得“我明明用了批量插入还是很慢”的根本原因。4. 阅读体验的核心分页、缓存与进度同步4.1 服务端分页还是客户端分页这个问题争议挺大。服务端分页就是把一章按固定字符数切成若干页前端一次只请求一页客户端分页是一次把整章内容拿过来前端按容器高度自己算分页。我最终选了客户端分页理由是阅读体验。服务端分页每次翻页都要发请求网络稍差就有明显白屏而且服务端分页要处理“按字符切还是按字节切”“标点符号算不算”这些麻烦事切得不好还会把一句话从中间断开。客户端分页虽然首次加载慢一点但一章 3000 字也就十几 KB加载完之后翻页是纯 CSS 的事丝滑得多。具体实现上我用的是column-count 固定容器高度的横向翻页方案或者更简单的纵向滚动 IntersectionObserver记录当前可见位置。前者更像真实阅读器后者实现成本低。我两种都做过个人更推荐纵向滚动因为在手机上横向翻页的手势冲突太多。4.2 Redis 缓存的粒度怎么定章节正文的缓存粒度有两个选择按章节缓存和按书籍缓存。按书籍缓存的问题是粒度太粗一本 3000 章的书全放进去可能有几十 MBRedis 内存很快撑爆而且每次更新一本书就要整体失效。按章节缓存粒度合适但 key 数量多。我采用的是按章节缓存 只缓存最近访问的章节key: chapter:content:{chapterId} value: 正文 JSON 字符串 ttl: 2 小时2 小时这个值不是拍脑袋定的。太短比如 5 分钟会导致同一用户连续翻回去看上一章时反复穿透数据库太长比如 24 小时会导致更新章节内容后用户看到旧内容。2 小时覆盖了一次典型阅读会话同时保证内容更新在半天内一定能生效。还有一个细节缓存要缓存序列化后的结果不要缓存对象。如果缓存 Java 对象序列化反序列化的开销反而不如直接查库。我一开始用RedisTemplateString, Object加 JDK 序列化单章读取耗时 8ms 左右改成缓存 JSON 字符串后降到 2ms。注意如果书籍被删除对应章节的缓存必须主动清理。我在删除书籍的 Service 方法里加了一段redisTemplate.delete(keys)keys 通过scan命令匹配chapter:content:*中属于该书的章节 id。千万别用keys *命令扫描生产环境上这玩意儿会阻塞整个 Redis。4.3 进度同步的冲突处理进度同步最容易出的问题是“多端覆盖”。用户在手机读到第 100 章在电脑上打开时因为网络慢手机的最新进度还没上报电脑端读到第 50 章并上报结果总进度被拉回到 50 章。我的解决办法是用时间戳做乐观锁INSERT INTO reading_progress (user_id, book_id, chapter_id, char_offset, percent, update_time) VALUES (?, ?, ?, ?, ?, NOW()) ON DUPLICATE KEY UPDATE chapter_id IF(VALUES(update_time) update_time, VALUES(chapter_id), chapter_id), char_offset IF(VALUES(update_time) update_time, VALUES(char_offset), char_offset), update_time IF(VALUES(update_time) update_time, VALUES(update_time), update_time);核心逻辑是“只有更新的时间戳才能覆盖”。不过这招有个副作用如果用户的手机系统时间不准永远比服务器慢那它的进度永远存不进去。所以我在服务端统一用NOW()接收前端上报时不采信客户端时间只用来做“同一秒内的先后判断”。另外还加了一条体验优化进度上报采用节流 页面隐藏时强制上报。节流间隔 5 秒同时监听visibilitychange事件用户切走页面时立刻补一次上报。这一招救回了很多“看完直接关浏览器”导致进度丢失的情况。5. 动手实现从零跑通最小可用版本5.1 环境与依赖版本锁定版本这块我建议直接抄不要自由发挥。我踩过 Spring Boot 2.7 和 JDK 17 组合下 MyBatis-Plus 版本不兼容的坑折腾了半天。组件版本说明JDK17LTS语法顺手Spring Boot 3 也支持Spring Boot3.2.x用 starter-web、starter-data-redisMySQL8.0支持utf8mb4别用 5.7MyBatis-Plus3.5.5配合 Boot 3 要用mybatis-plus-spring-boot3-starterRedis7.x本地 Docker 起一个就行Lombok最新减少样板代码dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version3.5.5/version /dependency提示Spring Boot 3 要求 JDK 17 起步如果你的机器上还是 JDK 8要么升级要么把 Boot 降到 2.7.x此时 MyBatis-Plus 的 starter 名字要把spring-boot3去掉。这个坑我在两个项目里各踩了一次。5.2 建表语句与索引设计CREATE TABLE book ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL, author VARCHAR(100) DEFAULT 佚名, cover_url VARCHAR(500), source_type TINYINT DEFAULT 1 COMMENT 1txt 2epub, chapter_count INT DEFAULT 0, word_count BIGINT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_title (title) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE chapter ( id BIGINT PRIMARY KEY AUTO_INCREMENT, book_id BIGINT NOT NULL, chapter_index INT NOT NULL, title VARCHAR(300), word_count INT DEFAULT 0, KEY idx_book_index (book_id, chapter_index) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE chapter_content ( chapter_id BIGINT PRIMARY KEY, content MEDIUMTEXT, content_hash CHAR(32), KEY idx_hash (content_hash) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE reading_progress ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, book_id BIGINT NOT NULL, chapter_id BIGINT, char_offset INT DEFAULT 0, percent DECIMAL(5,2) DEFAULT 0, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_user_book (user_id, book_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;两个索引必须重点说。chapter表的(book_id, chapter_index)联合索引是目录查询的命脉没有它3000 章的书查目录要全表扫。reading_progress的uk_user_book唯一索引是保证“一人一书一条进度”的前提配合ON DUPLICATE KEY UPDATE才能实现原子 upsert。chapter_content用MEDIUMTEXT而不是TEXT因为TEXT上限 64KB一章三万字按 UTF-8 三字节算就接近 90KB 了会被截断。这个坑我踩过表现为正文读到某一段之后突然没了排查了半天才发现是字段类型问题。5.3 三个关键接口的实现章节正文接口核心就是缓存穿透保护加缓存GetMapping(/api/chapter/{chapterId}) public ResultChapterContentVO getChapter(PathVariable Long chapterId) { String cacheKey chapter:content: chapterId; String cached redisTemplate.opsForValue().get(cacheKey); if (cached ! null) { return Result.ok(JSON.parseObject(cached, ChapterContentVO.class)); } ChapterContent content chapterContentMapper.selectById(chapterId); if (content null) { // 缓存空值防止穿透 redisTemplate.opsForValue().set(cacheKey, NULL, Duration.ofMinutes(10)); return Result.fail(章节不存在); } Chapter chapter chapterMapper.selectById(chapterId); ChapterContentVO vo new ChapterContentVO(chapter.getTitle(), content.getContent()); redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(vo), Duration.ofHours(2)); return Result.ok(vo); }书籍列表接口这里必须做字段裁剪只返回列表页需要的字段GetMapping(/api/books) public ResultPageBookListVO listBooks(RequestParam(defaultValue 1) int page, RequestParam(defaultValue 20) int size) { PageBook bookPage bookMapper.selectPage(new Page(page, size), new LambdaQueryWrapperBook().orderByDesc(Book::getUpdateTime)); return Result.ok(bookPage.convert(b - new BookListVO( b.getId(), b.getTitle(), b.getAuthor(), b.getCoverUrl(), b.getChapterCount(), b.getWordCount()))); }进度上报接口PostMapping(/api/progress) public ResultVoid report(RequestBody ProgressDTO dto) { progressMapper.upsertProgress(dto.getUserId(), dto.getBookId(), dto.getChapterId(), dto.getCharOffset(), dto.getPercent()); return Result.ok(); }对应 Mapper 里手写那条ON DUPLICATE KEY UPDATE不要用框架的saveOrUpdate那个是“先查后写”并发下会撞唯一索引。5.4 前端阅读区的排版参数前端这块我没有用复杂框架纯 HTML CSS 原生 JS重点是几个排版参数要拿出来说这些参数直接决定“读起来舒不舒服”。参数我的取值理由正文字号18px可调 14-2416px 在手机上偏小20px 以上长读眼睛累行高1.8中文行高 1.6 偏挤1.8 是最舒服的区间段间距0.8em中文小说段落多段间距太大显得散正文字体系统默认衬线体衬线体长文阅读疲劳感更低内容宽度最大 720px超过 720px 一行字太多眼睛容易串行背景色米白 #f8f5f0 或深灰 #1e1e1e纯白刺眼纯黑在强光下看不清字体大小调整我用了 CSS 变量切的时候只改一个--font-size所有依赖它的地方自动生效配合localStorage记住用户偏好。这个细节看着小但用起来体验差别巨大——每次打开书都要重新调字号谁都会烦。6. 踩坑实录与问题速查6.1 乱码、空章节与重复章节乱码上面讲过了编码探测加逐行回退能解决大部分。还有一种情况是文件里混了 HTML 实体比如nbsp;ldquo;直接显示出来很难看解析时统一做一次反转义即可。空章节很常见表现为目录里有“第一章”点进去白屏。原因是正则在“第一章”之后到“第二章”之间没抓到内容——可能是原文件这一章本来就是空的也可能是章节标题匹配成功但内容累积逻辑有 bug。我的排查方法是导出一份“章节内容长度分布”报表把所有word_count 0的章节挑出来人工看十有八九能定位到问题。重复章节更隐蔽。有些书前 20 章是重复的因为文件本身被人拼接了两遍。这种问题靠content_hash能发现——同一本书内出现两个不同chapter_index但content_hash相同的章节直接标记为疑似重复并跳过入库。实操心得解析完的书不要立刻标记为“可读”我加了一个“解析报告”页列出总章节数、空章节数、疑似重复章节数、最长章节字数、最短章节字数。数据异常的书直接走人工复核这比事后用户反馈“这本书读不了”要省事得多。6.2 上传超时与内存溢出50MB 的 TXT 上传默认 Spring Boot 配置直接给你一个 413 或者连接超时。需要改三处配置spring: servlet: multipart: max-file-size: 100MB max-request-size: 100MB mvc: async: request-timeout: 300000内存溢出主要来自两个地方一是把文件读成String二是一次性把所有章节内容塞进一个 List 再批量插入。前者改成BufferedReader逐行读后者改成每 500 章清一次 List。改完之后我用一个 80MB、4000 章的文件压测堆内存稳定在 200MB 以内没有出现 OOM。还有一点上传解析要改成异步。同步解析一本大书要 30 秒以上浏览器早超时了。我的做法是上传接口只负责存文件和创建一条“解析中”状态的记录然后扔给线程池异步解析前端轮询状态接口。状态从 1解析中到 2成功或 3失败失败时把异常信息存进remark字段用户能看到具体原因。6.3 进度错乱与滚动抖动进度错乱除了前面说的多端覆盖还有一个隐蔽原因章节 id 不稳定。如果用户重新导入同一本书章节全部重建id 全变了旧进度指向的 chapter_id 就失效了。解决办法有两种一是进度里同时存chapter_indexid 找不到时按 index 兜底二是重新导入时先做内容比对能复用的章节保留原 id。我选的是第一种实现简单容错够用。滚动抖动是前端问题表现在快速滑动时当前章节判断频繁切换进度上报了一堆中间值。解决靠IntersectionObserver加 300ms 防抖同时把判定阈值设成“章节标题露出 30% 以上才算进入该章”避免了边界处反复横跳。6.4 慢查询与索引缺失上线一段时间后我发现目录接口偶尔要 2 秒以上explain一看chapter表的查询走了全表扫描。原因是book_id上有索引但order by chapter_index没有走索引排序。补上(book_id, chapter_index)联合索引后降到 8ms。另一个慢点是书架列表。原来写的是select * from book where id in (select book_id from bookshelf where user_id ?)子查询在这个数据量下还好但一旦用户书架几百本、书籍表几万本性能就崩了。改成join之后快了很多SELECT b.id, b.title, b.author, b.cover_url, b.chapter_count, s.last_read_time FROM bookshelf s JOIN book b ON b.id s.book_id WHERE s.user_id ? ORDER BY s.sort_order ASC, s.last_read_time DESC LIMIT ?, ?;顺便说一句select *在正式代码里能不用就不用特别是book表以后可能会加description这种大字段。6.5 问题速查表现象最可能原因排查手段解决方式正文全是问号或方块编码判断错误用十六进制编辑器看文件头严格 UTF-8 试探后回退 GBK目录只有一章章节正则没匹配上打印前 100 行原文补充分支规则回归真实书籍点章节白屏正文表无记录查chapter_content是否有该 id检查切分时内容累积逻辑上传报 413multipart 限制看响应码调max-file-size上传超时同步解析耗时过长看日志时间戳改异步解析 状态轮询阅读进度丢失多端覆盖或章节 id 失效查update_time和chapter_id时间戳乐观锁 index 兜底目录接口慢缺联合索引explain看 type建(book_id, chapter_index)缓存不生效序列化方式不对看 Redis 里的 value 长度改存 JSON 字符串书架列表卡子查询或 select *看执行计划改 join 字段裁剪内存持续上涨大文件整体读入内存看堆监控改流式读取 分批入库最后分享一个我在这个项目里体会最深的事先把“一本书从头读到尾”这一条最短路径彻底打通再去加书签、笔记、多用户这些功能。我一开始贪多同时做书架分组、阅读统计、评论结果核心的解析和阅读体验反而一堆 bug天天在补窟窿。后来把附加功能全砍掉专心把“上传→解析→阅读→进度”跑顺再回头看那些功能其实都是在这一条主链路上挂载半天就能加一个。这个顺序如果你搞反了返工成本会比想象中大得多。
阅读完成 · 觉得有帮助?
咨询建站