毕业设计实战:基于SpringBoot的入校申报审批系统,从需求到部署避坑全指南

46 阅读10分钟

毕业设计实战:基于SpringBoot的入校申报审批系统,从需求到部署避坑全指南

当初做入校申报审批系统时,我在“健康码、行程码双码上传校验”功能上卡了整整三天——一开始没做文件格式和大小限制,结果用户传了个100MB的视频文件,服务器直接崩了,导师看了直摇头😫 后来踩了无数坑,终于总结出这套完整开发流程。今天就把入校申报审批系统的实战经验全部分享出来,宝子们跟着做,毕设稳过!

一、需求分析别想当然!先搞懂“谁申请,谁审批”

最开始我以为做个简单的表单提交就行了,结果导师说“要考虑疫情常态化管理,要有健康检查流程”。后来才明白,入校申报系统的核心是 “用户申报-管理员审批-门卫核验” 的三级流程,必须抓住这三个环节的核心需求。

1. 核心用户 & 核心功能(踩坑后总结版)

入校申报审批系统有三类核心用户:普通用户(申报人)、管理员(审批人)、门卫(核验人)。千万别把“辅导员”、“院系领导”都加进去!我当初加了,审批流程变得极其复杂,最后简化成三级才顺畅。

  • 用户端(申报人,必须做的功能):

    • 个人信息管理:维护姓名、身份证号、联系方式、头像等基本信息。
    • 入校申报:这是核心中的核心
      • 填写入校时间、出校时间、入校事由。
      • 选择人员身份(学生、教职工、访客等)。
      • 提交后生成唯一的申报编号。
    • 申报记录查询:查看所有申报记录及审批状态。
    • 公告查看:浏览学校最新防疫政策和通知。
  • 管理员端(审批人,核心功能):

    • 申报审批
      • 查看待审批的入校申报列表。
      • 审核申报信息,可“通过”或“驳回”。
      • 驳回必须填写理由(这个很重要!)。
    • 用户管理:管理所有用户账号,可冻结异常账号。
    • 入校检查记录管理:查看所有入校检查记录。
    • 公告管理:发布、编辑、删除防疫公告。
  • 门卫端(核验人,简化但必要):

    • 入校检查登记
      • 扫描申报编号或输入身份证号查询申报信息。
      • 登记体温、上传健康码和行程码截图。
      • 记录是否去过风险地区。
    • 今日入校统计:查看当天已入校人员列表。

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

  • 别空想流程,要画出来!用流程图工具画出完整的“申报-审批-入校检查”流程。我当初画出来后才发现,少了“驳回后用户重新提交”的环节,赶紧补上。
  • 一定要考虑异常情况
    • 用户填的出校时间比入校时间还早怎么办?(前端要做时间校验)
    • 健康码截图上传了假图怎么办?(虽然不能100%防伪,但可以做文件MD5校验)
    • 审批人长时间不审批怎么办?(可以加个“催办”功能,或者自动提醒)
  • 写清楚约束条件
    • “入校时间必须至少提前2小时申报”
    • “健康码必须为24小时内”
    • “体温超过37.3℃自动标记为异常”
    • “同一用户同一天只能申报一次入校”

3. 可行性分析(三句话说清楚)

  • 技术可行性:SpringBoot + MySQL + Vue,都是成熟技术。健康码识别可以用简单的颜色判断(绿码/黄码/红码),不需要复杂的AI识别。
  • 经济可行性:所有工具免费,部署到学校服务器或学生云服务器(学生优惠)成本极低。
  • 操作可行性:用户扫码就能申报,门卫用手机或平板就能核验,操作简单。

二、技术选型:SpringBoot是真香!

当初我看别人用传统的SSM,配置一堆XML文件。后来选择了 SpringBoot 2.7 + MyBatis-Plus + Vue 2 + Element UI ,开发效率提升了不止一倍!

技术栈详解与避坑

