学生勤工俭学系统毕设方案:从状态机设计到Spring Boot+Vue实现

发布时间:2026/10/25 15:32:49
学生勤工俭学系统毕设方案:从状态机设计到Spring Boot+Vue实现

简介:这份基于 JSP 的学生勤工俭学管理系统毕业设计资源包,面向高校计算机专业学生、毕业设计开发者及对校园服务类 Web 项目感兴趣的人群。系统围绕岗位发布、学生申请、工作时间记录、工资计算与双向评价等核心流程设计,覆盖需求分析、系统设计、编码实现、测试与文档编写等完整软件工程环节,可帮助理解 JSP、Servlet、JavaBean、JDBC 以及 MVC 分层架构在实际项目中的落地方式。压缩包内共 638 个文件,约 6.6MB,以 gif、js、html、jsp、jpg、class、css、jar、java、asp、doc、php 等类型为主,包含前端页面、界面图标、动态页面、后端逻辑、类库、数据库文件及项目文档,目录结构较完整。其中 JSP/HTML/JS/CSS 负责界面与交互,Java/Class/Jar 承担后端逻辑,Doc 与 Db 文件可用于查阅设计说明和数据表结构。已有 1085 人学习下载,对准备毕设答辩、自学 Web 开发或二次改造勤工助学系统均有参考价值。

1. 学生勤工俭学系统在毕设里到底值不值得写

每年毕业季都能看到大量类似标题的压缩包,学生勤工俭学系统属于典型的“业务流管理系统”:管理员发布岗位、学生在线申请、用工部门审核、签到签退记录工时、月底结算工资。它和图书管理系统、仓库管理系统的本质区别在于,业务状态更多、权限角色更明确、还有一条完整的“申请到结算”资金链,这些点恰好是答辩时最好讲、也最容易加分的部分。更关键的是,这个题目的数据表之间关联性强,能顺理成章地用到连表查询和事务处理,不会像“XX管理系统”那样把项目做成几张表的增删改查。

这个系统适合两类人:一类是后端基础一般、需要稳妥完成毕设的本科生,另一类是打算在简历里写“Spring Boot + Vue 全栈项目”的求职学生。如果你只想把 CRUD 做通就交差,那这题确实没什么难度;但如果你愿意把状态流转、工时结算、权限控制这几个点打磨扎实,它完全够格写进项目经验。下文就按一个可复现的完整方案来讲:建模、表设计、接口实现、避坑、验收数据,一路做到能现场演示的程度。

2. 从业务到建模:先把角色权限和岗位状态机画明白,再写第一行代码

2.1 三种角色的权限划分:管理员、学生、用工部门负责人

学生勤工俭学系统的第一件事不是建表,而是把“谁能干什么”理清楚。常见的错误是一上来就给用户表加一个 role 字段,然后前端用 v-if 控制按钮显隐,后端接口不做任何校验——答辩时老师只要用普通学生账号直接调管理员的接口,系统就穿了。

我一般把权限分成三个角色:学生、部门负责人(用工方)、系统管理员(团委或资助中心老师)。学生的权限边界是浏览岗位、投递申请、查看自己的签到记录和工资单;部门负责人可以发布岗位、审核申请、确认工时;管理员管全局,包括部门账号的开通、岗位下架、工资结算复核和系统参数配置。

这里有一个容易被忽略的设计点:部门负责人不应该有“创建学生账号”的权限,也不应该能直接改签到记录。工资数据必须由系统根据签到流水自动计算,人工只能做复核确认,不能直接改数字。这个约束在表结构上要体现出来——工资表里存的是计算快照,而不是一个可以被随手 UPDATE 的字段。

2.2 岗位与申请的状态流转:这是评分老师最爱追问的地方

学生勤工俭学系统里最值钱的业务逻辑是状态机。岗位有招募中、已满员、已下架;申请有待审核、已通过、已拒绝、已撤销;签到有未开始、进行中、已完成、异常(迟到/早退);工资有待核算、已确认、已发放。每一步状态迁移都要有对应的动作和校验,不能允许任意跳转。

举个例子,学生提交申请后、负责人还没审核时,学生可以撤销申请;但一旦审核通过,撤销动作就必须收回,此时的退路只有“离职/结束”,而且需要负责人或管理员操作。再比如,一个岗位招募人数到了上限,申请接口应该直接返回“岗位已满”,而不是让申请先进来再人工拒绝。

