从零构建内部任务与工时追踪系统:立项、选型到踩坑复盘
001项目总结终于能写出来了。这个代号001的项目实际是我接手团队内部第一个完整从立项、开发、测试、上线走完全流程的工具型项目。说“完整”其实有点心虚因为中间踩的坑比预想的多得多光是有记录的反复排查就占掉了三分之一的开发时间。但也正因为踩过这些坑我觉得这个项目总结值得认真写一写。这个项目本身不复杂目标是做一个内部用的任务与工时追踪小系统用来替代我们团队一直在用的共享表格。一开始的需求清单长得吓人任务看板、甘特图、消息提醒、移动端适配、周报自动生成……后来真正做进去才发现很多功能只是“看起来重要”。如果你正在带一个小团队或者准备从零做一个内部工具这篇文章可以供你参考它覆盖了立项、选型、开发、踩坑、上线、复盘这条完整链路也写清楚了很多环节背后的取舍逻辑。1. 立项起点001项目到底要解决什么问题1.1 项目背景与核心痛点我们这个项目启动的导火索很朴素团队一直用共享表格记录每个人的任务和工时每个月月底做统计的时候问题集中爆发。表格里同一个人的名字能有三四种写法任务状态标的乱七八糟有人填了工时却没写对应任务有人写了任务描述但忘了填时长。负责整理的人每个月都要花大半天在清洗数据上还经常被同事质疑统计结果不准。痛点其实就一句话登记和统计两条链路都完全依赖人工缺少约束和反馈机制。表格不能强制必填项不能限制枚举值也不能自动提醒“你这个任务还没填工时”。所以我们决定做一个轻量的内部系统目标非常具体让每个成员每天花不到一分钟完成登记月底统计时点一个按钮就能得到准确结果。这个目标听起来平淡但它决定了后面所有设计的方向。我们不需要一个复杂的项目管理工具我们需要的是一个“数据入口规范、统计口径统一、权限边界清晰”的记录系统。1.2 需求取舍与验收标准立项讨论时团队提了一堆功能第一版清单大概有十多项。后来我们用了两个维度做减法使用频率高不高没有它会不会导致现有流程跑不下去最终砍到只剩四个核心模块任务登记与状态流转待处理、进行中、已完成、已取消工时记录按任务、按日期追加记录统计报表按成员、按任务、按月汇总基础权限控制普通成员和管理员的查看范围区分甘特图是第一刀砍掉的。不是因为不需要而是因为团队现在根本没有人和精力去维护计划日期先把执行真实情况记录下来才是基础。移动端适配也砍了内部团队成员都用电脑办公先做桌面端足够。验收标准我们当时写得很朴素却直接决定了测试方案本月最后一个工作日以前系统能生成上月整月的工时统计报表报表数据与成员实际填报工时完全一致普通成员只能查看自己的任务和工时管理员可以查看全部整个统计流程不需要人工复制粘贴或修改数据。1.3 里程碑安排因为是内部项目我们按周分任务没有做很重的项目管理流程。实际排期如下阶段时间工作内容需求梳理第1周访谈团队日常登记习惯确定功能清单与验收标准原型设计第2-3周画页面草图、确定数据模型、设计接口草案开发实现第4-6周前后端并行开发核心接口先联调测试修复第7周内部试用、Bug修复、补测试用例灰度上线第8周组长先试用再放开全员现在回头看这个排期最大的问题是我低估了测试修复的耗时第7周实际上是连轴转的一周每天都能收到新问题。后面我会专门讲其中的典型坑。想提醒的是内部项目如果时间排得太满灰度期就很容易变成另一个开发期一定要留缓冲。2. 技术方案选型与整体设计2.1 选型思路技术选型这件事内部工具和对外产品有本质不同。对外产品要考虑团队长期演进、招聘成本、流量体量内部工具就一个原则团队里维护的人最少、上手最快、最容易排查问题的方案就是好方案。我们最终选定的组合是服务端轻量的 Python 服务框架逻辑简单生态成熟前端Vue 加现成的组件库开发速度快数据库PostgreSQL因为报表统计涉及聚合查询PostgreSQL 的日期函数和窗口函数写起来很顺手部署Docker Compose 跑在团队一台小主机上配置简单也能隔离环境差异。这个组合没有什么炫技成分。团队维护者一共三个人Python 和 Vue 大家都有基础Docker Compose 也是现成的实践。如果用过于复杂的微服务架构或消息队列对这个项目就是纯负担。2.2 数据模型设计数据模型是整个项目最值得花时间的地方。我见过太多项目在界面上折腾了半天最后卡在表结构上重新改。我们第一版的数据表设计分成三张核心表和一个扩展表。用户表主要字段id姓名角色普通成员 / 管理员所属组状态启用 / 停用任务表主要字段id标题描述指派人状态待处理 / 进行中 / 已完成 / 已取消优先级截止日期创建人创建时间更新时间工时记录表是这个项目最核心的表id任务id用户id记录日期工时分钟数备注创建时间最初我们差点把工时记录直接塞进任务表每一行任务带一个“工时”字段。后来提前推演了一下统计场景才改过来如果业务上需要统计某个成员某个月在某个任务上投入了多少时间把工时间内嵌任务表会非常难查。拆成独立的工时记录表后每次填写都是插入一行新记录统计时按用户、任务、月份做分组聚合就可以语义非常清晰。这算一个很基础的设计决策但它在后来避免了好几次“改表”的灾难。2.3 服务端接口与权限设计接口设计遵循了最简单直接的方式按资源划分接口写操作统一走事务。文档里我们定义了下面几组接口任务接口创建任务、更新任务状态、修改任务字段、查询任务列表工时接口添加工时记录、查询我的工时记录报表接口按月生成成员工时汇总、按任务汇总工时用户接口登录、获取当前用户信息、管理员管理成员权限上只分两个层级。普通成员登录后只能访问“自己的任务”和“自己的工时”管理员可以查看全部数据并在报表页面按成员筛选。权限校验在接口层做统一拦截前端菜单也会根据角色隐藏入口但真正的控制完全以后端为准。这个统一拦截看起来很简单实际非常重要。很多内部系统管不住权限就是因为觉得“页面隐藏了就行”但接口一旦被人猜到并直接访问数据就裸奔了。我们专门写了一个小的依赖函数所有需要登录态的接口直接调用从源头堵住漏配的可能。2.4 前端页面结构前端一共三个主页面没有多余导航。任务看板页按状态分列展示任务卡片支持拖拽变更状态。这个页面最耗精力的是拖拽后的排序权重问题如果不维护一个 order 字段卡片拖到中间位置就会乱跳回末尾。我们后来在任务表里加了一个 sort_order拖拽完成后一次性批量提交所有卡片的新顺序。工时记录页默认展示当天的任务列表旁边加上“记录半小时”“记录一小时”的快捷按钮另外提供一个补录入口允许选择过去七天内的日期进行补填。这个补录场景是真实需求周五忘记填的人很普遍。统计报表页默认显示当月统计表支持切换月份管理员可以按成员筛选。表格每一行是一个成员对一个任务的合计工时最后一行是总计。3. 开发落地核心功能实现的过程与要点3.1 任务状态流转的实现细节任务状态看起来只是四个枚举值但边界条件必须提前定清楚否则就是无数个隐藏Bug的来源。我们定义的流转规则是待处理 → 进行中进行中 → 已完成进行中 → 待处理已完成 → 进行中表示重新打开待处理 → 已取消已取消 → 待处理这里有一个关键约束已完成的任务不能直接变成已取消已取消的任务也不能直接变成已完成。原因很简单如果允许任意跳转月底统计报表里就会出现状态矛盾的数据比如同一个任务既“已完成”又“已取消”。所以后端在更新状态时不是简单赋值而是先判断当前状态和目标的组合是否在允许的流转集合里。前端卡片拖拽也是调用同一个接口接口返回业务码提示“不允许的流转关系”卡片会自动回弹原位置。这个小细节能避免用户误操作以后对着列表困惑。3.2 工时记录模块补录与防覆盖工时记录模块的核心操作是“插入而非更新”。我们设计了一个很明确的约束同一个人、同一个任务、同一天可以多次记录工时每次都是新增一行而不是覆盖之前的值。为什么这么设计因为实际使用场景里一个人在某个任务上投入的时间本来就可能是分段的。上午做了两小时下午又做了一小时如果只允许一条记录就需要用户反复修改“总工时”很容易改错。拆成多条记录后每一次提交都保留独立的时间戳和备注统计时再聚合账目最清楚。这是我个人在这个项目里最认可的一个决定。它避免了并发覆盖问题也给后续统计留了更细的粒度。3.3 统计报表的SQL与聚合逻辑报表模块写起来并不复杂但特别容易在细节上出错。月度统计的核心SQL长这样以PostgreSQL为例SELECT u.name AS user_name, t.title AS task_title, SUM(r.minutes) / 60.0 AS hours, COUNT(r.id) AS record_count FROM work_records r JOIN users u ON r.user_id u.id JOIN tasks t ON r.task_id t.id WHERE r.record_date DATE_TRUNC(month, CURRENT_DATE) AND r.record_date DATE_TRUNC(month, CURRENT_DATE) INTERVAL 1 month GROUP BY u.name, t.title ORDER BY u.name, hours DESC;这里有两个容易踩的坑我先说给可能照着写的朋友。第一个坑时间范围判断。如果直接用日期字段做等值匹配月末最后一天的数据极可能漏掉因为跨月的边界条件很多人写不好。我习惯用“大于等于月初且小于下月初”这种左闭右开区间能避开当天零点附近的数据边界问题。第二个坑工时单位。我们一开始让用户按“小时”填前端可以输入0.5、0.25这类小数数据库字段用的浮点数。结果累计之后出现了0.699999999这种数字报表显示非常难看。后来改成分钟整数存储前端展示时才转成小时彻底解决了精度问题。3.4 写操作的事务与统一返回结构内部工具虽然并发量不大但写操作的事务保护不能省。所有新增任务、更新状态、添加工时记录后端都在一个事务上下文中执行任何一个步骤失败就整体回滚。接口返回结构也做了统一格式固定为{ code: 0, message: success, data: {} }错误时 code 返回对应业务码message 给出人能看懂的原因。前端拿到 code 不为0时统一弹出提示不会因为某次请求失败导致页面白屏或数据丢失。这个小约定让前后端联调快了很多不需要各自猜返回格式。4. 踩坑记录三个最有代表性的问题排查链路4.1 并发写入导致工时记录被覆盖上线后第一周我们收到反馈两个同事几乎同时给同一个任务补录当天工时后保存的那个把前一个的记录覆盖了。我排查这个问题花了一个下午。第一反应是并发锁问题但看数据库锁日志没有异常。接着我打开后端日志发现一个关键现象每次保存前端提交过来的不是一个“新增记录”动作而是一个“完整对象保存”动作。后端的处理逻辑带了UPDATE语义它先按“用户任务日期”查找已有记录找到就直接更新没找到才插入。而前端为了省事把单条记录当成了“可编辑的表格行”。也就是说用户在页面上看到之前有人填过就以为可以改改完提交后系统不是新增而是把同条件下原记录整体覆盖掉。解决方案分两层。数据层禁止在这个场景下做UPSERT工时记录只允许INSERT后端如果收到带id的保存请求就直接拒绝。统计层增加了一条数据校验规则每天每个任务每个成员最多只能有一条“系统自动生成”的初始记录如果出现多条说明需要人工关注。这个问题教育了我一个原则写代码前先想清楚“你的接口是幂等更新还是新增事件”数据类型定错了后面所有排查都会被带偏。4.2 月中报表和月底对不上第二个坑更隐蔽。月中我们截图发的统计报表数字和月底最终报表差了0.5小时。大家一开始互相怀疑有人补录没通知后来查下来发现是典型的浮点误差累积。我们最初的工时字段是“小时”数据库类型是浮点数。用户在页面上填0.5计算之后变成0.50填0.1二进制存储会变成0.10000000000000001。单条记录误差微乎其微但一个成员一个月积累几十条记录舍入误差就可能在末位显示成0.9999或1.00001报表格式化后视觉上对不上。另外还有一个重复统计问题。状态变成“已完成”后成员又“重新打开”并补录了工时这时候如果报表逻辑简单地按月、按任务分组求和就会把前后两段重复算进去。我们后来加了明显的历史记录标识只统计“非已取消”且“最新状态”对应的有效记录。这个坑让我以后形成了一个习惯凡是跟金额、工时、库存相关的字段一律用整数最小单位分钟、分、克等存储永远不在数据库里存带小数的“展示单位”。4.3 开发机正常但服务器部署失败开发阶段一切正常docker-compose 在本地一拉就跑。结果部署到团队那台小主机上第一件事就翻车中文乱码、数据库连接超时、静态文件加载不出来三个问题同时冒出来。排查顺序很重要我当时是这样一步步缩小的第一步看后端主日志发现数据库连接报错提示字符集不匹配第二步用命令行工具直接连数据库执行查询正常但查看数据库默认编码发现不是UTF8第三步对比本地和服务器环境发现docker-compose.yml里数据库服务少了字符集环境变量本地因为镜像缓存默认值碰巧正常服务器上的基础镜像版本不同默认字符集就暴露了问题第四步再检查代码里的路径配置发现有个模块读取配置文件时用了写死的本机绝对路径容器里当然找不到这个路径导致整个服务启动了一半就挂了。最后我们把所有环境相关配置全部改成从环境变量读取并通过部署脚本统一注入docker-compose文件里显式设置字符集和时区启动流程加了健康检查前端页面能正常返回后才标记服务可用。这类“换台机器就坏”的问题根因大概率是环境配置不是显式依赖。内部项目更要养成习惯写死路径和默认值看似省事后面排查就是浪费时间。5. 测试、灰度上线与运维保障5.1 测试用例设计与自动化程度内部项目没有条件做完整自动化测试但至少把核心链路的手工测试用例做到了可重复执行。我们整理了一张测试矩阵每次发版前按表跑一遍用例编号场景操作步骤预期结果TC01任务创建新建任务并指定负责人和截止日期任务出现在看板的待处理列TC02状态流转将待处理任务拖到进行中状态更新且看板卡片位置变化TC03非法流转将已完成任务直接拖到已取消系统拒绝并提示流转规则TC04工时登记对某任务添加30分钟记录统计报表该任务总工时增加30分钟TC05跨天补录补录3天前的工时记录补录成功且月度统计中包含该数据TC06权限隔离普通成员修改他人任务接口返回无权限错误TC07报表导出生成上月统计表格数据与明细记录逐条核对一致这个表看着简单实际价值非常大。团队后来新增功能时都靠它做回归避免“改好一个功能弄挂一个旧功能”的尴尬。5.2 灰度上线方案与回滚计划因为用户全是内部成员灰度发布不需要搞很重的规则但节奏必须有。我们的方案分三步第一步只开放给三位组长试用每天收集反馈重点关注统计报表数据是否准确第二步一周后开放给全团队但只用来登记新任务不要求历史数据一次性迁移第三步跑完一个完整的月底结算周期后确认报表数据和人工统计一致再宣布系统正式启用。灰度期间如果发现严重问题我们已准备好回滚服务上一版本的镜像还在Docker仓库里直接切换回旧容器即可。因为数据全部存在PostgreSQL里回滚不会丢数据最多是页面功能回到旧体验。这里值得强调的是内部小系统也要保留回滚能力。不要觉得就几个人用出了问题现场改就行。真到月底统计当天出了错你会发现“快”比“完美”重要得多。5.3 监控与备份的落地内部工具我们没上复杂的监控系统只做了三件最基础的事每天早上检查服务端口是否正常响应数据库每天凌晨3点做一次全量备份保留30天备份文件放在另一块磁盘接口错误日志按天滚动保留14天方便出问题时回溯。备份命令很简单用的就是PostgreSQL自带的备份工具定时任务写好之后不用人管。有一次真的派上了用场某个晚上我们改脚本误删了一张配置表第二天早上发现后直接从备份恢复到凌晨状态只丢失了当晚添加的少量临时配置影响可控。很多内部项目死就死在“觉得不用备份”一旦出了问题之前所有补救手段都不存在。备份占用的磁盘可能就几个GB但它是系统信任感的底线。5.4 用户反馈的主动收集上线不等于结束这是这个项目给我最深的体会之一。用户反馈不会自动跑来找你尤其是内部工具很多人遇到小问题宁愿忍着也不提。我们在前两周做了几件事每天下午在团队群里发一条消息问“今天系统有没有让你觉得卡壳的地方”每周约两位成员做一次五分钟的短访谈带他们实际操作一遍最常用的流程观察哪些动作犹豫了、哪些按钮找不到。用这种方法我们发现了一个被人忽视的小问题工时记录页面的“补录入口”藏得太深很多用户以为只能记录当天。改成一个更显眼的按钮后补录使用率立刻上升月底数据少填的问题明显减少。6. 项目复盘与后续演进建议6.1 复盘会上我们总结的几条经验项目正式稳定运行后的第二周我们开了一次复盘会。没有走形式就是对着开发记录一条条过最终沉淀下来几条自己认账的经验需求范围要严格先做最小可用版本。甘特图和移动端如果第一版就做上线时间至少要拖一个月测试时间不能砍。第7周几乎每天都在修复中度过的苦源头是开发期把接口自测压缩得太少数据模型比界面更重要。工时记录表独立设计和分钟整数存储这两个决定让后面所有统计都轻松了日志要早打。很多问题复盘时才想起来如果当时有详细的请求日志定位时间至少减半。6.2 沉淀下来的可复用资产项目带给团队的不止一个工具还有一批可以复用的资产需求文档模板、接口返回规范、测试用例矩阵、Docker部署脚本、月度报表的SQL片段。这些东西现在都放在团队的知识库里后续再做内部工具可以直接引用。尤其值得说的是接口返回规范。它本来是这个项目里随口定的约定后来成了我们内部所有小工具的默认标准。新成员接手时不需要重新理解接口格式沟通成本降了不少。6.3 下一步的功能规划与优先级001项目稳定之后我们列了下一个版本的候选需求按使用频率重新排了优先级周报自动汇总每周五下午定时推送直接替代手动整理任务超期提醒超过截止日期自动标注红色移动端简单查看只读模式方便不在工位时确认状态更自由的自定义标签按成员需要打上“会议”“沟通”“编码”等分类这里我想提醒的是规划后续功能时不要迷信“技术难度”要看团队实际使用习惯。我们一开始觉得移动端重要但调研下来大家最烦的是每周手动写周报所以把周报自动汇总排到了第一。6.4 写给后续接项目的人最后说点实在话。如果让我重新做一遍001项目我会在立项当天就把数据模型画出来把测试用例写出来。这两件事做在前面后面能少加班两周。数据模型决定了系统能长多大测试用例决定了你能不能放心改代码。内部工具项目不会像对外产品那样光鲜但它的价值恰恰在于贴身解决真实问题。只要团队成员每天愿意打开页面登记说明它真的做到了“好用”。001项目就是这样一个起点它不复杂但它把流程走全了把坑踩明白了这就很值得。