☰
SSM+MySQL访客登记系统实战:表设计、三层架构与避坑指南
2026/9/29 19:29:20 网站建设 项目流程

简介:面向高校软件专业学生的学校访客登记系统毕业设计/课程设计资源,基于SSM(Spring+SpringMVC+MyBatis)与MySQL实现访客登记、查询与管理功能。系统采用典型分层架构:Spring管理业务对象与事务,SpringMVC处理Web层请求和数据封装,MyBatis负责数据库交互与持久化,代码结构清晰,适合Java初学者系统学习。资源包大小34.16MB,共957个文件,涵盖jsp页面、java源文件、class编译文件、jar依赖库、xml配置、css/js前端样式与脚本、gif/jpg图片素材,以及SQL建库脚本和Word、PPT说明文档,从运行环境到项目文档一应俱全。已有169人学习下载。压缩包内提供完整工程源码和数据库设计,按说明配置即可启动运行;附带的PPT和设计文档可直接用于答辩展示或功能讲解,也便于针对访客登记流程做二次扩展,对巩固SSM框架整合与MySQL表设计能力很有帮助。

1. 学校访客登记 + SSM + MySQL:一个值得亲手做一遍的入门组合

门卫的登记本换成网页表单,就是学校访客登记系统要解决的事。标题里的 SSM 指 Spring + SpringMVC + MyBatis,MySQL 是背后的存储,源码、论文和 PPT 则是课程设计或毕业设计常见的三件套交付物。为什么这个组合值得做?因为访客登记的业务边界比电商、办公系统小得多,要管的实体就几个:访客、被访教职工、预约单、进出记录、后台账号。SSM 把请求、事务和 SQL 拆成三层,正好把后端最常被问的框架问题全兜住。适合两类人:一是需要交课程设计或毕业设计的在校生,二是想用一个完整项目补 Spring 后端经验、准备转开发的在职者。这篇按我自己的落地顺序来讲:先建表,再写三层代码,最后把预约、登记、签离串起来。

2. 先建模再写码:访客登记系统的库表设计与 MySQL 初始化

SSM 项目写代码前,我把一半时间花在定表和字段上。MySQL 里表结构定了,MyBatis 的实体类、Mapper XML 全都要跟着走,后期改一张表,Controller、Service、页面都得动,代价很大。访客登记的业务量不大,但流程有分支:有的访客提前预约过,有的直接拿身份证来校门口登记。为了避免把不同状态搅在同一张表里,我习惯拆成下面这种分表方式。

2.1 拆业务建表:为什么是五张表而不是一张大表

先用一句话描述业务:访客是谁、找哪位教职工、有没有预约、什么时间进校、什么时间离校。这五个问题对应五张表就够了:visitor(访客档案)、staff(教职工名单)、appointment(预约单)、visit_log(进出记录)和 admin_user(后台管理员)。它们的分工用一张表就能讲清:

表名记录的是事实关键外键/索引与主流程的关系
visitor人的档案id_card 唯一索引登记时先查或先落这条
staff被访教职工无外键预约和登记时被选中
appointment预约关系visitor_id、staff_id可选,访客可预约也可直登
visit_log一次进出的事实visitor_id、enter_time每行代表一次入校
admin_user谁在后台操作username 唯一登录用,业务上独立

新手最容易踩的直觉误区是“访客表里存一个 visit_count 字段,省得每次 join 统计”。这个字段加上去,每次进出都要 UPDATE 主表,历史记录一多,visit_count 和 visit_log 就成了两个事实副本,两边迟早对不上。访问次数应当在报表页用 COUNT 动态算,MySQL 在这类小表上的聚合毫秒级返回,没必要牺牲一致性。

visitor 和 visit_log 之间的主从关系是整张模型的核心。visitor 存人,id_card 加唯一索引;visit_log 存事,同一个人可以出现多行。常见错法是直接把 id_card 塞进 visit_log,那同一个身份证会重复存储,改一次联系方式就得批量更新历史记录,没必要。保留 visitor 表,visit_log 只存 visitor_id,查询走主键和外键,变更也只动一张表。

