ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring Boot + UniApp智能笔记实战:全栈开发与小程序多端部署

Spring Boot + UniApp智能笔记实战:全栈开发与小程序多端部署 简介这是一套基于SpringBoot与Vue、Uniapp技术栈的智能笔记应用前后端分离项目适合正在学习Java后端、小程序或Vue前端开发的初学者也可用于毕业设计、课程设计、工程实训或初期项目二次开发。压缩包共681个文件约11.76MB核心类型包括82个Java后端源码、72个Vue页面组件、98个JavaScript脚本、186个PNG图片资源以及SQL、XML、配置文件等可清晰对应后端服务、前端界面、数据库脚本与项目配置等模块。项目已适配JDK1.8、MySQL5.7、Tomcat7及Maven3.3.9等常见开发环境配有文档与调试好的可运行源码能帮助使用者快速理解SpringBoot接口开发与Uniapp多端适配的完整流程。该资源已有838人学习下载文件结构清晰目录区分明确便于按需查阅与二次开发适合作为实战练习或毕业设计的基础工程。1. 这套“Spring Boot UniApp”智能笔记到底解决什么问题用户嘴上说要“智能笔记”但真正想要的是笔记能跨端随时写、按标签快速找、内容多了不乱。这个标题里同时出现 Spring Boot、小程序、Vue、UniApp说明它不是一个花架子 Demo而是一条能直接落地的全栈链路——后端用 Spring Boot 出 REST API前端用 UniApp 写一套代码编译到微信小程序和 H5Vue 语法负责页面交互。适合正在做毕业设计或接外包项目、又不想在客户端和服务端之间来回切换的人也适合想从单体 CRUD 往“可上线小程序”迈一步的开发者。选这个组合的理由很实际Spring Boot 生态成熟做笔记这种以文本、标签、时间线为核心的数据模型能快速搭出稳定接口UniApp 则把 Vue 的开发体验带到了多端一套代码跑小程序、H5 和 App省掉重复开发。接下来的内容不会去复述某个现成源码包而会按照一线工程中最常走的路线从后端模型、前端联调、打包上线到笔记检索的进阶策略逐步拆给你看。2. Spring Boot 后端智能笔记的数据模型与接口设计2.1 笔记实体与用户体系的表结构设计智能笔记的数据模型比普通博客多一个“用户”和“标签”的维度。设计表结构时我一般会拆成四张核心表用户表、笔记本表、笔记表、标签表再加一张笔记-标签关联表。这样做的好处是后续加全文搜索、做标签筛选时不用改表结构。CREATE TABLE note_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(64) NOT NULL UNIQUE, password_hash VARCHAR(255) NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE notebook ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, name VARCHAR(128) NOT NULL, sort_order INT DEFAULT 0, FOREIGN KEY (user_id) REFERENCES note_user(id) ); CREATE TABLE note ( id BIGINT PRIMARY KEY AUTO_INCREMENT, notebook_id BIGINT NOT NULL, title VARCHAR(255) NOT NULL, content LONGTEXT, word_count INT DEFAULT 0, is_pin TINYINT DEFAULT 0, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (notebook_id) REFERENCES notebook(id) ); CREATE TABLE tag ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, name VARCHAR(64) NOT NULL, UNIQUE KEY uk_user_tag (user_id, name) ); CREATE TABLE note_tag ( note_id BIGINT NOT NULL, tag_id BIGINT NOT NULL, PRIMARY KEY (note_id, tag_id) );这段 SQL 里最关键的是note表的word_count字段它不是在插入时由数据库计算的而是后端在保存笔记时统计出来再写入方便后续做“笔记字数图”一类统计。updated_at用ON UPDATE自动刷新前端拉取笔记列表时按这个字段倒序就能直接实现“最近编辑排最前”。标签采取独立表和关联表是为了避免用逗号分隔字符串存储标签导致查询性能下降也方便后续做标签云。2.2 用 Spring Data JPA 或 MyBatis-Plus 搭建 REST API在 Spring Boot 里实现这套接口最常见的两个选择是 Spring Data JPA 和 MyBatis-Plus。JPA 的优势是实体映射简单适合表结构规范、改动不频繁的场景MyBatis-Plus 则更适合想自己控制 SQL、多表联查复杂的团队。做笔记这种中小型项目我会优先选 MyBatis-Plus因为它自带分页插件和代码生成器能省不少样板代码。RestController RequestMapping(/api/note) public class NoteController { Autowired private NoteService noteService; PostMapping(/save) public RNote save(RequestBody Note note, RequestAttribute(userId) Long userId) { note.setUserId(userId); note.setWordCount(countWords(note.getContent())); return R.ok(noteService.saveOrUpdate(note)); } GetMapping(/list) public RPageNote list(RequestParam(defaultValue 1) long page, RequestParam(defaultValue 20) long size, RequestParam(required false) Long tagId, RequestAttribute(userId) Long userId) { LambdaQueryWrapperNote wrapper new LambdaQueryWrapper(); wrapper.eq(Note::getUserId, userId) .eq(tagId ! null, Note::getId, noteService.findNoteIdsByTag(userId, tagId)) .orderByDesc(Note::getUpdatedAt); return R.ok(noteService.page(new Page(page, size), wrapper)); } }这段代码里RequestAttribute(userId)是从 JWT 拦截器里塞进去的不在 Controller 里重复查用户信息。LambdaQueryWrapper是 MyBatis-Plus 的查询条件构造器eq方法的第二个参数传false时条件会自动忽略所以tagId为空时就返回全部笔记不为空时才执行标签过滤。countWords是一个工具方法用正则去掉 Markdown 符号再统计中文和英文单词数避免用户看到字数跳动。2.3 笔记内容检索与标签体系的实现接口只做 CRUD 还不够智能笔记的搜索是刚需。线上环境如果数据量不大最直接的做法是利用 MySQL 的LIKE查询public PageNote search(long page, long size, Long userId, String keyword) { LambdaQueryWrapperNote wrapper new LambdaQueryWrapper(); wrapper.eq(Note::getUserId, userId) .and(w - w.like(Note::getTitle, keyword) .or() .like(Note::getContent, keyword)) .orderByDesc(Note::getUpdatedAt); return noteService.page(new Page(page, size), wrapper); }这里有个容易被忽略的点LIKE %keyword%不会走索引但笔记应用的特点是单用户数据量有限几百条笔记用全表扫描也能在几十毫秒内返回没必要为了“搜索性能”过早引入 Elasticsearch。如果未来数据量真的涨到几十万条再考虑给note表加FULLTEXT索引或同步到 ES。标签筛选时可以先在note_tag表查出所有tagId对应的noteId再放进主查询的IN条件里这种方案在标签粒度较粗时效率不错。2.4 接口安全基于 JWT 的登录态与参数校验小程序端没有 Cookie 概念登录态一般用 JWT 解决。用户在小程序里调用wx.login拿 code后端换成 openid 并生成 token之后每次请求在 header 里带Authorization: Bearer token。Spring Boot 侧用拦截器统一解析 token建议把 openid 转成业务里的userId塞到 Request 属性里接口层不要关心 token 怎么来的。public class JwtInterceptor 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 { Long userId JwtUtil.parseToken(token); request.setAttribute(userId, userId); return true; } catch (Exception e) { response.setStatus(401); return false; } } response.setStatus(401); return false; } }拦截器里只做两件事解析 token、转发 userId。真正的权限粒度控制要放在 Service 层比如noteService里每次更新都检查当前userId和笔记归属是否一致否则用户拼一个参数就能改别人的笔记。参数校验建议用javax.validation的注解在Note实体上给title加NotBlank给content加Size(max 100000)避免单条笔记过长把接口拖死。3. Vue UniApp 前端从页面到接口联调3.1 UniApp 项目结构与 Vue3 组合式 API 的选型UniApp 支持 Vue2 和 Vue3 两套语法。新项目我建议直接选 Vue3 搭配组合式 API核心原因是setup语法让逻辑复用更干净而且现在微信小程序的基础库对 Vue3 编译产物兼容性已经很好。如果你手头有老项目用的是 Vue2 语法迁到 Vue3 时主要是把data、methods拆成ref、reactive和普通函数事件绑定规则基本不变。script setup import { ref, onMounted } from vue; import { getNoteList, deleteNote } from /api/note.js; const noteList ref([]); const loading ref(false); async function loadNotes(tagId) { loading.value true; try { const res await getNoteList({ page: 1, size: 20, tagId }); noteList.value res.records; } finally { loading.value false; } } function handleDelete(note) { uni.showModal({ title: 确认删除, content: 删除后不可恢复${note.title}, success: async (result) { if (result.confirm) { await deleteNote(note.id); loadNotes(); } } }); } onMounted(() loadNotes()); /scriptref和onMounted来自 Vue3 组合式 API。这里的loading状态可以配合流式布局也可以直接用在scroll-view的下拉刷新指示器上。删除前弹窗确认是小程序端交互的标准姿势比直接在列表里滑出删除按钮更安全。3.2 封装 request 请求层对接 Spring Boot 接口UniApp 内封装 request 不能照搬 axios因为uni.request是 API 风格而且不同平台对返回值结构处理也不完全一样。一般是单独建一个utils/request.js把 baseURL、token 注入、错误提示统一放在里面。const BASE_URL https://api.example.com; export function request({ url, method GET, data {} }) { return new Promise((resolve, reject) { uni.request({ url: BASE_URL url, method, data, header: { Authorization: Bearer uni.getStorageSync(token), Content-Type: application/json }, success: (res) { if (res.statusCode 200) { if (res.data.code 200) { resolve(res.data.data); } else { uni.showToast({ title: res.data.msg, icon: none }); reject(res.data); } } else if (res.statusCode 401) { uni.navigateTo({ url: /pages/login/login }); reject(res.data); } else { uni.showToast({ title: 服务器开小差了, icon: none }); reject(res.data); } }, fail: (err) { uni.showToast({ title: 网络连接失败, icon: none }); reject(err); } }); }); }统一处理 token 和错误码后页面代码里就不用到处写uni.showToast。这里要特别注意的是Content-Type小程序端如果后端用RequestBody接 JSON必须明确设置为application/json否则参数会以表单形式提交后端解析不到。code字段假设每个接口的业务成功码是200如果和 HTTP 状态码冲突就在后端单独定义枚举。3.3 笔记编辑页与 v-model 的双向绑定细节笔记编辑页是一个典型的“内容多、状态多”场景。标题输入框、正文 textarea、标签选择、字数统计都要同步。这里用v-model直接绑定reactive对象里的字段但需要注意 textarea 在小程序中原生组件的层级问题建议用uni-easyinput或自己包一层cover-view。template view classeditor-page input classtitle-input v-modelnote.title placeholder标题 / textarea v-modelnote.content inputhandleInput placeholder开始输入... / view classword-count{{ note.word_count }} 字/view view classtag-row text classtag v-fortag in selectedTags :keytag.id tapremoveTag(tag) {{ tag.name }} × /text picker :rangeallTags range-keyname changeaddTag text classadd-tag 标签/text /picker /view /view /templateconst note reactive({ id: null, title: , content: , word_count: 0 }); function handleInput(e) { const text e.detail.value; note.content text; note.word_count countWords(text); }v-model在 input 上能自动同步但 textarea 的input事件拿到的是e.detail.value不是事件对象本身这是小程序和 Web 端一个明显差异。如果直接用v-model绑定 textarea字数统计的位置可能需要单独监听。selectedTags和picker组合是移动端选标签的常用做法比手动输入标签更顺手。3.4 用自定义分享和动态标题提升小程序体验笔记应用经常需要分享某条笔记给微信好友。默认分享只能带当前页面路径太丑。UniApp 里可以通过onShareAppMessage自定义标题和封面。注意这个生命周期函数必须写在setup里通过defineOptions声明或使用选项式 API 定义。script setup import { defineOptions } from vue; defineOptions({ onShareAppMessage() { const note noteDetail.value; return { title: note.title, path: /pages/note/detail?id${note.id}, imageUrl: note.coverUrl || /static/share-cover.png }; } }); /script动态设置小程序顶部标题可以在页面加载后调用uni.setNavigationBarTitle但更推荐在路由配置里把 navigationBarTitleText 留空然后在onLoad里根据笔记标题动态设置。这样用户看到的不再是所有笔记详情页都叫“笔记详情”而是一条具体的标题加深印象。这个体验细节对笔记类工具型产品很重要。4. 小程序运行、打包与部署的常见坑4.1 开发环境从 HBuilderX 到微信开发者工具的联调UniApp 项目一般用 HBuilderX 打开运行到微信开发者工具前需要先做好两个配置HBuilderX 里的微信开发者工具路径以及微信开发者工具的安全设置里开启“服务端口”。如果改完配置点运行没反应多半是端口没开或路径带了空格。流程是HBuilderX 点“运行到小程序模拟器”里的“微信开发者工具”会自动生成dist/dev/mp-weixin目录并在微信开发者工具中打开。这种模式是热更新的直接改代码就能看到小程序页面变化不用重新打包。注意默认生成的项目名可能带乱码需要在manifest.json里把mp-weixin的appid配好否则很多接口无法走真实环境。4.2 Manifest 配置与小程序上线前的备案备注小程序上线前需要在 mp.weixin.qq.com 后台配置服务器域名并且在manifest.json里把接口域名填进mp-weixin的networkTimeout和domain配置。不要直接在代码里写 IP 甚至http://localhost真机上是访问不到的。备案备注信息怎么填按微信官方要求写“工具类应用”或具体场景即可不要写空泛的“个人博客”。如果服务端用了 IP 加端口的地址备案时还需要说明用途审核较容易被打回。建议在上线前两周就提交备案因为审核周期通常需要 3 到 7 个工作日。4.3 真机调试与接口地址切换的注意事项开发时用电脑端浏览器模拟正常一放到真机就请求失败九成是接口地址问题。真机上不能访问localhost必须使用局域网 IP 或公网域名。我自己常用的方案是做一个config.js根据运行环境切换 baseURL。// config.js const ENV dev; // 切换成 prod 时使用线上地址 const API_BASE_URL { dev: http://192.168.1.100:8080, prod: https://api.example.com }[ENV]; export default { API_BASE_URL };真机调试时还有一个坑安卓真机用http明文域名必须在 manifest 的mp-weixin里勾选“不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书”否则请求被拦截。但这个选项只在开发工具和调试模式下有效正式版必须配域名加 HTTPS 证书。4.4 打包发布到安卓/iOS 应用市场的基本步骤如果不仅要小程序还要 Android/iOS AppUniApp 可以打包成原生 App。安卓建议用自有证书因为云打包的证书可能会导致签名频繁变化影响后续更新。iOS 打包则必须用 Mac 环境通过云端打包需要上传.p12证书和描述文件。HBuilderX 里选择“发行 - 原生App-云打包”填写包名、证书别名、密码勾选需要的权限比如相机和相册笔记应用一般要加图片附件。iOS 上架 App Store 时还要注意隐私政策尤其是笔记内容会同步到服务器的必须在应用描述里说明数据存储位置。5. 智能笔记的进阶按热度排序与标签自动提取算法前面章节都在讲基础功能现在单独聊一下如何让笔记“智能”起来。很多项目会把“智能”等同于全文搜索但更接地气的做法是给笔记增加一个hot_score字段通过编辑频率、访问次数、内容长度综合计算排序权重这样老用户打开小程序时看到的不是完全按时间倒序的列表而是频繁编辑的内容优先。实现这个积分策略最简单的办法是在每次保存和阅读时给note表累加一个值得分。保存加 3 分阅读加 1 分新建时初始分等于字数除以 100。然后在查询列表时按hot_score和updated_at联合排序。// 标签自动提取基于词频的简易实现 public ListString extractTags(String content) { String[] words content.replaceAll([\\p{Punct}\\s], ).toLowerCase().split( ); MapString, Integer freq new HashMap(); for (String w : words) { if (w.length() 1 !STOP_WORDS.contains(w)) { freq.put(w, freq.getOrDefault(w, 0) 1); } } return freq.entrySet().stream() .sorted(Map.Entry.String, IntegercomparingByValue().reversed()) .limit(5) .map(Map.Entry::getKey) .collect(Collectors.toList()); }这个标签提取算法不依赖任何 NLP 库原理是把笔记内容按标点和空格分词去掉停用词找出出现频率最高的前 5 个词作为标签。对于中文环境需要先用 jieba 分词或者直接把一段连续文本按字切分以后做 bigram 统计。把提取结果保存到标签表用户后续搜索标签时就能覆盖到本来没手动打标签的笔记。如何验证这个功能是否有效可以找一批笔记内容做单元测试检查同一篇笔记重复执行提取出的标签是否稳定以及不同主题笔记的标签重合度是否低。把这些测试用例放到 CI 里以后改分词逻辑不会破坏原有行为。真正上线前可以给自己的笔记跑一遍看提取出的标签是否符合直觉不对就在停用词表里补充常见词。用这个思路做智能笔记花不了几天但体验会比纯 CRUD 明显高一个档次。本文还有配套的精品资源点击获取
返回列表