把这套状态流转画成一张表,比写十页文档都好用。建模阶段的成果就是下面这张状态表,它同时也是答辩PPT里的核心素材:

业务对象状态字段允许的迁移路径触发动作
岗位recruit_status招募中 → 已满员;招募中 → 已下架;已满员 → 招募中发布、系统自动满员、管理员下架、岗位重新开放
申请apply_status待审核 → 已通过;待审核 → 已拒绝;待审核 → 已撤销审核、驳回、学生撤销
签到sign_status未开始 → 进行中;进行中 → 已完成;未开始/进行中 → 异常签到、签退、超时自动判异常
工资单salary_status待核算 → 已确认;待核算 → 已复核;已确认 → 已发放系统月结、负责人确认、管理员发放

2.3 技术栈选型:为什么我用 Spring Boot + Vue 而不是 SSM 裸 JSP

如果这题放在五年前,SSM + JSP 是最稳妥的答案;但现在再交一套 JSP 项目,论文查重和老师追问都会比较难受。常见做法是 Spring Boot + MyBatis Plus + MySQL + Vue 2/3,前端用 Element UI 或 Element Plus 搭后台管理界面。Spring Boot 的理由是约定优于配置,省掉大量 XML 配置,MyBatis Plus 让单表 CRUD 几乎不用写 SQL,省下来的时间全砸在业务逻辑上。

前端这块,如果你是后端方向,不要硬上前后端分离。分离架构意味着你要多维护一套 Node 构建流程和跨域配置,答辩现场演示时前端一崩,后端再漂亮都白搭。我见过太多人卡在 npm 依赖安装上。折中的方案是:用 Vue + Vite 构建前端,后端把打包后的 dist 目录放到 Spring Boot 的 static 下,统一用 8080 端口访问,不分开部署。这样代码里还是前后端分离的结构,简历上也写得出“前后端分离”四个字,但部署时只有一个 jar 包,一个进程,省掉了 nginx 和跨域这两层变数。

如果是 Servlet + JSP 的老路子,也不是不能做,但你要预判两个问题:一是页面里 Java 代码和 HTML 混在一起,改样式容易碰坏逻辑;二是 JSP 的页面片段复用能力弱,学生端和工作台页面会有大量重复 header。相比之下,Vue 的组件化开发体验是好一个档次的,而且现在网上现成的后台模板非常多,把布局和菜单改一改就能用。

3. 学生勤工俭学系统的数据库设计:五张核心表把流程闭环

3.1 学生信息表与岗位表:开题查重最爱看的边界字段

学生表不只是 username 和 password。真实业务里,学生申请勤工俭学岗位需要提供学号、学院、专业、年级、联系电话、目前可用时段(比如周一下午、周三上午),甚至还要有辅导员姓名。这些字段直接影响后面的“岗位匹配度”功能——虽然匹配算法本身很简单,但表里没存这些字段,你想加都加不出来。

岗位表的核心字段包括:岗位名称、用工部门、岗位类型(助教/行政助理/实验室助理等)、招聘人数、已录取人数、时薪、每周最低工时、每周最高工时、岗位描述、状态。这里要特别强调“已录取人数”这个冗余字段,它和申请记录表的数量理论上可以实时 COUNT,但岗位列表页需要频繁展示招聘进度,每次 COUNT 全表性能不划算;用冗余字段加乐观锁更新,是更常见的做法。

涉及工资的字段要注意精度设计:时薪用 DECIMAL(6,2),每周工时上限用 INT,不要用 FLOAT 或 DOUBLE。金融计算用浮点数的后果,等你算完一个月工资对不上账的时候就知道了,那时候你连金额哪里出了问题都查不出来,属于典型的“改了不如不改”的坑。

