SpringBoot+Vue3笔记分享网站系统设计与实现
先说个背景。我帮人做了不少“课程设计级”的 Web 项目发现一个规律同样是笔记记录分享网站有人做得像博客后台点开全是页面堆砌有人做得像内容社区注册、发笔记、设公开、打标签、被搜索、被浏览一整套动作能闭环。差别不在页面多少而在于数据模型和权限边界有没有认真设计。这篇文章就是基于我用 Java SpringBoot Vue3 MyBatis MySQL 搭建笔记记录分享网站系统源码的全过程来写的前后端分离接口式联调涵盖核心模块设计、数据库建模、登录鉴权、笔记发布与分享、搜素检索以及部署阶段遇到的各种坑。想从零跑通一套完整 Web 项目尤其是准备做类似课题、需要自己动手写系统源码的朋友可以直接照着这份思路落地。笔记类系统最容易纠结的问题就是做了一个像是博客的东西又不太像博客所以没必要照搬博客的导航、评论、分类那套逻辑它是一个“用户自己生产内容、按意愿公开分享”的系统。读这篇文章之前你最好有点 Java 基础、了解 Vue 基本语法如果都不会建议先把基础语法过一遍再回来不然看代码细节会吃力。我下面讲的不是某个固定框架生成的模板代码而是从业务出发一步步推导出来的设计过程。1. 从需求到选型笔记分享站为什么不是个普通博客1.1 用户到底想要什么做系统之前最忌讳一上来就建表。我先整理了几个核心使用场景后面所有设计都围绕这些场景来用户注册登录后可以新建一篇笔记笔记支持富文本内容也可以只写纯文本。笔记创建时可选“公开”或“私有”公开后其他登录用户能浏览、搜索到。用户可以对笔记打标签、选分类方便自己管理和别人按标签找内容。未登录用户可以浏览公开笔记列表和公开笔记详情但需要登录后才能收藏、点赞。用户需要能通过一个分享链接把某篇笔记分享到外部场景对方打开链接直接能看到只读内容。这些场景决定了系统不能再简单套用“文章 评论”的博客模型。博客的数据关系往往是单作者的笔记系统则要求每一篇笔记都有明确的归属用户同时还要维护“公开/私有”两种可见性状态。权限判断不再是“管理员/普通用户”这种扁平的二元关系而变成了“所有人可读 / 仅自己可写 / 仅自己可见”的细粒度控制。1.2 技术选型为什么是 SpringBoot Vue3 MyBatis MySQL这套组合在目前的单体业务系统里属于“水桶型”配置每一项都不是最时髦的但组合起来非常稳。后端用 SpringBoot核心理由有三个第一它把 Tomcat 内嵌、依赖管理、自动配置都处理好了起步快不需要为了一个接口去折腾容器配置第二生态里做鉴权、文件上传、参数校验都有现成方案课程设计和中小型项目里基本不用重复造轮子第三Java 的强类型对团队协作很友好接口传参错误在编译期就能暴露一部分后期维护时看代码也能更快定位问题。如果换成 Node 或 PHP写起来虽然短但一旦业务逻辑复杂类型不安全的成本会逐渐显现。前端选 Vue3主要是组合式 API 更适合一个功能中等偏上、需要拆组件协作的系统。相比 Vue2 的选项式写法Vue3 用setup组织响应式变量、计算属性和监听器逻辑内聚性更强配合 Vite 的开发服务器热更新的响应速度也明显比 Webpack 时代舒服。组件化开发放这个项目里不是炫技笔记编辑器、标签选择器、卡片列表这些都会被多处复用拆成组件管理比堆在一个页面里好维护太多。持久层用 MyBatis是因为它保留 SQL 的灵活性。这个项目里有很多带条件的分页查询、多表关联查询、权限过滤查询用 MyBatis 写 XML 时可以通过where、if、foreach动态拼筛条件SQL 优化也直观可见。对比 JPAMyBatis 的学习成本更低对 SQL 不熟练的人也更容易理解“我查的到底是哪几张表、哪个字段”。MySQL 则是关系型数据存储的稳定选择ACID 特性、事务能力、索引支持都够用部署成本也低。1.3 整体运行链路系统采用前后端分离部署但开发时可先用联调模式跑。前端通过 Axios 请求后端接口后端暴露http://localhost:8080/api/**的 RESTful 接口用户请求先打前端页面页面再去调接口拿数据接口收到请求后先经拦截器或过滤器校验 Token再进入控制器层控制器调用服务层服务层通过 MyBatis Mapper 访问 MySQL。这个链路里权限校验不是只在拦截器做一次而是在涉及笔记归属、私有笔记访问等具体业务里各查一次宁可多查一次数据库也不在没有数据支撑的情况下做信任判断。我把后端和前端分成两个独立目录backend放 SpringBoot 工程frontend放 Vue3 工程。这样目录清晰两个人同时开发也不会互相踩。后面所有模块拆解都按这个结构来讲。2. 数据模型与表结构设计从 user_id 开始就把路铺好2.1 核心表清单这个系统一期核心表我设计了六张不算太多但每张表的关系需要想清楚用户表、笔记表、分类表、标签表、笔记标签关联表、用户收藏表。如果后面要加点赞、评论可以再扩展但项目起步阶段不要贪多先把主链路跑通。表结构设计最重要的一条经验是把“谁的数据”放在每一张业务表的第一个外键位置。用户表用自增id做主键笔记表必须带user_id从设计上保证每篇笔记都能追溯到作者。这样后面做权限判断、统计个人笔记数、生成个人主页都很方便。表名作用核心字段users用户账号信息id、username、password、avatar、nickname、create_timenotes笔记主体id、user_id、category_id、title、content、summary、cover_image、status、share_code、view_count、like_count、create_time、update_time、deletedcategories笔记分类id、name、sort_ordertags标签字典id、namenote_tags笔记与标签关联id、note_id、tag_idfavorites用户收藏id、user_id、note_id、create_time2.2 建表语句要点笔记表是最核心的一张表我把建表语句要点列出来字段命名全部用下划线风格和 Java 实体类里的驼峰命名在 MyBatis 配置里做自动映射。CREATE TABLE notes ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 笔记ID, user_id BIGINT NOT NULL COMMENT 作者用户ID, category_id BIGINT DEFAULT NULL COMMENT 分类ID, title VARCHAR(200) NOT NULL COMMENT 笔记标题, content LONGTEXT COMMENT 笔记内容存富文本或Markdown原文, summary VARCHAR(500) DEFAULT NULL COMMENT 摘要列表页展示用, cover_image VARCHAR(500) DEFAULT NULL COMMENT 封面图URL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0私有 1公开, share_code VARCHAR(32) DEFAULT NULL COMMENT 公开分享码, view_count INT DEFAULT 0 COMMENT 浏览数, like_count INT DEFAULT 0 COMMENT 点赞数, deleted TINYINT DEFAULT 0 COMMENT 软删除标记, create_time DATETIME NOT NULL, update_time DATETIME NOT NULL, KEY idx_user_status (user_id, status), KEY idx_share_code (share_code), KEY idx_create_time (create_time), KEY idx_category (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字段类型有几点容易踩坑。content用LONGTEXT不要用VARCHAR因为笔记内容可能很长VARCHAR在 MySQL 里最长是 65535 字节换算成中文字符没多少很容易被截断用TEXT也可以但富文本带图片的话LONGTEXT更保险。status用TINYINT不用布尔值因为后续可能扩展“草稿、审核、封禁”等多种状态布尔类型不好扩展。deleted是软删除标记业务层查询时统一带deleted 0物理删除只留给管理员手动处理。索引设计上idx_user_status这个复合索引特别关键因为“某个用户的笔记列表”是这个系统最频繁的查询场景如果只建单列索引WHERE 条件同时过滤 user_id 和 status 时性能会下降。愿意更极致一点的话可以再加一个(user_id, status, create_time)的索引让排序也走索引但数据量不大的情况下我不建议堆太多索引写操作性能会受拖累。2.3 几个设计决策的说明第一个决策分类和标签为什么要分开分类是单选、层级简单、数量少适合在左侧导航里做固定入口标签是多对多、自由创建、数量多适合给每篇笔记打多个标签。如果只留一个分类笔记覆盖主题有限如果只用标签导航结构就松散。两个一起用相当于既有“目录”又有“关键词索引”。第二个决策为什么笔记表里放summary摘要字段很多初学项目会直接在列表页干掉content或者用substring截取这样既不高效也不优雅。更好的做法是用户在保存笔记时自动截取前 200 个纯文本字符存入summary列表页只查小结字段。这样查询体积小、渲染快也避免了把大段 HTML 传给前端导致的布局崩坏。第三个决策分享码为什么要单独建字段而不是直接用笔记 ID直接暴露笔记 ID 的分享链接很容易被遍历别人通过修改 URL 里的数字就能看到你没打算公开的笔记。虽然我们还有权限校验但从工程习惯上就不该暴露内部自增 ID。我用的方案是生成一个 32 位随机字符串存到share_code查询时只看分享码内部 ID 只用于编辑、删除等操作。3. 后端核心实现登录鉴权与笔记管理的接口实践3.1 注册登录与 Token 机制后端接口设计围绕两个身份状态未登录的游客和已登录的用户。登录鉴权我选择基于 Token 的方式没有引入 Spring Security 全家桶因为这套系统需要的是轻量拦截不是复杂权限模型。核心流程注册时密码用 BCrypt 加密后入库登录时校验密码成功后生成 Token 返回前端前端后续请求在 Header 携带 Token后端用拦截器解析 Token 并把当前用户 ID 放入请求上下文。BCrypt 不是普通哈希它会自动加盐每次加密结果都不同数据库中即便两个用户密码相同存储的密文也不一样这能有效抵抗彩虹表攻击。使用 Spring Security 里的BCryptPasswordEncoder可以单独引入不需要启用整个过滤器链。Token 我用的是 JWT结构上分成 Header、Payload、Signature 三段Payload 中只放userId和expireTime不放密码等敏感信息。生成 Token 的简化逻辑如下public String generateToken(Long userId, long expireMillis) { long now System.currentTimeMillis(); Date nowDate new Date(now); Date expireDate new Date(now expireMillis); return Jwts.builder() .claim(userId, userId) .setIssuedAt(nowDate) .setExpiration(expireDate) .signWith(SignatureAlgorithm.HS256, secretKey) .compact(); }为什么用 JWT 而不是传统的 Session因为前后端分离后后端接口可能同时服务于 Web、小程序、移动端等多种客户端Session 依赖 Cookie 和服务端存储跨端时要处理会话同步问题。JWT 是无状态的后端只有验签逻辑天然适合接口场景。但注意 JWT 有个明显的缺点无法主动失效。用户修改密码或者被封禁后旧 Token 在过期前仍然有效。对这个项目影响不大但如果你要上线生产建议把 Token 版本号也放到数据库里校验时对比一下。3.2 笔记列表和详情接口的权限过滤笔记列表是系统里访问最频繁的接口之一我没有简单写一个“查询所有笔记”的 SQL而是根据场景拆成了三个方法公开笔记列表、我的笔记列表、某个用户的公开笔记列表。这三个方法业务含义不同但都能复用同一个动态 SQL 模板。公开笔记列表的查询条件应该是status 1 AND deleted 0如果要支持分类筛选再加category_id #{categoryId}。这里最关键的 SQL 技巧是条件判断放在 XML 中动态拼接不能在 Java 服务层人为判断后再拼字符串否则容易产生 SQL 注入风险。select idselectPublicNotePage resultTypeNoteVO SELECT id, title, summary, cover_image, view_count, like_count, status, create_time, update_time FROM notes where if testcategoryId ! null AND category_id #{categoryId} /if if testkeyword ! null and keyword ! AND (title LIKE CONCAT(%, #{keyword}, %) OR summary LIKE CONCAT(%, #{keyword}, %)) /if AND status 1 AND deleted 0 /where ORDER BY create_time DESC /select这种设计有几个隐藏好处列表接口不返回content字段节省了网络传输动态 SQL 避免了写死 SQL 导致的多余查询权限过滤直接写在 SQL 里任何人即使绕过 Java 层也很难拿到非公开数据。详情接口则更严格我没法只用id去查笔记因为私有笔记只有作者本人和自己可见游客或别的用户不应该能访问。我把详情查询做成了带“当前用户 ID”的过程查询时同时校验status 1或user_id #{currentUserId}再过滤deleted 0。这个权限判断必须在数据库查询里完成不能在 Java 代码里查出结果后判断因为查询失败和查到数据后拒绝返回是两种不同的行为后者会白白暴露数据是否存在。3.3 编辑、删除与越权防护编辑接口比详情接口更敏感。前端传noteId和新的标题、内容后端首先要做“归属校验”“这篇笔记是否属于当前登录用户”。我写了一个通用的校验服务传入noteId和userId返回一个布尔值public boolean isNoteOwner(Long noteId, Long userId) { Note note noteMapper.selectById(noteId); return note ! null note.getUserId().equals(userId); }更新时应该只更新允许修改的字段比如title、summary、content、category_id、status。万万不能直接把前端传的对象全字段 update否则用户可以顺带把view_count、like_count、create_time等字段一起改了。我用的方案是定义独立的更新 DTO只接收需要修改的字段Mapper 的 XML 里用if判断字段是否为空再决定是否更新这样也解决了“部分字段不想改成空值”的问题。删除接口我统一做软删除即把deleted字段置为 1不真正删行。对内容型系统来说误删后想要恢复会非常麻烦而且笔记可能被其他人收藏过物理删除会导致收藏记录变成脏数据。软删除的好处是保留历史恢复也容易缺点是每一条查询都要记得带上deleted 0条件否则会把“已删除”的数据查出来。4. Vue3 前端落地编辑器、拦截器与笔记状态管理4.1 前端项目结构与基础配置前端使用 Vue3 的组合式 API配合 Vite 工程化搭建。项目结构分成几个清晰的目录api按资源模块放接口请求函数views放页面组件router放路由配置stores放 Pinia 状态管理components放公共组件。路由按功能拆分成三类不需要登录的公开页面如登录页、注册页、公开笔记详情页需要登录的页面如个人笔记列表、新建笔记页、编辑笔记页可以公开访问但登录后表现不同的页面比如首页公开笔记列表。const routes [ { path: /login, component: LoginPage }, { path: /register, component: RegisterPage }, { path: /, component: HomePage }, { path: /note/:id, component: NoteDetailPage }, { path: /note/create, component: NoteEditPage, meta: { requiresAuth: true } }, { path: /note/:id/edit, component: NoteEditPage, meta: { requiresAuth: true } }, { path: /my/notes, component: MyNotesPage, meta: { requiresAuth: true } } ]4.2 编辑器选型与内容处理笔记系统的核心交互是编辑内容所以编辑器选型直接影响体验。我将需求拆成两层普通用户编辑笔记时希望像写 Word 一样有工具栏点击加粗、插入图片开发人员存储时希望内容是可序列化的 HTML前端详情页直接用富文本渲染不需要额外解析。基于这个需求我推荐使用所见即所得的富文本编辑器而不是 Markdown 编辑器。Markdown 对技术人友好但目标用户如果包含非技术背景的人学习成本就高了偏远一点说如果你的笔记里要贴表格、截图、流程图富文本的上手难度远远低于 Markdown。编辑器的接入要注意防 XSS。用户提交的富文本内容可能包含script标签或者onerror事件前端在提交前可以用白名单过滤后端在接收时最好再做一次校验或转义。更严格的做法是后端使用一个 HTML 清洗工具只允许保留p、strong、img、h1等安全标签别的都过滤掉。这一步千万不要省否则用户 A 可以在笔记里写段恶意脚本用户 B 打开详情页时就会被攻击。4.3 Axios 拦截器与路由守卫前后端分离后前端每个接口请求都要自动携带 Token后端返回 401 时前端要统一处理跳转登录。我用一个 Axios 实例统一封装请求逻辑const service axios.create({ baseURL: /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( response response.data, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) } return Promise.reject(error) } )这里的拦截器把“每个请求都加 Token”和“401 自动处理”集中到一层避免每个页面重复写判断逻辑。路由守卫负责页面级的跳转控制进入需要登录的页面时检查是否存了 Tokenrouter.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) next(/login) else next() })注意前端路由守卫只是门面真正的鉴权必须靠后端接口校验。前端防君子不防小人有时候用户直接手动改 localStorage守卫形同虚设但后端接口如果做了 Token 和归属校验数据安全才能兜住底。4.4 笔记状态管理笔记的“公开/私有”切换是一个状态变化我用 Pinia 保存用户全局信息登录用户的 ID、昵称、头像。编辑器页面里笔记的编辑状态是局部的不需要全局状态管理用组件内部的ref即可。Pinia 在这里的核心作用是避免登录后刷新页面丢失用户信息刷新时从本地缓存恢复并请求一次/api/user/info接口拉取最新用户数据。开发时踩过一个细节坑从用户信息接口返回的字段也可能是null比如新用户还没设置昵称。前端处理时要给默认值否则模板渲染时会出现空白或者报错。我习惯在 Pinia 的 getter 里做空值兜底保证任何页面拿到的都是可用字符串。5. 分享与检索链路让笔记被看到也要防止被滥看5.1 公开状态、分享码与只读页面分享功能是这个系统区别一般笔记工具的地方。用户在编辑页点“保存并公开”时后端如果发现笔记当前状态不是公开就生成一个share_code并写入。分享链接的格式是/share/{shareCode}前端路由里对应一个只读页面只展示笔记内容不显示编辑按钮也不加载编辑器的资源文件干净轻量。生成分享码的代码很简单但要注意唯一性public String generateShareCode() { String code UUID.randomUUID().toString().replace(-, ); return code.substring(0, 16); }小规模场景用 UUID 截断就够用。上线后如果不放心唯一性可以查一次表确认没有重复再插入或更新。分享码不要用纯数字纯数字的短链接太容易被遍历混合大小写和数字的 16 位字符串相对安全得多。只读页面通过GET /api/notes/share/{shareCode}获取笔记接口内部逻辑是按share_code查询笔记再判断status 1 AND deleted 0条件不满足就返回 404而不是返回 200 然后给空数据。这里语义上要“假装不存在”避免泄露笔记存在状态。5.2 标签、分类与关键字搜索的组合查询分类筛选、标签筛选、关键字搜索这三个功能组合在一起时很容易写出性能极差的 SQL。我的做法是在列表接口中接收一个条件对象categoryId、tagId、keyword、page、size。如果按标签筛选就不能只查 notes 表需要关联note_tags和tags表。先查tag_id对应的关联记录再只返回满足条件的笔记 ID。这个操作在数据量不大时可以放在一条子查询里WHERE id IN ( SELECT note_id FROM note_tags WHERE tag_id #{tagId} )这种写法比JOIN在大表场景下更容易走索引语义也更直观。关键字搜索我用LIKE CONCAT(%, #{keyword}, %)先跑通数据量超过十万条后建议升级为 MySQL 全文索引。全文索引对于中文分词有额外要求默认分词器效果一般如果真要考虑中文分词可能还得引入专门的检索引擎但那是后话项目初期不建议为性能过度设计。5.3 浏览量、点赞数与收藏的实现细节这三个功能看着简单实现起来也有坑。浏览量的真实场景是“同一用户短时间内反复刷新不能反复加 1”。我做的方案是前端拿到笔记 ID 后在详情页加载时调用一个独立的浏览量接口后端用 Redis 做去重不过这个项目为了不增加环境依赖我用简单的方式同一个会话一次增加一次浏览量通过一个带时间戳的 Map 做短暂去重保证同一用户五分钟内不会重复计数。点赞和收藏则必须依赖用户登录状态。收藏表favorites要建一个唯一索引(user_id, note_id)防止用户重复收藏。点赞我不单独建表直接对笔记表的like_count做UPDATE notes SET like_count like_count 1 WHERE id #{noteId}。为什么不先查后写因为并发时先查后写会丢更新直接走原子更新更稳。如果要支持取消收藏、取消点赞就需要记录用户行为单独建表更清晰。收藏记录在个人中心里要能显示笔记列表删除笔记时收藏记录也要一并清理。6. 环境配置、联调与部署踩坑记录6.1 跨域的三种处理方式前后端分离后开发环境下前端跑在 Vite 的 5173 端口后端跑在 8080 端口跨域问题是跑不掉的。解决方式有三种后端CrossOrigin注解、后端全局 CORS 配置、前端 Vite 代理。生产环境则用 Nginx 反向代理。开发阶段我推荐使用 Vite 代理因为它在浏览器层表现为同源请求后端不需要额外处理跨域逻辑server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }后端如果同时也要允许某些测试工具直接调用可以配置一个全局 CorsFilter但注意不要把允许来源直接设成*否则携带 Token 的请求会被浏览器拦掉。使用代理解方案最干净后端只需要专注业务接口。6.2 MyBatis 的常见坑位第一个坑是下划线字段到驼峰属性映射。默认情况下 MyBatis 不会把user_id自动匹配为userId必须在application.yml里开启mybatis: configuration: map-underscore-to-camel-case: true不开这个配置你会发现查询结果里的userId永远是 null而user_id又是 Java 实体类里不存在的属性名纠结半天查不到原因。第二个坑是更新语句只更新部分字段。如果在 XML 里写死UPDATE notes SET title #{title}, content #{content}, summary #{summary} WHERE id #{id}那么前端只传了标题时content 和 summary 就会被置空。必须用set加if动态拼接update idupdateSelective UPDATE notes set if testtitle ! nulltitle #{title},/if if testcontent ! nullcontent #{content},/if if testsummary ! nullsummary #{summary},/if update_time now() /set WHERE id #{id} AND user_id #{userId} /update第三个坑是插入后拿不到主键。默认情况下 MyBatisinsert返回的是影响行数不是主键。如果需要插入后立刻操作这条数据要在insert标签里配置useGeneratedKeystrue keyPropertyid。笔记打完标签后需要把标签关联记录插到note_tags表这个场景就必须拿到新笔记的id。6.3 数据库时区与连接串配置数据库连接串一定要显式指定时区否则不同环境里时间可能差八小时jdbc:mysql://localhost:3306/note_share?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue别小看这个配置。我曾经在一台服务器上部署后所有笔记的create_time都显示比当前时间晚八小时排查了很久才发现是数据库连接串没指定时区MySQL 默认按系统时区取UTC 时间和本地时间差了整整一个作息周期。6.4 打包部署完整步骤后端打包mvn clean package -DskipTests java -jar target/note-share-server.jar --spring.profiles.activeprod前端打包npm run build # 生成 dist 目录生产环境我用 Nginx 托管前端静态文件并把/api路径反向代理到后端服务server { listen 80; server_name your-domain.com; root /var/www/note-share/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; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location / { try_files $uri $uri/ /index.html; } }这里的try_files $uri $uri/ /index.html是必须的。Vue 是单页应用路由跳转后如果直接刷新页面Nginx 会去磁盘找/note/1这个路径找不到就会返回 404加了try_files回退到index.html前端路由器接管后就能正确展示页面。6.5 上线后的三个隐藏问题第一个是上传图片大小限制。Spring Boot 默认单个文件上传大小只有 1MB笔记里插入大图很容易直接报错。要在配置里放开限制spring.servlet.multipart.max-file-size10MB spring.servlet.multipart.max-request-size20MB同时 Nginx 里面也检查一下client_max_body_size默认值是 1m同样会拦截大文件。第二个是富文本里的图片路径问题。编辑器上传图片时如果返回的是相对路径比如/upload/2025/xx.png本地开发因为代理没问题但生产环境如果前端和后端不在同一个域名下图片就会 404。正确处理是返回完整 URL 或者通过 Nginx 把/upload路径也反向代理到后端静态资源目录。第三个是数据库连接池耗尽。默认 HikariCP 连接池最大连接数是 10如果接口里有个别慢查询连接池很快会被占满。上线前把最大连接数调整到 30同时给数据库加max_connections配置避免并发稍大时接口全部超时。最后再分享一个部署脚本的小技巧我每次发布这个系统时会写一个简单的部署脚本把后端打包、前端打包、拷贝静态文件、重启服务串起来执行。脚本本身不复杂但能省掉大量手工操作cd backend mvn clean package -DskipTests cd ../frontend npm run build rm -rf /var/www/note-share/dist cp -r dist /var/www/note-share/ systemctl restart noteserver过程中最容易忘的是清理旧的dist目录。如果不删旧的 JS 文件名和新的文件名混在一起浏览器缓存会导致线上出现“改了代码但页面没变化”的假象。生产上线前最好再强制刷新一次 CDN 或者浏览器缓存。很多人在做完之后觉得“这不就是一个增删改查系统吗”但真正动手做过一遍就知道难点不在框架用法而在边界条件的覆盖私有笔记能不能被搜到、被删除的笔记能不能通过分享链接访问、别人能不能改你的笔记、Token 过期后用户是否还能停留在页面上。把这些边界想清楚你的笔记记录分享网站才真正可交付。如果这篇文章对你有点帮助可以按这个结构自己动手复刻一遍跑通之后再按自己的想法加功能比如评论、关注、数据导出系统的扩展空间很大框架选型也不会白费。

