刚做完一套Java高校疫情防控系统的小程序毕设,趁着记忆还热乎,把整个从选题到答辩的全过程写下来。这套系统的完整名称是“计算机毕设 java 高校疫情防控系统小程序 SSM 框架高校疫情防控服务平台 Java 开发的高校疫情防控全流程管理系统”,核心就是用Java + SSM框架写后端接口,用微信小程序做前端,完成一所高校从健康上报、出入审批到异常追踪的闭环管理。
先说说为什么选这个题目。先说句实在话:如果你正在找毕设题目,又希望技术栈足够“正统”、演示起来有东西可讲、数据库关系又不会复杂到一个人搞不定,这个方向确实很合适。原因有三:第一,SSM框架(Spring + Spring MVC + MyBatis)是Java后端岗位面试中绕不开的经典组合,做一遍等于把框架原理、配置流程、事务管理全部过了一遍;第二,小程序端天然就是“移动端应用”的展示载体,答辩时直接在开发者工具里跑起来,比纯网页演示更有冲击力;第三,业务场景是高校管理,流程清晰、角色明确、规则可量化,特别适合用来展现你建表、写接口、做权限控制的基本功。
这篇博文不打算给你抄代码,而是把整个系统的架构思路、数据库设计、关键业务模块的实现逻辑、还有那些你一个人闷头做的时候必然会踩的坑,一条条掰开来讲清楚。不管你最终是打算自己从头写,还是拿着类似的项目二次开发,这篇文章都能帮你少走不少弯路。
1. 为什么选“高校疫情防控”当毕设题:选题逻辑与价值判断
1.1 这个题目到底在考什么
毕设答辩时,老师最常问的一句话就是:“你这个系统解决了什么问题?”你要是答不上来,后面代码写得再花哨也白搭。高校疫情防控系统的核心业务其实非常清晰,就是围绕“人”的流动和“健康状态”做管理。具体拆解下来,无非是这么几件事:
- 学生/教职工需要每天填报健康信息(体温、健康码状态、行程情况);
- 进出校门需要提交申请,由辅导员或院系管理员审批;
- 出现异常情况(体温异常、健康码变色)时,系统要能记录、追踪、上报;
- 管理人员需要查看统计数据,知道今天多少人上报了、多少人没上报、有多少异常。
你把这几个场景在答辩PPT里一摆,老师立刻就能明白你做的不是一个“玩具”,而是一个有实际管理含义的系统。而且这些场景之间天然存在角色差异:学生、教师、辅导员、系统管理员,每个角色看到的页面和能做的操作都不一样,这就逼着你必须做基于角色的权限控制,而权限控制恰恰是Java后端面试里最喜欢问的点。
1.2 从毕设评分标准反推系统功能
大多数学校的毕设评分表上都有这么几栏:选题意义、需求分析、系统设计、功能实现、界面美观度、创新性、文档规范。我当初设计功能时就是按这个评分标准反着推的:
- 选题意义:疫情防控这个切入点在近几年的高校管理里有现实背景,但写文档时注意只谈“日常健康管理”“信息化管理流程”,不要涉及任何政策层面的表述;
- 需求分析:画出用例图,明确学生、教师、辅导员、管理员四类角色各自的用例;
- 系统设计:给出总体架构图、功能模块图、数据库ER图;
- 功能实现:后端接口不少于20个,前端页面不少于10个,必须包含增删改查、状态流转、权限拦截、统计图表这几个要素;
- 界面美观度:小程序端采用微信官方组件库或ColorUI,后台管理页面用AdminLTE或者原生Bootstrap模板;
- 创新性:加一个“异常轨迹追踪”功能,记录异常人员的近期出入记录,这样的亮点在答辩时非常加分。
1.3 什么人适合选这个题
如果你满足以下任意一条,这个题都很值得考虑:
- Java基础一般,想通过一个完整的SSM项目巩固Spring IoC、Spring MVC、MyBatis的使用,而不是一上来就盲目追Spring Boot全家桶;
- 需要快速出成果,小程序端的开发效率高、界面产出快,不太会出现代码写了一堆但界面没法看的情况;
- 想给简历上加一个完整项目经历,这个系统的业务闭环完整,写简历时可以概括为“独立设计并实现一套高校健康管理服务平台,覆盖健康上报、出入审批、异常追踪全流程”。
当然,如果你已经对Spring Boot非常熟,也可以把SSM替换成Spring Boot + MyBatis Plus的组合,但作为一个毕设来说,传统SSM更能体现出你对框架底层配置的理解,特别是Spring配置文件、MyBatis映射文件这些“老底子”的东西,反而能在答辩时讲出深度。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术栈选定与系统架构:SSM框架 + 小程序端,这个组合为什么能打
2.1 为什么不用Spring Boot而选传统SSM
我一度纠结过要不要直接用Spring Boot。Spring Boot的自动配置确实省事,但问题在于:省事意味着你在答辩时“没东西可讲”。老师问“Spring IOC容器是怎么启动的”“Spring MVC的请求流程是怎样的”,你要是只答得出“加了@SpringBootApplication注解”,场面会很尴尬。
传统SSM的好处是“配置即知识”。在你手动编写web.xml、spring-mvc.xml、mybatis-config.xml的过程中,你会被迫搞清楚每一个配置项的作用:DispatcherServlet拦截什么请求、MapperScan扫描什么包、连接池怎么配、事务管理器怎么接。这些东西在你求职面试“SSM框架讲解使用”类问题时,全是加分素材。
所以我最终选用的技术栈是:
| 层次 | 技术选型 | 说明 |
|---|---|---|
| 前端 | 微信小程序原生框架 | 不引入uniapp等跨端框架,直接用WXML + WXSS + JS,避免框架封装带来的黑盒问题 |
| 后端 | Spring + Spring MVC + MyBatis | 经典SSM组合,手动配置,便于答辩讲解 |
| 数据库 | MySQL 5.7 | 体积小、部署快、InnoDB引擎支持事务 |
| 权限控制 | Spring MVC拦截器 + 自定义注解 | 按角色校验登录状态与访问权限 |
| 接口风格 | RESTful API + JSON | 小程序端通过wx.request调用 |
| 项目管理 | Maven | 使用war包部署到Tomcat |
| 开发工具 | IntelliJ IDEA + 微信开发者工具 + Navicat | 一套完整开发链 |
2.2 总体架构:从请求到响应的完整链路
很多第一次做前后端分离项目的同学容易犯一个错:把后端接口写成了“返回页面”,小程序那边拿到HTML直接傻眼。前后端分离的核心逻辑是:小程序端只负责收集数据、展示数据;后端只负责处理业务逻辑、返回JSON数据。
这套系统的请求链路是这样的:
- 用户在微信小程序里提交表单(比如填体温);
- 小程序通过
wx.request向后端接口发送POST请求,数据格式为JSON; - 请求先经过Spring MVC的DispatcherServlet,被拦截器拦下做登录校验;
- 校验通过后,请求进入Controller层,Controller负责接收参数、调用Service层;
- Service层编写业务规则(比如判断体温是否超过37.3℃、判断用户是否已提交过当日上报),必要时通过事务管理器控制数据库操作;
- MyBatis通过Mapper接口操作MySQL数据库,把结果返回给Service层;
- Service层把结果封装为统一的Result对象(含code、message、data三个字段),返回给Controller;
- Controller把Result对象通过Jackson转换成JSON字符串,响应给小程序端;
- 小程序端拿到JSON后,通过
setData更新页面。
这个链路你只要在答辩前对着框图讲一遍,老师基本就能确认你“理解了整个系统是怎么跑的”,而不是只会复制粘贴代码。
2.3 后端代码结构怎么分
我见过不少毕设的代码结构是“一个Controller里写几百行代码,Service层形同虚设”。这种写法在功能上可能没问题,但答辩时老师一看源码就会皱眉。规范的SSM分层应该是这样的:
code复制src/main/java
├── com.example.epidemic
│ ├── controller # 接口层,只做参数接收和结果返回
│ │ ├── UserController.java
│ │ ├── HealthReportController.java
│ │ ├── AccessApplyController.java
│ │ └── StatisticsController.java
│ ├── service # 业务逻辑层,存放核心业务规则
│ │ ├── HealthReportService.java
│ │ ├── AccessApplyService.java
│ │ └── impl
│ │ ├── HealthReportServiceImpl.java
│ │ └── AccessApplyServiceImpl.java
│ ├── mapper # MyBatis接口层,对应XML映射文件
│ │ ├── UserMapper.java
│ │ ├── HealthReportMapper.java
│ │ └── AccessApplyMapper.java
│ ├── entity # 实体类,与数据库表一一对应
│ │ ├── User.java
│ │ ├── HealthReport.java
│ │ └── AccessApply.java
│ ├── dto # 数据传输对象,接收前端参数、封装返回结果
│ │ ├── LoginDTO.java
│ │ └── ReportQueryDTO.java
│ ├── common # 通用类
│ │ ├── Result.java # 统一返回结果
│ │ └── ResultCode.java # 返回状态码枚举
│ ├── interceptor # 拦截器
│ │ ├── LoginInterceptor.java
│ │ └── AuthInterceptor.java
│ └── util # 工具类
│ ├── JwtUtils.java # 生成/解析令牌
│ ├── WxUtils.java # 调用微信接口获取openid
│ └── DateUtils.java
这个结构看起来简单,但它把每个类的职责划分得清清楚楚。Controller只管接参数和返回结果,Service专心写业务规则,Mapper只管数据库操作。这样一来,答辩时老师问你任何一个功能“代码在哪一层实现的”,你都能快速定位,这种“对代码烂熟于心”的状态才是答辩的最佳状态。
3. 数据库设计与核心表结构:一套能跑通“全流程”的数据模型
3.1 表的整体规划
数据库设计是毕设的重中之重,因为一旦表结构定下来了,后面的代码基本就是围着表转。这套系统我一共设计了6张核心表:
| 表名 | 功能说明 |
|---|---|
| t_user | 用户表,存放学生、教师、辅导员、管理员等所有账号 |
| t_health_report | 健康上报表,每人每天一条记录 |
| t_access_apply | 出入申请(审批)表,记录进出校门申请 |
| t_leave_record | 请假/离校记录表,与出入申请关联 |
| t_notice | 通知公告表,管理员发布消息 |
| t_contact_log | 异常轨迹追踪表,记录异常人员的近期行为 |
这6张表各有侧重,互相之间通过外键逻辑关联(实际建表时我保留了外键约束,但在代码里主要通过业务逻辑控制关联)。下面挑几张最核心的表详细说。
3.2 用户表:角色设计是权限控制的地基
sql复制CREATE TABLE `t_user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`student_no` varchar(32) DEFAULT NULL COMMENT '学号/工号',
`password` varchar(64) NOT NULL COMMENT '登录密码(MD5加密存储)',
`real_name` varchar(32) NOT NULL COMMENT '真实姓名',
`college` varchar(64) DEFAULT NULL COMMENT '学院',
`major` varchar(64) DEFAULT NULL COMMENT '专业',
`grade` varchar(16) DEFAULT NULL COMMENT '年级',
`phone` varchar(20) DEFAULT NULL COMMENT '手机号',
`role` tinyint(1) NOT NULL DEFAULT '1' COMMENT '角色: 1-学生, 2-教师, 3-辅导员, 0-管理员',
`openid` varchar(64) DEFAULT NULL COMMENT '微信小程序用户标识',
`status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态: 1-正常, 0-禁用',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_student_no` (`student_no`),
KEY `idx_role` (`role`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
用户表的设计要点有三个:
第一个重点是角色字段用数字枚举而不是字符串。很多新手会用字符串存“学生”“教师”这种中文值,但这会带来两个问题:一是数据库存储冗余,二是在代码里比较角色时要写成if ("学生".equals(role)),一旦字符串不一致就出错。用数字枚举配合常量类,代码里写if (user.getRole() == UserRoleConst.STUDENT),既清晰又安全。
第二个重点是openid字段的用途。小程序登录时,前端调用wx.login()拿到临时code,后端拿着code去微信接口换openid,这个openid就是用户在小程序体系里的“身份证号”。用户第一次在小程序里绑定学号密码时,系统把openid写入该用户记录;之后再次登录时,后端直接根据openid识别用户身份,不需要重复输入账号密码。
第三个重点是密码加密。我用了MD5加盐(salt)的方式,而不是明文存储。具体做法是:注册时生成一个随机盐值,密码存为MD5(password + salt),盐值和加密结果都存入数据库。虽然MD5本身不算安全,但在毕设项目里展示出“我有密码不能明文存储的安全意识”就足够了。答辩时如果老师问加密问题,你可以顺势说“生产环境可以换成BCrypt或SHA-256”。
3.3 健康上报表:唯一索引解决“一天只能上报一次”
sql复制CREATE TABLE `t_health_report` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL COMMENT '用户ID',
`report_date` date NOT NULL COMMENT '上报日期',
`temperature` decimal(4,2) NOT NULL COMMENT '体温',
`health_code` varchar(16) DEFAULT NULL COMMENT '健康码状态',
`is_cough` tinyint(1) DEFAULT '0' COMMENT '是否咳嗽: 0-否, 1-是',
`is_fever` tinyint(1) DEFAULT '0' COMMENT '是否发热: 0-否, 1-是',
`is_contact_risk` tinyint(1) DEFAULT '0' COMMENT '是否接触风险人群: 0-否, 1-是',
`current_location` varchar(128) DEFAULT NULL COMMENT '当前位置',
`remark` varchar(255) DEFAULT NULL COMMENT '备注',
`create_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_date` (`user_id`,`report_date`),
KEY `idx_report_date` (`report_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
这张表最核心的设计就是那个联合唯一索引uk_user_date。它从数据库层面保证了“同一个用户同一天最多只能有一条上报记录”。
这时候业务逻辑就变得非常简单:用户提交上报时,后端先按user_id和report_date查一条记录,查到了就提示“今日已上报”,查不到就执行插入操作。如果遇到多个请求同时提交(理论上不太可能,但防御性编程要考虑),唯一索引会直接报错,你再catch异常返回“请勿重复提交”,这个机制在答辩时提一句“我做了数据库层面的防重复控制”,老师会认为你想得比较周全。
体温字段用decimal(4,2),存储范围最大为99.99℃,足够存储人体温度范围。我这里建议加一个温度异常判断的规则:体温大于37.3℃时,系统自动给这条上报记录打上“异常”标记。判断逻辑可以放在Service层,也可以在SQL里用CASE WHEN实现,我更推荐Service层,因为这样标记逻辑调整起来更灵活。
3.4 出入申请审批表:状态字段驱动流程流转
sql复制CREATE TABLE `t_access_apply` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL COMMENT '申请人ID',
`apply_type` tinyint(1) NOT NULL COMMENT '申请类型: 1-出校, 2-入校',
`out_time` datetime DEFAULT NULL COMMENT '预计出校时间',
`in_time` datetime DEFAULT NULL COMMENT '预计入校时间',
`destination` varchar(128) DEFAULT NULL COMMENT '目的地',
`reason` varchar(255) DEFAULT NULL COMMENT '申请事由',
`status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态: 0-待审批, 1-已通过, 2-已驳回',
`approve_user_id` int(11) DEFAULT NULL COMMENT '审批人ID',
`approve_comment` varchar(255) DEFAULT NULL COMMENT '审批意见',
`approve_time` datetime DEFAULT NULL COMMENT '审批时间',
`create_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
这张表理解起来其实就是一个**“请假审批”的经典业务模型**。状态字段status是整个流程流转的核心,0表示待审批,1表示通过,2表示驳回。学生在小程序端提交申请后,记录状态为0;辅导员登录管理端看到待审批列表,选择通过或驳回;通过后状态变为1,学生端的申请状态同步更新。
不要小看这个看似简陋的三态流程,它背后涉及到的SQL能力可不简单:学生要看“我的申请记录及当前状态”,辅导员要看“待我审批的申请列表”,管理员要看“全部申请及审批结果”,这三类查询对应不同的WHERE条件组合,正好把多条件动态查询的写法练熟了。我当时在MyBatis的XML里用了<if>标签做动态SQL,按status、userId、applyType可选条件拼接查询,这个技巧在很多实际项目里都会用到。
3.5 异常轨迹追踪表:让系统多一个答辩亮点
sql复制CREATE TABLE `t_contact_log` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL COMMENT '异常用户ID',
`location` varchar(128) NOT NULL COMMENT '到访地点',
`contact_time` datetime NOT NULL COMMENT '到访时间',
`contact_type` varchar(32) DEFAULT NULL COMMENT '接触类型',
`description` varchar(255) DEFAULT NULL COMMENT '具体情况描述',
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
这张表是为了给系统增加“追踪溯源”能力而设计的。简单来说,当某个用户上报体温异常或健康码异常时,管理员可以给该用户补充登记“近期到访过的地点”,形成一条追踪记录。后续如果出现管理需要,可以通过用户ID定位到他的轨迹明细。
这个功能虽然逻辑简单,但它在系统演示时特别好看:从“用户管理”点进某个异常用户,能看到他的体温记录、出入申请记录、轨迹追踪记录,“全流程管理”的闭环感一下就出来了。答辩时你可以把这个功能当作“应用创新点”来讲——不是技术多难,而是你在需求分析阶段就想到了“异常之后还要怎么办”这个管理闭环,这种业务思考能力是老师很看重的。
4. 后端核心模块实现:SSM框架下的分层落地
4.1 Maven配置与SSM整合的几个关键节点
先说一个很多人的共同体验:SSM框架本身原理不复杂,但整合配置非常磨人。我第一次整合时,光是“启动Tomcat后报404”就折腾了一下午。后来总结下来,SSM整合就四个关键节点,只要这几个节点不出问题,整套框架基本就能跑起来。
第一个节点:pom.xml的依赖坐标。 Spring、Spring MVC、MyBatis三个框架整合时,依赖版本一定要互相兼容。我用的是Spring 5.2.8.RELEASE、MyBatis 3.5.6、mybatis-spring 2.0.6。这几个版本组合实测兼容,不会有“类找不到”的奇怪问题。数据库驱动用mysql-connector-java 8.0.21,连接池用druid 1.1.23。还要记得加jackson-databind依赖,否则Controller返回对象时无法自动序列化为JSON。
第二个节点:web.xml的配置顺序。 web.xml里要注册Spring的ContextLoaderListener(负责加载Spring根容器)和Spring MVC的DispatcherServlet。这里有个经典坑:DispatcherServlet的<url-pattern>如果是/,它会拦截所有请求,包括静态资源请求,所以必须额外配置静态资源映射。如果用*.do这种后缀匹配倒是省事,但RESTful风格就不标准了。我最后用的是/加<mvc:resources>映射静态资源。
第三个节点:Spring和MyBatis的桥接。 这是SSM整合最核心的部分。你的Spring配置文件中需要配置数据源DataSource、SqlSessionFactoryBean(注意参数是dataSource和mapperLocations,指定Mapper XML文件的位置)、MapperScannerConfigurer(指定Mapper接口所在包,自动生成代理对象)。这三样配完,MyBatis的Mapper接口才能注入到Service里用。
第四个节点:事务配置。 用<tx:annotation-driven transaction-manager="transactionManager"/>开启注解事务。启用后,在Service实现类或方法上加@Transactional就可以管理事务了。比如健康上报的“插入记录”和“统计异常数”如果需要保证一致性,就加到同一个事务里。我实际给所有写操作(插入、更新、删除)都加了@Transactional,避免数据操作一半成功一半失败。
4.2 登录鉴权:从wx.login到openid识别的完整流程
小程序登录和普通网页登录最大的区别在于:用户在小程序端不需要再输入密码,而是通过微信身份体系来识别。整个流程是这样的:
- 小程序端调用
wx.login()方法,微信返回一个临时code(有效期5分钟); - 小程序把code通过
wx.request发送到后端接口POST /api/login; - 后端Controller接收到code后,调用
HttpClient向微信接口https://api.weixin.qq.com/sns/jscode2session发起请求,附带小程序appid、secret、code三个参数; - 微信接口返回该用户对应的openid和session_key;
- 后端拿着openid去数据库查用户表,查到了就说明是已绑定用户,生成一个自定义登录令牌返回给小程序端;
- 查不到说明是首次使用,后端提示“请先绑定学号”,小程序端弹出绑定页面,用户在页面上输入学号密码,后端校验通过后把openid写入该用户记录,同时返回登录令牌。
这里有一个技术点需要特别注意:不能在小程序端直接请求微信的code2session接口,因为请求需要携带小程序的secret密钥,这个密钥一旦暴露在小程序代码里,任何人都能通过反编译拿到,安全隐患非常大。所以正确做法是:小程序端只负责传code,换取openid的操作必须放在后端完成。
登录成功后的令牌我用的JWT(JSON Web Token)。JWT的优点是“无状态”,后端不需要保存会话信息,用户身份全部包含在Token里。我自定义了一个JwtUtils工具类,生成Token时把userId和role放进去,并设置24小时过期时间。后续每个请求都在请求头里带上Authorization: Bearer <token>,后端的LoginInterceptor会解析Token并取出用户信息放入ThreadLocal,方便Service层随时获取当前登录用户。
4.3 健康上报模块:防重复提交与异常标记的Service层写法
健康上报是系统里最核心的功能,具体业务规则是:
- 学生只能上报自己的信息,不能替别人上报(但这里可以考虑一个合理扩展:辅导员可以代学生上报);
- 同一用户同一天只能上报一次;
- 体温超过37.3℃时自动标记为异常;
- 上报成功后返回当前用户的累计上报天数和连续上报天数。
下面给出健康上报Service层的核心代码逻辑(关键业务部分):
java复制public Result reportHealth(HealthReportDTO dto, Integer userId) {
// 1. 判断是否已上报过
HealthReport existing = healthReportMapper.selectByUserIdAndDate(
userId, new Date()
);
if (existing != null) {
return Result.error("今日已完成上报,请勿重复操作");
}
// 2. 判断体温是否异常
boolean isAbnormal = dto.getTemperature().compareTo(new BigDecimal("37.3")) > 0;
// 3. 封装实体类,设置默认值
HealthReport report = new HealthReport();
report.setUserId(userId);
report.setReportDate(new Date());
report.setTemperature(dto.getTemperature());
report.setHealthCode(dto.getHealthCode());
report.setIsFever(dto.getIsFever());
report.setIsCough(dto.getIsCough());
report.setIsContactRisk(dto.getIsContactRisk());
report.setIsAbnormal(isAbnormal ? 1 : 0);
report.setCurrentLocation(dto.getCurrentLocation());
report.setRemark(dto.getRemark());
report.setCreateTime(new Date());
// 4. 插入数据库
int rows = healthReportMapper.insert(report);
if (rows <= 0) {
return Result.error("上报失败,请稍后重试");
}
// 5. 统计个人累计上报天数
int totalDays = healthReportMapper.countByUserId(userId);
// 6. 返回结果
Map<String, Object> data = new HashMap<>();
data.put("reportId", report.getId());
data.put("totalDays", totalDays);
data.put("isAbnormal", isAbnormal);
return Result.success(data);
}
代码不长,但每一步都有明确的目的。第1步的防重复判断,第2步的异常规则,第5步的统计信息,回答“你为什么这么设计”时都能自圆其说。这里额外提一个注意点:BigDecimal比较大小不要用equals(),因为它比较的是精确值,37.30和37.3会被判为不相等;要用compareTo()方法。
4.4 出入申请审批模块:状态机流转与多条件列表查询
出入申请审批模块是另一个核心功能,也是展示“业务流程设计能力”的重点。
状态机设计我用了简单但实用的方式:
- 提交申请 → 状态为“待审批”(0)
- 辅导员审批通过 → 状态为“已通过”(1)
- 辅导员审批驳回 → 状态为“已驳回”(2)
这里有一个容易被忽略的细节:审批操作必须做状态校验。也就是说,辅导员点“通过”时,后端要检查这条申请当前的状态是否真的是0(待审批),如果已经是1或2(已处理),就不能再重复审批。我当时在Service层加了一个校验逻辑:
java复制public Result approveAccess(AccessApproveDTO dto, Integer approverId) {
AccessApply apply = accessApplyMapper.selectById(dto.getApplyId());
if (apply == null) {
return Result.error("申请记录不存在");
}
// 关键校验:只有待审批状态才能被处理
if (apply.getStatus() != 0) {
return Result.error("该申请已处理,请勿重复操作");
}
apply.setStatus(dto.getApproveResult()); // 1通过,2驳回
apply.setApproveUserId(approverId);
apply.setApproveComment(dto.getComment());
apply.setApproveTime(new Date());
accessApplyMapper.updateById(apply);
return Result.success("审批完成");
}
这个校验在并发场景下尤其重要。虽然毕设项目一般不会真正遇到并发审批的情况,但“在修改数据前先检查当前状态”这个思维习惯,是从学生到工程师转变的关键一步。答辩时如果老师追问“两个人同时审批同一条记录会怎样”,你回答“我的状态校验可以保证只有第一次操作生效”,这就是一个漂亮的加分回答。
多条件列表查询主要用在两个场景:学生端的“我的申请列表”需要按当前登录用户过滤;管理端的“待审批列表”需要按状态过滤。这两个查询都涉及MyBatis动态SQL。以管理端为例:
xml复制<select id="selectApplyList" resultType="com.example.epidemic.entity.AccessApply">
SELECT * FROM t_access_apply
<where>
<if test="status != null">
AND status = #{status}
</if>
<if test="applyType != null">
AND apply_type = #{applyType}
</if>
<if test="userName != null and userName != ''">
AND user_id IN (
SELECT id FROM t_user WHERE real_name LIKE CONCAT('%', #{userName}, '%')
)
</if>
</where>
ORDER BY create_time DESC
LIMIT #{offset}, #{pageSize}
</select>
<where>标签会自动处理AND前缀问题,<if>标签实现可选条件拼接,不用为了不同查询条件写多个SQL。分页用LIMIT offset, pageSize实现,配合PageHelper插件也可以,但手写LIMIT更直观。
4.5 统计报表模块:用一条SQL点亮“管理驾驶舱”
毕设系统如果只是“增删改查”,虽然也能过,但显得单薄。我加了一个统计报表模块,让管理员在后台首页看到多张统计卡片,这部分的视觉冲击力非常强。
统计的SQL其实并不复杂。比如统计“今日全校上报率”:
sql复制-- 今日已上报人数
SELECT COUNT(DISTINCT user_id) FROM t_health_report WHERE report_date = CURDATE();
-- 全校总人数
SELECT COUNT(*) FROM t_user WHERE role = 1;
-- 今日异常人数
SELECT COUNT(*) FROM t_health_report
WHERE report_date = CURDATE() AND temperature > 37.3;
上报率趋势图的数据,可以用一条SQL按日期分组查询最近7天的数据:
sql复制SELECT report_date, COUNT(*) AS cnt
FROM t_health_report
WHERE report_date >= DATE_SUB(CURDATE(), INTERVAL 6 DAY)
GROUP BY report_date
ORDER BY report_date;
这类SQL写完,图表数据就有了。小程序端可以用wx-charts或者ec-canvas(ECharts的小程序版本)画折线图、柱状图。我推荐ec-canvas,因为ECharts的生态熟悉,网上示例多,配置起来不费劲。后端接口返回7天的日期数组和数量数组,前端直接setData渲染图表组件,“数据可视化”这个评分点就有了着落。
5. 小程序端搭建与联调:从登录到功能闭环的实操记录
5.1 小程序项目结构规划
小程序的页面结构我按照角色拆分,每个角色的 tabBar 不同。学生端是:首页、健康上报、出入申请、我的;管理端是:工作台、审批管理、统计报表、我的。这样拆的好处是页面职责清晰,代码不会乱七八糟。
整体目录结构大致如下:
code复制miniprogram
├── pages
│ ├── login # 登录/绑定页
│ ├── student
│ │ ├── index # 学生首页
│ │ ├── report # 健康上报
│ │ ├── reportHistory # 上报记录
│ │ ├── accessApply # 出入申请
│ │ └── accessList # 申请记录
│ ├── admin
│ │ ├── index # 管理端工作台
│ │ ├── approve # 审批管理
│ │ ├── statistics # 统计报表
│ │ └── noticePublish # 公告发布
│ ├── user
│ │ ├── profile # 个人信息
│ │ └── binding # 学号绑定
├── utils
│ ├── request.js # 封装wx.request,统一处理Token和错误码
│ ├── config.js # 后端接口地址配置
│ └── util.js # 时间格式化等工具函数
├── components
│ ├── empty-state # 空状态占位组件
│ └── status-tag # 审批状态标签组件
├── app.js
├── app.json
└── app.wxss
5.2 登录流程与Token管理:小程序端最容易出错的一环
小程序端的登录实现,新手最常见的坑是:在onLoad里直接调wx.login,然后拿code调后端接口,但后端返回了“用户未绑定”的提示,页面就卡住了。 正确的处理方式是分两种情况:
- 用户已经绑定过学号:进入小程序后,用wx.login获取code,调后端登录接口,拿到Token直接进入首页;
- 用户从未绑定过:调后端接口时,后端返回特定错误码(比如
code=1001),小程序端收到这个码后跳转到绑定页面,用户输入学号密码,绑定成功后再调用登录接口拿Token。
Token的存储统一放在wx.setStorageSync('token', token)里。封装request.js时,在每次请求的header里自动带上Token:
javascript复制// utils/request.js
const request = (url, method, data) => {
return new Promise((resolve, reject) => {
const token = wx.getStorageSync('token');
wx.request({
url: `${config.baseUrl}${url}`,
method: method || 'GET',
data: data || {},
header: {
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : ''
},
success: (res) => {
if (res.statusCode === 401) {
// 登录过期,跳转登录页
wx.removeStorageSync('token');
wx.reLaunch({ url: '/pages/login/login' });
reject(res.data);
return;
}
resolve(res.data);
},
fail: (err) => reject(err)
});
});
};
这里有个细节:为什么把Token放在请求头的Authorization字段而不是请求体里? 因为RESTful接口风格中,身份认证信息是“元信息”,不应该混在业务参数中。后端拦截器也是从Header里取Token解析,这种规范做法在面试时也会被问到。
5.3 健康上报页面的表单校验与交互反馈
健康上报页面本质就是一个表单收集页,但体验细节决定了老师演示时的好感度。我当时在页面上做了这样几个交互:
- 温度输入框用
type="digit",弹起数字键盘,方便用户输入小数; - 体温超过37.3℃时,前端实时提示“体温偏高,请确认是否正常”,避免用户提交后才发现系统标记异常;
- 健康上报成功后弹Toast提示“上报成功”,并展示当天累计天数;
- 如果用户当天已上报,进入页面时后端返回“今日已上报”,前端直接跳转到上报记录页,不再显示表单。
小程序端的onPullDownRefresh下拉刷新功能可以用来刷新上报记录列表。提交按钮加一个loading状态防止重复点击,这个虽然不是核心技术,但老师操作用起来会觉得“手感不错”。
表单校验的逻辑不要依赖后端提示。后端校验是防止恶意请求的,前端校验是提升用户体验的。两者职责不同,不能互相替代。
5.4 与后端联调时绕不开的几个“环境问题”
联调是最花时间的一步,主要坑集中在三个地方:
第一个:开发者工具的“不校验合法域名”设置。 微信开发者工具默认会校验请求域名必须是HTTPS,否则会报“url not in domain list”。本地调试时,可以在“详情 → 本地设置”里勾上“不校验合法域名、web-view(业务域名)、TLS版本以及HTTPS证书”。不勾的话,你连http://localhost:8080都请求不通。
第二个:本机IP访问。 如果你用真机预览而不是开发者工具,就不能用localhost,必须改成你电脑在局域网里的IP地址,比如http://192.168.1.100:8080。同时要保证手机和电脑在同一个WiFi下。我在config.js里用一个常量管理baseUrl,要切换环境时只改一处。
第三个:跨域问题。 小程序端请求后端接口时,因为后端用的是http://localhost:8080,前端小程序域名是https://servicewechat.com(真机时是微信官方的域名),这会触发浏览器的同源策略吗?实际上微信小程序的wx.request不受浏览器同源策略限制,它不校验CORS。所以后端基本不用处理跨域。但如果你在写管理后台网页端,就需要在后端加CORS配置了。这里只需要区分清楚:小程序 ≠ 浏览器网页。
5.5 个性化页面:让小程序看起来“像个系统”
系统功能齐全了,但页面如果长得太朴素,答辩效果会打折扣。我做了几个低成本但高感知的UI优化:
- 首页网格导航:用flex布局做四宫格/六宫格的图标导航,每个功能一个色块图标,视觉上非常清晰;
- 状态标签组件:待审批显示橙色标签、已通过显示绿色标签、已驳回显示红色标签。单独拆一个组件,所有列表页复用,改一处全局生效;
- 统计卡片:管理端首页放“今日上报人数”“今日异常人数”“待审批申请”“累计用户数”四张卡片,数字大号加粗,配合背景渐变色,一眼看出数据重点;
- 空状态组件:列表为空时显示一个简单的插图和文字,比直接白屏强很多。
这些UI优化不涉及复杂技术,只需要花时间打磨,但它们的价值在于:老师第一眼看到你系统时,会觉得“这个学生做事细致”,这种印象分在答辩时是非常值钱的。
6. 实测踩坑记录:SSM整合、小程序适配、图片上传那些坎
6.1 经典报错“Invalid bound statement (not found)”的根因排查
这个报错几乎每个SSM新手都会遇到。报错信息说找不到Mapper的SQL语句,你检查Mapper接口名和XML命名空间都对得上,但就是报错。我当时排查了很久,最后发现问题出在Mapper XML文件没有被编译到classes目录。
原因是Maven项目里,放在src/main/java目录下的XML文件默认不会被Maven打包。解决办法是在pom.xml的<build>节点里加资源过滤配置:
xml复制<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>
加上之后,重新编译,target/classes目录下就会出现Mapper XML文件,报错就消失了。这个坑的原理很简单:MyBatis的mapperLocations配置指向的路径找不到XML文件,自然就无法绑定SQL语句。排查“Invalid bound statement”时,先看target目录,再检查Spring配置。如果你把XML文件放在src/main/resources目录下,则不会遇到这个问题,所以这也是一个可行的规避方案。
6.2 Spring MVC返回JSON时日期格式不正常的处理
后端接口返回的Java Date对象,默认会被Jackson序列化为时间戳数字,比如1612345678901。小程序端拿到这个数字还要自己转格式,非常麻烦。更标准的方式是自定义日期格式:
在Spring配置文件中配置Jackson的ObjectMapper:
xml复制<mvc:annotation-driven>
<mvc:message-converters>
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
<property name="objectMapper">
<bean class="com.fasterxml.jackson.databind.ObjectMapper">
<property name="dateFormat">
<bean class="java.text.SimpleDateFormat">
<constructor-arg value="yyyy-MM-dd HH:mm:ss"/>
</bean>
</property>
</bean>
</property>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
也可以更简单,在每个实体类的日期字段上加@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")注解。两种方式我实际都测过,实体类加注解的方式更灵活,因为你可能某些接口想要时间戳,某些想要格式化字符串。但毕设系统里统一格式更省心,我最后选择了在配置里全局格式化。注意timezone一定要写GMT+8,否则序列化出来的时间会比本地时间少8个小时。
6.3 小程序真机调试时wx.request请求报错
本地开发者工具里一切正常,一上真机就报错,这种情况我遇到过好几次。常见的几个原因是:
- 手机和电脑不在同一个局域网,真机无法访问电脑的IP;
- 后端Tomcat没监听在
0.0.0.0,只允许本机访问。这个需要在Tomcat的server.xml里检查Connector的address配置; wx.request的请求时间默认60秒,如果后端接口处理慢(比如第一次启动MyBatis初始化),会超时报错;- 开发者工具里勾了“不校验合法域名”,真机上这个设置不生效。如果要做真机演示,要么把后端接口配置到一台公网服务器并配上HTTPS域名,要么使用微信开发者工具的“真机调试”模式(这个模式会忽略域名校验)。
我做演示时最怕的就是“本地能跑,真机挂了”,所以提前在答辩前一周就反复用真机测试所有核心流程,这个工作千万不能省。
6.4 图片上传功能的实现与踩坑
如果你的系统需要支持用户上传健康码截图,就会涉及文件上传功能。小程序端用wx.chooseMedia选择图片,然后通过wx.uploadFile上传到后端接口:
javascript复制wx.chooseMedia({
count: 1,
mediaType: ['image'],
sourceType: ['album', 'camera'],
success: (res) => {
const filePath = res.tempFiles[0].tempFilePath;
wx.uploadFile({
url: `${config.baseUrl}/api/upload`,
filePath: filePath,
name: 'file',
success: (uploadRes) => {
const data = JSON.parse(uploadRes.data);
// data.url 就是上传后的文件访问路径
}
});
}
});
后端接收文件的接口:
java复制@PostMapping("/api/upload")
public Result upload(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
return Result.error("上传文件为空");
}
String originalFilename = file.getOriginalFilename();
String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
String fileName = UUID.randomUUID().toString().replace("-", "") + suffix;
// 保存到本地目录,实际生产环境会传到云存储
String savePath = "D:/upload/" + fileName;
file.transferTo(new File(savePath));
String url = "/files/" + fileName;
return Result.success(url);
}
这里需要注意:后端接收文件的参数名file必须和前端wx.uploadFile的name: 'file'保持一致;同时Spring MVC需要配置multipart解析器:
xml复制<bean id="multipartResolver"
class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<property name="maxUploadSize" value="5242880"/>
<property name="defaultEncoding" value="UTF-8"/>
</bean>
上传后的文件如果要能被浏览器访问,需要配置静态资源映射,把/files/**映射到本地磁盘目录。虽然D盘路径写死不好,但毕设项目里用这种方式保存足够了,写在文档里注明“实际生产环境建议使用云存储”反而是加分项。
6.5 跨天数据边界问题:健康上报“昨天漏报”的处理
最后一个我踩过的业务坑:健康上报表是按“自然日”来区分的,但有些用户会在凌晨12点前后连续提交两次,这时候report_date的生成逻辑就很重要。我当时犯过错:直接用Java的new Date(),然后存到数据库的report_date字段(date类型),结果发现凌晨12点后提交的记录日期还是昨天——因为代码里获取的是当前时区的时间,而数据库连接时设置时区有偏差。
解决办法是在数据库连接URL里明确指定时区:
properties复制jdbc:mysql://localhost:3306/epidemic?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
serverTimezone=Asia/Shanghai一定要加,否则Java 8以上的MySQL驱动默认用UTC时区,和北京时间差8小时,所有日期操作都会错乱。
7. 复盘与扩展:这套系统除了答辩,还能往哪里走
7.1 答辩前最值得复盘的四个核心问题
做完系统不等于答辩稳了。你得能回答“为什么这么做”,而不是只能说“我做了”。我总结了老师最可能追问的四个问题,提前准备好回答思路:
问题一:为什么用户表里要有openid字段,它是怎么用的?
回答思路:openid是微信用户在某个小程序下的唯一标识。用户首次进入小程序时,通过wx.login获取code,后端用code换openid。如果用户未绑定,跳到绑定页输入学号密码;绑定后openid写入用户表,后续登录直接通过openid识别身份,无需重复输入账号密码。这里要强调openid不能替代学号账户体系,它是“微信身份”和“系统账户”之间的桥接。
问题二:健康上报如何防止同一天多次提交?
回答思路:双重保障。第一层在数据库层面,给user_id和report_date建联合唯一索引,数据库本身就拒绝重复记录;第二层在业务逻辑层面,提交时先查询当天是否已有记录。即使两个请求同时到达,数据库会拒绝其中一个,后端的唯一索引冲突异常会被捕获并返回“请勿重复提交”。
问题三:权限控制是怎么实现的?
回答思路:后端采用三层权限控制。第一层是登录拦截器,所有接口先验证Token是否有效、是否过期;第二层是自定义@RequireRole注解加在Controller方法上,根据用户角色判断是否有访问权限;第三层是页面级控制,小程序端根据用户角色动态渲染不同的tabBar和页面入口。
问题四:如果用户量很大,系统可能存在哪些性能瓶颈?
回答思路:从小程序和数据库两个角度分析。小程序端的主包限制是2M,如果页面代码过多需要分包加载;数据库层的热点表是健康上报表,数据量增长快,需要为report_date建索引,并按时间做分表或归档。还可以提到Redis缓存热点数据,比如用户信息、统计报表结果。
7.2 可以继续扩展的三个方向
如果你时间充裕,想在毕设基础上再做一些亮点,我会推荐以下三个方向,按性价比排序:
方向一:消息通知。 用微信小程序订阅消息做审批结果通知。用户提交出入申请后,辅导员审批通过/驳回时,系统通过订阅消息模板通知学生。这个功能技术难度不大(只需要调用微信的subscribeMessage.send接口),但“业务闭环”的完整度会瞬间提高不少。
方向二:管理后台用Vue3重构。 当前管理端如果做在小程序里,管理功能扩展起来页面会越来越臃肿。可以单独做一个Vue3 + Element Plus的Web后台,专门给辅导员和管理员用,小程序端只保留学生端功能。这个扩展方案正好匹配“vue3连接ssm框架”的常见面试题场景,一次开发两篇收获。
方向三:大数据量下的查询优化。 给健康上报表加上定时任务,每天晚上统计当天的上报数据汇总到一张报表表,查询报表时读汇总表而不是扫明细表。这个点如果演示出来,效果会比单纯的功能叠加更有深度。
7.3 关于这套系统的几句话
这套系统我从选型到完工用了大概三周,每天投入三到四个小时。回头复盘,最大的收获其实不是你写了多少代码,而是在一个完整业务场景里,把SSM框架的配置、MyBatis的映射、小程序端的交互、前后端联调这些问题全走了一遍。很多东西书上讲了八百遍,自己踩一遍才真正理解。
你在做毕设时如果遇到某一步卡了很久,先停下来想清楚“这个报错到底在说什么”,而不是急着换方案。大多数报错信息都给出了明确的线索,只是你还没学会去读它。比如404去查URL和控制器映射,500去查Tomcat日志,SQL报错去查MyBatis打印的预处理语句——按这个思路排查,九成以上的问题都能自己解决。
最后说一个关于数据的处理细节:系统里的演示数据可以自己造一些,但不要造得太假。学生姓名、学院、专业这些字段要用合理数据,体温数值要在一个正常范围内波动,出入申请的审批时间要有逻辑性。这些数据准备工作做得好,演示时整个系统看起来就特别“真实”,老师在操作时会觉得你确实是认真做了一套能用的系统,而不是一个交给文件复制粘贴的作业。
