情绪小恐龙:一个微信小程序从架构设计到部署上线的全记录

关键词:微信小程序、本地优先(Local First)、Spring Boot 3、MongoDB、增量同步、游客模式、双 Token、系统部署

项目 mood_log 包含两个工程:emotion-monster(微信小程序前端)+ mood-backend(Java 后端)。本文以一个个人开发者视角的完整周期,拆解它的设计取舍与落地细节。


一、项目背景与目标

市面上的情绪 / 生理期记录工具大多"重云端、轻隐私":打开就要注册,数据默认躺在别人的服务器上。本项目反其道而行------本地优先(Local First) 是贯穿始终的核心价值观:

  • 隐私可控:所有数据默认存在用户设备本地,离线可用,打开即用,不强制注册。
  • 零门槛:未登录也能完整记录;想备份换机时,再登录把数据合并上云。
  • 轻量可维护:个人开发者维护,技术栈必须简单、依赖少、可独立部署。

产品目标很朴素:让用户"今天花 10 秒打一个情绪卡 + 生理期卡",再通过统计与可视化,帮助ta看见自己的身心节律。技术目标则要更硬核一些:本地优先架构下的离线写入、增量同步、幂等合并、游客一键上云


二、整体架构设计

系统采用前后端分离 + 本地优先的两段式结构:

复制代码
┌──────────────────────────────┐         HTTPS          ┌─────────────────────────────┐
│   emotion-monster (小程序)     │ ─────────────────────▶ │   mood-backend (Spring Boot) │
│                              │   /api/v1/**            │   8200                      │
│  storage.js  (L4 本地引擎)    │                        │   Spring Security + JWT      │
│  data-adapter (门面)          │                        │   MongoDB (mood_log)        │
│   ├─ LocalAdapter  (本地)     │                        │   calculator / service / ...  │
│   └─ RemoteAdapter (云端)     │ ◀───────────────────── │                             │
└──────────────────────────────┘   增量 pull / push      └─────────────────────────────┘

核心抽象是 Api 门面utils/data-adapter.js):页面层只调用 Api.request(module, action, payload),至于数据最终落在本地还是云端,由适配器决定。

javascript 复制代码
init(force) {
  if (this.adapter && !force) return;
  const logged = !!storage.getSession();
  this.adapter = logged ? new RemoteAdapter() : new LocalAdapter();
}
  • 游客态 :走 LocalAdapter,所有读写直接操作 wx.Storage 上的内存镜像。
  • 登录态 :走 RemoteAdapter,写操作先落本地、后台推送云端;读操作镜像回本地缓存,页面无感。

这种"门面 + 适配器"的模式,让页面代码完全不感知存储位置,是本地优先能成立的关键。


三、关键技术决策及理由

3.1 为什么用原生小程序而非框架

项目选择原生 WXML/WXSS/JS,不引入 Taro/uni-app。理由:

  • npm install、零构建,微信开发者工具导入即跑,部署心智负担最低;
  • 个人开发者维护,减少"框架升级踩坑"的隐性成本;
  • 需要的是稳定可交付,而不是跨端复用。

代价是组件复用靠手写,但这对功能规模来说是可接受的。

3.2 为什么后端选 Spring Boot 3 + MongoDB

维度 选择 理由
语言/框架 Java 17 + Spring Boot 3.3 与算法/类型系统契合,Spring Security 鉴权开箱即用
数据库 MongoDB 文档模型天然适配"每个用户一份变长记录集",schema 演进成本低
鉴权 JWT(jjwt 0.12.6, HS256)+ 双 Token 无状态、适合小程序与未来 Web 管理端共享
文档 springdoc-openapi 自动生成 /v3/api-docs,前端类型可代码生成

MongoDB 的"无表结构"特性尤其重要:本地存储引擎(storage.js)也是一套 JSON 结构,前端与后端的数据形态高度一致,同步算法得以在两端复用同一套语义。

3.3 为什么"本地优先"而非"云优先"

云优先意味着断网即废、注册即门槛、隐私即妥协。本地优先把可用性隐私前置:

  • 写操作永远先成功(落本地),再异步上云;
  • 网络异常进入离线队列 pendingOps,恢复后重放;
  • 即使后端宕机,用户的核心记录体验不受影响。

它带来的唯一复杂度是"同步",我们用增量 push/pull + clientId 幂等 + updatedAt 冲突仲裁来化解(见第五节)。

3.4 为什么游客与正式用户共享同一套记录体验

设计原则叫"进得来、用得起、走得掉"------游客与正式用户唯一差异在"数据是否上云":

  • 游客身份 = 本地 deviceIdwb- + 随机串);
  • 登录即用 openid 关联合后端用户,触发本地数据与云端按 clientId + updatedAt 合并;
  • 合并前自动备份到 em_guest_backup,失败可回滚,绝不丢数据。

这样"登录"不是一个门槛动作,而是一次可选的"备份升级"。


四、核心功能实现思路

4.1 L4 本地存储引擎(storage.js

这是整个前端最关键的模块,能力包括:分域多 key 读写、结构校验、版本迁移、写锁串行、防抖持久化、事件广播、内存镜像

几个值得说的设计点:

  • 内存镜像 :启动时把四个核心域(meta/mood/period/settings)读入内存,页面读写只操作内存,避免频繁 wx.Storage 同步 IO。
  • 写锁 + 防抖 :所有写操作进入串行队列 writeQueue,并以 500ms 防抖批量落盘(flush),既保证并发安全又减少 IO。
javascript 复制代码
function lockWrite(task) {
  const run = writeQueue.then(task, task);
  writeQueue = run.catch(() => {});
  return run;
}
  • 版本迁移与自恢复schemaVersion 驱动 migrate();一旦结构损坏,转存 em_corrupted_backup 后空库启动,不白屏。

4.2 静默登录 + 游客降级(app.js

登录流程刻意做到"无弹窗、不打断":

  1. 启动时优先判断本地会话是否可复用(canReuseSession():refreshToken 剩余 > 7 天直接复用,不发新请求);
  2. 不可复用时才 wx.login() 取 code,静默调 /auth/wx-login
  3. 任何失败一律降级为游客态,本地数据完全不受影响。

4.3 双 Token 与单飞续期(http.js

后端签发 accessToken(2h)+ refreshToken(30d,哈希落库 + 轮换)。前端封装统一请求层:

  • 业务请求 401(token 过期)→ 单飞(single-flight)/auth/refresh → 重放原请求;
  • 单飞用模块级 refreshTask 变量保证并发 401 只刷新一次,避免"刷新风暴"。
javascript 复制代码
function refreshAccessToken() {
  if (refreshTask) return refreshTask;   // 并发复用同一 Promise
  refreshTask = new Promise(/* ... */);
  return refreshTask;
}

