主题
界面
主题
从本节起进入 S5 项目实战。前面 161 节学的是"知识点",这一阶段要解决另一件事:
把它们串起来,做出一个能跑、能测、能部署、能讲清楚的东西 ✓★ 项目 A 是一个真实的、可运行的图书借阅管理系统 —— 代码在 labs/project-a,30 个测试全部通过 ✓(含并发测试)。
★ 本节做第一件事:把需求说清楚,并画出范围边界 ✓✓
先看一段真实的需求 ✗:
"我们要一个图书馆系统,能借书还书就行。"
★ 这句话没法直接开工 ✗✗ —— 因为它没说清:
★ 需求分析的产出,就是把这些问题变成可执行的决定 ✓✓
★ 项目 A 的用例清单 ✓:
| 角色 | 用例 | 结果 |
|---|---|---|
| 读者 ★★★ | 借书 | 库存 -1,生成借阅记录(带应还日) |
| 读者 ★★★ | 还书 | 库存 +1,记录标记为已归还 |
| 读者 ★★ | 续借 | 应还日延长,续借次数 +1 |
| 读者 / 管理员 ★★ | 查图书(关键字 + 分页) | 返回分页列表 |
| 读者 ★★ | 查自己的借阅记录 | 含是否超期 |
| 管理员 ★★ | 入库图书 | 新增书目或增加副本 |
| 管理员 ★★ | 看超期清单 | 列出所有超期未还 |
★ 用例的写法 ✓✓:
"作为 <角色>,我要 <做什么>,以便 <得到什么价值>"
例:"作为读者,我要续借,以便在不方便到馆时延长借期" ✓
★ 这六条是需求分析的真正产出 ✓✓ —— 它们决定了数据库怎么设计、接口怎么写、测试怎么测:
| # | 规则 | 为什么重要 |
|---|---|---|
| ① | 一个读者最多同时借 3 本 | 防止少数人借空库存 |
| ② | 借期 30 天,到期日为借出日 + 30 | 决定 due_at 怎么算 |
| ③ | 最多续借 1 次,每次延长 15 天 | 防止无限续借 |
| ④ | 已超期不能续借 | 否则超期可以无限延后 ✗ |
| ⑤ | 同一本书未还不能重复借 | 防止一个人占多本同名书 |
| ⑥ | 读者停用后不能借(R003 就是停用) | 挂失/违规的处理 |
★★ 关键设计:规则要可配置 ✓✓
library:
rules:
max-borrow-per-reader: 3
borrow-days: 30
max-renew-count: 1
renew-days: 15如果写死成
if (count > 3),业务方改一次就要改代码、测试、发版 ✗抽成配置后,改配置就能生效 ✓✓
代码里对应
LibraryRules(@ConfigurationProperties)✓
★ 而且测试要断言"规则真的来自配置" ✓(见 《项目 A:测试方案与分层测试》)
★ 这是需求分析里最容易被跳过、也最值钱的一步 ✓✓:
| 内容 | |
|---|---|
| ✅ 本期做 | 图书 CRUD(含入库/加副本)、查询(关键字+分页)、借/还/续借、借阅历史、超期清单 |
| ❌ 本期不做 | 登录与权限、罚金计算、预约、多分馆、图书推荐、消息通知、前端框架 |
★★★ 为什么"不做什么"必须写下来 ✓✓:
- 防止范围蔓延:做到一半有人加需求,你可以拿出这份清单说"这不在本期范围" ✓
- 让设计能收敛:不知道"不做登录",就会一直纠结要不要做用户表
- 面试时能讲清:能说清边界,说明你想清楚了 ✓✓
★ 一条实用写法 ✓:把"不做"也写成将来可以做的形式:
本期不做:登录与权限
理由:本项目是内部系统,先不做鉴权;接口层已预留(S5-003 的错误码结构)
将来做:接入 Spring Security,或在网关层(S4-020)统一鉴权 ✓★ 每条需求都要写出"怎么算做完" ✓✓:
| 需求 | 验收标准(可测) |
|---|---|
| 借书 | 库存 -1;记录 status=BORROWED;到期日 = 借出日 + 30 天 ✓ |
| 库存不足 | 报 NO_AVAILABLE_COPY;库存保持 0,不能变负 ✓✓ |
| 读者停用 | 报 READER_SUSPENDED;不产生任何写操作 ✓ |
| 重复归还 | 第二次报 ALREADY_RETURNED;库存只加一次 ✓✓ |
| 并发借 | 10 个线程抢 1 本,恰好 1 个成功 ✓✓✓ |
★★ 这些验收标准直接变成了测试用例 ✓✓ —— 项目的 BorrowFlowIntegrationTest 与 BorrowConcurrencyTest 就是照着这张表写的 ✓
★ 这就是"需求可测试"的价值 ✓:写完需求时,测试清单也就有了 ✓
★ 需求分析之后,下一步不是写代码,而是画三张图 ✓:
① 用例图 / 用例清单 → 本节(5.1)
② 实体关系(有哪些东西、它们什么关系)→ S5-002
③ 接口清单(对外提供什么)→ S5-003★ 本项目的三个实体 ✓:
| 实体 | 说明 |
|---|---|
| Book(图书) | 有 ISBN、书名、作者、总量与可借量 |
| Reader(读者) | 有卡号、姓名、状态(正常/停用) |
| BorrowRecord(借阅记录) | 一次借还是一条记录(含借出日、应还日、归还日) |
★ 一个关键判断 ✓✓:
"借阅记录"是核心实体,不是附属信息 ✓
因为:借还这个动作本身要被留痕(谁、什么时候、还了没)✓✓
如果只在 Book 上记 "当前被谁借了",就查不到历史 ✗
需求:借书
作为:读者
我要:借一本在馆的图书
以便:把它带回家看
前置条件:读者状态正常、未达借阅上限、该书有可借副本
后置条件:该书可借数 -1,生成一条借阅记录(到期日 = 今天 + 30 天)
异常:
· 读者不存在 / 已停用 → 拒绝(READER_NOT_FOUND / READER_SUSPENDED)
· 已达上限 → 拒绝(BORROW_LIMIT_REACHED)
· 该书已借未还 → 拒绝(ALREADY_BORROWED)
· 无可用副本 → 拒绝(NO_AVAILABLE_COPY)@ConfigurationProperties(prefix = "library.rules")
public class LibraryRules {
private int maxBorrowPerReader = 3;
private int borrowDays = 30;
// ...
}// ✗ 业务方说"改成 5 本",你要改代码 + 发版
if (activeCount > 3) { throw new BusinessException("已达上限"); }
// ✓ 规则来自配置
if (activeCount >= rules.getMaxBorrowPerReader()) { ... }代码:labs/project-a(图书借阅管理系统)
启动:
cd labs
.\mvnw.cmd -pl project-a spring-boot:run
# 打开本机 18080 端口 → 前端页面
# 接口:/api/books或用部署脚本(推荐):
pwsh -File scripts/run-project-a.ps1 # 打包 + 启动 + 等健康检查为什么讲义里不写
http://...这样的外链 ✗✓: VitePress 构建时会校验外链是否可达,localhost在构建机上当然不可达 ✗ —— 于是整个构建失败(本项目就是这么踩到的 ✓)。写端口、写在代码块里即可。
★ 本节的需求在代码里的落点 ✓:
| 需求 | 代码位置 |
|---|---|
| 六条业务规则 | application.yml 的 library.rules + LibraryRules |
| 用例(借/还/续借) | BorrowService |
| 验收标准 | BorrowFlowIntegrationTest(12 个)、BorrowConcurrencyTest(3 个) |
★ 实测 ✓:30 个测试全部通过 ✓✓
max-borrow-per-reader 改成 5:观察借阅上限测试是否跟着变 ✓✓;| # | 错误 | 说明 |
|---|---|---|
| 1 | 需求模糊就开工 ✗✗ | 会返工,且返工成本随进度指数上升 ✓ |
| 2 | 只写"做什么",不写"不做什么" ✗✗ | 范围蔓延 ✓✓ |
| 3 | 业务规则写死在代码里 ✗✗ | 每次调整都要发版 ✓ |
| 4 | 验收标准不可测(如"要好用")✗✗ | 无法判断做没做完 ✓✓ |
| 5 | 需求分析就想把表设计好 ✗ | 顺序错了(先需求 → 再实体 → 再表)✓ |
| 6 | 忽略异常分支 ✗✗ | 异常才是要实现的大头(本例 4 种异常)✓✓ |
目标:把一个模糊需求,做成能开工的清单。
步骤:
验收方式:能说出"你的需求里哪三条最可能被误解"。
扩展任务:给本项目加一个真实需求「预约」(书被借走后可以预约):
"借书要快,不能出错。"《项目 A:数据库设计》:把三个实体变成三张表,并决定约束与索引。
暂无