简介:这份面向在线教育平台建设者、系统分析师与课程设计人员的需求分析说明书,围绕学校在线教育网的业务目标,梳理学员管理、网上教学、校园信息、在线考试等模块的功能边界与角色权限,可作为项目立项、开发评审和教学案例的参考。压缩包内共1个doc文件,约2.21MB,集中呈现项目背景、用户环境、UML与E-R建模要求、功能清单及用例说明,便于按章节查阅。页面显示已有1588人学习,说明其在同类文档中具备一定参考热度。读者可从中获取B/S架构下的权限管理、班级与科目维护、教学资料上传下载、题库组卷、自动与人工改卷、成绩查询等需求描述,并借助用例图和参与者说明理解管理员、教师、学员的交互关系。文档结构完整,适合用于需求梳理、功能拆解、文档模板借鉴以及毕业设计或课程项目的早期分析。
1. 一份 1.0.0 版的需求分析说明书,值不值得逐字读
翻到文件头,版本栏写着 1.0.0,状态勾在“正在修改”而不是“正式发布”。这个细节很关键:需求分析说明书这份东西,多数团队拿到手只当存档附件,扫一眼功能清单就去画表建库,最后返工的时候才发现验收标准根本没对齐。这份文档面向 B/S 架构的在线教育网,把学员管理、网上教学、在线考试三块业务连同权限模型一起摊开写了,还配了功能编号、用例清单和参与者说明。它适合三类人:要接手历史教育类项目、得先把业务边界摸清的人;需要给存量系统补需求基线的人;以及要拿它当课程设计或方案附件、必须能自圆其说的人。真正的价值不在图,在那套能逐条对上的功能编号。
2. 文档骨架拆解:三张功能清单怎么变成可验收的需求基线
需求分析说明书最常见的问题,是图好看、清单齐全、但没法验收。功能写“教学资料管理”,开发理解成上传下载,测试理解成能改能删,验收时谁也说服不了谁。要把这份文档用起来,先得把它从“描述性文字”翻译成“可判定条目”,翻译的抓手就是功能编号和用例描述里的定语。
2.1 功能编号才是这份文档里最值钱的部分
文档里三块业务各自带了一套编号:界面权限部分用 01 到 04,下挂 021、022、023、031 至 034、041、042;网上教学部分用 001 到 007,其中 003 再挂 00301;在线考试部分用 01 到 05,下挂 0101、0102、0201、0202、0401、0501、05402。这套编号看起来像个形式主义的表格,实际上是整份文档里唯一具备唯一性的东西。
为什么强调唯一性?因为需求变更、测试用例、缺陷单、上线核对单,都需要一个共同的键去关联。用功能名称当键,就会出现“教学资料管理”和“教学资源管理”到底是不是同一个功能的扯皮;用编号当键,只要编号没重复,关联就是确定的。我一般会做的第一件事,是把这三套编号原样抄进一张 Excel,一行一个编号,列上功能名称、参与者、前置条件、验收方式,然后强制要求后续所有产出物都必须带这个编号。
抄的过程中会暴露问题。这份文档的在线考试部分,0401 这个号出现了两次:一次给“系统改卷”,一次给“教师改卷”;05 下面又写成 0501“教师查分”和 05402“学员查分”,后者明显是 0502 敲错了多打一位。这种编号错位在真实文档里非常常见,因为写文档的人是在 Word 里手打的。发现之后不要默默改掉,而是建一张编号映射表,记录“原编号 → 修正编号 → 修正依据”,把它作为需求变更记录的一部分。否则半年后有人拿着旧版文档来对进度,又要重新解释一遍。
编号还有一层用途:粒度校验。比如 02 权限设置下面挂了 021“角色对模块的权限”和 022“功能模块管理”、023“角色的管理”,但 03 角色管理下面又挂了 034“权限设置管理”。同一个东西在树里出现了两次。这说明撰写者在“角色管权限”和“权限配角色”这两个方向上没想清楚,落地时必须二选一,通常是保留“角色 → 模块 → 操作”的单向授权,把 034 归并进 02 域,否则代码里会出现两个入口改同一张表。
2.2 用例描述里的参与者、前置条件与扩展关系
文档的用例说明部分采用了一种很典型的格式:用例图 + A 用例图 + B 描述 + C 参与者。真正能拿来干活的是 B 和 C 两段,尤其是描述里那些带条件的句子。
举几个例子。“可以浏览本年级以及以下年级的教学内容(教师或管理员不受限制)”——这一句里藏了三条规则:学员的可见范围是grade_level <= 学员年级,教师和管理员不受限,可见性的判断依据是年级而不是班级。如果不把这句话拆出来,开发很可能写成“只能看自己班的”,测试按“能看所有年级”去验,两边都不算错,但都对不上需求。
再看“学员可以针对所浏览章节的教学内容进行提问。相应:用例 007:疑问进行解答,教师针对疑问进行解答”。这里“所浏览章节”是个强约束:提问必须绑定到具体章节,而不是绑定到课程。落地时问题表里必须有chapter_id而不是只有course_id,并且要在提问接口里校验该学员确实有权访问这个章节——否则会出现越权提问,反过来又把不该让学员看到的章节标题泄露出去。
扩展关系那几行值得单独拎出来看。文档写“02.2 权限设置管理扩展了功能模块的管理”“02.3 权限设置管理扩展了角色的管理”“033 角色的修改和删除扩展了角色的创建”。按 UML 的语义,扩展(extend)表示在特定条件下插入的附加行为,被扩展的用例是基础用例,扩展用例依赖它。但这里的表述方向很含糊:到底是“管权限的时候顺带管了模块”,还是“管模块的时候触发了权限配置”?两种理解对应两套完全不同的界面组织方式。
我的处理原则是:图形只作参考,文字描述为准,两者冲突就补一段自然语言把它写死。具体做法是在编号表里加一列“行为方向”,把每个含糊的扩展关系翻译成一句主谓宾,比如“管理员在角色详情页为某模块勾选操作项时,写入角色-模块-操作授权记录”,一句话就把交互入口、操作对象、落库位置全定死了。这条句子写不出来,说明需求本身没想清楚,该去问而不是该去猜。
2.3 把“备注”栏和空白处当风险登记表读
需求文档里最有信息量的往往不是写了的,是没写的。这份文档写清了“在线考试包含手动选题和系统选题”,但没写一道题被抽中后能不能重复抽、抽不满配额怎么办、考试中途断线能不能续考、交卷并发两个请求会不会生成两份成绩。这些空白不是撰写者偷懒,是那个年代的文档习惯——默认这些是“开发自己会处理的技术细节”。
这些空白必须在开工前变成显式的风险条目,否则后期全是返工。我一般按下面这张表把空白项过一遍,每一条都要求给出处置结论,而不是留空:
| 文档原文/空白处 | 隐含约束 | 需要补齐的判定 | 验收方式 |
|---|---|---|---|
| 系统选题 | 按题型配额随机抽题 | 同卷内是否允许重复题、配额不足时的降级策略 | 构造题库题量不足的科目,检查是否报错或降配 |
| 学员浏览本年级及以下 | 数据可见范围按年级 | 留级/跳级学员的年级取值来源 | 造一个跨年级账号,验证列表与详情一致 |
| 教学资料上传下载 | 含 FLASH、视频、课件 | 单文件大小上限、允许的扩展名白名单 | 上传超限文件与非法后缀,验证拒绝行为 |
| 教师改卷 / 系统改卷 | 客观题自动、主观题人工 | 教师改分是否覆盖系统分、覆盖是否留痕 | 同一份答卷先自动后人工,检查分值与操作日志 |
| 图文并茂的信息管理 | 富文本含图片 | 图片存储位置、正文 XSS 过滤 | 提交带脚本的富文本,检查落库与渲染 |
| 会员制与权限 | 部分功能需相应权限 | 未登录用户的可见范围边界 | 匿名访问各栏目,逐一核对返回码 |
把这张表填满,需求基线才算立住。它的作用是让“我以为”变成“文档规定”,让争议在写代码之前就发生完。
3. 权限模型落地:角色、模块、操作三层怎么建表
界面权限这块是整份文档里描述最细的,也是最容易做歪的。文档给了四个功能域:模块功能管理、权限设置、角色管理、栏目管理。落到代码上,如果真按这四个域建四张业务表,很快就会卡住——因为“谁能看哪个栏目”和“谁能改哪个模块”本质上是同一件事,只是对象不同。
3.1 从四个功能域反推 RBAC 三层结构
先看文档的原始表述:“对部分功能实行会员制,有相应权限才能给予相应功能”“角色对模块的权限”“角色的创建、修改以及删除”。这三句话指向的是一个标准的 RBAC 模型,只是当年没有把这个词写出来。
抽象出来是三层:主体(用户/会员)、角色(管理员、教师、学员、栏目编辑)、资源与操作(模块 + 动作,栏目也作为一类资源)。用户不直接挂权限,用户挂角色,角色挂“资源-操作”对。这样做的直接好处是文档里那句“角色的修改和删除”变得可实现——改角色就是改授权关系,不需要动任何一个用户的记录。
还有一个容易被忽略的点:文档把“栏目管理”拆成了 041 外观样式设置和 042 栏目内容动态设置。这说明栏目本身是有属性的资源,样式和内容要分开授权。常见做法是给资源表加一个resource_type字段,取值为MODULE(功能模块)或COLUMN(内容栏目),授权表统一,前端按类型渲染不同的配置界面。这样 01 到 04 四个域最终收敛成两张表加一张关联表,复杂度大幅下降。
3.2 建表与初始化:一份能直接跑的 SQL
下面这套 DDL 是按文档的功能域反推出来的最小可用结构,去掉业务字段,只留权限骨架。字段命名刻意保留了module和column的区分,方便和文档里的 04 栏目管理对上号。
-- 角色表:对应文档 03 角色管理(031 创建 / 032 修改 / 033 删除) CREATE TABLE sys_role ( id NUMBER(10) PRIMARY KEY, role_code VARCHAR2(32) NOT NULL, -- 角色编码,程序内唯一,如 ADMIN / TEACHER / STUDENT role_name VARCHAR2(64) NOT NULL, -- 展示名,允许重名 role_type NUMBER(1) DEFAULT 0, -- 0 系统内置不可删,1 自定义可删 status NUMBER(1) DEFAULT 1, -- 1 启用,0 停用 created_at DATE DEFAULT SYSDATE, CONSTRAINT uk_role_code UNIQUE (role_code) ); -- 资源表:模块与栏目共用,对应 01 模块功能管理 + 04 栏目管理 CREATE TABLE sys_resource ( id NUMBER(10) PRIMARY KEY, resource_code VARCHAR2(64) NOT NULL, -- 与需求文档的功能编号对应,如 "003"、"041" resource_name VARCHAR2(128) NOT NULL, resource_type VARCHAR2(16) NOT NULL, -- MODULE 功能模块 / COLUMN 内容栏目 parent_id NUMBER(10) DEFAULT 0,-- 0 表示顶级,支撑文档里的树形结构 url VARCHAR2(256), -- 模块对应的入口地址,栏目为空 sort_no NUMBER(6) DEFAULT 0, CONSTRAINT uk_resource_code UNIQUE (resource_code) ); -- 操作表:把“权限”从模块级下沉到动作级 CREATE TABLE sys_action ( id NUMBER(10) PRIMARY KEY, action_code VARCHAR2(32) NOT NULL, -- VIEW / CREATE / UPDATE / DELETE / AUDIT / GRADE action_name VARCHAR2(64) NOT NULL, CONSTRAINT uk_action_code UNIQUE (action_code) ); -- 授权表:角色 × 资源 × 操作,对应 021 角色对模块的权限 CREATE TABLE sys_role_grant ( id NUMBER(10) PRIMARY KEY, role_id NUMBER(10) NOT NULL, resource_id NUMBER(10) NOT NULL, action_id NUMBER(10) NOT NULL, data_scope VARCHAR2(16) DEFAULT 'SELF', -- SELF 本人 / GRADE 本年级及以下 / ALL 全部 created_at DATE DEFAULT SYSDATE, CONSTRAINT uk_grant UNIQUE (role_id, resource_id, action_id), CONSTRAINT fk_grant_role FOREIGN KEY (role_id) REFERENCES sys_role(id) ); CREATE INDEX idx_grant_role ON sys_role_grant(role_id); CREATE INDEX idx_resource_parent ON sys_resource(parent_id);几个参数要单独说明。resource_code直接复用文档里的功能编号字符串,是为了让代码、测试用例、需求文档三者能靠一个字符串对齐,排查越权时不用来回翻表。parent_id用 0 表示顶级而不是 NULL,是为了避开 Oracle 里NULL参与等值比较恒为假的坑,取子树时用START WITH parent_id = 0 CONNECT BY PRIOR id = parent_id就够了。data_scope这个字段文档里没写,但它对应的是“学员浏览本年级及以下”那句需求——权限不止管“能不能进”,还要管“进去能看到几行”,这两件事必须分开表达,混在模块权限里迟早出问题。
初始化数据把文档里的四类参与者落进去,管理员和学员内置不可删,其余可增删:
INSERT INTO sys_role (id, role_code, role_name, role_type) VALUES (1, 'ADMIN', '管理员', 0); INSERT INTO sys_role (id, role_code, role_name, role_type) VALUES (2, 'TEACHER', '教师', 1); INSERT INTO sys_role (id, role_code, role_name, role_type) VALUES (3, 'STUDENT', '学员', 1); INSERT INTO sys_role (id, role_code, role_name, role_type) VALUES (4, 'COLUMN_EDITOR', '栏目编辑', 1); -- 资源按文档功能清单录入,编号原样照抄 INSERT INTO sys_resource (id, resource_code, resource_name, resource_type, parent_id, url) VALUES (100, '003', '教学资料管理', 'MODULE', 0, '/teach/material'); INSERT INTO sys_resource (id, resource_code, resource_name, resource_type, parent_id, url) VALUES (101, '00301', '教学进度管理', 'MODULE', 100, '/teach/progress'); INSERT INTO sys_resource (id, resource_code, resource_name, resource_type, parent_id, url) VALUES (200, '041', '外观样式设置', 'COLUMN', 0, NULL); INSERT INTO sys_resource (id, resource_code, resource_name, resource_type, parent_id, url) VALUES (201, '042', '栏目内容动态设置', 'COLUMN', 0, NULL); INSERT INTO sys_action (id, action_code, action_name) VALUES (1, 'VIEW', '查看'); INSERT INTO sys_action (id, action_code, action_name) VALUES (2, 'CREATE', '新增'); INSERT INTO sys_action (id, action_code, action_name) VALUES (3, 'UPDATE', '修改'); INSERT INTO sys_action (id, action_code, action_name) VALUES (4, 'DELETE', '删除');注意:
sys_action是全局字典,不要给每个模块单独建一套动作。文档里 031 到 033 写的是角色的创建、修改、删除,041 和 042 写的是栏目的样式与内容设置,动作集合其实是重合的,抽成字典能省掉大量重复配置。
3.3 校验顺序与越权排查
权限校验最常见的翻车方式不是“没校验”,是“校验顺序不对”。一个请求进来,先查角色、再查资源、再查操作、最后查数据范围,四步任何一步跳过都会漏。我习惯把它固化成一条链路:登录态 → 角色是否启用 → 资源是否存在且启用 → 该角色是否被授予该资源的该动作 → 数据范围过滤。前四步是布尔判断,第五步是拼查询条件,两者不要混在同一个方法里,否则日志里看不出到底是“没权限”还是“查不到数据”。
排查越权时,别去看代码,去看数据。一句 SQL 就能定位大部分问题:
-- 查某角色对某资源的全部授权,data_scope 异常基本都在这里暴露 SELECT r.role_code, res.resource_code, res.resource_type, a.action_code, g.data_scope FROM sys_role_grant g JOIN sys_role r ON r.id = g.role_id JOIN sys_resource res ON res.id = g.resource_id JOIN sys_action a ON a.id = g.action_id WHERE r.role_code = 'STUDENT' ORDER BY res.resource_code, a.action_code;跑出来如果发现学员对003教学资料管理有UPDATE,或者data_scope是ALL,那问题就不在代码在配置。另一种高频情况是角色被停用(status = 0)但授权记录还在,登录时没校验角色状态,导致停用形同虚设——所以校验链路里的第二步“角色是否启用”不能省,这不是冗余,是后悔药。
4. 网上教学与在线考试:用例转状态机与判分规则
文档里网上教学有 7 个用例、在线考试有 5 组功能,看起来是两块独立业务,实际在数据上是连着的:教学资料挂在章节上,考题挂在科目上,而章节和科目都属于同一个班级体系。把它们割裂去做,最后会出现“考试考了还没教的内容”这种业务上说不通、技术上却拦不住的情况。
4.1 教学资料与教学进度的绑定关系
用例 003 写的是“教学资料管理”,00301 写的是“教学进度管理”,描述里那句“根据教学进度和节次的需求,科目发布教学资料”是整块业务的关键。它意味着资料不是平铺在科目下的,而是挂在“进度节点”上。
落地时我一般建三层:科目 → 进度节点(第几周/第几节)→ 资料。资料表里带progress_id,进度表里带subject_id、chapter_no、publish_at。publish_at这个字段是文档没写但必须有,因为“教学进度”天然带时间属性,资料提前上传但未到上课时间不应该对学员可见。少这个字段,就只能靠人工记着哪天开哪份资料,运营三个月准乱。
资料的可见范围还要叠加上一章说的data_scope:学员只能看chapter_no小于等于自己当前进度的节点,教师和管理员不受限。这条规则落到查询里是一个AND条件,但它的来源是需求文档那句括注,测试用例必须覆盖两个方向——本年级能看到、高年级看不到,只测一边等于没测。
提示:资料类型文档明确列了 FLASH、视频、课件三类,扩展名白名单要按类型分别配置,不要做成一个统一列表。FLASH 在今天的浏览器里基本不可用,如果这个系统要迁移,这一类需要单独标记为“历史资源只读”,而不是直接删掉——老课件里往往还有唯一一份内容。
4.2 试卷生成:手动选题和系统选题是两套逻辑
文档把 0201 手动选题和 0202 系统选题并列为两个功能,这个划分是对的,但只对了一半。它们的区别不只是“谁来挑题”,更在于约束的来源不同:手动选题由教师决定内容和顺序,系统选题由题型配额和难度分布决定。前者要支持拖拽排序和替换,后者要保证同一份卷子里不出现重复题。
系统选题在 Oracle 9i 上的常见写法是下面这种三层嵌套,外层控行数、内层随机排序:
-- 按题型配额随机抽题:内层打乱顺序,外层用 ROWNUM 截断 SELECT id, type_code, difficulty, score FROM ( SELECT q.id, q.type_code, q.difficulty, q.score FROM exam_question q WHERE q.subject_id = :subjectId -- 科目,对应班级下的科目创建 AND q.type_code = :typeCode -- 题型,单选/多选/判断/简答 AND q.status = 1 -- 只抽启用状态的题 ORDER BY DBMS_RANDOM.VALUE ) WHERE ROWNUM <= :needCount; -- 该题型的配额subjectId和typeCode是绑定变量,needCount是配额。这里有个血泪经验:ORDER BY DBMS_RANDOM.VALUE在数据量大时会全表排序,题库上万条就会有明显延迟,实际项目里通常改成先按type_code建索引再抽,或者维护一张“可用题目 ID 池”按段随机。另外这段 SQL 本身不保证跨题型不重复,因为一次只抽一个题型;如果业务上要求整卷不重复,就得在应用层维护已抽中 ID 的集合,逐题型传入AND q.id NOT IN (...),或者干脆一次性按题型分组用分析函数同时出卷。
手动选题的逻辑反过来:先按题型和难度筛出候选池给教师看,教师勾选后写入试卷明细表,顺序按sort_no排。这里必须加一个约束——同一份试卷内question_id唯一,用唯一索引兜底,不要指望前端不让人重复勾。
4.3 改卷冲突:系统改卷与教师改卷谁覆盖谁
文档把 0401 系统改卷和 0401 教师改卷(编号重复那个)并列,功能说明里只写了“教师根据题目类型手工生成试卷”“学员进行登录进行考试”,对改卷的先后关系和覆盖规则一个字没提。这是整份文档里最容易埋雷的地方。
实际业务里一定是这个顺序:学员交卷 → 客观题由系统即时判分 → 主观题进入待改队列 → 教师逐题给分 → 合成总分 → 学员查分。问题出在“教师能不能改客观题的分数”。有的学校要求教师可以复核调整,有的要求客观题锁死。这个必须做成配置项而不是硬编码,我一般给答卷明细表加一个grade_status字段,取值AUTO_LOCKED(系统判分锁定)、AUTO_EDITABLE(系统判分可改)、MANUAL(人工已改)、FINAL(归档不可动),再用带条件的更新挡并发:
-- 教师改分:带版本号和状态双条件,防并发覆盖与非法修改 UPDATE exam_answer SET score = :score, grade_by = :teacherId, grade_status = 'MANUAL', version = version + 1 WHERE id = :answerId AND version = :version -- 乐观锁,前端提交时携带 AND grade_status IN ('AUTO_EDITABLE', 'MANUAL', 'AUTO_LOCKED'); -- 执行后必须检查影响行数,为 0 说明被别人改过或状态不允许version是乐观锁,grade_status的白名单决定这次修改合不合法。影响行数为 0 时不要静默成功,要返回明确的冲突提示,让前端刷新后重试。这套机制挡的是两种真实事故:两个教师同时打开同一份答卷互相覆盖,以及学员交卷请求重发导致成绩行被插了两遍——后者靠(exam_id, student_id)的唯一约束兜底,不靠代码判断。
还有一条文档完全没提但必须补的:考试时长和断线续考。交卷接口要做幂等,用exam_record表里的status做状态机(NOT_STARTED → IN_PROGRESS → SUBMITTED → GRADED),状态回退一律拒绝。没有这个状态机,学员在提交瞬间刷新页面,很可能留下两份记录,然后查分查到两个不一样的分数。
5. 避坑清单:这份说明书没写死、落地必翻车的五处
这一节是纯粹的踩坑记录,都是按这份文档开工时最容易撞上的。每条按“现象 → 原因 → 解决”写,能对上号就直接照做。
第一坑:功能编号重复,验收单对不上号。现象是测试写完用例回头核对需求,发现 0401 在文档里既是系统改卷又是教师改卷,0502 被写成 05402,于是同一份卷子的两条用例被标成同一个编号,覆盖率统计永远是错的。原因是文档在 Word 里手工维护编号,插入行之后没有重排。解决办法是开工前先做一次编号全量校对,把冲突项按“功能名称优先”重新编号,产出一张新旧编号映射表,此后所有产出物只用新号,旧号只在追溯历史时出现。
第二坑:用例图的扩展方向被当成了调用关系。现象是开发照着“033 角色的修改和删除扩展了角色的创建”去实现,把删除做成了创建的子流程,结果删除角色时必须先走一遍创建页。原因是 UML 的 extend 表示条件触发的附加行为,不表示调用顺序,而文档的描述语序又恰好相反。解决办法是以文字描述为准重写行为方向,图只用来给非技术方看个大概;实在要保留图,就在图下面补一句“本图不表示调用顺序”。
第三坑:权限只做到模块级,数据范围失控。现象是学员登录后能搜到高年级的教学资料标题,点进去提示无权限,但标题已经泄露了。原因是文档只写了“角色对模块的权限”,没写“数据行可见范围”,实现时只做了菜单级拦截,列表查询没加过滤。解决办法是给授权表加data_scope,所有列表查询统一走一个数据范围注入,详情接口单独再做一次归属校验——列表拦了详情没拦,是最常见的漏。
第四坑:把技术选型当成了业务需求写进验收。现象是验收会上有人坚持“必须能在 IE6 下正常显示”,理由是文档的用户环境里写了 IE6.0 或以上版本。原因是文档把 2005 年前后的技术栈和业务需求混在同一章里,没有区分硬约束和软约束。解决办法是把“用户环境”整节单独拎出来做一次分类:浏览器访问、B/S 结构属于业务约束必须保留;具体版本号属于当年的选型,迁移时按现网环境重新定,并把这个结论写进变更记录,避免每次评审都翻旧账。
第五坑:在线考试没有并发和断线的兜底设计。现象是考试结束前三分钟集中交卷,部分学员成绩缺失或重复。原因是文档只描述了“学员登录进行考试”,没有定义考试状态、交卷幂等和超时判定。解决办法是补一张考试记录表,用状态机约束流转,交卷接口以(exam_id, student_id)唯一索引兜底,超时由服务端时间判定而不是前端倒计时。这三样缺一个,大考当天就得出事。
6. 老技术栈怎么迁移验证:把当年的约束翻译成今天的验收单
这份文档的用户环境写得很具体:Windows 2000 Server、.NET Framework 2.0、Visual Studio.NET 2005、Oracle 9i、IE6.0 及以上,建模工具指定 UML 用 Rational Rose、E-R 用 PowerDesigner 11.0。放到今天直接照抄没有意义,但直接扔掉也不对——里面有一部分是业务约束,扔掉就会改变系统行为。
6.1 哪些约束必须原样保留
区分方法很简单:问一句“换掉它,用户的做事方式会不会变”。会变的保留,不会变的可以换。
| 原始约束 | 处置 | 理由 |
|---|---|---|
| B/S 结构,浏览器访问 | 保留 | 决定了部署形态和客户端零安装,属于业务前提 |
| 学员/教师/管理员三角色 | 保留 | 权限模型的根,换掉等于重做需求 |
| 学员可见范围为“本年级及以下” | 保留 | 业务规则,与技术栈无关 |
| 教学资料挂教学进度与节次 | 保留 | 决定数据模型,不是实现细节 |
| .NET Framework 2.0 / VS 2005 | 可替换 | 选型约束,替换后业务行为不变 |
| Oracle 9i | 可替换 | 同上,但分页、层级查询写法要随之调整 |
| IE6.0 及以上 | 可替换 | 当年的兼容目标,现状已无意义 |
| Rational Rose / PowerDesigner 11.0 | 可替换 | 建模工具,导出物能读就行 |
替换数据库时有一处必须改:文档时代的 Oracle 分页普遍用三层ROWNUM嵌套,迁到支持窗口函数的库就改成ROW_NUMBER() OVER (ORDER BY ...)加LIMIT,迁到 MySQL 系则直接LIMIT offset, size。层级查询也要改,Oracle 的CONNECT BY在多数库里有对应的递归 CTE 写法。这类改写不影响业务,但漏改一处就会出现分页重复或树形栏目加载不全,属于上线后才发现的类型。
6.2 一份可复现的迁移核对单
迁移完不要凭感觉验收,按下面这张单子走一遍,每项都有明确的判定标准,跑不通就退回。
| 核对项 | 操作 | 通过标准 |
|---|---|---|
| 角色权限矩阵 | 用四个内置角色分别登录,逐个功能点验证 | 与文档 01-04 功能清单逐条对齐,无多无少 |
| 数据可见范围 | 造三个不同年级的学员账号,访问同一资料列表 | 只返回本年级及以下节点,教师/管理员返回全量 |
| 教学资料绑定 | 新建进度节点并挂两份资料,一份设未来发布时间 | 未到时间学员不可见,教师可见 |
| 系统选题 | 构造题量不足的科目,按配额抽题 | 返回明确错误或降配提示,不返回半份卷 |
| 改卷并发 | 两个会话同时修改同一条答卷明细 | 后提交者收到冲突提示,分值不被覆盖 |
| 交卷幂等 | 同一学员连续提交两次交卷请求 | 只生成一条成绩记录,第二次返回已提交 |
| 栏目内容 XSS | 在动态信息里提交带脚本的富文本 | 落库转义或渲染时过滤,页面不执行 |
| 树形栏目加载 | 建三级栏目后加载导航 | 层级完整,无重复无缺失 |
这张单子跑完大概半天,能挡掉迁移后八成以上的返工。
6.3 我自己的习惯
最早接手这类历史需求文档时,我也是扫一遍功能清单就开工,觉得编号和括注都是形式主义。直到有一次上线前对验收单,发现文档里 0401 对应两个功能,测试按其中一种写了几十条用例,另一种压根没覆盖,返工时把改卷模块推倒重来了一遍。从那以后我每次拿到需求分析说明书,第一步都是把功能编号全量抄进表格做唯一性校验,冲突当场标出来,含糊的扩展关系当场翻译成一句主谓宾写死,然后才允许自己碰数据库。这个习惯看着笨,但它把“我以为”从流程里挤出去了。这份 1.0.0 版、状态还挂在“正在修改”的文档尤其如此——它本来就不是终稿,是让你去把空白填满的底稿。希望帮到你。
本文还有配套的精品资源,点击获取