CREATE TABLE student ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT '学号', real_name VARCHAR(20) NOT NULL COMMENT '姓名', college VARCHAR(50) COMMENT '学院', major VARCHAR(50) COMMENT '专业', grade VARCHAR(10) COMMENT '年级', phone VARCHAR(20) COMMENT '联系电话', available_time VARCHAR(255) COMMENT '可用时段,逗号分隔', password_hash VARCHAR(100) NOT NULL, status TINYINT DEFAULT 1 COMMENT '账号状态:1启用 0禁用', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生信息表'; CREATE TABLE position ( id BIGINT PRIMARY KEY AUTO_INCREMENT, dept_id BIGINT NOT NULL COMMENT '用工部门ID', title VARCHAR(50) NOT NULL COMMENT '岗位名称', position_type VARCHAR(20) COMMENT '岗位类型', headcount INT DEFAULT 1 COMMENT '招聘人数', hired_count INT DEFAULT 0 COMMENT '已录取人数', hourly_rate DECIMAL(6,2) NOT NULL COMMENT '时薪', weekly_min_hours INT DEFAULT 2 COMMENT '每周最低工时', weekly_max_hours INT DEFAULT 8 COMMENT '每周最高工时', description TEXT COMMENT '岗位描述', recruit_status TINYINT DEFAULT 1 COMMENT '1招募中 2已满员 3已下架', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='勤工俭学岗位表';

3.2 申请记录表与签到表:状态序列和时间戳是命根子

申请记录表是学生和岗位之间的关联实体,每一行代表一次申请。它的状态字段只有四个值:待审核、已通过、已拒绝、已撤销。申请表上要加一个唯一索引,确保同一学生同一岗位只能有一条有效申请记录,这个索引在并发场景下是最后一道防线——就算代码里忘了做幂等,数据库也能兜住重复提交。

签到表是整个系统里最体现“真实业务”的表。每次签到签退都生成一行记录,字段包括学生ID、岗位ID、签到时间、签退时间、工时(分钟)、是否异常、异常原因。这里的工时是一个冗余计算值,由后端在签退时算好写入,避免每次展示工资明细时都要拿时间戳现场算。

