验收不是「点一遍界面」,是「八条断言全部等于期望值」——眼睛会漏,数字不会。 桌面程序没有信任边界:用户能拿 SSMS 直连数据库,界面上的权限一行都拦不住。 这一章真正能带走的是五条:抠约束、表设计、分层、事务边界、权限按角色。
第 18 章的总结讲「这个项目训练了什么」,这一章要讲「它和下一个项目的分界在哪」。 因为下一章换成 Java + JSP,桌面变 Web,很多假设会整个翻过来。 教材 19.8 分两小节:桌面程序与 Web 程序的分界、下一章换成 Java 又换了什么。 这两小节看着像闲聊,其实是全书最重要的一页——它解释「为什么学了三遍同一件事」。 这一节先把八条断言跑一遍(验收动作),再摆那张分界表, 最后挑出「下一章还用得上」的五条。
一、八条断言:验收到底查什么
| 检查项 | 期望 | 它防的是什么 |
|---|---|---|
| 员工总数 | = 8 | 多一行少一行都要知道 |
| 在职员工都挂到部门上 | = 0 | 外键被绕过 |
| 在职员工都挂到岗位上 | = 0 | 同上 |
| 工号无重复 | = 0 | 唯一约束缺失 |
| 部门树无环 | = 0 | 从根走不到的部门 |
| 考勤无重复日 | = 0 | 重复导入 |
| 工资无重复月 | = 0 | 重复计算 |
| 账号都指向真实员工 | = 0 | 删人没删号 |
这八条的共同点是「期望值是个确定的数」。
「员工总数 = 8」比「员工总数看起来对」有用一万倍——后者只能靠眼睛,前者能挂到脚本里自动跑。
第五条(部门树无环)是含金量最高的一条。
它用递归把所有「从根能走到的部门」数出来,跟总数一比——
少一个就说明有部门挂在环上,从根永远走不到。
这种错误在界面上完全看不出来(那个部门还在列表里),只有断言能抓。
第八条(账号指向真实员工)也值得说: 删员工时忘了删账号,
账号还在,人已经没了——登录时查到账号、查不到员工,程序就崩在那一行。
所以删员工的正确做法是「改状态为离职 + 停用账号」,不是 DELETE。
二、桌面与 Web 的分界:六行表
| 维度 | 桌面(本章) | Web(下一章) |
|---|---|---|
| 连接 | 程序直连数据库 | 程序 → 服务端 → 数据库 |
| 连接串存哪 | 客户端配置文件里 | 服务端,客户端看不到 |
| 数据库账号 | 每个客户端一个库账号 | 只有服务端有库账号 |
| 权限 | 藏在程序里(能被绕过) | 服务端每次请求都校验 |
| 升级 | 每台机器重装 | 服务端改一次 |
| 信任边界 | 没有——用户能直接连库 | 有,客户端一律不信 |
最关键的是最后一行:桌面程序没有信任边界。
用户拿 SSMS 直连数据库,你的菜单一行都拦不住。
所以桌面程序的安全底线不在程序里,在数据库账号的授权上。
给人事的账号只 GRANT 它该看的表——这比界面上隐藏菜单有用一万倍。
而 Web 程序反过来: 客户端什么都不信,每次请求都重新校验。
这不是 Web 更安全,是它「必须」这么做——因为浏览器里的代码,用户想看什么就能看什么。
三、下一章换成 Java,换了什么、没换什么
| 东西 | 下一章还一样吗 |
|---|---|
| 从制度里抠约束 | 一样,而且更要紧 |
| 七张表 + 自引用树 | 换成班级/学生/成绩,思路一样 |
| 实体类 + 公共类 | 一样,只换语言 |
| 事务包住「要么全成」 | 一样 |
| 菜单按权限生成 | 一样,菜单换成页面 |
| WinForms 的坑 | 不用了 |
| 连接串放客户端 | 不用了,挪到服务端 |
「Framework 会换,套路不会换」这句话,在这一章终于有了具体的六行证据。 前三行是「换了语言还在」的,后两行是「换了架构才消失」的。 注意第四行「事务」在两个架构里都还在,但位置变了。 桌面程序里事务开在窗体的事件处理里;Web 程序里事务开在服务端的一个请求处理里—— 而且 Web 的请求天然是「一次请求一个事务」,比桌面更好管。 所以复习这一章时,别看代码,看 19.1 的那两张表(业务动作对到表、制度原话对到约束)。 那才是能带走的东西。 至于按钮怎么摆、窗体怎么弹,换个框架就全废。
四、动手 Demo:八条冒烟断言 + 分界对照
这一段把八条断言真跑一遍,每条都带上「期望值」和「结论」两列——跑完直接看有没有「不通过」。 顺便量一下八条总共多少毫秒,跟「人工点一遍界面」对比一下。
CHK = [
('员工总数', 'SELECT COUNT(*) FROM tb_employee', 8),
('在职员工都挂到部门上',
'SELECT COUNT(*) FROM tb_employee e'
' LEFT JOIN tb_dept d ON d.id = e.dept_id WHERE d.id IS NULL', 0),
('工号无重复',
'SELECT COUNT(*) FROM (SELECT eno FROM tb_employee'
' GROUP BY eno HAVING COUNT(*) > 1)', 0),
('部门树无环',
'WITH RECURSIVE t(id) AS ('
' SELECT id FROM tb_dept WHERE parent IS NULL'
' UNION ALL SELECT d.id FROM tb_dept d JOIN t ON d.parent = t.id)'
' SELECT (SELECT COUNT(*) FROM tb_dept) - (SELECT COUNT(*) FROM t)', 0),
]
t0 = time.perf_counter()
for name, sql, want in CHK:
got = c.execute(sql).fetchone()[0]
print(name, got, want, '通过' if got == want else '不通过')
print('总耗时 %.2f ms' % ((time.perf_counter() - t0) * 1000))
实跑输出:
--------------------------------------------------------------------
本章总结:一个项目验收前该查什么
--------------------------------------------------------------------
① 冒烟检查:八条断言
--------------------------------------------------------------------
| 检查项 | 实测 | 期望 | 结论 |
|---|---:|---:|:---:|
| 员工总数 | 8 | 8 | 通过 |
| 在职员工都挂到部门上 | 0 | 0 | 通过 |
| 在职员工都挂到岗位上 | 0 | 0 | 通过 |
| 工号无重复 | 0 | 0 | 通过 |
| 部门树无环(根可达数=部门数) | 0 | 0 | 通过 |
| 考勤无重复日 | 0 | 0 | 通过 |
| 工资表无重复月 | 0 | 0 | 通过 |
| 账号都指向真实员工 | 0 | 0 | 通过 |
八条检查总耗时:0.49 ms
第 5 条(部门树无环)是这张表里最有含金量的。
它用递归把所有「从根能走到的部门」数出来,跟总数一比——少一个就说明有部门挂在环上、
从根永远走不到。
这类「断言式检查」比人工点界面可靠,而且能挂到每次改代码之后自动跑。
② 桌面程序与 Web 程序的分界
--------------------------------------------------------------------
| 维度 | 桌面(本章) | Web(下一章) |
|---|---|---|
| 连接 | 程序直连数据库 | 程序 → 服务端 → 数据库 |
| 连接串存哪 | 客户端配置文件里 | 服务端,客户端看不到 |
| 数据库账号 | 每个客户端一个库账号 | 只有服务端有库账号 |
| 权限 | 藏在程序里(能被绕过) | 服务端每次请求都校验 |
| 升级 | 每台机器重装 | 服务端改一次 |
| 信任边界 | 没有——用户能直接连库 | 有,客户端一律不信 |
这张表里最关键的是「信任边界」那一行。
桌面程序里所有权限判断都只是「界面上的礼貌」——用户拿 SSMS 直连数据库,你的菜单一行都拦不住。
所以桌面程序的安全底线不在程序里,在数据库账号的授权上:给人事的账号只 GRANT 它该看的表,
这比界面上隐藏菜单有用一万倍。
③ 这一章真正训练的东西
--------------------------------------------------------------------
| 学到的东西 | 下一章还用吗 |
|---|---|
| 从制度里抠约束 | 用,而且更要紧 |
| 七张表 + 自引用树 | 换成班级/学生/成绩,思路一样 |
| 实体类 + 公共类 | 完全一样,只换语言 |
| 事务包住「要么全成」 | 完全一样 |
| 菜单按权限生成 | 一样,只是菜单换成 JSP/页面 |
| WinForms 的坑 | 不用了 |
最后一行要单独说:框架会换,套路不会换。
这一章真正沉淀下来的是前三列里「用」的那五条,至于按钮怎么摆、窗体怎么弹,换个框架就全废。
所以复习这一章时,别看代码,看 19.1 的那两张表——「业务动作 → 表」和「制度原话 → 约束」。
那才是能带走的东西。
八条加起来不到 1 毫秒,人工点一遍界面要五分钟。 而且人工只能看「有没有明显不对」,看不出「部门树里少了一个」。 这套东西可以再进一步: 把期望值也写成配置, 不符合就抛异常退出——那就成了「自动化验收」,可以挂到每次提交之后跑。 最后一句: 这一章写完的不是「一个人事系统」,是「一套做业务系统的套路」。 下一章换了语言、换了架构,套路一样。
30 秒实验
- 手动删掉
tb_dept里的一行(先关外键), 再跑「部门总数」那条—— 看断言怎么报出来。 再想想:这种错误界面上看得出来吗? 看不出来。 - 把
CHK里某条的期望值改错(比如写成 9),看它报「不通过」。 断言的价值就在这一行输出。 - 想一想:如果要求「每个在职员工都必须有账号」,这条断言怎么写? 员工表左连账号表,筛「在职且账号为空」,期望 0。 左连是关键——内连只会查出「有账号的那些」,永远为 0。
常见误区
- 「验收就是点一遍界面」。 眼睛只能看「明显不对」,看不出「少了一个部门」。 断言能。
- 「桌面程序也可以做权限控制」。 能做,但拦不住——用户能直连数据库。 真正的控制在库账号的授权上。
- 「学了三遍同一件事是浪费时间」。 换的是语言和架构,练的是同一套套路。 套路练三遍才长在身上。
自测三问(点开对答案)
1. 为什么验收要用「断言」而不是「看一遍」? —— **断言有确定的期望值,能自动跑、不会漏。** **眼睛只能发现「明显不对」,发现不了「少一个部门」。** 2. 桌面程序的「没有信任边界」是什么意思? —— **用户能拿 SSMS 直接连数据库**,**界面上的权限判断一行都拦不住。** **所以安全底线在库账号的 `GRANT` 上。** 3. 下一章换成 Java 之后,哪几样东西没变? —— **抠约束、表设计、分层、事务边界、权限按角色。** **变的是界面框架和连接方式。**下一篇进第 20 章 20.1,讲学生成绩管理系统:这回换成 Java + Web, 三种角色看到三个完全不同的界面——看看桌面那套权限思路在浏览器里怎么落地。
- ☐ (本篇完,下一篇 20.1 需求分析)