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

人事系统的三条主线是「组织、人员、薪酬」——它们不是并列的,是一层压一层。 制度文件里那些「不许」「必须」「只能」,一句句都是数据库约束。 需求分析真正的产出不是功能清单,是两张表:业务动作对到哪张表、制度原话对到哪条约束。

📝 本章和前面几章的关系

第 18 章做的是酒店:一间房、一位客、一张单,一天几十次高频小事务。 这一章做企业人事:人少、变动慢,但每一行数据都要能翻三年账。 教材 19.1 分三小节:业务背景、功能需求、权限需求。 「权限需求」这一节看着最不起眼,其实是这类系统最容易翻车的地方—— 工资表能不能互相看,决定这个系统上线第一天会不会吵架。 沙箱没有 C#、没有 WinForms、没有 SQL Server,做法照旧: 数据库层用 SQLite 真跑,界面层用「菜单树 + 表格输出」复刻数据流, C# 原码照贴标不执行——复刻的是「数据怎么流」,不是「界面怎么画」。

一、三条主线,其实是三层

主线 管什么 变了会怎样
组织 部门、岗位、职级 牵动所有人的人事归属
人员 员工档案、调岗、离职 牵动考勤和工资
薪酬 考勤汇总、工资核算 只影响当期,不动历史

这三条不是并列关系,是「组织决定人员挂在哪,人员决定薪酬算给谁」。 所以建模顺序也必须是这个顺序——先有部门表,才能有员工表的 dept_id。 反过来先建工资表,就会发现它谁都不认识。 新手最容易犯的错就是「从最想做的功能开始建表」—— 想先做工资计算,于是先建 tb_salary,然后发现连人是谁都填不进去。

二、业务动作对到表:需求分析的硬产出

业务动作 落到数据上
新人入职,填一张表 tb_employee 插一行
部门调整、换岗 改 dept_id/post_id,另留一条历史
每天记考勤 tb_attend 每天每人一行
月底算工资 tb_salary 每人每月一行
发账号、收账号 tb_user 增删改

这五条几乎覆盖了人事系统的全部。 剩下的(花名册、合同到期提醒、 离职面谈)都是这五条的组合或变体。 这张表最省力的用法:把「业务动作」一列先写满,再逐个问「它动到哪张表」。 能落到表上的是需求;落不下去的,是流程或者制度,不该写进系统。 举个落不下去的例子: 「部门之间要互相配合」。 这句话没有对应的表,它是一句管理口号——硬要做成「协作关系表」,最后一定没人填。

三、制度条款就是约束:一条一条对过去

制度原话 落到哪
每人一个工号,不许重 eno UNIQUE
性别只有男和女 CHECK (sex IN ('男','女'))
入职日期不能空 hire NOT NULL
岗位职级 1 到 10 级 CHECK (level BETWEEN 1 AND 10)
一人一天只能一条考勤 UNIQUE (emp_id, adate)
有在职员工的部门不能删 外键默认拦(RESTRICT)
离职不删人,改状态 state 字段 + 不写 DELETE

这张表比功能清单有用。 功能清单告诉你「做什么」,这张表告诉你「什么错不能犯」。 而错是不能犯的,功能是可以后加的。 最后一条要单独说:「离职不删人」不是技术选择,是业务选择。 删了行,这个人去年的工资单、考勤记录就全对不上了。 所以 state 这个字段从第一天就得有——后加的话,历史数据已经删没了,补不回来。

四、权限需求:三种人三种视野

角色 能看 不能看
管理员 全部 —
人事 员工、考勤、工资 系统用户与角色
普通 只看自己的考勤和工资 别人的一切

「普通」这一行是最容易被砍掉的,也是最容易出事的。 如果所有人的工资表都能点开,这个系统上线第一天就会吵架。 这不是危言耸听——工资是最敏感的一张表,敏感程度超过身份证号。 权限要做成「角色」而不是「人」。 八个员工逐个配权限,加一个人就得再配一遍;配三个角色,加人只是选一个角色。 还有一个坑: 「普通」只能看自己的,那「自己」怎么判断? 答案在 tb_user.emp_id 上——账号必须能对上员工,对不上就只能靠姓名拼音硬猜,一定出错。

