🪟 第19章 C#与SQL Server实现企业人事管理系统
📄 一句话概括

验收不是「点一遍界面」,是「八条断言全部等于期望值」——眼睛会漏,数字不会。 桌面程序没有信任边界:用户能拿 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 秒实验

  1. 手动删掉 tb_dept 里的一行(先关外键), 再跑「部门总数」那条—— 看断言怎么报出来。 再想想:这种错误界面上看得出来吗? 看不出来。
  2. 把 CHK 里某条的期望值改错(比如写成 9),看它报「不通过」。 断言的价值就在这一行输出。
  3. 想一想:如果要求「每个在职员工都必须有账号」,这条断言怎么写? 员工表左连账号表,筛「在职且账号为空」,期望 0。 左连是关键——内连只会查出「有账号的那些」,永远为 0。

常见误区

  1. 「验收就是点一遍界面」。 眼睛只能看「明显不对」,看不出「少了一个部门」。 断言能。
  2. 「桌面程序也可以做权限控制」。 能做,但拦不住——用户能直连数据库。 真正的控制在库账号的授权上。
  3. 「学了三遍同一件事是浪费时间」。 换的是语言和架构,练的是同一套套路。 套路练三遍才长在身上。
自测三问(点开对答案) 1. 为什么验收要用「断言」而不是「看一遍」? —— **断言有确定的期望值,能自动跑、不会漏。** **眼睛只能发现「明显不对」,发现不了「少一个部门」。** 2. 桌面程序的「没有信任边界」是什么意思? —— **用户能拿 SSMS 直接连数据库**,**界面上的权限判断一行都拦不住。** **所以安全底线在库账号的 `GRANT` 上。** 3. 下一章换成 Java 之后,哪几样东西没变? —— **抠约束、表设计、分层、事务边界、权限按角色。** **变的是界面框架和连接方式。**

下一篇进第 20 章 20.1,讲学生成绩管理系统:这回换成 Java + Web, 三种角色看到三个完全不同的界面——看看桌面那套权限思路在浏览器里怎么落地。

相关笔记