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

Web 的「主窗体」是三块拼起来的:顶栏、左菜单、右内容区,用 iframe 或 include 组装。 菜单还是「算」出来的,但这次算完要写进 HTML,不是往控件里塞。 三个角色的首屏复杂度差十倍,但这不是简配,是按职责给信息。

📝 本章和前面几章的关系

第 19 章的主窗体是一个窗口加一个面板,这一章变成三个页面拼一个框架。 思路一样:菜单按角色算、内容区换页、当前用户带下去。 教材 20.7 分三小节:概述、技术分析、实现过程。 「技术分析」里那句「iframe 加载子页面」是这一节的分水岭—— 它决定了「菜单不重画」,也决定了「后退按钮不能用」。 沙箱没有浏览器,所以页面用「打印 HTML 片段」的方式复刻, 复刻的是「页面怎么拼、参数怎么传」,不是「样式好不好看」。

一、三块拼一个框架

部分 文件 作用
顶栏 top.jsp 系统名 + 当前用户 + 退出
左菜单 left.jsp 按角色生成的菜单树
内容区 main.jsp iframe 或 include 加载功能页
框架页 index.jsp 把上面三块拼起来

iframe 和 include 的区别要分清。 iframe 加载的是「另一个页面」,地址栏不变; include 是「把另一个页面的内容拼进来」,每个功能页都要重新拼一次框架。 教材用的是 iframe,好处是简单、菜单不用重画; 坏处是「后退按钮不能用」,而且对搜索引擎不友好。 现代做法是「单页应用」,但那是另一个话题。

二、菜单按角色算:三份菜单

角色 一级菜单 二级项
管理员 4 9
教师 2 4
学生 1 1

「学生」只剩「成绩查询」一项,而且这一项对三种角色是同一个页面。 同一个页面,三种数据范围——页面不区分角色,WHERE 区分。 「一级节点下没有可显示的项,就整块隐藏」这条规则要写进代码, 否则会留下一个点开是空白的菜单。 空菜单比没有菜单更让人困惑。 还有一个坑:菜单项和页面文件必须一一对应。 少一个映射,菜单点下去就是 404——加功能时最容易漏这个映射表,而且漏了不会在编译期报错。

三、每个页面顶部那五行:校验登录

行 做什么 漏了会怎样
1 取 session 里的 role —
2 role == null 就跳登录页 匿名能进
3 取 uname —
4 页面显示当前用户 用户不知道自己在谁的账号里
5 return(跳转后必须返回) 后面代码继续跑,可能报空指针

第 5 行最容易被忽略:sendRedirect 之后必须 return。 不写 return,后面的代码还会继续执行——轻则报空指针,重则数据已经写进库了才跳转。 「每个页面都写一遍」这件事本身就是隐患。 所以标准做法是写成 Filter, 在 web.xml 里配一句「所有 *.jsp 都过这个过滤器」,全站一次生效。

四、三个首屏各自该有什么

角色 首屏放什么 为什么
管理员 模块入口 + 待办(未录成绩的班) 要一眼看到全貌
教师 我带的班 + 最近录入时间 进来就是为了录分
学生 本学期成绩单 进来就是为了看分

三个首屏,功能复杂度差十倍。 但这不是「简配」,是「按职责给信息」。 给学生的首屏放四个模块入口,他一个都点不了,只会以为自己没权限。 「待办」那一栏是最容易被砍的,也是最有用的。 管理员打开系统,最想知道的是「还有哪些班没录完」,不是「有哪些菜单」。

五、动手 Demo:框架页拆分与三份菜单

这一段做两件事:打印出 index.jsp 的三块结构, 再按角色算三份菜单并渲染成 HTML 片段。 重点看「学生」那份菜单:只有一个链接,但它指向的页面跟管理员是同一个文件。

HTML_TOP = '''<div class="top">成绩管理系统
  <span>当前用户:{uname}({role})</span>
  <a href="logout.jsp">退出</a></div>'''

def render_left(role):
    tree = build(role)
    out = ['<ul class="menu">']
    for top, subs in tree:
        out.append('  <li>' + top)
        out.append('    <ul>')
        for s in subs:
            out.append('      <li><a target="main" href="%s">%s</a></li>'
                       % (PAGE[s], s))
        out.append('    </ul>')
        out.append('  </li>')
    out.append('</ul>')
    return '\n'.join(out)

PAGE = {'班级设置': 'class_set.jsp', '班级学生名单': 'class_list.jsp',
        '成绩录入': 'score_in.jsp', '成绩查询': 'score_query.jsp'}
print(render_left('学生'))

实跑输出:

--------------------------------------------------------------------
主窗体:菜单是权限表的投影
--------------------------------------------------------------------


  ① 布局:一个框架页 + 一个内容页
  --------------------------------------------------------------------
