🪟 第20章 Java与SQL Server实现学生成绩管理系统
📄 一句话概括

画结构图先别管角色:先把「系统能做什么」画全,再决定「谁能做什么」。 角色是「视图」,不是「结构」——同一片叶子,三个人看到的数据不一样,但它在结构上只有一个。 「录过了」不是错误,是正常的业务状态,它需要「提示 + 确认」,不是「报错」。

📝 本章和前面几章的关系

20.1 回答「要什么」,20.2 回答「怎么分」。这一节的技术含量不高,但有一个关键判断: 什么时候该写三个页面,什么时候该写一个页面加三个 WHERE。 教材 20.2 分三小节:系统目标、系统功能结构、系统预览。 「系统预览」那一节走了一遍「录成绩」的全流程, 里面有一步是「已经录过了怎么办」——这一步是整章最有业务味的地方。 沙箱没有 Tomcat,所以「页面」用「打印菜单树和列表」的方式复刻, 复刻的是「页面之间怎么跳、数据怎么带」,不是「页面长什么样」。

一、先画一棵全的树,再谈角色

一级节点 下面挂什么
登录 (无子节点)
班级管理 班级设置、班级学生名单
学生管理 学生信息维护、批量导入
成绩管理 成绩录入、成绩修改、成绩查询
系统管理 用户与角色、数据导出

画结构图的时候先别管角色。 先把「系统能做什么」画全,再决定「谁能做什么」—— 两件事混着画,最后一定漏功能(因为你会下意识只画自己关心的那部分)。 角色是「视图」,不是「结构」。 同一个叶子节点,三种角色看到的不一样,但它在结构上只有一个。 这一点想通了,代码就不会写成三套。

二、三种角色,三套菜单,同一个页面

角色 一级菜单 二级项数
管理员 班级/学生/成绩/系统管理 9
教师 班级管理(只看自己班)/成绩管理 4
学生 成绩查询(只有自己) 1

注意「班级管理」教师也有,但只能看自己带的班。 同一个菜单项,两种数据范围——所以范围限制必须做在数据层,不能做在菜单层。 「成绩查询」这个页面,三种角色看到的是同一个文件。 差别只在 WHERE 那一段:管理员不加条件、教师加 class_id、学生加 stu_id。 为什么不写三个页面? 因为改一个需求要改三处,而且一定会漏。 「一个页面 + 数据范围」是这一章最省事的做法。

三、模块和表:谁读谁写

模块 主要读 主要写
班级设置 class/student class
学生信息 student/class student
成绩录入 student/score score
成绩修改 score score
成绩查询 score/student/class —
用户角色 user user

tb_score 被三个模块写(录入、修改、批量导入),这是最容易出「成绩被改乱」的地方。 tb_class 只被一个模块写——这种「单写」的表最好管,也是唯一能安全缓存的表。 成绩表绝对不能缓存:一秒前刚改的成绩必须立刻看到。

四、录成绩的全流程:第四步最关键

步 页面发生什么 数据库发生什么
1 教师选班级、选科目 SELECT ... FROM tb_class
2 列出该班所有学生 SELECT ... WHERE class_id=?
3 逐个填分,点保存 一批 INSERT INTO tb_score
4 有重复(之前录过) UNIQUE 拦住,整批回滚
5 提示「该科已录过,要覆盖吗」 确认后改 UPDATE
6 刷新列表 SELECT ... FROM v_score

第 4 步和第 5 步是这个模块的灵魂。 「录过了」不是错误,是正常的业务状态—— 它需要的是「提示 + 确认」,不是「报错 + 让人自己想办法」。 实现上有两条路: 一是先查一遍「这批录过没」,二是直接 INSERT 让约束拦。 选第二条——第一条在两个人同时录的时候会漏,第二条不会。 拦下来之后再查一次「已经有哪些」,提示就准确了。

五、哪些想法先不做

想法 为什么先不做
自动算排名并导出 Excel 导出要用另一个库,先出页面
成绩趋势折线图 先有数据,再谈可视化
家长端小程序 账号体系是另一套
操作日志审计到字段级 先记「谁改了哪张表」,别一步到位

判断标准还是那一条:谁维护。 一年改三次以上的规则,别硬编进代码。 还有一条更容易忘: 「能不能不做」本身就是一个选项。 导出 Excel 这件事,教务用「复制粘贴到表格」也能凑合半年—— 半年后需求清楚了再做,比现在猜着做强。

六、动手 Demo:功能树、模块矩阵、三套菜单

这一段把功能树和三套菜单各跑一遍,重点看「一级节点怎么跟着消失」。 再对照第三节那张矩阵,看 tb_score 到底被几个模块写。

MENU = [
    ('班级管理', ['班级设置', '班级学生名单']),
    ('学生管理', ['学生信息维护', '批量导入']),
    ('成绩管理', ['成绩录入', '成绩修改', '成绩查询']),
    ('系统管理', ['用户与角色', '数据导出']),
]
GRANT = {
    '管理员': 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))

实跑输出:

--------------------------------------------------------------------
系统设计:一棵树,三套菜单
--------------------------------------------------------------------


  ① 功能结构:不分角色,先画一棵全的
  --------------------------------------------------------------------
  学生成绩管理系统
  ├─ 登录
  ├─ 班级管理
  │   ├─ 班级设置
  │   └─ 班级学生名单
  ├─ 学生管理
  │   ├─ 学生信息维护
  │   └─ 批量导入
  ├─ 成绩管理
  │   ├─ 成绩录入
  │   ├─ 成绩修改
  │   └─ 成绩查询
  └─ 系统管理
      ├─ 用户与角色
      └─ 数据导出
  画结构图的时候先别管角色。
  先把「系统能做什么」画全,再决定「谁能做什么」——两件事混着画,
  最后一定漏功能(因为你会下意识只画自己关心的那部分)。
  角色是「视图」,不是「结构」。 同一个叶子节点,三种角色看到的不一样,但它在结构上只有一个。
  这一点想通了,代码就不会写成三套。

  ② 三种角色,三套菜单
  --------------------------------------------------------------------