4.4 增量同步与冲突仲裁

  • push :本地每次写操作后节流 2s 合并推送,断网进 pendingOps 队列,指数退避重试;
  • pull :登录/启动/回前台携带 since=pullCursor 增量拉取;
  • 幂等(userId, clientId) 唯一索引,重复触发不产生重复数据;
  • 冲突updatedAt 新者胜,相同则服务端优先;删除用软删墓碑(deleted=true,保留 90 天)。

4.5 统计算法前后端同源

后端 calculator/ 包(连续打卡 StreakCalculator、周期预测 PeriodPredictor、情绪统计 MoodStatsCalculator、关联分析 PeriodCorrelationAnalyzer)由前端 utils/calc.js 移植而来,保证本地计算与云端聚合口径完全一致------这是"本地优先"能给出可信统计的前提。

4.6 首页"情绪增强"模块(零后端改动)

一个很好的例子是首页增强模块:新增"快速因子标签栏 / 今日小滋养短句 / 情绪预警提示卡"三块功能,后端零改动、首页零新增网络请求 ,完全复用已请求的 5 个并发数据。它还把"情绪 × 生理周期相位"的联动提示前置到首页 ,并严格遵守合规红线:只提示、不诊断,固定尾行"⚠️ 只做提示,不做诊断。"。这种"纯前端、纯函数、可独立验证"的模块设计(utils/mood-tips.js)值得推广。


五、开发与测试流程

5.1 设计先行

项目最大的特点是有系列化、可实施的详细设计文档(v4/v5/v6 演进、首页增强、管理端方案、以及两份"诊断报告")。每次大的架构演进都先写文档再写代码:本地优先 → 叠加账号体系 → 登录与同步 → 性能诊断与优化。文档里甚至包含"落地改造清单"和"实施优先级",把"想清楚"作为第一道工序。

5.2 静态代码审计驱动优化

没有专职测试团队,团队用静态代码审计 + 增长建模 做性能预判。例如 refresh_tokens-数据增长诊断报告 通过逐行追代码,预言了集合会单调递增、垃圾率 >99%,并给出定量模型与上线检查清单。这种"在写之前就把坑画出来"的做法,比事后救火高效得多。

5.3 纯函数可测性

核心规则(预警判定 buildAlert、滋养句选取 pickNourish、连续打卡、周期预测)都被抽成无副作用的纯函数 ,不依赖 wx.*,可脱离小程序环境直接跑用例矩阵。首页增强文档里就附了 10 条 buildAlert 用例(T1--T10)作为验收基准。

5.4 接口契约前移

后端集成 springdoc,/v3/api-docs 可直接用 openapi-typescript 生成前端 DTO 类型,把契约错误消灭在编译期。


六、部署方案与环境配置

后端部署在单机 Linux,整体链路:小程序 → Nginx(HTTPS) → Spring Boot(8200, 仅回环) → MongoDB(27017)