CREATE TABLE apply_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT NOT NULL, position_id BIGINT NOT NULL, apply_status TINYINT DEFAULT 1 COMMENT '1待审核 2已通过 3已拒绝 4已撤销', apply_reason VARCHAR(255) COMMENT '申请说明', reviewed_by BIGINT COMMENT '审核人ID', reviewed_at DATETIME COMMENT '审核时间', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_student_position (student_id, position_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='岗位申请表'; CREATE TABLE sign_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT NOT NULL, position_id BIGINT NOT NULL, sign_in_time DATETIME COMMENT '签到时间', sign_out_time DATETIME COMMENT '签退时间', work_minutes INT DEFAULT 0 COMMENT '工作时长(分钟)', sign_status TINYINT DEFAULT 1 COMMENT '1进行中 2已完成 3异常', remark VARCHAR(255) COMMENT '异常说明', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='签到签退记录表';

3.3 工资核算表:把时薪与工作时长解耦,避免返工

工资表不建议做得太复杂,但有一个设计原则很关键:工资表存的是某一结算周期的“核算快照”,而不是实时计算的视图。也就是说,每个月月底系统根据签到记录把工时汇总,乘以岗位当时的时薪,生成一条工资记录;之后即使签到数据被修改,工资单的数字也不再自动变化,需要走“重算”逻辑才会更新。

为什么这么做?因为“确认工资”是一个业务动作,负责人看完数字点了确认,再改签到记录,工资单不能悄悄变化,否则账就对不上了。把工资做成快照,等于给财务环节加了一把锁,这也是答辩时能讲深的地方。

CREATE TABLE salary_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT NOT NULL, settle_month VARCHAR(7) NOT NULL COMMENT '结算月份,如2025-06', total_minutes INT DEFAULT 0 COMMENT '总工时(分钟)', hourly_rate DECIMAL(6,2) NOT NULL COMMENT '核算时薪', total_amount DECIMAL(8,2) GENERATED ALWAYS AS (ROUND(total_minutes / 60 * hourly_rate, 2)) STORED COMMENT '工资金额', salary_status TINYINT DEFAULT 1 COMMENT '1待核算 2待确认 3已确认 4已发放', confirmed_by BIGINT COMMENT '确认人ID', confirmed_at DATETIME COMMENT '确认时间', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='工资结算表';

这里用了 MySQL 的生成列自动算金额,省掉了后端手工计算的一步风险。同时按 (student_id, settle_month) 建唯一索引,确保同一个学生一个月只有一张工资单。如果你用的数据库版本不支持生成列,就在后端算好再插入,效果是一样的。

4. 后端关键接口实现:状态校验和排他逻辑比增删改查更值钱

4.1 申请岗位接口:幂等与状态校验

第一个核心接口是学生申请岗位。从 Controller 到 Service 的完整链路不复杂,最薄的写法也就三四十行。但真正的约束在 Service 层:岗位是否在招募中、是否已经满员、学生之前有没有申请过、学生的账号是否被禁用,这四个条件缺一不可。代码写起来像这样:

@Override @Transactional(rollbackFor = Exception.class) public Result<Void> apply(Long studentId, Long positionId, String reason) { Student student = studentMapper.selectById(studentId); if (student == null || student.getStatus() != 1) { return Result.error("学生账号不存在或已被禁用"); } Position position = positionMapper.selectById(positionId); if (position == null) { return Result.error("岗位不存在"); } if (position.getRecruitStatus() != 1) { return Result.error("岗位当前不在招募期"); } if (position.getHiredCount() >= position.getHeadcount()) { return Result.error("岗位已满员"); } // 唯一索引兜底并发下的重复申请 ApplyRecord existing = applyRecordMapper.selectOne( new LambdaQueryWrapper<ApplyRecord>() .eq(ApplyRecord::getStudentId, studentId) .eq(ApplyRecord::getPositionId, positionId) .in(ApplyRecord::getApplyStatus, 1, 2)); if (existing != null) { return Result.error("请勿重复申请"); } ApplyRecord record = new ApplyRecord(); record.setStudentId(studentId); record.setPositionId(positionId); record.setApplyStatus(1); record.setApplyReason(reason); applyRecordMapper.insert(record); return Result.success(); }

这段代码有两个关键点。一是把“已满员”判断放在 INSERT 之前,属于业务层校验;但并发下两个学生同时申请最后一个名额,业务层校验会同时通过,所以需要数据库层面的兜底——在岗位表上用UPDATE position SET hired_count = hired_count + 1 WHERE id = ? AND hired_count < headcount这种乐观锁写法,更新行数为 0 就说明名额已被抢走。二是申请状态用了in(1, 2)而不是eq(1),因为已经被拒绝的学生理论上可以再次申请,但如果走“允许重新申请”的逻辑,就必须保留历史拒绝记录,并且把之前的拒绝记录置为失效,否则唯一索引会挡住第二次申请。

4.2 签到签退接口:把规则设计成可配置的

签到签退的难点不在记录时间,而在于怎么处理异常情况。常见的要求是:学生签到必须在岗位排班开始时间的前后 15 分钟内,否则记为迟到;签退时间早于排班结束时间 15 分钟以上,记为早退;一天只能签到一次,不允许重复签到。

把这些阈值硬编码在代码里的做法是最原始的,我一般会把它们塞进系统参数表:允许签到时间偏移(分钟)、允许签退时间偏移(分钟)、最小有效工作时长(分钟)。这样答辩时可以说“规则可配置”,并且改一处参数就能适配不同部门的不同出勤制度。

public Result<Void> signIn(Long studentId, Long positionId, LocalDateTime now) { SignRecord active = signRecordMapper.selectOne( new LambdaQueryWrapper<SignRecord>() .eq(SignRecord::getStudentId, studentId) .eq(SignRecord::getPositionId, positionId) .eq(SignRecord::getSignStatus, 1)); if (active != null) { return Result.error("当前已有进行中的签到记录,请先签退"); } SignRecord record = new SignRecord(); record.setStudentId(studentId); record.setPositionId(positionId); record.setSignInTime(now); record.setSignStatus(1); signRecordMapper.insert(record); return Result.success(); } public Result<Void> signOut(Long studentId, Long positionId, LocalDateTime now) { SignRecord active = signRecordMapper.selectOne( new LambdaQueryWrapper<SignRecord>() .eq(SignRecord::getStudentId, studentId) .eq(SignRecord::getPositionId, positionId) .eq(SignRecord::getSignStatus, 1) .orderByDesc(SignRecord::getCreatedAt) .last("LIMIT 1")); if (active == null) { return Result.error("没有找到进行中的签到记录"); } long minutes = Duration.between(active.getSignInTime(), now).toMinutes(); if (minutes < minValidMinutes) { return Result.error("本次工作时长过短,不计入有效工时"); } active.setSignOutTime(now); active.setWorkMinutes((int) minutes); active.setSignStatus(2); signRecordMapper.updateById(active); return Result.success(); }

签到接口必须是幂等的:浏览器重复提交、前端一次请求被重试多次,都不能生成两条进行中的记录。代码里靠的是“查进行中记录存在则报错”。更稳的做法是给 (student_id, sign_status=1) 加一个部分唯一索引,数据库层面保证同一学生同一时间只能有一条未签退记录,MySQL 5.7 不支持部分索引但可以用生成列配合唯一索引模拟。

签退时要顺手算好工时写进去。不要等工资结算时现场把签到时间减签退时间,那样每个月都要重新核对一遍历史数据,出错了都不知道是哪一步算歪的。工资查询接口直接读 work_minutes,加总就是月工时,简单到不会出错。

4.3 工资核算:延迟结算,给复核留窗口

工资核算一般是按月触发,用定时任务在每月 1 日凌晨把上个月所有通过审核的签到记录聚合,生成工资草稿。这一步不要直接在 salary 表上算金额,我习惯的做法是:先查出所有 “岗位尚未下架” 的记录按学生分组,再把结果逐条插入工资表,最后用事务保证“要么全部插入,要么一条都不插”。

更关键的是,工资生成后不要立刻置为“已发放”,要留一个待确认状态。部门负责人需要在这个月的前三个工作日完成核对,确认无误后点“确认工资”,管理员再做最终“发放”操作。这三步状态不能省,因为答辩的时候老师一定会问:“如果工资算错了怎么办?”你如果回答“改数据库”,那就减分;如果能讲清楚“工资单是快照、确认后不可变、异常走重算流程”这一套,就能把业务理解讲透。

INSERT INTO salary_record (student_id, settle_month, total_minutes, hourly_rate) SELECT s.student_id, DATE_FORMAT(s.sign_out_time, '%Y-%m') AS settle_month, SUM(s.work_minutes) AS total_minutes, p.hourly_rate FROM sign_record s JOIN position p ON s.position_id = p.id WHERE s.sign_status = 2 AND s.sign_out_time >= '2025-06-01' AND s.sign_out_time < '2025-07-01' GROUP BY s.student_id, p.hourly_rate;

这个聚合 SQL 在数据量上没问题,几百条签到记录毫秒级就能跑完。但要注意一个问题:同一个月内,如果某个学生换了岗位,A 岗的时薪 18 元、B 岗时薪 20 元,上面的 SQL 会在同一行里按岗位分组生成两条记录——这不是错误,反而说明了工资表设计的核心思路:工资按“学生 × 岗位 × 月份”核算,同一学生一个月内不同岗位分开结算,这样部门和岗位维度清晰,财务复核时能一眼看到每一笔钱对应哪个岗位。

如果同一个岗位的时薪在月内调整过(比如月初 18 元、月中涨到 20 元),上面 SQL 会按最终时薪统一算,这并不严谨。严谨做法是按签到记录当时的时薪逐条结算,也就是在 sign_record 表里冗余一个 hourly_rate 字段,签到签退时把岗位当前时薪写进去,工资聚合时直接读这条记录上的时薪,彻底与岗位表的历史价格解耦。这个冗余字段的性价比很高,建议在毕设阶段就加上,省得论文里解释时薪变更的边界情况。

5. 答辩演示避坑手册:5 条来自现场的血泪经验

5.1 前端列表和详情页数据不一致

现象:岗位列表显示“招募中”,点进去详情页却显示“已满员”;工资页显示“已发放”,明细里状态还是“待确认”。答辩现场遇到这种,基本等于当着老师的面翻车。

原因:前端页面的数据没有刷新,或者列表页用的是缓存对象,详情页查库,两边拿到的时间点不同。本质原因是列表接口和详情接口返回数据不一致。

解决:列表页每次路由进入时强制重新请求接口,不依赖组件缓存的挂载数据;详情页打开时也从服务端拉最新状态。尽量保证同一角色的所有页面都按同一套接口取数,前端只对展示型字段做本地缓存,对状态类字段一律实时取。

5.2 演示账号的数据太“干净”,看起来像造假

现象:学生账号点开申请记录,只有一两行数据;工资页面只有一个总额,没有明细行。老师看到这种演示数据,第一反应是质疑系统没有经过真实使用。

原因:自己手动往数据库里插了几条测试数据,字段之间没有业务关联,比如签到工时和工资月对不上,岗位时薪和工资总额对不上。

解决:写一个 SQL 脚本批量生成一段时间的模拟数据,注意保持逻辑一致:某个学生在某月有 8 次签到记录,每次 3 小时,时薪 18 元,工资总额就必须是 432 元。生成之后,自己先按 SQL 聚合一遍,确认数字能对上再拿去做演示。这里花半小时造假数据,胜过去现场被老师抓到一个漏洞。

5.3 数据库时区导致签到时间相差 8 小时

现象:签到记录的created_at显示 10:25,但前端页面显示 18:25,工时算出来全是负数或非常大的正数。

原因:MySQL 连接串里没有配置 serverTimezone,或者服务器系统时区是 UTC,而 Java 程序用的是 GMT+8。数据库存进去是 UTC 时间,前端取出来没有做转换。

解决:JDBC 连接串里显式加serverTimezone=Asia/Shanghai和useLegacyDatetimeCode=false;同时约定所有时间字段统一用 DATETIME 类型,代码里所有的LocalDateTime.now()不带时区,所有展示统一由前端本地格式化。这件事看着小,但真的会被老师问到,而且问出来你很难当场反应过来。

5.4 签到页的“定位”功能变成鸡肋

现象:为了加分,给签到功能加了 GPS 定位,结果上课的教室和岗位办公室距离太近,定位精度根本区分不出人到底在哪里;或者学生不愿意开定位权限,导致功能直接不可用。

原因:毕设里的定位功能大多没有结合真实业务场景考虑。勤工俭学的签到场景,学生可能在任何地方完成工作(比如图书馆理架、实验室值日),这些地方的 GPS 坐标根本没必要校验。

解决:把定位做成“选填信息”,签到时间记为唯一依据,定位记录只做存证,不做硬性校验。答辩时换个说法:“系统支持上传定位快照作为辅助凭证,不限制学生的实际工作地点。”这样功能还在,但不至于把自己锁死。

5.5 学生账号直接绕过权限管到后台接口

现象:老师随手用一个普通学生账号打开浏览器开发者工具,手动调用了一个管理员的接口,结果返回了所有学生的工资明细;或者把普通页面里的 role 从 2 改成 1,页面就显示了管理菜单。

原因:前端只隐藏按钮,后端接口没有校验当前登录用户的角色;或者角色判断从前端传参,没有走后端的登录态。

解决:登录成功后后端返回 JWT,里面携带角色信息;接口上加一个简单的 AOP 注解或拦截器,针对管理端接口检查 JWT 里的角色字段,不是管理员直接返回 403。这个问题是答辩被追问的高频点,也是最容易现场演示翻车的地方,动手检查一遍不费事,千万别赌老师不看。

6. 从毕设到可演示系统:用三组数据验证业务闭环

前面把你搭建的是一套能跑的系统,但要真正站上答辩台,你需要用数据证明业务闭环是通畅的,而不仅仅是“页面能跳转”。我习惯在演示前先跑三组数据验证,每一组对应一个业务能力维度。

第一组是并发申请验证。建一个招聘人数为 1 的测试岗位,同时用两个学生账号点击申请,看是否只有一个成功、另一个提示“岗位已满员”。这一步验证的是乐观锁和幂等逻辑的真实效果,比口头说“我做了防重”有说服力得多。

第二组是异常签到验证。把系统允许签到偏移时间暂时改成 5 分钟,用学生账号在排班时间 20 分钟后签到,确认系统标记为“迟到”;签退时故意提前 30 分钟,确认系统标记为“早退”。这一组数据直接撑起“考勤规则可配置、异常自动识别”的功能点。

第三组是跨月工资验证。在数据库中构造两笔签到记录,一笔在 6 月 30 日 23:50 签退、一笔在 7 月 1 日 00:10 签退,触发月度结算后,确认两笔工时被分别计入 6 月和 7 月的工资单。这个案例几乎每个做考勤或工资系统的同学都会在答辩时讲,但很少有人在演示时把时间边界真正测一遍。

做完这三组数据验证,再结合第 5 章里的演示账号准备,整个系统在答辩台前的基本盘就稳了。最后说一个我的个人习惯:每次改完业务逻辑,我不会只点一遍页面就结束,而是先跑旧的测试数据,确认原有逻辑没坏;再跑新的边界数据,确认新逻辑生效。这两步缺一不可,我就是靠这个习惯躲过了无数“改了一个小功能、炸了一个老接口”的尴尬,希望帮到你。

本文还有配套的精品资源,点击获取