SpringBoot+Vue前后端分离笔记分享系统:从设计到部署全解析 这段时间整理自己的知识库发现零散的笔记管理起来特别费劲。本地保存的Markdown文件越来越多想找一个能在线记录、分类整理、必要时一键分享给别人查看的轻量工具。市面上的云笔记产品确实不少但对于我这种喜欢把数据攥在自己手里又顺便想完整练一遍前后端分离项目的人来说自建一套始终是最合适的路。于是就有了这套基于SpringBoot、Vue、MyBatis、MySQL的前后端分离笔记记录分享网站系统。整套系统麻雀虽小五脏俱全用户可以注册登录在线创建和编辑笔记按标签和分类管理内容也可以把某篇笔记一键生成公开分享链接别人不需要登录就能浏览。后端提供标准RESTful接口前端用Vue做SPA页面最终可以打包部署到Nginx上跑。无论你是刚开始接触SpringBootVue的新手还是想找一个轻量自托管知识库的老手这篇文章里从需求拆解、数据库设计、核心代码思路到部署排坑的内容应该都能直接帮上忙。1. 笔记系统为什么要自己做需求分析与功能边界1.1 一个笔记记录分享网站的核心场景我最早的需求其实很简单写技术笔记记完之后想分门别类存好别人问起来的时候能直接甩过去一个链接查看。最开始用的是某个笔记软件的公开分享功能但用久了发现两个问题一是分享出去的页面随平台改造会失效二是内容被平台格式限制住想自定义样式和交互很麻烦。后来我梳理了一下自己的核心使用场景其实就三个。第一个人记录场景写笔记、改笔记、删笔记需要一个好用的编辑器最好支持Markdown格式第二分类检索场景笔记越来越多必须靠标签和标题搜索快速定位第三对外分享场景把某篇笔记设置为公开状态生成一个链接别人打开链接就能阅读不需要注册登录。最后这个场景是大多数自建笔记项目容易忽略的很多人只做了增删改查没把分享当成独立功能来设计结果后面想加权限控制还得重构表结构。我把这三个场景当成整个系统的地基。功能上不要一开始就贪多什么多人协同编辑、评论互动、点赞收藏都先放一放。先把记录、检索、分享这三点做到位整个系统的主干就清楚多了。1.2 功能清单与权限模型根据上面分析出的场景我把第一版的功能模块定成下面这张表功能模块具体说明实现层级用户注册登录用户名、密码注册登录后签发JWT后端前端笔记管理创建、编辑、删除、查看自己的笔记后端前端标签分类每篇笔记可打多个标签按标签筛选后端前端全文搜索按标题或内容关键词查询笔记后端公开分享笔记设公开并生成分享链接免登录访问后端前端访问统计公开链接的浏览计数后端权限模型是这套系统里最重要的设计决策。我采用的是最简单的两级权限私有和公开。私有指只有作者本人能看到和操作公开指任何拿到链接的人都能看。这里有一个细节需要想清楚公开分享不是说笔记就完全裸露在全网而是通过一个随机生成的share_token来控制访问相当于任何人知道这个特殊链接就能看这既方便传播又不至于让所有公开笔记都出现在默认列表里被爬虫随意抓取。在实际实现的时候权限判断要分成两层来做。操作层面对私有接口做JWT校验谁登录了才能操作谁的笔记访问层面用share_token做无状态校验只要笔记的可见性是公开状态携带token的请求就放行。这样的模型简单清晰也方便后续扩展比如以后想加仅好友可见或者指定邮箱可见在这些基础上扩展即可。2. 技术选型与整体架构SpringBootVueMyBatisMySQL的搭配逻辑2.1 后端为什么选择SpringBoot和MyBatis后端方案里SpringBoot几乎没什么悬念。它内嵌了Tomcat自动配置做得非常成熟能省掉大量繁琐的XML配置让我可以把精力放在业务代码上。尤其适合这种中小型管理系统一个Application类启动项目配合application.yml完成数据源、Redis、日志等配置开发效率很高。ORM框架我确实在MyBatis和Spring Data JPA之间纠结了一阵。JPA在简单CRUD场景确实省事继承JpaRepository就能用一堆现成方法但到了标签筛选、多表联查、分页统计这类场景时JPA的查询方法命名很快就不够用了。MyBatis的优势在于把SQL控制权完全交给开发者XML里写复杂查询非常直观也方便后期做细粒度的SQL优化。笔记这类业务本身就离不开联合查询比如按标签过滤当前用户的所有公开笔记用MyBatis的select标签配合动态条件就能稳稳拿捏。最终我选了MyBatis事实证明在调试复杂SQL的时候这种掌控感是非常舒服的。数据库用MySQL没什么可讲成熟稳定、运维资料多、部署成本低。笔记系统本质上就是普通的在线读写作既没有极端的高并发写入也不是海量图数据场景MySQL的InnoDB引擎足够支撑。等哪天真到了需要全文搜索、高并发负载的阶段再上Elasticsearch和分库分表也不迟但那是后话第一步把最常用的技术栈吃透更重要。2.2 前端为什么选择Vue前端的Vue几乎也是顺理成章的选择。Vue的响应式数据绑定机制非常适合表单操作密集的管理页面写一个笔记编辑页绑定数据、监听字段、提交表单整个流程清晰直观。配合Vue Router做前端路由Pinia做全局状态存储Axios做HTTP请求这套组合已经是国内非常主流的前端技术栈。你可能好奇为什么不用React。说实话React也很好但Vue在中文生态里的资料更丰富Element UI这类组件库对后台管理系统覆盖得特别全表格、表单、弹窗、分页组件直接拿来组装页面非常快。对一个记录笔记的网站系统来说界面不用炫酷语义清晰、操作顺手就合格了。Vue的单文件组件模式让HTML、CSS、JavaScript写在一个文件里维护起来也比较省心。2.3 前后端分离下的目录结构前后端分离意味着前端不再依赖后端渲染页面而是纯静态资源加接口调用。这套系统的目录结构大致如下note-system/ ├── backend/ │ └── src/main/ │ ├── java/com/x/note/ │ │ ├── config/ // 跨域、拦截器配置 │ │ ├── controller/ // 接口层 │ │ ├── service/ // 业务逻辑层 │ │ ├── mapper/ // MyBatis Mapper接口 │ │ ├── entity/ // 数据库实体 │ │ └── common/ // 统一结果返回、异常处理 │ └── resources/ │ ├── mapper/ // MyBatis XML文件 │ └── application.yml // 项目配置 └── frontend/ └── src/ ├── api/ // 接口请求封装 ├── router/ // 前端路由 ├── store/ // 全局状态 ├── views/ // 页面组件 └── components/ // 公共组件后端严格按Controller-Service-Mapper三层来分Controller只管接收参数和返回结果Service写业务规则Mapper只负责SQL执行。前端按API、路由、状态、页面四个维度拆开。这个结构的最大好处是可替换性强以后想换掉任何一个技术组件都只需要改对应层的代码其他部分基本不受影响。3. 数据库建模支撑笔记、标签、分享的核心表设计3.1 表结构总览与建表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 昵称, status tinyint NOT NULL DEFAULT 1 COMMENT 1正常 0禁用, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE note ( id bigint NOT NULL AUTO_INCREMENT, user_id bigint NOT NULL COMMENT 作者ID, title varchar(200) NOT NULL COMMENT 笔记标题, content longtext COMMENT 笔记内容, visibility tinyint NOT NULL DEFAULT 0 COMMENT 0私有 1公开, share_token varchar(64) DEFAULT NULL COMMENT 分享token, view_count int NOT NULL DEFAULT 0 COMMENT 浏览数, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_id (user_id), KEY idx_share_token (share_token) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE tag ( id bigint NOT NULL AUTO_INCREMENT, name varchar(50) NOT NULL COMMENT 标签名, PRIMARY KEY (id), UNIQUE KEY uk_tag_name (name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE note_tag ( id bigint NOT NULL AUTO_INCREMENT, note_id bigint NOT NULL, tag_id bigint NOT NULL, PRIMARY KEY (id), KEY idx_note_id (note_id), KEY idx_tag_id (tag_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;注意所有表统一使用utf8mb4字符集这样才能完整存储中文和Emoji符号笔记内容里混点特殊字符不会出问题。MySQL 5.7以上版本对datetime类型默认值支持得很好主键都用自增bigint避免未来数据量上来之后int不够用的尴尬。3.2 关键字段的设计意图代码里最容易忽略的是share_token这个字段。实现公开分享最笨的办法是在笔记上加一个is_public布尔值然后对外提供GET /note/detail/{id}接口但这会让私有笔记的ID也可被遍历非常危险。用了share_token之后公开链接携带的是一串64位随机字符串比如/share/8f3a2b1c9d4e...别人无法根据这个token反推出笔记真实ID私密性会好很多。visibility字段要单独拎出来说。很多人会把它设计成枚举字符串比如PRIVATE和PUBLIC。但在这个系统里我用tinyint的0和1来表示理由是后端判断条件更简洁存库空间也更小。如果你乐意也可以在前端映射成对应的文本显示这个不冲突。password字段我用varchar(100)而不是常见的varchar(50)原因是BCrypt加密后的密文长度通常是60个字符留足余量是必要的。另外我对user表做了唯一索引防止同名用户重复注册这个约束必须在数据库层兜底不能只靠后端代码判断。updated_at这个字段用的是ON UPDATE CURRENT_TIMESTAMP每次更新记录时MySQL会自动刷新成当前时间排序最近修改的笔记列表就非常方便。view_count是冗余统计字段这么做是为了避免每次浏览都要联表查计数表笔记系统的访问频次根本没必须单独建统计表。3.3 分页与列表查询的SQL思路笔记列表接口是整个后端的核心查询逻辑。第一版我用的MyBatis XML里写动态SQL按关键词、标签ID、可见性等条件做筛选并统一分页返回。select idselectNotePage resultTypecom.x.note.entity.NoteVO SELECT n.*, GROUP_CONCAT(t.name) AS tagNames FROM note n LEFT JOIN note_tag nt ON n.id nt.note_id LEFT JOIN tag t ON nt.tag_id t.id where if testuserId ! null AND n.user_id #{userId} /if if testkeyword ! null and keyword ! AND (n.title LIKE CONCAT(%, #{keyword}, %) OR n.content LIKE CONCAT(%, #{keyword}, %)) /if if testtagId ! null AND n.id IN ( SELECT note_id FROM note_tag WHERE tag_id #{tagId} ) /if if testvisibility ! null AND n.visibility #{visibility} /if /where GROUP BY n.id ORDER BY n.updated_at DESC LIMIT #{offset}, #{size} /select这段SQL里GROUP_CONCAT把笔记关联的所有标签名合并成一个字符串返回前端在列表页展示标签时候就不需要再发N个额外请求了。一开始没加的话就会出现经典的N1查询问题列表有20条笔记就要额外查20次标签表。至于关键词搜索第一版用LIKE已经足够应对几百篇笔记的规模没必要上来就做全文索引等真到了几千篇再考虑升级。4. 后端核心实现认证、笔记、分享三条主链路4.1 基于JWT的用户认证用户认证这块我选JWT而不是传统Session方案原因真的很实际前端是SPA应用接口是无状态RESTful风格Session会导致后端需要保存大量连接状态水平扩展时还得搞Session共享麻烦。JWT把用户信息加密放在token里后端只需要验签即可天然适合前后端分离环境。用户注册时用BCryptPasswordEncoder加密密码登录成功后签发JWTtoken里只放userId和username两个核心信息过期时间设置在7天。我在项目里实现了一个拦截器专门做token校验拦截所有除了/api/auth/login、/api/auth/register、/api/note/share/*之外的接口。Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { token token.substring(7); try { Claims claims JwtUtil.parseToken(token); // 把用户信息放入请求上下文 request.setAttribute(userId, claims.get(userId)); return true; } catch (Exception e) { // token无效或过期 } } response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\message\:\未登录或登录已过期\}); return false; } }如果你之前没接触过JWT可以把它理解成一张带签名和有效期的通行证。用户登录时后端根据用户ID生成一张通行证token回到前端后前端将通行证存到localStorage之后每次发请求都在请求头里带上它。后端拿到通行证先验一下签名是否有效、有没有过期有效就放行这样后端就不用记住每个登录用户的状态了。4.2 笔记增删改查的服务实现笔记相关的接口是设计的核心我列一下我最后实现的接口清单GET /api/note/page 分页查询自己的笔记 GET /api/note/detail/{id} 查询单篇笔记 POST /api/note/create 创建笔记 PUT /api/note/update/{id} 更新笔记 DELETE /api/note/delete/{id} 删除笔记 POST /api/note/publish/{id} 将笔记设为公开并生成分享链接 POST /api/note/unpublish/{id} 将笔记设为私有创建和更新接口看起来是简单的CRUD但有几个业务规则必须写在Service层。第一创建时visibility默认值是0也就是私有绝不能默认公开否则用户写一半的草稿就暴露了第二更新和删除时必须校验note.user_id和当前登录用户ID是否一致防止越权操作第三删除笔记时要同步删除note_tag关联记录不然会产生孤立数据。分享操作单独提一下。点击公开分享按钮后后端会生成一个64位随机token写进note表的share_token字段同时把visibility改为1。这里有个细节我想特别说一下不要在用户点击分享的时候用UUID直接当token。UUID虽然唯一但它是标准的36位格式别人拿到一个token后很容易联想到其他token的格式进而尝试遍历。用SecureRandom生成64位随机字符串会更稳妥暴力碰撞的概率几乎可以忽略。4.3 分享链接的生成与匿名访问控制公开分享接口是这套系统里唯一能匿名访问的接口实现时要格外仔细。我最终的实现思路是不暴露笔记ID只通过share_token查询笔记并且一定要校验visibility字段的值。GetMapping(/share/{token}) public ResultNoteVO getSharedNote(PathVariable String token) { Note note noteMapper.findByShareToken(token); if (note null || note.getVisibility() ! 1) { return Result.error(404, 笔记不存在或已下架); } // 浏览数1异步或同步更新均可 note.setViewCount(note.getViewCount() 1); noteMapper.updateViewCount(note.getId()); return Result.ok(NoteVO.from(note)); }为什么visibility ! 1时也要返回404而不是403这里其实是刻意为之。因为分享链接既然是一种特定链接公开访问的模式就不该告诉访问者该笔记存在但已经下架否则会泄露笔记曾经存在的信息。直接返回404语义上更明确也不会误导用户去猜ID。另外view_count的更新要放在查询成功之后如果token无效就不该计入访问量避免无意义的脏数据。5. 前端核心实现页面组织、接口对接与状态管理5.1 Vue项目结构与路由设计前端我用Vue 3配合Vite构建路由通过Vue Router管理。页面结构不像大型后台系统那么复杂主要就是登录注册页、笔记列表页、笔记详情页、编辑页和公开分享页。const routes [ { path: /login, component: Login }, { path: /register, component: Register }, { path: /, component: Home, meta: { requiresAuth: true } }, { path: /note/edit/:id?, component: NoteEdit, meta: { requiresAuth: true } }, { path: /note/detail/:id, component: NoteDetail, meta: { requiresAuth: true } }, { path: /share/:token, component: ShareView } ]路由设计上我最关注的是meta.requiresAuth这个标记。它配合全局前置守卫实现页面级别的访问控制未登录用户访问需要认证的页面时自动跳转到/login。公开分享页/share/:token放在路由表最外层不设置任何登录要求这样别人拿到链接就能直接打开。这里顺便吐槽一个容易踩的坑使用Vue Router的createWebHistory模式虽然好看URL里不带#但刷新页面时需要后端或Nginx配合兜底否则会404。后面部署章节我会专门讲怎么配Nginx的try_files解决这个问题。5.2 Axios请求封装与token注入前端每个页面都要调接口绝对不能在每个组件里裸写axios.get。我给Axios做了一层统一封装集中处理baseURL、请求超时、token注入和错误提示。import axios from axios import router from ../router const service axios.create({ baseURL: import.meta.env.VITE_API_BASE || /api, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) service.interceptors.response.use( res { const code res.data.code if (code 200) return res.data.data return Promise.reject(new Error(res.data.message || 请求失败)) }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) } return Promise.reject(error) } ) export default service这套封装的收益长期看非常明显。假如以后需要在每次请求时加个请求ID或者统一统计请求耗时只需要改这一个文件。401统一跳转到登录页的逻辑也集中在这里处理避免每个页面自己写一套重复的判断。5.3 笔记编辑页与分享页的落地细节笔记编辑页是整个前端交互最复杂的部分。我采用了双栏布局左边是标题输入框和标签选择器右边是内容编辑区。第一版没有引入太重的富文本编辑器直接用textarea配合Markdown语法编辑后续可以再替换成更友好的所见即所得编辑器。保存逻辑里有一个关键体验点新增和编辑共用一个路由/note/edit/:id?如果没有id参数说明是新建有id参数说明是编辑。页面加载时根据id是否存在决定调用创建接口还是更新接口。很多新手容易在这里分成两个页面导致一堆重复代码其实用一个组件完全能覆盖两种场景。公开分享页ShareView是系统对外的门面部署时一定要做两件事。第一如果笔记内容支持Markdown前端渲染前必须先做XSS消毒防止笔记里嵌入恶意脚本第二分享页要有明确的这是一个分享内容的视觉标识顶部展示笔记标题、作者昵称和发布时间。实际写的时候可以引入marked做Markdown渲染再加DOMPurify做标签过滤这两个库配合是业内常见方案。6. 本地开发到线上部署完整步骤与避坑记录6.1 开发环境的准备与初始化拿到源码后先别看功能先把环境校准好。后端需要的软件版本我试下来最稳的是JDK 8或11、Maven 3.6以上、MySQL 5.7或8.0前端需要Node.js 14以上建议用16或18的LTS版本npm源不顺畅的话记得先切换镜像。后端配置文件的数据库连接部分有几个必改项spring: datasource: url: jdbc:mysql://localhost:3306/note_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true jwt: secret: your-secret-key-change-in-production expire-days: 7serverTimezoneAsia/Shanghai这一行很重要。MySQL驱动8.0版本如果缺了时区配置会出现整8小时的时差因为驱动默认使用UTC。map-underscore-to-camel-case建议打开这样数据库的user_id字段能自动映射到Java的userId属性省去大量resultMap配置。6.2 后端打包与运行后端开发环境可以直接在IDE里启动Application类但部署到服务器时必须打成jar包。我用Maven来完成cd backend mvn clean package -DskipTests java -jar target/note-backend-1.0.0.jar --server.port8080如果项目里用了lombok打包时记得确认pom.xml里注解处理器已经配置好否则会报找不到getter/setter的编译错误。另外生产环境我一般会把JWT密钥、数据库密码等配置从application.yml挪到启动参数或者环境变量里而不是写死在jar包里这样即使部署多个环境也不用重新打包。6.3 前端构建与Nginx部署前端构建相对简单cd frontend npm install npm run build执行成功后会在dist目录生成纯静态文件。我把这些文件上传到服务器的/data/www/note-frontend/dist目录再用Nginx做静态资源服务和反向代理。Nginx配置的核心代码段如下server { listen 80; server_name your-domain.com; root /data/www/note-frontend/dist; index 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; } # 前端路由History模式兜底 location / { try_files $uri $uri/ /index.html; } }location /api/这一段是生产环境前后端通信的关键。前端页面本身部署在80端口但所有以/api/开头的请求都会被Nginx转发到后端8080端口。这样浏览器只有一个源完全消除了跨域问题要比在开发环境里配CORS干净得多。try_files $uri $uri/ /index.html则是配合Vue Router的History模式刷新页面时把匹配不到文件的请求全部交回index.html处理。6.4 部署过程中最容易踩的坑下面这些坑是我实际部署过程中花了不少时间才解决的每一条都值得你提前注意。第一个是前端每次npm run build之后必须清理浏览器缓存否则改了代码刷新后还是旧页面。如果用的是Nginx我建议在index.html里加上禁止缓存的响应头或者打包时给静态文件自动加上指纹比如app.ab3f2cd.js这种带内容哈希的文件名。第二个是跨域配置的重复问题。如果你的前端本地开发地址是http://localhost:5173后端接口是http://localhost:8080开发环境确实需要后端开CORS或者前端配Vite代理。但部署后如果Nginx已经把/api/转发到了8080后端就不要再配置跨域了不然会出现Nginx转发正常但响应头又带了一堆CORS配置的冗余情况。第三个是数据库初始化权限。如果直接使用MySQL的root账号跑线上应用一旦项目被注入或者泄露了配置攻击者就能直接控制整个数据库实例。我给生产环境单独建了一个账号只授予note_db库的增删改查权限CREATE USER note_applocalhost IDENTIFIED BY strong_password; GRANT SELECT, INSERT, UPDATE, DELETE ON note_db.* TO note_applocalhost; FLUSH PRIVILEGES;第四个是JWT密钥写死的问题。源码里默认密钥至少要用32位以上的随机字符串并保证每个环境都不一样。部署后可以通过环境变量覆盖java -jar note-backend.jar --jwt.secret$(openssl rand -base64 32)第五个是服务器防火墙和云平台的安全组很多新手在云服务器上配好了Nginx但浏览器访问不通最后发现是安全组没有放行80端口。这个检查起来很快但如果没想起来会白白排查半天。如果以后想在这个系统上加图片上传还要记得调整Nginx的client_max_body_size默认1MB的上传限制会让大图直接报413。我从做这套系统到现在最大的感受就是项目功能本身不难难的是把每一个细节从头到尾串起来从数据库表设计到接口返回格式再到前端联调和生产部署前后端分离项目考验的是对全链路的理解。如果你也想自己动手做一套类似的笔记网站我的建议是不要急着写代码先从需求边界和表结构入手把谁能看到什么内容这个问题彻底想清楚后面的开发流程会顺畅很多。最后再分享一个小技巧开发阶段启动前端时不要用相对路径/api去调接口最好在Vite的配置里设置一个代理转发到本地后端服务这样既不用每次都手动切后端的接口地址也不会被跨域问题干扰。等真正部署上线时再改成Nginx反向代理方案效果会更好。