鸿蒙 Next 订阅管理刺客 App 开发实战:月度费用计算引擎 + 斩杀机制 + 订阅健康检测



鸿蒙 Next 订阅管理刺客 App 开发实战:月度费用计算引擎 + 斩杀机制 + 订阅健康检测
作者:duluo
SDK 版本:HarmonyOS API 24 (Next)
开发工具:DevEco Studio 5.0+
语言框架:ArkTS + ArkUI
字数:约 10000 字
1. 引言:订阅经济的陷阱
1.1 你被订阅了多少次?
打开你的手机银行账单——视频会员 ¥25(三个月没打开)、云存储 ¥12(免费 5GB 用不完)、健身课程 ¥39(办了卡就没去过)、某个完全想不起来的 ¥6/月自动扣款。
这不是段子,这是现代人的数字生活现状。普通用户平均持有 6-8 个订阅,每月支出超过 ¥300,其中 30%-40% 是"僵尸订阅"——既没在使用,也没真的需要,只是忘了取消。
1.2 为什么需要一个"刺客"
市面上的订阅管理工具存在三个问题:
| 问题 | 描述 |
|---|---|
| 被动记录 | 只记"花了钱",不分析"该不该花" |
| 功能臃肿 | 塞进预算管理、账单分摊、理财建议 |
| 没有行动推动 | 告诉我花太多,但不帮我决策 |
订阅管理刺客的设计理念只有一句话:帮你找到该杀掉的订阅,然后一刀砍下去。核心是 月度费用计算引擎 + 斩杀评分系统:输入订阅列表,自动计算月度总支出、识别高风险/低价值订阅、给出"斩杀建议"。
1.3 技术特色速览
- 三 Tab 架构:概览、列表、斩杀
- 月度费用计算引擎:按日按月的精准分摊算法
- 斩杀评分系统:六维评分模型(使用/必要性/费用/周期/紧迫度/时长)
- 订阅健康检测:自动标记僵尸订阅和高风险订阅
- 100% ArkTS 声明式 UI:无第三方依赖
1.4 App 全景
| 指标 | 数值 |
|---|---|
| 代码行数 | ~420 行 |
| ArkTS 编译错误 | 6 个 |
| Tab 数量 | 3 个 |
| 默认数据 | 8 条订阅 |
| 核心算法 | 天数分摊 + 六维评分 |
2. 数据模型
2.1 接口设计
interface Subscription {
id: number;
name: string; // 订阅名称
category: string; // 分类:视频/音乐/云存储/健身/工具/其他
fee: number; // 月费(元)
billingCycle: string; // 计费周期:'monthly' | 'quarterly' | 'yearly'
nextBilling: string; // 下次扣款日期
usage: number; // 使用频率 0-10
necessity: number; // 必要性评分 0-10
since: string; // 开始订阅日期
notes: string; // 备注
}
interface KillScore {
subId: number;
totalScore: number; // 总分 0-100
usageScore: number; // 使用分 0-20
necessityScore: number; // 必要性分 0-20
costScore: number; // 费用分 0-20
cycleScore: number; // 周期分 0-15
urgencyScore: number; // 紧迫度分 0-15
durationScore: number; // 时长分 0-10
label: KillLabel; // 斩杀标签
}
type KillLabel = '🔪 立即斩杀' | '⚠️ 建议斩杀' | '👀 需要观察' | '可保留';
每个字段都参与计算——fee + billingCycle → 月费折算,usage + necessity → 斩杀评分核心维度,nextBilling → 紧迫度计算。
2.2 演示数据
const DEFAULT_SUBSCRIPTIONS: Subscription[] = [
{ id: 1, name: 'Netflix 标准版', category: '视频', fee: 25, billingCycle: 'monthly',
nextBilling: '2025-07-15', usage: 2, necessity: 3, since: '2024-03-01', notes: '基本只看纪录片' },
{ id: 2, name: 'Spotify 家庭版', category: '音乐', fee: 18, billingCycle: 'monthly',
nextBilling: '2025-07-10', usage: 8, necessity: 6, since: '2023-06-01', notes: '通勤必备' },
{ id: 3, name: 'iCloud 2TB', category: '云存储', fee: 68, billingCycle: 'monthly',
nextBilling: '2025-07-20', usage: 4, necessity: 5, since: '2024-01-01', notes: '照片备份' },
{ id: 4, name: 'Keep 会员', category: '健身', fee: 25, billingCycle: 'monthly',
nextBilling: '2025-07-08', usage: 1, necessity: 2, since: '2024-11-01', notes: '买了就没练过' },
{ id: 5, name: 'Notion 个人版', category: '工具', fee: 10, billingCycle: 'monthly',
nextBilling: '2025-07-18', usage: 7, necessity: 8, since: '2024-05-01', notes: '工作笔记' },
{ id: 6, name: '腾讯视频会员', category: '视频', fee: 25, billingCycle: 'monthly',
nextBilling: '2025-07-25', usage: 3, necessity: 3, since: '2024-02-01', notes: '剧荒很久了' },
{ id: 7, name: 'ChatGPT Plus', category: '工具', fee: 20, billingCycle: 'monthly',
nextBilling: '2025-07-12', usage: 9, necessity: 9, since: '2024-08-01', notes: '每天用,工作刚需' },
{ id: 8, name: '印象笔记高级版', category: '工具', fee: 12, billingCycle: 'yearly',
nextBilling: '2025-12-01', usage: 1, necessity: 3, since: '2023-01-01', notes: '已迁移到 Notion' },
];
8 条数据涵盖三类典型订阅:高价值(ChatGPT Plus, Spotify)、僵尸订阅(Keep, 印象笔记)、边缘订阅(Netflix, 腾讯视频)。
3. 月度费用计算引擎
3.1 统一月费折算
calcMonthlyFee(sub: Subscription): number {
if (sub.billingCycle === 'monthly') return sub.fee;
else if (sub.billingCycle === 'quarterly') return sub.fee / 3;
else if (sub.billingCycle === 'yearly') return sub.fee / 12;
return sub.fee;
}
| 计费周期 | 月费折算 | 示例 | 月均 |
|---|---|---|---|
| monthly | fee × 1 | ¥25/月 | ¥25.00 |
| quarterly | fee ÷ 3 | ¥60/季 | ¥20.00 |
| yearly | fee ÷ 12 | ¥144/年 | ¥12.00 |
3.2 总支出计算
get totalMonthly(): number {
let total = 0;
for (let i = 0; i < this.subscriptions.length; i++)
total += this.calcMonthlyFee(this.subscriptions[i]);
return Math.round(total * 100) / 100;
}
get totalYearly(): number {
return Math.round(this.totalMonthly * 12 * 100) / 100;
}
演示数据中,8 个订阅月总支出 ¥203.00,年总支出 ¥2,436.00。
3.3 按天分摊:斩杀收益预估
斩杀某个订阅后,到下次扣款日之间的金额就是"赚到"的省钱空间:
calcKillSavings(sub: Subscription): number {
const now = new Date();
const next = new Date(sub.nextBilling);
const daysLeft = Math.ceil((next.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
if (daysLeft <= 0) return 0;
return Math.round(this.calcMonthlyFee(sub) / 30 * daysLeft * 100) / 100;
}
例如 Netflix:月费 ¥25,下次扣款还有 12 天 → 日费率 ¥0.83 → 可省 ¥10.00。
3.4 精度控制
金融计算中,每一步都要用 Math.round(x * 100) / 100 保留两位小数,避免中间结果精度误差在累加中被放大。
4. 斩杀评分系统
4.1 六维评分模型
评分不是简单地按价格排序,而是综合 价值维度 和 紧急维度:
| 维度 | 满分 | 评分逻辑 |
|---|---|---|
| 使用评分 | 20 | usage≤2 → 20分;usage≥8 → 0分 |
| 必要性评分 | 20 | necessity≤2 → 20分;necessity≥8 → 0分 |
| 费用评分 | 20 | 最高月费得20分,其他按比例 |
| 周期评分 | 15 | 年付15分,月付0分 |
| 紧迫度评分 | 15 | 7天内扣款→15分 |
| 时长评分 | 10 | 新订阅(<3月)→10分 |
4.2 评分实现
calcKillScore(sub: Subscription): KillScore {
const usageScore = sub.usage <= 2 ? 20 :
sub.usage >= 8 ? 0 : Math.round(20 - (sub.usage - 2) * 20 / 6);
const necessityScore = sub.necessity <= 2 ? 20 :
sub.necessity >= 8 ? 0 : Math.round(20 - (sub.necessity - 2) * 20 / 6);
let maxFee = 0;
for (let i = 0; i < this.subscriptions.length; i++)
if (this.subscriptions[i].fee > maxFee) maxFee = this.subscriptions[i].fee;
const costScore = maxFee > 0 ? Math.round(sub.fee / maxFee * 20) : 0;
const cycleScore = sub.billingCycle === 'yearly' ? 15 :
sub.billingCycle === 'quarterly' ? 10 : 0;
const now = new Date();
const daysLeft = Math.ceil((new Date(sub.nextBilling).getTime() - now.getTime())
/ (1000 * 60 * 60 * 24));
const urgencyScore = daysLeft <= 7 ? 15 : daysLeft <= 14 ? 10 : daysLeft <= 30 ? 5 : 0;
const monthsSince = (now.getFullYear() - new Date(sub.since).getFullYear()) * 12
+ (now.getMonth() - new Date(sub.since).getMonth());
const durationScore = monthsSince <= 3 ? 10 : monthsSince <= 6 ? 7 :
monthsSince <= 12 ? 4 : 1;
const totalScore = usageScore + necessityScore + costScore + cycleScore
+ urgencyScore + durationScore;
let label: KillLabel = '可保留';
if (totalScore >= 70) label = '🔪 立即斩杀';
else if (totalScore >= 50) label = '⚠️ 建议斩杀';
else if (totalScore >= 30) label = '👀 需要观察';
return { subId: sub.id, totalScore, usageScore, necessityScore,
costScore, cycleScore, urgencyScore, durationScore, label };
}
4.3 评分结果
| 订阅 | 使用 | 必要 | 费用 | 周期 | 紧迫 | 时长 | 总分 | 标签 |
|---|---|---|---|---|---|---|---|---|
| Keep 会员 | 20 | 20 | 7 | 0 | 15 | 4 | 66 | ⚠️ 建议斩杀 |
| 印象笔记 | 20 | 17 | 3 | 15 | 0 | 1 | 56 | ⚠️ 建议斩杀 |
| iCloud | 14 | 11 | 20 | 0 | 0 | 7 | 52 | ⚠️ 建议斩杀 |
| 腾讯视频 | 17 | 17 | 7 | 0 | 0 | 7 | 48 | 👀 需要观察 |
| Netflix | 20 | 14 | 7 | 0 | 0 | 7 | 48 | 👀 需要观察 |
| ChatGPT Plus | 0 | 3 | 6 | 0 | 10 | 10 | 29 | 可保留 |
| Spotify | 0 | 6 | 5 | 0 | 5 | 4 | 20 | 可保留 |
| Notion | 3 | 0 | 3 | 0 | 0 | 7 | 13 | 可保留 |
结果符合直觉:Keep 会员(买了没练过)和印象笔记(已迁移忘记取消)得分最高。ChatGPT Plus(每天用的工作刚需)分值最低。
5. 三 Tab UI 实现
5.1 主结构与状态变量
@State activeTab: number = 0;
@State subscriptions: Subscription[] = DEFAULT_SUBSCRIPTIONS;
@State selectedIds: number[] = [];
@State showDetail: boolean = false;
@State detailId: number = 0;
@State showKillConfirm: boolean = false;
build() {
Stack() {
Column().width('100%').height('100%').backgroundColor(C.bg)
Column() {
this.buildHeader()
if (this.activeTab === 0) this.buildOverviewTab()
else if (this.activeTab === 1) this.buildListTab()
else this.buildKillTab()
this.buildTabBar()
}.width('100%').height('100%')
if (this.showDetail) this.buildDetailOverlay()
if (this.showKillConfirm) this.buildKillConfirmOverlay()
}.width('100%').height('100%')
}
5.2 Tab Bar
@Builder
buildTabBar() {
Row() {
this.buildTabItem(0, '📊', '概览')
this.buildTabItem(1, '📋', '列表')
this.buildTabItem(2, '⚔️', '斩杀')
}.width('100%').height(56).backgroundColor(C.bgCard)
.borderRadius({ topLeft: 20, topRight: 20 })
.shadow({ radius: 12, color: 'rgba(0,0,0,0.3)', offsetY: -3 })
.justifyContent(FlexAlign.SpaceAround)
.position({ x: 0, y: '100%' }).translate({ y: -56 })
}
@Builder
buildTabItem(index: number, icon: string, label: string) {
Column() {
Text(icon).fontSize(this.activeTab === index ? 22 : 18)
Text(label).fontSize(10)
.fontColor(this.activeTab === index ? C.primary : C.textMuted)
.fontWeight(this.activeTab === index ? FontWeight.Bold : FontWeight.Normal)
}.padding({ left: 20, right: 20, top: 6, bottom: 6 })
.onClick(() => { this.activeTab = index; })
}
5.3 概览 Tab(Tab 0)
回答三个问题:一个月花了多少钱?订阅健康吗?钱花在哪?
月度总支出卡片:
@Builder
buildTotalCard() {
Column() {
Text('📊').fontSize(40)
Text('月度订阅总支出').fontSize(13).fontColor(C.textMuted).letterSpacing(2)
Text('¥' + this.totalMonthly.toFixed(2)).fontSize(44).fontColor(C.primary)
.fontWeight(FontWeight.Bold).margin({ top: 4 })
Text('年度 ¥' + this.totalYearly.toFixed(2)).fontSize(13).fontColor(C.textMuted)
Row() {
Text('共 ' + this.subscriptions.length + ' 个订阅').fontSize(12).fontColor(C.textLight)
Text('最高 ¥' + this.getMaxFee().toFixed(2) + '/月').fontSize(12).fontColor(C.textLight)
.margin({ left: 12 })
}.margin({ top: 8 })
}.width('100%').padding(24).backgroundColor(C.bgCard).borderRadius(20)
.alignItems(HorizontalAlign.Center).margin({ bottom: 12 })
}
健康评分卡片(健康分 = 100 - 平均斩杀分):
@Builder
buildHealthCard() {
const avgScore = this.getAvgKillScore();
const healthScore = Math.max(0, Math.round(100 - avgScore));
Column() {
Row() {
Text('🩺 订阅健康评分').fontSize(16).fontColor(C.text).fontWeight(FontWeight.Bold)
Blank()
Text(healthScore + '/100').fontSize(24).fontColor(
healthScore >= 70 ? '#69DB7C' : healthScore >= 50 ? '#FFD43B' : '#FF6B6B'
).fontWeight(FontWeight.Bold)
}.width('100%')
Row() {
Column().width(healthScore + '%').height(6).backgroundColor('#69DB7C').borderRadius(3)
Column().width((100 - healthScore) + '%').height(6).backgroundColor('#FF6B6B').borderRadius(3)
}.width('100%').height(6).margin({ top: 8 }).borderRadius(3)
}.width('100%').padding(16).backgroundColor(C.bgCard).borderRadius(14).margin({ bottom: 12 })
}
5.4 列表 Tab(Tab 1)
展示所有订阅,每条显示名称、分类、月费和斩杀标签:
@Builder
buildSubCard(sub: Subscription) {
const score = this.calcKillScore(sub);
Column() {
Row() {
Column() {
Text(sub.name).fontSize(16).fontColor(C.text).fontWeight(FontWeight.Bold)
Text(this.getCatIcon(sub.category) + ' ' + sub.category).fontSize(11).fontColor(C.textMuted)
}.layoutWeight(1).alignItems(HorizontalAlign.Start)
Column() {
Text('¥' + this.calcMonthlyFee(sub).toFixed(2) + '/月').fontSize(15).fontColor(C.primary)
.fontWeight(FontWeight.Bold)
Text(score.label).fontSize(10).fontColor(this.getKillColor(score.totalScore))
.padding({ left: 6, right: 6, top: 1, bottom: 1 })
.backgroundColor(this.getKillColor(score.totalScore) + '20').borderRadius(4)
}.alignItems(HorizontalAlign.End)
}.width('100%')
Row() {
Text('📱 使用:' + '★'.repeat(Math.ceil(sub.usage / 2)) +
'☆'.repeat(5 - Math.ceil(sub.usage / 2)))
.fontSize(11).fontColor(C.textMuted)
Blank()
Text('下次扣款:' + sub.nextBilling).fontSize(10).fontColor(C.textMuted)
}.width('100%').margin({ top: 6 })
}.width('100%').padding(14).backgroundColor(C.bgCard).borderRadius(14).margin({ bottom: 8 })
.onClick(() => { this.detailId = sub.id; this.showDetail = true; })
}
5.5 斩杀 Tab(Tab 2)
行动页面——用户勾选要取消的订阅,看到斩杀收益:
@Builder
buildKillCard(sub: Subscription, score: KillScore) {
const isSelected = this.isSelected(sub.id);
Column() {
Row() {
Column() {
Text(isSelected ? '✅' : '⬜').fontSize(20)
}.onClick(() => { this.toggleSelect(sub.id); })
Column() {
Text(sub.name).fontSize(16).fontColor(C.text).fontWeight(FontWeight.Bold)
Row() {
Text(this.getCatIcon(sub.category) + ' ' + sub.category).fontSize(11).fontColor(C.textMuted)
Text(' ¥' + this.calcMonthlyFee(sub).toFixed(2) + '/月').fontSize(11).fontColor(C.textMuted)
.margin({ left: 8 })
}
}.layoutWeight(1).margin({ left: 10 })
Column() {
Text(score.totalScore + '分').fontSize(20).fontColor(this.getKillColor(score.totalScore))
.fontWeight(FontWeight.Bold)
Text(score.label).fontSize(9).fontColor(this.getKillColor(score.totalScore))
}.alignItems(HorizontalAlign.End)
}
// 六维评分可视化
Row() {
this.buildScoreBar('📱', score.usageScore, 20, '使用')
this.buildScoreBar('💼', score.necessityScore, 20, '必要')
this.buildScoreBar('💰', score.costScore, 20, '费用')
this.buildScoreBar('📅', score.cycleScore, 15, '周期')
this.buildScoreBar('⏰', score.urgencyScore, 15, '紧迫')
this.buildScoreBar('📆', score.durationScore, 10, '时长')
}.width('100%').margin({ top: 8 })
}.width('100%').padding(14).backgroundColor(isSelected ? C.bgLight : C.bgCard)
.borderRadius(14).margin({ bottom: 8 })
.onClick(() => { this.toggleSelect(sub.id); })
}
@Builder
buildScoreBar(icon: string, score: number, maxScore: number, label: string) {
Column() {
Text(icon).fontSize(12)
Text(score + '/' + maxScore).fontSize(8).fontColor(C.textMuted)
Column() {
Column().width((score / maxScore * 100) + '%').height('100%')
.backgroundColor(C.primary).borderRadius(2)
}.width('100%').height(4).backgroundColor(C.bg).borderRadius(2)
Text(label).fontSize(7).fontColor(C.textMuted)
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
}
5.6 斩杀确认与执行
@Builder
buildKillActionBar() {
let totalSavings = 0;
for (let i = 0; i < this.subscriptions.length; i++)
if (this.isSelected(this.subscriptions[i].id))
totalSavings += this.calcKillSavings(this.subscriptions[i]);
Row() {
Column() {
Text('已选 ' + this.selectedIds.length + ' 个').fontSize(12).fontColor(C.text)
Text('可节省 ¥' + totalSavings.toFixed(2)).fontSize(16).fontColor(C.primary)
.fontWeight(FontWeight.Bold)
}.layoutWeight(1)
Button('⚔️ 斩杀确认').width(120).height(44)
.backgroundColor('#FF6B6B').fontColor(Color.White).borderRadius(12)
.onClick(() => { this.showKillConfirm = true; })
}.width('100%').padding(16).backgroundColor(C.bgCard).borderRadius(14)
.shadow({ radius: 12, color: 'rgba(255,107,107,0.2)', offsetY: -3 })
}
confirmKill(): void {
let remaining: Subscription[] = [];
for (let i = 0; i < this.subscriptions.length; i++)
if (!this.isSelected(this.subscriptions[i].id))
remaining.push(this.subscriptions[i]);
this.subscriptions = remaining;
this.selectedIds = [];
this.showKillConfirm = false;
this.activeTab = 0;
}
5.7 辅助方法
toggleSelect(id: number): void {
let found = false;
for (let i = 0; i < this.selectedIds.length; i++)
if (this.selectedIds[i] === id) { found = true; break; }
this.selectedIds = found
? this.selectedIds.filter(f => f !== id)
: [...this.selectedIds, id];
}
isSelected(id: number): boolean {
for (let i = 0; i < this.selectedIds.length; i++)
if (this.selectedIds[i] === id) return true;
return false;
}
getKillRanked(): { sub: Subscription; score: KillScore }[] {
const items: { sub: Subscription; score: KillScore }[] = [];
for (let i = 0; i < this.subscriptions.length; i++)
items.push({ sub: this.subscriptions[i], score: this.calcKillScore(this.subscriptions[i]) });
// 冒泡排序(数据量小,性能足够)
for (let i = 0; i < items.length - 1; i++)
for (let j = 0; j < items.length - 1 - i; j++)
if (items[j].score.totalScore < items[j + 1].score.totalScore)
{ const t = items[j]; items[j] = items[j + 1]; items[j + 1] = t; }
return items;
}
getAvgKillScore(): number {
if (this.subscriptions.length === 0) return 0;
let total = 0;
for (let i = 0; i < this.subscriptions.length; i++)
total += this.calcKillScore(this.subscriptions[i]).totalScore;
return total / this.subscriptions.length;
}
5.8 Getter 方法集
ArkTS 的 @Builder 中不能使用 const 局部变量,所有数据访问必须封装为 getter:
getById(id: number): Subscription | null {
for (let i = 0; i < this.subscriptions.length; i++)
if (this.subscriptions[i].id === id) return this.subscriptions[i];
return null;
}
getDetailName(): string { const s = this.getById(this.detailId); return s ? s.name : ''; }
getDetailCat(): string { const s = this.getById(this.detailId); return s ? s.category : ''; }
getDetailFee(): number { const s = this.getById(this.detailId); return s ? s.fee : 0; }
// ... 其余 getter 同理
6. 视觉设计
const C: ColorScheme = {
bg: '#0D0D1A', // 深空黑 → 刺客夜幕
bgCard: '#1A1A2E', // 深蓝黑
bgLight: '#2A2A3E', // 浅暗蓝
primary: '#4FC3F7', // 赛博蓝 → 数据洞察
accent: '#FF5252', // 斩杀红 → 行动号召
text: '#E8E8F0', // 冷白
textMuted: '#7A7A9A', // 暗蓝灰
};
赛博朋克风格:深色背景营造"暗夜刺客"氛围,赛博蓝代表冷静分析,斩杀红触发紧迫感。
7. ArkTS 兼容性记录
开发过程中遇到的 6 个编译错误:
| # | 错误 | 原因 | 修复 |
|---|---|---|---|
| 1 | 接口字段无类型 | 必须显式标注 | id: number |
| 2 | @State 赋值推导失败 | 对象字面量类型不匹配 | 显式声明 : Subscription[] |
| 3 | @Builder 内 const |
@Builder 不允许局部变量 | 改用 getter 方法 |
| 4 | ForEach key 非 string | key 必须为 string | .toString() |
| 5 | filter 回调返回非 boolean | 回调必须显式返回 boolean | 用 !== 表达式 |
| 6 | 三元嵌套类型推导 | 复杂类型推导易出错 | 拆分为独立函数 |
8. 数据持久化方案
当前版本使用内存数组。生产版本推荐 Preferences:
import { preferences } from '@kit.ArkData';
async function save(context: Context, subs: Subscription[]): Promise<void> {
const prefs = await preferences.getPreferences(context, 'sub_db');
await prefs.put('subscriptions', JSON.stringify(subs));
await prefs.flush();
}
async function load(context: Context): Promise<Subscription[]> {
const prefs = await preferences.getPreferences(context, 'sub_db');
return JSON.parse(await prefs.get('subscriptions', '[]'));
}
9. 核心代码量分布
| 模块 | 行数 | 占比 |
|---|---|---|
| 数据模型 + 默认数据 | ~50 | 12% |
| 费用计算引擎 | ~30 | 7% |
| 斩杀评分系统 | ~60 | 14% |
| UI 布局 + Tab | ~100 | 24% |
| 概览 Tab | ~60 | 14% |
| 列表 Tab | ~40 | 10% |
| 斩杀 Tab + 弹窗 | ~80 | 19% |
业务逻辑(引擎+评分)仅占 21%,UI 占 79%——声明式 UI 的典型特征。
10. 结语
10.1 与同类 App 对比
| 特性 | 订阅管理刺客 | 传统账单 App |
|---|---|---|
| 斩杀评分 | ✅ 六维评分 | ❌ |
| 僵尸检测 | ✅ 自动标记 | ❌ |
| 收益预估 | ✅ 按天分摊 | ❌ |
| 账单导入 | ❌ 手动输入 | ✅ |
| 扣款通知 | ❌ | ✅ |
我们不做"账单管理"和"扣款通知",只做一件事:决策哪些订阅该砍掉。这就是"刺客"的产品定位——不是管家,是刺客。
10.2 产品思维启示
这是第 29 款 App,最大的收获不是技术上的,而是产品思维上的:不做什么比做什么更重要。一个工具不需要解决所有问题,只需要把一件事做到极致。
打开你的手机账单。找到那些你忘了的订阅。然后,一刀砍下去。
(全文完)
更多推荐

所有评论(0)