毕设实战:基于Spring Boot的教师工作量管理系统,从零到一避坑全攻略!

39 阅读11分钟

毕设实战:基于Spring Boot的教师工作量管理系统,从零到一避坑全攻略!

家人们谁懂啊!做教师工作量管理系统毕设时,光教师表和工资表的数据关联就卡了整整3天——一开始没做数据校验,教师工资金额计算全错,导师看了直摇头说“这系统能用吗”😫。后来熬夜改代码才总结出这套实战经验,今天把需求、技术、实现到测试的细节全公开,帮你轻松搞定毕设!

一、先搞懂“教师工作量系统要啥”!需求分析别跑偏

刚开始我直接写代码,花了两周做了个“教师社交圈”功能,结果导师一句“核心是工作量统计和工资计算,不是社交”直接打回重做!后来才明白,需求分析要先抓住“谁用系统、要干啥”,这步做对了,后面能少走90%弯路。

1. 核心用户&功能拆解(实践总结版)

教师工作量管理系统主要有两类核心用户:管理员教师用户(别乱加“学生角色”!我当初加了后,权限逻辑全乱了):

  • 管理员端(核心功能):

    • 教师管理:添加教师信息、管理教师档案(支持Excel批量导入,我当初没加,手动录入50个教师信息到手酸)
    • 工作量管理:审核教师提交的工作量、设置工作量类型(教学/科研/行政等)、计算总工作量
    • 工资管理:生成工资单、计算实发工资(底薪+奖金-五险一金)、导出工资报表
    • 公告通知:发布学校通知、管理公告信息(加“紧急程度”标记,重要通知置顶)
    • 打卡管理:查看教师打卡记录、统计出勤情况(支持按月筛选)
  • 教师端(重要功能):

    • 个人信息管理:维护个人资料、上传头像(支持裁剪,我当初没加,图片变形严重)
    • 工作量申报:提交工作量记录、查看审核状态(用流程图展示:提交→待审核→已通过/退回)
    • 工资查询:查看个人工资明细、下载工资条(显示详细构成:底薪+奖金-五险一金=实发)
    • 打卡签到:每日打卡、查看打卡记录(支持定位打卡,防止代打卡)
    • 通知查看:查看学校通知、标记已读(重要通知红点提醒)

2. 需求分析避坑指南(血泪教训!)

  • 别空想需求!找几位老师模拟使用提意见:有老师说“想快速查看本月工资”,我才加了“工资概览”卡片
  • 一定要画用例图!用DrawIO画简洁版,标清“管理员-审核工作量”“教师-提交工作量”,汇报时直观明了
  • 写需求文档!不用复杂,把核心功能点写清楚: 1. 教师每月工作量申报(教学+科研+行政) 2. 管理员审核并计算总工作量 3. 系统自动计算工资(底薪+工作量奖金-五险一金) 4. 教师可查看工资明细和打卡记录

3. 可行性分析要实在!3点说清就过关

导师最爱问“你这系统可行吗”,从3个角度写:

  • 技术可行性:Spring Boot + MySQL + Vue.js,技术成熟,学习资料多
  • 经济可行性:开发工具全免费,部署到学校服务器成本低
  • 操作可行性:界面简洁,老师上手快,培训成本低

二、技术选型要靠谱!这套组合稳得很

技术工具选择理由避坑提醒!
Spring Boot 2.7快速开发,配置简单别用最新版,2.7.x最稳定
Vue.js 2.x渐进式框架,学习曲线平缓按需引入Element-UI组件
MySQL 8.0事务支持好,适合财务数据一定设utf8mb4编码!
Element-UI组件丰富,开发效率高注意浏览器兼容性
ECharts数据可视化,展示报表轻量级,渲染快

开发环境一键配置

# 1. 安装JDK 1.8
# 2. 安装Node.js 14+
# 3. 安装MySQL 8.0
# 4. IDEA创建Spring Boot项目
# 5. Vue CLI创建前端项目

三、数据库设计:表结构要合理

我当初没设计好“工作量-工资”关联,计算工资要手动写SQL,调试到凌晨😫。

1. 核心表结构设计