技术选择理由避坑提醒
SpringBoot 2.7.x自动配置,内嵌Tomcat,快速启动。别用3.x,部分依赖还没适配好。
MyBatis-Plus强大的CRUD操作,代码生成器好用。好好用它的LambdaQueryWrapper,写查询条件超方便。
Vue 2 + Element UI组件丰富,做管理后台界面很快。Vue 2够用了,别追求Vue 3增加学习成本。
MySQL 8.0JSON字段支持,存健康码路径方便。一定用utf8mb4字符集,否则Emoji表情会乱码。
Redis(可选)缓存申报编号,提高查询速度。如果数据量不大,可以不用,简化部署。

开发环境一步到位

# 1. 用Spring Initializr创建项目
# 勾选:Web、MyBatis、MySQL、Redis(可选)

# 2. 配置application.yml
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/ruxiao_system?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
  servlet:
    multipart:
      max-file-size: 10MB  # 限制上传文件大小
      max-request-size: 10MB

# 3. 集成MyBatis-Plus
mybatis-plus:
  mapper-locations: classpath:mapper/*.xml
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

三、数据库设计:状态流转是关键!

我当初的坑:把审批状态和检查状态混在一个表里,结果逻辑混乱。后来分成了入校申报表(管审批)和入校检查表(管核验),清晰多了。

核心表结构设计(重点!)

-- 入校申报表(核心表)
CREATE TABLE `ruxiaoshenbao` (
  `id` int NOT NULL AUTO_INCREMENT,
  `ruxiaoshenbao_uuid_number` varchar(50) NOT NULL COMMENT '申报编号:RX+年月日+6位随机数',
  `yonghu_id` int NOT NULL COMMENT '申报用户',
  `zhuanye_types` int DEFAULT 1 COMMENT '人员身份:1学生,2教职工,3访客',
  `shiyou` text COMMENT '入校事由',
  `ruxiaoshenbao_time` datetime NOT NULL COMMENT '计划入校时间',
  `cuxiao_time` datetime COMMENT '计划出校时间',
  `ruxiaoshenbao_yesno_types` int DEFAULT 1 COMMENT '审批状态:1待审批,2通过,3驳回',
  `ruxiaoshenbao_yesno_text` text COMMENT '审批意见',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_uuid` (`ruxiaoshenbao_uuid_number`),
  KEY `idx_user` (`yonghu_id`),
  KEY `idx_time` (`ruxiaoshenbao_time`)  -- 按时间查询加索引
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='入校申报表';

-- 入校检查表(核验记录)
CREATE TABLE `ruxiaojiancha` (
  `id` int NOT NULL AUTO_INCREMENT,
  `ruxiaoshenbao_id` int NOT NULL COMMENT '关联的申报记录',
  `tiwen` decimal(3,1) COMMENT '体温',
  `ruxiaojiancha_photo` varchar(500) COMMENT '健康码图片路径',
  `xingcheng_photo` varchar(500) COMMENT '行程码图片路径',
  `jiancha_result` int DEFAULT 1 COMMENT '检查结果:1正常,2异常',
  `ruxiaojiancha_content` text COMMENT '检查详情',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '检查时间',
  PRIMARY KEY (`id`),
  KEY `fk_shenbao` (`ruxiaoshenbao_id`),
  CONSTRAINT `fk_shenbao` FOREIGN KEY (`ruxiaoshenbao_id`) REFERENCES `ruxiaoshenbao` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='入校检查表';

-- 用户表
CREATE TABLE `yonghu` (
  `id` int NOT NULL AUTO_INCREMENT,
  `yonghu_name` varchar(100) NOT NULL COMMENT '姓名',
  `yonghu_phone` varchar(20) NOT NULL COMMENT '手机号',
  `yonghu_id_number` varchar(18) NOT NULL COMMENT '身份证号',
  `xueyuan` varchar(100) COMMENT '学院/部门',
  `banji` varchar(50) COMMENT '班级',
  `yonghu_delete` int DEFAULT 0 COMMENT '0正常,1已删除',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_phone` (`yonghu_phone`),
  UNIQUE KEY `uk_idcard` (`yonghu_id_number`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

设计亮点

  1. 申报编号生成规则RX20240520001234(RX+年月日+6位随机数),唯一且有意义。
  2. 状态分离:审批状态在申报表,检查结果在检查表,逻辑清晰。
  3. 索引优化:给常用的查询字段加索引,提高查询速度。

四、功能实现:抓住核心流程,做出亮点

1. 用户端:入校申报(核心体验)

关键逻辑:时间校验、重复申报校验、生成唯一编号。

前端实现要点(Vue + Element UI)

<template>
  <div class="shenbao-form">
    <el-form :model="form" :rules="rules" ref="formRef">
      <el-form-item label="计划入校时间" prop="ruxiaoshenbao_time">
        <el-date-picker
          v-model="form.ruxiaoshenbao_time"
          type="datetime"
          :picker-options="timeOptions"
          placeholder="选择入校时间"
        />
        <div class="tip">需至少提前2小时申报</div>
      </el-form-item>
      
      <el-form-item label="人员身份" prop="zhuanye_types">
        <el-select v-model="form.zhuanye_types" placeholder="请选择">
          <el-option label="学生" :value="1"></el-option>
          <el-option label="教职工" :value="2"></el-option>
          <el-option label="访客" :value="3"></el-option>
        </el-select>
      </el-form-item>
      
      <el-form-item label="入校事由" prop="shiyou">
        <el-input
          type="textarea"
          v-model="form.shiyou"
          :rows="4"
          placeholder="请详细说明入校事由"
          maxlength="500"
          show-word-limit
        />
      </el-form-item>
      
      <el-form-item>
        <el-button type="primary" @click="submitForm" :loading="submitting">
          提交申报
        </el-button>
      </el-form-item>
    </el-form>
  </div>
</template>

<script>
export default {
  data() {
    // 时间校验:只能选择未来时间,且至少提前2小时
    const validateTime = (rule, value, callback) => {
      if (!value) {
        callback(new Error('请选择入校时间'));
      }
      const now = new Date();
      const twoHoursLater = new Date(now.getTime() + 2 * 60 * 60 * 1000);
      if (value < twoHoursLater) {
        callback(new Error('入校时间需至少提前2小时'));
      }
      callback();
    };
    
    return {
      form: {
        ruxiaoshenbao_time: '',
        zhuanye_types: '',
        shiyou: ''
      },
      rules: {
        ruxiaoshenbao_time: [
          { required: true, validator: validateTime, trigger: 'change' }
        ],
        zhuanye_types: [
          { required: true, message: '请选择人员身份', trigger: 'change' }
        ],
        shiyou: [
          { required: true, message: '请输入入校事由', trigger: 'blur' },
          { min: 10, message: '事由描述至少10个字', trigger: 'blur' }
        ]
      },
      timeOptions: {
        disabledDate(time) {
          // 不能选择今天之前的日期
          return time.getTime() < Date.now() - 24 * 60 * 60 * 1000;
        }
      },
      submitting: false
    };
  },
  methods: {
    async submitForm() {
      try {
        await this.$refs.formRef.validate();
        this.submitting = true;
        
        // 检查今天是否已申报过
        const hasToday = await this.checkTodayApplication();
        if (hasToday) {
          this.$message.warning('您今天已提交过入校申报,请勿重复提交');
          return;
        }
        
        // 提交申报
        const res = await this.$api.shenbao.submit(this.form);
        this.$message.success(`申报成功!您的申报编号:${res.data.uuidNumber}`);
        this.$router.push('/my-applications');
      } catch (error) {
        console.error('提交失败:', error);
      } finally {
        this.submitting = false;
      }
    },
    
    async checkTodayApplication() {
      const today = new Date().toISOString().split('T')[0];
      const res = await this.$api.shenbao.checkToday({ date: today });
      return res.data.hasApplication;
    }
  }
};
</script>

后端关键代码(SpringBoot)

@Service
public class RuxiaoshenbaoServiceImpl implements RuxiaoshenbaoService {
    
    @Autowired
    private RuxiaoshenbaoMapper ruxiaoshenbaoMapper;
    
    @Transactional
    @Override
    public Result submitShenbao(RuxiaoshenbaoForm form, Integer userId) {
        // 1. 检查今天是否已申报
        LocalDate today = LocalDate.now();
        LambdaQueryWrapper<Ruxiaoshenbao> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(Ruxiaoshenbao::getYonghuId, userId)
               .ge(Ruxiaoshenbao::getCreateTime, today.atStartOfDay())
               .lt(Ruxiaoshenbao::getCreateTime, today.plusDays(1).atStartOfDay());
        long count = this.count(wrapper);
        if (count > 0) {
            return Result.error("您今天已提交过入校申报");
        }
        
        // 2. 生成唯一编号
        String uuidNumber = generateUuidNumber();
        
        // 3. 保存申报记录
        Ruxiaoshenbao entity = new Ruxiaoshenbao();
        BeanUtils.copyProperties(form, entity);
        entity.setYonghuId(userId);
        entity.setRuxiaoshenbaoUuidNumber(uuidNumber);
        entity.setRuxiaoshenbaoYesnoTypes(1); // 待审批
        
        this.save(entity);
        
        // 4. 记录操作日志(可选)
        logService.addLog(userId, "提交入校申报", "编号:" + uuidNumber);
        
        return Result.success("申报提交成功", uuidNumber);
    }
    
    private String generateUuidNumber() {
        // RX + 年月日 + 6位随机数
        String dateStr = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
        String randomStr = String.format("%06d", new Random().nextInt(999999));
        return "RX" + dateStr + randomStr;
    }
}

2. 管理员端:申报审批(核心业务)

关键逻辑:批量审批、驳回必须填理由、审批记录可追溯。

审批页面设计要点

  • 待审批列表:表格显示所有待审批记录,可多选批量审批。
  • 审批操作弹窗
    • 显示申报详情(用户信息、入校时间、事由)。
    • 单选框:“通过”或“驳回”。
    • 文本域:“审批意见”(驳回时必须填写)。
    • “提交审批”按钮。
  • 审批历史:可查看每条申报的审批记录。

批量审批后端代码

@PostMapping("/batchApprove")
@ResponseBody
@Transactional
public Result batchApprove(@RequestBody BatchApproveRequest request) {
    // request包含:申报ID列表、审批结果、审批意见
    
    if (CollectionUtils.isEmpty(request.getShenbaoIds())) {
        return Result.error("请选择要审批的申报");
    }
    
    if (request.getResult() == 3 && StringUtils.isBlank(request.getReason())) {
        return Result.error("驳回必须填写理由");
    }
    
    List<Ruxiaoshenbao> updateList = new ArrayList<>();
    for (Integer shenbaoId : request.getShenbaoIds()) {
        Ruxiaoshenbao shenbao = ruxiaoshenbaoService.getById(shenbaoId);
        if (shenbao != null && shenbao.getRuxiaoshenbaoYesnoTypes() == 1) {
            shenbao.setRuxiaoshenbaoYesnoTypes(request.getResult());
            shenbao.setRuxiaoshenbaoYesnoText(request.getReason());
            updateList.add(shenbao);
            
            // 发送审批结果通知(微信/短信)
            noticeService.sendApproveResult(shenbao.getYonghuId(), request.getResult());
        }
    }
    
    if (!updateList.isEmpty()) {
        ruxiaoshenbaoService.updateBatchById(updateList);
    }
    
    return Result.success("批量审批完成");
}

3. 门卫端:入校检查(移动端友好)

关键逻辑:扫码核验、双码上传、体温异常预警。

移动端核验页面

<!-- 简洁的核验页面,适合手机/PAD操作 -->
<div class="check-page">
  <div class="scan-section">
    <button @click="startScan">扫码核验</button>
    <div class="or"></div>
    <input v-model="searchText" placeholder="输入申报编号/身份证号">
    <button @click="search">查询</button>
  </div>
  
  <div v-if="shenbaoInfo" class="info-section">
    <h3>{{ shenbaoInfo.yonghuName }}</h3>
    <p>申报编号:{{ shenbaoInfo.uuidNumber }}</p>
    <p>计划入校:{{ shenbaoInfo.planTime }}</p>
    
    <div class="check-form">
      <input type="number" v-model="tiwen" placeholder="测量体温(℃)" step="0.1">
      
      <div class="upload-section">
        <label>健康码:</label>
        <input type="file" accept="image/*" @change="uploadHealthCode">
        <img v-if="healthCodeUrl" :src="healthCodeUrl" class="preview">
      </div>
      
      <div class="upload-section">
        <label>行程码:</label>
        <input type="file" accept="image/*" @change="uploadTravelCode">
        <img v-if="travelCodeUrl" :src="travelCodeUrl" class="preview">
      </div>
      
      <button @click="submitCheck" :disabled="!canSubmit">
        完成核验
      </button>
    </div>
  </div>
</div>

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

五、系统测试:重点测这些场景

核心测试用例

测试场景测试步骤预期结果重要性
重复申报同一用户同一天提交两次申报第二次提示“今天已申报过”
时间校验选择1小时后入校的时间提示“需至少提前2小时”
双码上传上传非图片文件(如PDF)提示“请上传图片文件”
体温异常输入体温37.5℃自动标记为“异常”,需要额外确认
批量审批选择多条记录,批量通过所有选中记录状态更新为“通过”
扫码核验用已过期的申报编号扫码提示“申报已过期”

压力测试(简单做)

用JMeter模拟50个用户同时提交申报,看系统响应时间。目标:95%的请求在2秒内响应。

六、部署与上线

1. 服务器准备

  • 学生优惠:阿里云/腾讯云学生服务器,¥10/月。
  • 配置:1核2G,CentOS 7.6。
  • 必备软件:JDK 1.8、MySQL 8.0、Nginx(反向代理)。

2. 一键部署脚本

#!/bin/bash
# deploy.sh

echo "开始部署入校申报系统..."

# 1. 备份旧版本
if [ -d "/app/ruxiao" ]; then
    mv /app/ruxiao /app/ruxiao_backup_$(date +%Y%m%d)
fi

# 2. 创建新目录
mkdir -p /app/ruxiao
cp target/ruxiao-system.jar /app/ruxiao/

# 3. 复制配置文件
cp application-prod.yml /app/ruxiao/

# 4. 启动应用
cd /app/ruxiao
nohup java -jar ruxiao-system.jar --spring.profiles.active=prod > app.log 2>&1 &

echo "部署完成!"

3. Nginx配置

server {
    listen 80;
    server_name ruxiao.yourschool.edu.cn;
    
    location / {
        proxy_pass http://localhost:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
    
    # 静态文件缓存
    location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
        expires 30d;
        add_header Cache-Control "public, immutable";
    }
}

七、答辩准备:讲好这个故事

  1. 演示流程要完整: “大家好,我演示一个完整的入校申报流程。首先,学生张三登录系统(展示),填写明天上午9点入校的申请,事由是‘实验室做实验’(展示表单校验)。提交后生成申报编号。接着,管理员李老师登录,在待审批列表看到这条申请(展示),审核后通过。最后,门卫王师傅用平板电脑扫描张三的申报二维码(展示移动端页面),登记体温36.5℃,上传双码,完成入校核验。”

  2. 重点讲“你的设计亮点”

    • “我设计了‘申报编号’规则,方便门卫快速核验。”
    • “做了严格的时间校验,必须提前2小时申报,避免临时申请。”
    • “双码上传做了文件类型和大小限制,防止恶意上传。”
    • “体温异常自动预警,需要门卫额外确认。”
  3. 准备好问答

    • Q:如果用户造假怎么办? A:系统不能100%防止造假,但我们可以记录所有操作日志,事后可追溯。另外可以和学校统一身份认证系统对接,提高可信度。
    • Q:数据量大怎么办? A:申报记录可以按月份分表存储,历史数据可以归档。查询时通过索引优化。
    • Q:系统安全性如何? A:所有密码MD5加密存储,接口有防重复提交和SQL注入防护,文件上传做了安全限制。

最后:一点真心话

入校申报系统看起来简单,但要把疫情管理的严谨性和用户体验的便捷性平衡好,需要很多细节考虑。关键是把“申报-审批-核验”这个核心流程做顺畅,把异常情况考虑周全。

需要完整源码数据库脚本部署文档的宝子,可以在评论区留言。

觉得这篇干货有帮助,记得点赞收藏!祝大家毕设顺利,轻松毕业!🎓