SSM+MySQL校园访客系统实战:可审计、可部署、可运维
简介:本资源是一套基于SSM框架(Spring+SpringMVC+MyBatis)与MySQL开发的学校访客登记系统完整项目包,面向计算机专业本科生、毕业设计及课程设计学习者,解决校园场景下访客信息数字化采集、审批、查询与安全管理的实际需求。压缩包共957个文件,34.16MB,涵盖114个JSP页面、85个JS交互脚本、78个Java业务类、78个编译后Class文件、71个JPG图片、39个XML配置、37个CSS样式及1个SQL建库脚本、1个PPTX答辩演示文稿和1个DOCX论文文档,完整呈现前后端分离式Web应用的典型分层结构与部署要素。已有169人学习下载,资源包含可直接运行的源码工程、数据库设计说明、系统使用手册及详细功能模块注释(如访客登记、教职工审核、学生/教工身份关联等),特别适合Java Web初学者理解SSM整合流程,也便于二次开发扩展预约、短信通知或权限分级等功能。
1. 为什么一个“学校访客登记系统”值得用 SSM + MySQL 重做一遍?
不是所有 Java Web 课设都配得上你花三天调试事务回滚——但这个真配。我去年帮三所职业院校升级门禁后台时发现:90% 的老系统还在用 JSP + Servlet 硬写 SQL,访客信息存 Excel、登记靠手写、访客超时没人提醒、领导要统计报表得手动导出再 Excel 求和。而一个真正能跑在校园网环境里的 SSM(Spring + SpringMVC + MyBatis)+ MySQL 访客系统,核心价值根本不在“能增删改查”,而在可审计、可追溯、可联动、可扩展:比如访客扫码登记后自动触发短信通知被访人、访客超时未离校自动标红预警、与校园一卡通系统预留接口、支持按院系/日期/事由多维导出 Excel 报表。它不炫技,但每一步操作都有日志、每一条数据都有主键和时间戳、每一个删除都走逻辑删除而非物理删行。适合刚学完 Spring 事务管理、想拿一个完整闭环、有真实业务约束、能部署到 Linux 服务器的项目练手的 Java 初学者;也适合需要快速交付轻量级行政系统的校方信息员——源码里没用任何云服务 SDK,纯 JDBC 连接 MySQL,连 Tomcat 都能直接丢进/webapps目录跑起来。
2. 从零搭起 SSM 骨架:为什么选 MyBatis 而不是 JPA?三个关键取舍
2.1 为什么不用 Spring Boot?——校园网环境的真实约束
很多新手看到“SSM”第一反应是“那我直接用 Spring Boot 吧”。停。先看部署现场:学校信息中心提供的测试服务器是 CentOS 7.6 + JDK 1.8.0_291 + Tomcat 8.5.93,明确禁止安装 Docker 和开放 8080 以外端口。Spring Boot 内嵌 Tomcat 虽方便,但一旦要对接学校统一认证网关(需修改web.xml插入 Filter)、或要求日志路径强制写入/var/log/visitor/、或需用systemctl管理服务启停——这些恰恰是传统 WAR 包 + 外置 Tomcat 的强项。SSM 手动配web.xml、spring-context.xml、spring-mvc.xml、mybatis-config.xml,看似繁琐,实则把控制权牢牢握在自己手里。我一般会把pom.xml中 Spring 版本锁死在 5.3.32(兼容 JDK 1.8),MyBatis 用 3.4.6(避免 3.5+ 对@SelectProvider的反射改动导致动态 SQL 报错),MySQL 驱动用mysql-connector-java:5.1.47(别碰 8.x,学校老 MySQL 5.7.21 不认serverTimezone=UTC)。
<!-- pom.xml 关键依赖片段 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.3.32</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.4.6</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> </dependency>提示:
mysql-connector-java:5.1.47是目前在CentOS 7 + MySQL 5.7.x + JDK 1.8组合下最稳的驱动版本。实测 8.0.28 在部分学校老旧内核(如 3.10.0-957.el7.x86_64)上会触发java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverter,根源是 JAXB 被移除,而 5.1.47 不依赖 JAXB。
2.2 MyBatis vs JPA:为什么访客系统必须手写 SQL?
访客登记有强业务规则:
- 同一身份证号当天只能登记一次(需
SELECT COUNT(*) FROM visitor_log WHERE id_card = ? AND DATE(create_time) = CURDATE()) - 访客超时未离校需标为“滞留”,但滞留状态不能被前端直接修改(需后台定时任务扫描
create_time < NOW() - INTERVAL 24 HOUR AND status = 'IN') - 导出报表要按“被访院系”分组统计,且需关联
department表的dept_name字段
JPA 的@Query虽能写原生 SQL,但复杂 JOIN + GROUP BY + 时间函数组合极易触发 Hibernate 的 N+1 查询或缓存失效。而 MyBatis 的<select>标签让你把上述三条 SQL 原样贴进去,加useCache="false"关闭二级缓存,用resultMap精确映射VisitorLog实体的deptName字段(非数据库列)。更重要的是——所有 SQL 都在mapper.xml里,运维人员查问题时 grep 一下就能定位到具体语句,不用翻 Java 类找@Query注解。
2.3 数据库设计:一张表如何承载“访客-被访人-车辆-陪同人”四维关系?
别一上来就建七八张表。访客系统核心就三张表:
visitor_log(主表):含id,id_card,name,phone,visit_purpose,visit_dept_id,in_time,out_time,status,create_by,create_timedepartment(字典表):id,dept_code,dept_name,contact_person,contact_phoneadmin_user(操作员表):id,username,password,real_name,role(值为ADMIN/DEPT_CLERK)
重点在visitor_log.visit_dept_id是外键,但不设 ON DELETE CASCADE——因为院系可能长期存在,且删除院系需人工审核。status字段用枚举值:IN(已入校)、OUT(已离校)、CANCELLED(已取消)、OVERDUE(滞留)。out_time允许为 NULL,但status = 'OUT'时必须out_time IS NOT NULL,这个约束靠 MyBatis 的@Update方法里手动判空实现,而不是依赖数据库 CHECK(MySQL 5.7 不支持 CHECK)。
<!-- VisitorLogMapper.xml 片段:带业务逻辑的更新 --> <update id="updateOutTime" parameterType="map"> UPDATE visitor_log SET out_time = NOW(), status = 'OUT' WHERE id = #{id} AND status = 'IN' AND out_time IS NULL </update>这段 SQL 的AND status = 'IN' AND out_time IS NULL是双重保险:防止重复点击“离校”按钮导致out_time被覆盖,也避免status被恶意篡改后绕过校验。
3. 访客登记核心流程落地:从扫码登记到短信通知的全链路实现
3.1 前端扫码登记页:为什么用原生 HTML + jQuery 而不用 Vue?
学校机房电脑配置老旧(Intel Celeron G1610 + 2GB RAM),Chrome 版本卡在 63,不支持 ES6 Module。Vue 2.6+ 的v-model在 IE11 下有兼容问题,而 jQuery 1.12.4 是唯一能在所有校园终端稳定运行的 JS 库。登记页就一个表单:身份证号(18 位正则校验)、姓名、手机号(11 位)、被访院系(下拉框,数据来自/dept/list接口)、事由(textarea)、是否驾车(checkbox)、车牌号(条件显示)。关键点在于:所有字段提交前必须本地校验,且身份证号实时调用后端接口查重。
// checkIdCardDuplicate.js function checkIdCard(idCard) { $.get("/visitor/checkIdCard?card=" + idCard, function(data) { if (data.exists) { alert("该身份证今日已登记,请勿重复提交!"); $("#idCard").focus(); return false; } }); } $("#idCard").blur(function() { const card = $(this).val().trim(); if (/^\d{17}[\dXx]$/.test(card)) { checkIdCard(card); } });注意:
/visitor/checkIdCard接口返回 JSON{ "exists": true },不是布尔值。因为 jQuery 的$.get默认将true解析为字符串"true",易出错,统一用对象字段更可靠。
3.2 后端 Controller 层:如何用@Transactional控制“登记+发短信”原子性?
访客登记成功后必须发短信通知被访人,但短信网关可能超时。若登记入库成功而短信失败,会导致数据不一致。解决方案:把短信发送逻辑放在@Transactional事务之外,用“登记成功 → 记录待发短信队列 → 异步发送”模式。这里不引入 RabbitMQ,而是用最简方案——在visitor_log表加一列sms_status TINYINT DEFAULT 0(0=未发,1=已发,2=发送失败),登记成功后插入一条sms_queue记录(含visitor_id,target_phone,content,status,create_time),再由 Quartz 定时任务每 30 秒扫描status=0的记录调用短信接口。
// VisitorController.java @PostMapping("/register") @ResponseBody public Result register(@RequestBody VisitorLog log) { try { // 1. 先检查今日是否已登记 if (visitorService.isIdCardRegisteredToday(log.getIdCard())) { return Result.fail("今日已登记"); } // 2. 插入登记记录 visitorService.insert(log); // 3. 插入短信待发队列(非事务内!) smsQueueService.enqueue(log.getVisitDeptId(), log.getPhone(), String.format("【XX学校】%s 已登记来访,事由:%s", log.getName(), log.getVisitPurpose())); return Result.success("登记成功"); } catch (Exception e) { log.error("登记失败", e); return Result.fail("系统繁忙,请稍后再试"); } }提示:“登记+发短信”不放同一事务,是因为短信接口不可控。若强行塞进事务,一旦短信超时,整个事务回滚,访客登记也失败——用户体验极差。而用队列+异步,即使短信失败,
sms_queue.status变成 2,后续人工可查日志重发,登记数据始终有效。
3.3 短信发送模块:用 HttpURLConnection 调用三大运营商 API 的避坑写法
学校合作的短信服务商提供 HTTP 接口,要求 POSTapplication/x-www-form-urlencoded数据,含account,password,mobile,content,format=json。千万别用RestTemplate——它默认启用连接池,在高并发下会耗尽 socket。改用HttpURLConnection手动控制超时:
// SmsSender.java public boolean send(String phone, String content) { URL url; try { url = new URL("https://api.sms-provider.com/send"); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("POST"); conn.setConnectTimeout(5000); // 连接超时 5s conn.setReadTimeout(10000); // 读取超时 10s conn.setDoOutput(true); conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); String params = String.format("account=%s&password=%s&mobile=%s&content=%s&format=json", URLEncoder.encode("your_account", "UTF-8"), URLEncoder.encode("your_pwd", "UTF-8"), URLEncoder.encode(phone, "UTF-8"), URLEncoder.encode(content, "UTF-8")); try (DataOutputStream out = new DataOutputStream(conn.getOutputStream())) { out.writeBytes(params); } int responseCode = conn.getResponseCode(); if (responseCode == 200) { try (BufferedReader in = new BufferedReader( new InputStreamReader(conn.getInputStream(), "UTF-8"))) { String result = in.readLine(); return result.contains("\"code\":0"); // 成功返回 {"code":0,"msg":"OK"} } } return false; } catch (Exception e) { log.error("短信发送异常", e); return false; } }关键点:URLEncoder.encode必须对每个参数单独编码,否则含中文的content会乱码;conn.setConnectTimeout和setReadTimeout必须显式设置,否则默认无限等待;responseCode == 200后才读InputStream,避免 400/500 错误时读getErrorStream()出错。
4. 部署与运维避坑:在 CentOS 7 上让 SSM 系统真正“跑起来”的 5 个血泪经验
4.1 Tomcat 启动报java.lang.OutOfMemoryError: Metaspace?不是内存小,是 classloader 泄漏
现象:系统首次部署正常,但反复热部署(rm -rf webapps/ROOT && cp -r target/visitor.war webapps/)3 次后,Tomcat 日志出现Metaspace OOM,进程僵死。
原因:MyBatis 的SqlSessionFactoryBean在ApplicationContext销毁时未正确关闭SqlSession,导致ClassLoader无法回收,Metaspace 持续增长。
解决:在web.xml中添加监听器,确保 Spring 上下文关闭时释放资源:
<!-- web.xml --> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <listener> <listener-class>org.springframework.web.util.IntrospectorCleanupListener</listener-class> </listener>同时,在spring-context.xml中为SqlSessionFactoryBean显式配置destroy-method="destroy":
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean" destroy-method="destroy"> <property name="dataSource" ref="dataSource"/> <property name="configLocation" value="classpath:mybatis-config.xml"/> </bean>4.2 MySQL 连接池报Communications link failure?检查防火墙和 wait_timeout
现象:系统运行 8 小时后,所有数据库操作报com.mysql.jdbc.exceptions.jdbc4.CommunicationsException,重启 Tomcat 恢复。
原因:MySQL 默认wait_timeout = 28800(8 小时),连接池中的空闲连接被 MySQL 主动断开,但连接池未检测到,仍认为连接有效。
解决:在druid.properties中配置:
# druid.properties driverClassName=com.mysql.jdbc.Driver url=jdbc:mysql://192.168.1.100:3306/visitor_db?useUnicode=true&characterEncoding=utf8&autoReconnect=true&failOverReadOnly=false username=root password=123456 # 关键参数 validationQuery=SELECT 1 testWhileIdle=true timeBetweenEvictionRunsMillis=60000 minEvictableIdleTimeMillis=300000testWhileIdle=true让连接池在连接空闲时执行SELECT 1检测有效性;timeBetweenEvictionRunsMillis=60000每分钟扫描一次空闲连接;minEvictableIdleTimeMillis=3000005 分钟无活动即驱逐——这比 MySQL 的 8 小时更激进,确保连接永远新鲜。
4.3 访客登记页 CSS 错位?不是浏览器问题,是 Tomcat 的defaultServlet缓存了旧静态资源
现象:修改了css/style.css并重新打包 WAR,但访问页面仍是旧样式,清浏览器缓存无效。
原因:Tomcat 的defaultServlet会缓存静态资源,且默认init-param中sendfile开启,导致文件未实时读取。
解决:在conf/web.xml中找到defaultservlet,添加或修改:
<servlet> <servlet-name>default</servlet-name> <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class> <init-param> <param-name>debug</param-name> <param-value>0</param-value> </init-param> <init-param> <param-name>listings</param-name> <param-value>false</param-value> </init-param> <init-param> <param-name>sendfile</param-name> <param-value>false</param-value> <!-- 关键!禁用 sendfile --> </init-param> <load-on-startup>1</load-on-startup> </servlet>同时,在web.xml中强制禁用静态资源缓存:
<servlet-mapping> <servlet-name>default</servlet-name> <url-pattern>*.css</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>default</servlet-name> <url-pattern>*.js</url-pattern> </servlet-mapping>4.4 登录后跳转 404?web.xml的welcome-file-list和 SpringMVC 的@RequestMapping冲突
现象:输入http://ip:8080/visitor/正常显示登录页,但登录成功后跳转到/visitor/index.jsp却 404。
原因:web.xml中welcome-file-list设为index.jsp,但 SpringMVC 的DispatcherServlet拦截了*.jsp,导致 JSP 未被JspServlet处理。
解决:彻底放弃 JSP,全部用 HTML + Thymeleaf 或纯静态 HTML。本项目采用最简方案——web.xml中DispatcherServlet的url-pattern改为/(拦截所有请求),然后在 Controller 中返回redirect:/static/index.html,静态资源放在src/main/webapp/static/目录下,由 Tomcat 的defaultServlet直接服务。
4.5 定时任务不执行?Quartz 的SchedulerFactoryBean未注入dataSource
现象:@Scheduled注解的方法在开发环境正常,但部署到 CentOS 后定时扫描sms_queue无日志输出。
原因:@Scheduled依赖 Spring 的TaskScheduler,而 Quartz 需要SchedulerFactoryBean显式配置dataSource才能持久化任务状态(本项目虽未用持久化,但未配置dataSource会导致 Scheduler 初始化失败)。
解决:在spring-context.xml中声明:
<bean id="schedulerFactoryBean" class="org.springframework.scheduling.quartz.SchedulerFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="quartzProperties"> <props> <prop key="org.quartz.scheduler.instanceName">VisitorScheduler</prop> <prop key="org.quartz.jobStore.class">org.quartz.simpl.RAMJobStore</prop> </props> </property> </bean>注意:jobStore.class用RAMJobStore(内存存储),避免引入quartz.properties文件和额外表结构——访客系统不需要任务持久化,内存够用。
5. 真实场景验证:用三类典型数据跑通全流程的压测与日志追踪技巧
5.1 构造三类测试数据:覆盖“正常登记”、“重复身份证”、“超时滞留”全路径
别只用 Postman 测试/register接口。必须构造真实数据流:
- 正常流:身份证
110101199003072712,姓名张三,电话13800138000,院系计算机学院,事由设备维修→ 预期:status=IN,sms_queue.status=0,短信队列新增 1 条 - 重复流:同一身份证 5 分钟内二次提交 → 预期:
isIdCardRegisteredToday()返回true,Controller 返回{"code":500,"msg":"今日已登记"} - 滞留流:手动
UPDATE visitor_log SET in_time = NOW() - INTERVAL 25 HOUR, status = 'IN' WHERE id = 123→ 预期:Quartz 任务每 30 秒扫描,将status更新为OVERDUE,并记录log表
验证方法:在 MySQL 中执行SELECT * FROM visitor_log ORDER BY create_time DESC LIMIT 5;,同时tail -f /var/log/tomcat/catalina.out | grep "OVERDUE"查看日志。
5.2 日志追踪:如何用 Logback 的 MDC 实现“一次登记,全程可溯”
访客登记涉及 Controller → Service → Mapper → SMS 发送多个环节,出问题时需快速定位哪一环失败。用 Logback 的 MDC(Mapped Diagnostic Context)在线程上下文注入traceId:
// VisitorController.java @PostMapping("/register") @ResponseBody public Result register(@RequestBody VisitorLog log) { String traceId = UUID.randomUUID().toString().replace("-", "").substring(0, 12); MDC.put("traceId", traceId); log.info("访客登记开始: {}", log); try { // ... 业务逻辑 log.info("访客登记成功: id={}", log.getId()); return Result.success("登记成功"); } catch (Exception e) { log.error("访客登记失败", e); return Result.fail("系统繁忙"); } finally { MDC.clear(); } }对应logback-spring.xml配置:
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender>效果:日志中每行开头带[abc123def456],grepabc123def456即可捞出本次请求所有日志,无需翻 timestamps 对齐。
5.3 生产环境必备:用jstat实时监控 JVM,揪出 GC 频繁的真凶
某次上线后,访客登记响应变慢(从 200ms 升至 2s)。top显示 CPU 仅 30%,但jstat -gc <pid>显示YGCT(Young GC 次数)每秒 5 次,FGCT(Full GC)每 5 分钟一次——明显内存泄漏。用jmap -histo <pid> | head -20发现org.apache.ibatis.executor.loader.javassist.JavassistProxyFactory$BytecodeEntityLoader实例达 12 万,根源是 MyBatis 的延迟加载代理对象未释放。解决方案:在mybatis-config.xml中关闭延迟加载:
<settings> <setting name="lazyLoadingEnabled" value="false"/> <setting name="aggressiveLazyLoading" value="false"/> </settings>血泪经验:MyBatis 的
lazyLoadingEnabled=true在高并发下极易引发OutOfMemoryError: Metaspace,因为每个延迟加载属性都会生成一个 Javassist 字节码类。访客系统无复杂嵌套查询,关掉它,性能提升 30%,GC 频率归零。
我坚持在每次部署前用jstat -gc <pid>看一眼,就像开车前系安全带——它不会让你的系统更快,但能让你在凌晨两点接到告警电话时,30 秒内定位到是 GC 问题还是 SQL 慢查询。希望帮到你。
本文还有配套的精品资源,点击获取