五、建出来看看:七张表各自几行

表 一行代表 这一版的初始行数
tb_dept 一个部门 7(三层树)
tb_post 一个岗位 6(带职级与薪酬档)
tb_employee 一个人 8
tb_transfer 一次调动 0
tb_attend 某人某天 0
tb_salary 某人某月 0
tb_user 一个能登进来的人 0

比第 18 章多了两类表: 一类是历史表(调岗、考勤),一类是按月存结果的表(工资)。 这不是拍脑袋加的——是被「要能翻旧账」这个需求逼出来的。 「一行代表」这一列值得盯着看三遍。 tb_attend 一行是「某人某天」, 所以主键该是 (emp_id, adate) 联合唯一—— 不是「一条考勤记录」,因为那样说不出哪两条算重复。

六、动手 Demo:从业务动作抠出七张表

下面这段代码只做一件事:把 19.1 那两张表落成一个真的库,然后数一数每张表几行。 重点看第三节那四处「试撞」——制度原话变成约束之后,真的能拦住错数据。

import sqlite3

DDL = u'''
CREATE TABLE tb_dept (
  id     INTEGER PRIMARY KEY AUTOINCREMENT,
  dname  TEXT    NOT NULL UNIQUE,
  parent INTEGER REFERENCES tb_dept(id),   -- 自引用,成树
  mgr    TEXT
);
CREATE TABLE tb_employee (
  id      INTEGER PRIMARY KEY AUTOINCREMENT,
  eno     TEXT    NOT NULL UNIQUE,
  ename   TEXT    NOT NULL,
  sex     TEXT    NOT NULL CHECK (sex IN ('男', '女')),
  dept_id INTEGER NOT NULL REFERENCES tb_dept(id),
  post_id INTEGER NOT NULL REFERENCES tb_post(id),
  hire    TEXT    NOT NULL,
  state   TEXT    NOT NULL DEFAULT '在职'
                  CHECK (state IN ('在职', '离职'))
);
'''

c = sqlite3.connect(':memory:', isolation_level=None)
c.execute('PRAGMA foreign_keys = ON')   # SQLite 默认是关的
c.executescript(DDL)

for t in ('tb_dept', 'tb_post', 'tb_employee',
          'tb_transfer', 'tb_attend', 'tb_salary', 'tb_user'):
    n = c.execute('SELECT COUNT(*) FROM ' + t).fetchone()[0]
    print(t, n)

实跑输出:

--------------------------------------------------------------------
需求分析:制度文件里全是规则
--------------------------------------------------------------------


  ① 业务背景:人事部一天在干什么
  --------------------------------------------------------------------
| 业务动作 | 落到数据上是什么 |
|---|---|
| 新人入职,填一张表 | tb_employee 插一行 |
| 部门调整,换岗 | 改 dept_id/post_id,另留一条历史 |
| 每天记考勤 | tb_attend 每天每人一行 |
| 月底算工资 | tb_salary 每人每月一行 |
| 发账号、收账号 | tb_user 增删改 |

  这五条几乎覆盖了人事系统的全部。剩下的(花名册、合同到期提醒、离职面谈)都是这五条的组合或变体。
  需求分析最省力的做法:把「业务动作」一列写满,再问每个动作动到哪张表。
  能落到表上的是需求,落不下去的是流程或制度,不该写进系统。

  ② 制度条款其实都是约束
  --------------------------------------------------------------------
| 制度原话 | 落到哪 |
|---|---|
| 每人一个工号,不许重 | eno UNIQUE |
| 性别只有男和女 | CHECK (sex IN (...)) |
| 入职日期不能空 | hire NOT NULL |
| 岗位职级 1 到 10 级 | CHECK (level BETWEEN 1 AND 10) |
| 一个人一天只能有一条考勤 | UNIQUE (emp_id, adate) |
| 有在职员工的部门不能删 | 外键默认拦(RESTRICT) |
| 离职不删人,改状态 | state 字段 + 不写 DELETE |

  「制度原话 → 落到哪」这张表是需求分析的核心产出。
  它比功能清单有用——功能清单告诉你做什么,这张表告诉你什么错不能犯。
  最后一条要单独说: 「离职不删人」不是技术选择,是业务选择。
  删了行,这个人去年的工资单、考勤记录就全对不上了。
  所以 state 这个字段是必须的,而且从第一天就得有。

  ③ 权限需求:三种人三种视野
  --------------------------------------------------------------------
