做了个鸿蒙App解决“中午吃什么“,聊聊三种决策模式的技术实现
起因每天最难的决策不是写代码是吃什么我在公司的日常大概是这样的上午写代码挺顺畅11点半左右开始走神脑子里只有一个问题——中午吃什么。三四个同事围在一起“随便”“都行”你定吧的对话能持续十分钟。情侣之间更夸张我和女朋友为这事吵过不止一次说出来有点丢人。后来我想这不就是个决策问题吗写个工具把它解决掉不就行了。刚好那段时间在研究 HarmonyOS 开发ArkTS 写起来和 TypeScript 很像上手没什么门槛就直接在鸿蒙上做了。App 叫「决定今天吃什么」目前版本 1.2.0已经上架华为应用市场。三种模式覆盖不同场景同类工具我翻了一圈大多只有一个随机功能点一下出个结果完事。但吃饭这事的场景差异挺大的一个人的时候随机就够了两个人的时候重点是减少分歧一群人的时候要先排除大家不想吃的所以我做了三种模式随机选择、转盘选择、投票选择。随机模式没什么好说的。转盘模式加了点仪式感转的过程本身就挺解压。投票模式是我觉得最有意思的——每个人划掉自己不想吃的选项剩下的再随机决定。说白了就是排除法 随机但实际用起来真的减少了很多扯皮。投票模式的实现投票这块我折腾了一阵子。核心逻辑不复杂但要处理平票的情况。说实话最后的处理方式很朴素平票就是等概率随机。我一开始想搞复杂的——比如用历史偏好加权、用投票轮次做衰减什么的试了两个方案都觉得过度设计。几个人投票投出平局说明大家对这几家的接受度差不多随机挑一个就完事了没必要硬凹差异。export function finishVote(session: VoteSession): string { const entries Object.entries(session.votes) let max -1 let winners: string[] [] for (const [id, count] of entries) { if (count max) { max count; winners [id] } else if (count max) { winners.push(id) } } if (winners.length 1) return winners[0] // 平票等概率随机没必要搞复杂 return winners[Math.floor(Math.random() * winners.length)] } 投票模式真正有意思的地方不在最后怎么决胜而在划掉这个动作本身——每个人都参与了排除过程最后不管选到哪家大家的接受度都比较高。这比一个人拍板的体验好太多了。 ## 历史排重避免连续吃同一家 这个功能是我自己用了两周之后加的。因为随机嘛真的会连续三天选到同一家。概率上完全正常但体验上很烦。 解决方案很直接在 pickRandom 的时候把最近 N 次选过的餐厅权重设为 0。N 是可配置的默认我设的 3。 有个边界情况值得说一下如果用户收藏的餐厅本来就少比如只有 4 家avoidRecentN 设成 3那过滤完可能所有候选权重都是 0。这种情况下我的处理是忽略排重约束回退到全量随机。总不能因为排重逻辑选不出来给用户弹个无候选吧那也太蠢了。 avoidRecentN 这个默认值我纠结了挺久。设太大候选池容易被排空设太小排重效果约等于没有。最后决定让用户自己调默认 3餐厅少的人可以改成 1 或 0。 ## 筛选候选距离、预算、口味标签 餐厅数据模型里我加了 distanceKm、pricemin/max、cuisines、tags 这些字段。用户可以按条件过滤候选列表比如3公里以内、人均50以下、不要辣的。 过滤逻辑我写成了管道式的链式调用每个条件都是有值才过滤没值就跳过 arkts export function getCandidates(all: Restaurant[], query: CandidateQuery): Restaurant[] { const s (query.search || ).trim().toLowerCase() const f query.filters || {} return all .filter(r !r.archived) .filter(r !(f.tagsAll?.length) || f.tagsAll.every(t r.tags.includes(t))) .filter(r !f.budgetMax || r.price.max f.budgetMax) .filter(r !f.distanceMaxKm || r.distanceKm f.distanceMaxKm) .filter(r !s || ${r.name} ${r.cuisines.join( )} ${r.tags.join( )} .toLowerCase().includes(s)) } 这种写法的好处是每个 .filter() 职责单一后面加新条件直接追加一行就行。比如我后来加了只看收藏的功能就是多挂了一个 .filter(r !onlyFav || r.favorite)改动量很小。 ArkUI 上的交互实现主要是筛选面板。说实话鸿蒙的 State 和 Link 装饰器用起来和 React 的 useState 思路差不多响应式更新做得还行。踩坑的地方是列表刷新的时机——我一开始把筛选逻辑放在 UI 层做列表一长就卡后来挪到 service 层先算好再传给组件流畅多了。 ## 数据全部本地存储 这个 App 没有后端所有数据都存在本地。用的是 HarmonyOS 的轻量级存储方案餐厅列表、历史记录、用户设置分开存。 为什么不做云端两个原因一是吃饭这种数据没必要上云二是我一个人做维护服务器的精力不够。保持简单能跑就行。 数据模型上有个小设计我觉得还不错餐厅的 archived 字段。删除操作我没做真删除而是标记归档。这样历史记录里引用的餐厅 ID 不会变成空指针回头翻历史还能看到当时选的是哪家。这个坑是我第一版直接 delete 之后发现的——历史列表全是已删除的餐厅挺难受的。 ## 转盘的段数限制 转盘模式有个 MAX_WHEEL_SEGMENTS 的限制我设的 12。候选餐厅太多的话转盘上塞不下那么多格子12 个以上文字就开始重叠视觉上根本看不清。 超出 12 个时我按权重排序取前 12 个放到转盘上。 这个处理方式有个小问题权重低的餐厅永远上不了转盘。我想过随机抽取的方案但那样转盘内容每次都不一样用户会困惑我明明收藏了那家怎么不在上面。权衡之后还是选了按权重截断——至少结果是确定的、可预期的。反正用户想让某家店上转盘把它权重调高就行操作上说得通。 ## 目前的状态 App 今年上架的华为应用市场当前版本 1.2.0。说实话下载量还很少刚起步阶段。 我自己每天在用身边几个同事也装了午饭决策确实快了不少。上周有个同事说投票模式挺好的但能不能加个匿名投票这个我在考虑下个版本加进去。 对了餐厅数据目前是手动录入的这个体验确实有点重。我在想要不要接入地图 API搜附近餐厅直接导入。但 HarmonyOS 的地图能力和 API 调用还在摸索中不确定什么时候能做。 ## 一些鸿蒙开发的体感 ArkTS 写业务逻辑没啥问题类型系统和 TypeScript 基本一致数据模型定义、工具函数这些迁移成本很低。 ArkUI 的声明式写法上手也快但组件库的丰富度跟 Flutter 或 SwiftUI 比还是有差距。转盘动画那块我花了不少时间自己画没有现成的轮盘组件可以用。 调试工具目前够用DevEco Studio 的预览功能帮了不少忙。真机调试偶尔会断连重启一下就好不算大问题。 整体感受是鸿蒙的工具链已经能支撑中小型 App 的开发了但社区资源还比较少遇到问题能搜到的解决方案不多很多时候得自己翻文档试。 对了想问一下同样在做 HarmonyOS 开发的各位转盘段数超限时你们会怎么处理按权重截断、随机抽取、还是有别的方案我一直觉得这两种都不够好评论区聊聊。