1. 整体定位:这个平台到底在解决什么问题
不夸张地说,SpringBoot+Vue这个组合,现在已经是Java后端开发最主流的搭配之一。如果你在找毕设或课设题目,"学生读书笔记共享平台"这类项目是个非常好的选择——技术覆盖全面、业务逻辑清晰、数据模型也不复杂,特别适合用来展示前后端分离开发能力。很多学校用这个题目一用就是好几年,就是因为它在"够用"和"出彩"之间找到了一种很好的平衡。
先把这个项目讲清楚。它本质上是一个围绕"书籍+笔记"双核心的UGC(用户生成内容)平台:普通用户浏览书籍、撰写读书笔记、收藏点赞、在笔记下交流;管理员负责书籍和分类的维护、用户管理、数据概览。听上去和简书、豆瓣读书很像,但规模小得多,技术实现上也没那么复杂。正因为复杂度适中,它才能成为毕设和课设的经典选择——后端有CRUD、有鉴权、有关联查询,前端有列表页、详情页、表单页,数据库有多表关联,把这些串起来,一份像样的毕业设计就齐了。
我第一次带学生做这个项目时,最深的感受是:这题目不像电商系统那样堆一堆订单、库存、支付逻辑,却能把SpringBoot和Vue的核心用法全部覆盖到。对学习者来说,做完这个项目,你对"SpringBoot+MyBatis-Plus+JWT+Vue+Axios"这一整条链路会有一个完整的肌肉记忆,而不是停留在"看过视频"的程度。
而且要注意,这类项目市面上成品源码很多,但很多是十几年前SpringMVC+JSP的老思路改造的,表面写着SpringBoot,实际还是服务端渲染那一套。我下面讲的这套方案是真正的前后端分离结构,前端Vue工程独立运行,后端只提供RESTful接口,这套思路直接对标企业开发方式,哪怕以后你去做真正的商业项目,底层逻辑也是一样的。
2. 技术选型与整体设计思路
1.1 核心需求拆解:读书笔记平台必须有哪些功能
做项目最忌讳的是一上来就敲代码,先把需求理清楚。学生读书笔记共享平台,核心参与者就两种:普通用户和管理员。围绕"读书"这个动作,普通用户关心的路径是:找书→看书介绍→写笔记→浏览别人的笔记→互动;管理员关心的路径是:内容能不能控制→用户能不能管→网站状态怎么样。把这两条路径落到功能菜单上:
- 用户端:注册登录、书籍列表(按分类筛选、关键词搜索)、书籍详情、笔记列表(按发布时间、热度排序)、笔记详情、发布笔记、编辑删除自己的笔记、点赞收藏、个人书单/我的笔记、修改资料和密码。
- 管理端:登录(独立账号体系或复用用户表)、书籍管理(增删改查、上传封面)、笔记管理(审核/下架违规内容)、分类管理、用户管理(禁用/启用账号)、统计数据(用户数、笔记数、书籍数)。
这套功能清单不贪多,但每个功能都不是摆设。比如"审核/下架笔记"看起来简单,但它涉及用户表和笔记表的状态字段设计,也涉及后端接口的权限判断,加了它项目的完整度会高一个档次。
1.2 技术选型:为什么是这套组合拳
SpringBoot+MyBatis-Plus+MySQL+Vue这套组合,在2025年的Java生态里依然是学生项目的主流配置,原因有三:
第一,SpringBoot极大降低了配置成本。以前SSM时代的XML配置能写几百行,SpringBoot自动配置把大部分事情办了,非常适合学习周期有限的毕设人群。用IDEA初始化项目时选几个依赖,工程就已经能跑了。
第二,MyBatis-Plus解决"单表CRUD不想写SQL"的痛点。毕设项目里80%的操作是单表增删改查,用MyBatis-Plus的BaseMapper直接搞定,既保留了手写SQL的灵活性,又不用像JPA那样绕弯子。书籍笔记这类表之间有关联,复杂查询就自己写XML里的SQL,怎么都不亏。
第三,Vue+Element UI/Element Plus让前端有"成品感"。写后台管理界面时,Element的表格、表单、弹窗组件拿过来就能用,页面颜值在线,答辩时不会因为界面丑被扣分。
至于数据库,MySQL 5.7或8.0都可以。新机器建议直接上8.0,性能好、默认字符集搞定utf8mb4,但要注意和驱动版本匹配,后面我会专门讲这个坑。
1.3 一个核心设计决策:前后端分离的原因
我曾经见过同一个项目有"低代码版本"和"前后端分离版本"两种实现,最后大家公认分离版本更容易讲清楚。因为前后端分离让职责边界非常干净:前端管页面渲染和用户交互,后端管业务规则和数据存取。开发时前端用Vue devServer跑在8080端口,后端SpringBoot跑在9090端口,通过代理转发搞定联调,部署时前端npm run build生成静态文件扔给Nginx,后端打包成jar独立运行。整套流程是标准的企业级流水线,答辩时一说"前后端分离架构",老师基本不会再追问架构层面的问题。
3. 数据库设计与核心表结构
2.1 表全景:七张表把业务闭环串起来
数据库设计是毕设答辩时容易被追问的部分,所以每一张表都要想清楚为什么存在。我的建议是七张表起步:用户表、图书表、分类表、笔记表、点赞表、收藏表、笔记评论表。如果你想让数据统计更灵活,还可以加一张浏览记录表,但学生项目一般用不到。
这几张表的关系我用大白话拆解一下:
- 用户和笔记:一对多,一个用户可以写多篇笔记,用户被删了笔记怎么处理?这里建议用逻辑删除而不是物理删除。
- 图书和笔记:一对多,一篇笔记必须归属某本书。
- 图书和分类:多对一,一本书属于一个分类。
- 用户和图书:多对多,通过收藏表/点赞表实现,虽然这两个表存的内容不一样,但表结构上都是"用户ID+图书ID/笔记ID"的关联模式。
实际建表时我踩过的一个坑是:笔记表里既要有图书ID,又要冗余一个书名和作者。表面上看这违反了数据库范式,但实际操作中,笔记列表页要展示"某本书下的笔记",如果每次都要JOIN图书表,列表一长查询就会变慢,代码也会啰嗦。对毕设项目来说,适当的字段冗余换来的是接口书写流畅,值得。
2.2 建表SQL与关键字段设计的细节
下面这套SQL是我实际用过的,直接复制后根据自己需要改库名即可:
-- 用户表 CREATE TABLE `user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '登录名', `password` varchar(100) NOT NULL COMMENT '密码(加盐哈希)', `nickname` varchar(50) DEFAULT NULL COMMENT '昵称', `avatar` varchar(255) DEFAULT NULL COMMENT '头像URL', `role` tinyint(4) DEFAULT '0' COMMENT '0普通用户 1管理员', `status` tinyint(4) DEFAULT '1' COMMENT '1正常 0禁用', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 分类表 CREATE TABLE `category` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL, `sort` int(11) DEFAULT '0', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 图书表 CREATE TABLE `book` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `title` varchar(100) NOT NULL COMMENT '书名', `author` varchar(50) DEFAULT NULL, `cover` varchar(255) DEFAULT NULL COMMENT '封面图URL', `category_id` bigint(20) DEFAULT NULL, `description` text COMMENT '图书简介', `status` tinyint(4) DEFAULT '1', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_category` (`category_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 笔记表 CREATE TABLE `note` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `book_id` bigint(20) NOT NULL, `user_id` bigint(20) NOT NULL, `title` varchar(100) NOT NULL, `content` longtext COMMENT '笔记正文,支持Markdown', `view_count` int(11) DEFAULT '0', `like_count` int(11) DEFAULT '0', `status` tinyint(4) DEFAULT '1' COMMENT '1正常 0下架', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_book` (`book_id`), KEY `idx_user` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 点赞表、收藏表、评论表略,核心是(user_id, target_id)联合唯一索引两个容易被忽视的关键点:点赞表和收藏表要建联合唯一索引,防止同一用户对同一篇文章点两次赞,这比在Java代码里先查再插要可靠得多;所有时间字段建议直接用datetime类型,不要用int存时间戳,否则前端展示还要自己转格式,平白多出很多代码量。
2.3 逻辑删除还是物理删除
这个点几乎每次答辩都会被问。我的建议是:用户表和笔记表用逻辑删除,点赞收藏评论用物理删除。逻辑删除就是加一个deleted字段(或者上面写的status字段),删除时UPDATE状态,查询时默认只查状态正常的。它最大的好处是数据不丢失,管理员可以从后台恢复误删的笔记。而点赞记录这类无价值的关联数据,物理DELETE干净利落,不需要操心状态过滤。
MyBatis-Plus对逻辑删除支持得很到位,在实体类的status字段上加@TableLogic注解,配置全局逻辑删除值,然后你调deleteById的时候它自动变成UPDATE,查的时候自动加上WHERE status = 1。这个细节说出来老师会认为你是懂行的。
4. 后端核心实现:SpringBoot+JWT+接口开发全记录
3.1 工程初始化与依赖管理
用IDEA新建SpringBoot项目时,Group填com.example,Artifact填reading-note-platform,Java版本按8或11都行(如果用的新版IDEA和SpringBoot 3.x,就选17,后面会讲兼容性)。依赖方面,我建议勾选这几个:
- Spring Web:提供RESTful接口能力
- MySQL Driver:数据库驱动
- Lombok:省掉getter/setter模板代码
- Spring Boot DevTools:热部署,改代码自动重启,提升开发效率
pom.xml里手动补上MyBatis-Plus和JWT相关依赖:
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.4</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.22</version> </dependency>Hutool是个Java工具库,里面有现成的密码加密、随机验证码、日期格式化方法,能帮你省掉一大批重复代码。当然你也可以不用它,但用了之后代码会精简很多,我项目里的MD5加盐加密和Token生成都用它完成。
3.2 application.yml配置与统一返回体设计
项目配置文件是重灾区,很多同学起步就挂在MySQL时区问题上。我这里给出一份可以直接跑的配置:
server: port: 9090 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/reading_note?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&useSSL=false username: root password: 你的密码 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: status logic-delete-value: 0 logic-not-delete-value: 1注意serverTimezone=Asia/Shanghai,不加这个你连数据库时会报时间错误。jackson.date-format是为了让后端返回的时间字段直接变成"2025-01-15 21:30:00"格式,前端就不需要再写格式化函数了。
接口返回结构建议统一封装,以后所有的Controller都返回这个类型:
@Data public class Result<T> { private Integer code; // 200成功,4xx业务错误,401未登录 private String msg; private T data; public static <T> Result<T> success(T data) { Result<T> r = new Result<>(); r.setCode(200); r.setMsg("操作成功"); r.setData(data); return r; } public static <T> Result<T> error(String msg) { Result<T> r = new Result<>(); r.setCode(500); r.setMsg(msg); return r; } public static <T> Result<T> unauthorized() { Result<T> r = new Result<>(); r.setCode(401); r.setMsg("请先登录"); return r; } }这样做最直接的好处是,前端Axios拦截器只需要判断code === 200就进入业务逻辑,其余情况统一弹错误提示,代码里不用到处写try-catch。
3.3 JWT登录鉴权:流程与代码
登录鉴权这块是后端最容易被追问的部分。完整流程是:用户提交用户名密码→校验密码→生成JWT字符串→返回给前端→前端后续请求在Header里带Authorization: Bearer <token>→后端拦截器解析token并获取用户ID→写入ThreadLocal方便业务层取用。
JWT生成工具类核心代码如下:
public class JwtUtils { private static final String SECRET = "your-secret-key-change-it"; public static String generateToken(Long userId, String username, String role) { return Jwts.builder() .setSubject(username) .claim("userId", userId) .claim("role", role) .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24)) // 1小时,可以视情况调整 .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody(); } }拦截器的核心逻辑:在preHandle里取出Header,去掉"Bearer "前缀,调用parseToken解析,成功就把userId放到ThreadLocal里,失败直接返回401。放行列表要包含登录注册接口、书籍列表查询、笔记列表查询这些公开接口。
这里强调一个很多新手容易搞混的点:拦截器只管"是否登录",至于"能不能操作这篇笔记"(比如删除别人的笔记),那是业务接口内部要做的归属判断。我见过很多代码只在拦截器里判断一遍,导致用户可以随意改别人的内容,这个问题在设计时就要想好。
3.4 核心业务接口实现思路
以笔记模块为例,最核心的接口是"发布笔记"和"笔记列表分页查询"。
发布笔记时前端会传来bookId、title、content这三个字段。后端Controller里从ThreadLocal拿到当前登录用户ID,new一个Note实体,通过MyBatis-Plus的save方法插入,然后让图书表的笔记数加1。这个"图书表笔记数+1"操作,初学者容易忽略,但它是对关联数据的维护,不做的话图书详情页的统计数量永远是0。
笔记列表分页查询用MyBatis-Plus的Page对象即可:
@GetMapping("/notes") public Result<Page<Note>> pageNotes(@RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, @RequestParam(required = false) Long bookId, @RequestParam(required = false) String keyword) { Page<Note> page = new Page<>(pageNum, pageSize); LambdaQueryWrapper<Note> wrapper = new LambdaQueryWrapper<>(); if (bookId != null) { wrapper.eq(Note::getBookId, bookId); } if (StrUtil.isNotBlank(keyword)) { wrapper.like(Note::getTitle, keyword); } wrapper.orderByDesc(Note::getCreateTime); noteMapper.selectPage(page, wrapper); return Result.success(page); }如果列表页需要连带显示用户名和书名(而不是查回前端再去挨个表查),就要用自定义SQL联表查询。在NoteMapper.xml里写一个selectPageWithDetail,用LEFT JOIN关联user表取nickname,关联book表取title。这套多表查询代码也是答辩时的加分点。
3.5 文件上传:封面图处理方案
书籍封面上传看似简单,里面也有坑。我推荐把图片存服务器本地目录,而不是塞进数据库BLOB字段。后端接收MultipartFile,用UUID生成文件名,保存到resources/upload/目录,同时把文件的访问URL返回给前端。实现方案如下:
@PostMapping("/upload") public Result<String> upload(@RequestParam("file") MultipartFile file) throws IOException { String originalFilename = file.getOriginalFilename(); String suffix = originalFilename.substring(originalFilename.lastIndexOf(".")); String newName = UUID.randomUUID().toString().replace("-", "") + suffix; File dir = new File(uploadPath); if (!dir.exists()) { dir.mkdirs(); } file.transferTo(new File(dir, newName)); String url = "/upload/" + newName; return Result.success(url); }但你一定要给SpringBoot配置一个虚拟路径映射,否则图片URL访问不到:
@Configuration public class WebConfig implements WebMvcConfigurer { @Value("${file.upload-path}") private String uploadPath; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceHandler("file:" + uploadPath + "/"); } }这里有个细节:把绝对路径写在application.yml里,以后部署到服务器不用改代码,只改配置。如果你把uploadPath写死在代码里,换环境就麻烦。
5. 前端实现:Vue工程搭建与页面逻辑
4.1 工程初始化与路由设计
前端我建议用Vue 2 + Element UI,还是Vue 3 + Element Plus?如果是全新学习,直接上Vue 3,现在不管是中文社区还是插件的生态都已经很成熟了。创建工程用Vite比webpack快得多,一条命令搞定:
npm create vite@latest reading-note-web -- --template vue cd reading-note-web npm install npm install element-plus axios vue-router@4 pinia路由规划我建议分两层:前台页面和后台管理页面。前台包括图书列表、图书详情、笔记详情、登录注册、个人中心;后台包括概览、书籍管理、笔记管理、分类管理、用户管理。用路由懒加载避免首屏加载太慢:
const routes = [ { path: '/', component: () => import('../views/Home.vue'), }, { path: '/book/:id', component: () => import('../views/BookDetail.vue'), }, { path: '/admin', component: () => import('../layout/AdminLayout.vue'), redirect: '/admin/dashboard', children: [ { path: 'books', component: () => import('../views/admin/BookManage.vue'), }, // 其他管理页面 ], }, ];路由守卫是必须写的。需求是:进入个人中心和后台管理页必须登录,管理员页面还需要判断角色。在beforeEach里读取本地存储的token和用户信息,没有就直接跳登录页。
4.2 Axios封装与API模块管理
Axios拦截器封装是前端工程的关键环节。你肯定不想每个页面都手动用axios.get然后写错误处理,所以统一封装成工具库:
import axios from 'axios'; import { ElMessage } from 'element-plus'; import router from '../router'; const request = axios.create({ baseURL: '/api', // 开发环境通过Vite代理转发到后端 timeout: 10000, }); request.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }); request.interceptors.response.use( response => { const res = response.data; if (res.code !== 200) { ElMessage.error(res.msg || '请求失败'); return Promise.reject(new Error(res.msg)); } return res.data; }, error => { if (error.response?.status === 401) { localStorage.removeItem('token'); router.push('/login'); } ElMessage.error('网络请求失败'); return Promise.reject(error); } ); export default request;这样每个页面的API调用就变成了:
export const getNoteList = (params) => request.get('/notes', { params }); export const publishNote = (data) => request.post('/notes', data);页面里只管拿数据和渲染,出错弹提示和跳登录都是拦截器统一处理。这种写法在答辩时讲"前端工程化",说服力很强。
4.3 核心页面实现:笔记发布与Markdown编辑
笔记发布页是前端最有技术含量的页面。因为笔记内容是Markdown格式,所以需要引入一个编辑器组件。我这里用的是mavon-editor(Vue 3兼容版),几行代码就能在页面里放一个完整的Markdown编辑器+预览区域:
<template> <div> <el-form label-width="80px"> <el-form-item label="选择书籍"> <el-select v-model="form.bookId" placeholder="请选择书籍"> <el-option v-for="book in bookList" :key="book.id" :label="book.title" :value="book.id" /> </el-select> </el-form-item> <el-form-item label="标题"> <el-input v-model="form.title" placeholder="请输入笔记标题" /> </el-form-item> <el-form-item label="正文"> <mavon-editor v-model="form.content" style="height: 400px" /> </el-form-item> <el-form-item> <el-button type="primary" @click="submit">发 布</el-button> </el-form-item> </el-form> </div> </template> <script setup> import { ref, reactive, onMounted } from 'vue'; import request from '../../api/request'; import { ElMessage } from 'element-plus'; const form = reactive({ bookId: null, title: '', content: '' }); const bookList = ref([]); onMounted(async () => { bookList.value = await request.get('/books/all'); }); const submit = async () => { if (!form.bookId || !form.title || !form.content) { ElMessage.warning('请填写完整信息'); return; } await request.post('/notes', form); ElMessage.success('发布成功'); // 跳转到新笔记详情 }; </script>这里有两个小细节:编辑器组件取决于项目版本的兼容性,如果使用Vue 3 + Vite,需要确认编辑器插件是否有对应的Vue 3版本,否则会出现"插件不渲染"的诡异问题。提交前必须有非空校验,后端也要校验,绝不能只做前端校验。
6. 前后端联调与权限细节处理
5.1 跨域问题:开发环境和生产环境的两种解法
前端跑在5173端口(Vite默认),后端跑在9090端口,跨域是绕不开的问题。我推荐开发环境用Vite代理,生产环境用Nginx代理。
Vite配置vite.config.js:
export default defineConfig({ server: { port: 5173, proxy: { '/api': { target: 'http://localhost:9090', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, ''), }, }, }, });这样前端请求/api/notes时代理会转发到http://localhost:9090/notes,前端代码里没有任何跨域痕迹。生产环境则让Nginx把/api/前缀转发到jar包端口,配置更简单。用这种代理方式,后端的WebMvcConfigurer里就不需要额外写CorsRegistry,但如果你想让其他域名也能直接访问接口,后端仍然需要配置跨域,二选一,别两种都写,否则会出现"两次Access-Control-Allow-Origin"的坑。
5.2 权限操作的前后端配合
我讲一个学生项目里最常见的安全漏洞:前端把"删除"按钮隐藏了,但后端接口完全可以被直接调用,导致用户绕开前端操作接口越权。正确做法是:后端接口里必须做归属校验,比如删除笔记时先根据noteId查笔记,判断note.getUserId().equals(currentUserId)才放行。
用AOP做权限控制对学生项目来说有点重,拦截器+方法内判断就够了。核心是养成习惯:凡是修改型接口,都要问一句"当前登录用户有没有权利操作这个资源"。
5.3 点赞收藏的状态同步问题
点赞/收藏在页面上的交互是"点亮/取消点亮"和数量同步变化。做法是:进入详情页时,前端带着笔记ID请求后端GET /notes/{id}/like-status,后端根据当前登录用户的ID去like表查,返回true/false。点击点赞按钮时,调POST /notes/{id}/like,返回操作后的最新likeCount。前端用这个返回值更新显示,而不是前端自己加减,避免数值不一致。
7. 环境搭建与部署上线
6.1 本地开发环境版本匹配建议
版本问题占了这个项目70%的启动报错。给出的建议:
- JDK:SpringBoot 2.x用JDK 8或11;SpringBoot 3.x必须JDK 17及以上。如果你用IDEA 2023以上版本创建项目,默认拉取的是SpringBoot 3.x,所以不小心选了JDK 8就会直接编译失败。
- MySQL:5.7或8.0均可,但驱动版本注意。SpringBoot 2.7.8默认的MySQL驱动是8.0.33,连接MySQL 5.7没问题;如果用的更老驱动去连MySQL 8.0,会报认证协议错误。
- Node.js:Vite 5需要Node 18以上,先执行
node -v检查。 - 前端依赖安装慢:换国内镜像源,
npm config set registry https://registry.npmmirror.com,这是最快的解决方案。
6.2 前后端打包与服务器部署
后端打包直接用Maven插件:mvn clean package -DskipTests,然后得到一个reading-note-platform-0.0.1-SNAPSHOT.jar。上传到服务器后执行:
nohup java -jar reading-note-platform-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod > log.txt 2>&1 &前端部署前要注意接口地址不能写死为localhost。在vite.config.js里设置envDir或用环境变量区分开发/生产,然后执行npm run build,Vite会生成dist目录,把里面所有文件上传到服务器Nginx的html目录。
Nginx配置核心部分:
server { listen 80; server_name your-domain.com; root /var/www/reading-note-web/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:9090/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # 前端路由使用history模式时的关键配置 location / { try_files $uri $uri/ /index.html; } }try_files那一行是很多同学部署后白屏的原因——Vue Router的history模式下,直接刷新/book/3时Nginx找不到这个文件,必须让它回退到index.html,由前端路由接管。
6.3 进程守护:不挂项目的后台运行方案
很多人用nohup java -jar启动后端,但服务器一重启进程就没了。学生项目虽然不要求高可用,但答辩演示时服务挂了很尴尬。推荐用systemd管理Java进程,新建/etc/systemd/system/note.service:
[Unit] Description=Reading Note Platform After=network.target [Service] ExecStart=/usr/bin/java -jar /opt/reading-note-platform.jar Restart=on-failure RestartSec=10 [Install] WantedBy=multi-user.target然后systemctl enable note设置开机自启,后面用systemctl start note、systemctl status note管理服务,比nohup靠谱得多。
8. 常见问题与排查技巧实录
7.1 SpringBoot启动直接报"Failed to configure a DataSource"
这类报错通常校验的是配置项,最典型的原因是:application.yml里数据库密码配置了中文,或者driver-class-name写成了老版的com.mysql.jdbc.Driver。新驱动必须是com.mysql.cj.jdbc.Driver。还有一个隐藏坑是SpringBoot启动时会先去校验spring.datasource.url,如果你的数据库服务没启动,也会报这个错。排查顺序:先确认MySQL服务启动,再用Navicat那边测试连接。
7.2 Vue前端一直报401但接口是对的
如果你确定登录成功、token存在localStorage里,但还是401,十有八九是Axios请求拦截器没有正确读取token。排查方法:打开浏览器DevTools的Network面板,看请求头里有没有Authorization字段——没有就是前端拦截器没生效,有但是报401,那就是后端解析token失败。另外注意拦截器里config.headers.Authorization = ...在Vue 3 + Axios新版里一般没问题,但老版本Axios有大小写问题,统一用Authorization。
7.3 前端白屏:没有报错但页面只剩背景
这是后端路由配置的问题,90%是后端返回了一个路由不存在的错误,但前端没有做统一捕获,导致页面崩溃。先用Postman直接调接口,确认返回数据正常,再看浏览器Console里有没有具体报错。如果发现是404,排查Controller的@RequestMapping路径是否和前端请求路径一致。很多同学喜欢在类上写@RequestMapping("/api/note"),又在方法上写@GetMapping("/list"),前端请求却是/api/notes/list,字母对不上,非常容易疏忽。
7.4 Markdown编辑器显示原始代码而不是渲染结果
这个坑几乎每个人都踩过。原因很简单:后端返回的content是Markdown源码,丢到Vue的{{ }}插值里自然纯文本显示。正确做法是使用mavon-editor的v-html渲染,或者用marked库转成HTML再展示。分享一个细节:预览时用编辑器组件的只读模式,可以让笔记详情页和编辑页的渲染效果保持一致,不用另写一套样式。
7.5 打包后Element Plus字体文件404
Vite打包时Element Plus的字体文件路径容易出问题,表现为图标不显示、控制台报woff2文件404。解决方法是修改vite.config.js里的base配置,或者使用CDN方式引入Element Plus相关CSS。我在项目里用了组件按需导入,配合unplugin-element-plus插件,这类问题基本不再出现。
9. 毕设答辩要点与后续扩展建议
最后聊几个答辩时很加分的小技巧。项目演示不要登录后直奔管理后台,而是按照"游客→注册→找书→写笔记→管理端审核"这条故事线走,把平台的完整业务闭环展示出来,比零散地演示功能效果好得多。被问到"这个项目你遇到最大的困难是什么",不要回答没有困难,挑一个具体的坑讲(比如跨域问题),说清楚你是什么时候发现的、排查思路是什么、最后怎么解决的,老师对这类回答印象非常好。
项目做完想继续提升的话,有几个方向可以考虑:搜索引擎用Elasticsearch替代MySQL的LIKE模糊查询,提升搜索性能;笔记内容增加Word/PDF导出功能,这只需要Java后端加一个docx模板渲染;用Redis存储点赞数和浏览量,降低数据库压力;把前台从"多页面跳转"升级为单页滚动加载加无限下拉。这些点单独拿出来每一个都可以作为你在答辩时说"对未来展望"的话题。
我自己做了几个版本之后整体的体会是:这类平台型项目最重要的不是功能堆砌,而是把"用户能正常写、正常读、正常互动"这条链路打磨得足够顺。你哪怕只做一套书籍和笔记的完整交互,做得精致,也比做了一堆半吊子功能强。起步阶段代码可以粗糙,但每个模块一定要亲手敲一遍,尤其是JWT鉴权和联表查询这两块,它们不是"背下来就能过",而是"写一遍才有感觉"。
最后再分享一个小建议:如果你打算把这个项目当作毕设,从现在开始就把每个表每个字段的作用写进文档,答辩时老师随便指一个字段,你都能说清楚它是干嘛的,这份从容感比任何花哨的代码都值钱。