第八期 · 情绪闯关挑战盒:一个「无惩罚」成就系统的数据建模
第六期验证了注册表驱动的可扩展第七期补上了「本地常驻」这种不出站的数据角色。第八期要回答一个更「软」的问题——怎么做一个游戏化功能才能不制造焦虑答案是把它当成一道数据建模题来解。本文记录「情绪闯关挑战盒」的落地过程。关键词派生而非存储 · 跳过一等公民 · 单一事实源 · 能力探测降级 · 前后端错峰上线目标读者前端 / 后端 / 产品一、为什么「连续打卡天数」是危险的设计大多数习惯类产品靠 streak连续天数留人断一天火焰熄灭一切归零。它对「要不要继续用」的激励极强但对情绪类产品制造的是另一种东西——愧疚。用户状态不好、连着几天没打开回来看到「你已中断 3 天」第一反应不是重新开始而是「我已经失败了」然后卸载。挑战盒立了三条反直觉的规矩常规游戏化挑战盒连续天数、断卡惩罚累计次数 徽章永不归零完成率、击败 X% 用户无排行榜、无百分比「你已离开 X 天」召回回来永远只显示「欢迎回来」这三条不是贴在文案里的承诺而要落到数据模型上——于是有了本期最核心的决定进度与徽章是派生数据一个字段都不存。二、派生而非存储徽章进度不落库如果给徽章建一张「成就表」存badge_id / earned / progress / unlocked_at会立刻冒出三个问题前后端各维护一份进度、需要额外的同步与冲突仲裁、以及最致命的——进度是「连续天数」这类不可派生状态的温床。挑战盒反着来。em_challenge域只存一样东西// emotion-monster/utils/storage.jsfunctiondefaultChallenge(){return{records:[],swapCount:0};// records: 打卡/跳过记录swapCount: 换一个计数纯本地}徽章与统计全部从records实时算出。前端calc.challengeSummary是纯函数输入 records、输出 weekCount / totalCount / byStar / 徽章列表判定规则本身就是数据不是代码分支// emotion-monster/utils/constants.js —— CHALLENGE_BADGES{id:emotion_container,name:情绪容纳者,icon:wave,series:allow,rule:{kind:doneStar,star:2,min:10}}// 二星任务累计完成 10 次doneTotal / doneStar / doneTask / skipTotal / swapCount / reunion六种规则覆盖全部徽章判定逻辑十几行。好处是结构性的不存在「进度字段过期」——删一条记录进度自动回退无需维护缓存一致性不存在「前后端进度不一致」——两边用同一份 records、同一套规则不存在「连续天数」——records 里根本没有 streak 字段从数据模型层面消灭断卡焦虑。代价只是每次进觉察册做一次内存聚合。用户一年最多几千条记录这个量级可以忽略。三、跳过是一等公民status 只有两种传统打卡系统里未完成是「缺失」跳过是「失败」。挑战盒把两者合并成同一条记录的两种取值// emotion-monster/utils/adapters/challenge-local.js —— create 的落库status:p.status,// done | skip「跳过」不是不做记录而是写入一条status: skip的记录。两个后果当日状态是「休息」而不是「空缺」。首页今日状态三态已完成 / 今天休息 / 还没打开休息与完成视觉权重相同——休息不是空白。休息本身可被统计、被奖励。徽章「休息知晓者」的条件就是skip ≥ 15「十五次你诚实地对自己说了’今天不行’。听懂自己的’不想做’也是一种觉察。」这是本期最想强调的一点接纳不是口头禅而是把「不想做」建模成合法数据并给予正向反馈。用户不做的权利在数据层和交互层同时被尊重。跳过还必须零成本无二次确认弹窗、不扣进度、不弹挽留。交互上的「跳过无确认」对应数据上的「跳过不惩罚」——同一条规矩的两面。四、徽章判定前后端同构 单一事实源徽章这件事上前后端的分工又有微妙不同。进度判定必须在两端各算一遍游客态走本地calc.challengeSummary登录态走服务端聚合。但展示字段名称、图标、文案、分组若各存一份就是前后端双份文案漂移——本次开发真实踩过后端没同步升级时前端拿到的徽章缺icon字段图标全渲染成了默认的「返回箭头」。结论是展示字段收口到前端单一事实源后端summary只返回进度[{id, earned, progress}]id是前后端约定的关联键前端用CHALLENGE_BADGES字典按id补齐name / emoji / icon / series / desc。// mood-backend/.../service/ChallengeService.java —— buildBadges 只算进度不碰文案addBadge(out,emotion_container,countStar(done,2)10,countStar(done,2),10);// emotion-monster/utils/adapters/remote-adapter.js —— 云端进度 前端字典补齐展示constdefCHALLENGE_BADGES.find((d)d.idb.id);constmergeddef?Object.assign({},b,{name:def.name,emoji:def.emoji,icon:def.icon,series:def.series,desc:def.desc}):b;「算法两端同构、文案一端维护」——对第五期「同构纪律」的一次细化同构的是计算不是展示。五、能力探测与降级新模块如何与旧后端错峰共存小程序要审核、用户要手动更新前端上线天然滞后于后端反过来后端也可能因故障回滚。于是出现经典组合新前端 旧后端。对挑战盒这是致命的新前端把module:challenge推给旧后端的/sync/push旧分发逻辑不认识 challenge会掉进else兜底——当成 mood 记录写进去产生脏数据。两层防御后端给 challenge 加独立分支绝不复用兜底// mood-backend/.../service/SyncService.java —— push 的分发}elseif(challenge.equalsIgnoreCase(change.module())){challengeService.applyChallenge(userId,change.action(),change.record(),clientId);}else{applyMood(userId,change,clientId);// 兜底只留给 mood}前端写之前先「能力探测」调一次GET /challenges/today返回 404/405 说明后端未就绪就把记录降级落本地、标记待同步不推/sync/push探测结果缓存后端恢复后走正常路径。// emotion-monster/utils/adapters/remote-adapter.js —— ensureChallengeSupportedreturnhttp.get(/challenges/today,{date:DateUtil.today()}).then((){this._challengeSupportedtrue;returntrue;}).catch((e){if(e(e.statusCode404||e.statusCode405)){this._challengeSupportedfalse;// 后端未就绪降级本地绝不推云returnfalse;}returntrue;// 网络错误走正常推送失败入离线队列});能力探测把一个潜在的脏数据事故降级成了一次本地降级。六、Schema v2 → v3第七期那个坑再来一遍新增域要改migrate()但真正的坑在initStore()的首次启动判空——第七期已经总结过。挑战盒照搬这套经验这次几乎没有新坑// emotion-monster/utils/storage.js —— migrateif(from3){db.challengedb.challenge||defaultChallenge();}challenge域独立加载、缺失即回退默认不参与核心结构判空dirty与flush()补上 challenge否则置脏后 flush 早退、数据永远停在内存backupGuest()快照带上 challenge合并失败可回滚。第七期交的学费这期直接变成了 checklist。七、验证清单老用户v1/v2升级后 challenge 域补齐、无 INIT_FAILED历史数据不丢游客态抽盒、完成、跳过全可用断网写入进离线队列联网重放后端未就绪404时写 challenge 降级本地、不推云、不产生 mood 脏数据同日多次打卡幂等「换一个」计数仅本地、不上云徽章进度本地/云端一致同一份 rules、同一份 recordsreduceMotion下无缩放动画日记默认剥离不上云。小结三条工程原则派生优于存储。能算出来的就别存——它自动解决一致性、过期、迁移三件套把「不做什么」也建模。skip 是数据不是失败接纳才有落点而不是停在文案新能力上线先想怎么降级。能力探测不是锦上添花是保护旧数据的底线。

