聚沙攒钱 1.9:重构成就系统时踩的坑,以及一些下载量为零的反思

0 阅读6分钟

最近 7 天新增下载为零,但我还是把 1.9 发了

这是我独立开发的存钱 App「聚沙攒钱」的真实数据。说出来有点难受,但我觉得版本该发还是得发——不能因为没人下载就停止打磨产品。1.9 的重点是重构成就徽章系统、新增 365 天挑战,还有一些一直想解决的性能问题。

这篇文章聊聊这次更新里几个技术决策背后的思考,特别是成就系统的设计和双模式架构的取舍。

成就系统重构:从暴力遍历到摘要驱动

之前的做法很糙:用户每次存款后,遍历所有徽章条件,逐一查数据库判定是否解锁。目标少的时候没感觉,但当用户有十几个目标、几百条存款记录时,每次存钱后会卡大概 200ms——在动画播放的关键帧里,这个延迟肉眼可见。

重构思路是把高频查询的统计数据预计算成一个 StatsSummary 摘要,存款时只更新摘要的对应字段,徽章判定变成纯内存操作:

struct BadgeDefinition: Identifiable {
    let id: String
    let name: String
    let category: String
    let condition: (StatsSummary) -> Bool
}

// 每个徽章就是一个闭包
BadgeDefinition(id: "streak_7", name: "Week Streak", category: "streak") {
    $0.currentStreak >= 7
},
BadgeDefinition(id: "night_owl", name: "Night Owl", category: "special") {
    $0.nightDeposits >= 10
},
BadgeDefinition(id: "ritual_master", name: "Ritual Master", category: "special") {
    $0.ritualCount >= 5
},
BadgeDefinition(id: "collector", name: "Collector", category: "special") {
    $0.activeGoals >= 5
}

新增徽章只需要一行定义,不改业务代码。目前一共 16 个徽章,停在这个数字是刻意的——我试过加更多(比如「连续 3 天存款金额递增」「周末存款达 10 次」),但测试下来发现条件太多用户反而没有目标感,看着一堆灰色锁头会焦虑。16 个大概是一屏半能展示完的量,用户划两下就能看全。

几个我觉得比较有意思的徽章设计:

  • Night Owl / Early Bird:凌晨 0-5 点存款 10 次解锁夜猫子,早上 5-8 点 10 次解锁早起鸟。这两个是行为洞察——固定时间段存钱的人留存明显更好,用徽章做正向引导
  • Ritual Master:完成 5 次「砸罐」仪式(目标达成后的庆祝动画)才解锁,意味着用户至少完成过 5 个储蓄目标
  • Starter Week:新用户引导专用,要求同时满足「建了目标 + 存过款 + 连续 2 天」,门槛很低但把三个关键行为串起来了

双模式架构:丑代码和没选策略模式的原因

App 有两种核心模式:愿望模式(wish,有目标金额)和自由模式(free,纯粹存钱罐没上限)。

自由模式是上线后用户反馈加的——有人说「我就想随手存零钱,不想设目标」。加了之后 targetAmount 可能为 0,进度计算、完成状态、UI 展示全部要分支处理,代码里到处是 if goal.mode == .free

我确实考虑过用协议抽象,定义 SavingsGoalProtocol,让 WishGoal 和 FreeGoal 各自实现进度计算。但实际情况是:这两种模式共享 95% 的逻辑(存款、打卡、统计、备份),真正不同的只有「进度百分比怎么算」和「是否显示完成状态」这两个点。为了两个 if 引入一套协议体系,我觉得是过度设计。

策略模式也想过——把进度计算抽成 Strategy 对象注入。但 SwiftData 的模型层不太方便持有策略对象,序列化的时候还要额外处理。最后的决定是忍受 if 判断,把所有分支集中在一个 extension 里,至少改的时候知道去哪里找。

丑吗?丑。但作为一个人的项目,能跑、好改、不出 bug 比优雅重要。

挑战模式:52 周存钱法的妥协

挑战模板四种:30 天、52 周、100 天、365 天。

ChallengeTemplate(
    id: .week52,
    titleKey: "Challenge 52 Weeks",
    icon: "📈",
    totalDays: 364,
    suggestedTargetAmount: 1378  // 1+2+3+...+52
)

52 周的 suggestedTargetAmount 是 1378,对应经典的递增存法。我一开始想做成强制每周递增——第 1 周必须存 1 块,第 2 周必须 2 块,不满足就算断签。后来自己测了一轮,到第 35 周(每周 35 块)就开始觉得烦了,对学生用户来说更是压力。

最终方案是只记录总进度,UI 上提示「本周建议存 XX 元」但不强制。保留了仪式感,去掉了压迫感。

365 天挑战是 1.9 新加的,配合本地 JSON 备份一起上线。一年跨度太长,用户换手机的概率不低,备份文件带了 schema version 方便以后做格式迁移,限制最大 8MB(正常用几年也到不了)。

SpriteKit 硬币动画的性能坑

这是用户感知最强的功能:存款后硬币从顶部落下,碰到罐壁弹跳堆叠。用 SpriteKit 物理引擎,每个硬币一个 SKSpriteNode + physicsBody。

踩的坑:用户一次存 1000 块,按 10 块一个硬币就是 100 个节点同时参与物理模拟,iPhone SE 2 上帧率直接掉到 30fps 以下。

解决方案:同时渲染的硬币上限 30 个,超出的用加速动画直接落入底部合并,视觉上是「哗啦一下」的感觉反而更爽快。CPU 占用从峰值 45% 降到 18% 左右。

下载量为零的反思

技术层面我对 1.9 还算满意,但产品推广确实是短板。分析了几个原因:

  1. ASO 关键词选的「存钱」「攒钱」,但用户实际搜索习惯偏向「记账」,搜索量差了一个数量级
  2. 硬币动画这个卖点在静态截图里完全体现不出来,需要录视频但我一直没做
  3. 25-35 岁有储蓄意愿的目标群体不太会在 App Store 主动搜「存钱 App」——他们更可能在社交平台看到别人的打卡截图后被种草

下一步打算做的:挑战模式完成阶段打卡后生成分享卡片,让用户帮我做传播。这可能是目前成本最低的获客手段。

如果你也在做游戏化设计

我在徽章条件的设计上花了不少时间——什么样的条件既有挑战性又不让人焦虑,门槛设多高用户才会觉得「差一点就够到了」而不是「算了太难了」。这些没有标准答案,我也还在摸索。

如果你的产品里也有成就系统或者游戏化激励的设计,评论区聊聊?特别想知道大家怎么定阈值的——是靠数据分布还是靠手感。