毕业设计实战:基于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.0 | JSON字段支持,存健康码路径方便。 | 一定用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='用户表';
设计亮点:
- 申报编号生成规则:
RX20240520001234(RX+年月日+6位随机数),唯一且有意义。 - 状态分离:审批状态在申报表,检查结果在检查表,逻辑清晰。
- 索引优化:给常用的查询字段加索引,提高查询速度。
四、功能实现:抓住核心流程,做出亮点
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";
}
}
七、答辩准备:讲好这个故事
-
演示流程要完整: “大家好,我演示一个完整的入校申报流程。首先,学生张三登录系统(展示),填写明天上午9点入校的申请,事由是‘实验室做实验’(展示表单校验)。提交后生成申报编号。接着,管理员李老师登录,在待审批列表看到这条申请(展示),审核后通过。最后,门卫王师傅用平板电脑扫描张三的申报二维码(展示移动端页面),登记体温36.5℃,上传双码,完成入校核验。”
-
重点讲“你的设计亮点”:
- “我设计了‘申报编号’规则,方便门卫快速核验。”
- “做了严格的时间校验,必须提前2小时申报,避免临时申请。”
- “双码上传做了文件类型和大小限制,防止恶意上传。”
- “体温异常自动预警,需要门卫额外确认。”
-
准备好问答:
- Q:如果用户造假怎么办? A:系统不能100%防止造假,但我们可以记录所有操作日志,事后可追溯。另外可以和学校统一身份认证系统对接,提高可信度。
- Q:数据量大怎么办? A:申报记录可以按月份分表存储,历史数据可以归档。查询时通过索引优化。
- Q:系统安全性如何? A:所有密码MD5加密存储,接口有防重复提交和SQL注入防护,文件上传做了安全限制。
最后:一点真心话
入校申报系统看起来简单,但要把疫情管理的严谨性和用户体验的便捷性平衡好,需要很多细节考虑。关键是把“申报-审批-核验”这个核心流程做顺畅,把异常情况考虑周全。
需要完整源码、数据库脚本、部署文档的宝子,可以在评论区留言。
觉得这篇干货有帮助,记得点赞收藏!祝大家毕设顺利,轻松毕业!🎓