| 部分 | 文件 | 作用 |
|---|---|---|
| 顶栏 | top.jsp | 系统名 + 当前用户 + 退出 |
| 左菜单 | left.jsp | 按角色生成的菜单树 |
| 内容区 | main.jsp | iframe 或 include 加载功能页 |
| 框架页 | index.jsp | 把上面三块拼起来 |

  iframe 和 include 的区别要分清。 iframe 加载的是「另一个页面」,地址栏不变;
  include 是「把另一个页面的内容拼进来」,每个功能页都要重新拼一次框架。
  教材用的是 iframe,好处是简单、菜单不用重画;坏处是「后退按钮不能用」,而且对搜索引擎不友好。
  现代做法是「单页应用」,但那是另一个话题。

  ② 菜单按角色算:三份菜单真跑
  --------------------------------------------------------------------

  [管理员] 一级 4 个,二级 9 个
    班级管理:班级设置、班级学生名单
    学生管理:学生信息维护、批量导入
    成绩管理:成绩录入、成绩修改、成绩查询
    系统管理:用户与角色、数据导出

  [教师] 一级 2 个,二级 4 个
    班级管理:班级学生名单
    成绩管理:成绩录入、成绩修改、成绩查询

  [学生] 一级 1 个,二级 1 个
    成绩管理:成绩查询

  「学生」只剩「成绩查询」一项,而且这一项对三种角色是同一个页面。
  同一个页面,三种数据范围——页面不区分角色,WHERE 区分。
  如果按角色写三个页面,改一个需求要改三处,而且一定会漏。
  「一个页面 + 数据范围」是这一章最省事的做法。

  ③ 页面顶部那一行:从 Session 取,不从 URL 取
  --------------------------------------------------------------------

  [教材原码 · 本沙箱不执行]
  | <%
  |     String role = (String) session.getAttribute("role");
  |     if (role == null) {
  |         response.sendRedirect("login.jsp");   // 没登录,踢回去
  |         return;
  |     }
  |     String uname = (String) session.getAttribute("uname");
  | %>
  | <span>当前用户:<%= uname %>(<%= role %>)</span>
  这五行每个页面都要有;漏一个页面,那个页面就能匿名访问。

  「每个页面都写一遍」这件事本身就是隐患。
  所以标准做法是写成 Filter,在 web.xml 里配一句「所有 /*.jsp 都过这个过滤器」,全站一次生效。
  漏一个页面的后果,比想象中严重。
  比如「删除学生」那个页面忘了校验,任何人敲 URL 都能删——而且日志里看不出来,
  因为请求是「合法」的。

  ④ 三个入口页面各自该有什么
  --------------------------------------------------------------------
| 角色 | 首屏放什么 | 为什么 |
|---|---|---|
| 管理员 | 四个模块的入口 + 待办(未录成绩的班) | 要一眼看到全貌 |
| 教师 | 我带的班 + 最近一次录入时间 | 进来就是为了录分 |
| 学生 | 本学期成绩单 | 进来就是为了看分 |

  三个首屏,功能复杂度差十倍。 但这不是「简配」,是「按职责给信息」。
  给学生的首屏放四个模块入口,他一个都点不了,只会以为自己没权限。

target="main" 这个属性把链接指到 iframe 的 name 上。 这是整个框架能工作的关键一句——少了它,点菜单会在整个窗口里打开新页面,框架就被替换掉了。 PAGE 这张映射表就是「菜单项 → 页面文件」。 加功能时最容易漏的是它,因为漏了之后菜单照样显示,点了没反应。 渲染出来的 HTML 里没有「按角色隐藏按钮」这类东西。 因为「能不能看这个菜单」已经决定了「能不能进这个页面」, 页面内部的按钮权限,是另一层(而且要配合后端校验)。

30 秒实验

  1. 把 target="main" 去掉,想想会出什么事? 框架被整个替换掉,菜单消失。
  2. 给「学生」加上「成绩录入」,看渲染出的 HTML 有什么问题? 菜单有了,但 PAGE 里没有对应文件 → 404。
  3. 想一想:如果菜单有 50 项,PAGE 表要不要手工维护? 该由「菜单表」和「页面表」在数据库里维护,否则每次加功能都要改代码。

常见误区

  1. 「每个页面自己写登录校验就行」。 迟早漏一个。 用 Filter 一次搞定。
  2. 「sendRedirect 之后不用 return」。 代码会继续往下跑,可能已经把数据写进库了。
  3. 「三个角色三套首页没必要,统一一个就行」。 学生进来看四个点不了的入口,只会以为自己没权限。
自测三问(点开对答案) 1. `iframe` 和 `include` 的区别? —— **`iframe` 加载另一个页面,地址栏不变、菜单不用重画**; **`include` 把内容拼进来,每个功能页都要重新拼框架。** 2. `sendRedirect` 之后为什么必须 `return`? —— **否则后面的代码会继续执行**,**可能报空指针,也可能数据已经写进去了。** 3. 为什么一级节点下没内容就要整块隐藏? —— **否则留一个点开是空白的菜单。** **空菜单比没有菜单更让人困惑。**

下一篇 20.8,第一个业务模块: 班级设置——看着最简单,但「删不掉」的三种情况和「改个班名影响谁」这两件事,比想象中绕。

相关笔记