FlowDesk二期阶段总结:数据库迁移、权限重构与实时通知的复盘

FlowDesk二期阶段总结:数据库迁移、权限重构与实时通知的复盘 写阶段总结这件事我一直有个观点真正有价值的不是那份文档本身而是强迫自己把过去几个月的决策重新过一遍的过程。尤其是项目做到一半的时候往前看全是问题往后看全是选择不找个时间把思路理顺很容易在细节里越陷越深。这篇阶段性开发总结整理的是我们团队一个内部项目——一套面向中小团队的轻量级工单管理系统FlowDesk的第二阶段开发记录。从最初的需求梳理到核心模块落地前后花了大约四个月。文章不会贴完整的项目代码而是把我在这个阶段里做过的关键决策、踩过的坑、以及最终沉淀下来的方案完整复盘一遍。如果你也在做类似的团队协作工具、后台管理系统或者正处于项目中期需要一次系统性回顾这篇内容应该能给你一些参考。先说清楚一个前提FlowDesk不是什么复杂的分布式系统它就是一套典型的业务型Web应用——前端Vue 3 TypeScript后端Node.js NestJS数据库最初用的MySQL后来迁移到了PostgreSQL配套Redis做缓存Docker Compose负责本地和测试环境部署。整体体量不大但正因为不大很多问题反而更有代表性技术选型怎么取舍、需求边界怎么控制、踩坑之后怎么快速定位。这些问题在大项目里有成熟的框架兜底在小项目里全靠自己判断。我的核心体会是阶段总结最重要的不是罗列做了什么而是讲清楚为什么这么做以及哪些决策回头看是不划算的。1. 这个阶段完成了什么目标回顾与实际落地范围1.1 半年前定的计划和最终交付的差距在哪FlowDesk这个项目第一阶段做的是最基础的用户认证、工单CRUD、简单的角色区分。第二阶段启动的时候我们列了一个看起来不算夸张的目标清单多级权限系统、工单实时通知、附件管理、全文检索、以及一套操作审计日志。按当时的粗略估算这些够做三到四个月。实际做下来最终交付的情况是这样的计划功能原定状态最终状态说明多级权限系统完整交付完整交付做了两版第一版推倒重来了工单实时通知完整交付完整交付方案从WebSocket改成了SSE附件管理完整交付部分交付只做了本地上传对象存储延后全文检索完整交付延后到第三阶段依赖数据库迁移周期不够操作审计日志完整交付完整交付比较顺利因为设计得早数据看板未列入计划额外增加需求方中途提出最后用两周做了个简化版这个表格我是特意留着的不是为了展示我们完成了多少而是想说明一个规律阶段总结里最诚实的部分不是那些完成的模块而是那些主动砍掉或者延后的需求。全文检索之所以延后根本原因在于我们中途决定把数据库从MySQL迁到PostgreSQL。这个迁移本身就是个大工程中文全文检索方案在MySQL上需要额外配置ngram插件效果也不算理想与其两头顾不如先顺好主力存储。附件管理的对象存储延后则纯粹是成本权衡团队没有专职运维自己部署MinIO再加内网穿透测试周期会明显拉长不如先用服务器本地磁盘顶着。1.2 需求膨胀时怎么判断什么该砍、什么该留第二阶段进行到第三个月的时候需求方提了一堆新想法工单要支持自定义字段、要能做SLA超时统计、要接入企业微信机器人、要出一个移动端适配版。每一个单看都合理但全都加进来这个阶段恐怕要拖到半年。我在这个项目里学到的砍需求思路可以总结成三个问题第一个问题这个需求是提升已有核心链路还是开辟一条新链路自定义字段属于前者它对工单编辑和展示页的改动是全局性的企业微信机器人属于后者它需要处理回调、消息格式转换、重试机制跟现有代码几乎没有交集。优先级上前者永远高于后者。第二个问题不做这个需求用户手头有没有临时替代方案SLA超时统计如果来不及做可以让管理员每周手动导一次数据在Excel里筛虽然痛苦但能撑住而权限系统如果出错用户根本没法正常开工单这是硬阻断。第三个问题这个需求能不能拆成简化版先上数据看板就是个典型例子。完整版要做趋势图、分布图、多维筛选但简化版只需要三个数字——今日新增、今日待处理、平均响应时长。我们直接在前端页面里用三个卡片展示后端只加了一个聚合查询接口两周搞定效果也够用。砍需求不是偷工减料而是把有限的时间花在不做就会出事和做了能显著省时间的地方。这个判断标准成了我们后续每次排期的默认准则。2. 从MySQL迁移到PostgreSQL一次被迫的正确决定2.1 当初为什么选MySQL后来又为什么换掉这个项目第一阶段选MySQL其实没什么深度考量纯粹是团队熟悉、文档多、出问题好搜。如果项目永远是第一阶段那个体量——几张表、几千条数据、简单的等值查询——MySQL完全撑得住换数据库纯粹是折腾。触发迁移的导火索有两个。第一个是全文检索需求。FlowDesk的工单包含标题、详细描述、回复记录用户希望能按关键词搜到相关工单。MySQL的全文索引对中文支持不够友好InnoDB的全文索引默认分词器对中文是按字符切分的搜打印机故障会被拆成打印印机故障这样的片段召回结果惨不忍睹。虽然能通过ngram插件改善但配置和调优要花不少精力效果依然不如PostgreSQL的全文检索来得自然。第二个原因是JSON查询的复杂度。附件信息、自定义字段、通知偏好设置这些半结构化的数据在第一阶段存在MySQL的JSON类型列里凑合能用。但到了第二阶段我们需要按附件上传者过滤工单、按自定义字段的某个值做统计。MySQL的JSON查询语法写起来非常啰嗦而且没法有效利用索引数据量上来之后性能衰减明显。PostgreSQL的JSONB类型配合GIN索引这类需求写起来顺手得多——更贴合查数据而不是想办法把数据查出来。2.2 迁移的完整过程工具选择、数据校验、回滚方案数据库迁移是我在这个阶段最谨慎的一项操作毕竟数据丢了不是闹着玩的。整个流程分四步走第一步结构迁移。先用PostgreSQL兼容的写法重建表结构字段类型做映射比如MySQL的TINYINT转成PostgreSQL的SMALLINTDATETIME转成TIMESTAMPTZJSON转成JSONB。这一步用的是手工迁移脚本没有用自动化工具因为表不多手工写反而更可控。第二步数据迁移。用的工具是pgloader这个工具支持从MySQL直接迁移到PostgreSQL配置里写好连接信息就能跑。但千万别完全信任默认配置尤其是字符集和时区。我们在测试环境跑了一轮发现时间字段整整差了8个小时原因是pgloader默认按UTC处理DATETIME而业务数据是东八区时间。解决方案是在迁移前修改配置文件的timezone选项统一指定为Asia/Shanghai。第三步数据校验。迁移完成不等于数据正确。我写了一个比对脚本按表、按主键范围分批对比源库和目标库的行数、关键字段的哈希值。这个脚本要重点检查NULL值的处理差异MySQL里空字符串和NULL是两回事PostgreSQL同样区分但某些类型转换可能会把两者混在一起。第四步切换与回滚。正式切换放在凌晨低峰期先把应用停掉做最后一轮增量同步然后改配置文件里的数据库连接指向启动应用验证核心链路。同时把旧库保留两个星期不删应用侧做好开关配置万一发现问题可以立刻切回。整个迁移过程中最花时间的不是迁移本身而是迁移后回归测试——所有涉及SQL的接口全都要过一遍。我们当时写了一个简单的回归脚本把每个接口的请求和期望响应记录下来切换后自动轮询比对。2.3 迁移后实实在在感受到的差异迁移完成之后最直观的感受是三条。第一复杂查询的SQL写得舒服了窗口函数、LATERAL JOIN、FILTER子句都是现成能力不再需要绕道子查询拼临时表。第二JSONB字段的GIN索引让自定义字段的筛选从秒级直接降到毫秒级。第三PostgreSQL的VACUUM机制需要适应——刚开始我们被膨胀的表吓到过后来才理解这是MVCC的正常现象定期autovacuum会处理。当然也有不适应的地方。PostgreSQL的索引创建和重建会锁表大表上做DDL操作必须小心而且它的配置参数比MySQL多得多work_mem、shared_buffers、effective_cache_size这些需要根据服务器配置做基础调优默认值在低配服务器上表现一般。3. 两个核心模块的实战拆解权限系统和实时通知3.1 第一版权限系统的失策与重构权限系统是我在这个阶段最纠结的模块前后做了两版。第一版用的是最标准的RBAC模型用户挂角色角色挂权限点权限点精确到按钮级别。设计文档画得漂漂亮亮代码写出来也挺规整但进入联调阶段就发现问题了。FlowDesk里的工单有公开和仅处理人可见两种状态还有指派给某个人的操作。用RBAC的静态权限点去描述这类动态权限会遇到一个经典的难题权限的判断依赖资源本身的状态而不是单纯依赖登录用户的角色。比如一个普通成员可以查看被指派给自己的私密工单但不能查看别人的私密工单——这用RBAC根本表达不出来因为判断条件里需要带着当前工单的处理人是否包含当前用户。后来我们改成了类ABAC的方案权限判断不再是用户有哪些权限点而是定义一组策略规则规则里引用用户属性、资源属性、上下文条件最终返回允许或拒绝。核心数据结构变成了一个策略集合每条策略包含主体条件、资源条件和动作。实际操作中我们把规则引擎做得很轻没有引入复杂的表达式框架就是一组可组合的TypeScript函数// 权限策略的最小示意规则即函数 type PermissionContext { user: { id: string; roles: string[]; departmentId: string }; resource: { ownerId: string; visible: public | private; assigneeIds: string[] }; }; const canViewTicket (ctx: PermissionContext): boolean { // 管理员或本部门成员可以看公开工单 if (ctx.resource.visible public) { return ctx.user.roles.includes(admin) || ctx.user.departmentId ctx.resource.departmentId; } // 私密工单处理人、指派人和创建人可见 if (ctx.resource.visible private) { return ( ctx.resource.ownerId ctx.user.id || ctx.resource.assigneeIds.includes(ctx.user.id) || ctx.user.roles.includes(manager) ); } return false; };这套方案的好处在于规则可以按需组合改需求的时候不用改表结构改函数就行测试也方便每个策略函数都可以单独写单元测试用不同的context跑断言。从RBAC改成策略判断表面上是代码重构本质上是把权限是静态标签这个错误假设纠正成了权限是条件表达式。这个认知转变很关键如果你的系统里也出现了同一个角色在不同资源上有不同权限的需求趁早考虑ABAC别在RBAC上硬撑。3.2 实时通知为什么放弃WebSocket转向SSE工单系统里有个典型场景用户A提交了一个工单用户B在处理A希望B回复的时候能实时收到通知而不是手动刷新页面。最早我们的实现是前端每5秒轮询一次未读通知接口。数据量小的时候没问题但工单一多每个在线用户都轮询服务器的压力直线上升而且通知延迟最高能到5秒体验很一般。正常的思路是上WebSocket我们也在本地用Socket.IO跑通了Demo还做了断线重连、心跳保活。但最后上线的方案却是SSEServer-Sent Events原因很实际维度WebSocketSSE通信方向双向仅服务器到客户端单向协议独立协议升级握手机制基于普通HTTP天然穿透代理和防火墙重连机制需要自己实现内置自动重连业务匹配度高聊天、游戏中通知、动态刷新FlowDesk的通知场景是典型的单向推送——只有服务器需要告诉用户你有新工单了用户不需要通过同一个长连接向服务器发消息。真有消息要发走普通POST请求反而更可靠。SSE用原生EventSource就能接收服务器端在NestJS里实现也只需要设置正确的响应头// NestJS中的SSE端点示例 Sse(notifications/stream) streamNotifications(Req() req: Request) { const userId req.user.id; return new ObservableMessageEvent((subscriber) { const handler (payload: NotificationPayload) { subscriber.next({ data: payload }); }; notificationService.subscribe(userId, handler); return () notificationService.unsubscribe(userId, handler); }); }使用SSE的时候有个关键坑我放在踩坑章节详细讲但这里先提个醒Nginx默认会缓冲响应内容如果你发现SSE不是一条一条往外吐、而是攒了一堆才一次性推过来八成就是Nginx缓冲在捣鬼需要配置proxy_buffering off。3.3 通知去重与已读状态的存储设计实时推送只是通知系统的一半后半部分是通知的持久化和已读状态管理。我们的设计是通知数据写PostgreSQL每用户每通知一条记录字段包括用户ID、工单ID、通知类型、是否已读、创建时间。Redis用来做推送通道的缓冲和去重。这里有个细节容易忽略用户短时间内在同一个工单下被多次不应该产生三条几乎一样的通知。解决方案是在写入数据库之前做一次去重检查——同一个用户、同一个工单、同一种通知类型如果5分钟内有相同记录就不再新增而是更新已有记录的触发时间。这个小逻辑用一句话就能说清楚但没做之前用户是真的会来吐槽通知轰炸的。已读状态的更新也做了一个小优化已读不是逐条UPDATE而是收集一批通知ID之后批量更新减少数据库的写压力。对工单系统这种体量来说这个优化谈不上技术含量但确实能让接口响应从几十毫秒降到十几毫秒。4. 这个阶段踩过的坑按排查链路逐个复盘4.1 一次Redis缓存穿透一个热点Key拖垮整个查询链路现象是这样的某天下午运营反馈工单列表页越来越慢最后直接超时。看监控PostgreSQL的CPU飙升慢查询日志里全是同一条SQL——查工单状态统计的聚合查询。这条SQL本来做了Redis缓存key的设计是ticket:stats:status过期时间30分钟。问题出在缓存失效的一瞬间缓存到期后第一个请求发现缓存不存在去数据库执行聚合查询结果这个聚合查询在数据量大的时候要跑好几秒。在这几秒内后续所有请求全都发现缓存不存在又全都去查数据库直接把数据库打满。这就是典型的缓存穿透——热点key失效引发请求直接打到数据库。排查过程不算复杂先看慢查询日志锁定SQL再看Redis里的key发现已经过期最后看应用日志确认大量缓存未命中的日志在同一时间点爆发。定位到原因之后修复方案用了两层// 解决缓存穿透互斥锁 短期空值缓存 async function getTicketStatusStats() { const cacheKey ticket:stats:status; const cached await redis.get(cacheKey); if (cached) return JSON.parse(cached); // 请求并发时只让一个请求查数据库 const lockKey ${cacheKey}:lock; const acquired await redis.set(lockKey, 1, EX, 5, NX); if (!acquired) { // 拿不到锁就短暂等待后重新读缓存 await sleep(100); return getTicketStatusStats(); } try { const stats await prisma.ticket.groupBy({ by: [status], _count: true }); await redis.set(cacheKey, JSON.stringify(stats), EX, 1800); return stats; } finally { await redis.del(lockKey); } }第一层互斥锁保证同一时间只有一个请求去查数据库第二层把缓存过期时间从30分钟改成1800秒降低失效频率。后来又补了一步对这个聚合查询建立物化视图每5分钟刷新一次把这个查询彻底从接口链路中剥离出去。这次故障的教训是任何聚合统计类的热点查询缓存策略必须考虑并发失效场景。单机请求量不大时不容易出事一旦并发上来一个key的失效就能放大成整个数据库的灾难。4.2 批量导出任务的内存泄漏一个没被清理的Map另一个花了两天才解决的bug是内存泄漏。现象是测试环境的Node.js进程内存占用持续上涨每次执行批量工单导出功能都会涨几十MB重启后恢复正常但再导出几次又开始涨。排查链路可以从三个方向入手先看heapdump再查可疑的全局持有最后用二分注释法定位代码。我们先用node --inspect连接进程触发一次批量导出后抓heapdump用Chrome DevTools的Memory面板分析。结果很明确内存里有一个Map对象持续膨胀里面的key是工单IDvalue是导出任务的状态对象。这个Map在执行导出任务时写入数据但任务结束后没有清空造成引用一直存在GC无法回收。顺着引用链找回去发现是同事在实现导出功能时为了方便任务进度查询把任务状态存进了一个模块级的Map。他本意是任务执行期间前端轮询进度时能快速拿到状态但忘了加删除逻辑导致每个导出任务的数据都永久驻留在内存里。修复方式很简单任务结束时在finally块里执行map.delete(taskId)同时给Map加了一个上限超过1000条就自动清理最早的数据。代码层面的bug往往十几分钟能改完真正花时间的是定位过程。这个bug给我最大的启发是任何模块内部的临时存储都必须考虑它什么时候被释放。JavaScript里一个Map、一个数组如果在模块作用域被引用它不会被GC回收这就是内存泄漏的形成机制。如果当初写批量任务时就用Redis存任务状态而不是内存Map这个坑根本不会出现。4.3 Nginx缓冲导致SSE消息不实时一个响应头引发的假延迟前文提到SSE实时通知上线后测试环境一切正常但部署到生产环境后用户反馈通知延迟严重有时候过了好几分钟才弹出来。一开始怀疑是网络问题后来发现是Nginx配置的问题。SSE依赖的是流式响应服务器每推送一条消息客户端应该立刻收到。但Nginx默认会开启proxy_buffering把上游服务器的响应先缓存起来攒到一定量再一次性返回给客户端。这个机制对普通HTTP响应是优化对SSE是毁灭性的——服务器明明早就推了消息Nginx却握着不放直到缓冲区满了才吐出去。解决办法是在Nginx的location配置里显式关闭缓冲location /api/notifications { proxy_pass http://backend:3000; proxy_buffering off; proxy_cache off; proxy_set_header Connection ; proxy_http_version 1.1; chunked_transfer_encoding on; proxy_read_timeout 3600s; }这里还要注意proxy_set_header Connection 因为HTTP/1.1下默认是keep-alive如果不置空连接头某些场景下会出现连接复用问题。proxy_read_timeout也要设置长一点不然SSE长时间没有消息推送时Nginx默认60秒没有响应就会断开连接客户端虽然会自动重连但会造成不必要的连接抖动。这个坑的难点不在于修复而在于为什么测试环境没发现、生产环境才出现。原因很简单本地开发是浏览器直连Node.js服务完全没有Nginx这一层测试环境虽然有Nginx但流量小缓冲问题不容易暴露生产环境并发上来了Nginx缓冲区的攒量变大问题才显现出来。任何涉及流式响应的功能从第一天就应该在完整的反向代理链路里测试。4.4 前端路由懒加载导致的白屏chunk加载失败的回退策略前端这边也踩了一个印象深刻的坑。Vue 3项目里用动态import实现路由懒加载上线后部分用户反馈进入系统后有时候会白屏刷新一下就好了。排查后发现白屏的原因有两个因素叠加。第一个是路由懒加载的chunk包上传到服务器后文件名带哈希值用户打开页面时加载了旧的HTML里面引用的还是旧chunk的路径但服务器上旧chunk已经被新版本覆盖删除了所以加载失败。第二个因素是前端代码里没有处理chunk加载失败的情况import()的Promise reject后Vue Router直接抛异常组件渲染中断页面就白了。修复方案有两层。第一层是在构建部署层面做改进新版本发布时保留上一版本的部分chunk文件避免立即删除或者干脆不做覆盖式发布而是用带版本号的目录存放静态资源。第二层是在前端兜底监听动态import失败如果是ChunkLoadError就强制刷新页面重新拉取最新HTML。// 全局捕获路由懒加载失败并刷新 router.onError((error) { if (error.message.includes(Failed to fetch dynamically imported module)) { window.location.reload(); } });这个兜底方案虽然粗暴但对内网系统来说是有效的。当然更好的做法是配合PWA的版本更新提示不过那就是第三阶段的事了。这个坑的通用启示是前端部署和静态资源版本管理的问题在开发环境永远不会暴露只有在多人协作、频繁发布的时候才显现。5. 测试、性能数据和交付规范5.1 测试策略单元测试、集成测试与手工复盘的比例第二阶段我们对测试的投入比第一阶段明显加大这不是觉悟提高了而是第一阶段的线上故障实在是太多了。当时一提改需求开发心里就发虚生怕改坏哪个角落。单元测试覆盖了后端主要的服务层和权限策略函数覆盖率从第一阶段的不到20%提到了60%左右。这个数字不算高但重点是覆盖的都是核心逻辑权限判断、工单状态流转、通知去重。前端这边主要测了Pinia状态管理和工具函数组件层用Vitest做了几个关键流程的测试创建工单流程、登录流程。集成测试我们没搭太重的基础设施用了一个取巧的方案NestJS的e2e测试框架启动测试数据库把核心API链路跑一遍。覆盖的场景是用户创建工单-分配处理人-回复-关闭这个主干流程以及权限系统中非处理人查看私密工单被拒绝这类反向用例。这些e2e测试在每次CI里自动跑基本保证了主干链路不会挂。手工测试也没有完全抛弃。每次发版前有一个固定的冒烟测试清单包含20个左右的页面操作路径我习惯让不写这个模块的同事来执行因为开发自己测试往往会有隧道视野——总觉得用户会按照预期的方式操作但实际上用户的操作路径千奇百怪。5.2 优化前后对比性能数据要给自己一个交代这个阶段我们做了几轮性能优化拿数据说话会更有说服力。下面是两组比较有代表性的优化数据指标优化前优化后主要手段工单列表接口P95响应850ms120ms补索引、拆开N1查询、Redis缓存列表摘要工单详情页首屏加载2.8s1.2s路由懒加载、组件按需引入、移除冗余Sass变量全局搜索响应3.5s850ms全文检索用PostgreSQL替代应用层LIKE模糊匹配批量导出1000条工单23s8s导出改为分批查询避免一次性全量载入内存最有含金量的是列表接口的优化。原始的NestJS查询用Prisma一连串include关联了用户表、部门表、标签表、附件表生成的SQL是一大堆LEFT JOIN数据量一大就慢。优化策略是列表页只查核心字段关联信息在循环里用in批量查出来再在内存里组装。这在数据库层面从一次大JOIN变成了三次小查询总耗时反而大幅下降。这个反直觉的点值得记一下ORM的include太方便了但方便不等于高效列表场景优先考虑批量查询内存组装而不是层层嵌套关联。5.3 分支管理策略和Code Review的约定FlowDesk用Git Flow做了简化版主干分支main保护开发分支dev用于日常集成功能分支从dev切出命名规范是feat/模块名/描述或fix/问题描述。合并到dev的MR必须通过两个检查跑一遍CIlint 单测 e2e以及至少一个非作者的review批准。Code Review这个环节我们总结经验后发现最有效的约定不是谁看谁的代码而是设了几个硬性检查项有没有打印token或密码日志、有没有在地事务中调用外部HTTP请求、有没有直接在存储层拼接SQL、有没有改接口签名而不改调用方。拿这四个检查项去卡至少能挡住一半的低级问题。Reviewer不要求读懂每一行代码但必须确认改动符合项目既有的模式如果发现一段代码跟项目其他位置的习惯不一致那大概率是值得开个会讨论的点。6. 遗留的技术债和下一阶段的优先级排序6.1 不逃避的债务清单每个阶段结束都会有没做完的事把这个清单写下来比心里装着强第一全文检索在存量数据上的索引重建策略还没做优化。现在能搜但新数据入库后索引更新的时机和性能还需要调优。第二附件管理还挂在本地磁盘没有统一的对象存储抽象。后续如果要做多节点部署这个环节必须重构。第三数据看板目前只有三个数字卡片聚合查询没做物化视图数据量再翻几倍会扛不住。第四前端的错误监控体系基本是空的。现在线上出了问题全靠用户反馈没有主动采集前端异常的机制。第五测试覆盖率距离理想值还有差距尤其是前端组件的测试覆盖不多很多改动仍然依赖手工回归。这些债不是必须马上还但不能假装它们不存在。每一项我都标注了触发条件——比如当工单表数据量超过100万时全文检索需要先优化——这样不至于突然被动应对。6.2 下一阶段的三个优先级基于现阶段的观察和维护成本第三阶段的优先级排序是这样的第一优先补齐监控和告警链路。系统到了这个体量没有监控就是蒙眼开车。计划接入开源监控体系重点盯接口P95、慢SQL、Redis命中率、异常日志这四个维度。第二优先完成全文检索的PostgreSQL深度优化和附件存储抽象。这两件事都跟基础设施相关越早做越省事。第三优先优化工单详情页的交互流程。现在功能都有但使用体验不够顺。这个可能不算技术债务但对用户价值最直接。6.3 关于阶段复盘本身的一个心得这段时间我反复在想一个问题阶段总结到底给谁看给领导看进度给团队看结果给下一个人看经验后来想明白了首先还是给自己看。写这份总结的过程里我重新审视了每一个技术决策有的当时觉得稳现在看其实有明显的坏味道比如第一版权限系统、比如没有加互斥锁的缓存策略。如果不写下来这些问题会在下一次遇到类似需求时换一张脸重新出现。当然阶段总结不是要写一本流水账。我的做法是控制在能完整叙述每个决策的理由、每个坑的排查链路、每个模块的当前状态这个粒度上。太细的代码实现交给文档注释太粗的愿景规划交给项目规划。这个中间粒度恰好是团队协作和知识沉淀最需要的内容密度。最后分享一个我很推荐的小习惯每次修完一个bug顺手把根因和为什么之前没发现记在项目的FAQ文档里。这个文档不需要固定格式我经常直接记三行字加上一个复现步骤。四个月积累下来它已经变成团队排查问题时的第一参考顺序比搜索引擎的开发者社区答案靠谱得多。这次阶段总结能写得比较顺很大程度也是托了这个习惯的福。