微信小程序毕业设计:在线阅读系统源码包使用指南与避坑攻略 简介面向计算机专业毕业生与课程设计开发者这份基于微信小程序的在线阅读系统毕业设计资料完整覆盖小程序前端、Java后台与MySQL数据库整体采用B/S架构。资源共八十个文件包括十六个脚本逻辑文件、十五个配置文件、十四个图标文件、十一个页面文件、十三个样式文件另有数据库脚本和演示视频压缩包大小约八十八点三四兆。功能上系统分后台管理和微信小程序两大块后台可实现小说信息管理、注册用户管理等小程序端包含首页、分类、我的三个模块前后端业务衔接完整。目前已有1131人学习下载适合毕业设计、课程设计及小程序开发实践参考。压缩包内附数据库脚本、演示视频和源码目录结构便于快速运行部署、对照学习与二次开发。1. 基于微信小程序的毕业设计-在线阅读系统这套源码包该怎么用、坑在哪拿到一个标注着「源码数据库演示视频」的微信小程序在线阅读系统毕业设计包多数同学的第一反应是解压、翻目录、打开微信开发者工具直接编译然后在一个或几个红色报错里卡住。这个包的实质是三层交付物小程序前端源码、一份能导入的数据库脚本、一段用来答辩展示的演示视频。它解决的是毕业设计里「方向好选、完整落地难」的普遍问题——前后端联调、表结构设计、真机表现这些环节每一步都能劝退一批人。这篇笔记按我带毕业设计的顺序写先讲三层架构为什么这样搭再拆数据库表落一遍小程序端最小闭环最后把最容易翻车的几个环节逐条排掉。2. 先把架构理顺小程序端、后端接口、数据库三层怎么分工2.1 为什么毕业设计偏爱「原生微信小程序 后端接口 MySQL」这个组合在线阅读系统在毕设题目里出现频率很高因为它功能边界清晰用户能注册登录、浏览书目、打开章节、记录阅读进度。边界清晰意味着好讲、好验收、好画架构图。而「原生微信小程序 后端接口 MySQL」是这类包最常见的组合原因很实际。原生小程序不需要额外学 uni-app 或 Tarowxml 和 wxss 的语法半天就能上手毕设周期内完全来得及改页面。后端接口用 Node.js、PHP、Java 哪个都行只要它能连上 MySQL能接收小程序发来的 wx.request 请求并返回 JSON职责就完成了。数据库脚本独立成一个 .sql 文件也是这类包的常见做法目的很直接答辩前把数据库删了重新导入一遍系统就能恢复初始状态这是现场演示的「后悔药」。对比一下云开发方案云开发把小程序的数据库和云函数托管在微信侧写起来省事但答辩时老师多半会追问「你的表结构和后端逻辑是怎么设计的」你很难把云数据库的权限模型讲清楚。而自建 MySQL 的表结构、索引、外键关系都能一条条摆出来讲增删改查的 SQL 也能现场跑说服力强得多。所以我一般建议只要包里的数据库是 .sql 文件就老老实实走本地或服务器部署的 MySQL 路线不要中途换成云开发。2.2 一次完整请求长什么样从按钮点击到 JSON 回显理解这套系统不需要从源码第一行读起你只要抓住一条数据链路页面事件 → wx.request → 后端路由 → SQL 查询 → JSON 返回 → setData 渲染。以「打开书架页」为例。用户点进小程序app.js 的 onLaunch 里先发起登录请求拿到登录态后书架页 onLoad 再请求图书列表接口。后端收到请求后去 book 表做一次 SELECT把结果包成统一格式的 JSON 回给前端前端再通过 wx:for 渲染成卡片列表。整个链路里前端只负责「发请求、收 JSON、渲染」后端只负责「收参数、查库、回 JSON」数据库只负责「存和取」。这套契约关系不写清楚后面联调就是黑匣子。我见过的翻车案例里十有八九是前端改了字段名、后端没同步或者后端返回的 data 结构里多包了一层前端取不到值。所以拿到源码后先别急着跑找一找后端代码里的统一返回结构一般长这样// 后端统一返回格式示例Node.js 写法PHP/Java 同理 { code: 0, // 0 表示业务成功非 0 表示有错误 message: ok, // 错误时放提示文案 data: { // 业务数据前端渲染全从这里取 list: [], total: 12, page: 1 } }这个格式里有三个约定需要你记住。第一code 不能只判断 HTTP 状态码因为 200 只能说明请求通了业务是否成功要看 code 是不是 0。第二data 里如果有分页page 和 pageSize 要跟前端传参名字完全一致差一个字母就查不出数据。第三时间字段后端返回的格式要统一要么全是字符串要么全是时间戳混着来会让前端排序和展示一起出错。2.3 交付包里的目录结构miniprogram、server、database 三块怎么认毕设源码包的命名五花八门但拆开看一般逃不出三块。前端是小程序目录常见命名是 miniprogram 或 client后端是服务端目录常见命名是 server 或 api数据库脚本通常单独放名字类似 database、db 或 sql。用代码块示意一下最常见的组织方式. ├── miniprogram/ # 小程序前端 │ ├── app.js # 全局入口登录逻辑通常在这里 │ ├── app.json # 页面路由与窗口配置 │ ├── pages/ │ │ ├── index/ # 书架或首页 │ │ ├── category/ # 分类页 │ │ ├── reader/ # 阅读器页面 │ │ └── mine/ # 个人中心 │ └── utils/ # 请求封装、工具函数 ├── server/ # 后端接口服务 │ ├── app.js # Node.js 入口或 index.php │ ├── routes/ # 路由登录、图书、书架 │ └── package.json └── database/ └── reading.sql # 建表语句 初始数据拿到包以后先做一件事把 app.js 里请求地址的 baseUrl 找出来确认它指向哪个后端。常见情况是包里的接口地址写着 127.0.0.1 或 localhost这是作者在本地调试时留下的。你要是直接在开发者工具里跑前端后端没启动请求必然失败。后端启动方式看 server 目录的说明文件。Node 项目一般是先 npm install 再 npm startPHP 项目要配好 Apache 或 Nginx 环境Java 项目通常是打包成 jar 后运行。如果包里没写启动步骤按你本机熟悉的语言环境来猜比盲目改代码高效。还有一点server 目录里的数据库连接配置通常会单独放一个 config 文件里面是数据库地址、端口、用户名、密码这段配置前后端能不能连上库全看它。2.4 后端用什么写不重要接口契约才重要很多同学拿到包以后纠结一件事后端的语言自己没学过是不是要重写一遍。其实不用。我见过不少毕设包的后端是 PHP 写的但学生只学过 Java于是硬生生把接口用 Spring Boot 重写了一遍结果改装了半个月得不偿失。毕设答辩的核心是「讲清楚业务逻辑」不是「证明你代码量大」。后端是黑匣子也好、白匣子也好只要接口路径、请求方式、参数名和前端代码对得上就能跑通演示。你真正要花时间读的是接口契约登录接口传什么、返回什么书架列表怎么分页翻页后进度用哪个字段上报。这些信息在 server/routes 下每个文件里都能找到路由函数里的注释和参数名就是最好的说明书。如果你确实想换后端语言也请先让原来的后端跑通一遍用 Postman 或命令行 curl 验证几个关键接口返回正常再动手重写。重写时只保留接口路径和返回格式前端一行都不用改。这样即使新代码出了 bug你也知道问题出在后端而不是前端联调。3. 数据库先行四张核心表的设计与建表 SQL3.1 表清单与字段关系在线阅读系统再简单也绕不开四个核心实体用户、图书、章节、书架。分类信息一般做成第五张表也可以直接用一个字段存图书的 category_id。下面这张表是这类毕设包里最常见的结构字段名可能会有出入但职责八九不离十。表名职责核心字段关系user用户信息id, openid, nickname, avatar_url被 bookshelf 引用book图书元数据id, title, author, intro, cover, category_id被 chapter 和 bookshelf 引用chapter章节正文id, book_id, chapter_no, title, content多对一指向 bookbookshelf用户书架与阅读进度id, user_id, book_id, last_chapter_id, progress连接 user 和 book表之间的关系一句话就能讲完一本书有多个章节一个用户可以在书架上放多本书每本书记录最近读到第几章。这个关系在答辩时一定会被问到所以你要能随手画出 ER 图并说出为什么用外键关联而不是把所有信息塞进一张大表。3.2 建表 SQL用户、图书、章节、书架下面这段 SQL 是完整的建库建表脚本字符集统一用 utf8mb4目的是让中文和 emoji 都能正常存储。逐段说明会在代码块后面展开。-- 建库utf8mb4 支持中文与 emoji CREATE DATABASE IF NOT EXISTS reading_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE reading_db; -- 用户表 CREATE TABLE user ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, openid VARCHAR(64) NOT NULL COMMENT 微信登录唯一标识, nickname VARCHAR(50) DEFAULT COMMENT 昵称, avatar_url VARCHAR(255) DEFAULT COMMENT 头像, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_openid (openid) ) ENGINEInnoDB; -- 图书表 CREATE TABLE book ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, title VARCHAR(100) NOT NULL COMMENT 书名, author VARCHAR(50) DEFAULT COMMENT 作者, intro TEXT COMMENT 简介, cover VARCHAR(255) DEFAULT COMMENT 封面图 URL, category_id INT UNSIGNED DEFAULT 1 COMMENT 分类 ID, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; -- 章节表 CREATE TABLE chapter ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, book_id INT UNSIGNED NOT NULL COMMENT 所属图书 ID, chapter_no INT UNSIGNED NOT NULL COMMENT 章节序号, title VARCHAR(100) DEFAULT COMMENT 章节标题, content MEDIUMTEXT COMMENT 正文内容, UNIQUE KEY uk_book_chapter (book_id, chapter_no) ) ENGINEInnoDB; -- 书架表记录阅读进度 CREATE TABLE bookshelf ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, user_id INT UNSIGNED NOT NULL COMMENT 用户 ID, book_id INT UNSIGNED NOT NULL COMMENT 图书 ID, last_chapter_id INT UNSIGNED DEFAULT 0 COMMENT 最近阅读章节 ID, progress TINYINT UNSIGNED DEFAULT 0 COMMENT 阅读进度百分比 0-100, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_user_book (user_id, book_id) ) ENGINEInnoDB;几个参数要说清楚。openid 加了唯一索引因为同一微信号只能注册一次这个唯一约束在登录接口里会用到重复插入时会报错你得在代码里捕获。chapter 表的 book_id 和 chapter_no 也建了联合唯一索引防止同一本书出现两个「第 3 章」。content 用 MEDIUMTEXT 而不是 TEXT因为在线阅读的章节正文动辄几千上万字TEXT 上限 64KB 可能不够装一章MEDIUMTEXT 上限是 16MB存书稿完全够。建表顺序也有讲究。先建 user 和 book再建 chapter最后建 bookshelf因为 bookshelf 的外键依赖前面三张表。如果你用 Navicat 或 phpMyAdmin 可视化导入顺序错了一般会自动处理如果用命令行 source 导入外键表在前面创建就会失败。3.3 补一批可演示的初始数据空表是没法演示的。包里如果有 SQL 文件一般会附带几本样书和若干章节。如果没有你自己也要插几条。初始数据的原则是够演示但不臃肿两本书每本书三到五章每章内容两三段即可。下面这个示例可以照抄也可以按需改书名-- 示例图书 INSERT INTO book (id, title, author, intro, category_id) VALUES (1, Spring Boot 实战笔记, 陈一, 面向初学者的后端入门书, 1), (2, 小程序开发常见坑, 李四, 收集小程序开发中的典型问题, 1); -- 示例章节 INSERT INTO chapter (book_id, chapter_no, title, content) VALUES (1, 1, 环境准备, 安装 JDK 与 Maven配置镜像。), (1, 2, 第一个接口, 编写 Controller 并测试返回 JSON。), (2, 1, 导航栏高度问题, 不同机型上 custom 导航栏高度不一致。), (2, 2, 登录态过期, token 过期后要引导用户重新登录。); -- 示例用户与书架 INSERT INTO user (id, openid, nickname) VALUES (1, test_openid_001, 演示用户); INSERT INTO bookshelf (user_id, book_id, last_chapter_id, progress) VALUES (1, 1, 2, 50), (1, 2, 1, 30);插入初始数据时有两个习惯值得养成。第一书的 id 尽量显式指定因为后面的 chapter 和 bookshelf 都引用它自增 id 在你本地跑的时候可能和包作者不一致。第二openid 字段先放一个测试值比如 test_openid_001这样本地登录还没调通时你可以直接在数据库里改出一条记录来测试书架接口不受微信登录限制。命令行导入脚本的方式也顺手记一下。假设 SQL 文件在 database/reading.sql终端里执行 source 命令或者用 mysql 客户端直导mysql -u root -p reading_db database/reading.sql导入成功的标志是执行完没有报错然后输入这行命令验证一下表是否建好mysql -u root -p -e USE reading_db; SHOW TABLES;看到 user、book、chapter、bookshelf 四张表都列出来数据库部分才算真正就绪。3.4 毕设答辩里的「数据库增删改查」四个高频 SQL 用例答辩时老师很少让你凭空背 SQL更喜欢指着页面问这个书架列表的数据是怎么查出来的那个进度是哪个字段存的所以你要把四个最常用的 SQL 场景提前准备好。第一查书架列表并带上图书信息这是首页最核心的查询。它涉及 bookshelf 和 book 两张表的关联用 JOIN 一次查出来比自己写两个查询再拼数据高效SELECT b.id, b.title, b.author, b.cover, s.last_chapter_id, s.progress FROM bookshelf s JOIN book b ON s.book_id b.id WHERE s.user_id 1 ORDER BY s.updated_at DESC;第二查一本书的章节列表按 chapter_no 升序排列阅读器翻页就靠它SELECT id, chapter_no, title FROM chapter WHERE book_id 1 ORDER BY chapter_no ASC;第三更新阅读进度翻页后把 last_chapter_id 和 progress 写回书架表。这里要注意更新语句必须带 WHERE 条件锁定 user_id 和 book_id否则会把整张表的进度都改掉UPDATE bookshelf SET last_chapter_id 3, progress 60 WHERE user_id 1 AND book_id 1;第四删除一本不再阅读的书从书架移除DELETE FROM bookshelf WHERE user_id 1 AND book_id 2;这四个 SQL 正好覆盖增删改查里的查、改、删插入你已经通过初始数据感受过了。建议把这几条语句存在一个 txt 里答辩前一晚对着表结构跑一遍确保手不抖。还有一个小技巧如果你用的工具支持「数据库同步」功能在本地改完数据后可以 export 一份最新 SQL 出来留着备用防止现场演示时手滑把数据弄乱这是最简单的后悔药。4. 小程序端跑通最小闭环登录、书架、阅读器的代码骨架4.1 app.js 里该放什么全局配置与静默登录小程序端所有页面都要向后端要数据所以请求地址和登录态最好放在全局避免每个页面重复写。app.js 是全局入口一般负责三件事定义 baseUrl、在 onLaunch 里触发登录、把 token 存进 storage。下面是一段典型的写法// app.js App({ globalData: { baseUrl: http://127.0.0.1:3000/api, // 本地调试地址 userInfo: null }, onLaunch() { this.login() }, login() { // 如果已经有 token就不重复登录 if (wx.getStorageSync(token)) { return } wx.login({ success: (res) { wx.request({ url: ${this.globalData.baseUrl}/auth/login, method: POST, data: { code: res.code }, success: (res) { if (res.data.code 0) { wx.setStorageSync(token, res.data.data.token) wx.setStorageSync(userInfo, res.data.data.userInfo) } else { console.error(登录失败, res.data.message) } } }) } }) } })这段代码里有几个关键参数值得注意。baseUrl 写法是 http 加端口如果你在开发者工具里调试并且勾选了「不校验合法域名」这个地址能直接通但如果换到真机预览127.0.0.1 指的是手机自己必须改成你电脑的局域网 IP这也是新手最容易踩的坑。login 方法里先检查 storage 里有没有 token是为了避免每次冷启动都重复换码。这里有一个取舍token 的过期时间通常由后端控制如果后端返回的 token 有效期很短这个提前 return 的逻辑反而会让用户莫名掉线到时候要手动清理缓存才能恢复。我个人的习惯是本地毕设演示阶段token 有效期直接拉长到一天省去很多麻烦。4.2 登录链路wx.login 换 code后端换 openid小程序的登录流程有个固定的套路前端 wx.login 只负责拿一个临时凭证 code真正的 openid 换取向微信服务器请求这个动作只能在后端做。很多同学以为 wx.login 返回的就是 openid其实不是code 五分钟内就失效而且只能换一次。前端部分已经写在 app.js 里了现在把后端的处理逻辑补上。下面用 Node.js 的 Express 写法示意PHP 和 Java 的实现的逻辑完全等价只是请求微信接口的方式不同// server/routes/auth.js —— 登录接口 const express require(express) const router express.Router() const https require(https) router.post(/login, (req, res) { const { code } req.body // 用 code 向微信服务端换取 openid const url https://api.weixin.qq.com/sns/jscode2session?appid${APPID}secret${APPSECRET}js_code${code}grant_typeauthorization_code https.get(url, (resp) { let body resp.on(data, chunk body chunk) resp.on(end, () { const data JSON.parse(body) if (data.errcode) { res.json({ code: 1001, message: 微信登录失败, data: null }) return } // 查一下用户是否存在不存在则插入 const openid data.openid // 伪代码SELECT * FROM user WHERE openid ? // 如果查不到INSERT INTO user(openid) VALUES (?) res.json({ code: 0, message: ok, data: { token: 自生成token, userInfo: { openid } } }) }) }) }) module.exports router这段代码里有两个地方需要你重点盯住。第一APPID 和 APPSECRET 来自微信公众平台的「开发管理」配置APPSECRET 一旦泄露别人就能冒充你的小程序换取用户信息所以它只能存在后端配置文件里绝不能出现在小程序前端代码中。第二换取 openid 的接口请求是后端发出的它需要你的服务器能正常访问微信服务器本地调试时网络通就行不需要额外配置。关于登录还要泼一盆冷水很多同学想在毕设里实现「微信手机号快捷登录」也就是点一个按钮直接获取用户手机号但这个能力的前提是小程序完成了企业主体认证个人主体的毕设小程序几乎不可能申请下来。所以包里的登录方案只要是 wx.login 加 openid就别折腾着升级成手机号登录老老实实做一个用户信息编辑页让用户自己填昵称这反而是新规下最稳的做法。4.3 书架页请求图书列表与 wx:for 渲染登录态就绪后第一个要跑通的页面就是书架首页。这个页面做的事情很简单onLoad 时请求 /bookshelf 接口把返回的列表渲染出来。下面是书架页的 JS 逻辑// pages/index/index.js const app getApp() Page({ data: { books: [], loading: false }, onLoad() { this.fetchBookshelf() }, fetchBookshelf() { this.setData({ loading: true }) wx.request({ url: ${app.globalData.baseUrl}/bookshelf, method: GET, header: { Authorization: wx.getStorageSync(token) }, success: (res) { if (res.data.code 0) { this.setData({ books: res.data.data.list }) } else { wx.showToast({ title: res.data.message, icon: none }) } }, complete: () { this.setData({ loading: false }) } }) }, openBook(e) { const id e.currentTarget.dataset.id wx.navigateTo({ url: /pages/reader/reader?id${id} }) } })对应的 wxml 模板用 wx:for 循环渲染view classbook-list view wx:for{{books}} wx:keyid classbook-card bindtapopenBook >// pages/reader/reader.js const app getApp() Page({ data: { bookId: 0, chapter: null, scrollTop: 0 }, onLoad(options) { this.setData({ bookId: options.id }) this.fetchChapter(options.id, 1) }, fetchChapter(bookId, chapterNo) { wx.request({ url: ${app.globalData.baseUrl}/books/${bookId}/chapters/${chapterNo}, method: GET, success: (res) { if (res.data.code 0) { this.setData({ chapter: res.data.data }) } } }) }, // 滚动结束时回调节流上报进度 onPageScroll(e) { if (this.scrollTimer) { clearTimeout(this.scrollTimer) } this.scrollTimer setTimeout(() { this.reportProgress() }, 800) } })pages 目录下的阅读器页面需要在 app.json 的 pages 数组里注册否则 navigateTo 跳转时会提示页面不存在。这里有一个新手常犯的错误手改 app.json 时漏加了逗号或者页面路径写错整个项目编译直接失败报错信息指向 app.json但问题实际出在路径配置。进度上报这行里有个小设计没有用 onPageScroll 里的每一次回调都上报而是设了 800ms 的防抖。因为 onPageScroll 在滚动时频繁触发如果每次触发都写一次数据库书架列表那里的 updated_at 会被疯狂刷新还会造成接口压力。等用户停止滚动 800ms 后再上报一次效果完全一样性能却好很多。如果你觉得页面滚动到底部自动加载下一章更有「阅读」的感觉可以在 scroll-view 的 bindscrolltolower 事件里调用 fetchChapter传参换成当前章节号加 1。但要记得在章节接口返回里带上该书的章节总数否则翻到最后一章时会一直请求不存在的章节。5. 常见问题排查从开发者工具到真机、从本机到服务器最典型的坑5.1 开发者工具正常真机一打开就白屏现象在微信开发者工具里预览页面正常渲染数据也能加载但用手机扫码预览小程序可以打开页面却一直转圈或白屏控制台报 request 失败。原因开发者工具默认勾选了「不校验合法域名」所以 http 明文地址、局域网 IP 地址都能请求。而真机上这个校验是强制的小程序在真机环境下会拒绝向非 HTTPS 且未备案的域名发起请求。解决分两步走。本地演示阶段把 baseUrl 从 127.0.0.1 改成电脑的局域网 IP比如 192.168.x.x但手机和电脑必须连同一个 WiFi同时关掉电脑防火墙对后端端口的外来访问限制。如果你想上线或者给评委远程演示那就在微信公众平台配置 request 合法域名并且准备一个带 HTTPS 证书的备案域名。毕设阶段不建议走第二步成本太高第一步够用。5.2 想用手机号快捷登录结果按钮一直报错现象代码里用 button open-typegetPhoneNumber点击后拿到的是 errMsg: getPhoneNumber:fail no permission或者按钮直接没有反应。原因手机号快捷登录是一个需要接口权限的能力只有完成微信认证的企业主体小程序才能调用。个人主体开发的小程序这个能力默认不开放和代码写不写对没有关系。解决把登录方案改回 wx.login 换 openid 的标准流程个人主体的毕设项目能正常跑通。用户头像和昵称也不要再用旧版的 wx.getUserProfile 去弹窗获取新规下这个接口对大部分场景已经受限正确做法是在个人中心放一个可点击的输入框和头像选择器由用户主动填写。5.3 数据库里中文全部变成问号现象SQL 文件导入后查询时看到的中文是乱码或者一串问号前端页面显示的书名、作者名全是「???」。原因八成是字符集不一致。SQL 文件本身是 utf8但数据库连接没指定字符集或者建库时用了默认的 latin1。字符集这种东西建库一步错后面全程错属于典型的「玄学」问题排查起来很费劲。解决统一三步。第一步建库语句显式写 DEFAULT CHARACTER SET utf8mb4就是第 3 章那段 SQL 的写法。第二步后端连接数据库的配置里加 charsetutf8mb4Node 的 mysql2 连接串是mysql://root:pwd127.0.0.1:3306/reading_db?charsetutf8mb4JDBC 是characterEncodingUTF-8。第三步已经导错的库不要只改表字段直接把库删了重新导入省事。5.4 自定义导航栏在真机上被胶囊按钮挡住现象页面上方自定义的导航栏在开发者工具里刚刚好一上真机标题文字被右上角胶囊按钮盖住或者左右两边的元素位置不齐。原因微信小程序的胶囊按钮右上角那个「…」和关闭按钮在不同机型上位置是不同的尤其是有灵动岛的新机型和普通机型状态栏高度和胶囊高度都有差异。自定义导航栏时你写的 top 值如果是一个写死的数字比如 64px那必然在某个机型上翻车。解决不要硬编码高度。用 wx.getWindowInfo() 获取 statusBarHeight 状态栏高度再用 wx.getMenuButtonBoundingClientRect() 拿到胶囊按钮的位置信息动态计算导航栏高度。这两个 API 返回的值是真机上实测的比任何固定参数都可靠。涉及页面滚动时还要记得把固定定位的 top 值也改成动态计算的结果。5.5 数据库导入报错版本差异、字符集、导入顺序现象用命令行 source 导入 SQL 文件时报错信息五花八门要么是 unknown collation要么是 Foreign key constraint is incorrectly formed要么是 Table already exists。原因第一种是 SQL 文件的字符集或排序规则版本比本地 MySQL 旧高版本里某些 collation 名字已经变了第二种是外键依赖的表还没存在就被引用第三种最简单你之前已经导过一次没有先删旧库。解决导入前先确认本地 MySQL 版本。MySQL 5.7 和 8.0 的差异最常见的就是 utf8mb4_0900_ai_ci 这个排序规则在 5.7 里不存在如果报错提到 collation把 COLLATE 去掉重新建库。外键顺序问题在第 3 章提过先建主表再建从表。至于 Table already exists一条 DROP TABLE IF EXISTS 语句解决或者直接把整个库删掉再建。如果你用的是可视化工具导入时可以勾选「遇到错误继续执行」但我不建议因为部分失败会留下脏数据还不如报错后清干净重来。5.6 阅读进度保存不准翻几页就一直跳现象阅读器里读了几章回到书架页面看进度要么停在 0%要么跳到一个完全不对的百分比。刷新几次进度忽高忽低。原因进度上报的时机和字段对不上。常见两种一种是上报接口传的是章节 ID但书架页展示时却除以了全书总章节数分母错了另一种是每次都传整书的章节序号却因为防抖设置得太短滚动一下触发了好几次把中间态的旧章节点覆盖了。解决在代码里打日志把每次上报的 bookId、chapterNo、进度值打出来看是不是「读第 3 章 → 上报 3 → 回列表显示 30%」这条链路。如果数值全对但显示不对检查 bookshelf 表的 updated_at 和书架列表的排序逻辑问题可能出在 ORDER BY 用的是创建时间而不是更新时间。6. 交付前最后一公里演示视频怎么录、答辩怎么讲演示视频这个交付物看起来只是录个屏实际作用是让老师在不打开你电脑的情况下也能判断项目完整度所以它的优先级很高。我建议按三条主线来录每条别超过一分钟。第一条线是启动与登录打开小程序、展示后端服务在运行、数据库连接正常、进入书架看到初始数据。第二条线是核心功能打开一本书、翻几章、回到书架确认进度变化再退到后台重进一次验证数据真的存住了。第三条线是数据管理用数据库工具或后端管理接口加一本新书回到小程序刷新能看到它。这三段下来系统的前端、后端、数据库三个层次就都展示全了。录之前把数据库恢复到初始状态避免出现「书架里有很多条测试脏数据」这种尴尬。录制工具用手机自带的录屏就好记得打开麦克风边录边讲你在操作什么评委看视频时不需要猜你的意图。真机录完再用开发者工具的模拟器录一遍界面特写两个视角拼接观感会专业很多。答辩提问基本围绕三个方向展开。数据结构相关的会问「user 表和 bookshelf 表是什么关系」你要能说出一个用户对应多条书架记录。设计决策相关的会问「为什么选 MySQL 而不是直接存文件」答案围绕结构化查询、进度更新可靠、易于备份展开。扩展性相关的会问「如果用户量大了怎么办」你不需要真正做过优化能说出加索引、分页查询、引入缓存这三个词就已经超出多数毕设的水平了。最后说一个我自己的教训。有一年带的学生做类似系统答辩前一周数据库莫名连不上后来发现是他改了 MySQL 密码但后端配置文件里没同步更新。那一次让我意识到所有配置项都该在演示前走一遍「从零恢复」的流程把数据库删掉重新导入、改掉 baseUrl 指向新环境、后端重启、小程序冷启动四步能串起来现场才不会翻车。这套流程也是你拿到任何一个源码包之后最值钱的验证方式跑得过它这个项目才算真正属于你。希望帮到你。本文还有配套的精品资源点击获取