功能结构图画完,模块就分完了——每个叶子节点最后都对应「一个窗体 + 一条 SQL」。 「配置」和「数据」要分成两个模块,不然权限没法分开给。 画不出来的地方,就是还没想清楚的地方,别留到写代码时再说。
19.1 回答「要什么」,19.2 回答「怎么分」。 这一节最容易被认为是画图凑数,其实不是——模块怎么切,直接决定后面七章代码怎么组织。 教材 19.2 分三小节:系统目标、系统功能结构、系统预览。 「系统预览」那一节最有意思——它用一段「从点菜单到数据落库」的叙述,把功能和表串起来, 比结构图更能看出设计对不对。 本章的结构图沿用第 18 章那套「树 + 表格」的复刻方式, 不同的是这次多了一张「模块 ↔ 表」矩阵——那张矩阵才是这一节的重点。
一、五棵子树:一棵树画完,模块就分完了
| 一级节点 | 下面挂什么 |
|---|---|
| 登录 | (无子节点) |
| 员工管理 | 员工档案、调岗与离职、花名册导出 |
| 组织管理 | 部门设置、岗位设置 |
| 考勤管理 | 每日考勤、月度汇总 |
| 工资管理 | 工资计算、工资条 |
| 系统管理 | 用户与角色、数据备份 |
这五个一级节点,正好对应 19.1 那张业务动作表里的五件事。 一棵树画完,模块就分完了——这不是巧合,是需求分析做得扎实的结果。 结构图的用处不在于「好看」,在于「能不能对着它派活」。 一个叶子节点如果说不清「谁做、做完怎么验收」,这一块就是没想清楚。 还有一条:叶子节点最好是「动词 + 名词」(新增员工、计算工资)。 如果只能写出名词(比如「员工信息」),说明它其实是一张表,不是功能。
二、组织管理为什么不塞进员工管理
| 放法 | 好处 | 坏处 |
|---|---|---|
| 塞进员工管理 | 菜单少一层 | 给人事开权限时,配置也跟着开了 |
| 独立一级节点 | 配置与数据分开授权 | 菜单多一个 |
这是这一节最值得记的一条:配置和数据必须分开。 部门、岗位是「配置」(一年改两次,改了影响所有人); 员工是「数据」(天天改,改了只影响一个人)。 混在一起,权限就没法分开给。 给人事开「员工管理」,他顺手就能把部门全删了—— 而部门一删,所有人的归属就断了。 这条经验可以迁移到任何系统: 字典表、参数表、机构表,统统该和业务数据分模块, 因为它们的使用者和修改频率完全不同。
三、模块 ↔ 表:谁读谁写
| 模块 | 主要读 | 主要写 |
|---|---|---|
| 员工档案 | employee/dept/post | employee |
| 调岗离职 | employee/transfer | employee/transfer |
| 部门岗位 | dept/post | dept/post |
| 每日考勤 | employee | attend |
| 工资计算 | employee/attend/salary | salary |
| 用户角色 | user | user |
tb_employee 被四个模块读写(员工档案、调岗离职、每日考勤、工资计算)。
这是全系统最热的一张表,也是并发冲突最容易发生的地方。
tb_salary 只被「工资计算」一个模块写。 这种「单写多读」的表最好管——
加锁范围小,冲突面窄,出问题也好定位。
这张矩阵还有个排查用处: 数据不对时,
先看「谁可能写它」,嫌疑范围立刻从整个系统缩到一两个模块。
四、系统预览:一次入职走完整条链
| 步 | 界面发生什么 | 数据库发生什么 |
|---|---|---|
| 1 | 人事点「新增员工」 | — |
| 2 | 填工号、姓名,选部门岗位 | 下拉框读 dept/post |
| 3 | 点保存,校验工号未重复 | SELECT 1 FROM employee WHERE eno=? |
| 4 | 校验通过,写一行 | INSERT INTO employee |
| 5 | 顺手建登录账号 | INSERT INTO user |
| 6 | 刷新列表 | SELECT ... FROM v_emp |
第 3 步和第 4 步之间藏着一个经典问题。
两个人同时保存同一个工号,可能都通过校验、都写进去——
最后靠 UNIQUE 兜住,但报错信息就变成「数据库错误」,而不是「工号已存在」。
更好的做法:别先查,直接插,靠 UNIQUE 拦,再把「唯一约束冲突」翻译成人话。
这样只要一次数据库往返,而且天然没有竞态窗口。
这个思路值得记成一句口诀: 能靠约束保证的,别靠代码检查。
代码检查和写库之间永远有一个时间窗,约束没有。
五、哪些想法不该做成模块
| 想法 | 为什么先不做 |
|---|---|
| 工资自动报税 | 税率表年年变,做进去就得养一个人 |
| 人脸考勤 | 硬件对接是另一个项目 |
| 组织架构图拖拽 | 能画图不等于能用,先出报表 |
| 员工自助 App | 桌面版跑通再说 |
这一列判断标准就一条:谁维护。 一年要改三次以上的规则,就不该硬编进代码。
税率、社保基数、公积金比例,都属于这一类。
做法是把它做成配置表(tb_rate 之类),每年改数据不改代码。
代价是查询要多一次 JOIN,收益是第二年不用发版。
六、动手 Demo:功能结构树、模块矩阵、权限矩阵
这一节把三样东西各跑一遍:功能结构树、模块读写矩阵、角色权限矩阵。 最后一样是重点——它说明「菜单不是画出来的,是算出来的」。
MENU = [
('员工管理', ['员工档案', '调岗与离职', '花名册导出']),
('组织管理', ['部门设置', '岗位设置']),
('考勤管理', ['每日考勤', '月度汇总']),
('工资管理', ['工资计算', '工资条']),
('系统管理', ['用户与角色', '数据备份']),
]
GRANT = {
'管理员': None, # None 表示全给
'人事': ['员工档案', '调岗与离职', '花名册导出',
'每日考勤', '月度汇总', '工资计算', '工资条'],
'普通': ['每日考勤'],
}
def build(role):
ok = GRANT[role]
out = []
for top, subs in MENU:
keep = subs if ok is None else [x for x in subs if x in ok]
if keep:
out.append((top, keep))
return out
for role in ('管理员', '人事', '普通'):
tree = build(role)
print(role, len(tree), sum(len(s) for _, s in tree))
实跑输出:
--------------------------------------------------------------------
系统设计:功能结构决定模块边界
--------------------------------------------------------------------
① 功能结构:一棵树
--------------------------------------------------------------------
企业人事管理系统
├─ 登录
├─ 员工管理
│ ├─ 员工档案(增删改查)
│ ├─ 调岗与离职
│ └─ 花名册导出
├─ 组织管理
│ ├─ 部门设置
│ └─ 岗位设置
├─ 考勤管理
│ ├─ 每日考勤
│ └─ 月度汇总
├─ 工资管理
│ ├─ 工资计算
│ └─ 工资条
└─ 系统管理
├─ 用户与角色
└─ 数据备份
五个一级节点,正好对应「19.1 那张业务动作表」里的五件事——一棵树画完,模块就分完了。
注意「组织管理」是独立的一级节点,不是「员工管理」的子节点。
因为部门、岗位是「配置」,员工是「数据」——配置和数据混在一个模块里,权限就没法分开给。
② 模块和表的关系:谁读谁写
--------------------------------------------------------------------
| 模块 | 主要读 | 主要写 |
|---|---|---|
| 员工档案 | employee/dept/post | employee |
| 调岗离职 | employee/transfer | employee/transfer |
| 部门岗位 | dept/post | dept/post |
| 每日考勤 | employee | attend |
| 工资计算 | employee/attend/salary | salary |
| 用户角色 | user | user |
tb_employee 被四个模块读写——这是全系统最热的一张表,也是并发冲突最容易发生的地方。
tb_salary 只被「工资计算」一个模块写——这种「单写多读」的表最好管,加锁范围小。
③ 系统预览:一次「员工入职」走完整条链
--------------------------------------------------------------------
| 步骤 | 界面 | 数据库 |
|---|---|---|
| 1 | 人事点「新增员工」 | — |
| 2 | 填工号、姓名、选部门岗位 | 下拉框读 dept/post |
| 3 | 点保存,校验工号未重复 | SELECT 1 FROM employee WHERE eno=? |
| 4 | 校验通过,开事务写一行 | INSERT INTO employee |
| 5 | 顺手建账号(可选) | INSERT INTO user |
| 6 | 刷新列表 | SELECT ... FROM v_emp |
第 3 步和第 4 步之间必须是一个事务。
否则两个人同时保存同一个工号,都可能通过校验、都写进去——最后靠 UNIQUE 兜住,
但报错信息就变成「数据库错误」而不是「工号已存在」。
更好的做法:先插、靠 UNIQUE 拦、把「唯一约束冲突」翻译成「工号已存在」。
一次数据库往返,且天然没有竞态窗口。
④ 哪些想法其实不该做成模块
--------------------------------------------------------------------
| 想法 | 为什么先不做 |
|---|---|
| 工资自动报税 | 税率表年年变,做进去就得养一个人 |
| 人脸考勤 | 硬件对接是另一个项目 |
| 组织架构图拖拽 | 能画图不等于能用,先出报表 |
| 员工自助 App | 桌面版跑通再说 |
这一列判断标准是「谁维护」。 一年要改三次以上的规则,就不该硬编进代码。
注意「人事」拿到 7 个二级项,但没有「系统管理」整块。
因为系统管理下面是用户与角色、数据备份——这两件事人事不该碰。
「普通」只拿到 1 个二级项。 看着寒酸,但这是对的——普通员工进系统就是为了查自己的考勤。
给他更多菜单,只会增加误操作和被越权的可能。
这张权限矩阵是「数据」不是「代码」。
加一个功能、调一次授权,改的是表里的行,不是窗体的 if。
这一条决定了系统能不能在第二年还改得动。
30 秒实验
- 给「人事」加上「部门设置」,看菜单怎么变。再想想:这个改动要不要重新发版? 如果权限存在表里,不用;存在代码里,要。
- 把
GRANT里「普通」改成None,看普通员工能看到什么。这就是「权限配置错了」的真实后果。 - 想一想:如果加一个「财务」角色,只能看工资条,要改几行? 一行——加一个 key。这就是角色模型的价值。
常见误区
- 「功能结构图是给领导看的」。 它是给你自己分模块用的。 叶子节点说不清验收标准,那一块就是没想清楚。
- 「配置和数据放一起省菜单」。 省一层菜单,赔掉整套权限。 部门和员工必须分开授权。
- 「先查再插,能防重复」。
查和插之间有时间窗,并发下照样重。
靠
UNIQUE约束拦,再把冲突翻译成人话,才没有窗口。
自测三问(点开对答案)
1. 为什么「组织管理」要单独做一级节点? —— **因为它是配置,不是数据。** **配置一年改两次、影响所有人**, **数据天天改、只影响一个人**——**混在一起就没法分开授权。** 2. 模块 ↔ 表矩阵有什么用? —— **两个用处:** 一是看出哪张表最热(`tb_employee` 被四个模块写); **二是出问题时快速圈定「谁可能改坏了它」。** 3. 入职为什么建议「直接插、靠约束拦」? —— **因为「先查再插」中间有时间窗**,**并发下两个人可能都通过校验。** **约束是原子的,没有窗口。**下一篇 19.3,把这七张表真正建出来:
重点是部门那棵树怎么用 parent 指自己,以及七张表之间那几条外键到底该谁拦谁。
- ☐ (本篇完,下一篇 19.3 数据库设计)