| 角色 | 能看 | 不能看 |
|---|---|---|
| 管理员 | 全部 | — |
| 人事 | 员工、考勤、工资 | 系统用户与角色 |
| 普通 | 只看自己的考勤和工资 | 别人的一切 |

  「普通」这一行是最容易被砍掉的,也是最容易出事的。
  如果所有人的工资表都能点开,这个系统上线第一天就会吵架。
  权限做成「角色」而不是「人」——八个员工逐个配权限,加一个人就得再配一遍;
  配三个角色,加人只是选角色。

  ④ 建出来看看:七张表几行数据
  --------------------------------------------------------------------

| 表 | 行数 | 说明 |
|---|---:|---|
| tb_dept | 7 | 部门树,含父子 |
| tb_post | 6 | 岗位与薪酬档 |
| tb_employee | 8 | 在职员工 |
| tb_transfer | 0 | 调岗历史,还没有 |
| tb_attend | 0 | 考勤,还没有 |
| tb_salary | 0 | 工资,还没到月底 |
| tb_user | 0 | 账号,等 19.5 建 |

  比第 18 章多了两张「历史表」(调岗、考勤),还有一张「按月存结果」的表(工资)。
  这不是拍脑袋加的——是被「要能翻旧账」这个需求逼出来的。

三张 0 行的表是故意的。 调岗、考勤、工资都还没发生——建库阶段就要能接受空表, 别为了好看硬塞假数据进业务表。 样例数据的作用是「验证设计」:八个员工、六个岗位、七层部门试一遍, 立刻能看出「一个人能不能兼两个岗」这类漏洞——光看表结构看不出来。 「一个人兼两个岗」这一版是做不到的(post_id 只有一个),这是刻意的简化。 真要做,得加一张 tb_emp_post 关联表——但那会让工资计算复杂一倍,先把主干跑通。

30 秒实验

  1. 把 sex 的 CHECK 临时去掉,插一条 '未知',看能不能进库。 再把它加回来——体会一下「约束是改一次就永久生效」这件事。
  2. 试插一个 dept_id = 99 的员工。如果没报错,说明 PRAGMA foreign_keys 没打开。 这是 SQLite 最常见的坑,SQL Server 里不存在。
  3. 想一想:如果业务加一句「一个人可以兼多个岗」,要改几张表、几个模块? 一张关联表 + 工资计算模块——这就是「需求变更成本」,需求阶段就该估出来。

常见误区

  1. 「需求分析就是列功能清单」。 清单只能告诉你要做什么,不能告诉你什么错不能犯。 真正值钱的是「制度原话 → 约束」那张表。
  2. 「先建最想做的功能需要的表」。 建表要按依赖顺序:组织 → 人员 → 薪酬。 倒着建,建到一半就会发现外键指不到人。
  3. 「权限是界面上隐藏几个按钮」。 桌面程序里界面拦不住任何人——用户拿 SSMS 直连数据库,菜单一行都不管用。 真正的权限在数据库账号的 GRANT 上。
自测三问(点开对答案) 1. 人事系统的三条主线是哪三条,为什么有先后? —— **组织、人员、薪酬。** **组织决定人挂在哪,人员决定薪酬算给谁**—— **建表顺序必须照这个来,否则外键指不到。** 2. 「离职不删人」为什么要从第一天就做成字段? —— **因为删了行,历史工资单和考勤记录就全对不上了。** **`state` 字段后加时,历史数据已经没了,补不回来。** 3. 权限为什么要做成角色而不是人? —— **配一次管一批,加人只是选角色。** **逐个配的话,人员一流动就得重配,迟早漏。**

下一篇 19.2,把上面那五个业务动作画成一棵功能树: 画完你会发现,「组织管理」必须单独当一级节点,不能塞进「员工管理」里。

相关笔记