简介:这份需求分析模板面向软件工程与系统工程方向的学习者、课程设计者及初级开发人员,用于在新建或改造系统时厘清目标、范围、功能边界与优先级划分,可作为撰写需求文档的参照框架。压缩包内共1个doc文件,体积约26KB,以完整报告范例的形式呈现,便于直接套用其结构与表述方式。文档以高校工资管理系统为实例,依次展开编写目的、项目背景、功能定义、系统目标、测试环境与测试概要,并给出测试结果与发现;其中功能定义覆盖员工信息录入修改删除、职务与职称工资标准设定、工资表创建、工资调整、工资统计及用户权限设定,系统层面则划分为用户管理、员工信息管理、工资标准设定与工资信息管理四大模块,并说明普通用户与管理员的权限差异。目前已有284人学习,适合需要参考文档框架、梳理模块划分与测试记录写法的读者。
1. 从一张算错的工资条说起:这份需求分析模板到底解决什么
每年发薪那几天,财务处最怕听到的一句话是「上个月的工资好像算错了」。高校工资这件事琐碎、重复、不容出错:教职工上千人,职务、职称、津贴各有一套标准,手工填表核一遍要两三天,改一个数字整张表就得重算。这份高校工资管理系统需求分析报告就诞生在这个场景里——它先把人工流程摊开,再用需求分析的方法把每一步翻译成计算机能执行的动作。它适合两类人:要交一份能过评审的需求文档的学生和初级开发,以及要接手老工资系统、得先摸清业务边界的人。技术栈写着 Delphi 7.0 加 Windows 98,年代感拉满,但报告的结构和追溯思路一点不过时。
2. 文档骨架拆解:一份能过评审的需求分析报告长什么样
这份报告的结构是典型的瀑布式:引言(编写目的、背景、功能定义、系统目标、测试环境)→ 测试概要 → 测试结果与发现 → 模块划分 → 运行评价。看着像教科书目录,但它能立住,是因为每一项都在回答一个具体问题:这份文档为谁写、痛点有多大、系统做哪几件事、做到什么程度算完成、在什么环境里跑。拆开看,真正决定文档质量的是三处——功能定义有没有编号、系统目标能不能验收、功能到模块的映射有没有对上号。
2.1 引言三件套:编写目的、背景、功能定义各自该写多细
编写目的这一段,原文写的是「分析人工管理高校工资的流程,把人工模式抽象为可在计算机上处理的自动模式」。这句话看着朴素,却把「现状态→目标态」的关系说清楚了,比很多模板里那句「为了完成本次课程设计」强得多。我一般会把它收敛成三句话:这份文档服务于谁(开发小组、财务处、验收方)、界定了什么范围(基本信息、工资标准、按月发薪、统计打印)、明确不包含什么(银行代发对接、税务申报、考勤采集)。最后那句「不包含」尤其重要,评审会上 80% 的扯皮都来自没人说清边界。
背景段落的失分点在于写成行业科普。原文铺垫了高校规模扩大、教职工增多、手工填表耗时易错,方向对,但缺量化的锚点。合格的写法应该落到本单位的具体数字:在册教职工多少人、每月发薪手工核对几遍、平均耗时几天、历史上出过几次错账。有了这几个数字,「为什么要上系统」就不再是一句正确的废话,而是可被质询、也可被验证的诉求。这一段写扎实了,后面拒绝不合理的加需求才有底气。
功能定义是整份文档最有价值的部分,原文列了七条:员工基本信息录入修改删除、工资标准设定(职务工资、职称工资、其他工资)、工资信息浏览、员工工资表创建、工资调整管理、工资统计、用户级别设定与口令修改。七条本身没问题,问题在于没有编号。没有编号的功能项在后续的模块划分、测试用例、验收单里就对不上号,评审时有人问「工资统计到底归在哪个模块」,全组翻文档翻五分钟,这就是血泪经验。正确做法是从一开始就固定编号 F-01 到 F-07,全文档只引用编号,不再重复描述。
## 3. 功能定义 | 编号 | 功能名称 | 输入 | 输出 | 适用角色 | |-------|------------------|----------------------------|--------------------------|-----------------| | F-01 | 员工信息维护 | 工号、姓名、部门、职务、职称 | 员工主记录 | 管理员 | | F-02 | 工资标准设定 | 标准类型、档级、金额、生效月 | 标准记录(带生效日期) | 管理员 | | F-03 | 工资信息浏览 | 工号、月份 | 工资明细列表 | 管理员 / 教职员工 | | F-04 | 月度工资表生成 | 工资期间(YYYYMM) | 该月工资明细快照 | 管理员 | | F-05 | 工资调整 | 工号、调整项、调整额、原因 | 调整流水 + 明细修订 | 管理员 | | F-06 | 工资统计 | 期间范围、部门维度 | 汇总表(应发/扣款/实发) | 管理员 | | F-07 | 用户与口令管理 | 账号、角色、新口令 | 用户记录 | 管理员 |这张表把原来七条文字描述压成了可执行的规格:每一行都有输入、输出、角色。参数上要注意两点,一是「工资期间」必须用 YYYYMM 六位字符串而不是日期类型,否则按月分组统计时跨库跨语言都会踩格式坑;二是 F-02 的标准记录必须带「生效月份」,工资标准是会随政策调整的,没有生效日期就没法回答「三月发的这笔钱是按哪版标准算的」这个必被问到的问题。
2.2 系统目标与测试环境:把不可验收的话改成可验收的
原文的系统目标写得挺全:能添加修改教职工基本信息、按职务工资/职称工资/其他工资拆分、能调整标准、能按月生成工资表、能打印报表、能做用户管理、教职员工只能查自己的工资、系统应「简单、易用、小巧、经典」。前几条都是好东西,问题出在最后那几个形容词上。「简单、易用、小巧」在验收会上没有任何可操作性,甲方说不够易用,你没有任何依据反驳。
把形容词换成指标,是这一步唯一要做的事。我一般按这个套路转:易用性→新用户不看手册完成一条员工信息录入的时间上限;性能→单月 2000 人工资表生成耗时上限、查询单条工资的响应时间;小巧→安装包体积和最低内存占用;可靠性→连续生成 12 个月工资数据无重复无遗漏。原文里「不同的管理级别有不同的操作权限」这句其实已经是可验收的表述了,只是没落到具体条目,补一张权限矩阵就完整。
测试环境这一段在原文里是「CPU Pentium Ⅲ 以上、内存 128M 以上、Windows 98 以上、开发工具 Delphi 7.0」。放在当年这是标准配置,但作为一个模板传下来,它有个隐蔽的坑:只写了下限,没写依赖。真实部署时跑不起来,往往是 BDE(Borland Database Engine)没装、打印机驱动不对、局域网共享目录权限不足这些事。合格的测试环境章节应该写成三层——硬件下限、操作系统与运行时依赖(BDE 版本、数据库服务端版本)、外设与网络条件(针式打印机型号、局域网带宽)。少写一层,交付现场就得现场 debug。
提示:测试环境章节的作用不是「显得正式」,而是当线上跑不起来时,你能拿它证明「这是环境不符合约定」,而不是「你的程序有 bug」。
2.3 功能定义到模块划分:一张映射表把需求钉死在模块上
原文在测试结果部分给出了四个模块的划分:用户管理模块、员工信息管理模块、工资标准设定模块、工资信息管理模块。四个模块的职责描述得还算清楚——用户管理负责管理级别划分与账号维护,员工信息管理负责基本信息增删改查并按学院组织,工资标准设定负责职务/职称/其他三类标准的设定修改删除保存,工资信息管理负责工资表生成、个人查询、修改、结算、统计、打印。
但这里有个明显的断点:七条功能定义和四个模块之间没有显式对应关系,读者得自己在脑子里连线。原文甚至把模块结构图写成了粘连的一串文字(「工资信息管理工资结算工资修改工资创建工资统计…」),大概率是从画图工具里复制粘贴时丢了格式。这种图在评审时等于没有。补一张映射表,成本极低,收益极高。
| 功能编号 | 功能名称 | 归属模块 | 验收方式 |
|---|---|---|---|
| F-01 | 员工信息维护 | 员工信息管理模块 | 增删改查各跑一遍,查库比对 |
| F-02 | 工资标准设定 | 工资标准设定模块 | 三类标准各设一档,验证生效月 |
| F-03 | 工资信息浏览 | 工资信息管理模块 | 管理员全量、员工仅本人 |
| F-04 | 月度工资表生成 | 工资信息管理模块 | 同月重复生成不产生重复记录 |
| F-05 | 工资调整 | 工资信息管理模块 | 调整后重算,留调整流水 |
| F-06 | 工资统计 | 工资信息管理模块 | 与手工 Excel 结果逐项比对 |
| F-07 | 用户与口令管理 | 用户管理模块 | 角色切换后菜单与数据均受限 |
这张表的第三列是模块归属,第四列是验收方式。第四列是最容易被跳过的一列,但它直接决定了后面的测试用例怎么写。原文的测试概要要求「用表格形式列出每一项测试的标识符及其测试内容,并指明实际测试与计划的差别」,有了这张映射表,测试标识符就能直接编成 T-F01-01、T-F01-02 这样的形式,一眼看出测的是哪条需求。没有这张表,测试用例就是一堆孤立的操作步骤,覆盖率永远说不清。
3. 四个核心模块的需求落地:权限、标准、工资表、统计
模块划分定下来之后,真正需要动脑子的是四个地方:权限到底卡在哪一层、员工信息和工资标准怎么对应、按月生成的工资表怎么保证不被后来的标准调整污染、统计口径按应发还是实发。这四件事在需求文档里各写一段话,落到代码里就是四组表结构和校验逻辑,写不清楚后面全是返工。
3.1 权限模型:管理员与教职员工两个角色的边界在哪一层
原文的权限设计是两角色制:管理员是财务部门人员,可以对系统进行一切操作;教职员工是普通用户,只能运行个人工资查询功能。同时还有「用户级别设定以及口令修改」。两角色在高校工资这个场景下是够用的,但需求文档必须把「只能查自己的」这句话翻译成技术约束,否则开发很可能做成「普通用户能看到所有人的工资列表,只是不能改」——这在真实场景里是事故。
权限要分两层看:功能级权限决定你能不能打开这个菜单、能不能调用这个功能,数据级权限决定你能看到哪些行。原文只说了功能级,数据级是隐含的。我的做法是写一张权限矩阵,把两个角色对七条功能的开关列全,再单独加一段说明数据级约束:F-03 对教职员工角色,查询条件强制附加emp_no = 当前登录工号,不接受前端传入的工号参数。
| 功能编号 | 管理员 | 教职员工 | 数据级约束 |
|---|---|---|---|
| F-01 | 可执行 | 禁止 | — |
| F-02 | 可执行 | 禁止 | — |
| F-03 | 可执行 | 可执行 | 员工角色强制限定本人工号 |
| F-04 | 可执行 | 禁止 | — |
| F-05 | 可执行 | 禁止 | — |
| F-06 | 可执行 | 禁止 | — |
| F-07 | 可执行 | 仅改自己口令 | 口令修改只允许改当前账号 |
落到 Delphi 里,这类校验千万不要散落在各个 Form 的事件里,那是最典型的翻车现场——十几个窗体每个写一遍if UserRole = 'admin' then,加一个角色要改十几处。正确做法是抽一个独立单元做统一入口,所有业务窗体只调它。
// SysAuth.pas —— 权限统一入口,所有业务单元都从这里过 unit SysAuth; interface type TRoleType = (rtAdmin, rtStaff); // rtAdmin=财务管理员, rtStaff=教职员工 // AModuleCode 传需求文档里的功能编号,如 'F-04' function CanRun(const AUserRole: TRoleType; const AModuleCode: string): Boolean; // 数据级校验:员工角色只能看自己 function CanSeeEmployee(const AUserRole: TRoleType; const ALoginEmpNo, ATargetEmpNo: string): Boolean; implementation const // 仅管理员可执行的功能编号,与需求文档 F-0x 一一对应 ADMIN_ONLY: array[0..4] of string = ('F-01', 'F-02', 'F-04', 'F-05', 'F-06'); function CanRun(const AUserRole: TRoleType; const AModuleCode: string): Boolean; var I: Integer; begin Result := True; if AUserRole = rtAdmin then Exit; // 管理员全通,直接返回 for I := Low(ADMIN_ONLY) to High(ADMIN_ONLY) do if ADMIN_ONLY[I] = AModuleCode then begin Result := False; // 普通用户命中黑名单即拒绝 Exit; end; // 未命中黑名单的只剩 F-03 与 F-07 end; function CanSeeEmployee(const AUserRole: TRoleType; const ALoginEmpNo, ATargetEmpNo: string): Boolean; begin if AUserRole = rtAdmin then Result := True else Result := (ALoginEmpNo = ATargetEmpNo); // 员工只能看本人,工号严格相等 end; end.这段代码有两处值得说。第一,ADMIN_ONLY里存的是需求编号而不是菜单名,好处是需求变更时改常量数组就行,不用满代码库搜索字符串。第二,Exit后面不加括号参数——Delphi 7 的Exit是无参过程,Exit(True)是后来版本的写法,直接照抄新版代码在 Delphi 7 里编译不过,这个坑我踩过一次,报错信息还特别不直观。参数AModuleCode建议统一用大写加连字符的规范格式,跟文档严格一致,别一会儿F1一会儿F-01。
3.2 员工信息与工资标准的对应关系:三个维度怎么挂上去
原文有一句话点到了核心:「工资标准的依据恰好与教职员工的基本信息相一致,形成对应关系」。这句话翻译成数据模型就是:员工表存职务和职称两个维度值,工资标准表按「标准类型 + 档级 + 生效月份」存金额,发薪时按员工当前的职务、职称去标准表里匹配。
这里要提前想清楚一个设计选择:标准表是分成三张(职务标准表、职称标准表、其他标准表)还是一张带类型字段的表。原文的功能描述倾向于三张,因为「职务工资标准、职称工资标准、其他工资标准」是并列的三类;但从扩展性看,一张表加std_type字段更好维护,加一类津贴不用改表结构。我的建议是在需求文档里就写成一张表加类型字段,同时把三种类型作为字典数据初始化进去,这样既满足原文的三类语义,又留了扩展口。
工资构成也要在需求里写死公式,不能含糊。按原文的意思,一名教职工的应发工资 = 职务工资 + 职称工资 + 其他工资合计,再减去扣款得到实发。这个公式必须写进文档,因为「其他工资」是个筐,可能是教龄津贴、住房补贴、课时费,每一项的算法都不同。如果不在需求阶段把「其他工资」拆成可配置的工资项,后面每加一项就要改一次代码。
-- 工资标准表:一张表承载三类标准,std_type 区分 CREATE TABLE t_salary_std ( std_id INTEGER NOT NULL, -- 主键 std_type VARCHAR(8) NOT NULL, -- DUTY=职务 TITLE=职称 OTHER=其他 std_code VARCHAR(8) NOT NULL, -- 档级编码,如 'D03' / 'T02' std_amount NUMERIC(10,2) NOT NULL, -- 月标准金额 effect_from VARCHAR(6) NOT NULL, -- 生效期间 YYYYMM PRIMARY KEY (std_id) ); -- 取某员工在 202403 适用的职务工资:取生效月 <= 目标月的最大一版 SELECT s.std_amount FROM t_salary_std s WHERE s.std_type = 'DUTY' AND s.std_code = :duty_code AND s.effect_from <= '202403' ORDER BY s.effect_from DESC FETCH FIRST 1 ROWS ONLY;这段 SQL 的关键是effect_from <= :期间加倒序取第一条。很多新手会写成effect_from = :期间,那样只要某个月没维护标准就直接查空,工资表里出现一片零。用「取不晚于目标月的最大版本」这个写法,标准调整后老月份依然能算出当时正确的金额。std_amount用NUMERIC(10,2)而不是浮点,钱这个东西一分都不能飘。
3.3 工资表的按月生成与统计口径:快照比关联更靠谱
原文说「按月生成工资表,保存在数据库中」,这句话看着平淡,其实藏着一个非常重要的设计决策:工资表是快照还是视图。如果工资明细表只存emp_no和std_id,每次查询时实时关联标准表算金额,那么一旦标准调整,历史月份的工资数字就会跟着变——查三月工资,看到的却是按八月标准算出来的数,这是最典型的对账噩梦。
正确的做法是生成即快照:F-04 执行时,把当期每名在职员工的职务工资、职称工资、其他工资、扣款、实发逐项算出来,以金额的形式写进明细表,同时保留标准记录的引用(std_id)作为溯源。这样历史数据永远是你当月发出去的那个数,标准怎么改都动不了它。这一点必须在需求文档里明确写出来,否则开发为了省事很可能做成实时计算。
| 字段 | 类型 | 说明 |
|---|---|---|
| period | VARCHAR(6) | 工资期间 YYYYMM |
| emp_no | VARCHAR(12) | 工号 |
| duty_pay | NUMERIC(10,2) | 职务工资快照 |
| title_pay | NUMERIC(10,2) | 职称工资快照 |
| other_pay | NUMERIC(10,2) | 其他工资合计快照 |
| deduct | NUMERIC(10,2) | 扣款合计 |
| net_pay | NUMERIC(10,2) | 实发 = duty+title+other-deduct |
| std_ref | VARCHAR(64) | 引用的标准记录 id 串,用于溯源 |
统计口径(F-06)要在需求里明确到「按什么分组、算什么字段」。按原文的「工资统计」四个字,能衍生出至少五种报表:按学院汇总、按职务档级汇总、按月份趋势、按工资项构成、按人均。不写清楚,开发做出来的报表甲方一定不满意。我的做法是在需求文档里给统计章节配一张示例表头,把维度(期间、部门、职务)和指标(人数、应发合计、扣款合计、实发合计、人均实发)全列出来,让甲方在纸上先确认,比上线后改省事得多。
注意:月度工资表生成必须做幂等,同一期间重复执行只能覆盖不能追加。落地方式是在
(period, emp_no)上建唯一索引,生成前先按期间删除旧数据,整个过程包在一个事务里。
4. 从需求文档到可运行骨架:Delphi 7.0 下的表结构与模块分层
需求文档写完,下一步是把它变成能跑的骨架。这一步最怕的是文档一套、代码一套,评审时没人发现,联调时全线崩。我的习惯是先落四张主表,再按模块划分出 DataModule,最后把需求编号、模块、测试用例三者串成追溯矩阵。这套骨架在 Delphi 7 加任意一种关系型数据库上都能直接套。
4.1 四张主表把四条主需求钉住
对应 F-01、F-02、F-04、F-07,最小可用集合是四张表:员工表、工资标准表、工资明细表、用户表。表名和字段名尽量和文档用词一致,别文档写「教职员工」代码写teacher,那种不一致三个月后自己都看不懂。
-- 员工基本信息表:对应 F-01 CREATE TABLE t_employee ( emp_no VARCHAR(12) NOT NULL PRIMARY KEY, -- 工号,业务主键 emp_name VARCHAR(30) NOT NULL, dept_code VARCHAR(8) NOT NULL, -- 所属学院/部门编码 duty_code VARCHAR(8) NOT NULL, -- 职务档级,关联 t_salary_std title_code VARCHAR(8), -- 职称档级,可为空 bank_acct VARCHAR(30), hire_date DATE, status SMALLINT DEFAULT 1 -- 1=在职 0=离职 ); -- 系统用户表:对应 F-07 CREATE TABLE t_user ( login_id VARCHAR(20) NOT NULL PRIMARY KEY, -- 登录账号 pwd_hash VARCHAR(64) NOT NULL, -- 口令散列,不存明文 role_code VARCHAR(8) NOT NULL, -- ADMIN / STAFF emp_no VARCHAR(12), -- 员工角色绑定的工号 enabled SMALLINT DEFAULT 1 ); -- 工资明细表:对应 F-04 / F-05,快照式存储 CREATE TABLE t_salary_detail ( period VARCHAR(6) NOT NULL, -- YYYYMM emp_no VARCHAR(12) NOT NULL, duty_pay NUMERIC(10,2) DEFAULT 0, title_pay NUMERIC(10,2) DEFAULT 0, other_pay NUMERIC(10,2) DEFAULT 0, deduct NUMERIC(10,2) DEFAULT 0, net_pay NUMERIC(10,2) DEFAULT 0, PRIMARY KEY (period, emp_no) -- 天然幂等:同月同人只有一条 ); -- 工资调整流水:对应 F-05,保留「谁在什么时候因为什么改了哪一笔」 CREATE TABLE t_salary_adj ( adj_id INTEGER NOT NULL, period VARCHAR(6) NOT NULL, emp_no VARCHAR(12) NOT NULL, item_code VARCHAR(8) NOT NULL, -- 调整项 adj_amount NUMERIC(10,2) NOT NULL, reason VARCHAR(200), oper_id VARCHAR(20), oper_time TIMESTAMP, PRIMARY KEY (adj_id) );四个表里有两个细节值得单拎出来。t_salary_detail用(period, emp_no)做联合主键,等于在数据库层面把幂等约束死了,程序里就算写错逻辑也插不进重复行,这是比在代码里判断更可靠的兜底。t_salary_adj单独建表而不是在明细表里加个备注字段,是因为工资调整在高校场景里必须可审计——绩效扣款、补发、追扣都要留痕,出了问题要能回答「这笔 300 块是谁在几号改的、为什么」,这个表就是那份后悔药。
4.2 单元划分:一个模块一个 DataModule,业务逻辑不进 Form
Delphi 7 项目里最常见的坏味道,是把 SQL 和业务判断全写在 Form 的按钮事件里。一个btnGenerateClick里塞两百行,连数据库、算工资、拼报表、弹提示混在一起,改一个字段要翻三层嵌套。按原文的四个模块划分,对应做法是四个 DataModule 加若干纯业务单元,Form 只负责收集参数和展示结果。
// dmSalary.pas —— 工资信息管理模块的数据与逻辑入口 unit dmSalary; interface uses DB, ADODB, SysUtils, SysAuth; type TdmSalary = class(TDataModule) qryEmp: TADOQuery; qryStd: TADOQuery; private function LoadDutyPay(const ADutyCode, APeriod: string): Currency; function LoadTitlePay(const ATitleCode, APeriod: string): Currency; public // 生成指定期间的工资表;返回成功写入的行数,-1 表示失败 function GenerateMonthly(const APeriod: string; out AErrMsg: string): Integer; end; implementation function TdmSalary.LoadDutyPay(const ADutyCode, APeriod: string): Currency; begin Result := 0; if Trim(ADutyCode) = '' then Exit; // 职称可为空,职务不可为空 qryStd.Close; qryStd.SQL.Text := 'SELECT std_amount FROM t_salary_std ' + 'WHERE std_type=''DUTY'' AND std_code=:c AND effect_from<=:p ' + 'ORDER BY effect_from DESC'; qryStd.Parameters.ParamByName('c').Value := ADutyCode; qryStd.Parameters.ParamByName('p').Value := APeriod; qryStd.Open; if not qryStd.IsEmpty then Result := qryStd.FieldByName('std_amount').AsCurrency; end; function TdmSalary.GenerateMonthly(const APeriod: string; out AErrMsg: string): Integer; var DutyPay, TitlePay, OtherPay, Deduct: Currency; begin Result := 0; AErrMsg := ''; // 生成前先清当期,保证重复执行结果一致 qryEmp.Close; qryEmp.SQL.Text := 'DELETE FROM t_salary_detail WHERE period=:p'; qryEmp.Parameters.ParamByName('p').Value := APeriod; qryEmp.ExecSQL; qryEmp.Close; qryEmp.SQL.Text := 'SELECT emp_no, duty_code, title_code FROM t_employee WHERE status=1'; qryEmp.Open; while not qryEmp.Eof do begin DutyPay := LoadDutyPay(qryEmp.FieldByName('duty_code').AsString, APeriod); TitlePay := LoadTitlePay(qryEmp.FieldByName('title_code').AsString, APeriod); OtherPay := 0; // 其他工资项由配置表累加,此处先留零 Deduct := 0; // 扣款来自考勤与借款,二期接入 // 逐条写入快照,金额落库即固化 with TADOQuery.Create(nil) do try SQL.Text := 'INSERT INTO t_salary_detail ' + '(period, emp_no, duty_pay, title_pay, other_pay, deduct, net_pay) ' + 'VALUES (:p,:e,:d,:t,:o,:x,:n)'; Parameters.ParamByName('p').Value := APeriod; Parameters.ParamByName('e').Value := qryEmp.FieldByName('emp_no').AsString; Parameters.ParamByName('d').Value := DutyPay; Parameters.ParamByName('t').Value := TitlePay; Parameters.ParamByName('o').Value := OtherPay; Parameters.ParamByName('x').Value := Deduct; Parameters.ParamByName('n').Value := DutyPay + TitlePay + OtherPay - Deduct; ExecSQL; Inc(Result); finally Free; end; qryEmp.Next; end; end; end.GenerateMonthly的参数APeriod是六位字符串,调用前必须由界面层校验格式,别指望数据库帮你兜;out AErrMsg用来回传失败原因,比直接弹 MessageBox 更好测。整个流程的执行顺序是「先删当期、再逐人算、再逐条插」,删和插应该包在同一个事务里,否则中途异常会留下半张工资表。上面为了篇幅没展开事务,实际代码里我会在GenerateMonthly开头Connection.BeginTrans,结尾Commit,异常分支Rollback。
LoadDutyPay里的空值判断也不是多余的。职称对刚入职的年轻教师经常是空的,如果直接拿空串去查标准表,查不到就返回零,账面上看不出错,但对账时会发现某几个人少了一笔钱。需求文档里应该明确写一句「职称未评定时,职称工资按最低档或零处理」,这种边界条件不写,开发只能自己猜。
4.3 追溯矩阵:需求编号、模块、测试用例三者对齐
原文的测试概要提到要用表格列出每项测试的标识符和测试内容,并说明实际测试与计划的差异。这个要求在当年写起来费劲,但它的思路非常对——测试必须可追溯到需求。落到实操上就是一张三列矩阵:需求编号 → 模块 → 测试用例编号,全覆盖才算测完。
| 需求编号 | 模块 | 测试用例编号 | 关键测试点 |
|---|---|---|---|
| F-01 | 员工信息管理模块 | T-F01-01/02 | 新增重复工号应被拒绝 |
| F-02 | 工资标准设定模块 | T-F02-01/02 | 同档级同生效月不允许重复 |
| F-03 | 工资信息管理模块 | T-F03-01/02 | 员工角色查他人工资返回空 |
| F-04 | 工资信息管理模块 | T-F04-01/02 | 同月重复生成结果一致、条数不变 |
| F-05 | 工资信息管理模块 | T-F05-01 | 调整后重算,流水可查 |
| F-06 | 工资信息管理模块 | T-F06-01 | 汇总数与明细逐项相加一致 |
| F-07 | 用户管理模块 | T-F07-01/02 | 改口令后旧口令立即失效 |
这张表最大的用处不是应付评审,而是自己心里有数。七个需求、十来个用例,跑完一轮大概半天,每次改完代码重跑一遍,比上线后被动救火强太多。原文里「功能测试 1 人员信息输入」「功能测试 2 人员信息删除」两条都写了「完全正确实现」,这种结论没有测试数据支撑,属于典型的自说自话。合格的写法是附上测试数据:输入什么、期望什么、实际什么。
5. 避坑与排查:这类文档和系统最容易翻车的五个地方
需求文档写得好不好,往往不是写作水平问题,而是有没有踩过那些固定的坑。以下五条是我在不同项目里反复见到的,按「现象 → 原因 → 解决」记下来,下次照着排一遍能省不少返工。
5.1 文档层面的三个坑
坑一:功能条目没有编号,模块划分另起一套名字。现象是评审会上有人问「工资统计归哪个模块」,全组翻文档五六分钟对不上,最后靠猜。原因是功能定义和模块划分分属两个章节,各写各的,中间没人做映射。解决办法是从第一版就固定 F-01 到 F-07 的编号,模块表、测试用例、验收单全部只引用编号;模块名可以和功能名不同,但映射表必须存在,且放在模块划分章节的第一屏。
坑二:系统目标写成一串形容词。现象是验收时甲方说「不够易用」,你没法反驳也没法改,因为「易用」没有阈值。根源在于把期望写成了感受。解决办法是把每条形容词翻译成可测指标:易用性改成新用户不看手册完成一条员工录入的耗时上限;性能改成单月两千人工资表生成时间上限;稳定性改成连续生成十二个月数据无重复无遗漏。指标要写具体数字,别写「较快」「尽量短」。
坑三:测试环境只写下限不写依赖。现象是本地跑得好好的,到财务处那台机器上一点就报错。原因是文档里只写了「Windows 98 以上、Delphi 7.0」,没写 BDE 版本、数据库客户端、打印机驱动、局域网共享目录。Delphi 7 时代的应用大量依赖 BDE 和本地配置,少一个都起不来。解决办法是把环境章节拆成硬件下限、运行时依赖、外设与网络三段,交付前照着清单在目标机器上过一遍。
5.2 落地层面的两个坑
坑四:工资标准调整后,历史月份的工资跟着变了。现象最典型:三月的工资条发给老师是这个数,八月查三月记录发现对不上,财务被追问。原因是明细表只存了标准记录的外键,查询时实时关联标准表,而标准表被后来的调整覆盖了。解决办法是生成工资表时把金额快照进明细表,std_ref字段只用于溯源不参与计算。这个决策必须在需求文档里写死,别留给开发临场发挥。
坑五:权限只做在菜单上。现象是普通用户把配置文件里的角色字段一改,或者直接调一下窗体,就能进管理界面看全员工资。根源在于只在界面层隐藏菜单项,数据访问层没有任何校验。解决办法是把权限判断收到统一的业务单元里,所有取数、写数的入口都先过一遍CanRun,涉及员工数据的再加一次CanSeeEmployee的行级校验。界面隐藏只是体验优化,不是安全边界。
提示:每月工资生成完成后,别只信程序返回的「成功」。抽三个人工算过的样本,逐个字段比对,这一分钟能挡住后面几小时的扯皮。
6. 二次开发:把这份模板改成你自己项目的需求书
拿到这份报告,多数人的真实需求不是「照着做一个高校工资系统」,而是「借它的结构写我自己的需求文档」。这时候要改的不是格式,是三处内容:业务对象的替换、薪酬维度的抽象、以及验收标准的落地。
第一处是业务对象替换。原文的角色是「财务管理员 + 教职员工」,部门维度是「学院」。换成企业场景,就是「HR/财务 + 普通员工」,部门维度换成「事业部/成本中心」;换成医院场景,职务换成「岗位序列」,职称换成「专业技术职务」。这一层改起来最省事,把dept_code的语义说明、权限矩阵里的角色名、统计报表的分组维度换掉就行,表结构基本不用动。
第二处是薪酬维度抽象,这是最值得动手的地方。原文把工资硬拆成「职务工资 + 职称工资 + 其他工资」三项,一旦单位要加一条「司龄补贴」,就得改表、改代码、改报表。我的改法是在t_salary_std之外再加一张t_pay_item配置表,把工资项变成数据。
-- 工资项配置表:加新津贴不再改代码,只加一条记录 CREATE TABLE t_pay_item ( item_code VARCHAR(8) NOT NULL PRIMARY KEY, -- 如 DUTY / TITLE / SENIOR item_name VARCHAR(30) NOT NULL, -- 职务工资 / 职称工资 / 司龄补贴 calc_type VARCHAR(8) NOT NULL, -- STD=查标准表 FIX=固定额 FORMULA=公式 std_type VARCHAR(8), -- calc_type=STD 时对应标准表的类型 sort_no INTEGER DEFAULT 0, -- 工资条上的显示顺序 enabled SMALLINT DEFAULT 1 );改造后的生成逻辑从「硬编码三项相加」变成「遍历启用的工资项,按calc_type分派算法」,t_salary_detail也从定长字段改成一个明细子表t_salary_item_detail(period, emp_no, item_code, amount)。这样加一项补贴,只需在t_pay_item插一条、在标准表维护对应档级金额,代码零改动。代价是明细表行数变多,查询时要按period + emp_no做聚合,索引建好就行。
第三处是验收标准落地。原文的目标段落里那些形容词,在改造版里应该全部消失,换成一张指标表,直接抄第 2 章里的写法就行。验证方法也很朴素:先构造 20 条覆盖各种边界的员工数据(职务为空、职称为空、当月入职、当月离职、有调整流水),跑一遍完整流程生成工资表,再拿 Excel 手工算一遍,两边逐字段比对。差一分钱都要查到底,因为差异背后往往就是某个空值分支没处理。
| 改造点 | 原来 | 改造后 | 验证方式 |
|---|---|---|---|
| 工资项 | 三项硬编码 | t_pay_item配置驱动 | 新增一项后不改代码即生效 |
| 明细存储 | 定长字段 | 子表按项存行 | 汇总数与明细相加一致 |
| 权限 | 两角色、菜单级 | 角色表 + 数据层校验 | 改角色后数据立即受限 |
| 验收指标 | 简单、易用、小巧 | 录入耗时、生成耗时、重复生成一致性 | 逐项实测取数 |
有件事我吃过不止一次亏:改表结构的时候顺手把老数据的字段也动了,结果历史月份的报表全乱。从那以后我每次动表之前都强制走一遍「备份 → 改结构 → 用上个月真实数据回放 → 比对汇总数」这套流程,慢是慢一点,但心里踏实。上面这套改造思路不是唯一的,你可以只抄其中的配置表设计,也可以只抄追溯矩阵,按自己项目的体量裁剪。希望帮到你。
本文还有配套的精品资源,点击获取