-- 教师表(核心)
CREATE TABLE `jiaoshi` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `jiaoshi_name` VARCHAR(50) NOT NULL COMMENT '教师姓名',
  `jiaoshi_phone` VARCHAR(20) COMMENT '手机号',
  `jiaoshi_id_number` VARCHAR(18) COMMENT '身份证号',
  `jiaoshi_photo` VARCHAR(500) COMMENT '头像路径',
  `dixin_money` DECIMAL(10,2) DEFAULT 5000.00 COMMENT '底薪',
  `create_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_phone` (`jiaoshi_phone`),
  UNIQUE KEY `uk_id_number` (`jiaoshi_id_number`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 工作量表(重点)
CREATE TABLE `gongzuoliang` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `jiaoshi_id` INT NOT NULL COMMENT '教师ID',
  `gongzuoliang_name` VARCHAR(100) COMMENT '工作标题',
  `gongzuoliang_types` INT COMMENT '类型:1教学/2科研/3行政',
  `gongzuoliang_number` DECIMAL(10,2) COMMENT '工作量数值',
  `gongzuoliang_date` DATE COMMENT '工作日期',
  `status` INT DEFAULT 0 COMMENT '状态:0待审核/1通过/2退回',
  `create_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `fk_jiaoshi` (`jiaoshi_id`),
  KEY `idx_date` (`gongzuoliang_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 工资表(核心财务)
CREATE TABLE `gongzi` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `jiaoshi_id` INT NOT NULL,
  `year_month` VARCHAR(7) COMMENT '年月,格式:2024-01',
  `dixin_money` DECIMAL(10,2) COMMENT '底薪',
  `jiangjin_money` DECIMAL(10,2) COMMENT '奖金',
  `wuxianyijin_money` DECIMAL(10,2) COMMENT '五险一金',
  `shifa_money` DECIMAL(10,2) COMMENT '实发工资',
  `is_paid` INT DEFAULT 0 COMMENT '是否已发放:0未发/1已发',
  `create_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_teacher_month` (`jiaoshi_id`, `year_month`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

2. 关键关联查询

-- 查询教师某月工资明细
SELECT 
    j.jiaoshi_name,
    g.year_month,
    g.dixin_money,
    g.jiangjin_money,
    g.wuxianyijin_money,
    g.shifa_money,
    g.is_paid
FROM gongzi g
JOIN jiaoshi j ON g.jiaoshi_id = j.id
WHERE g.jiaoshi_id = 1 
  AND g.year_month = '2024-01';

-- 统计教师月度工作量
SELECT 
    j.jiaoshi_name,
    SUM(CASE WHEN gz.gongzuoliang_types = 1 THEN gz.gongzuoliang_number ELSE 0 END) as jiaoxue,
    SUM(CASE WHEN gz.gongzuoliang_types = 2 THEN gz.gongzuoliang_number ELSE 0 END) as keyan,
    SUM(CASE WHEN gz.gongzuoliang_types = 3 THEN gz.gongzuoliang_number ELSE 0 END) as xingzheng,
    SUM(gz.gongzuoliang_number) as total
FROM gongzuoliang gz
JOIN jiaoshi j ON gz.jiaoshi_id = j.id
WHERE DATE_FORMAT(gz.gongzuoliang_date, '%Y-%m') = '2024-01'
  AND gz.status = 1
GROUP BY j.id;

四、核心功能实现(代码+页面)

1. 工作量申报模块(教师端核心)

Spring Boot后端Controller
@RestController
@RequestMapping("/api/workload")
public class WorkloadController {
    
    @Autowired
    private WorkloadService workloadService;
    
    /**
     * 教师提交工作量
     */
    @PostMapping("/submit")
    public Result submitWorkload(@RequestBody WorkloadDTO dto, 
                                HttpServletRequest request) {
        try {
            // 1. 获取当前登录教师
            Integer teacherId = (Integer) request.getSession().getAttribute("teacherId");
            if (teacherId == null) {
                return Result.error("请先登录");
            }
            
            // 2. 数据校验
            if (dto.getWorkloadNumber() <= 0) {
                return Result.error("工作量必须大于0");
            }
            
            // 3. 提交工作量
            workloadService.submitWorkload(teacherId, dto);
            
            return Result.success("工作量提交成功,等待审核");
        } catch (Exception e) {
            return Result.error("提交失败:" + e.getMessage());
        }
    }
    
    /**
     * 查询教师工作量统计
     */
    @GetMapping("/statistics/{yearMonth}")
    public Result getWorkloadStatistics(@PathVariable String yearMonth,
                                       HttpServletRequest request) {
        Integer teacherId = (Integer) request.getSession().getAttribute("teacherId");
        
        Map<String, Object> statistics = workloadService.getMonthlyStatistics(teacherId, yearMonth);
        
        return Result.success("查询成功", statistics);
    }
}

@Service
@Transactional
public class WorkloadService {
    
    @Autowired
    private WorkloadMapper workloadMapper;
    
    /**
     * 提交工作量(带业务逻辑)
     */
    public void submitWorkload(Integer teacherId, WorkloadDTO dto) {
        // 校验是否重复提交(同一天同类型)
        QueryWrapper<Workload> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("jiaoshi_id", teacherId)
                   .eq("gongzuoliang_types", dto.getWorkloadType())
                   .eq("gongzuoliang_date", dto.getWorkDate());
        
        if (workloadMapper.selectCount(queryWrapper) > 0) {
            throw new RuntimeException("该类型工作量今日已提交");
        }
        
        // 创建工作量记录
        Workload workload = new Workload();
        workload.setJiaoshiId(teacherId);
        workload.setGongzuoliangName(dto.getWorkloadName());
        workload.setGongzuoliangTypes(dto.getWorkloadType());
        workload.setGongzuoliangNumber(dto.getWorkloadNumber());
        workload.setGongzuoliangDate(dto.getWorkDate());
        workload.setStatus(0); // 待审核
        
        workloadMapper.insert(workload);
        
        // 记录操作日志
        logService.addLog(teacherId, "提交工作量", 
                         String.format("提交%s工作量%.2f", 
                                     getTypeName(dto.getWorkloadType()), 
                                     dto.getWorkloadNumber()));
    }
    
    /**
     * 获取月度统计(用于工资计算)
     */
    public Map<String, Object> getMonthlyStatistics(Integer teacherId, String yearMonth) {
        Map<String, Object> result = new HashMap<>();
        
        // 教学工作量
        QueryWrapper<Workload> teachingQuery = new QueryWrapper<>();
        teachingQuery.eq("jiaoshi_id", teacherId)
                    .eq("gongzuoliang_types", 1) // 教学
                    .eq("status", 1) // 已通过
                    .apply("DATE_FORMAT(gongzuoliang_date, '%Y-%m') = {0}", yearMonth);
        
        BigDecimal teachingTotal = workloadMapper.selectList(teachingQuery)
                .stream()
                .map(Workload::getGongzuoliangNumber)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        
        // 科研工作量
        QueryWrapper<Workload> researchQuery = new QueryWrapper<>();
        researchQuery.eq("jiaoshi_id", teacherId)
                    .eq("gongzuoliang_types", 2) // 科研
                    .eq("status", 1)
                    .apply("DATE_FORMAT(gongzuoliang_date, '%Y-%m') = {0}", yearMonth);
        
        BigDecimal researchTotal = workloadMapper.selectList(researchQuery)
                .stream()
                .map(Workload::getGongzuoliangNumber)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        
        result.put("teaching", teachingTotal);
        result.put("research", researchTotal);
        result.put("total", teachingTotal.add(researchTotal));
        
        return result;
    }
}

2. 工资计算模块(管理员端核心)

Spring Boot工资计算Service
@Service
@Transactional
public class SalaryService {
    
    @Autowired
    private SalaryMapper salaryMapper;
    
    @Autowired
    private TeacherMapper teacherMapper;
    
    @Autowired
    private WorkloadService workloadService;
    
    /**
     * 计算并生成工资单
     */
    public Result generateSalary(String yearMonth) {
        // 1. 验证月份格式
        if (!isValidYearMonth(yearMonth)) {
            return Result.error("月份格式错误,应为YYYY-MM");
        }
        
        // 2. 检查是否已生成
        QueryWrapper<Salary> checkWrapper = new QueryWrapper<>();
        checkWrapper.eq("year_month", yearMonth);
        
        if (salaryMapper.selectCount(checkWrapper) > 0) {
            return Result.error("该月工资单已生成");
        }
        
        // 3. 获取所有教师
        List<Teacher> teachers = teacherMapper.selectList(null);
        
        // 4. 为每个教师计算工资
        List<Salary> salaries = new ArrayList<>();
        
        for (Teacher teacher : teachers) {
            Salary salary = calculateTeacherSalary(teacher, yearMonth);
            salaries.add(salary);
        }
        
        // 5. 批量保存
        if (!salaries.isEmpty()) {
            salaryMapper.batchInsert(salaries);
        }
        
        return Result.success("工资单生成成功", salaries.size());
    }
    
    /**
     * 计算单个教师工资
     */
    private Salary calculateTeacherSalary(Teacher teacher, String yearMonth) {
        Salary salary = new Salary();
        salary.setJiaoshiId(teacher.getId());
        salary.setYearMonth(yearMonth);
        
        // 底薪
        BigDecimal baseSalary = teacher.getDixinMoney();
        salary.setDixinMoney(baseSalary);
        
        // 获取工作量统计
        Map<String, Object> statistics = workloadService.getMonthlyStatistics(
            teacher.getId(), yearMonth);
        
        BigDecimal teachingTotal = (BigDecimal) statistics.get("teaching");
        BigDecimal researchTotal = (BigDecimal) statistics.get("research");
        
        // 计算奖金(教学每课时100元,科研每分50元)
        BigDecimal teachingBonus = teachingTotal.multiply(new BigDecimal("100"));
        BigDecimal researchBonus = researchTotal.multiply(new BigDecimal("50"));
        BigDecimal totalBonus = teachingBonus.add(researchBonus);
        
        salary.setJiangjinMoney(totalBonus);
        
        // 五险一金(底薪的22%)
        BigDecimal insurance = baseSalary.multiply(new BigDecimal("0.22"));
        salary.setWuxianyijinMoney(insurance);
        
        // 实发工资 = 底薪 + 奖金 - 五险一金
        BigDecimal actualSalary = baseSalary.add(totalBonus).subtract(insurance);
        salary.setShifaMoney(actualSalary);
        
        salary.setIsPaid(0); // 未发放
        
        return salary;
    }
    
    /**
     * 导出工资报表
     */
    public void exportSalaryReport(String yearMonth, HttpServletResponse response) {
        List<Salary> salaries = salaryMapper.selectByMonth(yearMonth);
        
        try {
            // 使用EasyExcel导出
            String fileName = URLEncoder.encode(
                String.format("工资报表_%s.xlsx", yearMonth), "UTF-8");
            
            response.setContentType("application/vnd.ms-excel");
            response.setCharacterEncoding("utf-8");
            response.setHeader("Content-disposition", 
                             "attachment;filename=" + fileName);
            
            // 导出逻辑
            EasyExcel.write(response.getOutputStream(), SalaryExportVO.class)
                    .sheet("工资报表")
                    .doWrite(convertToExportVO(salaries));
                    
        } catch (Exception e) {
            throw new RuntimeException("导出失败", e);
        }
    }
}

在这里插入图片描述 在这里插入图片描述 在这里插入图片描述

五、系统测试要全面!

1. 功能测试用例

表1:工作量申报测试
测试场景操作步骤预期结果实际结果
正常申报教师登录→填写工作量→提交申报成功,状态为"待审核"
重复申报同一天同一类型重复提交提示"该类型工作量今日已提交"
数据验证工作量输入负数或0提示"工作量必须大于0"
表2:工资计算测试
测试场景测试数据预期结果实际结果
正常计算底薪5000+教学10课时+科研20分实发=5000+1000+1000-1100=5900
空数据教师无工作量记录实发=底薪-五险一金
批量生成生成全教师某月工资所有教师工资单生成成功
表3:权限测试
测试场景操作角色预期结果实际结果
教师查工资教师A登录只能查看自己工资
管理员工资管理员登录可查看所有教师工资
未登录访问直接访问工资页跳转到登录页面

2. 性能测试

// 使用JMeter测试脚本模拟并发
// 1. 100个教师同时申报工作量
// 2. 管理员批量生成200个教师工资
// 3. 导出大数据量Excel报表

3. 测试报告模板

## 教师工作量管理系统测试报告

### 一、测试概述
- 测试时间:2024年1月
- 测试环境:Windows 10 + Chrome 120
- 测试人员:XXX

### 二、测试结果
1. 功能测试:通过率96%
   - 工作量申报:通过
   - 工资计算:通过  
   - 权限控制:通过
   
2. 性能测试:响应时间<2秒
   - 并发申报:支持50人同时操作
   - 数据导出:1000条记录<5秒

### 三、发现问题
1. 工资计算精度问题:已修复,使用BigDecimal
2. 文件上传大小限制:已设置为10MB
3. 日期选择范围:限制只能选当前及之前月份

### 四、测试结论
系统功能完整,性能稳定,满足毕业设计要求。

六、答辩准备:3个加分技巧

  1. 演示要专业

    • 按"教师申报→管理员审核→工资计算→报表导出"完整流程演示
    • 准备测试数据:5个教师,3个月的工作量记录
    • 展示核心功能:工资自动计算、Excel导出、数据可视化
  2. 突出技术难点

    • "我解决了工资计算的精度问题,使用BigDecimal避免浮点数误差"
    • "实现了工作量的自动审核规则,减少管理员工作量"
    • "使用ECharts实现了工作量统计可视化"
  3. 准备常见问题

    • Q:为什么选择Spring Boot?
    • A:快速开发,生态成熟,适合中小型管理系统
    • Q:数据安全如何保证?
    • A:密码加密存储、权限控制、操作日志记录
    • Q:系统如何扩展?
    • A:模块化设计,可轻松添加考勤、绩效等模块

最后:真心建议

  1. 代码管理:一定要用Git!每天提交代码,写好commit信息
  2. 文档完整:除了论文,还要写技术文档、用户手册
  3. 数据备份:数据库定时备份,测试数据要清理干净
  4. 提前演示:找同学帮忙测试,修复bug后再给导师看

需要完整源码数据库脚本部署教程的同学,可以在评论区留言。遇到具体问题(如Spring Boot配置、Vue路由等)也可以问我。

祝大家毕设顺利,答辩一次过!🎓


小贴士:答辩时带个U盘,里面存系统演示视频、源码、论文PDF,以防现场网络不好!