
我见过太多团队把软件设计文档当成一种过场。评审会开完文档归档代码该怎么写还怎么写。等开发到一半发现接口对不上、模块边界模糊、数据库要推倒重来大家才想起来翻文档——结果文档里什么都没写清楚。这不是设计文档没用是很多人把设计文档写成了需求文档的复述版或者干脆写成了一堆设计原则的空话。我自己这些年写过、评审过不少设计文档踩过很多坑之后总结了一套可以直接套用的模板。这篇文章就做两件事第一把模板的结构和每一段该写什么都讲清楚第二拿一个最常见的用户登录认证模块当例子从头到尾走一遍让你看完就能照着写自己项目的设计文档。无论你是刚带团队的技术组长还是被分配了补个设计文档任务的开发同学这份内容都可以直接参考。1. 设计文档为什么总被当成废纸先想清楚读者和用途1.1 设计文档的第一读者不是领导是三个月后的你自己很多设计文档写得烂根源在于没搞清给谁看。有人写文档是为了应付审批于是把背景、意义、目标写得冠冕堂皇有人写文档是为了记录过程于是把讨论过的每个方案、每次取舍都堆进去。结果文档既不指导开发也不帮助评审成了没人愿意打开的资料。我自己在写设计文档前一定会先问一个问题如果三个月后有一个新人接手这个模块他拿着这份文档能不能在半天内搞清楚系统是怎么设计的、为什么这样设计这才是设计文档最核心的价值——把脑子里的设计决策固化成团队能共享的信息。开发同学看文档想知道模块边界在哪、数据模型长什么样、接口怎么对接测试同学看文档想知道核心流程是什么、异常分支有哪些、状态怎么流转你自己三个月后看文档想知道当初为什么选了A方案而不是B方案。1.2 设计文档和需求文档的分工一个说做什么一个说怎么做新手最容易犯的错是把需求文档的内容抄进设计文档。需求文档回答的是我们为什么要做这个功能、用户需要什么设计文档回答的是在现有系统里这个功能具体怎么落地。两者有关联但分工完全不同。举个例子。需求文档会写用户需要能通过手机号登录系统这是从用户视角描述的业务诉求。设计文档要写的是用户提交手机号和密码后认证服务先做图形验证码校验然后根据手机号查询用户表通过 bcrypt 校验密码哈希成功则签发 access_token 和 refresh_token并将会话记录写入 refresh_tokens 表。这就是设计它把一句业务需求翻译成了系统里能执行的动作和数据流。写设计文档时一旦发现自己开始复述业务背景就该停下来转换视角。如果一段内容只是解释了系统要解决什么问题而没有说明系统内部怎么协作、数据结构怎么组织那这段内容就不该出现在设计文档里。1.3 什么项目必须写设计文档什么项目可以省不是所有代码都需要先写设计文档。一个后端接口十几个的小工具写十页设计文档纯属浪费。但下面几类情况不写设计文档会出大问题多人协作的功能模块三个人以上同时改一个模块没有文档统一接口和数据模型合并代码时一定冲突。跨模块改动改了数据库表结构或者改了公共接口受影响的不只你一个人。文档是通知和确认机制。技术选型存在分歧多个方案各有利弊需要文档记录决策理由避免以后被人反复翻案。项目周期比较长超过一个月的项目不写文档到后期自己都会忘掉当初的设计约束。反过来一次性的脚本、纯前端展示页面、没有数据模型变化的简单功能就不用套模板了。文档是手段不是目的。2. 一个可复用的设计文档骨架九段式结构说明2.1 模板总览九个核心段三个扩展段我常用的设计文档结构是93。九个核心段覆盖设计文档必须回答的问题三个扩展段按需添加。这套结构我内部叫软件设计文档示例模板任何项目都可以按这个框架往里填。序号章节核心问题1背景与目标为什么做这件事做到什么程度算完成2术语与名词文档里用的专业词、缩写统一口径3需求约束功能范围边界、非功能要求性能、安全、兼容性4总体架构系统分几块块与块之间什么关系5模块设计每个模块的职责、内部逻辑、依赖关系6核心流程设计关键业务的主流程、分支流程、异常流程7数据设计表结构、字段约束、存储方案、缓存方案8接口设计对外API、内部调用、错误码、协议格式9异常与安全设计系统异常怎么处理安全边界在哪10扩展部署与配置环境变量、部署依赖、发布顺序11扩展测试要点需要重点验证的用例、边界场景12扩展遗留问题与后续演进本期不做的事、已知风险、后续优化方向这套结构看起来很重但实际写的时候每个章节的长度完全根据项目复杂度来。一个模块级设计文档背景、术语、异常三节可能各三四行就够了一个系统级设计文档架构和数据设计可能是大头。模板是提醒你别漏信息不是说每段都得写满。2.2 每个章节的合格标准背景与目标三句话能讲完。第一句说现状痛点第二句说这次要做什么第三句说不做什么。很多人忽略不做什么结果评审时被各种期望牵着走项目范围越滚越大。术语与名词不是凑字数是真的能统一口径。比如会话到底指什么是服务端保存的 session还是客户端持有的 token这两个定义不清后面所有讨论都会鸡同鸭讲。需求约束功能范围要列得能验收。一个功能列表每项后面标注是否在本期实现。性能要求和安全要求这两类非功能需求在验收时容易扯皮必须量化比如接口响应时间 P95 小于 200ms密码存储必须使用 bcrypt 不落明文。总体架构要画到新人三分钟能看懂的粒度。不管是文字描述还是框图必须讲清楚系统分哪些部分、谁调用谁、数据往哪儿流。如果新人看完还得来问你这两个服务哪个调哪个就说明没画清楚。模块设计每个模块的职责边界要清晰。最实用的写法是职责描述 关键类/函数 依赖说明。尤其要把这个模块不做的事写出来防止开发时功能越界。核心流程设计这是设计文档的高潮部分也是最容易看到真功夫的地方。除了主流程一定要有分支和异常流程。一个登录功能正常路径五分钟能讲完但判断验证码过期、账号锁定、密码连续错误、token 过期后刷新这些分支才是设计的重点。数据设计别只列字段名。类型、约束、索引、以及为什么这样设计都比字段名更重要。一个表为什么要冗余存某个字段一个价格字段为什么用整数存储写出来才有讨论价值。接口设计请求响应结构要具体到字段级。别写返回登录结果这种话要写清楚返回哪些字段、错误码怎么定义。接口文档模糊是前后端联调返工的第一大原因。异常与安全设计别只写做好异常处理。要列出系统可能遇到的具体异常场景每一个场景下系统应该怎么表现。安全方面从认证、传输、存储三个层面分别检查这是最不容易漏的思路。2.3 模板怎么管理我现在用 Obsidian 管理全部模板模板写好后管理是个容易被忽略的问题。我现在的做法是把所有设计模板放在 Obsidian 里作为 template 存起来每次新建项目设计文档时直接插入模板然后按章节填空。Obsidian 的好处是支持模板变量、双链管理一个大型项目的设计文档可以拆成多个文件相互引用比 Word 里的一篇超长文档好维护得多。当然不用 Obsidian 也完全没问题Git 仓库里放一个 markdown 模板文件团队所有人都能复用效果是一样的。关键是模板要进版本管理跟着团队实践迭代别让它变成三个月没更新的僵尸文件。3. 手把手示例登录认证模块的设计文档长什么样3.1 场景设定和背景示例为了让你不只在抽象层面打转我拿一个最常见的场景来完整走一遍。假设我们现在维护一个后台管理系统原来一直用的是第三方认证现在因为安全合规要求需要改成自建账号体系包含手机号密码登录、token 鉴权、密码找回、强制下线这几个核心能力。设计文档的第一节可以这样写当前后台管理系统接入第三方认证平台存在两个问题一是账号体系不受我方控制人员离职后无法及时回收权限二是第三方认证的接口稳定性影响我方登录成功率。本期目标是将认证能力收敛到自建账号体系实现手机号密码登录、token 鉴权、密码找回、管理员强制下线四个核心能力。本期不考虑扫码登录、短信验证码快捷登录、第三方 OAuth 登录。这些留待下一期演进。这段示例里有三个关键信息现状痛点、本期目标、明确不做的事。尤其是本期不考虑这段在评审时能帮你挡掉大量顺便加个功能的请求。3.2 需求约束示例把能登录变成可验收的登录设计文档里的需求不是产品需求而是经过技术视角转化的约束。我一般列一张表把功能需求和非功能需求分开编号需求项说明是否本期FR-01手机号密码登录手机号密码图形验证码是FR-02token 鉴权除登录和刷新接口外其余接口均校验 access_token是FR-03密码找回通过管理员重置密码用户凭重置码设置新密码是FR-04强制下线管理员可将指定用户的所有会话置为失效是NFR-01登录接口性能正常网络条件下登录接口响应时间 P95 1s是NFR-02密码存储安全使用 bcrypt 哈希成本因子不低于 10是NFR-03登录防暴力破解同一手机号连续失败 5 次锁定 30 分钟是写非功能需求时最容易犯的错是写登录要快密码要安全这种无法验收的主观描述。一旦写成P95 1sbcrypt 成本因子不低于 10测试和开发就都有了一致的判断标准。3.3 总体架构示例三个服务模块的边界划分这一节把系统结构画出来并交代模块划分。登录认证模块内部我按职责分了三个子模块认证服务负责登录、登出、token 签发与刷新、验证码校验。不直接操作业务数据只处理证明你确实是你这件事。用户服务负责用户账号的增删改查、密码重置、账号状态管理。它保存用户所有资料包括手机号、密码哈希、锁定状态。会话管理负责 refresh_token 的存储、撤销、过期清理。用一张独立的会话表承载方便做强制下线。三个子模块的依赖关系很清晰认证服务调用用户服务验证账号密码调用会话管理存储和校验会话用户服务在密码重置时通知会话管理撤销该用户的所有会话。为什么要把会话拆出来单独一个模块因为强制下发、踢人下线这个能力涉及找到该用户所有有效会话并全部作废如果会话信息散落在 token 里比如纯 JWT无状态那你根本没法主动让其失效。这是无状态 token 的一个典型缺陷拆出会话管理就是为了解决主动吊销的问题。3.4 核心流程设计示例登录、刷新、登出三条链路这是设计文档里最有价值的部分。我给出登录、刷新、登出三条主流程的细化设计。登录流程客户端提交手机号、密码、图形验证码 ID 和验证码值。认证服务先校验图形验证码。验证码值从 Redis 读取校验后无论成败立即删除防止重放。验证码通过后根据手机号查询用户服务获得账号记录。检查账号状态。锁定或禁用直接拒绝并返回明确错误码。用 bcrypt 校验密码哈希。如果失败增加失败计数达到 5 次则锁定 30 分钟。校验通过后生成 access_token有效期 2 小时和 refresh_token有效期 30 天。将 refresh_token 的 SHA-256 哈希值与会话信息写入会话管理模块的会话表。记录登录日志更新用户表的 last_login_at返回双 token 给客户端。这里每个步骤都不是凭空设计的。第 2 步校验验证码后立即删除是为了防止同一个验证码被重放多次第 7 步存的是 token 的哈希而不是明文避免会话表泄露后被直接盗用refresh_token 有效期 30 天是为了保证用户体验不用一个月内反复重新登录。这些点每一个单独拿出来在评审时都值得被讨论我建议在文档里对这些关键决策附一句简要理由。刷新流程客户端持有 refresh_token请求刷新接口获取新的 access_token。认证服务将收到的 refresh_token 做哈希在会话表查询。校验会话是否存在、是否被撤销、是否过期。校验通过后签发新的 access_token可选地轮换 refresh_token。如果原 refresh_token 已经失效返回特定错误码客户端收到后强制回到登录页。刷新流程里有一个我踩过坑的细节不使用 refresh_token 轮换的话一个 refresh_token 可以在三十天内无限次换新的 access_token被盗后很难回收。使用轮换策略后每次刷新都会生成新 token旧 token 立即失效安全等级上一个台阶。代价是客户端要做并发防护如果用户同时开了多个页面每个页面都拿同一个 refresh_token 刷新第一个成功之后后面几个全都会失败。方案是在刷新接口做 2 秒内的 refresh_token 重用保护或者要求客户端刷新串行化。登出流程客户端携带 access_token 请求登出接口。认证服务解析 access_token 拿到会话 ID将会话表中的对应记录置为 revoked。返回登出成功。这个流程看起来简单但实际落地时我发现很多团队只做了客户端删掉 token服务端会话数据没有处理导致 refresh_token 仍然有效登出形同虚设。设计文档里明确写登出必须服务端撤销会话记录这个约束能挡住 90% 的实现偏差。3.5 数据设计示例三张表的字段级设计数据库设计是设计文档里最不能含糊的部分。登录认证模块我设计了三张表。users 用户表字段类型约束说明idBIGINTPK, AUTO_INCREMENT用户 IDusernameVARCHAR(50)UNIQUE用户名可空phoneVARCHAR(20)UNIQUE, NOT NULL手机号登录账号password_hashVARCHAR(100)NOT NULLbcrypt 哈希值statusTINYINTNOT NULL, DEFAULT 00-正常 1-锁定 2-禁用failed_countINTNOT NULL, DEFAULT 0连续登录失败次数lock_expire_atDATETIMENULL锁定到期时间可空last_login_atDATETIMENULL最近登录时间created_atDATETIMENOT NULL创建时间refresh_tokens 会话表字段类型约束说明idBIGINTPK, AUTO_INCREMENT会话 IDuser_idBIGINTNOT NULL, INDEX用户 ID外键到 userstoken_hashCHAR(64)NOT NULL, UNIQUErefresh_token 的 SHA-256 哈希expires_atDATETIMENOT NULL过期时间revokedTINYINTNOT NULL, DEFAULT 0是否已撤销created_atDATETIMENOT NULL创建时间login_log 登录日志表字段类型约束说明idBIGINTPK, AUTO_INCREMENT日志 IDuser_idBIGINTNULL, INDEX用户 ID登录失败时为 NULLipVARCHAR(45)NOT NULL客户端 IP兼容 IPv6user_agentVARCHAR(255)NULL客户端 UAsuccessTINYINTNOT NULL是否成功fail_reasonVARCHAR(100)NULL失败原因created_atDATETIMENOT NULL创建时间索引设计这张表的时候有一个值得强调的原则失败登录日志里 user_id 为什么不设 NOT NULL因为登录失败可能是手机号根本不存在此时拿不到 user_id。很多团队在这张表上纠结半天要不要留空其实设计文档里一句话就能说清楚——日志表的核心目的是追踪异常不是维护引用完整性。3.6 接口设计示例连错误码一起定齐接口设计最忌讳只写路径成功返回错误码体系必须一起定义。我以登录和刷新两个接口为例说明。POST /api/v1/auth/login请求体{ account: 13800138000, password: plaintext_password, captcha_id: a1b2c3d4, captcha_code: 8e4f }成功响应{ code: 0, message: success, data: { access_token: eyJhbGciOiJIUzI1NiIs..., expires_in: 7200, refresh_token: 8f4f2a1c9e... } }失败响应{ code: 10021, message: 密码错误剩余尝试次数 4 次, data: null }POST /api/v1/auth/refresh请求体{ refresh_token: 8f4f2a1c9e... }成功响应返回新的 access_token 和新的 refresh_token。失败时错误码要和登录接口区分开比如 10031 表示 refresh_token 已过期10032 表示会话已撤销。客户端可以根据不同的错误码决定是引导用户重新登录还是静默重试一次。如果这些错误码不定清楚前端拿到一个 401 根本不知道是该跳登录页还是该重新刷新最后一定会在群里互相甩锅。3.7 异常与安全设计示例从认证、传输、存储三层面排查安全设计不应该是事后补丁在文档阶段就要逐层排查。我习惯从认证、传输、存储三个层面分别列出设计方案。认证层面密码存储只用 bcrypt哈希值包含随机盐成本因子不低于 10。密码明文在服务端不做任何日志输出。登录接口增加图形验证码防止自动化脚本撞库连续失败 5 次锁定账号 30 分钟锁定状态必须可以被人为解除。access_token 有效期 2 小时refresh_token 有效期 30 天且服务端可撤销。token 中不存手机号等个人隐私信息只存用户 ID 和会话 ID。传输层面所有接口必须走 HTTPStoken 只允许在 Authorization 请求头中传递不允许出现在 URL 参数里。为什么URL 会被网关、浏览器历史记录、反代日志等各个层面积累token 一旦进 URL 就在多个地方留了副本泄露面大大增加。存储层面refresh_token 在数据库里只存 SHA-256 哈希值不存明文。登录日志不记录密码相关字段。Redis 里存的验证码值设置 5 分钟过期时间这个时间足够用户完成输入又不会因为留太久增加被暴力试出的风险。写到这里你应该发现了安全设计其实是很多个具体决策的集合每个决策都有它要防的具体攻击场景。把每个决策和它防的东西写清楚评审的时候别人才好帮你挑毛病。4. 写完之后别急着发自查清单和评审翻车现场4.1 发出前的自查清单设计文档初稿写完我建议先过一遍自查清单。这张表是很多次踩坑之后总结的每次发出去之前花五分钟过一遍能挡掉大部分低级问题。检查项说明自检结果背景目标是否回答为什么做是否说清楚现状痛点和不做什么可自检非功能需求是否量化性能指标、安全指标有没有具体数值可自检模块边界是否写清不做什么每个模块职责有无歧义可自检核心流程是否覆盖异常分支每个主流程都问一遍如果这里失败了怎么办可自检数据表字段是否有完整约束类型、空值、索引、默认值是否都写了可自检接口错误码是否提前定义每种失败场景是否有独立错误码可自检安全设计是否三层都覆盖认证、传输、存储是否都有方案可自检遗留问题和风险是否明说本期不做的事和已知风险是否列出可自检这个表格本质上是把设计文档的内容质量检查显性化。如果你发现某一栏没法打勾说明文档信息还不够发出去大概率会被评审人问住。4.2 我见过的高频翻车现场分享两个真实的评审翻车案例你会印象更深。第一个案例某团队做支付回调模块设计文档写了主流程——收到回调、验签、更新订单、返回成功结构很完整。结果评审时测试同学问了一句如果重复收到同一笔订单的回调怎么办全屋安静了。文档里没有幂等设计数据库也没提唯一约束。当天评审没结论回去补了半页异常流程设计和一张幂等键字段的表。这个教训后来被我写进了设计模板的异常分支检查项里——任何接收外部通知的流程必须先想重复、乱序、延迟三种异常。第二个案例某设计文档的后续演进一节写了未来可支持微信扫码登录、短信快捷登录。结果产品经理看到后在下一轮迭代规划里直接把这些未来功能当成了既定承诺开发团队被临时拉去需求评审非常被动。后来我定了一条规矩设计文档里不允许出现任何未来可能支持之类的表述没排期的想法统一放到独立的技术演进文档里和项目设计隔离开。4.3 设计文档也要进版本管理最后提一个很容易被忽略的实践设计文档本身也是代码库的一部分必须跟着项目走版本管理。我现在习惯把设计文档以 Markdown 格式放进项目仓库的 docs/design 目录和代码一起提交、一起评审、一起合并。每次评审后的修改在文档头部加一个变更记录表说清楚改了什么、基于谁的意见改的。这样做的直接好处是当代码行为和文档描述不一致时通过 git 历史能定位是文档没更新还是代码偏离了设计责任边界非常清楚。用 Obsidian 管理模板的情况下模板文件放在单独的知识库里项目文档放在仓库里模板负责沉淀通用经验项目文档记录具体决策两者各司其职。如果你的团队用别的工具也没关系核心原则就一条设计文档不能只是评审时看一眼之后就躺在共享目录里发霉。我自己现在的习惯是写设计文档之前先找个白板把模块和时序画一遍画明白了再动笔。文档不追求一次成稿而是跟着评审意见迭代但每次迭代都在变更记录里写清楚改了什么。这套模板不是标准答案而是一条足够好用的基线——你会发现真正有经验的人拿到这套模板会删掉一半的章节然后把自己模块最特殊的那部分写透。设计文档的意义永远不在于格式有多规范而在于让所有人在写代码之前先在纸面上达成共识。这才是它值得存在的唯一理由。