资讯动态

从零数据到可复盘:心情记录与习惯打卡的数据模型设计

发布时间:2026/8/22 1:53:51 来源:尧图企业网站定制
为什么不把所有内容塞进“每天一条记录”最初设计心情日记时一个直观模型是interfaceDayRecord{date:string;mood:string;note:string;tags:string[];habits:string[];}它适合原型但功能增加后会遇到问题习惯改名后历史记录是否全部重写习惯归档后历史打卡如何保留没有心情、只有习惯打卡的日期如何出现删除一个习惯时哪些历史数据应该删除统计习惯创建之前的日期是否合理因此“心晴手记”把数据拆成三个独立实体MoodEntry Habit HabitCompletion标签目前是稳定枚举作为MoodEntry.tags中的 Key 保存。一、MoodEntry每天唯一的心情记录exportinterfaceMoodEntry{id:string;timestampMillis:number;dayIdentifier:string;mood:MoodKind;note:string;tags:string[];createdAtMillis:number;updatedAtMillis:number;}几个时间字段分别承担不同职责dayIdentifier业务日期主键如2026-08-14timestampMillis最近一次记录的时间可用于排序createdAtMillis首次创建时间updatedAtMillis最后更新时间。保存今日记录时执行 Upsertconstexisting:MoodEntry|undefinedthis.todayEntry();if(existing){constupdated:MoodEntry{id:existing.id,timestampMillis:now,dayIdentifier:id,mood:this.selectedMood,note:this.journalNote,tags:this.selectedTags.slice(),createdAtMillis:existing.createdAtMillis,updatedAtMillis:now};this.moodEntriesthis.moodEntries.map((entry)entry.idexisting.id?updated:entry);}else{// 创建新记录}关键不变量是同一个 dayIdentifier 最多一条 MoodEntry它应该由业务层持续保证而不是只依赖 UI 上“今天已经保存”的提示。二、为什么同时保存 ID 和日期主键既然每天唯一能否直接把dayIdentifier当作 ID可以但独立 ID 仍有价值未来可能允许一天多条记录导入数据时可以保留记录身份更新对象时不必改变引用标识可以把日期从身份中解耦支持纠正错误日期。当前使用本地生成 IDexportfunctionnewId():string{constrandom:stringMath.floor(Math.random()*1000000000).toString(36);return${Date.now().toString(36)}-${random};}它适合单设备轻量应用。如果未来多设备合并数据应使用冲突概率更低的标准 UUID并建立同步冲突策略。三、Habit定义目标本身而不是某天是否完成exportinterfaceHabit{id:string;name:string;icon:string;color:string;isActive:boolean;createdAtMillis:number;updatedAtMillis:number;archivedAtMillis:number;}习惯实体保存的是名称、图标、颜色和生命周期。“今天是否完成”不放在 Habit 中否则只能表达一个当前状态无法保存历史。isActive与archivedAtMillis同时存在isActive便于今日页快速筛选archivedAtMillis让统计知道从哪一天停止计入分母。归档时不删除 Habitreturn{...habit,isActive:active,updatedAtMillis:now,archivedAtMillis:active?0:now};因此历史仍可以显示曾经的习惯和打卡。四、HabitCompletion关联习惯与自然日exportinterfaceHabitCompletion{id:string;habitId:string;timestampMillis:number;dayIdentifier:string;isCompleted:boolean;createdAtMillis:number;updatedAtMillis:number;}当前业务采用“存在即完成”的简化模型找得到记录 → 已完成 找不到记录 → 未完成唯一条件是habitId dayIdentifier切换打卡时constexistingthis.completions.find((completion:HabitCompletion)completion.habitIdhabit.idcompletion.dayIdentifierdayId);if(existing){this.completionsthis.completions.filter((completion)completion.id!existing.id);}else{this.completions[completion,...this.completions];}模型中仍保留isCompleted为兼容旧数据或未来扩展留出空间。若未来需要记录“跳过”“部分完成”可以升级为明确状态枚举而不是继续依赖布尔值。五、历史日期为什么要合并两类来源用户可能某天只完成了习惯没有记录心情。如果历史列表只遍历MoodEntry这一天会完全消失。项目从两类数据合并日期privatehistoryDayIds():string[]{constids:string[][];this.moodEntries.forEach((entry:MoodEntry){if(!ids.includes(entry.dayIdentifier)){ids.push(entry.dayIdentifier);}});this.completions.forEach((completion:HabitCompletion){if(completion.isCompleted!ids.includes(completion.dayIdentifier)){ids.push(completion.dayIdentifier);}});returnids.sort((left,right)right.localeCompare(left));}这说明“历史中的一天”是一个派生视图不必在磁盘中再维护第四张 Day 表。六、删除和归档必须是两种不同语义用户管理习惯时至少需要归档从今日列表移除保留 Habit保留历史打卡统计只计算归档前的有效日期将来可以恢复。删除删除 Habit删除所有关联 HabitCompletion不可恢复必须二次确认。删除实现privatedeleteHabit(habitId:string):void{this.habitsthis.habits.filter((habit:Habit)habit.id!habitId);this.completionsthis.completions.filter((completion:HabitCompletion)completion.habitId!habitId);this.persist();}如果只删 Habit不删关联打卡就会产生孤儿数据历史统计仍可能计算到一个 UI 中已经找不到的习惯。七、统计分母来自 Habit 生命周期一个习惯在统计窗口内的可参与日期必须满足日期 习惯创建日 且 习惯未归档或 日期 归档日这使新建习惯不会被追溯惩罚归档习惯也不会在之后持续拉低完成率。模型中的createdAtMillis和archivedAtMillis因此不只是审计字段它们直接决定指标含义。如果未来允许“暂停习惯再恢复”单个archivedAtMillis就不够了需要增加生命周期区间或状态变更事件表。八、标签为什么暂时不是独立实体当前标签是固定的八种情境工作、学习、睡眠、运动、人际、家庭、天气和放松。它们使用稳定 KeyexportinterfaceTagDefinition{key:string;resourceName:string;icon:string;color:string;}MoodEntry 只保存tags:string[]展示时根据 Key 获取当前语言资源。固定标签没有用户自定义生命周期因此暂时不需要单独的 Tag 实体和关联表。如果未来支持自定义标签、改名、归档或合并才需要升级为Tag MoodEntryTag不要为了“数据库看起来正规”提前拆分也不要在需求已经变化后继续硬塞字符串。九、默认习惯使用稳定存储名内置默认习惯需要随应用语言显示但磁盘中的数据不能在切换语言时反复改名。因此默认习惯保存稳定名称storedName:sleep-early展示时通过resourceName本地化。用户自定义名称则直接保存和显示。这种设计把“业务身份”和“用户看到的翻译”分开也让历史数据在中英日切换时保持一致。十、根状态和导出格式要版本化所有实体最终进入exportinterfacePersistedState{exportVersion:number;settings:AppSettings;moodEntries:MoodEntry[];habits:Habit[];habitCompletions:HabitCompletion[];}新增字段时读取旧状态要提供默认值改变引用关系时需要显式迁移导出 JSON 中也保留exportVersion。一个可靠的数据模型不仅描述“今天怎么保存”还必须回答“明年版本升级后旧记录怎么继续使用”。最终关系图MoodEntry ├─ dayIdentifier每天唯一 ├─ mood稳定枚举 └─ tags[]稳定标签 Key Habit ├─ id习惯身份 ├─ isActive当前是否显示 └─ createdAt / archivedAt统计生命周期 HabitCompletion ├─ habitId → Habit.id └─ dayIdentifier完成日期 唯一约束habitId dayIdentifier总结这套模型的核心不是用了几个 interface而是明确了四条不变量每个自然日最多一条心情记录每个习惯每天最多一次有效打卡归档保留历史删除级联清理统计只覆盖习惯真实存在的日期。一旦这些边界稳定今日页、历史页、统计页、导出和多语言都可以围绕同一套数据语义工作。本文案例来自“心晴手记MoodMemoir”HarmonyOS 版的数据模型。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价