说真的,做完这套被我命名为“Homework”的作业管理小工具,我最想聊的其实不是代码本身,而是“作业焦虑”这件事到底能不能被工具化解。期末那一阵,家里孩子每天抱着一堆作业,一会儿说语文忘写,一会儿说数学卷子找不到了,我的手机里全是老师发来的作业通知,东一条西一条,根本拼不出完整的任务清单。后来我决定自己动手,把“Homework”从纸上的一句话变成一个真正能收口、能提醒、能复盘的作业管理系统。这篇文章就是完整的实战复盘,包含需求拆解、数据建模、代码实现、部署方案,以及我在实际使用中踩过的坑。如果你手头有作业管理、任务清单、学习追踪类的需求,无论你是学生、家长还是想去校内做工具的开发者,这篇内容都应该能帮到你。
1. 到底要做成什么样:Homework 的设计起点
1.1 一个被“作业焦虑”逼出来的需求
触发我做这个项目的场景很具体:孩子连续两周丢三落四,三科作业拆成十几个子任务,有的要周六前拍照上传,有的要下周三课堂默写,还有几个小组合作任务要凑齐组员才动得了笔。我开始试图用普通待办清单来管,但试了一圈发现,普通清单根本不懂“作业”这种任务类型的特殊逻辑:它有关联课程、有截止时间、有预估完成时长、有提交/检查环节,还有延期风险。单靠一条条打勾,解决不了“什么时候该做”“做到哪一步了”“还剩多少时间”这三个核心问题。
所以我给“Homework”定下的目标很朴素:把所有作业收进同一个地方,用最短的时间录入,每天早上和晚上各推送一次当天待办,每个任务能快速切换状态,每周出一份完成情况统计。工具本身不替人写作业,但它能让人清楚地知道“现在该做什么”,这和靠记忆硬扛完全是两种体验。
1.2 三条硬约束,决定了技术方案方向
一个工具如果没人愿意用,再炫酷也白搭。我在设计之初给自己定了三条硬约束,每条都直接影响后面的技术选型。
第一,录入成本必须足够低。一条作业如果要在手机上点七八下才能登记完,用两天就会被嫌弃。理想状态是打开页面、填几个字段、点保存,三秒内结束。
第二,提醒必须具备“主动性”。光靠人自己打开页面看是不行的,必须到时间就主动推送。而且推送不能只在电脑浏览器里弹通知,手机端也要能收到,哪怕页面关着也得响。
第三,数据要有沉淀价值。每一门科目、每一周完成率、每一次拖延,都应该能变成可视化的东西。这样月底复盘时,不是凭感觉说“好像比上个月进步了”,而是直接看数据,这周到底哪天任务积压了。
这三条硬约束直接把思路拉向了“轻量级自建网页+定时消息推送+数据本地存储”的组合。没有引入重型框架,也没有加复杂权限系统,就我一个人维护,够用就好。
1.3 为什么不做现成App,而要自己搭
其实市面上现成的作业记录App不少,但它们大多有两个问题:一是广告和社交功能太多,打开之后容易被无关内容带走注意力;二是数据结构固定,没法按我的需求定制“预估时长”“课程权重”“状态闭环”这些字段。
自己做的好处很简单——每一项数据都按我的逻辑来,所有提醒语都能自控,数据也完全在自己手里。代价则是从零开始,需要写页面、写接口、跑定时任务。好在这个项目边界比较窄,不涉及复杂权限、不涉及多人协同,整体工程量完全在可控范围内。
2. 核心设计拆解:作业任务的数据模型
2.1 一条作业不只是“一个标题”,而是五个关键字段
我建模的时候,把一条作业拆成了五个核心字段,分别是“课程”“标题”“截止时间”“预估时长”“当前状态”。为什么这么拆?因为作业管理的痛点,恰恰来自这些维度没有被分开对待。
课程字段决定归类和统计口径,没有它,每周各科完成率就无从谈起。标题负责描述任务内容,我额外要求录入时写“动词+对象”的格式,比如“完成数学卷子第三单元”“背诵语文文言文两篇”,而不是只写“数学卷子”“语文文言文”,因为动词能让孩子更清楚动作目标,避免想半天才想起到底要干嘛。截止时间是最硬的约束,所有排序、倒计时、提醒都围着它转。预估时长则用来做“今天能不能做完”的判断,如果今晚剩余可支配时间只有两小时,而待办任务加起来要四个小时,系统就该主动提示“任务过载”,而不是等睡前才发现做不完。
当前状态我设计了四个档位:未开始、进行中、已完成、待复盘。前三个好理解,第四个“待复盘”是我后加的,专门标记那些虽然交了,但老师批改后有问题的作业。没有这个状态,错题和订正任务很容易被遗忘在“已完成”里。
2.2 提醒机制的核心:不是“每天提醒”,而是“该做的时候提醒”
提醒这步我做了三轮迭代。第一版是固定时间推送“今日待办”,早上七点半一次、晚上六点一次,内容就是当前状态为“未开始”和“进行中”的任务列表。这个方案最大的问题是粒度太粗,比如一份三天之后才截止的调查报告,可能连续被提醒三天,容易产生“反正还早”的疲劳感。
第二版我引入了“倒计时分级提醒”。任务进入三个关键时间点会触发推送:
- 剩余时间大于三天时,只在每日汇总里出现,不单独打扰
- 剩余时间进入24小时内,单独推送一次“明天截止”预警
- 剩余时间低于两小时,且状态还没到“已完成”,立刻追加推送“紧急处理”
这套机制实测下来,比普通待办清单的“截止当天才提醒”要从容得多。我自己的体会是,孩子根本不需要知道系统背后怎么算,但她会在前一天晚上就看到“明天截止”的预警,然后主动把时间腾出来。
2.3 排序算法:别再用“截止时间单一排序”了
如果所有作业只按截止时间排序,那所有长线任务永远排在后面,永远不被看见。我在“今日待办”里用的排序公式是这样一个综合评分:
紧急度评分 = (1 / 剩余小时数) × 加权系数 + 预估时长系数 + 课程优先级系数
剩余小时数越少,倒数越大,紧急度越高;预估时长越长的任务,越应该被安排到精力好的时间段,而不是拖到睡前;课程优先级是我手动维护的一个表,像数学、物理这类需要大块思考时间的科目,权重会高一些。
举个例子,两条作业同时截止,但一条预估半小时、一条预估两个半小时,排序时后者会排到前面提醒,因为完成成本更高。这个逻辑从一开始的“按最后期限排”改成了“按完成成本排序”,体感上的变化非常明显。它不是把DDL忽略了,而是在DDL的基础上叠加了“完成难度”这个维度。
3. 实操过程:从零搭建这套作业管理系统
3.1 技术选型:我最终选了“极简后端+原生前端+定时任务”
项目一开始,我考虑过三条路线:原生App、小程序、网页。原生App开发周期长,还要考虑两端发布;小程序有审核而且依赖平台规则;最后选了网页方案,因为手机浏览器直接打开就能用,没有安装流程,改版也快。后端用轻量级Python框架,数据库直接用SQLite,部署在一台低配置云服务器上,域名和HTTPS都配好,访问链路足够简单。
定时提醒我最初用系统自带的定时任务来做,后来发现部署环境有时候会把服务中断,又开始折腾常驻进程和看门狗。最后迫不得已,我退回了一个更稳的土办法:定时任务仍然用系统的cron,但脚本会在机器上留一个心跳日志,每天检查一次日志文件时间,如果发现连续两小时没有心跳就直接告警推送给自己。
3.2 数据库设计:五张表支撑全部功能
整个系统最核心的是五张表,我逐一说明:
第一张是“作业主表”,字段包含课程ID、标题、截止时间、预估分钟数、状态、创建时间、更新时间。这张表是所有页面的数据源。
第二张是“课程表”,包含课程名称、显示颜色、周频率、权重。显示颜色管卡片配色,权重管排序算法,周频率管每周统计期望值。
第三张是“提醒日志表”,记录每次提醒推送的时间、任务ID、推送类型。这张表很重要,它帮我排掉了“重复提醒”的bug,也方便排查“为什么这条任务提醒了三遍”。
第四张是“完成记录表”,用来存每次任务从“未开始”变成“已完成”的时间戳。统计“这周平均响应时长”靠的就是它。
第五张是“复盘表”,存每周课程完成率、超时任务数、连续打卡天数等聚合结果。我不太想每打开一次统计页就全量计算一遍,所以让定时任务每天凌晨把这些结果算好写进去,页面展示时直接读。
3.3 建表脚本与核心代码:可以直接复用的版本
数据库脚本我用纯SQL实现,关键表结构如下:
CREATE TABLE courses ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, color TEXT DEFAULT '#3B82F6', weight REAL DEFAULT 1.0, created_at TEXT DEFAULT (datetime('now', 'localtime')) ); CREATE TABLE homework ( id INTEGER PRIMARY KEY AUTOINCREMENT, course_id INTEGER NOT NULL, title TEXT NOT NULL, deadline TEXT NOT NULL, estimate_minutes INTEGER DEFAULT 60, status TEXT DEFAULT 'todo', created_at TEXT DEFAULT (datetime('now', 'localtime')), updated_at TEXT DEFAULT (datetime('now', 'localtime')), FOREIGN KEY (course_id) REFERENCES courses(id) ); CREATE TABLE remind_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, homework_id INTEGER NOT NULL, remind_type TEXT NOT NULL, created_at TEXT DEFAULT (datetime('now', 'localtime')) ); CREATE TABLE finish_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, homework_id INTEGER NOT NULL, finished_at TEXT NOT NULL );Python端的核心逻辑主要集中在三个函数上:查询当日待办、计算排序评分、生成推送文案。当日待办的查询逻辑是:
def get_today_tasks(cursor): now = datetime.now() today_start = now.replace(hour=0, minute=0, second=0, microsecond=0) # 未完成任务 + 已完成但今天才完成的任务 cursor.execute(""" SELECT h.*, c.name AS course_name, c.color, c.weight FROM homework h JOIN courses c ON h.course_id = c.id WHERE h.status IN ('todo', 'doing') OR (h.status = 'done' AND h.updated_at >= ?) ORDER BY h.deadline ASC """, (today_start.isoformat(),)) return cursor.fetchall()这里有个细节,就是“今日待办”不只是未完成任务,还包括今天刚完成的任务,方便晚上复盘“今天到底干了多少”。如果你也想参考,这一步建议直接抄。
排序评分函数:
def task_score(task, now): deadline = datetime.fromisoformat(task["deadline"]) remaining_hours = max((deadline - now).total_seconds() / 3600, 0.25) urgency = 1.0 / remaining_hours estimate_hours = task["estimate_minutes"] / 60.0 return urgency * 10 + estimate_hours + task["weight"]为什么要加一个“最低剩余时间”防止除以零?如果任务已经过期,剩余时间可能是负数,直接参与计算会得到负数评分,导致过期任务排到最底下。加0.25小时的保底,保证过期任务永远在最上方,逼着你先处理逾期项。
3.4 页面交互设计的几个细节
前端我只做了两个页面:录入页和看板页。
录入页的设计原则是“一条流水线”。从上到下依次是课程选择、任务标题、截止时间、预计时长,最后一个大按钮“存草稿+保存”。我用了一个小技巧:课程选择默认记住上次选的那一门,截止时间默认从当前时间向后推24小时,预估时长默认填60分钟。也就是说,用户如果赶时间,只需要填一个标题,连点两下就能录完一条作业。实测下来,从打开页面到存好,最快只要四秒。
看板页的核心是卡片列表。每张卡片上显示课程名称、任务标题、倒计时、预估时长和状态按钮。我特意做了状态颜色:未开始是浅灰,进行中是蓝,已完成是绿,逾期是红。倒计时也有自己的规则:超过48小时显示“剩余X天”,低于48小时显示“X小时X分”,低于3小时开始用红色并加上轻微闪烁的样式。这个设计不是为了好看,是为了让人扫一眼就能识别出风险等级。
打卡操作我做得极其克制:每个卡片上只有一个主按钮,未开始显示“开始做”,点击后变“完成”,不会有二级菜单,也不会有弹窗确认。这个“不要问第二次”的设计,极大提高了孩子的点击意愿。如果系统弹窗问“你确定要完成吗?”,体验就会冷掉,仿佛做作业是一个需要反复确认的命令而不是一个状态记录。
3.5 部署与自动化:从开发机到云端
本地写完第一版后,我开始部署。云服务器选的是最低配置的Linux虚机,Python环境装好依赖后,用反向代理绑定端口,再配上HTTPS证书。数据库文件放在一个专门的数据目录下,每天凌晨用打包脚本备份一次,保留最近30天版本。
定时任务这一块,我写了两个cron档位:
0 7 * * * cd /opt/homework && python3 remind.py --type morning 0 18 * * * cd /opt/homework && python3 remind.py --type evening */30 * * * * cd /opt/homework && python3 check_heartbeat.py第一个是早间汇总,第二个是晚间汇总,第三个是半小时一次的心跳检查。心跳检查的逻辑很简单:如果脚本发现提醒日志表里最后一小时的记录数为空,并且当前正好处于应该推送提醒的窗口内,就会发动一个“系统可能挂了”的标记。我用这个方式,成功在三次异常宕机的时候第一时间收到了消息。
4. 常见问题与排查技巧实录
4.1 问题一:提醒怎么老不响?排了一圈,最后发现是时区惹的祸
上线后第三天,孩子说“早上啥都没收到”。我当时第一反应是定时任务挂了,检查cron一切正常,再查进程也都活着,最后发现是服务器默认时区是UTC,而我所有截止时间都是用本地时间存储的。Python脚本从数据库读出来的时候,把字符串当成UTC时间处理,减一减,倒计时凭空多了八个小时,自然不可能触发24小时预警。
解决办法是在所有涉及时间的代码入口处统一做时区转换,服务器环境变量直接设置成本地时区。这一条如果你要沿用我的方案,务必第一时间处理,不然后面所有时间计算都会带着隐蔽偏差。
4.2 问题二:已完成的任务“复活”了,状态一片混乱
状态混乱是我用得越久越明显的痛点。起因是孩子有些任务并没有真正的截止时间,只是“本周内完成”,我把截止时间往后拖,结果昨天标记完成,今天定时任务又因为截止时间在未来自动生成了待办,显得任务没做完。排查后发现,问题出在我查询“今日待办”的SQL过滤条件上:它单纯根据状态判断,没有把“已完成”但“截止时间晚于今天”的任务排除掉。
调整思路很简单:列表页显示逻辑改成“未完成任务,加上当天完成的所有任务”,而不是“所有截止时间在今天的任务”。这个小小的语义差异,直接决定了一个清爽的看板还是一个到处冒泡沫的看板。
4.3 问题三:数据是空的,因为没人用
工具做出来最怕的不是bug,而是没人打开。最开始一周,孩子完全不主动打开网页,提醒推送也经常被当成垃圾消息不细看。后来我加了一个“连续打卡”机制,每完成一个任务,首页就会亮起一个小星星图案,连续一周每天都至少有五个完成记录,周末可以兑换30分钟的自由活动时间。这不是什么复杂游戏化体系,但亲测对低龄段学生特别有效。
另外一个感同身受的点是,自己做工具容易陷入“功能导向”的误区,单纯觉得把数据管好就会有人用,但实际上工具得像一个朋友,每天看见你在做什么,给你一点反馈。一旦反馈机制跟上,使用频率和习惯度就会陡增。
4.4 问题四:统计图表为什么和我的记忆对不上
我一度觉得“上周明明做了很多作业”,但统计页显示完成率只有60%。核查后发现,有些任务是在截止日期之后补交完成的,清理时把它们分到了逾期完成,而“完成率”指标只算按时完成数。这类口径问题要尽早定下来,默认“完成率=按时完成/全部任务”,另外单列“超期整改率”。如果一开始就混在一起,每周复盘就会自欺欺人。
这里的经验是:做统计指标必须定义清楚口径,并且把这些口径在页面上展示出来,不然看到数字的人会用自己的直觉去理解它,双方根本不在一个频道。
5. 做完了,说点实在话:这套系统的真实体验与扩展方向
5.1 用了三周后的静悄悄变化
系统性使用三周后,最大的变化不是成绩单,而是“睡前焦虑”明显下降了。原来九点钟洗完澡还要翻聊天记录看明天有什么作业要交,现在只需要早上打开看板页扫一眼,晚上系统会把当晚该做的任务推过来。孩子主动说了一句“至少知道该干嘛”,对我来讲这就是项目最大的成功。
从数据上看,逾期率从第一周的41%降到了第三周的15%,连续一周完成率超过85%的次数出现了两次。我不认为这是系统有什么魔法,只是“知道何时该做”这件事本身,就是一种巨大的行动力解放。
5.2 扩展方向:从“管作业”到“管任何截止日期”
这套模型本质上是一个单人的截止日期管理系统,所以稍微改一改词汇,它就能变成年度目标追踪器、阅读计划管家、写作任务看板甚至是家庭琐事提醒器。
我目前正在做的重构是引入“多目标”支持,等于一个作业任务可以拆成多个子任务并分别计时。比如“完成数学卷子”可以拆成“订正错题”“重做计算题”“整理错题本”,三个子任务共享同一个截止时间,但各自独立计状态。这样拆完之后,任务看着更具体,完成起来也更有节奏感。
5.3 给想复刻一套的人的三点建议
第一,先搞定录入速度和提醒可靠性再聊其他。这两个功能占整体好用程度的八成,其他都是锦上添花。
第二,数据结构一定要给状态留足余量。哪怕你现在只分了“未开始”“已完成”,也要在数据库里给“进行中”“待复盘”留位置,因为真实使用中一定会冒出这些需求,到时候再改表结构代价就大了。
第三,别一上来就写大而全的权限和多人协作功能。自己做给自己用,单用户模型就是最大的效率优势。如果多人一起用,那就得多处理“谁可以改这条任务”“改完状态要不要通知别人”这些额外复杂度,吃力不讨好。
最后再分享一个实际体会:做这类工具,最难的事情不是你写不出代码,而是你要像一个傻子一样去观察真实的使用场景,然后愿意砍掉那些看起来很酷但不切实际的功能。记录一条作业只要四秒钟,看到今天要做的事只要一秒钟,这两件事加起来,比任何花哨的数据可视化都更有价值。如果你也打算做一个类似的东西,我强烈建议先把“最快录入”和“清晰呈现”这两个目标钉死在墙上,再去考虑别的。