6.1 服务托管(systemd)

deploy/mood-backend.service 用 systemd 托管,做了基础加固:NoNewPrivilegesPrivateTmpProtectSystem=full,JVM 参数按规格收敛(-Xmx256m、G1GC),凭据通过 EnvironmentFile(chmod 600)注入,优先级高于 jar 内配置,便于轮换而无需重新打包。

6.2 反向代理(Nginx)

nginx-mood.conf 承担:HTTP→HTTPS 全量跳转、TLS≥1.2+现代 cipher 套件、Strict-Transport-Security、gzip、单 IP 限流(limit_req)、文件上传大小限制。后端只监听 127.0.0.1:8200,外网一律经 Nginx 进入,缩小攻击面。

6.3 滚动发布与自动回滚(rollout.sh

每次发版一个脚本搞定:备份旧 jar → 替换 → systemctl restart → 健康检查 GET /api/v1/health → 超时则回滚。还保留最近 5 个备份,失败可一键还原。

bash 复制代码
until curl -fsS "$HEALTH_URL" >/dev/null 2>&1; do
  if [ "$(date +%s)" -ge "$DEADLINE" ]; then
    # 回滚到上一个可用版本
  fi
  sleep 2
done

6.4 配置与密钥

真实凭据放 application-local.yml(gitignore 忽略),生产优先用环境变量(MONGODB_URI / JWT_SECRET / WECHAT_APP_ID / WECHAT_APP_SECRET)。文档特别提示:密码含 @ 需 URL 编码为 %40JWT_SECRET 必须 ≥32 字节并定期轮换。


七、遇到的挑战及解决方案

7.1 refresh_tokens 集合失控增长

问题 :诊断发现,小程序 onLaunch 无条件静默登录,每次冷启动签发一条新 refreshToken,旧记录永不回收;deleteByExpiresAtBefore 已定义却 0 处调用,无 TTL 索引、无定时任务。建模估算典型公测场景年增约 186 万条、~740MB,垃圾率 >99%。

解决方案(分阶段落地)

  1. 客户端会话复用canReuseSession() 判断 refreshToken 剩余 >7 天直接复用,消除 90%+ 的签发请求(改动约 10 行)。
  2. TTL 索引 :给 expiresAtexpireAfterSeconds=604800,MongoDB 自动回收过期记录。
  3. (userId, deviceId) 唯一索引 + upsert :每用户每设备恒定 1 条记录,集合规模从 O(启动次数) 降为 O(用户数 × 设备数)
  4. CAS 原子刷新:用"删除返回值"做乐观锁,杜绝并发重复创建孤儿 token。
  5. 兜底定时清理 + 监控TokenCleanupTask,每日低峰输出 validRate 等指标日志)。

上线踩坑点 :第二步引入唯一索引后,若存量已有 (userId, deviceId) 重复记录,Spring Data 启动时建索引会抛 DuplicateKeyException 导致启动失败。文档给出"预检 → 清理重复 → 复检"的发布前检查清单,必须执行。

7.2 deviceId 不稳定击穿容量优化

诊断 deviceId-稳定性对刷新链路的影响分析 发现:虽然 deviceId 变化不会打断刷新(refresh 靠 tokenHash 定位、JWT 中 deviceId 只是透传 claim),但会让"每设备 1 条"的优化失效,并有小概率触发唯一索引冲突导致掉登录。

根因storage.jsmeta.deviceId 缺失兜底时,只写入内存变量、未回写并持久化,导致同一设备每次冷启动都生成新 deviceId

修复 :兜底生成后必须 enqueueWrite('meta')flushNow()------注意顺序,因为 flush()dirty 标记为开关,直接调 flushNow() 时若 dirty.meta 仍为 false 会直接返回不落盘。同时后端 refresh 不再写回 deviceId(语义上续期不应改变设备归属),彻底消除索引冲突。

7.3 弱网与冲突

  • 断网写 :先落本地 + 进 pendingOps,恢复后批量重放,指数退避最多 5 次。
  • 合并冲突updatedAt 新者胜;合并前备份 em_guest_backup,任一步失败可安全重试,不进入半合并状态。
  • 管理端并发写 :引入乐观锁 expectedUpdatedAt,冲突返回 SYNC_CONFLICT(50001),提示用户刷新后重试,避免静默覆盖。

