背景:一个真实的个人痛点
作为独立开发者,我自己就是重度订阅用户——视频平台、云存储、AI 工具、音乐会员……林林总总每月接近 400 块。问题是,很多订阅早已沦为「僵尸服务」,每月自动扣费,打开次数却屈指可数。
市面上记账类 App 能告诉你「花了多少」,但很少能回答「这笔钱花得值不值」。我想做的事情很简单:用数据驱动决策,把主观的「觉得没用」变成客观的「确实没用」。
于是在我的独立项目「订阅斩」v1.6 中,我设计并实现了一套**砍单顾问(Kill Advisor)**模块。这篇文章聊聊它背后的产品逻辑和技术思路。
核心设计:三层漏斗
整个砍单顾问可以拆成三层:
┌─────────────────────────────┐
│ Layer 1: 数据采集(打卡) │
├─────────────────────────────┤
│ Layer 2: 可视化(热力图) │
├─────────────────────────────┤
│ Layer 3: 决策建议(规则引擎)│
└─────────────────────────────┘
Layer 1:极低摩擦的打卡采集
每天只需回答一个布尔问题:「昨天用了这个服务吗?」 交互成本控制在 5 秒以内。
技术上用了 iOS 的 Local Notification 做每日提醒,配合一个轻量 Widget 直接在桌面完成打卡,避免用户需要打开主 App。数据存储使用 Core Data,schema 非常简单:
// 简化示意
struct CheckIn {
let subscriptionID: UUID
let date: Date
let didUse: Bool
}
之所以不做自动化检测(比如读取屏幕使用时间),是因为隐私边界的考虑——订阅斩全程纯本地运行,不联网、不上传任何数据。这是我给自己定的硬约束。
Layer 2:30 天热力图可视化
采集到打卡数据后,用一个类似 GitHub Contribution Graph 的 30 天热力图呈现。横轴是日期,每个订阅一行,格子着色表示「当天是否使用」。
这一步的价值在于让僵尸订阅无所遁形——一个连续灰色(未使用)的行,比任何文字提醒都直观。
实现上用了 SwiftUI 的 LazyVGrid 配合自定义 Shape,性能开销很小。颜色映射:
func cellColor(for checkIn: CheckIn?) -> Color {
guard let checkIn = checkIn else { return .gray.opacity(0.15) } // 未打卡
return checkIn.didUse ? .green.opacity(0.7) : .red.opacity(0.3)
}
Layer 3:规则引擎 → 三档建议
这是砍单顾问的核心输出。根据 30 天打卡数据,给每个订阅打上标签:
| 标签 | 规则(可配置) |
|---|---|
| 🔴 立即斩杀 | 30 天使用率 ≤ 10%(≤3 天) |
| 🟡 认真考虑 | 使用率 10%-40%,或存在免费替代方案 |
| 🟢 建议保留 | 使用率 > 40% 且日均成本在阈值内 |
其中「日均成本」的计算是另一个细节:把月费 / 30 换算成每天花多少钱。用户看到「这个视频会员每天花 1.6 元但你一个月只打开了 2 次」比单纯看月费 48 元更有决策冲击力。
struct Advice {
enum Level { case kill, consider, keep }
static func evaluate(subscription: Subscription, checkIns: [CheckIn]) -> Level {
let usedDays = checkIns.filter { $0.didUse }.count
let useRate = Double(usedDays) / 30.0
let dailyCost = subscription.monthlyFee / 30.0
if useRate <= 0.1 { return .kill }
if useRate <= 0.4 || dailyCost > subscription.perceivedValue { return .consider }
return .keep
}
}
这里没有用机器学习或复杂算法——对于这种决策场景,透明可解释的规则 >> 黑盒模型。用户需要知道「为什么建议我砍掉它」,而不是一个无法追溯的分数。
数据录入的工程细节:OCR 扫描账单截图
手动录入订阅信息是个很高的门槛。为了降低 onboarding 摩擦,我加了一个 OCR 模块:用户截图微信/支付宝的扣费通知或账单页面,App 通过 Vision 框架识别文字,提取服务名称和金额。
let request = VNRecognizeTextRequest { request, error in
guard let observations = request.results as? [VNRecognizedTextObservation] else { return }
let strings = observations.compactMap { $0.topCandidates(1).first?.string }
// 正则匹配金额和服务商关键词
let parsed = SubscriptionParser.extract(from: strings)
}
request.recognitionLanguages = ["zh-Hans", "en-US"]
request.recognitionLevel = .accurate
识别准确率不是 100%,所以设计上是「OCR 预填 + 用户确认修改」的模式,而非全自动。
一个小彩蛋:订阅墓园
被砍掉的订阅不会直接删除,而是进入一个「墓园」页面,展示墓碑 UI + 累计节省金额的实时计数器。
这是个纯产品设计层面的巧思——给用户正反馈。看着「累计节省 ¥168」的数字随时间增长,能强化用户持续管理订阅的动力。技术上只是一个 Timer + 简单数学:
var cumulativeSaved: Double {
graveyard.reduce(0) { total, sub in
let daysSinceKilled = Calendar.current.dateComponents([.day], from: sub.killedDate, to: Date()).day ?? 0
return total + (sub.monthlyFee / 30.0) * Double(daysSinceKilled)
}
}
实际效果
用我自己的数据举个例子:
- 爱奇艺:30 天只打卡了 2 天 → 立即斩杀
- 某 AI 写作工具:连续 30 天未使用 → 立即斩杀
- 网易云音乐黑胶:使用率尚可但有免费替代 → 认真考虑
- 腾讯视频:可以和朋友拼家庭组 → 认真考虑
- ChatGPT Plus:每天都在用,日均成本不到 5 块 → 建议保留
最终砍掉 5 个订阅,月省 168 元,年化超过 2000 元。
总结与思考
做这个功能的过程让我体会到几点:
- 隐私优先是独立开发者的差异化武器。大厂做订阅管理一定会联网同步,而纯本地方案反而成了卖点。
- 低摩擦 > 全自动。打卡看似原始,但 5 秒成本换来的是用户对数据的主动感知,比后台静默采集更能驱动行为改变。
- 可解释性决定信任度。规则引擎的「为什么」比 ML 模型的「相信我」更适合涉及金钱决策的场景。
- 正反馈闭环不可或缺。墓园的累计节省计数器看似花哨,但它是用户留存的关键锚点。
如果你也在做工具型 App,希望这些设计思路能提供一些参考。
「订阅斩」是我个人的独立开发项目,目前 iOS 平台可用。本文重在分享产品设计与技术实现思路,欢迎交流。