Day 20-23:四天三个里程碑
四天。166次commit。三个里程碑交付。17个PR合并。
盘盘猫冲过三个里程碑旗帜的马拉松赛道
这四天,项目的性质变了。从"做功能"变成了"做生意"。
一个人写PR是不是有病?
我开始引入里程碑——每个里程碑是一个完整的、可交付的增量,有明确的前后对比。同时开始认真写PR。
一个人写PR听起来有点蠢。但说实话,PR干了三件事:记录当时的意图、留下可追溯的变更历史、逼自己想清楚"到底在交付什么、为什么要做"。
每个PR都有结构化描述:摘要、变更表格、带复选框的测试计划。三周后你需要搞明白"当时为什么改了这个"的时候,这些东西是实打实有用的。
M1:积分经济体系 — 100个文件变更
从"免费用"到"免费试+付费用"的转变。
技术上最难的部分,不是积分本身。
匿名用户合并:状态机地狱
新用户进来就是Supabase匿名账户——零注册摩擦。能浏览、能用免费积分、能保存测算结果。但等他们决定登录(Google OAuth、邮箱、兑换码),所有数据得无缝合并:积分余额、测算记录、聊天历史、推荐关系。
兑换码流程本身就是个状态机:idle → checking → redeeming/logging_in → success/error。RedeemCodeInput组件自动格式化PPM-XXXX-XXXX输入,处理完整生命周期。匿名用户输入兑换码时,后端直接把兑换码设成他们匿名账户的密码——码本身就是下次登录的凭证。
后来PR #103修了个竞态条件:liunian应用类型在llm_costs_app_check约束中缺失,导致生产环境INSERT失败。这种bug最阴——只在特定功能的特定路径上才炸。
跨标签页积分同步:一个隐蔽的双重扣费bug
在一个标签页买了积分,切到另一个标签页,余额还是旧的。用户以为购买失败了,又买了一次。
双重扣费。
解决方案是Supabase实时订阅。每个标签页监听积分变动,一个标签页扣了或加了积分,其他标签页一秒内同步。听着简单,但订阅生命周期要做对——挂载时订阅、卸载时取消、标签页休眠后重连——细节很多。
7种场景化升级提示
UpgradePrompt组件有7种触发类型,每种针对不同的用户心理:
-
测算过程中积分用完(高紧迫感,购买意愿最强)
-
每日运势浏览时余额不足(低紧迫感,意识培养)
-
测算完成后的满足时刻(正面情绪,追加销售)
-
推荐提示(社交场景,"送礼"框架)
-
首次发现新功能(好奇心驱动,试用转付费)
还有卡片和横幅两种变体,加上24小时关闭冷却——不能让用户觉得被骚扰。
M2:每日运势Hub — 100个文件变更
算命天然是事件驱动的:有问题了才来,得到答案就走。
每日运势Hub解决的就是留存——给用户一个每天回来的理由。
五张卡片:八字日柱运势、星座运势、每日塔罗牌、黄历、占卜引导。每张都有自定义CSS主题。
预生成才是核心洞察
这里有个关键决策:北京时间午夜通过cron任务提前生成内容,而不是用户打开时现生成。
/api/cron/daily-content端点用Gemini AI生成八字和星座运势,存到daily_content表,自动清理7天前的数据。
用户早上8点打开Hub,内容已经在那了。瞬间加载。没有转圈。
即时加载和等3秒的差距,不是技术上的3秒,是用户心理上的"这个产品靠不靠谱"。
cron端点用常量时间XOR做密钥比较(安全审计发现的),基于IP的频率限制(30次/分钟),加Asia/Shanghai时区一致处理。
占星模块重构
同一个PR里还做了占星模块的国际化重构。一个347行的三语字典(zh-CN/zh-TW/en),涵盖行星、星座、相位、宫位,加上70多个结果页面字符串。
星盘轮盘重新设计为金色-暗色主题,带度数刻度线、中文行星/星座标签和交互式提示框。78个测试覆盖星盘计算和布局函数。
合盘持久化
新增synastry_readings表,出生信息加密存储、完整的保存/加载/聊天流程,历史记录API集成。出生数据加密是刚需——合盘测算包含两个人的出生信息。
M3:数据分析 — 11个文件变更
PostHog集成。用after()做兑换码事件追踪,避免阻塞兑换响应。清理了被管理后台取代的旧管理路由。
PR规模最小,但它闭合了反馈环路。
之前只能猜用户在干什么。现在能看到。
从"做功能"到"做用户行为"
M1-M3之前,我每天问自己:"下一个该做什么功能?"
M1-M3之后,问题变了:"下一步该驱动什么用户行为?"
转化漏斗、留存钩子、日活参与、推荐激励——这些不是工程问题。这些是恰好需要用代码来解决的商业问题。
这是我第一次开始想用户留存,而不是功能清单。