2.2 建库建表 SQL:字段类型、索引和默认值

下面这段 SQL 按 MySQL 5.7 和 8.0 都能直接跑的方式写。建库时显式声明 utf8mb4,避开后期中文和特殊字符入库乱码。执行顺序是先建库、再建表,有外键的表放在被依赖表后面,否则 MySQL 会报“外键关联的表不存在”。

CREATE DATABASE IF NOT EXISTS school_visitor DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE school_visitor; CREATE TABLE visitor ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(30) NOT NULL COMMENT '访客姓名', id_card VARCHAR(18) NOT NULL COMMENT '身份证号', phone VARCHAR(11) DEFAULT '' COMMENT '手机号', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '建档时间', PRIMARY KEY (id), UNIQUE KEY uk_id_card (id_card) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='访客基础信息表'; CREATE TABLE staff ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(30) NOT NULL COMMENT '教职工姓名', dept_name VARCHAR(50) DEFAULT '' COMMENT '所在部门', office VARCHAR(50) DEFAULT '' COMMENT '办公室', phone VARCHAR(11) DEFAULT '' COMMENT '联系电话', status TINYINT NOT NULL DEFAULT 1 COMMENT '1在职 0停用', PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='被访教职工表'; CREATE TABLE appointment ( id INT NOT NULL AUTO_INCREMENT, visitor_id INT NOT NULL, staff_id INT NOT NULL, appoint_date DATE NOT NULL COMMENT '预约日期', appoint_time VARCHAR(20) DEFAULT '' COMMENT '希望到访时间段', reason VARCHAR(200) DEFAULT '' COMMENT '来访事由', status TINYINT NOT NULL DEFAULT 0 COMMENT '0待处理 1已通过 2已拒绝 3已取消', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_visitor (visitor_id), KEY idx_staff (staff_id), CONSTRAINT fk_app_visitor FOREIGN KEY (visitor_id) REFERENCES visitor (id), CONSTRAINT fk_app_staff FOREIGN KEY (staff_id) REFERENCES staff (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='预约单表'; CREATE TABLE visit_log ( id INT NOT NULL AUTO_INCREMENT, visitor_id INT NOT NULL, appointment_id INT DEFAULT NULL COMMENT '预约单ID,直接登记时为空', enter_time DATETIME NOT NULL COMMENT '进校时间', leave_time DATETIME DEFAULT NULL COMMENT '签离时间,空表示在校', remark VARCHAR(200) DEFAULT '' COMMENT '备注', PRIMARY KEY (id), KEY idx_enter_time (enter_time), CONSTRAINT fk_log_visitor FOREIGN KEY (visitor_id) REFERENCES visitor (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='进出记录表';

几个字段的取舍说明:status 用 TINYINT 而不是 VARCHAR,因为“待处理、已通过、已拒绝、已取消”是固定的四态,用数字存占用空间小,程序里定义常量比字符串比较更安全。appointment 表的 appoint_date 用 DATE,appoint_time 用 VARCHAR,因为校园预约一般只约到“上午、下午、晚上”,没必要拆成时间区间表。visit_log 的 appointment_id 允许为空,对应未预约直接到校登记的场景,这是流程里很重要的一支。

索引上不需要堆砌:visitor 的 id_card 建唯一索引,是业务层面的强约束,一个人不能有两份档案;visit_log 的 enter_time 建普通索引,是因为“今日访客、滞留未签离”这类查询全是按时间范围过滤的,没有索引时数据量过万后明细页会明显变慢。MySQL 里想让整数字段默认值为 0,直接在 DDL 写 DEFAULT 0 就行,比在 INSERT 语句里每行都带上更稳,也避免程序漏传字段时写入 NULL。

2.3 MySQL 连接配置:驱动名、时区和连接池参数

建完表开始接 SSM,第一步是 jdbc.properties。这里有一个隐藏最深的坑:MySQL 5 和 MySQL 8 的驱动类名不一样。如果本机装的是 MySQL 8,还用老写法 com.mysql.jdbc.Driver,Tomcat 启动时类加载都会失败。我给出一版 MySQL 8 下能直接跑的配置:

jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/school_visitor?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true jdbc.username=root jdbc.password=123456

参数逐个说:useUnicode 和 characterEncoding 保证中文以 utf8 传输,服务端建库也要对齐,否则出来就是问号;serverTimezone=Asia/Shanghai 是 MySQL 8 的强制要求,不写会抛异常说 CST 时区无法识别;useSSL=false 关掉加密连接,本地开发没有证书,开着反而会报 ssl 连接错误;allowPublicKeyRetrieval=true 针对 MySQL 8 默认的 caching_sha2_password 认证插件,不开的话某些驱动版本会连不上。如果你正好卡在 mysql 安装配置这一步,先在命令行跑mysql -uroot -p,能进去就说明服务起来了,剩下的就是授权和 URL 参数的事。

SSM 里数据源常用 Druid 连接池,spring 配置文件的 bean 长这样:

<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="${jdbc.driver}" /> <property name="url" value="${jdbc.url}" /> <property name="username" value="${jdbc.username}" /> <property name="password" value="${jdbc.password}" /> <property name="initialSize" value="5" /> <property name="maxActive" value="20" /> <property name="maxWait" value="60000" /> <property name="validationQuery" value="SELECT 1" /> </bean>

initialSize 是启动时建立 5 个连接,maxActive 是上限 20,maxWait 是拿不到连接时的等待毫秒数。validationQuery 写成 SELECT 1,作用是让连接池定期探活,避免 MySQL 端的空闲连接超时被断开后,首次请求拿到的还是“已死”的连接。这几项是受访客系统并发量不大却最容易出问题的参数,先按这个设,后面避坑章节再展开讲。

3. SSM 三层代码落地:Controller → Service → Mapper 的最小可跑骨架

表结构定完,进入 SSM 的编码环节。很多照着视频写的人卡在“不知道文件该放哪”。SSM 的 Maven 工程目录是约定俗成的,放错位置轻则扫描不到,重则启动直接 404。

3.1 项目结构和 Maven 依赖:先看包怎么分

一个最常用的骨架结构如下:

src/main/java com.school.visitor controller/ 接收请求,返回页面或 JSON service/ 业务逻辑和事务 mapper/ MyBatis 的 Mapper 接口 entity/ 与表字段对应的实体类 common/ 统一返回结果 Result、分页参数等 src/main/resources mybatis/mapper/ Mapper XML,和 Mapper 接口同名同路径 mybatis-config.xml spring/ spring 配置、springmvc 配置 jdbc.properties src/main/webapp WEB-INF/jsp/ 管理端页面和保安端页面

依赖里最容易被漏的是 jackson。SpringMVC 的 @ResponseBody 要把 Java 对象转成 JSON,没有 jackson-databind 会直接报 500。核心依赖我一般固定这几样:spring-webmvc、mybatis、mybatis-spring、mysql-connector-java、druid、jstl、jackson-databind。pom 的关键片段如下:

<dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.3.20</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.10</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.7</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.30</version> </dependency>

这里的版本是五三的通用稳定版,可以按你本机的 Spring 版本微调。核心是 mybatis-spring 必须存在,它负责把 MyBatis 的 SqlSessionFactory 交给 Spring 容器管理,否则 @Autowired 注入 Mapper 接口全会失败。接口和 XML 的对应关系,我的惯例是 Mapper 接口放 com.school.visitor.mapper.VisitLogMapper,XML 放 resources/mybatis/mapper/VisitLogMapper.xml,然后在 spring 配置里加上 mapperLocations 指向 classpath:mybatis/mapper/*.xml。这样做的好处是接新表时不用改配置文件,文件放进去就生效。

3.2 Controller 层:接收登记、按身份证查老访客

Controller 是请求入口。访客登记的核心操作是:保安输入身份证号,系统先查这个人在不在 visitor 表,在就复用档案,不在就建档;然后往 visit_log 插入一条进入时间记录。Controller 代码如下:

@Controller @RequestMapping("/visit") public class VisitLogController { @Autowired private VisitLogService visitLogService; @PostMapping("/register") @ResponseBody public Result register(@RequestBody VisitRegisterDTO dto) { if (dto.getIdCard() == null || dto.getIdCard().trim().isEmpty()) { return Result.error("身份证号不能为空"); } int logId = visitLogService.register(dto); return Result.ok("登记成功,记录ID=" + logId); } @GetMapping("/list") public String list() { return "visit/list"; } }

注意三个点:@PostMapping 把入口限制为 POST,避免浏览器直接拼 URL 造成脏数据;@RequestBody 接收 JSON,前端需要配合 axios 或 jQuery 的 JSON 提交,别用 form 表单;Result 是统一返回体,里面放 code、message、data 三个字段,避免每个接口各返回各的格式。list 方法返回字符串,SpringMVC 会去 WEB-INF/jsp/visit/list.jsp 找视图,这是 SSM 和前后端分离项目一个很重要的区别:页面跳转靠视图解析器,不靠重定向。

3.3 Service 层:事务边界和完整的登记方法

Service 层最容易被问到的就是事务。登记业务里包含两步:第一步查访客或插入访客,第二步插入 visit_log。这两步必须同生共死,否则会出现 visit_log 里有记录但 visitor 表没有档案的情况。ServiceImpl 的写法如下:

@Service public class VisitLogServiceImpl implements VisitLogService { @Autowired private VisitorMapper visitorMapper; @Autowired private VisitLogMapper visitLogMapper; @Override @Transactional(rollbackFor = Exception.class) public int register(VisitRegisterDTO dto) { Visitor visitor = visitorMapper.findByIdCard(dto.getIdCard()); if (visitor == null) { visitor = new Visitor(); visitor.setName(dto.getName()); visitor.setIdCard(dto.getIdCard()); visitor.setPhone(dto.getPhone() == null ? "" : dto.getPhone()); visitorMapper.insert(visitor); } VisitLog log = new VisitLog(); log.setVisitorId(visitor.getId()); log.setAppointmentId(dto.getAppointmentId()); log.setEnterTime(new Date()); log.setRemark(dto.getRemark()); visitLogMapper.insert(log); return log.getId(); } }

@Transactional(rollbackFor = Exception.class) 必须写全。默认情况下 Spring 事务只对 RuntimeException 回滚,如果 visitLogMapper.insert 抛的是 IOException 或自定义的 checked exception,不加 rollbackFor 就只提交不回滚,访客表已插入的数据就留下了。这个细节是答辩时最高频的追问点,也能直接展示你是否真的理解事务。查询方法不要加 @Transactional,纯查询加事务等于白占连接池资源,并发一高就把连接耗尽。

3.4 Mapper 层 XML:动态 SQL 与参数绑定

Mapper 接口只写方法签名,真正的 SQL 在 XML 里。访客列表中“按姓名模糊查、按手机号精确查、不传条件就查全部”是典型场景,动态 SQL 正好解决:

<select id="findVisitorPage" resultType="com.school.visitor.entity.Visitor"> SELECT id, name, id_card, phone, create_time FROM visitor <where> <if test="name != null and name != ''"> AND name LIKE CONCAT('%', #{name}, '%') </if> <if test="phone != null and phone != ''"> AND phone = #{phone} </if> </where> ORDER BY create_time DESC </select>

标签会自动去掉第一个条件前面的 AND,这是 MyBatis 里最实用的语法,避免拼 SQL 时出现 WHERE AND name LIKE 这种错误。参数绑定必须用 #{},它会生成预编译占位符,防 SQL 注入;${} 是字符串拼接,只适合传 ORDER BY 的列名这种受信任值,绝不能用它拼用户输入。resultType 指定实体类全限定名,字段名和下划线列名的映射要在 mybatis-config.xml 里开启 mapUnderscoreToCamelCase,这样 id_card 才能自动映射到 idCard,不然查出来全是 null。

4. 把预约、进校登记、离校签离串成一条完整流程

表和三层代码都有了,接下来要把几条独立的功能拼成完整流程。一个访客从进校到离校,状态要经历预约(可选)到登记再到签离。每一步都要有对应的数据落库。

4.1 预约状态机和管理端审批

访客在手机端填预约单,选择教职工、日期和原因。管理端看到的是一条状态为“待处理”的记录。审批通过后,访客到校门报身份证号,保安在登记页面能看到这条预约,一键带入访客信息。预约状态建议按下表设计:

status 值含义后续动作
0待处理教职工或管理员审批
1已通过可到校登记,关联 visit_log
2已拒绝流程结束,可备注原因
3已取消访客自行取消,流程结束

审批动作的 Service 逻辑不复杂,但有个规范要守:update 语句要带 status 条件。例如把“待处理变成已通过”时,SQL 应写成UPDATE appointment SET status = 1 WHERE id = #{id} AND status = 0。这样能防止两个管理员同时点审批,导致业务状态被覆盖。如果 update 返回行数为 0,说明这条预约已经被别人处理过了,直接提示“该预约已处理,请刷新”,而不是盲目覆盖成通过。

4.2 进校登记:老访客复用和记录落库

进校登记分两种情况。查询 visit_log 时,先按身份证查预约记录,如果能查到“已通过”的今天预约,把预约单里的 staff_id 和 reason 带到 visit_log 里,保安不用重复录入;查不到就直接走 3.3 的直登流程。这里我用一个前端流程描述来替代代码:

  1. 保安输入身份证号,页面发 AJAX 请求查访客档案,有则回显姓名、电话,无则显示“新访客”。
  2. 选择被访教职工,教职工下拉框按部门分组,数据来自 staff 表。
  3. 若有有效预约,自动带出预约信息,被访人和来访事由不许改,只允许补备注。
  4. 点击登记,后端 service 执行事务,写入 visit_log,enter_time 为当前时间。

这一步的实际开发重点在“无效预约”的判定。预约的 appoint_date 等于今天、status 等于 1,两个条件缺一不可。有人会把 status 等于 0 也算有效,结果访客到了门口还进不去,又改成只要记录存在就放行,导致已拒绝的预约也能登记。判定条件我一般单独抽成一个私有方法,返回布尔值,方便写单元测试也方便答辩时讲。

4.3 离校签离与滞留提醒:一条 SQL 和一条定时任务

签离功能是最简单的更新操作,按记录 ID 把 leave_time 置为当前时间即可。难的是“哪些人还没走”的查询。一个实用 SQL 是查所有签离时间为空且进入时间早于 8 小时的记录:

SELECT vl.id, v.name, v.phone, vl.enter_time, s.name AS staff_name FROM visit_log vl JOIN visitor v ON v.id = vl.visitor_id LEFT JOIN appointment a ON a.id = vl.appointment_id LEFT JOIN staff s ON s.id = a.staff_id WHERE vl.leave_time IS NULL AND vl.enter_time < DATE_SUB(NOW(), INTERVAL 8 HOUR) ORDER BY vl.enter_time;

这段 SQL 用了两个 LEFT JOIN,把预约单和教职工信息连出来,方便保安直接看到要找谁。滞留提醒在 SSM 里可以用 Spring 的定时任务实现,在配置类或 XML 开启 task 命名空间后,方法上写 @Scheduled 注解:

@Component public class StuckVisitorTask { @Autowired private VisitLogMapper visitLogMapper; @Scheduled(cron = "0 0 8 * * ?") public void checkStuck() { List<VisitLog> stuckList = visitLogMapper.findStuckVisitors(); if (stuckList != null && !stuckList.isEmpty()) { // 推送提醒给保安端,或生成待处理消息 } } }

cron 表达式的意思是每天早上 8 点整执行一次。注意这里的“8 小时”阈值要放到配置项里,不要写死在代码中。不同学校的校门开放时间不同,有的学校晚上 10 点封校,那这个阈值可能得改成 4 小时。定时任务里只做查询和推送,不做写操作,避免同一批滞留访客在多个任务实例里被重复处理。

4.4 后台报表:今日访客、部门访问量两张统计图

完整访客系统还需要给管理员一点统计能力,哪怕只是两张简单的表。今日访客数用这个 SQL 就能拿到:

SELECT COUNT(*) AS today_count FROM visit_log WHERE enter_time >= CURDATE() AND enter_time < DATE_ADD(CURDATE(), INTERVAL 1 DAY);

部门统计可以按 visit_log 关联预约单和教职工表,按 dept_name 分组计数,再用 ECharts 画个柱状图。这里不需要做复杂的报表引擎,MySQL 的实时聚合足够。演示 PPT 里放这两张图,比放十张列表页截图更说明问题,尤其能体现“系统能回答管理者的提问”,而不只是能增删改查。

5. 排查与避坑:SSM 访客系统最常见的 5 个踩坑点

这一章也是血泪经验。自己在部署和演示这个项目时翻过几次车,每一条都对应一个明确的根源。下面的现象、原因、解决三步,照着排错比看异常栈乱猜快得多。

5.1 MySQL 8 连接失败:驱动名、时区、公钥三项查一遍

现象:Tomcat 启动时报java.sql.SQLException: Unable to load authentication plugin 'caching_sha2_password',或者能加载驱动但连接超时。

原因分三种:驱动类名还是 com.mysql.jdbc.Driver、URL 缺少 serverTimezone、MySQL 8 默认认证插件与旧驱动不兼容。

解决:把驱动改成 com.mysql.cj.jdbc.Driver,URL 加上 serverTimezone=Asia/Shanghai,并配置 useSSL=false 和 allowPublicKeyRetrieval=true。如果本机同时装了 MySQL 5 和 8,注意看端口,连到 3306 的到底是哪一台。检验是否连通的命令是mysql -uroot -p -h127.0.0.1 -P3306,能进就说明服务和端口没问题,问题在应用配置。

5.2 中文乱码:建库、连接串、页面三层必须一致

现象:插入访客姓名后,MySQL 里显示“???”或“鍚?”,页面读出来是乱码。

原因:数据库是 utf8 或 latin1,连接串 characterEncoding 没设置,JSP 页面没有声明 UTF-8,三层任一层掉链子都会乱。

解决:第一步用下面语句把表转换过来:

ALTER TABLE visitor CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

第二步保证连接串里带 characterEncoding=utf8mb4,第三步在 JSP 顶部写<%@ page pageEncoding="UTF-8" %>,同时把 Tomcat 的 URIEncoding 设为 UTF-8。三个地方都对齐后还乱码,就用 MySQL Workbench 或 Navicat 打开表看看底层字符集是哪个,直接排查最底层的事实,别在页面层反复试。

5.3 身份证号唯一索引把演示数据整懵了

现象:为了演示多次进校,准备了三组访客数据,导入第二次时报Duplicate entry 'xxx' for key 'uk_id_card'。

原因:visitor 表的 id_card 唯一索引生效,演示数据里同一个身份证号被当成新访客插入了两次。这在逻辑上是对的,但演示脚本没考虑幂等。

解决:初始化演示数据的 SQL 要按“存在则更新,不存在则插入”来写,用 MySQL 的 ON DUPLICATE KEY UPDATE:

INSERT INTO visitor (name, id_card, phone) VALUES ('张三', '110101199001011234', '13800000000') ON DUPLICATE KEY UPDATE phone = VALUES(phone);

第二次执行就不再报错,只更新手机号,主键不变。后面 visit_log 插入时复用这个 visitor_id,就能做出“同一个老访客多次进校”的效果。演示数据脚本要能重复执行,这是答辩前最重要的检查项。

5.4 统计“今日访客”把没签离的历史数据算进去了

现象:酒店顶层页的“今日访客”数字越来越大,明明是今天的统计,昨天没签离的访客全被算进来。

原因:SQL 写成WHERE DATE(enter_time) = CURDATE(),只看进入日期,没有限定签离状态。昨天进来今天还没走的人,进入日期是昨天,DATEDIFF 计算错误。

解决:把 SQL 拆成两块:已签离的按 leave_time 归属到相应日期,未签离的按 enter_time 归属并作为“在访”单独计数:

-- 今日已签离 SELECT COUNT(*) FROM visit_log WHERE leave_time >= CURDATE() AND leave_time < DATE_ADD(CURDATE(), INTERVAL 1 DAY); -- 当前在校 SELECT COUNT(*) FROM visit_log WHERE leave_time IS NULL;

这段 SQL 把“发生于今天”“当前状态为在校”这两个概念分开,逻辑上不打架。统计报表的 SQL 写完后,我习惯用已知数据手工验一遍,比如上午登记 3 人签离 2 人,预期今天签离数就是 2。别迷信聚合结果。

5.5 连接池耗尽:界面卡死,重启 Tomcat 才恢复

现象:系统运行一小时后突然卡死,后续请求全部等待,日志出现waiting for connection,重启 Tomcat 又能撑一阵。

原因:连接没有归还连接池。最典型的是查询方法误加 @Transactional,事务未提交导致连接不释放;或者是 Mapper 查询耗时长,把连接占满。

解决:先检查所有 Service 方法,删掉查询方法上的 @Transactional;再检查事务是不是在 try-catch 里被吞掉,事务拦截器只有在异常传播到代理时才会回滚并释放连接。Druid 的监控页能直接看到活跃连接数和泄露数量,建议临时打开stat-view-servlet,万不得已时用removeAbandoned=true兜底,把超过时间未归还的连接强杀。这一步是黑匣子变透明的关键。

6. 答辩演示前的最后一步:准备演示数据集和验收脚本

演示日和日常使用是两回事。答辩现场只有十分钟,不要让评委看你自己手忙脚乱敲数据。我习惯在演示前把流程跑成一套可重复的“演示剧本”,对应三条数据线:新访客现场登记、老访客二次进校、已预约访客到校核验。三条线各自准备一条 SQL 初始化数据,演示前一键重置到初始状态。

给这个项目准备验收清单比具体代码更重要。按功能维度自测一遍,能避免现场翻车:

功能点测试输入预期结果
新访客登记身份证未在库中自动建档并生成 visit_log
老访客登记身份证已在库中不重复建档,只新增 visit_log
预约核验预约单状态为已通过自动带出被访人,不可改
无效预约拦截预约单状态为已拒绝提示无有效预约
签离点签离按钮leave_time 写入当前时间
滞留查询修改 enter_time 为 9 小时前出现在滞留名单

再往下走一步,这些新功能点也是论文里“功能测试”章节的现成素材,把预期结果和截图放进去,比堆十页代码截图有说服力。做完这套系统后我会再看一眼 Spring Boot:SSM 的 Controller、Service、Mapper 分层照搬过去,只把 XML 换掉,就能变成 Spring Boot 项目,简历上两种写法都能聊。SSM 让我看清了请求怎么流转、事务怎么生效,它适合当第一块完整项目的垫脚石。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询