八、性能优化措施

  1. 本地存储引擎:内存镜像 + 写锁串行 + 500ms 防抖,把高频小写聚合成低频落盘;万条记录冷启动目标 <100ms。
  2. 会话复用:避免每次冷启动签发 token,把刷新集合增长压降 ~98%。
  3. 增量同步push 节流合并、pull 携带 since 游标,只传变化量,减少带宽与计算。
  4. 统计走服务端聚合 :复杂聚合(趋势、分布、热力、留存)由后端 calculator 计算,前端只做视图映射,杜绝"前端拉明细再统计"的崩溃风险。
  5. 索引治理 :补齐跨用户检索索引({recordDate:-1,deleted:1} 等)、聚合强制带日期区间、预聚合表(daily_stats)应对大数据量,管理端读可选走从节点。
  6. Nginx 层 :gzip、连接复用(keepalive)、单 IP 限流,缓解接口被刷。
  7. 可观测TokenCleanupTask 输出容量指标(total/validRate/active24h),traceId 贯穿前后端日志,问题可主动发现而非事后救火。

九、经验总结与最佳实践

9.1 把"想清楚"前置,用文档驱动开发

这个项目最值得借鉴的不是某段代码,而是设计文档即实施清单的习惯:每次大的演进都先产出一份带"落地改造清单 / 优先级 / 检查项"的文档,甚至包含定量增长模型。它让一个人的开发也能保持架构一致性,降低返工。

9.2 本地优先是价值观,不是技术噱头

本地优先带来的离线可用、隐私可控、零门槛体验是产品差异点,但它要求你认真设计同步、幂等、冲突仲裁、合并回滚。一旦偷懒,就会在"集合膨胀""设备号漂移"等地方付出代价。本项目的诊断报告证明:这些代价是可以被提前建模并消除的。

9.3 用"最小改动"验证最大收益

很多优化只需十几行:会话复用(前端 10 行)、TTL 索引(实体 1 行)、deviceId 兜底回写(几行)。先做收益最高、风险最低的改动,再逐步上结构性优化,是个人项目的务实节奏。

9.4 一致性要靠"协议"而非"默契"

前端本地、前端云端、后端、未来的管理端,四方对"软删墓碑 / clientId 幂等 / updatedAt 仲裁"必须口径一致。管理端方案据此规定:只软删、必更新 updatedAtclientId 不变、source=admin、乐观锁------守住这五条,小程序无需任何改造即可感知后台改动。

9.5 安全与隐私是默认项,不是补丁

  • 密码 BCrypt;openid/session_key 不下发、不落库;
  • 所有查询强制按 CurrentUser.userId 隔离,拒绝客户端注入 userId;
  • 全局异常不泄漏堆栈;日志敏感数据脱敏;
  • 合规上定位"工具 > 效率/生活服务"类目,文案避免诊断/医疗建议,隐私指引与 wx.requirePrivacyAuthorize 完整接入。

9.6 部署要"一键可回滚"

rollout.sh 把备份、重启、健康检查、回滚串成一条命令,配合 systemd 自恢复(Restart=on-failure)。对个人开发者而言,能在 2 分钟内把坏版本退回去,比追求零停机更重要。


十、写在最后

情绪小恐龙 不是一个追求技术炫酷的项目,它更像一个"一个人在约束下把事情做对"的样本:本地优先的产品理念、文档先行的开发纪律、用静态审计补位测试、用最小改动拿最大收益、把安全与隐私当默认值。

对同样在独立做小程序的开发者,我的建议只有一句:先想清楚数据怎么存、怎么同步、怎么回滚,再写第一行页面代码------因为后面所有功能,都会建立在这套地基之上。

项目已在 Gitee 开源,有兴趣的朋友可以私信联系获取地址

相关推荐
m0_587383001 小时前
折扣卡CPS软件开发实战:从系统架构设计到上线指南
java·小程序·架构·需求分析
mykj15511 小时前
赛事报名小程序系统:一站式解决赛事管理难题
小程序·app开发·体育赛事报名小程序·赛事app
EatFan3 小时前
Java接入微信支付保姆式教程(一):支付流程、商户号与环境准备
小程序·微信支付·jsapi支付
00后程序员张7 小时前
怎么用 Egret(白鹭引擎)打包 iOS 应用并上架 App Store?
android·ios·小程序·https·uni-app·iphone·webview
Bs_MoneyMagnet8 小时前
基于springboot+vue的旅游行程分享与推荐小程序的设计与实现 源码+文档
java·vue.js·spring boot·后端·微信小程序·毕业设计·计算机毕业设计
西木风落8 小时前
业余发展——零后端微信小程序口算练习实战
微信小程序·vibe coding·口算小达人
xujuzheng9 小时前
2026深圳小程序/App/AI智能体开发公司选型指南(附本地服务商盘点)
数据库·科技·微信小程序·小程序·uni-app
盟道科技20 小时前
消息队列消费端幂等性实战:订单场景下重复消息不重复扣款的完整方案
小程序
毕业设计70320 小时前
(免费领源码) 基于微信小程序的预制菜商城的设计与实现25172-java、PHP、python、C#、小程序、大数据、单片机、网络工程等)
vue.js·python·mysql·微信小程序·pycharm·微信开发者工具·推荐算法