相关新闻

folium地图资源本地化指南:彻底解决离线/内网JS/CSS加载失败

folium地图资源本地化指南:彻底解决离线/内网JS/CSS加载失败

简介:面向使用地图可视化组件、希望摆脱远程加载缓慢问题的开发者,这份打包内容包含前端依赖的完整静态文件,包括核心地图库、交互脚本、界面框架、图表字体等资源,以及配套的样式源文件和地图标记图片,共77个文件&…

2026/10/18 1:22:43 阅读更多 →
EmulatorJS 版本选择指南:Stable、Latest、Nightly 三个渠道怎么选对

EmulatorJS 版本选择指南:Stable、Latest、Nightly 三个渠道怎么选对

EmulatorJS 版本选择指南:Stable、Latest、Nightly 三个渠道怎么选对 【免费下载链接】EmulatorJS A web-based frontend for RetroArch 项目地址: https://gitcode.com/GitHub_Trending/em/EmulatorJS 选 EmulatorJS 版本(一个基于网页的 RetroA…

2026/10/18 1:22:43 阅读更多 →
AI接管终端:架构师为何把命令行当成主战场?

AI接管终端:架构师为何把命令行当成主战场?

1. 一个反直觉的现象:越资深,越把终端当主战场1.1 我观察到的"倒挂"最近一段时间,AI编辑器几乎是所有技术社区里最热的话题。智能补全、对话式生成、自动重构,听起来每一招都能把生产力拉满。可我在观察周围团队之后发现…

