ARTICLE DETAIL

资讯详情

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

多功能记事本小程序开发:数据模型、同步与防乱码实践

多功能记事本小程序开发:数据模型、同步与防乱码实践 简介这是一套面向高校计算机相关专业毕业设计场景的多功能记事本系统项目资料集成记事、分类管理、记录检索等常见功能模块采用Java技术栈实现前后台分离适合需要快速完成系统设计、源码阅读或二次开发的学生使用。资源包整体约49.19MB主要内容包括毕业设计论文文档、前后台Java项目源码、数据库脚本以及项目运行效果截图能够覆盖从环境搭建、功能演示到论文撰写说明的完整链路。目前已有240人学习下载热度虽不算高但对于需要参考完整可运行项目的初学者而言具有较高实用价值。通过这份材料使用者可以拿到可直接导入开发工具的工程代码结合数据库脚本快速搭建本地环境借助项目截图核对页面效果同时依据论文文档理解系统架构、数据库设计与核心流程便于后续在此基础上扩展功能或调整界面也为毕业设计答辩准备提供了较好的支撑素材。1. 记事本小程序设计从本地备忘录到多功能系统真正的坑是数据同步拿到“记事本小程序设计多功能记事本系统”这个需求时多数人会直接打开微信开发者工具拖一个 textarea 上去。等真机跑起来才发现用户要的“多功能”不是点选一两个开关而是把笔记、待办清单、定时提醒、标签归档甚至多设备交叉编辑全放进一个小程序当个人工作台用。记事本系统的难点因此从“怎么存文本”转移到了另一件事——数据模型怎么同时兼容多种笔记形态本地缓存与云端记录怎么合并不丢字导出的 txt 怎么在 Windows 记事本里打开不乱码。我按实际搭这套系统的顺序来拆解先建模再做编辑再同步最后处理编码和防破解。2. 多功能记事本系统的数据模型与本地优先存储设计多功能记事本的第一版需求通常长这样能打字、能写清单、能定时提醒、能在微信里分享出去。如果为每个功能建一套表最终列表页要连查好几张表小程序端的代码会变得没法维护。我最终用一张 notes 表加 type 字段来统一承载所有笔记形态实际跑下来增删功能和同步都简单得多。2.1 一张笔记表同时承载文本、清单、提醒三种类型有人一开始会建三张表note、todo、reminder结果列表页聚合逻辑非常痛苦。我会用一张 notes 表加 type 字段区分。文本、清单、提醒统一为一种“笔记实体”只是渲染方式不同。字段设计如下表字段类型说明_idstring云数据库主键本地用uuid生成titlestring标题最长 50 字contentstring富文本 JSON 字符串或纯文本typenumber0文本笔记1清单2提醒tagsarray标签数组用于筛选remindTimenumber提醒时间戳仅 type2 使用updatedAtnumber最后修改时间戳同步核心deletedboolean软删除标记这个设计的关键在于 content 字段富文本编辑器和清单编辑器都把序列化后的结果当成普通字符串存进去渲染时再 parse 出来这样新增样式功能不需要改表结构。deleted标记也很重要同步时不直接删数据防止某台离线设备上的旧记录在恢复网络后把云端的最终状态覆盖掉。2.1.1 本地 Storage 读写的正确姿势小程序本地缓存单条上限约 1MB但整体容量受设备限制。我不用wx.setStorageSync(notes, allNotes)存整个数组那样每次改一个字符都要重写全部数据列表长起来会明显卡顿。正确的做法是按 id 分 key 存单条笔记再用一个索引 key 只存列表展示时需要的小字段// 本地写入单条笔记 function saveNoteLocal(note) { const key note_ note.id; wx.setStorageSync(key, note); // 索引列表只存列表页需要展示的冗余字段 const index wx.getStorageSync(note_index) || []; const summary { id: note.id, title: note.title, type: note.type, updatedAt: note.updatedAt }; const found index.find(item item.id note.id); if (found) { Object.assign(found, summary); } else { index.push(summary); } wx.setStorageSync(note_index, index); }代码逻辑是先写单条数据再维护索引列表。索引里不存 content避免列表页一次性把几百段富文本都加载到内存。参数说明note.id用Date.now().toString(36) Math.random().toString(36).slice(2, 10)生成保证离线创建时也有唯一主键updatedAt用Date.now()毫秒时间戳不要直接存Date对象否则云函数比较字符串和数字时会多出一步类型转换。2.2 本地优先为什么先看本地再请求云端用户打开小程序时第一眼应该看到本地已经存好的历史记录云同步在后台静默进行。这样在电梯、地铁里也能正常写笔记等有网时再合并。实现时列表页用note_index渲染点击某条再读取note_${id}对应的完整内容。读取完整内容时注意处理索引存在但单条缓存被系统清掉的情况要捕获异常并给一个默认的空笔记结构。云同步的启动时机放在onShow里而不是onLoad因为小程序切后台再回来时内容可能已经被另一台设备改过每次进入都检查增量才能把多端修改拉下来。这个方案并不复杂却比直接在onLoad写一次同步更能覆盖真实使用场景。2.3 云数据库集合、权限与索引配置在云开发控制台创建notes集合权限设为“仅创建者可读写”对应的安全规则是{ read: doc._openid auth.openid, write: doc._openid auth.openid }这样的好处是客户端不能读别人的数据但云函数作为管理员仍可操作。索引管理里我会建一个_openid deleted updatedAt的复合索引专门服务增量拉取。为什么不把_openid放进每个文档的data里云函数使用cloud.getWXContext().OPENID获取当前用户身份解析即可。增量查询的关键代码是const db cloud.database(); const _ db.command; const res await db.collection(notes) .where({ _openid: openid, updatedAt: _.gt(lastSyncTime) }) .limit(100) .get();注意limit(100)是单次读取上限实际数据量超过 100 条时还需要用res.data[res.data.length - 1].updatedAt作为下一批的起点循环拉取直到返回条数不足 100。参数说明lastSyncTime是客户端上次同步完成后拿到的时间戳用来拉取“这个时间之后变化过”的记录。使用_.gt而不是_.gte可以避免把上一次已经拉过的数据重复拉一遍但代价是极端情况下同一秒内有新修改会被漏掉。所以我会在客户端每次同步完后把lastSyncTime更新为服务器返回的serverTime而不是本地时间防止设备时钟慢导致后续拉不全。3. 记事本小程序的编辑模块清单勾选、动态标题与提醒逻辑多功能记事的差异化不在存储而在编辑交互。标题要跟着输入走清单要能一条条勾掉提醒要按时出现。实现中常见的错误是每敲一个字就 setStorage 一次还有把整个 content 数组重新 setData。下面是我在页面里实际采用的写法。3.1 动态设置导航栏标题并延迟保存微信小程序动态设置标题要用wx.setNavigationBarTitle但注意导航栏标题有长度限制且用户连续输入时每次都调 API 会频繁触发导航栏重绘。我通常的做法是输入事件里只更新页面 data用计时器做 1.5 秒防抖后再写入存储并同步标题Page({ data: { noteTitle: , currentNote: null }, onTitleInput(e) { const value e.detail.value; this.setData({ noteTitle: value }); if (this._titleTimer) clearTimeout(this._titleTimer); this._titleTimer setTimeout(() { const note this.data.currentNote; note.title value; note.updatedAt Date.now(); saveNoteLocal(note); wx.setNavigationBarTitle({ title: value.slice(0, 10) || 新建记事 }); }, 1500); } });代码逻辑_titleTimer挂在this上而不放 data是为了避免计时器状态触发视图更新。value.slice(0, 10)截断标题是因为微信导航栏在大部分机型上只显示约 10 个字截断后不会出现标题换行或闪烁。1.5 秒的防抖参数可以根据用户打字习惯调整如果测试发现丢内容可以降到 500ms如果只想在页面卸载时保存也可以省略防抖直接写但真机上快速退出时容易丢最后几个字。3.2 清单模式的勾选状态维护用户习惯把清单项也叫单选框其实是多选清单所以用checkbox而不是radio因为每一条都可以独立勾选。content 字段里存的是 JSON 数组每一项是{ text: 买牛奶, done: false }渲染用wx:forview classtodo-item wx:for{{todoList}} wx:keyindex checkbox checked{{item.done}} bindchangetoggleTodo >toggleTodo(e) { const index e.currentTarget.dataset.index; const note this.data.currentNote; const content note.content; content[index].done !content[index].done; note.content content; note.updatedAt Date.now(); // 使用路径更新只修改对应那一行 this.setData({ [todoList[ index ].done]: content[index].done }); saveNoteLocal(note); }逻辑说明>onShow() { const now Date.now(); const index wx.getStorageSync(note_index) || []; const expired index.filter(item item.type 2 item.remindTime item.remindTime now ); expired.forEach(item { const note wx.getStorageSync(note_ item.id); if (note !note.notified) { note.notified true; note.updatedAt Date.now(); saveNoteLocal(note); wx.showModal({ title: 记事提醒, content: note.title, showCancel: false }); } }); }这里没有用wx.requestSubscribeMessage做一次性订阅因为用户点“允许”之后依然只给一次下推机会对长期记事本来说体验很差。前台弹窗方案适用于那些“打开小程序时顺便看到”的场景比如每天第一次进入时把过期的纪念日、待办全量弹一遍。如果业务必须支持离线推送需要在云端使用定时触发器遍历到期笔记再通过云调用下发订阅消息但那样必须在用户授权且有可订阅余量时才能成功我一般把这种推送当作补充而非主力。4. 记事本系统的多端同步与导入导出本地和云端的同步其实是两个方向的合并云端要拉取比 lastSyncTime 新的变更本地要把未上传的笔记推给云端。时序和冲突是最大的坑。我设计的同步虽然是按时间戳覆盖的简单策略但对个人记事本场景已经足够。4.1 云函数按 updatedAt 合并本地与云端笔记云函数放在cloudfunctions/syncNotes目录接收客户端传来的本地变更数组和上次同步时间执行增量拉取与写回const cloud require(wx-server-sdk); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }); const db cloud.database(); const _ db.command; exports.main async (event) { const { notes, lastSyncTime } event; const { OPENID } cloud.getWXContext(); // 拉取云端增量 const cloudRes await db.collection(notes) .where({ _openid: OPENID, updatedAt: _.gt(lastSyncTime) }) .limit(1000) .get(); // 本地传来的笔记按 id set 回去 for (const note of notes) { if (!note.deleted) { await db.collection(notes).doc(note.id).set({ data: { ...note, _openid: OPENID } }); } else { await db.collection(notes).doc(note.id).update({ data: { deleted: true, updatedAt: note.updatedAt } }); } } return { cloudNotes: cloudRes.data, serverTime: Date.now() }; };逻辑说明doc(note.id).set用本地生成的 id 作为云数据库主键重复执行不会产生重复数据所以云函数天然幂等。deleted的写法是只更新标志位不物理删除因为另一台离线设备可能还持有旧数据物理删除后那台设备一旦同步又会把它当成新笔记插入。云函数返回serverTime客户端把它存为新的lastSyncTime避免设备本地时钟不准导致同步倒退。同步冲突的处理可以按优先级归为下表冲突场景冲突策略理由同一笔记两端都修改updatedAt较大者覆盖一般编辑间隔超过秒级时间戳够用一端删除另一端修改删除胜出用户删笔记的意图更明确网络中断导致云函数执行一半重试整个同步set和update幂等不会重复写入4.2 导出 .txt 文件并兼容 Windows 记事本中文乱码导出功能不是把 note.content 直接写成文件那么简单。很多用户在电脑上打开导出的 txt 时遇到中文乱码原因是高版本 Windows 的记事本对 UTF-8 无 BOM 文件判断不准确会按系统本地代码页解析成乱码。我的做法是导出时在文件开头加 BOM 字符\uFEFFfunction exportNote(note) { const fs wx.getFileSystemManager(); const filePath ${wx.env.USER_DATA_PATH}/export_${note.id}.txt; const text 标题${note.title}\n更新时间${formatTime(note.updatedAt)}\n\n${note.content}; const withBom \uFEFF text; try { fs.writeFileSync(filePath, withBom, utf8); wx.shareFileMessage({ filePath: filePath, fileName: ${note.title}.txt, fail: err console.error(导出失败, err) }); } catch (e) { wx.showToast({ title: 导出失败, icon: none }); } }参数说明\uFEFF是 Unicode 字节顺序标记Windows 记事本看到 BOM 会立刻识别为 UTF-8。wx.env.USER_DATA_PATH是小程序本地用户文件目录不需要申请权限就能写。注意BOM 只在导出边界添加内部存储、云数据库里不要加否则一条笔记的 content 拼接后每段都带上 BOM渲染时会出现不可见字符导致文本两端对齐异常。4.3 uni-app 工程发行微信小程序时遇到的 npm 脚本限制如果项目是基于 uni-app 开发的发行到微信小程序时会遇到 HBuilderX 的“发行 - 小程序-微信”流程。步骤是先在 HBuilderX 里填好微信小程序 AppID点击发行生成dist/dev/mp-weixin目录再用微信开发者工具导入编译结果。工程里如果依赖 npm 包构建脚本经常会碰到这样一个报错npm.ps1 无法加载因为在此系统上禁止运行脚本。这个报错来自 Windows PowerShell 的默认执行策略 Restricted.ps1 文件不能运行。处理方式是以管理员身份打开 PowerShell执行Set-ExecutionPolicy -ExecutionPolicy RemoteSigned执行后确认输入Y重启 HBuilderX 再发行即可。RemoteSigned只允许本机创建的脚本运行从互联网下载的脚本仍需要数字签名是相对平衡的配置不会对系统安全性造成明显影响。注意这和 npm 本身没关系是 Node.js 在 Windows 下安装时通过 .ps1 脚本设置环境变量导致的问题。5. 记事本小程序发布前的两个硬检验BOM 防乱码与反编译防护发布前除了调样式建议把两个容易被忽略的硬检验加进构建流程一个是 BOM 是否重复另一个是云环境是否被反编译后误连到测试库。5.1 写一个自检函数保证导出文件不会重复 BOM有些富文本编辑器生成的 content 里本身带了 BOM导出时再拼接\uFEFF就会形成双重 BOMWindows 记事本打开时可能显示成“锟斤拷”等乱码字符。我写了一个幂等函数放公共工具库function ensureBom(text) { return text.charCodeAt(0) 0xFEFF ? text : \uFEFF text; }自检方法是在导出前调一行console.log(ensureBom(text).charCodeAt(0).toString(16))如果输出feff就说明 BOM 已正确落在第一位。真机测试时至少覆盖一台 Windows 10 以上电脑和一台旧 Windows 7分别用记事本打开导出的文件确认标题和时间中文显示正常。5.2 防止反编译后联到错误云环境微信小程序反编译工具能还原出所有前端代码但云函数代码不会被打进小程序包里所以最敏感的服务端逻辑应该放进云函数前端只负责调用。另一个值得做的是在onLaunch里根据envVersion区分云环境避免反编译者使用开发版调试你的代码时把所有页面操作都发到生产数据库const envMap { develop: dev-db-env, trial: trial-db-env, release: prod-db-env }; wx.cloud.init({ env: envMap[wx.getAccountInfoSync().miniProgram.envVersion] || prod-db-env });参数说明envVersion在真机上由微信自动注入反编译运行在开发者工具里时 envVersion 为 develop只能连到开发库避免误写线上数据。这个技巧不是绝对安全但能把事故半径限制在测试环境。最后在真机发布流程里跑一遍“新建笔记 - 编辑清单 - 导出 txt - Windows 打开”的完整链路确认编码和同步都正常后再提审。本文还有配套的精品资源点击获取
返回列表