起因:每天中午最烦的不是工作,是吃什么
说实话这个 App 的起源特别朴素。我们办公室六个人,每天中午 11 点半准时开始那个经典问题——“今天吃啥?”。每次讨论至少 10 分钟,最后大概率还是去楼下那家黄焖鸡。
我之前在 iOS 上用过几个随机选餐的工具,但都只有一个功能:随机。问题是办公室这种场景,你随机出来的结果别人不认啊。“我不想吃这个”——一句话就把随机结果否了。
所以我想做一个带投票淘汰机制的版本。正好去年开始搞 HarmonyOS 开发,就把这个需求当练手项目做了。
App 叫「决定今天吃什么」,现在 1.2.0 版本,已经上架华为应用市场。
三种模式,覆盖不同决策场景
我设计了三种决策模式:
🎲 随机模式 — 一个人的时候用,从本地收藏的餐厅库里直接抽。带历史排重,最近 N 次选过的不会再出现。
🎡 转盘模式 — 有点仪式感的随机,视觉上比直接弹结果有趣。转盘最多显示有限数量的分段,超出的话会按权重取 top N。
🗳️ 投票模式 — 这才是核心。多人场景下,每个人划掉自己不想吃的选项,剩下的再随机。平票的情况下走加权随机,不会卡死。
投票模式的逻辑其实不复杂,但实际用起来体验差别很大。同事们从"我不想吃这个"变成了"我把这三个划掉了",心态完全不一样——参与感到位了,对结果的接受度就高。
技术实现:ArkTS 状态管理和筛选逻辑
整个项目用 ArkTS 写的,状态管理没用特别重的框架,自己封装了一个 AppStore 类来管理餐厅列表、历史记录和设置项。数据持久化走的本地存储。
候选餐厅的筛选逻辑是我觉得写得比较清爽的一块。支持按标签、预算上限、距离三个维度过滤,加上模糊搜索:
export function getCandidates(all: Restaurant[], query: CandidateQuery): Restaurant[] {
const { search, filters } = query
const s = (search || '').trim().toLowerCase()
const f = {
tagsAll: filters?.tagsAll || [],
budgetMax: filters?.budgetMax || 0,
distanceMaxKm: filters?.distanceMaxKm || 0
}
return all
.filter(r => !r.archived)
.filter(r => {
if (f.tagsAll.length > 0 && !f.tagsAll.every(t => r.tags.includes(t))) return false
if (f.budgetMax > 0 && r.price.max > f.budgetMax) return false
if (f.distanceMaxKm > 0 && r.distanceKm > f.distanceMaxKm) return false
return true
})
.filter(r => {
if (!s) return true
const hay = `${r.name} ${r.cuisines.join(' ')} ${r.tags.join(' ')}`.toLowerCase()
return hay.includes(s)
})
}
```
这段看着简单,但实际场景里"距离 2 公里内、人均 30 以下、不要火锅"这种组合筛选用起来挺顺手的。
随机选取这块我做了加权随机 + 历史排重。每个餐厅有个 weight 字段,可以手动调(比如你特别喜欢某家店就调高),同时最近选过的自动权重归零:
```arkts
export function pickRandom(restaurants: Restaurant[], options: RandomOptions): { winner: Restaurant; durationMs: number } {
const start = Date.now()
const { settings, recentHistory } = options
const avoidIds = new Set<string>()
const n = Math.max(0, settings.avoidRecentN)
for (let i = 0; i < Math.min(n, recentHistory.length); i++) {
avoidIds.add(recentHistory[i].resultRestaurantId)
}
const weights = restaurants.map(r => {
const base = settings.weightedRandom ? r.weight : 1
if (avoidIds.has(r.id)) return 0
return Math.max(0, base)
})
const { item: winner } = weightedPick(restaurants, weights)
return { winner, durationMs: Date.now() - start }
}
```
`avoidRecentN` 默认设的 3,就是最近三顿选过的不会再出现。这个细节说小也小,但没有的话你会发现"随机"了三天连着去同一家——那就不叫随机了。
## 投票模式的平票处理
做投票模式的时候纠结过一个问题:平票怎么办?
最早我想做二次投票,但实际测下来太麻烦了,大家中午赶着吃饭没耐心再投一轮。最后的方案是平票直接用当轮投票中被保留(也就是没被划掉)的次数作为权重,做一次加权随机。说白了就是:平票的几家里,当轮被更多人留下来的那家,胜出概率稍微大一点。
这个取舍花了我大概两天时间。试过完全等概率随机、试过让发起人决定、最后还是觉得加权随机最省事也最公平。
## 数据模型的一些设计取舍
餐厅模型我放了不少字段:cuisines(菜系)、tags(自定义标签)、price(价格区间)、distanceKm(距离)、weight(权重)、favorite(收藏)。
说实话 distanceKm 这个字段有点尴尬。我一开始想接地图 API 自动算距离,但鸿蒙的地图服务在某些场景下需要联网授权,而我想把这个 App 做成纯离线可用的。所以最后变成了用户手动填一个大概距离。有点土,但能用。
后面如果华为地图的离线能力更完善了,我可能会加一个"自动获取距离"的选项。
## 关于 HarmonyOS 开发的一些感受
这是我第三个上架华为应用市场的鸿蒙 App 了。说几个真实感受:
ArkUI 的声明式写法上手之后效率确实不错,状态驱动 UI 刷新这套逻辑跟 SwiftUI 很像。但调试工具还是差点意思,有时候状态变了 UI 没更新,排查起来比较费劲。
ArkTS 本身写逻辑很舒服,类型系统够用。我整个项目的 models.ts 和 services 层写得挺顺的,TypeScript 的底子在这里。
上架审核比 iOS 快很多。我这个 App 从提交到过审大概两天。
## 当前状态和后续计划
目前 1.2.0 版本,功能基本稳定。坦白说下载量还很少,刚上线不久,还在冷启动阶段。
后续想做的事:
- 加一个"今日推荐"功能,根据天气、星期几、最近吃的东西来推荐(比如连续三天重口味了就推荐清淡的)
- - 投票模式支持分享链接,让不在身边的人也能参与
- - 导入大众点评收藏(如果 API 允许的话)
## 一个想讨论的问题
多人决策场景下平票处理,我试过三种方案都有坑:等概率随机大家觉得"白投了",发起人拍板容易得罪人,二次投票没人有耐心。你们如果遇到类似的场景是怎么处理的?或者有没有更好的思路?
华为应用市场搜「决定今天吃什么」就能找到这个 App。
390



被折叠的 条评论
为什么被折叠?