相关新闻

从泊松噪声到张正友标定:相机噪声模型与几何标定实战指南

从泊松噪声到张正友标定:相机噪声模型与几何标定实战指南

1. 从一张“脏”图说起:为什么你的相机需要被“标定”如果你拍过暗光照片,或者用工业相机做过测量,大概率遇到过两种让人头疼的情况:一是画面里莫名其妙的噪点,二是明明对着标定板拍,算出来的尺寸却总差那么…

2026/10/14 18:55:11 阅读更多 →
Android Q MTK Camera HAL3 Search Sensor机制与实战排查

Android Q MTK Camera HAL3 Search Sensor机制与实战排查

做 Camera 的老司机都清楚一个道理:预览虚、曝光乱、对焦飘,最后多半能在 Sensor 这一层找到蛛丝马迹。Android Q 的 MTK Camera Hal3 Search Sensor,就是这条请求链路的最前端。之前我跟着调一个模拟项目里的前摄点亮任务,发现不…

2026/10/14 18:55:11 阅读更多 →
CLIP-ReID:用视觉语言对齐重构行人重识别范式

CLIP-ReID:用视觉语言对齐重构行人重识别范式

简介:这是一套面向计算机视觉研究者与深度学习工程师的行人与车辆重识别(ReID)开源实现方案,基于CLIP多模态预训练思想构建统一框架,有效解决跨摄像头目标身份匹配中的外观变化、遮挡与视角差异等核心挑战。资源共77个…

