☰
SpringBoot+Vue前后端分离笔记分享系统:从设计到部署全解析
2026/10/17 12:10:46 网站建设 项目流程

这段时间整理自己的知识库,发现零散的笔记管理起来特别费劲。本地保存的Markdown文件越来越多,想找一个能在线记录、分类整理、必要时一键分享给别人查看的轻量工具。市面上的云笔记产品确实不少,但对于我这种喜欢把数据攥在自己手里,又顺便想完整练一遍前后端分离项目的人来说,自建一套始终是最合适的路。于是就有了这套基于SpringBoot、Vue、MyBatis、MySQL的前后端分离笔记记录分享网站系统。

整套系统麻雀虽小五脏俱全:用户可以注册登录,在线创建和编辑笔记,按标签和分类管理内容,也可以把某篇笔记一键生成公开分享链接,别人不需要登录就能浏览。后端提供标准RESTful接口,前端用Vue做SPA页面,最终可以打包部署到Nginx上跑。无论你是刚开始接触SpringBoot+Vue的新手,还是想找一个轻量自托管知识库的老手,这篇文章里从需求拆解、数据库设计、核心代码思路到部署排坑的内容,应该都能直接帮上忙。

1. 笔记系统为什么要自己做:需求分析与功能边界

1.1 一个笔记记录分享网站的核心场景

我最早的需求其实很简单:写技术笔记,记完之后想分门别类存好,别人问起来的时候能直接甩过去一个链接查看。最开始用的是某个笔记软件的公开分享功能,但用久了发现两个问题,一是分享出去的页面随平台改造会失效,二是内容被平台格式限制住,想自定义样式和交互很麻烦。

后来我梳理了一下自己的核心使用场景,其实就三个。第一,个人记录场景,写笔记、改笔记、删笔记,需要一个好用的编辑器,最好支持Markdown格式;第二,分类检索场景,笔记越来越多,必须靠标签和标题搜索快速定位;第三,对外分享场景,把某篇笔记设置为公开状态,生成一个链接,别人打开链接就能阅读,不需要注册登录。最后这个场景是大多数自建笔记项目容易忽略的,很多人只做了增删改查,没把"分享"当成独立功能来设计,结果后面想加权限控制还得重构表结构。

我把这三个场景当成整个系统的地基。功能上不要一开始就贪多,什么多人协同编辑、评论互动、点赞收藏,都先放一放。先把记录、检索、分享这三点做到位,整个系统的主干就清楚多了。

1.2 功能清单与权限模型

根据上面分析出的场景,我把第一版的功能模块定成下面这张表:

功能模块具体说明实现层级
用户注册登录用户名、密码注册,登录后签发JWT后端+前端
笔记管理创建、编辑、删除、查看自己的笔记后端+前端
标签分类每篇笔记可打多个标签,按标签筛选后端+前端
全文搜索按标题或内容关键词查询笔记后端
公开分享笔记设公开并生成分享链接,免登录访问后端+前端
访问统计公开链接的浏览计数后端

权限模型是这套系统里最重要的设计决策。我采用的是最简单的两级权限:私有和公开。私有指只有作者本人能看到和操作;公开指任何拿到链接的人都能看。这里有一个细节需要想清楚,公开分享不是说笔记就完全裸露在全网,而是通过一个随机生成的share_token来控制访问,相当于"任何人知道这个特殊链接就能看",这既方便传播,又不至于让所有公开笔记都出现在默认列表里被爬虫随意抓取。

在实际实现的时候,权限判断要分成两层来做。操作层面对私有接口做JWT校验,谁登录了才能操作谁的笔记;访问层面用share_token做无状态校验,只要笔记的可见性是公开状态,携带token的请求就放行。这样的模型简单清晰,也方便后续扩展,比如以后想加"仅好友可见"或者"指定邮箱可见",在这些基础上扩展即可。

2. 技术选型与整体架构:SpringBoot+Vue+MyBatis+MySQL的搭配逻辑

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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `tag` ( `id` bigint NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL COMMENT '标签名', PRIMARY KEY (`id`), UNIQUE KEY `uk_tag_name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

注意所有表统一使用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 id="selectNotePage" resultType="com.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 test="userId != null"> AND n.user_id = #{userId} </if> <if test="keyword != null and keyword != ''"> AND (n.title LIKE CONCAT('%', #{keyword}, '%') OR n.content LIKE CONCAT('%', #{keyword}, '%')) </if> <if test="tagId != null"> AND n.id IN ( SELECT note_id FROM note_tag WHERE tag_id = #{tagId} ) </if> <if test="visibility != 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个额外请求了。一开始没加的话,就会出现经典的N+1查询问题,列表有20条笔记就要额外查20次标签表。至于关键词搜索,第一版用LIKE已经足够应对几百篇笔记的规模,没必要上来就做全文索引,等真到了几千篇再考虑升级。

4. 后端核心实现:认证、笔记、分享三条主链路

4.1 基于JWT的用户认证

用户认证这块,我选JWT而不是传统Session方案,原因真的很实际:前端是SPA应用,接口是无状态RESTful风格,Session会导致后端需要保存大量连接状态,水平扩展时还得搞Session共享,麻烦。JWT把用户信息加密放在token里,后端只需要验签即可,天然适合前后端分离环境。

用户注册时用BCryptPasswordEncoder加密密码,登录成功后签发JWT,token里只放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;charset=UTF-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 Result<NoteVO> 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?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false 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: 7

serverTimezone=Asia/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.port=8080

如果项目里用了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_app'@'localhost' IDENTIFIED BY 'strong_password'; GRANT SELECT, INSERT, UPDATE, DELETE ON note_db.* TO 'note_app'@'localhost'; 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反向代理方案,效果会更好。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询