ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于SpringBoot2+Vue3的学生读书笔记共享平台设计与实现

基于SpringBoot2+Vue3的学生读书笔记共享平台设计与实现 1. 学生读书笔记共享平台本质上是在解决什么问题先说个我常遇到的场景很多同学做毕业设计或者课程项目选来选去都会落到某某管理系统上学生管理、图书管理、商品管理换汤不换药。但学生读书笔记共享平台这个题目有意思的地方在于它挂着一个管理系统的壳内核却是一个带有社区属性的内容共享平台。如果你只是把它当作又一个增删改查项目来做那就浪费了这个题目本身的价值。这个平台要解决的核心问题可以拆成两半来看。第一半是读书笔记学生读完一本书需要有一个地方记录自己的思考、摘录、书评这是个人知识管理层面的需求。第二半是共享笔记不应该只躺在自己的数据库里其他同学可以浏览、点赞、收藏、评论从而形成一个基于阅读兴趣的交流社区。所以这个系统的核心不是笔记表的增删改查而是如何设计一套合理的权限与互动机制让笔记既能被安全地分享又能沉淀出社区氛围。我拿到这类项目时第一件事就是先想清楚角色划分。这个系统里至少有三类角色普通学生用户、游客未登录、管理员。普通用户能创建笔记、编辑自己的笔记、浏览他人笔记、互动点赞收藏评论游客只能看不能写也不能互动管理员负责用户管理和违规内容的处理。这种角色设计直接决定了后端接口的权限控制粒度也会影响前端路由守卫的写法。如果你一开始就把所有功能平铺开做到后面一定会被权限逻辑缠住。再说说这个项目的技术栈组合SpringBoot2 Vue3 MyBatis-Plus MySQL8.0。这套组合在当前的 Java Web 教学和毕设圈子里可以说非常典型它兼顾了后端的主流框架、前端的现代工程化方案、持久层的开发效率和数据库的新版本特性。下面我会从业务设计、数据模型、后端实现、前端工程、部署上线这几个维度把这套项目从0到1的完整思路过一遍也会把我实测中踩过的坑、补过的课都交代清楚。2. 技术栈选型为什么是 SpringBoot2 Vue3 MyBatis-Plus MySQL8.02.1 MySQL8.0 带来的不是版本号变化很多人在环境准备阶段习惯性装 MySQL5.7因为教程多、踩坑少。但在这个项目里我建议直接上 MySQL8.0原因有几点。第一认证插件变了。MySQL8.0 默认的认证插件是caching_sha2_password而 5.7 是mysql_native_password。这意味着如果你用旧版本的 JDBC 驱动连接 8.0会直接报Public Key Retrieval is not allowed或者认证失败。对应到 SpringBoot2 项目里你需要使用com.mysql.cj.jdbc.Driver8.0 系列的驱动类而不是老的com.mysql.jdbc.Driver同时连接串里要显式加上useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue这几个参数。第二utf8mb4 成为了默认字符集。这是什么概念如果用 MySQL5.7 创建库表时没指定字符集默认可能是 latin1存中文和 emoji 符号都会出问题。MySQL8.0 默认就是 utf8mb4省掉了建库时DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci这一堆手写配置的繁琐。第三窗口函数、CTE公共表表达式、JSON 类型的支持更完善。在这个项目里JSON 类型就很有用后面讲笔记标签和用户扩展信息的时候会提到。2.2 SpringBoot2 还是 SpringBoot3说实话2024 年之后 SpringBoot3 已经成了新项目的默认选择但回到这个项目本身SpringBoot2 依然是一个稳妥的、也是网上参考资料最丰富的版本。为什么这么讲SpringBoot3 基于 Jakarta EE 9包名从javax.*变成了jakarta.*很多老教程和老代码直接复制过来是编译不过的。另外 SpringBoot3 要求 JDK17而 SpringBoot2 可以跑在 JDK8 上。如果你的开发机器或者学校机房还在用 JDK8选 SpringBoot2 能省掉非常多环境兼容性的麻烦。更关键的是你现在要用的 MyBatis-Plus、Sa-Token 或 JJWT 这些库在 SpringBoot2 生态下的兼容版本非常成熟出了问题一搜就有答案没有人愿意在最基础的环境配置上卡一整天。SpringBoot2 这边的版本我建议选 2.7.x这是 2.x 系列的最后一个维护版本生命周期长同时兼容性也经过了大量项目验证。如果你的需求不是特别激进不要追新追到 2.5 或 2.6 的早期版本。2.3 MyBatis-Plus 和 Vue3 的组合价值MyBatis-Plus 解决的是 MyBatis 在单表 CRUD 上重复劳动过多的问题。比如我要对note表做分页查询、条件构造、逻辑删除用原生 MyBatis 需要手写 XML mapper 和 resultMap代码量不小。而 MyBatis-Plus 提供了BaseMapperT和IServiceT配合LambdaQueryWrapper大部分单表操作可以完全不用写 SQL。Vue3 的价值则在前端工程化。Vue3 的 Composition API 把组件的逻辑按功能聚合而不是按选项分散这在笔记详情页、编辑页这种逻辑复杂的页面里体验非常明显。配合 Vite 做开发服务器热更新速度远快于 Vue2 时代的 Webpack。我个人的体会是这四样技术放在一起核心思路就是让每个层级都去解决自己最擅长的问题。MySQL8.0 提供可靠的数据存储和高级查询能力SpringBoot2 提供稳定的后端基础和庞大的生态MyBatis-Plus 消灭重复的持久层代码Vue3 提供现代的前端开发体验。你不需要在某个点上炫技而是要让整体节奏流畅。3. 数据模型设计从业务反推读书笔记平台需要哪些表和关键字段3.1 核心表结构用户、图书、笔记、互动我设计数据模型时通常从一句话业务描述出发学生记录某本图书的读书笔记其他学生可以浏览并互动。这句话最少能拆出四个实体用户、图书、笔记、互动行为。用户表sys_user的字段如下字段名类型说明idbigint主键雪花算法生成usernamevarchar(50)登录账号唯一passwordvarchar(100)BCrypt 加密后的密码nicknamevarchar(50)昵称默认与用户名相同avatarvarchar(255)头像地址roletinyint角色1-用户 2-管理员statustinyint状态0-禁用 1-正常create_time / update_timedatetime创建和更新时间图书表book用于存与笔记关联的图书信息字段包括title、author、publisher、isbn、cover封面图 URL、summary等。这里有个容易被忽略的点笔记和图书的关系应该是多对一还是一对多一个学生可能给同一本书写多篇笔记比如初读笔记和重读笔记所以note表里加book_id外键关联到book表而不是把笔记内容直接塞进图书表。笔记表note是这个系统的核心字段设计要花心思。我列一下比较关键的字段id、user_id笔记作者book_id关联的图书title笔记标题content笔记正文用LONGTEXT类型content_type正文格式是纯文本还是 Markdown前端渲染方式不同is_public是否公开0 私密仅自己可见1 公开所有人可见view_count、like_count、favorite_count、comment_count统计数据topic_type笔记类型比如 读书摘抄、读后感、书评、思维导图等互动表可以拆成三张like_record点赞记录、favorite_record收藏记录、comment_record评论记录。为什么不直接在笔记表里加一个点赞数字段就完事因为你要展示我是否点过赞这是需要查询条件来判定的。只用计数无法判断当前用户是否已经互动过。所以互动记录表必须存在而笔记表里冗余的like_count只是优化展示效率的缓存字段。这种冗余计数 明细记录的双写方案在实际开发中非常常见。好处是列表页展示互动数时不需要去like_record表里 count(*)性能好代价是每次点赞都要同时更新明细表和计数逻辑上要稍微细心一点。3.2 标签系统简单方案用 JSON进阶方案用关联表读书笔记还有一个常见的需求给笔记打标签。比如文学历史方法论这种主题标签。最简单的做法是笔记表里加一个tags字段用 MySQL8.0 的 JSON 类型存一个数组比如[文学, 小说, 余华]。查询的时候可以用JSON_CONTAINS匹配也可以用LIKE %文学%模糊查。这种方案的优势是表结构极其简单写起来快适合毕设和中小并发场景。如果要做得严谨一点就拆成tag表和note_tag关联表多对多关系。这样做的好处是未来能统计哪个标签下笔记最多某个标签的访问量趋势而且能避免同名标签比如文学和 文学导致的脏数据。但代价是查询笔记列表时要么多表 join要么先查关联表再二次查询代码和 SQL 都会变复杂。我的建议是如果目标是快速交付并且想让系统看起来完整JSON 字段方案完全够用如果论文里有推荐算法或者数据统计分析这样的进阶章节那就老老实实拆关联表。另外用 JSON 方案时记得给字段定义默认值和索引策略否则数据量上去后查询会偏慢。3.3 逻辑删除与自动填充MyBatis-Plus 的两个基本功这个项目里删除笔记怎么办很多人第一反应是 DELETE 语句直接删行。但共享平台有一个现实问题笔记被删了之后它的点赞记录、评论记录怎么办如果全都物理删除一旦误删数据就彻底没了。实际项目里我习惯用逻辑删除note表加一个deleted字段默认 0删除时改成 1查询时自动过滤deleted0的记录。MyBatis-Plus 对此有内建支持只要在配置里设置logic-delete-field或者在实体字段上加TableLogic注解它就会自动把删除操作转成 UPDATE 而不是 DELETE。自动填充是另一个容易被新手忽略的功能。每张表都有create_time和update_time如果每写一条 SQL 都在代码里手动setCreateTime(new Date())会非常无聊且容易漏。MyBatis-Plus 提供了MetaObjectHandler接口我通常这样写Component public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, createTime, LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } }实体类字段加上TableField(fill FieldFill.INSERT)和TableField(fill FieldFill.INSERT_UPDATE)之后插入和更新的时候就会自动带上时间完全不需要手写。这套机制我在好几个项目里都用了稳定性没有问题。4. 后端实现细节SpringBoot2 MyBatis-Plus 的工程化落地4.1 三层结构下的模块划分与通用返回体后端项目我习惯按controller、service、mapper、entity、dto、vo、config、common、utils这些包来切分。对于这个项目还可以加一个interceptor包放登录拦截器一个exception包放全局异常处理。这里有一个每个项目都会遇到的设计决策接口返回什么结构我的做法是定义一个统一的ResultT包装类包含code、message、data三个字段。成功时code200失败时code500或者更细的业务码比如登录过期用401无权限用403。前端 axios 响应拦截器里统一判断 code非 200 时直接弹错误提示。有人会问为什么不直接用 HTTP 状态码能但不够细。业务层面你有用户名已存在验证码错误笔记不存在没有权限删除别人的笔记这种非常具体的错误这些如果用 HTTP 状态码表达只能笼统归到 400 或 500前端拿不到具体原因。用 Result 包一层前端 switch code 就能精准提示真实开发效率高很多。4.2 登录认证JWT 还是 Session读书笔记共享平台这种前后端分离项目登录态的维护我首推 JWT。为什么因为后端是无状态的认证信息全在 token 里前端每次请求在 header 里带Authorization: Bearer token后端配置拦截器解析验证即可不需要在服务端保存 session天然适合多实例部署。JWT 的实现流程不复杂但我建议至少做到以下几点登录接口校验用户名密码密码用 BCrypt 加密存储比对用BCryptPasswordEncoder.matches()生成 token 时把userId、role放进去过期时间设为 24 小时写一个JwtUtils工具类负责生成和解析 token注册拦截器放行登录、注册、首页笔记列表、笔记详情等公开接口其他接口全部校验 token拦截器里拿到 userId 后可以用ThreadLocal存起来方便后续在 service 里获取当前登录用户。这一步不做好后面每个接口都要手动从 token 里解析用户非常痛苦。代码大致是这样public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { token token.substring(7); try { Claims claims JwtUtils.parseToken(token); UserContext.setUserId(Long.valueOf(claims.get(userId).toString())); return true; } catch (Exception e) { // token 无效或过期 } } response.setStatus(401); return false; } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { UserContext.clear(); } }这里有个特别容易踩的坑拦截器里解析 token 失败后直接response.setStatus(401)然后返回 false前端只会收到一个空响应体。更好的做法是写一个 JSON 到响应流里把 Result 结构写进去让前端能统一处理。我当时在这上面卡了半天前端一直拿不到明确的错误信息排查了很久才发现是后端响应体是空的。4.3 笔记模块的一个完整闭环分页查询的两种玩法笔记的首页推荐和列表页核心是分页查询。MyBatis-Plus 的分页插件要先在配置类里注册Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }然后 Service 里写查询public PageNoteVO getPublicNotes(long current, long size, String keyword) { LambdaQueryWrapperNote wrapper new LambdaQueryWrapper(); wrapper.eq(Note::getIsPublic, 1) .eq(Note::getDeleted, 0) .and(StringUtils.hasText(keyword), w - w .like(Note::getTitle, keyword) .or() .like(Note::getContent, keyword)) .orderByDesc(Note::getCreateTime); PageNote page noteMapper.selectPage(new Page(current, size), wrapper); // 转换成 NoteVO补充 book 信息和 userName 信息 }这里有一个很关键的优化点分页查询查出来的是Note实体但页面要展示作者昵称、图书封面、图书标题而这些信息分散在别的表里。如果在循环里逐条去查 user 表和 book 表N 条笔记就会产生 2N 次额外查询数据少的时候无所谓数据量一大就等着被喷吧。正确做法是查出笔记列表后把其中的user_id集合和book_id集合一次性查出来组装成 Map再在内存里填充 VO。MyBatis-Plus 的listByIds正好适合这种场景。另外内层PageNoteVO的输出需要手动构建。MyBatis-Plus 的Page可以convert()方法把实体列表转换成 VO 列表但涉及到关联表补充信息时我通常是在 service 层手动组装逻辑更清晰。4.4 我在 MyBatis-Plus 上踩过的三个坑和 MyBatis-Plus 相关的坑我在这个项目里遇到过几个有代表性的。第一个是LambdaQueryWrapper 的 and/or 优先级陷阱。还是上面那段代码如果不包一层and(...)直接写.eq(Note::getIsPublic, 1).eq(Note::getDeleted, 0).like(Note::getTitle, keyword).or().like(Note::getContent, keyword)生成的 SQL 是is_public 1 AND deleted 0 AND title LIKE ? OR content LIKE ?。由于 AND 优先级高于 OR这条 SQL 会把content 匹配关键词但 is_public0 的私密笔记也查出来直接泄露权限数据。这个坑我在初期联调时踩实过新手写这个特别容易翻车。第二个是逻辑删除字段与唯一索引的冲突。如果用户表对 username 建了唯一索引逻辑删除用户后被删的 username 仍然占着索引位置导致新用户无法注册相同的用户名。解决办法一般是把deleted字段并入唯一索引或者删除前给 username 加个随机后缀。第三个是wrapper 里使用 select 只查部分字段时实体类里其它字段会被置空。如果某个字段在数据库有值但实体里是 null后续如果你又用这个实体去做更新操作比如updateByIdMyBatis-Plus 默认只更新非空字段所以不会出问题。但如果你把实体直接返回给前端前端拿到一堆 null 字段体验就不好了。所以查列表一定要明确要哪些字段或者使用 VO 来承接。4.5 点赞、收藏、评论的接口设计要点这三个互动模块结构高度相似我以点赞为例拆一下实现逻辑。点赞接口POST /api/note/{noteId}/like的核心逻辑是先查like_record表有没有当前用户对这篇笔记的记录。如果不存在插入一条点赞记录同时note表的like_count加一如果已存在删除记录like_count减一。这个设计叫原子切换前端拿到结果后直接切换点赞状态即可。这里的关键是并发问题。两个请求同时点赞可能都查到不存在记录然后都插入就会产生两条重复记录。解决方式有两种一是给like_record表建唯一索引字段是user_id note_id插入时用INSERT IGNORE或者捕获 DuplicateKeyException 来处理二是用 Redis 做计数和去重但这会增加复杂度对毕设和中小项目来说没必要。评论的逻辑比点赞复杂一点因为评论有嵌套结构。如果只支持一级评论直接在笔记下面评论那表结构非常简单。如果想支持楼中楼就得设计parent_id字段查询时先查一级评论再批量查子评论组装成树。我建议这个项目先做一级评论 评论点赞即可树形评论会牵扯到分页和排序的一系列设计问题工作量大不少。5. Vue3 前端的工程化实践与接口联调5.1 Vite 初始化与项目骨架Vue3 项目的初始化我用npm create vitelatest就能完成选择 Vue TypeScript 模板或者直接用 JavaScript 也可以。这个项目如果团队里所有人都熟悉 JS那用 JS 会更顺手如果前端要做维护建议 TS但 TS 会带来额外的类型编写成本。项目的目录结构建议按功能划分src/ api/ # 接口请求封装 assets/ # 静态资源 components/ # 通用组件 router/ # 路由配置 stores/ # Pinia 状态管理 views/ # 页面组件 utils/ # 工具函数 App.vue main.jsVite 的代理配置也是这个项目里的一个重点。开发环境下前端跑在 5173 端口后端跑在 8080 端口直接跨域。最简单的解决方案是在vite.config.js里配置 proxyexport default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })这样前端请求/api/note/list时会被转发到后端的http://localhost:8080/api/note/list浏览器里看不到跨域也就省去了在后端写CrossOrigin或者配置 CORS 过滤器的麻烦。但注意这个是开发环境的配置生产环境部署到 Nginx 后需要重新配置反向代理后面会讲。5.2 登录态与路由守卫让页面访问有规则前端的登录态管理我用的方式是 Pinia 维护一个userStore保存 token 和用户信息。因为 token 要跨页面持久化所以写入时同步存到 localStorage。路由守卫是前端权限控制的重点。项目里会有公开页面首页、笔记详情、需要登录的页面创建笔记、个人中心、管理员页面用户管理、笔记审核。路由配置里给每条路由加meta字段{ path: /note/create, component: () import(/views/note/NoteCreate.vue), meta: { requiresAuth: true } }, { path: /admin, component: () import(/views/admin/AdminHome.vue), meta: { requiresAuth: true, requiresAdmin: true } }然后在全局前置守卫里判断router.beforeEach((to, from, next) { const userStore useUserStore() if (to.meta.requiresAuth !userStore.token) { next({ path: /login, query: { redirect: to.fullPath } }) } else if (to.meta.requiresAdmin userStore.role ! 2) { next({ path: / }) } else { next() } })这种守卫的好处是路由层面的权限规则一目了然新增页面时只要在 meta 里声明即可不需要在组件内部写一堆判断。5.3 axios 封装与接口联调的经验axios 封装我做了三层实例层、请求拦截器层、响应拦截器层。实例层设置baseURL为/api和超时时间请求拦截器里统一加 token响应拦截器里统一处理错误码比如 401 跳转登录页非 200 弹错误消息。代码大致是这个感觉service.interceptors.response.use( (response) { const res response.data if (res.code ! 200) { ElMessage.error(res.message || 请求失败) if (res.code 401) { userStore.logout() router.push(/login) } return Promise.reject(new Error(res.message)) } return res }, (error) { ElMessage.error(error.message || 网络异常) return Promise.reject(error) } )联调阶段最容易出问题的反而不是接口本身而是前后端对字段名的约定不一致。后端的实体字段是createTime前端 JS 里习惯写create_time或者createTime一旦不一致数据就莫名奇妙地展示不出来。我的经验是在项目启动的第一天前后端就定好一份接口文档字段名统一用驼峰命名法。哪怕是简单写在一个共享的 Markdown 文档里都能帮后面省大量沟通成本。另外前端列表页还有一个常见的性能小问题笔记列表返回了完整的content长文本前端还要做摘要截断。处理方式有两个一是后端在 list 接口里不返回全量 content只返回一个summary字段比如从正文中截取前 100 个字符二是前端用 CSS 截断加text-overflow: ellipsis。我建议后端直接处理因为详情页还能再看全文列表页根本不需要完整的 content 数据传输体积能小很多。5.4 笔记编辑页Markdown 编辑器的选型与落地笔记正文用纯文本框太简陋用富文本编辑器又太重折中方案是 Markdown。前端组件我推荐md-editor-v3它对 Vue3 的支持非常到位中文文档齐全而且支持预览和编辑双栏同步滚动。引入方式很简单import { MdEditor } from md-editor-v3 import md-editor-v3/lib/style.css用于展示 Markdown 时用MdPreview组件。这里有个注意点从后端返回的 Markdown 文本在渲染时可能会包含 XSS 风险特别是用户输入里夹带script标签的情况。md-editor-v3默认有安全机制但我还是建议后端在存储前做一层过滤把明显的危险标签和属性去掉省得后面出幺蛾子。5.5 后台管理页面的表格与筛选后台管理页面通常会用到表格展示用户列表或笔记列表。Vue3 生态里 Element Plus 是绕不开的选择它的el-table、el-pagination、el-form组件能大大加速页面开发。管理页的筛选逻辑通常是这样顶部表单放若干筛选条件关键词、状态、时间范围点击查询按钮后把表单数据作为查询参数传给后端后端通过 wrapper 动态构造查询条件返回分页数据。这个模式非常通用个人中心的我的笔记列表也可以用同一套思路来实现。6. 数据库配置与部署阶段的关键操作6.1 MySQL8.0 在开发机上的安装与初始化配置开发环境我用的是 Docker 部署 MySQL8.0因为干净、卸载方便、不会污染系统环境。一条命令就能把服务跑起来docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD123456 \ -e MYSQL_DATABASEbook_note \ --restartalways \ -v /data/mysql:/var/lib/mysql \ mysql:8.0注意-v /data/mysql:/var/lib/mysql这个数据卷挂载如果容器哪天挂了重建数据还在不至于一夜回到解放前。初始化数据库时我建议直接用utf8mb4排序规则。建库脚本CREATE DATABASE IF NOT EXISTS book_note DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE book_note;6.2 SpringBoot 后端的配置文件与启动后端的application.yml是这个项目最核心的配置文件之一我贴一份实测可用的关键配置server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/book_note?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 123456 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0allowPublicKeyRetrievaltrue这个参数很多人容易漏掉。MySQL8.0 的caching_sha2_password认证在无 SSL 连接时需要先获取公钥来加密密码传输不加这个参数就会报Public Key Retrieval is not allowed。第一次遇到这个问题时我一脸懵排查了半天才发现是连接参数的问题。开发阶段把 MyBatis-Plus 的 SQL 日志打开StdOutImpl能直观看到每条 SQL 的执行情况排查问题效率高很多。但上线前记得关掉不然日志文件会被刷爆。6.3 生产部署前端 Nginx 后端 Jar 包本地开发跑通之后部署上线是另一个考验。一般生产环境的架构是前端打包成静态文件放到 Nginx后端打包成 jar 直接跑在服务器上Nginx 再把/api开头的请求反向代理到后端。前端的部署流程很简单npm run build生成的dist目录上传到服务器的 Nginx 站点目录Nginx 配置如下server { listen 80; server_name your-domain.com; root /var/www/book-note/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }两个非常关键的注意点。第一Vue3 路由如果使用 history 模式刷新页面时 Nginx 会找不到对应的静态文件所以必须配置try_files $uri $uri/ /index.html;把路由请求全部回退到 index.html。如果你不想处理这个就在前端路由里改用 hash 模式URL 会多一个#号但省心很多。第二proxy_pass http://127.0.0.1:8080;这里不要加尾斜杠。因为前端请求的路径是/api/note/list代理到后端时要保留完整的/api前缀这样后端 Controller 的RequestMapping(/api/note)才能正确匹配。如果写成proxy_pass http://127.0.0.1:8080/;路径会被替换成/note/list后端就 404 了。一个斜杠之差能让你排查一晚上。后端 jar 的启动也不难但要在生产环境跑得稳我建议用下面这个命令nohup java -jar book-note-server.jar \ --spring.profiles.activeprod \ --server.port8080 \ /var/log/book-note.log 21 nohup加是为了让进程在后台运行并且断开 SSH 后不退出。如果想做守护进程用systemd写一个 service 文件更规范但普通场景下 nohup 够用了。6.4 上线前必须检查的清单回到这个项目我每回部署完都会过一遍下面这个检查清单虽然琐碎但能挡住不少问题数据库连接串是否区分了开发和生产环境密码是否换了强密码前端请求的 baseURL 是否还是开发环境的 localhost生产环境应该走相对路径/apiCORS 策略是否收紧开发时你可能会在后端加CrossOrigin(origins *)上线前要删掉改成 Nginx 同源代理日志级别是否调整MyBatis-Plus SQL 日志不要再输出默认密码是否改掉管理员账号不要再用 admin/123456文件上传目录是否有权限头像、封面图上传后能否被 Nginx 直接访问数据库是否做了定时备份crontab 每天凌晨 dump 一次是最低保障这些问题里任何一个在测试阶段不炸到了上线后都会在某个深夜突然冒出来。7. 从能跑到像样这个项目还能怎么延伸很多同学拿着这种系统源码跑通之后就交差了。但从学习价值的角度看这个平台可以往上加的东西其实非常多而且每加一个都会让你的理解上一个台阶。第一个方向是全文检索。笔记内容是典型的非结构化文本数据等数据量到几万条以后MySQL 的LIKE %关键词%性能会明显下降。这时候可以引入 Elasticsearch 或者渺小的方案用 MySQL 的全文索引。你要是把这一步做了简历上就能写实现了笔记内容的关键词检索优化含金量完全不一样。第二个方向是内容推荐。整个平台天然有用户的阅读偏好数据点赞了哪些笔记、收藏了哪些笔记基于标签做简单的协同过滤推荐完全是可行的。不用搞得很复杂一个基于笔记标签的相似度计算就能出成果这个在论文里能占一整章。第三个方向是协作与社交。现在的设计还是一个人写笔记大家看的单向模式。你可以扩展成读书小组、共读计划、笔记评论区内测互评让用户之间产生更多互动关系。这个方向加进去后系统的复杂度和业务完整度都会上一个档次但核心的笔记-用户-互动表结构不需要大改扩展性强。我自己的体会是拿到一个现成的项目源码最好的使用方式不是跑起来看效果而是先读懂它的表结构和接口设计再动手改一个属于自己的功能点比如加一个新的笔记类型、增加一个导出 PDF 的接口。当你发现可以在别人的骨架上熟练地加肉时这套技术栈才算真正长在你身上了。这个项目用到的 SpringBoot2、Vue3、MyBatis-Plus、MySQL8.0 四个核心件每一件单拿出来都有大量文档但把它们组合在一起并跑通一个完整业务的过程才是最有价值的学习路径。你先照着我上面这套设计把项目搭起来跑通第一版再回过头来看哪些模块可以优化那时候你会比照着文档敲代码有更清晰的方向感。
返回列表