2026/10/14 18:54:09 阅读更多 →

最新新闻

Vue项目实现Office文档在线预览:DOCX、XLSX、PPT一次搞定

Vue项目实现Office文档在线预览:DOCX、XLSX、PPT一次搞定

做前端这几年,被文档预览折磨的次数真不算少。之前接一个合同管理需求,用户提了个特别朴素的要求:附件里的Word合同不要下载到本地再打开,在网页里直接能看就行。我一开始也心想,这有什么难的?不就是嵌一个…

2026/10/15 5:37:22 阅读更多 →
C00系统PLC梯形图维修指南:钻攻中心信号排查与修改实战

C00系统PLC梯形图维修指南:钻攻中心信号排查与修改实战

简介:面向日本兄弟钻攻中心C00系统的操作与维护人员,这是一份系统讲解PLC(可编程逻辑控制器)应用的官方技术手册,内容由浅入深,特别适合现场工程师和培训学员快速掌握设备控制逻辑。手册从PLC基础概念与系统…

2026/10/15 5:37:22 阅读更多 →
PyTorch核心机制与实战:动态图、自动微分、显存优化及多卡训练

PyTorch核心机制与实战:动态图、自动微分、显存优化及多卡训练

1. 从一次模型训练卡死说起:为什么值得重新认识PyTorch很多人第一次接触深度学习框架,都是从一行import torch开始的。但真正让PyTorch在众多框架中站稳脚跟的,不是它简洁的API,而是它背后那套"动态图自动微分"的组合拳…

2026/10/15 5:37:22 阅读更多 →
PyTorch深度学习实战:从张量到分布式训练与部署全链路指南

PyTorch深度学习实战:从张量到分布式训练与部署全链路指南

1. 从一张显卡到一套完整训练链路:我为什么把PyTorch当作深度学习的主力框架如果你刚接触深度学习,或者已经用过其他框架但总觉得“跑得起来却调不明白”,那PyTorch大概率是你绕不开的一站。它不是一个“点一下就能出结果”的黑盒工具&#x…

2026/10/15 5:37:22 阅读更多 →
兄弟钻攻中心C00系统PLC手册:地址、梯形图与维修实战

兄弟钻攻中心C00系统PLC手册:地址、梯形图与维修实战

简介:日本兄弟钻攻中心C00系统PLC系统手册是针对S500X1/S700X1机型的技术文档,专供经过认证或培训的操作与维护人员使用,用于掌握钻攻中心PLC程序编辑、调试与故障排查。内容从PLC基础概念、系统连接示意图讲起,再进入PLC功能操作…

2026/10/15 5:37:21 阅读更多 →
LLM request timed out排查指南:OpenClaw接入第三方API超时解决方案

LLM request timed out排查指南:OpenClaw接入第三方API超时解决方案

LLM request timed out 这个报错,接第三方 API 的朋友应该不陌生。OpenClaw 本身是个开源智能体框架,跑起来很轻,但一旦把模型源切到第三方兼容接口,超时就像定时炸弹一样时不时爆一下。这篇文章把我自己调通的全过程写出来&#…

2026/10/15 5:36:21 阅读更多 →

日新闻

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

1. 为什么选择容器化方式部署 LoRa 网络服务LoRa 物联网项目落地时,网络服务器是整个链路的核心枢纽。它负责接收网关上报的数据包、完成去重与解密、再把有效载荷分发给上层应用。早期大家习惯把网络服务器直接装在物理机上,依赖一堆系统库和特定版本的…

2026/10/15 0:00:46 阅读更多 →
共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

1. 共享推理服务里,扩缩容到底难在哪做过大模型推理服务部署的人,大概都经历过这样的场景:一个团队共用一套推理集群,A 业务跑的是 7B 的小模型,B 业务跑的是 72B 的大模型,C 业务白天流量高、晚上几乎归零…

2026/10/15 0:00:46 阅读更多 →
Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

1. 引子:一次内核恐慌引发的追查先从一个真实场景说起。某次我给一台老旧的嵌入式设备折腾系统裁减,为了把启动时间压进两秒以内,我手贱地把一串自认为“没用”的 systemd 服务给 disable 了,其中就包括systemd-remount-fs.servic…

2026/10/15 0:00:46 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/14 16:30:29 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 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/15 0:14:36 阅读更多 →
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/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练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/14 7:53:34 阅读更多 →