1. 第34天不是里程碑,而是最容易放弃的高危平台期
今天这篇没有项目代号,标题就一个词:day 34。熟悉我博客节奏的朋友应该猜到了,这是我自己定下的一个100天长期计划——每天固定时间写作、写代码、做复盘,并把整个过程用一套自动化脚本记录下来。第34天,听起来既不是开头也不是结尾,但它恰恰是我认为整个周期里最危险的一个节点。
很多人喜欢引用"21天养成习惯"的说法,但以我自己跑了三年个人项目的经验来看,21天只能解决"开始"的问题,真正决定项目能不能做完的,是第五周前后这个平台期。到第34天,你已经没了第一周的兴奋感,也不会像第二周那样靠"立flag"的羞耻心硬撑,同时你还没看到足以证明一切的成果。卡在这个位置的人,通常会用一个特别体面的理由退场:"最近太忙了,先停几天,后面再补。"可这一停,基本就停到项目结束了。
我之所以专门为"day 34"写这么一篇,是因为这周我确实有好几天早上打开电脑时,脑子里闪过了"要不今天就算了"的念头。我把前33天的节奏盘了一遍,把项目的自动化记录脚本又加固了一轮,最后没断更,还顺手修掉了一个隐藏了很久的时区问题。这篇就聊聊这个节点上我具体做了什么,以及为什么我说第34天才是项目真正的分水岭。
1.1 前33天走过的三条曲线
如果你也在跑类似的长期计划,我想先给你一个参照系:任何持续性的个人项目,前33天都会同时走过三条方向完全不同的曲线。
第一条是兴奋曲线。第1到第10天,你每天都在接触新东西,哪怕只写十行代码,都觉得"我今天进步了"。这种情绪是天然的燃料,但也是不可持续的燃料。到第三周,同样的任务变得无聊,你开始意识到大部分工作都是重复劳动。第二条是能力曲线。前30天确实是能力爬升最快的阶段,尤其是如果项目涉及你不熟悉的工具链,你会发现自己每天都能学会几个命令、踩掉几个坑。但能力爬升会带来一个副作用:你能做的事变多了,你给自己定的"每日标准"也跟着水涨船高。第三条是期待曲线。第一周期待很朴素——"我只要坚持就行"。到了第34天,期待变成了"我要看到成绩、看到成品、看到别人认可"。这三条曲线一交叉,就形成了所谓的高危平台期:能力在涨、兴致在降、眼光在变高。不把这个状态点破,你很容易把"项目本身有问题"错判成"我不适合做这个"。
我自己的解决方式很土:在第34天做一次项目体检,把项目从"热情驱动"切换成"系统驱动"。说白了一句话——坚持不靠意志力,靠的是你给自己设计的规则。
1.2 我在第34天做的"项目体检"清单
这次体检我列了四个问题,每一个都值得单独写进当天的记录里:
- 如果这个项目今天就停在这里,之前33天做的哪些事情是白费的?
- 有没有一项每天都花时间、但从来没被认真审视过的重复动作?
- 如果让现在的我回到第1天,我会改变哪条初始设定?
- 有哪些环节仍然高度依赖手工操作,一旦状态不好就会断档?
这四个问题的价值在于,它们会把"我今天不想做"这种情绪,翻译成可排查的系统问题。比如我当时的答案是:前33天里,最浪费时间的动作不是写代码,而是每晚打开好几个工具去汇总当天的完成情况。状态好时这只要十分钟,状态不好时我会坐在那里刷半天手机。这件事很自然地指向了我这周要做的一个技术改动——给日报自动化脚本补一套状态流转逻辑,也正是下一篇要展开的部分。
2. 这周的正事:给自动化日报脚本补齐状态流转逻辑
项目进行到这个阶段,我手头主要在做一个小工具,名字很直白,叫"dayslog"。它做的事情用一句话概括:每天定时从几个数据源拉取我的活动数据——代码提交次数、本地文件变更、日历上的事项完成情况,然后汇总成一条结构化记录写入本地数据库。每周日再自动生成一份周报,告诉我这周的有效投入时间、产出节奏和中断次数。
前33天里,这个脚本的运行逻辑一直很简单:每晚10点,cron触发一次Python脚本,把当天数据insert进SQLite。听起来够用了,但它有两个很致命的问题:第一,如果当天cron没跑,那一天的数据就永远丢了;第二,如果我在第二天早上想补充修改前一天的数据,没有人能看出这条记录到底是"当天正常完成"还是"事后补的"。你可能会说这有什么区别?区别大了。数据一旦进入复盘系统,就必须保留它的可信度。我第34天的工作,就是给每条记录加一个完整的状态机,让"没跑"和"没做完"都成为一等公民,而不是静默消失的异常。
2.1 为什么需要状态字段,而不是一条INSERT到底
在设计这个状态机之前,我花了半小时认真想一个问题:每天的记录到底有几种可能的存在状态?
实际跑下来,无非是这几种:计划里安排了但还没开始,进行到一半被中断,正常完成,cron漏跑导致缺失,以及事后手动补齐。如果你只用一张表存"完成内容",这些状态天然就在信息损失。比如你看到周一有一条记录,内容是空的,你没法判断是周一没工作,还是脚本坏了没采集到。这对周报的统计影响很大——我后来看数据时发现,有三天的"空窗"其实都是cron环境问题,而不是我真的没干活。
所以我给表结构加了一个status字段,取值如下:
| 状态值 | 含义 | 进入条件 |
|---|---|---|
planned | 今日计划已写入 | 当天早上脚本初始化 |
running | 正在进行中 | 连续工作时间超过15分钟 |
done | 正常完成 | 当日收尾,主动标记 |
missed | 缺失 | cron漏跑或手动标记失败 |
patched | 事后补齐 | 次日修改并写入上一日记录 |
这套设计真正带来的收益不在"状态本身",而在它逼着我去思考"每一条记录是谁、在什么条件下、以什么形式写入的"。一旦把写权限和状态分离,后面做统计、出周报、画趋势图才有底气。数据源可以粗糙,但数据的血缘关系必须清楚,这是我在监控系统里学到的经验。
2.2 状态流转的触发点和幂等处理
实现状态流转时,最容易被忽略的是"同一份数据被重复触发"的问题。cron每晚10点触发一次,但如果我当天熬夜到11点还没收尾,而脚本11点又被我手动跑了一次,就有两条running记录。我一开始的做法是直接再insert一条,结果数据库里出现了同一天的多条记录,周报的统计全乱了。
修复方法是给(date, source)加唯一约束,同时用一条UPSERT语句把状态更新和写入合并起来:
INSERT INTO daily_log(date, source, status, payload, updated_at) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP) ON CONFLICT(date, source) DO UPDATE SET status = excluded.status, payload = excluded.payload, updated_at = CURRENT_TIMESTAMP;Python侧只保留业务层判断:当脚本启动时会先查询status,如果是done或patched,就不再写入,而是把本次运行记录进一个单独的执行日志。这套逻辑跑了一周,我再也没出现过同一天重复记账的脏数据。
这里有个小教训顺便说一句:写自动化工具,别一上来就追求"智能",先把幂等性做对。幂等的意思是,同一个操作不管执行一次还是两次,最终数据都一致。你只要保证任何一步重跑都不会把数据搞乱,后面的维护成本会直线下降。
3. 实测踩坑:cron 与 Python 对"今天"的理解各说各话
我是在第32天发现这个问题的——周报里出现了周四和周五两天的记录时间都比实际晚8小时,周五的报告里还出现了"2025-06-06 23:00"这种明显属于周六凌晨的时间点。第一反应当然是脚本有bug,但手动跑了一遍脚本,写入的时间戳又是对的。这种"手动正常、定时任务出错"的现象,几乎可以断定是环境差异,而不是代码逻辑问题。
3.1 现象描述:手跑正常,cron跑出来差8小时
先说环境:我的脚本跑在一台廉价的海外VPS上,系统默认时区是UTC,日常使用全靠应用层手动指定Asia/Shanghai。手跑那次,Shell里已经通过环境变量把时区设置好了,所以datetime.now()返回的是东八区时间。但cron启动的进程是全新的环境,不会继承我手动export的变量。于是Python拿到的是UTC时间,而我在周报SQL里又只用了strftime('%Y-%m-%d')截取日期,这就导致所有记录的实际日期都被往前挪了一天。
还有一个更隐蔽的坑:我用了数据库的CURRENT_TIMESTAMP作为写入时间,SQLite这个函数返回UTC时间,和Python侧生成的本地时间戳混在一起,日期的错乱程度会加倍。我一开始以为是cron漏跑,后来一查数据库,发现记录都在,只是日期错位了。这种错位比丢失更坑,因为它会悄悄污染你的周报统计,不仔细看根本发现不了。
3.2 完整排查链路:从crontab到进程环境变量
这个问题的排查过程比较典型,我完整记录下来,方便你以后遇到类似问题直接按这个顺序走。
第一步,先确认cron到底有没有执行。我看了一眼cron的执行日志,确认任务确实触发了,脚本也没有报错,说明问题出在"执行了但结果不正确"这一类。
第二步,在cron任务里加一行输出环境变量的命令,把进程的TZ和日期打出来:
* 22 * * * /usr/bin/env | grep -E 'TZ|LANG' >> /tmp/cron_env.log && date >> /tmp/cron_env.log跑了一天后看日志,date输出的是UTC时间,而我自己在Shell里执行date得到的是东八区时间。到这里基本锁定了:cron进程没有继承用户Shell里的环境变量。
第三步,验证Python获取时间的方式。在脚本里临时加了一行print(datetime.now().astimezone().tzinfo),跑下来输出的是UTC,进一步确认了根因。
修复方案不复杂,我做了两处调整:第一,在crontab文件顶部显式声明时区,CRON_TZ=Asia/Shanghai,这样整个cron进程都会按东八区跑;第二,把脚本里所有拿到时间戳的地方改成统一走一个helper函数,内部用zoneinfo.ZoneInfo("Asia/Shanghai")明确指定时区,彻底不依赖系统默认值。
from datetime import datetime from zoneinfo import ZoneInfo TZ = ZoneInfo("Asia/Shanghai") def now_local() -> datetime: return datetime.now(TZ)改完之后,我把数据库中已错位的记录重新校正了一次,然后连续观察了一周,周报里的日期全部恢复正常。这个问题技术上不复杂,但如果不是手动对比cron和交互Shell的差异,我可能还会在"数据缺失"这个错误方向上查很久。
3.3 修完之后的稳定性收益
这个bug的修复,表面看只是时区问题,但它带来的连锁收益比我想象中大。首先,之前为了"防止漏跑",我在cron里设了三个不同的触发时间:晚上10点、11点、凌晨1点。最初的想法是"多跑几次更保险",但因为没有幂等逻辑,实际上制造了大量重复数据。修复时区问题后,我把三个任务合并成了一个,每晚11点触发一次,由脚本自身处理重复执行,cron层面不需要冗余补偿。
其次,这次排查逼着我把"环境差异"纳入了脚本设计。现在脚本启动时会先做一次自检,把当前进程的时区、Python版本、数据库文件路径都记录到一个meta表里。谁在哪次运行、用了什么环境,全部可追溯。以后再出类似问题,我不需要靠猜,直接查meta表就能定位。
4. 跑完34天,我用数据抓出了三个低效习惯
技术问题解决之后,我趁着第34天做了一次相对完整的复盘。因为从第1天开始,每天的记录都进了SQLite,所以我手头有34天的结构化数据。这些数据单个看没什么,但按周聚合之后,暴露出了三个我平时完全没察觉的低效习惯。
先说统计方式。我写了几条简单的SQL:按星期几统计有效工作时间,按小时段统计启动次数,把每天的running状态时长汇总。没有用任何复杂的分析模型,就是最普通的GROUP BY和平均值。数据量只有34条,但足够说明问题了。
SELECT strftime('%H', started_at) AS hour, COUNT(*) AS cnt FROM activity_sessions WHERE status IN ('done', 'patched') GROUP BY hour ORDER BY cnt DESC;4.1 一组让我意外的统计结果
第一组数据:我的产出时间高度集中在凌晨时段,晚上11点到1点之间的有效工作时间占全天的百分之四十。这看起来像我是一个夜猫子,但实际不是——我每一晚都计划10点收工,只是"收工前顺手再做一件事"的惯性总把我拖到凌晨。也就是说,我自己认为的"夜间高效"其实是"白天拖延后的时间挤压"。
第二组数据:从周四到周六,连续中断的记录明显增多,中断时长短则20分钟,长则两小时。一开始我以为是工作太忙,但对照日历后发现,这三天都有同一个共性——下午安排了需要大量沟通的会议。会议本身没问题,问题是会议之后我没有任何缓冲时间,导致晚上状态切换不过来,于是靠熬夜补偿。
第三组数据:每天上午9点到11点的记录里,有60%的状态是planned而不是running。意思是,我每天早上花两小时制定计划,但真正动手的时间很少。这个发现挺扎心的,因为我一直认为自己是个"计划型选手",数据却在说:你的计划是拖延的变体。
4.2 三个低效习惯与对应调整
发现问题的下一步是调整,而不是自责。我针对这三个习惯分别做了非常具体的改动。
第一个习惯,白天拖延、晚上熬夜补偿。对应调整:我把每日的核心产出任务设置成"上午必须启动"。所谓启动,不需要完成,只需要做15分钟,把状态从planned改成running。这个设置利用的是"一旦开始就不想停下来"的心理惯性,副作用很小,但确实有效。
第二个习惯,会议后无缓冲。对应调整:下午的沟通类日程结束后,强制插入30分钟的低强度处理时间,专门处理简单事务,不做深度工作。这30分钟不写入产出统计,它的唯一目的就是让我大脑"换挡"。
第三个习惯,计划时间过长。对应调整:把每日计划的时间盒子从原来的一小时压缩到20分钟,而且只写三件事:今天必须完成的一件事,今天最多做三件事的清单,以及一件如果精力允许再去做的"加分项"。其余的想法全部丢进收集箱,不进入今日计划。
这些调整都是基于数据的,不是拍脑袋。我现在回看第34天,最大的收获反而不是那天修好了什么bug,而是我意识到:长期项目的日常记录,本质上是给自己装一个仪表盘。你不需要每天盯着仪表盘感动自己,你只需要定期看一次,然后冷不丁发现"哦,原来我是这么用完时间的"。
5. 后半程计划:放弃完美日报,转守最小闭环
第34天过完,项目还有66天,我心里很清楚,单靠"打卡"是不可能走到第100天的。打卡式坚持有一个隐藏成本:你要么最终变成敷衍,要么因为某天没打卡而全盘否定自己。这两个结果我都不想要,所以在第34天,我主动把项目的执行标准降了下来——不是降低目标,而是把目标从"每天完美记录"改成"每天完成一个最小闭环"。
5.1 为什么我把每日复盘从长文改成三行
前20天,我每天的日报写得很用力,恨不得把看过的每篇文章、每段代码、每个想法都记进去。结果到了第25天左右,写日报变成了一种负担,每晚一想到"要写两小时复盘"就开始逃避。这其实是记录工具最典型的失败:记录行为的成本超过了记录本身的收益。
现在我把每日复盘改成三行固定句式:
- 今天真正做完的一件事是什么?
- 今天最大的浪费发生在哪个时间段?
- 明天的最小闭环是什么?
这三行字,哪怕状态再差,5分钟内也写完了。它牺牲了记录的"丰富性",但换来了"可持续性"。长期记录项目的本质不是每天都有高光,而是让你在数据层面保持一条连续的线,断掉一天,整条线的分析价值就没了。
5.2 新的每日闭环与"允许失败"的容错设计
执行层面,我现在每天只守一条铁律:晚上11点前,无论进度如何,把当天的记录状态标记掉。完成了就标done,没完成就标missed,绝不允许大脑再花一小时去想"我明天要怎么补"。这个"不补卡"的规则是我从跑步训练里学来的——那些今天缺了跑量就第二天加倍补的人,最后几乎都受伤了。个人项目也一样,你今天没写完,第二天硬着头皮补两天的量,通常第三天你会直接放弃。
合理的容错线应该是一条长一点的曲线:每10天允许出现2次missed,只要平均值还在可控范围,就不启动惩罚机制。很多人把容错设计当成"给自己偷懒留后门",但我的实际体会是,没有容错系统的计划,本质上是在押注自己每天都拥有满格意志力,而现实显然不是这样。
5.3 怎么判断自己是真累了还是在逃避
说到放弃,这也算长期项目里最常被问到的问题。第34天之后我大概率还会遇到"想放弃"的日子,所以我提前给自己写了一条判断标准。不是靠感觉,而是靠几个可验证的信号:
- 睡眠平均值连续三天低于6小时,且白天明显注意力涣散,这是"真累了",该做的是休息,不是硬撑。
- 每天启动时间越来越晚,但启动后的专注时长其实没变,这是"逃避",问题多半出在任务本身。
- 连续三天在同一个步骤反复卡住,且没有尝试新解法,这是"方法失效",需要降级难度或者请教别人。
- 一想到项目就觉得烦,但在做别的琐事时反而有精力,这是典型的"逃避信号",需要重新找回项目的初始动机。
这套判断标准并不完美,但它最大的价值是把"要不要放弃"这个模糊的情绪问题,变成了四个可以回答的事实问题。我在之前的项目里欠过很多次"结局",几乎每一次都是栽在这个地方:把疲惫当成了厌倦,又把厌倦当成了"我不行"。
现在项目走到第34天,我反而比第10天更笃定。第10天靠的是冲动,第34天靠的是一套已经跑通的数据闭环——每天有记录、每周有统计、遇到问题能追踪到环境差异。如果你也卡在自己的某个"第34天",我建议你今晚上不要急着加班赶进度,先花半小时回答一下我前面那张体检清单里的四个问题。项目的问题,基本都能在产品、工具和习惯这三个层次里找到根源,而停留在情绪层面的自责,是这个阶段最浪费时间的动作。