2026/10/18 1:22:43 阅读更多 →

最新新闻

NPU/TPU硬件加速本质:软硬协同的决策框架与落地实践

NPU/TPU硬件加速本质:软硬协同的决策框架与落地实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 2:13:56 阅读更多 →
20倍光学变焦迷你机芯CM8210AP:小体积如何兼顾远距离高清观测

20倍光学变焦迷你机芯CM8210AP:小体积如何兼顾远距离高清观测

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 2:13:56 阅读更多 →
智能穿戴APP开发服务商怎么选?四个维度评估硬件适配能力

智能穿戴APP开发服务商怎么选?四个维度评估硬件适配能力

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 2:13:56 阅读更多 →
MT5风控插件开发实战:架构、性能优化与稳定性部署

MT5风控插件开发实战:架构、性能优化与稳定性部署

1. 风控插件的定位与整体设计思路1.1 为什么风控插件是MT5搭建后的第一优先级MT5服务器部署完成、VPS稳了、交易终端连上了,这只能算完成了1%。真正决定这套平台能不能长期跑下去的关键,是风控插件。我见过太多团队在搭建阶段兴奋得不行,觉得…

2026/10/18 2:13:56 阅读更多 →
RH850 GHS编译链路全解析:从.c到.elf的工程实践

RH850 GHS编译链路全解析:从.c到.elf的工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 2:13:56 阅读更多 →
2026年AI接口聚合服务深度横评:从调用稳定性看词元之河与硅基流动的中小团队适配度

2026年AI接口聚合服务深度横评:从调用稳定性看词元之河与硅基流动的中小团队适配度

2026年5月完成的一轮 AI 接口聚合服务实测横评,方法论为 72 小时连续压测、12 类典型调用场景模拟与 37 位一线开发者访谈。行业背景值得关注:据《中国AI基础设施白皮书(2026Q1)》,超过 68% 的中小技术团队在大模型应用…

2026/10/18 2:12:56 阅读更多 →

日新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/18 0:00:55 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/18 0:00:55 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 0:55:02 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 17:22:31 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 14:23:36 阅读更多 →