| 角色 | 一级菜单 | 二级项数 |
|---|---|---:|
| 管理员 | 班级管理/学生管理/成绩管理/系统管理 | 9 |
| 教师 | 班级管理(只看自己班)/成绩管理 | 4 |
| 学生 | 成绩查询(只有自己) | 1 |

  注意「班级管理」教师也有,但只能看自己带的班。
  同一个菜单项,两种数据范围——所以范围限制必须做在数据层,不能做在菜单层。
  「学生」只有一个菜单项,这是对的。 不是功能少,是职责就只有这一件事。
  反而「管理员」九个二级项,才是真的需要工作台和搜索框的人。

  ③ 模块和表:谁读谁写
  --------------------------------------------------------------------
| 模块 | 主要读 | 主要写 |
|---|---|---|
| 班级设置 | class/student | class |
| 学生信息 | student/class | student |
| 成绩录入 | student/score | score |
| 成绩修改 | score | score |
| 成绩查询 | score/student/class | — |
| 用户角色 | user | user |

  tb_score 被三个模块写(录入、修改、还有后面的批量导入),这是最容易出「成绩被改乱」的地方。
  tb_class 只被一个模块写——这种「单写」的表最好管。
  它也是唯一能安全缓存的一张表(班级不常变),成绩表绝对不能缓存,一秒前刚改的成绩必须立刻看到。

  ④ 系统预览:一次录成绩走完整条链
  --------------------------------------------------------------------
| 步 | 页面发生什么 | 数据库发生什么 |
|---|---|---|
| 1 | 教师选班级、选科目 | SELECT ... FROM tb_class |
| 2 | 列出该班所有学生 | SELECT ... WHERE class_id=? |
| 3 | 逐个填分,点保存 | 一批 INSERT INTO tb_score |
| 4 | 有重复(已录过) | UNIQUE 拦住,整批回滚 |
| 5 | 提示「该科已录过,要覆盖吗」 | 确认后改 UPDATE |
| 6 | 刷新列表 | SELECT ... FROM v_score |

  第 4 步和第 5 步是这个模块的灵魂。
  「录过了」不是错误,是正常的业务状态——它需要的是「提示 + 确认」,不是「报错 + 让人自己想办法」。
  实现上有两条路: 一是先查一遍「这批录过没」,二是直接 INSERT 让约束拦。
  选第二条——第一条在两个人同时录的时候会漏,第二条不会。
  拦下来之后再查一次「已经有哪些」,提示就准确了。

  ⑤ 哪些想法先不做
  --------------------------------------------------------------------
| 想法 | 为什么先不做 |
|---|---|
| 自动算班级排名并导出 Excel | 导出是另一个库的活,先出页面 |
| 成绩趋势折线图 | 先有数据,再谈可视化 |
| 家长端小程序 | 账号体系是另一套 |
| 操作日志审计到字段级 | 先记「谁改了什么表」,别一步到位 |

  判断标准还是那一条:谁维护。 一年改三次以上的规则,别硬编进代码。

「学生」只剩「成绩查询」一项,而这一项对三种角色是同一个页面。 页面不区分角色,WHERE 区分——这就是「一个页面 + 数据范围」的全部含义。 如果按角色写三个页面,改一个「排名规则」要改三处。 改三处的东西,第三处一定会忘。 注意「班级学生名单」给了教师,但「班级设置」没给。 这是刻意的:教师要看名单,但不能改班级本身。 同一个模块下,读和写的权限可以分开给。

30 秒实验

  1. 给教师加上「批量导入」,看菜单怎么变。再想想:教师能不能批量导入成绩? 能,但要限制在自己班。 所以「加菜单」和「加数据范围」是两件事。
  2. 把 GRANT['学生'] 改成 ['成绩查询', '成绩录入'],看会出什么事。 学生能进录入页,但录谁的分? 这就是「有菜单没数据」的荒谬状态。
  3. 想一想:如果学校要求「班主任只能看自己班,但年级组长能看整个年级」,怎么表达? 加一个角色「年级组长」+ 一个「管辖年级」字段,WHERE 从 class_id=? 变成 grade=?。

常见误区

  1. 「按角色写三套页面更清楚」。 清楚一时,改起来三倍。 一个页面 + 数据范围才是正解。
  2. 「录过了就直接报错」。 这是正常业务状态,不是错误。 要给「覆盖吗」的选择。
  3. 「功能越多越好」。 先做「不做也能凑合」的判断。 半年后需求清楚了再做,比现在猜着做强。
自测三问(点开对答案) 1. 为什么先画完整的树,再分配角色? —— **混着画会漏功能**——**因为你会下意识只画自己关心的那部分。** **角色是视图,不是结构。** 2. 为什么「成绩查询」只写一个页面? —— **三种角色的差别只在 `WHERE`**——**写三个页面,改需求要改三处,一定会漏。** 3. 「录过了」为什么不该只报错? —— **因为它是正常业务状态。** **用户需要的是「覆盖吗」的选择**,**不是「失败了,自己想」。**

下一篇 20.3,把三张表建出来: 重点是成绩表那个「三列联合唯一」——它是挡住「录两次」的唯一防线。

相关笔记