您的位置:时间博客>优词记>连续打卡天数怎么算:一次从 PHP 层下沉到 SQL 的重构

连续打卡天数怎么算:一次从 PHP 层下沉到 SQL 的重构

背单词小程序「优词记 Pro」技术复盘第五篇。前面几篇偏架构,今天讲一个具体的业务问题:学习日历上的「连续学习天数」,最后为什么用 SQL 算而不是 PHP 算

业务背景

小程序里每答一道题都会写一条学习日志(记录答对还是答错),学习日历根据日志给有学习记录的日期打标记,同时展示「当前连续学习天数」和「历史最长连续天数」——这是打卡激励体系的核心数字。

第一版:把日期拉回来遍历

最直觉的实现:把用户所有学习日志的日期去重后查回来,在 PHP 里排序遍历,相邻日期差一天就累加,断了就重置。十几行代码,跑得也对。

问题出在增长上。重度用户一年就是三百多个日期,而这个数字显示在个人中心首屏,每次打开都要算一遍。全量日期从 MySQL 传输到应用层、在协程里逐个比较——单次不慢,但它是高频路径上的 O(n) 传输加 O(n) 遍历,数据和用户量一起涨的时候,这种"能跑就行"的实现会最先出问题。

第二版:gaps-and-islands,让数据库自己算

连续日期统计在 SQL 里是个经典问题,叫 gaps-and-islands(间隙与孤岛)。核心技巧一句话就能说明白:

日期 - ROW_NUMBER() = 同一段连续日期共享的锚点

对去重后的学习日期按时间排序编号,用每个日期减去它的行号:连续的日期减出来的结果完全相同,一旦断档,差值就变了。按这个差值 GROUP BY,每组就是一段连续区间,COUNT(*) 是段长,取最大值就是最长连续天数,包含今天(或昨天)的那段就是当前连续天数。

整个计算在数据库内完成,网络上只传回一两个数字。索引配合下,执行时间基本不随日志总量线性增长——这正是数据库擅长而应用层不擅长的事:贴着数据做集合运算

顺手踩的一个 Hyperf 坑

迁移过程中还遇到一个值得记录的坑:用 Model 执行这类带聚合别名的查询时,first() 拿到的聚合字段总是 0。原因是查询结果会走 Model 的属性处理链路——驼峰转换、列元数据校验等——而 max_days 这种临时别名不在表的列定义里,被处理链路"矫正"丢了。解法是聚合查询统一用 toBase()->first(),跳过 Model 属性处理,直接拿原始结果。这个坑在任何重写了属性访问的 ORM 里都可能出现,遇到聚合值莫名为 0 可以往这个方向查。

沉淀下来的分层规则

这次重构还让我把一条规则写进了项目规范:数据计算逻辑必须封装在 Model 层的静态方法里,Service 层只做编排,不做内联计算。上一版的问题本质就是计算逻辑散落在 Service 里,既不好测试,也让"换 SQL 实现"这种优化牵动业务代码。收进 Model 之后,Service 调用的方法签名没变,重构对上层完全透明。

产品本体是个背单词小程序,微信搜「优词记 Pro」可以体验,日历页那个连续天数就是这条 SQL 算出来的。明天换个轻松的话题:一次 HBuilderX 生产构建的踩坑复盘——组件被静默丢弃、样式全乱,排查过程挺有意思,欢迎关注。


转载请注明本文标题和链接:《 连续打卡天数怎么算:一次从 PHP 层下沉到 SQL 的重构

相关推荐

网友评论 0

未登陆 表情
Ctrl+Enter快速提交