资讯动态

HarmonyOS智能日记实战:本地情绪分析、关键词提取与数据驱动设计

发布时间:2026/10/20 22:30:53 来源:尧图企业网站定制
做日记App这件事很多开发者的路径都差不多先做一个能打字、能存图片的本地记事本然后越用越觉得不对劲——记是能记了但想翻出三个月前某件小事的时候费半天劲写的时候没有灵感忘记记了也没人提醒。这次我把自己的HarmonyOS日记应用从“简单记录”往“智能日记”方向做了一次完整的功能深化核心就是把三类能力塞进一个个人工具里情绪分析自动打标、关键词抽取做关联检索、定时回顾推送和周统计报表。这篇实践笔记会从数据模型、轻量算法选型、ArkUI交互改造一路讲到实际踩过的坑适合正在做HarmonyOS应用、或者想把本地工具型App做得更“通人性”的开发者参考。1. 整体设计思路先想清楚“智能”到底解决什么问题做功能深化之前我先把老版本的使用体验复盘了一遍。旧版就是典型的“哑存储”打开App、写一段话、配张图、保存然后这条记录就躺在列表里再也不被翻到。问题不是“能不能记”而是记完之后这套系统有没有给用户提供任何额外价值。所以这次深化的目标很明确让应用理解内容、主动出现、定期给用户一个回顾生活的窗口。1.1 从“记录”到“智能”的差距在哪里普通日记和智能日记之间隔了三层能力这三点也是我这次改造的主线第一内容理解。纯文本对应用来说只是一个字符串不知道用户今天心情如何、写了什么主题、有哪些反复出现的词汇。要“智能”第一步就是把日记内容解析成结构化信息情绪分、关键词标签、字数、时间分布。第二主动服务。老版本的App自己不“开口”用户不打开就什么都没发生。深化之后应用应该在晚上某个时间提醒你“今天还没写”或者在某个特殊日子把去年的今天翻出来给你看。第三周期整理。日记的价值有一部分在复盘。按时间轴翻是线性阅读但人更想要的是“这周我总体开心吗”“最近总在聊什么话题”。这需要应用有能力在后台做聚合统计而不是每次都全量扫描。想清楚这三点之后功能清单就顺理成章了情绪打标、关键词标签、每日提醒、往年今日、周报统计。1.2 功能深化前先想清楚的三件事确认要做哪些功能之后我给自己立了三条原则现在看来每一条都帮了大忙。首先是离线优先。日记是非常私密的数据用户对它的安全预期远高于普通应用。如果把正文上传到云端做分析不管是出于什么理由都会劝退一批用户。所以我把情绪分析和关键词抽取全部放在本地执行一个字不出设备。这个决定也让应用在网络差、没网的情况下依然完整可用反而成了卖点。其次是克制加功能。我见过不少工具型App死于功能堆砌。这次每个候选功能上线前都问一个问题这功能到底降低了用户的哪一项操作成本比如自动打标签降低了“手动分类”的成本周报降低了“回看”的成本但像“好友排行榜”这种功能属于社交属性和日记的私密调性冲突直接砍掉。第三是数据结构先行。智能功能最怕的就是回头补数据。老版本只有content和date两个字段这次建表时我直接把mood、tags、word_count全部预留好。宁可先空着也不能等到用户写了几百条之后再做迁移那才是真正的灾难。1.3 技术栈与整体架构技术选型没有追求花哨全部围绕HarmonyOS现有能力展开开发语言用ArkTSUI层用ArkUI数据层用Preferences加关系型数据库RelationalStore。智能分析模块没有接任何云端大模型SDK而是自研了一套轻量词库算法原因后面专门讲。后台提醒走系统提醒代理耗时分析走任务池并发。整体分四层视图层是ArkUI组件状态层用State、Observed这类装饰器管理界面数据业务层拆成日记服务、分析服务、统计服务三个模块数据层统一封装RdbStore的增删改查。把分析逻辑单独抽成服务是有意的未来如果真的想换成能力更强的模型只需要替换分析服务内部实现数据层和UI层完全不用动。2. 数据层设计存储选型、建表和升级功能深化最怕地基歪。这章我把数据层从头讲一遍包括为什么选关系型数据库、表结构每个字段的来龙去脉、以及升级时怎么平稳过渡这些都是直接能抄的作业。2.1 存储方案怎么选HarmonyOS应用有几种常见本地存储手段我当时画了个对比结论很清晰存储方案适合场景为什么不作为日记主存储Preferences 键值存储用户设置项、上次同步时间、小体量配置结构弱无法按日期、情绪、标签做筛选和聚合数据一多就难维护RelationalStore 关系型数据库条目型业务数据需要SQL查询、排序、索引——最终选它分布式键值数据库多设备同步轻量数据更适合同步配置和摘要全文同步会遇到数据量和冲突处理的复杂度日记的每条记录天然是“一行数据”而列表页、时间轴、周报、情绪趋势这些功能本质都是对数据做条件查询和聚合运算。关系型数据库的SQL能力和索引机制做这件事最合适。Preferences虽然用起来简单但它在逻辑上是扁平键值对想表达“某月所有日记的平均情绪值”这种查询非常别扭硬写能写代价是代码可读性和性能一起崩。另外我在项目里仍然保留了Preferences用来存少量设置项比如每日提醒时间、用户是否同意过通知权限、上次数据统计的时间戳。它和RDB各管一段分工明确。2.2 日记数据模型与建表细节数据模型是这次深化最关键的一步。建表SQL如下字段解释放在后面CREATE TABLE IF NOT EXISTS diary ( id INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT NOT NULL, date_text TEXT NOT NULL, mood INTEGER DEFAULT 0, tags TEXT DEFAULT [], image_uris TEXT DEFAULT [], word_count INTEGER DEFAULT 0, create_time INTEGER NOT NULL, update_time INTEGER NOT NULL ); CREATE INDEX IF NOT EXISTS idx_diary_date ON diary(date_text); CREATE INDEX IF NOT EXISTS idx_diary_mood ON diary(mood);每个字段都有具体考虑。content存正文date_text用“YYYY-MM-DD”这种格式化字符串而不是直接用时间戳原因是“按天/按月归类”变成纯字符串范围查询简单直观而且不受时区换算影响。create_time和update_time用长整型毫秒时间戳负责精确排序和“最近修改”逻辑。mood不是布尔或简单的三档枚举而是存一个-10到10的整数情绪分交给你后面章节的情绪分析模块来填。之所以保留细粒度分值而不是直接存“开心/难过”是为了周报里能算“平均情绪值”。tags字段存JSON数组字符串比如“[工作,加班,地铁]”。有人会问为什么不用一张独立的标签关联表我的实际判断是个人日记一年也就几百条JSON字符串在读写上更简单查询时用接口里的“包含”匹配或反序列化后内存过滤都够快真到几万条再拆表为时未晚。word_count是冗余字段每次保存时算好字数存进去。周报插件聚合每天字数时直接SUM这一列避免在统计时对几百条全文做扫描。这一类“预计算字段”是本地统计场景里性价比最高的优化。image_uris同理JSON数组存图片URI列表页不需要每次读取原图元数据就知道有几张图。索引方面date_text建索引是为了时间轴分页查询mood建索引是为了情绪趋势筛选。注意一点tags这类JSON字符串列不适合建普通索引因为查询走的是包含匹配索引帮不上忙这种字段保持普通存储就好。2.3 分页查询与数据库迁移列表页的数据读取不能一次全查数据多了之后Ano崩溃。我用的分页方式很朴素// 示意按时间倒序分页 let predicates new relationalStore.RdbPredicates(diary); predicates.orderByDesc(date_text); predicates.limit(20, page * 20); store.query(predicates, [id, content, date_text, mood, tags, image_uris], (err, resultSet) { // 处理结果集 });初始阶段每页20条足够配合滚动到底部加载下一页。顶部再做一个月末筛选查某个月的日记predicates.between(date_text, 2025-03-01, 2025-03-31)直接走索引。数据库迁移是另一个必须提前准备的问题。RDB有版本号机制每次调整表结构版本号加1系统会触发对应的升级回调。我当时的处理是if (oldVersion 2) { store.executeSql(ALTER TABLE diary ADD COLUMN mood INTEGER DEFAULT 0); store.executeSql(ALTER TABLE diary ADD COLUMN tags TEXT DEFAULT \[]\); store.executeSql(ALTER TABLE diary ADD COLUMN word_count INTEGER DEFAULT 0); store.executeSql(ALTER TABLE diary ADD COLUMN image_uris TEXT DEFAULT \[]\); }这里有个重要提醒ALTER TABLE能加列但改不了已有列的类型也删不了列。如果你在后续版本里想把mood从INTEGER改成TEXT就必须走“建新表、拷贝数据、删旧表、重命名”这条完整迁移流程。迁移操作请务必包在事务里并在开始前提示用户备份数据别问我为什么知道。3. 智能功能落地情绪分析、自动打标与回顾提醒这一章是全文的技术重点。我详细讲清每一个算法模块的思路、代码和设计取舍包括为什么坚持本地轻量方案以及如何保证这些功能在不同型号设备上都跑得动。3.1 情绪分析不依赖云端也能跑情绪分析我没有接云端大模型而是用了一个经过扩充的情绪词库加规则打分。原因有三第一日记内容必须是私密的上传分析很多人不接受第二本地词库分析毫秒级完成体验远好于网络请求的等待第三词库方法完全可解释用户可以看着这个词表知道应用凭什么判断心情大模型则是个黑盒。核心思路很简单准备一张积极词表、一张消极词表每个词带一个权重分。扫描正文时每命中一个词就把对应分值累加同时处理否定词和程度副词。否定词会把情绪取反程度副词会把分值放大或缩小。最后截断到-10到10之间const positiveWords: Mapstring, number new Map([ [开心, 2], [高兴, 2], [满足, 2], [感谢, 3], [顺利, 2], [爱, 3] ]); const negativeWords: Mapstring, number new Map([ [难过, 2], [失望, 3], [焦虑, 3], [生气, 2], [累, 1], [烦, 1] ]); const negators: string[] [不, 没, 未, 别]; const intensifiers: Mapstring, number new Map([ [非常, 1.5], [特别, 1.5], [有点, 0.6] ]); export function analyzeMood(text: string): number { let score 0; // 去掉标点便于按字符窗口扫描 const cleaned text.replace(/[。、,.!?\s]/g, ); for (let i 0; i cleaned.length - 1; i) { const word cleaned.substring(i, i 2); let w positiveWords.get(word) ?? negativeWords.get(word) ?? 0; if (w 0) continue; // 否定词前一个字符是否定词则取反 if (i 0 negators.includes(cleaned.charAt(i - 1))) { w -w; } // 程度副词前一个字符是程度词则调整权重 if (i 0 intensifiers.has(cleaned.charAt(i - 1))) { w Math.round(w * intensifiers.get(cleaned.charAt(i - 1))!); } score w; } return Math.max(-10, Math.min(10, score)); }这段代码用的是双字符滑窗对中文二字词很有效但对“不开心”这类三字表达滑窗会先命中“开心”再被否定词“不”取反逻辑上也说得通。实际调参时我发现把词表的权重差异拉大比堆词量更有效高频基础情绪词开心、难过、累权重落在1到2之间强烈情绪词绝望、惊喜给到3避免一段话里三四个情绪词就冲爆分数。保存日记时把mood写进数据库。列表页用颜色区分正分绿色系、负分红色系、零分灰色系。周报里再算平均分转成“本周心情指数”。3.2 关键词提取与自动打标自动打标本质是“抽取式关键词”不需要生成新词只需要从原文里挑出最值得作为标签的词汇。HarmonyOS端不方便直接引入重量级分词库我采用的是“正向最大匹配 词频统计”的轻量方案。正向最大匹配需要一个内置词表。我手工维护了一个几百词的常用词表覆盖生活、工作、天气、运动、饮食等常见主题用户还能在设置页把自己常用的词加进去。算法从文本开头尝试取出最长的词表命中词跳过分词结果中的停用词最后按出现频次排序取前几个const DICT: Setstring new Set([ 今天, 天气, 周末, 项目, 加班, 地铁, 朋友, 生日, 跑步, 旅行 ]); const STOP_WORDS new Set([的, 了, 是, 我, 你, 在, 和, 也, 都]); function forwardMatch(text: string): string[] { const words: string[] []; let i 0; const maxLen 4; while (i text.length) { let matched ; for (let len maxLen; len 0; len--) { if (i len text.length DICT.has(text.substring(i, i len))) { matched text.substring(i, i len); break; } } if (matched) { words.push(matched); i matched.length; } else { i; } } return words; } export function extractTags(text: string, topN 3): string[] { const freq: Mapstring, number new Map(); for (const w of forwardMatch(text)) { if (STOP_WORDS.has(w)) continue; freq.set(w, (freq.get(w) ?? 0) 1); } return [...freq.entries()] .sort((a, b) b[1] - a[1]) .slice(0, topN) .map(e e[0]); }这个方案的优点是快几百字正文分析在本地跑基本无感。缺点也明显词表外的词识别不到。比如用户写“去咖啡馆坐了一下午”词表里没有“咖啡馆”就提取不出这个标签。我的缓解办法是在设置页提供“自定义词库”入口同时把提取结果展示在编辑页让用户确认允许手动增删标签。自动生成的内容永远只是草稿最终判断权在人手里这一点做工具类产品很重要。标签存到tags字段后搜索逻辑就分两层先按标签精确检索再看内容LIKE匹配。这样既快又准比全文扫描强得多。3.3 每日提醒、往年今日与周报统计智能日记不能只在用户打开应用时才有智能。这部分的实现我拆成三个小功能。每日提醒走的是系统提醒能力而不是单纯的后台定时任务。原因是系统级提醒可以在应用不处于前台、甚至进程被回收后依然按时间触发稳定得多import { reminderAgentManager } from kit.BackgroundTasksKit; // 示意代码模块路径与参数以当前SDK版本为准 let reminder: reminderAgentManager.ReminderRequestAlarm { reminderType: reminderAgentManager.ReminderType.REMINDER_TYPE_ALARM, title: 日记时间, content: 今天发生了什么值得记的事花两分钟写下来。, hour: 21, minute: 0, daysOfWeek: [1, 2, 3, 4, 5, 6, 7] }; reminderAgentManager.publishReminder(reminder, (err, reminderId) { // 保存reminderId下次先取消旧的再发布 });这里有个非常容易踩的坑每次启动应用都去重复发布提醒会生成一堆相同的通知。正确做法是把reminderId存到Preferences里发布前先检查存在就取消旧的再重新发布。“往年今日”是体验提升最明显的一个功能。实现上就是在应用启动或用户点击回忆入口时用当天日期构造一个查询取date_text中间部分等于当前月日的数据展示成“去年的今天你写了这些”。比如今天是3月18日就筛date_text以“-03-18”结尾的记录。这个查询用LIKE %-03-18就能实现走不上索引但数据量小完全够用。周报统计则是把一周的数据做一次聚合。查询本周所有记录后在内存里计算平均情绪分、总字数、每日写没写连续打卡天数、出现最多的标签。展示层不需要复杂图表我用了一组统计卡片加进度条情绪分对应一个笑脸连续打卡天数对应一个数字标签用普通的流式布局。用户看到“你这周记录了4天平均心情指数6.2最多出现的话题是工作”的时候是真的会感受到这个应用不一样了。4. UI交互深化时间轴、卡片与分析联动智能能力有了但用户首先接触的是界面。这章讲界面怎么从“列表盒子”变成“有温度的时间轴”以及编辑器里智能分析的实时联动。4.1 从列表到时间轴卡片老版本是标准的列表行标题、摘要、日期。看久了会发现一个问题——日记是很强调“时间感”的内容一行行的平铺完全抹掉了这种感受。改成时间轴之后每条记录变成一张卡片卡片左侧有一条竖线和节点圆点视觉上就是在一条时间线上流动。实现上不复杂ArkUI的List加ListItem就能做Entry Component struct DiaryTimelinePage { State diaries: DiaryItem[] []; build() { List({ space: 12 }) { ForEach(this.diaries, (item: DiaryItem) { ListItem() { Row() { // 时间轴节点和竖线 Column() { Circle({ width: 10, height: 10 }) .fill(item.mood 0 ? #4CAF50 : (item.mood 0 ? #F44336 : #BDBDBD)) Divider() .strokeWidth(2) .color(item.mood 0 ? #4CAF50 : (item.mood 0 ? #F44336 : #BDBDBD)) .height(60) } .width(20) // 卡片主体 Column() { Text(item.dateText).fontSize(12).fontColor(#888888) Text(item.preview).fontSize(16).maxLines(3) .textOverflow({ overflow: TextOverflow.Ellipsis }) Row() { ForEach(item.tags, (tag: string) { Text(# tag) .fontSize(12) .fontColor(#3F6DFB) .margin({ right: 8 }) }) } } .layoutWeight(1) .padding(12) .backgroundColor(Color.White) .borderRadius(12) } .alignItems(VerticalAlign.Top) } }) } } }注意节点颜色直接跟随mood正负值变化绿色代表这天过得不错红色代表情绪低落。用户翻历史的时候一眼就能扫出哪段时间是低谷哪段时间是高峰——这个视觉反馈比任何文字都直接。卡片里还放了最多三张图片缩略图。这里有个性能细节不能直接加载原图我保存图片时会同时生成一份宽边不超过480像素的缩略图列表只加载缩略图点击卡片才看原图否则翻页卡顿会让人崩溃。4.2 编辑器与智能分析联动编辑页这次也做了深化。用户输入正文时底部会有一个轻量的“分析预览”区域实时显示字数、预测的心情标签和提取出的关键词标签标签允许直接点击删除或手动添加。实时分析不能每次按键都跑全套算法我加了一个300毫秒的防抖用户停止输入300毫秒后才触发一次分析。分析逻辑丢到任务池TaskPool里的子线程执行避免在UI线程做字符串扫描。任务池的好处是不用手动管理线程系统根据设备负载调度适合这种短暂的计算密集型任务。保存按钮点击后应用把最终确认的mood和tags写进数据库。注意分析结果在编辑页是可以被用户改掉的保存时以用户当前看到的结果为准而不是重新跑一遍算法。这一点非常关键否则用户手动调整过的标签会被自动结果覆盖。4.3 多设备场景的设计取舍最后聊一点多端。HarmonyOS强调多设备协同但在日记这个场景我不建议一开始就把全文做成分布式同步。全文同步会遇到冲突处理、同步时序、数据量控制一系列问题做成型的成本不低。我实际采用的折中方案是通过分布式键值库同步“最近七天的日记摘要”包括日期、情绪分、标签列表让用户在手机和折叠屏之间切换时能看到近况概览正文和图片仍然留在本机只有在用户主动点击某一条时才从本机数据库查询完整内容。这样实现了“轻量协同”的感觉又避免了把整套同步机制拖进项目。等用户规模和数据量真涨上来了再考虑服务端同步也不迟。5. 常见问题与排查技巧实录功能做完不是终点联调阶段的问题才最磨人。这章把我实际遇到的高频问题按主题整理成速查每个问题都给了排查路径和最终方案。5.1 数据库升级与历史数据兼容第一代版本发布后有用户更新一升级就出现列表空白。排查后发现问题出在数据库迁移的时序上我在初始化数据库对象之后、执行迁移脚本之前就先发起了查询结果查询跑在新表结构上而旧数据还没完成转换自然什么都查不到。解决方法是把“打开数据库 - 执行迁移 - 再允许业务查询”改成严格串行用一个初始化Promise包住业务层所有查询都await这个Promise结束后再执行。另外给迁移脚本追加了幂等判断如果目标列已经存在就跳过ALTER操作避免重复升级时炸掉。老版本遇到类似问题先看日志里迁移回调有没有执行到再确认版本号是否真的递增了基本能定位。还有一次是给已有表增加tags列后老数据全部是“[]”导致周报统计标签时出现了空数组的解析报错。处理方式是迁移时顺手把存量行更新为默认值逻辑很简单但很多人会忘掉“存量数据也需要初始化”这一步。5.2 提醒不触发与通知权限每日提醒上线后测试发现部分设备上收不到。第一层原因是通知权限HarmonyOS的通知需要用户授权用户第一次拒绝后应用就再也发不出通知。我在首次打开时主动弹权限请求并说明用途被拒绝后在应用内设置页提供二次开启的入口同时加了一个“最近未写日记”的角标作为兜底提示。第二层原因是系统对后台任务有省电策略限制。提醒代理机制整体比普通后台任务可靠但个别设备还是会在长时间未打开应用后暂停提醒。我的处理是在用户打开应用时做一次补偿检查如果发现最近三天没有写日记直接在本机补一条本地通知。这不算完美的方案但在“不打扰用户”和“保证提醒可达”之间已经是我能给出的最稳的组合。5.3 性能与内存缩略图、任务池与防抖日记应用的数据量虽然不大但图片和全文扫描还是会带来肉眼可见的卡顿。图片这块除了前面说的缩略图策略我还在保存图片时限制了最长边不超过2048像素并转成JPEG压缩。一张手机原图动辄三五兆压缩后一两百KB对存储空间和多端轻量同步都是实打实的改善。情绪分析和关键词提取虽然算法不重但频率一高UI线程也会掉帧。处理方式是全套分析模块设计成纯函数丢进任务池执行主线程只拿到结果再更新状态。这里踩过一个小坑任务池的回调里不能直接操作非线程安全的共享对象我给分析结果定义了一个独立的结构体通过返回值传出来而不是让子线程去改页面状态。还有一个容易被忽略的瓶颈是字数统计。编辑器里用onChange实时数字数如果正文很长每次输入都扫描全量字符串老旧设备会明显卡。我用了和情绪分析一样的防抖方案300毫秒统计一次实测性能从明显卡顿变成毫无感知。6. 一些个人体会整个项目做下来我最深的感受是个人工具里的“智能”重点不在模型有多大而在于能不能在合适的时间把用户真正需要的内容送到眼前。一个词库算法撑起的情绪标签配合一个按时出现的提醒和一份直观的周报带给用户的“懂我”的感觉可能比一个云端大模型生成的华丽总结更实在。如果这个项目还要继续往下走我建议下一代做两件事一是把情绪分析和关键词抽取的接口整理得更干净之后想接更重模型时直接替换实现二是对历史数据做一次批量回填让上线前的老日记也能拥有情绪标签和关键词这样周报和回忆功能的覆盖范围才完整。开发永远是这样先把地基打牢再慢慢往上面添砖。

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

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

免费获取报价 →
↑