ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Java勤工助学管理系统设计与实现:从数据库到Spring Boot状态机

2026/9/18 21:55:45 拓冰建站 浏览量
Java勤工助学管理系统设计与实现:从数据库到Spring Boot状态机 简介一份基于Java的勤工助学管理系统设计与实现文档适合作为高校计算机专业毕业设计、课程设计参考也适用于需要建设勤工助学信息化平台的管理者。系统以MySQL为数据库采用Java及SSM框架开发涵盖学生申请勤工岗位、教师发布岗位信息、管理人员审核与分配等主要功能并兼顾稳定性、安全性、扩展性与用户友好性。文档包内仅1个docx文件压缩包大小9.45MB内容包括中英文摘要、系统需求分析、功能模块划分、数据库设计、关键代码实现与运行部署说明目录结构清晰有助于读者快速掌握该系统的设计思路与实现方式。该资源上线后已有93人学习下载适合正在撰写相关课题论文或准备开发类似勤工助学管理系统的开发者参考。1. 基于 Java 的勤工助学管理系统本质是带状态流转的业务系统看到“基于 java 勤工助学管理系统设计与实现”这个标题别急着建表。它的典型用户是学生、用工部门和资助管理员学生要浏览岗位、提交申请、看考勤和工资用工部门要发布岗位、上报工时管理员要终审、算钱、导报表。整条链路不是简单增删改查而是岗位从“招聘中”到“已关闭”、申请从“待审核”到“录用/不录用”的状态机。我一般会按“业务状态机 数据权限”来设计而不是堆菜单。这篇博客就从数据库表开始讲到 Spring Boot 接口、并发与权限坑最后补充课设文档里能写的验证和加分项。适合课程设计、毕业设计也适合后端刚转 Java 的开发者照着复现。2. 从业务流到表结构勤工助学系统的数据设计先于代码2.1 把“勤工助学”翻译成状态机先把角色和流程列出来再确定状态。常见角色有三种学生、用工部门、管理员。一条申请要经历岗位发布状态草稿→招聘中→关闭→ 学生申请待审核→录用/不录用→ 考勤待上报→已确认→ 工资未发放→已发放。这里的“状态”如果散落在业务代码里后面每个接口都要判断一堆 if所以最好在数据库中用一个 TINYINT 字段保存并约定常量。还有一个“数据权限”问题管理员能不能看全部岗位取决于登录用户类型而不是查询参数。这个我通常放在接口上做权限控制表只负责记录归属关系。下面的表格是三个角色对应的核心操作也是后面设计菜单和接口的参照。角色数据范围关键操作典型首页学生本人、公开招聘的岗位填报信息、申请岗位、查看考勤与工资岗位列表、申请进度用工部门本部门发布的岗位发布岗位、审核考勤、上报工时岗位概览、考勤审核管理员全部数据岗位终审、生成工资单、统计报表数据看板、用户管理有了这张表接口就比较容易拆成三个 Controller 包student、department、admin。后面前后端联调时权限也按这个表格来配。2.2 核心表结构用户、岗位、申请表、考勤、工资我一般会把“用户”和“角色”拆成两张表但课设阶段用user_type字段省掉角色关联表能更快跑通。下面这套 DDL 是我在本地 MySQL 8.0 里常用的最小骨架字段名保持英文注释写清楚中文含义。CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 用户ID, user_type TINYINT NOT NULL COMMENT 1学生 2用工部门 3管理员, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(100) NOT NULL COMMENT BCrypt加密后的密码 ) COMMENT 用户表; CREATE TABLE job_post ( id BIGINT PRIMARY KEY AUTO_INCREMENT, dept_user_id BIGINT NOT NULL COMMENT 发布岗位的部门用户, title VARCHAR(100) NOT NULL COMMENT 岗位名称, headcount SMALLINT NOT NULL DEFAULT 1 COMMENT 招聘人数, salary_hour DECIMAL(10,2) NOT NULL COMMENT 时薪, status TINYINT NOT NULL DEFAULT 0 COMMENT 0草稿 1招聘中 2关闭, publish_time DATETIME COMMENT 发布时间, KEY idx_dept(dept_user_id) ) COMMENT 岗位表; CREATE TABLE job_apply ( id BIGINT PRIMARY KEY AUTO_INCREMENT, job_id BIGINT NOT NULL, student_user_id BIGINT NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待审核 1录用 2不录用, apply_time DATETIME NOT NULL, UNIQUE KEY uk_job_student (job_id, student_user_id) ) COMMENT 岗位申请表; CREATE TABLE work_attendance ( id BIGINT PRIMARY KEY AUTO_INCREMENT, apply_id BIGINT NOT NULL COMMENT 录用后的申请ID, work_date DATE NOT NULL, hours DECIMAL(4,1) NOT NULL COMMENT 当天工时, confirm_status TINYINT NOT NULL DEFAULT 0 COMMENT 0待部门确认 1已确认 ) COMMENT 考勤表; CREATE TABLE salary_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_user_id BIGINT NOT NULL, work_month CHAR(6) NOT NULL COMMENT 月份如202505, amount DECIMAL(10,2) NOT NULL COMMENT 应发金额, status TINYINT NOT NULL DEFAULT 0 COMMENT 0未发放 1已发放, UNIQUE KEY uk_student_month (student_user_id, work_month) ) COMMENT 工资表;这里几个字段要重点解释一下。job_apply里的UNIQUE KEY uk_job_student (job_id, student_user_id)不是可有可无它是后面并发控制的第一道防线同一个学生同一个岗位数据库层面不允许重复申请。headcount是招聘人数面试时容易被问到“岗位满了怎么办”那时候应该检查已经录用人数而不是单独改 status。work_attendance里放的是apply_id而不是student_user_id因为考勤必须挂在一条“已录用”的申请上否则一个学生同时做两份工作时工时归属会乱。salary_record的uk_student_month保证同一月份不会生成两条工资配合事务使用。2.2.1 岗位状态与人数别只用一个 status 字段岗位表只有 status 是不够的录取过程中还要知道“当前招了几个”。常见做法是在后端查SELECT COUNT(*) FROM job_apply WHERE job_id ? AND status 1然后比headcount。虽然看起来多一次查询但代码比在表里维护一个current_count字段简单得多。如果担心并发超招可以在job_apply.status从 0 改到 1 时用“乐观锁更新”来处理这在第 4 章会再展开。2.2.2 为什么不用外键DDL 里没有加 FOREIGN KEY这不是偷懒。业务表之间的关联用“逻辑外键”代码里维护配合索引能让删除和导出更灵活。学生在系统里可能被禁用而不是删掉岗位下存在申请表时也需要“软关闭”。数据库外键在导入初始化数据时还会增加麻烦所以课设和中小型管理系统普遍不建物理外键。只要在 Java 代码里保持一致性能问题通常不存在。3. 用 Spring Boot 把申请、审核、考勤链路写成接口3.1 选型Spring Boot MyBatis-Plus 的取舍对于这个标题最常见的 Java 实现有三种JSP/Servlet、SSMSpring SpringMVC MyBatis、Spring Boot MyBatis-Plus。如果学校要求 JSP可以在 SSM 上改视图层但从“设计与实现”的含金量来说Spring Boot 更符合现在的开发习惯。个人建议用 Spring Boot 3.x MyBatis-Plus因为 BaseMapper 自带单表 CRUD能把精力放在业务状态机上。如果面试官问为什么不用 Spring Data JPA就回答MyBatis-Plus 能直接控制 SQL工资统计和考勤报表这类多表关联查询写 XML 更清晰。引入 Maven 依赖时很多初学者会踩版本坑。Spring Boot 3 需要 JDK 17如果学校环境是 JDK 8就改用 Spring Boot 2.7 与 MyBatis-Plus 3.5.x。下面以 Spring Boot 2.7 为例因为课设环境兼容性更高。dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency这个依赖会自动把 MyBatis 和 Spring Boot 集成起来application.yml 里只配置数据源spring: datasource: url: jdbc:mysql://localhost:3306/work_study?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: your_password mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml configuration: map-underscore-to-camel-case: truemap-underscore-to-camel-case: true这句非常重要数据库里的student_user_id会自动映射到 Java 属性studentUserId省掉大量TableField注释。3.2 实体和 Mapper 的最短写法一个实体对应一张表用TableName指定表名主键用TableId(type IdType.AUTO)。比如JobApplyData TableName(job_apply) public class JobApply { TableId(type IdType.AUTO) private Long id; private Long jobId; private Long studentUserId; /** 0待审核 1录用 2不录用 */ private Integer status; private LocalDateTime applyTime; }Mapper 接口继承BaseMapperJobApply后insert、selectById、selectList这些单表方法都有了。接着要加自定义的“条件更新”用来做状态流转时的原子操作。3.3 核心业务申请与审核的状态机代码勤工助学的核心不是写 CRUD而是保证状态流转的唯一性和正确性。下面这个 Service 方法是一个最小可用的申请逻辑。Service public class JobApplyService { private final JobApplyMapper applyMapper; private final JobPostMapper jobMapper; public JobApplyService(JobApplyMapper applyMapper, JobPostMapper jobMapper) { this.applyMapper applyMapper; this.jobMapper jobMapper; } Transactional(rollbackFor Exception.class) public void applyJob(Long jobId, Long studentUserId) { JobPost post jobMapper.selectById(jobId); if (post null) { throw new BizException(岗位不存在); } if (post.getStatus() ! JOB_STATUS_OPEN) { throw new BizException(岗位当前不可申请); } JobApply apply new JobApply(); apply.setJobId(jobId); apply.setStudentUserId(studentUserId); apply.setStatus(APPLY_STATUS_PENDING); apply.setApplyTime(LocalDateTime.now()); try { applyMapper.insert(apply); } catch (DuplicateKeyException e) { throw new BizException(你已经申请过这个岗位请不要重复提交); } } }这段代码把业务规则放在了 Service 层而不是 Controller 里这点在答辩时值得强调。Transactional保证select和insert在同一个事务里DuplicateKeyException捕获的是数据库唯一索引冲突而不是用selectOne预检查因为并发场景下检查完再插入可能被另一个请求抢先。审核采用“乐观更新”因为审核员可能被同一个申请打开两个标签页Transactional(rollbackFor Exception.class) public void reviewApply(Long applyId, Integer expectStatus, Integer targetStatus) { LambdaUpdateWrapperJobApply wrapper new LambdaUpdateWrapper(); wrapper.eq(JobApply::getId, applyId) .eq(JobApply::getStatus, expectStatus) .set(JobApply::getStatus, targetStatus); int rows applyMapper.update(null, wrapper); if (rows 0) { throw new BizException(该申请已被处理请刷新页面); } }这里expectStatus是调用方在改前查到的状态targetStatus是目标状态。更新条件里带上status expectStatus如果数据库里的状态已经变了rows就是 0于是拒绝本次变更。这个手法现场比直接 “update by id” 更能体现出状态机设计。3.3.1 参数说明为什么超时事务只放大不解决业务Transactional的默认回滚条件是运行时异常BizException最好继承RuntimeException否则事务不会回滚。另外insert和update的超时配置不要依赖事务级别而要在数据库连接池上设置max-wait避免连接池被慢查询占满。这些参数不是必填项但排错时先看application.yml再看业务代码。4. 权限与并发勤工助学系统里最容易丢分的三个点4.1 角色权限用 Sa-Token 还是 Spring Security这个系统的权限其实很简单按用户类型区分菜单和接口。Sa-Token 比 Spring Security 更适合中小型管理系统因为拦截器风格直观。引入sa-token-spring-boot-starter后在配置类里注册登录拦截Configuration public class SaTokenConfigure implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new SaInterceptor(handle - StpUtil.checkLogin())) .addPathPatterns(/**) .excludePathPatterns(/api/auth/login, /api/auth/captcha); } }接口权限用注解或代码判断userType例如GetMapping(/api/student/jobs) public ListJobPostVO listOpenJobs() { LoginUser user (LoginUser) StpUtil.getSession().get(user); if (user.getUserType() ! USER_TYPE_STUDENT) { throw new ForbiddenException(仅学生可访问); } return jobService.listOpenJobs(); }用StpUtil.getSession().get(user)拿到登录时放进去的用户信息避免每个接口再查一遍用户表。这里提示一下Sa-Token 默认把 session 放在内存里多实例部署时需要集成 Redis否则换一台机器登录态就丢了。课设阶段单机部署可以直接用默认内存模式。4.2 事务边界发工资和确认考勤不能分开工资生成是很典型的事务场景。如果先汇总工时再逐条插入工资明细中间有一步失败就会造成“工时已确认但工资没生成”。所以工资生成要放在一个事务方法里用Transactional(rollbackFor Exception.class)。Transactional(rollbackFor Exception.class) public void generateSalary(String workMonth) { ListAttendanceRecord records attendanceMapper.selectConfirmedByMonth(workMonth); MapLong, BigDecimal totals calcHours(records); for (Map.EntryLong, BigDecimal entry : totals.entrySet()) { SalaryRecord salary new SalaryRecord(); salary.setStudentUserId(entry.getKey()); salary.setWorkMonth(workMonth); salary.setAmount(entry.getValue().multiply(pricePerHour)); salary.setStatus(0); try { salaryMapper.insert(salary); } catch (DuplicateKeyException e) { // 已生成过跳过重复月份 } } }这里的DuplicateKeyException靠salary_record表的唯一索引兜底防止定时任务重复执行时重复插入。注意不要在 catch 里吞掉所有异常只捕获唯一键冲突其他异常继续抛出让事务回滚。4.3 并发申请和超招靠唯一索引扛别靠 synchronized很多初学者会在 Service 方法上加synchronized这在单实例下勉强有效但换到多实例部署就失效了。正确做法是三层配合第一层job_apply的唯一索引拒绝重复岗位申请第二层UPDATE ... WHERE status 0保证状态从待审核到录用只生效一次第三层在岗位表加一个校验可用名额的查询例如计算当前录用数是否小于headcount。如果学校要求做“并发测试”可以用 JMeter 模拟 50 个线程同时申请同一个岗位再检查表中是否存在超录。这个过程写进课设文档的“系统测试”部分比写“系统运行稳定”更有说服力。4.3.1 一个容易被面试官追问的细节为什么要用乐观锁乐观锁的本质是“更新时带上旧值作为条件”数据库行锁会挡住第二个事务直到第一个提交。由于条件不满足第二个事务影响行数为 0业务就知道重试或提示用户。回答这个问题时把“读已提交”隔离级别、唯一索引和乐观更新三者串起来就已经超过大部分 java 八股文准备了。5. 课设收尾时值得加的定时任务、导出和验证5.1 月底自动汇总考勤生成工资草稿在 Spring Boot 里用EnableScheduling开启定时任务在生成工资的方法上标注Scheduled(cron 0 0 1 1 * ?)这样每个月 1 日凌晨 1 点自动跑一次。计算金额时保留两位小数用BigDecimal而不是double。接口里再加一个“试算”功能让管理员在月底前预览工资单便于提前核对工时。Scheduled(cron 0 0 1 1 * ?) Transactional(rollbackFor Exception.class) public void autoGenerateSalary() { String month LocalDate.now().minusMonths(1).format(DateTimeFormatter.ofPattern(yyyyMM)); generateSalary(month); }这个 cron 表达式从左到右是“秒 分 时 日 月 星期”所以0 0 1 1 * ?表示每月 1 日 1 点 0 分 0 秒执行。?用在星期位上表示不指定具体星期避免与“每月 1 日”冲突。5.2 考勤导出留一个 Excel 按钮管理员页面最常用的是“导出本月工时明细”。用 EasyExcel 写一个导出接口把AttendanceRecord转成 VO输出 Excel 到响应流。这样比手写 CSV 更专业也能在文档里体现“数据可视化”的一角。GetMapping(/export/attendance) public void exportAttendance(RequestParam String month, HttpServletResponse response) throws IOException { ListAttendanceVO list attendanceService.listConfirmedByMonth(month); response.setContentType(application/vnd.ms-excel); response.setCharacterEncoding(utf-8); EasyExcel.write(response.getOutputStream(), AttendanceVO.class) .sheet(考勤明细).doWrite(list); }注意response.setContentType要放在获取输出流之前否则 Excel 文件头会缺少 MIME 类型浏览器可能把它当成普通文本下载。5.3 把测试结果当文档证据答辩时可以说用 JUnit 写了 3 个用例覆盖重复申请、并发审核和工资重复生成用 JMeter 对登录接口压测 100 并发平均响应时间在 150ms 以内。这些数据需要先跑一遍再写不要拍脑袋。最后可以在系统初始化时插入一组演示数据方便评委直接登录查看。具体账号密码写在 SQL 初始化脚本注释里即可。本文还有配套的精品资源点击获取