Java后端
Spring Boot
工作思路
需求拆解
全流程纪实
暗规则
本篇记录一位5年经验Java后端开发者(老王)完整的一天。需求是:「用户中心增加手机号绑定功能,需要短信验证码验证,且一个手机号只能绑定一个账号」。你会看到他每一步的思考——不是"怎么做",而是"为什么这么做"。
需求到手
→
拆解分析
→
风险识别
→
DB设计
API设计
→
排期估时
→
编码实现
→
自测验证
代码评审
→
部署上线
→
复盘总结
09:00需求到手:先不写代码,先读三遍
需求文档来了。第一反应不是"用什么技术实现",而是"这个需求到底要解决什么业务问题"。如果业务问题没搞清楚就开始写代码,后面一定会返工。
老王打开JIRA,看到了PM提的需求单:
需求描述
- 用户在个人中心可以绑定手机号
- 绑定时需要发送短信验证码,用户输入验证码验证通过后完成绑定
- 一个手机号只能绑定一个账号(全局唯一)
- 用户可以解绑手机号,解绑后可以重新绑定新的手机号
- 已有手机号的用户可以更换手机号(先验证旧手机号,再绑定新手机号)
验收标准
- 验证码60秒内只能发送一次
- 验证码5分钟内有效
- 同一天同一手机号最多发5次
- 绑定成功后发送站内通知
负责人:老王
期望上线:2026-04-18
涉及方:前端、后端、短信网关
第一遍粗读——了解大概是什么功能。
第二遍细读——找出边界条件和限制规则。
第三遍带着问题读——"什么场景下会出问题?"
⚡ 后端习惯 #1:读需求时不只看"要做什么",更要找"限制条件"和"异常场景"。验证码发送频率限制、手机号唯一性、更换手机号的双向验证——这些才是后端要处理的核心难点。如果只看到"发送验证码+验证"就开写,上线后一定会被刷短信接口、重复绑定等问题打脸。
09:15需求拆解:把一句话拆成8个接口
老王在笔记本上画出了功能拆解:
1. 发送验证码→ 调短信网关 + Redis存储验证码
2. 验证验证码→ 从Redis取验证码 + 比对
3. 绑定手机号→ 检查唯一性 + 写DB
4. 解绑手机号→ 清除绑定关系
5. 更换手机号→ 先验旧号 + 再绑新号(=2+3的组合)
6. 查询绑定状态→ 前端展示用
7. 站内通知→ 绑定成功后异步发送
8. 操作日志→ 记录谁什么时候绑定了什么手机号
PM写的是"绑定手机号",看起来是一个功能。但拆开来,至少8个子功能/接口。如果开始写代码前不做这一步拆解,写的时候会一会儿想到这个没做,一会儿想到那个漏了,代码结构会很混乱。
类比——装修房子:业主说"我要装个厨房",装修师傅不会直接买砖头开始砌。他要先拆成:水电改造→防水→贴砖→橱柜安装→台面→电器接线→验收。每一项都有依赖顺序。后端拆需求就像装修拆工序。
⚡ 后端习惯 #2:拆需求时画出"功能依赖关系图"。"更换手机号"依赖"验证旧号码"和"绑定新号码"两个功能,这决定了代码层面的复用关系。先想清楚依赖关系,再写代码,Service层会很干净。
09:40识别风险:那些上线后会爆炸的炸弹
老王在纸上列出了这个需求可能的风险点:
| 风险类型 | 具体场景 | 影响程度 | 解决方案 |
| 短信轰炸 | 恶意用户循环调用发验证码接口,消耗短信费用 | 🔴 高(花钱) | Redis限流:同IP/同手机号发送频率限制 |
| 并发绑定 | 两个用户同时绑定同一个手机号 | 🔴 高(数据不一致) | 数据库唯一索引 + 分布式锁 |
| 验证码暴力破解 | 攻击者枚举4位验证码(10000种组合) | 🟡 中 | 验证码6位 + 最多尝试5次 + 超限锁定 |
| 短信网关超时 | 短信服务商接口响应慢 | 🟡 中 | 调用超时控制 + 降级(切备用通道) |
| 解绑后数据残留 | 解绑后旧绑定记录丢失,无法审计 | 🟢 低 | 绑定记录软删除 + 独立日志表 |
| 中间人攻击 | 验证码传输过程中被截获 | 🟡 中 | 全链路HTTPS + 验证码一次性使用 |
这些风险点90%都不是PM写在需求里的。后端开发的一个重要职责就是"帮产品把异常场景都想全"。这个习惯来自经验——被生产事故打过的开发者,看到"发验证码"四个字就会条件反射地想到短信轰炸。
⚡ 后端习惯 #3:拿到需求后,花至少30分钟专门做"风险识别"。用红黄绿三色标注影响程度。这些风险点会直接变成代码里的防御逻辑——限流、锁、重试、降级。如果跳过这一步直接写码,上线后等着被安全团队和运维找上门。
🔴 真实事故案例(老王亲历)
曾经有个项目,手机号绑定功能没有做"验证码尝试次数限制"。上线第三天,安全扫描发现有人在暴力枚举验证码,试了8万次,猜中了3个账号的验证码并成功绑定。最终的结果是:紧急加限流 + 全量密码重置 + 写事故报告。代价是整个组加班到凌晨2点。从此以后,老王看到任何验证码功能,第一件事就是加尝试次数限制。
10:00数据库设计:先想数据,再想代码
很多新手后端拿到需求就打开IDE写Controller。但老手第一步永远是先设计数据库表结构。因为代码可以重构,表结构上线后就很难改(要改表结构=数据迁移=风险巨大)。所以先想清楚数据怎么存。
老王打开了DataGrip,开始设计表结构:
注意这个唯一索引的设计——uk_phone_status(phone, status)。为什么phone和status组合唯一?因为如果用户解绑了(status=3),这个手机号应该可以被其他用户绑定。所以"一个手机号只能绑定一个账号"的准确含义是"一个手机号在同一时间只能有一个有效绑定"(status≠3的记录唯一)。这个设计如果不仔细想,写成UNIQUE KEY uk_phone(phone),用户解绑后手机号就永远不能被别人绑定了。
⚡ 后端习惯 #4:设计表结构时,要考虑"状态流转"。绑定→解绑→再绑定,数据生命周期不是只有"创建"和"删除"。软删除(status标记)比物理删除好——因为审计、回溯、数据分析都需要历史数据。但软删除的唯一索引设计要特别小心,否则软删除的记录会阻止新数据插入。
老王接着设计了验证码存储方案(用Redis,不用MySQL):
验证码存Redis而不存MySQL,原因有三:
1. 验证码是临时数据,5分钟后就没用了,不需要持久化
2. Redis自带TTL过期,不用自己写定时清理
3. 高频读写(发码、验证、限流),MySQL扛不住
类比——寄快递:验证码就像快递的取件码。你不会把取件码写在户口本上(MySQL永久存储),而是写在一张便利贴上(Redis临时存储),贴在快递柜上,5天后自动撕掉(TTL过期)。这就是为什么验证码用Redis而不是MySQL。
10:30API设计:后端的第一步产出是"接口文档"
API设计完之后,第一时间把接口文档发给前端。让前端先看接口定义,能并行开发——前端对着Mock数据写页面,后端写接口实现。这就是"前后端并行开发"的前提。如果接口定义不清晰,前端开发到一半发现接口缺字段,就得等你改完才能继续。
老王用YAPI(接口管理平台)设计了以下接口:
Request Body (JSON)
{
"phone": "13800138000"
}
Response 200
{
"code": 200,
"message": "验证码已发送",
"data": {
"expireIn": 300
}
}
Response 400 — 发送太频繁
{
"code": 4001,
"message": "发送太频繁,请60秒后重试"
}
Request Body (JSON)
{
"phone": "13800138000",
"code": "123456"
}
Response 200
{
"code": 200,
"message": "绑定成功",
"data": {
"phone": "138****8000",
"boundAt": "2026-04-15T14:30:00"
}
}
Response 400 — 手机号已被绑定
{
"code": 4002,
"message": "该手机号已被其他账号绑定"
}
注意一个细节:响应里手机号是脱敏的(138****8000),不是完整号码。这是安全习惯——API返回的数据永远脱敏,完整号码只在后台管理系统中可见。这个细节90%的新手不会注意,但安全审计一定会查。
⚡ 后端习惯 #5:API设计的三原则:
1. URL是名词,方法是动词:/api/v1/user/phone/bind(POST),不是/api/v1/bindUserPhone
2. 错误码有层次:4001=发送太频繁、4002=手机号已绑定、4003=验证码错误。不要所有错误都返回code:400
3. 敏感数据脱敏:手机号、身份证号、银行卡号,输出时一律打码
11:00排期估时:为什么后端总说2天,实际要4天
老王在排期时,做了一张任务分解表:
| 子任务 | 乐观估时 | 实际估时 | 为什么差这么多 |
| 建表 + Entity + Mapper | 0.5h | 2h | 要写AES加密/解密工具类,要对字段做校验注解 |
| 短信发送Service | 1h | 3h | 要对接短信网关SDK,要做限流逻辑,要做发送日志 |
| 绑定/解绑/更换Service | 2h | 4h | 要处理并发绑定(分布式锁),要做事务保证 |
| Controller + 参数校验 | 1h | 2h | 每个接口的参数校验注解、异常处理、Swagger文档 |
| 站内通知 | 0.5h | 1.5h | 要用MQ异步发,要写消费者 |
| 自测(Postman逐个测) | 1h | 3h | 正常流程 + 异常流程 + 边界条件,至少20个场景 |
| 联调(和前端) | 1h | 3h | 前端问题、字段不匹配、数据格式调整,改来改去 |
| 代码评审修改 | 0.5h | 2h | 评审意见修改、补充测试、优化代码结构 |
| 合计 | 7.5h | 20.5h | 实际估时 = 乐观估时 × 2.7 |
新手后端容易犯的错误是:按"乐观估时"报排期。7.5小时 = "今天下午就能做完"。但实际干起来你会发现:AES加密工具类的密钥管理就花了半小时;短信网关SDK文档有坑;分布式锁的key设计要想清楚;前端联调时总有字段对不上。所以老手的经验法则是:乐观估时 × 2.5~3 = 实际估时。
⚡ 后端习惯 #6:估时的时候,一定要把"自测时间"和"联调时间"单独列出。很多新人只估"写代码的时间",觉得自己半天就能写完。但写完代码只是30%的工作量——自测要花30%、联调20%、改评审意见20%。如果排期里不包含自测联调时间,你一定会加班。
类比——装修报价:装修师傅报价的时候不会只报"买材料的钱"。他会报:材料费+人工费+垃圾清运费+验收整改费+意外修补费。后端估时也一样——"写代码的时间"只是材料费,"自测+联调+改评审"就是垃圾清运和验收整改。
13:00开始写码:分层不是形式主义,是工程纪律
午休回来,老王打开IntelliJ IDEA,开始写代码。注意他写代码的顺序——不是从Controller往下写,而是从底层往上写:
① Entity → Mapper
→
② Service接口
→
③ Service实现
④ Controller
→
⑤ 参数校验
→
⑥ 异常处理
从底层(数据)写到上层(接口),不是反过来
为什么从底层往上写?因为底层最稳定——表结构你已经设计好了,Entity和Mapper几乎是机械翻译。写完底层,上层就有了"地基"。如果从Controller往下写,你写到Service的时候发现表结构要改,底层一改上层全要跟着改。这就像盖楼——你不会先搭屋顶再挖地基。
第一步:Entity层(数据实体)
UserPhoneBinding.java — user-center-service/src/main/java/.../entity/
UserPhoneBinding.java
UserPhoneBindingMapper.java
SmsService.java
1@Data
2@TableName("user_phone_binding")
3public class UserPhoneBinding {
4 @TableId(type = IdType.AUTO)
5 private Long id;
6
7 private Long userId;
8
9 // 手机号加密存储,不存明文!
10 private String phone; // AES加密后的值
11
12 /** 1=绑定中 2=已绑定 3=已解绑 */
13 private Integer status;
14
15 @TableField(fill = FieldFill.INSERT)
16 private LocalDateTime createdAt;
17
18 @TableField(fill = FieldFill.INSERT_UPDATE)
19 private LocalDateTime updatedAt;
20}
✓ No errors
UTF-8
Java 17
Ln 10, Col 27
第二步:Service层 — 核心业务逻辑
@Service
@Slf4j
public class PhoneBindingServiceImpl implements PhoneBindingService {
@Autowired
private StringRedisTemplate redis;
@Autowired
private SmsGatewayClient smsClient; // 短信网关
@Autowired
private UserPhoneBindingMapper bindingMapper;
@Autowired
private RedissonClient redisson; // 分布式锁
/**
* 发送验证码 — 4层防护
* 1. 60秒内不能重发
* 2. 当日不超过5次
* 3. 手机号格式校验
* 4. 生成6位随机码 + 存Redis + 调网关
*/
public SendCodeResult sendCode(String phone) {
// 1. 手机号格式校验
if (!PhoneUtil.isValid(phone)) {
throw new BizException("手机号格式不正确");
}
// 2. 60秒发送间隔限制
String limitKey = "sms:limit:" + phone;
Boolean notExpired = redis.hasKey(limitKey);
if (BooleanUtil.isTrue(notExpired)) {
throw new BizException("发送太频繁,请60秒后重试");
}
// 3. 当日次数限制(最多5次)
String countKey = "sms:count:" + phone + ":" + DateUtil.today();
Long count = redis.opsForValue().increment(countKey);
if (count == 1) {
redis.expire(countKey, Duration.ofDays(1));
}
if (count > 5) {
throw new BizException("今日发送次数已达上限");
}
// 4. 生成6位验证码
String code = RandomUtil.randomNumbers(6);
// 5. 调短信网关发送
try {
smsClient.send(phone, "您的验证码是:" + code + ",5分钟内有效");
} catch (Exception e) {
log.error("短信发送失败, phone={}", phone, e);
throw new BizException("验证码发送失败,请稍后重试");
}
// 6. 存Redis(5分钟过期)+ 设60秒间隔锁
redis.opsForValue().set(
"sms:code:" + phone, code, Duration.ofSeconds(300));
redis.opsForValue().set(limitKey, "1", Duration.ofSeconds(60));
log.info("验证码发送成功, phone={}, code={}", phone, code);
return new SendCodeResult(300);
}
/**
* 绑定手机号 — 并发安全处理
*/
@Transactional(rollbackFor = Exception.class)
public BindResult bindPhone(Long userId, String phone, String code) {
// 1. 验证码校验
String cacheCode = redis.opsForValue().get("sms:code:" + phone);
if (cacheCode == null) {
throw new BizException("验证码已过期,请重新获取");
}
if (!cacheCode.equals(code)) {
// 验证码错误,增加重试计数
Long retries = redis.opsForValue().increment("sms:retry:" + phone);
if (retries >= 5) {
redis.delete("sms:code:" + phone); // 锁定,删验证码
throw new BizException("验证码错误次数过多,请重新获取");
}
throw new BizException("验证码错误,还可尝试" + (5 - retries) + "次");
}
// 2. 分布式锁:防止同一用户并发绑定
RLock lock = redisson.getLock("phone:bind:lock:" + phone);
try {
boolean locked = lock.tryLock(3, TimeUnit.SECONDS);
if (!locked) {
throw new BizException("系统繁忙,请稍后重试");
}
// 3. 检查手机号是否已被绑定(唯一性校验)
boolean exists = bindingMapper.existsActiveBinding(phone);
if (exists) {
throw new BizException("该手机号已被其他账号绑定");
}
// 4. 写入绑定记录(加密存储)
UserPhoneBinding binding = new UserPhoneBinding();
binding.setUserId(userId);
binding.setPhone(AESUtil.encrypt(phone));
binding.setStatus(2); // 已绑定
bindingMapper.insert(binding);
// 5. 删除验证码(一次性使用)
redis.delete("sms:code:" + phone);
// 6. 异步发送站内通知
// (通过MQ发送,不阻塞主流程)
log.info("手机号绑定成功, userId={}, phone={}", userId, phone);
return new BindResult(PhoneUtil.mask(phone));
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
看这段代码,注意几个后端"本能反应":
1. 每个外部依赖调用都包try-catch:短信网关可能超时/挂掉
2. 分布式锁的finally块一定要unlock:否则锁泄漏
3. 验证码验证通过后立刻删除:防止重放攻击
4. 手机号加密存储:数据库被脱库也不泄露
5. 每一步操作都有日志:出问题能排查
⚡ 后端习惯 #7:看一个后端是否成熟,看他代码里的"防御性编程"程度。成熟的代码里充满了if (xxx != null)、try-catch、finally。不是因为胆小,而是因为生产环境的每一行代码都会被极端情况考验。NPE(空指针异常)是Java后端最常见的线上事故。
15:00自测:你不测的,线上一定会出问题
代码写完了,老王不急着提PR,而是打开Postman开始自测。他的自测清单:
📋 自测清单(逐项打勾)
| # | 测试场景 | 预期结果 | 状态 |
| 1 | 正常发送验证码 | 返回200,收到短信 | ✅ |
| 2 | 60秒内重复发送 | 返回4001"发送太频繁" | ✅ |
| 3 | 当日发送第6次 | 返回4001"次数已达上限" | ✅ |
| 4 | 错误的手机号格式 | 返回400"格式不正确" | ✅ |
| 5 | 正确验证码绑定 | 返回200"绑定成功" | ✅ |
| 6 | 错误验证码 | 返回4003"验证码错误" | ✅ |
| 7 | 验证码连续错5次 | 返回4003"次数过多" | ✅ |
| 8 | 已过期验证码 | 返回4003"已过期" | ✅ |
| 9 | 同一手机号被两个用户绑定 | 第二个返回4002"已被绑定" | ✅ |
| 10 | 解绑后重新绑定同一号码 | 返回200"绑定成功" | ✅ |
| 11 | 验证成功后重放同一验证码 | 返回4003"已过期"(已被删除) | ✅ |
| 12 | 参数为空/类型错误 | 返回400"参数校验失败" | ✅ |
很多新人觉得自测是"浪费时间"——"代码我写完了,不会有bug的"。但老手知道,你脑子里跑通的逻辑和实际跑的代码之间,永远有一条沟。比如第11条——验证成功后重放验证码,如果你忘了在绑定成功后删除验证码,用户可以用同一个验证码反复调用绑定接口。自测就是填这条沟的过程。
Test #2: 60秒内重复发送
{ "phone": "13800138000" }
400 Bad Request 47ms
{
"code": 4001,
"message": "发送太频繁,请60秒后重试",
"timestamp": "2026-04-15T15:02:13.456"
}
⚡ 后端习惯 #8:自测时不仅要测"正常流程",更要花70%时间测"异常流程"。正常流程你的代码当然能走通——你写代码时就是按正常流程写的。出问题的地方永远在边界条件:空值、超长字符串、并发请求、网络超时、数据库连接断开。测异常场景才是自测的核心价值。
16:00代码评审:你漏的,同事帮你找
自测通过后,老王提交了Git MR(Merge Request):
wang@macbook ~/user-center-service $ git checkout -b feature/phone-binding
wang@macbook ~/user-center-service (feature/phone-binding) $ git add .
wang@macbook ~/user-center-service (feature/phone-binding) $ git commit -m "feat: 用户手机号绑定功能(发送验证码+绑定+解绑+更换)"
[✓] 代码格式(IDEA格式化了)
[✓] 单元测试(核心Service有测试)
[✓] 日志(关键路径都有info/error日志)
[✓] 异常处理(外部依赖都包了try-catch)
[✓] 安全(手机号加密、验证码限流、分布式锁)
[✓] 接口文档(YAPI已更新)
wang@macbook ~/user-center-service (feature/phone-binding) $ git push origin feature/phone-binding
Enumerating objects: 24, done.
Counting objects: 100% (24/24), done.
Writing objects: 100% (15/15), 4.2 KiB | 4.2 MiB/s, done.
remote: To create a merge request, visit:
remote: https://gitlab.com/.../merge_requests/new
组长(老李)15分钟后在GitLab上给出了评审意见:
李
老李(组长)16:15 评论
整体写得不错,几个点改一下:
1. PhoneBindingServiceImpl.java L52
验证码校验通过后删除验证码之前,重试次数也要删除。否则下次发送验证码后,重试计数还是之前的值。
2. PhoneBindingServiceImpl.java L87
分布式锁的key用手机号没问题,但如果用户快速点击两次"绑定"按钮(同一手机号),第二次会拿不到锁直接返回"系统繁忙"。建议把提示语改成"正在处理中,请勿重复操作"。
3. UserPhoneBindingMapper.java
existsActiveBinding 方法的SQL,记得用 phone 的加密值去查(AESUtil.encrypt(phone)),不要用明文查。
4. 建议补充 ✨
在 bindPhone 方法里加一个 @RateLender 注解,限制单个用户1分钟最多绑3次。防止恶意绑定/解绑操作。
评审意见里有3个修改、1个建议。第1条是个真实bug——重试计数没清理。第2条是用户体验优化。第3条是安全漏洞——如果用明文查数据库,那加密就没意义了。这些就是代码评审的价值:你写代码时注意力在"实现功能",评审时注意力在"找漏洞和隐患"。
⚡ 后端习惯 #9:评审代码时关注点排序:安全 > 数据一致性 > 性能 > 可读性 > 代码风格。安全问题和数据不一致是P0(必须改),性能是P1(建议改),代码风格是P2(看心情)。不要把时间花在争论"花括号要不要换行"上。
16:30部署上线:从不直接点"发布"
评审通过,MR合并到dev分支。老王开始部署流程:
dev环境
→
test环境
→
预发布环境
→
生产环境
每个环境都要验证,不是直接上生产
ci@gitlab-runner $ mvn clean package -DskipTests
[INFO] BUILD SUCCESS
ci@gitlab-runner $ docker build -t user-center:v2.3.1 .
Successfully tagged user-center:v2.3.1
ci@gitlab-runner $ kubectl set image deployment/user-center \
user-center=user-center:v2.3.1 -n test
deployment.apps/user-center image updated
QA验证通过 ✅ (2026-04-15 16:45)
ci@gitlab-runner $ kubectl set image deployment/user-center \
user-center=user-center:v2.3.1 -n staging
冒烟测试通过 ✅ (2026-04-15 17:00)
部署到生产前,老王做了3件准备工作:
1. 数据库迁移脚本先在staging跑一遍——确认DDL不会锁表
2. 准备回滚方案——记下当前版本号v2.3.0,出问题30秒内回滚
3. 通知相关方——在工作群发"准备发布v2.3.1,预计5分钟"
⚡ 后端习惯 #10:上线前的检查清单(老王的版本):
✅ 数据库迁移脚本在staging验证过
✅ 回滚方案准备好(上一版本镜像tag记下来了)
✅ 监控告警已配置(接口RT、错误率、短信发送量)
✅ 功能开关/灰度策略已设置
✅ 发布窗口选在工作日下午4-5点(不是周五!不是凌晨!)
✅ 通知群里发过上线公告
🚫 永远不要在周五下午发布!
这是后端的铁律。周五下午发布 = 如果出问题,你周末得加班修。如果必须周五发,至少留出2小时的窗口期,并且确保有人值班。老王见过太多"周五下午发布导致整个周末都在修bug"的事故了。
17:30复盘总结:今天的工作思路拆解
一天结束,老王在笔记本上做了简单的复盘——这是他的日常习惯:
📊 今日复盘
| 环节 | 用时 | 做的好的 | 可改进的 |
| 需求分析 | 1h | 拆出了8个子功能,识别了6个风险 | 应找PM当面确认验证码尝试次数限制的逻辑 |
| DB设计 | 1h | 唯一索引设计考虑了状态流转 | - |
| API设计 | 0.5h | 接口文档同步给了前端,并行开发 | 响应体可以统一加requestId |
| 编码 | 3h | 分层清晰,防御性编程到位 | 分布式锁可以抽出独立方法减少嵌套 |
| 自测 | 2h | 测了12个场景,包括异常场景 | 应补一个并发绑定的压测 |
| 评审 | 1h | 评审意见有价值,改了3个问题 | 应在提MR前自查第1条(重试计数) |
| 部署 | 0.5h | 分环境部署,有回滚方案 | - |
复盘的目的不是"自我批评",而是"提取经验"。今天评审发现的3个问题,以后写代码时会主动检查。今天在需求分析阶段多花1小时,省下了后面至少3小时的返工。这就是"慢即是快"——前期准备越充分,后期返工越少。
后端工作思路核心总结
🎯 后端工作10条核心习惯
| # | 习惯 | 新人做法 | 老手做法 |
| 1 | 读需求 | 只看"要做什么" | 找"限制条件"和"异常场景" |
| 2 | 拆需求 | 直接开写 | 先画出功能拆解和依赖关系 |
| 3 | 识别风险 | 不考虑异常 | 花30分钟列风险矩阵(红黄绿) |
| 4 | 设计DB | 先写Controller | 先设计表结构,再写代码 |
| 5 | 设计API | 边写边定接口 | 先出接口文档,和前端并行 |
| 6 | 估时 | 按乐观估时 | 乐观估时 × 3 |
| 7 | 写码 | 从上往下写 | 从底层往上写(Entity→Service→Controller) |
| 8 | 自测 | 只测正常流程 | 70%时间测异常场景 |
| 9 | 评审 | 只看代码风格 | 关注安全 > 数据一致性 > 性能 |
| 10 | 部署 | 直接上生产 | 分环境部署,准备回滚方案 |
类比——后端开发像做手术:外科医生不是进了手术室才开始想怎么开刀。术前要读病历(需求分析)、做检查(风险评估)、制定手术方案(DB+API设计)、准备器材和血库(编码+测试方案)、安排手术间和麻醉(部署准备)、术后观察(监控+复盘)。后端开发也一样——准备时间占60%,实际"写代码"只占20%,剩下20%是测试和部署。新手以为后端就是"写代码",老手知道后端是"系统工程"。