简介:一套基于Spring Boot框架的来访管理系统完整源码,面向需要开发访客登记、预约与统计功能的后端学习者,也适合中小型场所的来访管理场景。系统区分管理员与访客两种角色,覆盖访客注册登录、密码重置、资料维护,以及访客预约、来访记录的新增、查询、修改、删除和分页统计等操作,并针对日期范围筛选提供提醒接口,可直接作为课程设计或生产项目基础。压缩包共含361个文件,大小约9.07MB,主体为67个Java后端文件与34个Vue页面、21个JS脚本、7个CSS样式,另配有SQL建库脚本、XML/YML配置、SVG/PNG/JPG等前端素材和部署批处理文件,源码目录层次清晰,便于按功能模块阅读。目前已有27人学习浏览。通过该工程可以同时查看管理员端与访客端的菜单配置和接口实现,也能借助预约及来访统计模块理解分页查询、按值/按时间分组统计等常见业务写法,适合毕业设计、课程实训或Spring Boot项目练习。
1. 一套基于 Spring Boot 的来访管理系统,拿到手先确认三件事
把公司前台那本写满手写字迹的纸质来访登记换成一整套线上流程,是我拿到这套基于 Spring Boot 框架的来访管理系统源码时想到的第一个真实场景。访客提前提交预约、被访人在后台审批、门岗扫码核销、离开时自动归档,再加上黑名单拦截和记录查询,这套代码把最常见的访客管理闭环都覆盖了。源码包里带着前端页面、数据库初始化脚本和可以直接编译运行的 Spring Boot 后端,适合想在公司或园区快速搭一套访客登记后台的人,也适合拿它当完整业务项目来学 Spring Boot 的开发者。下面顺着拆包、跑通、改造、防坑这条路径来讲,每一步都可以直接照着做。
2. 技术底子先摸清:pom版本、核心数据表、脚本导入前的三个陷阱
拿到任何一个 Spring Boot 源码包,我一般不会直接点 Run。Spring Boot 项目能不能在本地跑起来,90% 在运行之前就已经能判断了。先花十分钟看两个文件:pom.xml和数据库初始化脚本。这两个文件能告诉你本机要不要装 Redis、用哪个版本的 JDK、数据库叫什么、默认管理员账号是什么。跳过这一步直接启动,后面报错时再回头查,花费的时间往往是十分钟的好几倍。
2.1 打开 pom.xml:Spring Boot 版本决定了 JDK 和依赖走向
pom.xml的parent节点是整个依赖树的根,它锁定了 Spring Boot 的版本,也就锁定了你本机必须具备的 JDK 版本。以最常见的 Spring Boot 2.7 系为例(你手头那份以 pom 里实际写的为准),结构长这样:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.x</version> <relativePath/> </parent>版本号是几,直接决定了后端代码里的包名是javax.*还是jakarta.*。如果是 2.x,JDK 8 或 11 都能跑;如果是 3.x,JDK 17 起步,装了 JDK 8 的话连编译都过不去。我在本地遇到过最典型的翻车就是 JDK 21 跑 Spring Boot 2.7,反射相关的警告刷屏,某些老版本 Lombok 直接在编译阶段失效。
再看dependencies部分,主要判断四类东西:有没有spring-boot-starter-web(没有它 HTTP 接口都起不来);ORM 用的是spring-boot-starter-data-jpa还是 MyBatis / MyBatis-Plus(决定后面看数据操作的代码方式);有没有spring-boot-starter-data-redis(有就说明本机必须把 Redis 拉起来,否则启动可能失败);有没有spring-boot-starter-security或 Shiro(决定登录校验到底走框架过滤器还是自定义拦截器)。
2.2 核心数据表串起来看:预约、访问记录、黑名单怎么协作
来访管理系统的业务闭环是这样的:访客先填一张预约单,写明姓名、手机号、来访事由、要找谁、打算什么时间来;被访人审批通过后,访客到门口亮出预约凭证;门岗核销后生成一条实际到访记录;离开时再做签退;如果某个访客被列入黑名单,他在预约环节就会被拦下。
从数据模型上看,这套闭环最少需要四张表:visitor存访客的基本档案(手机号、证件号),appointment存每一次预约的时段和状态,visit_log存实际到访的签到签退记录,blacklist存被拉黑的手机号或证件号。访客档案和预约记录拆开而不是全塞一张表,是因为同一个人会反复来访,档案只需要存一份,而预约记录可以无限累积,这样查询历史记录时不会把访客基础信息也翻出来一遍。状态字段用TINYINT而不是字符串存,省空间而且索引效率高,代价是代码里必须维护一套状态常量,不然看数据库的人不知道3代表什么:
CREATE TABLE appointment ( id BIGINT PRIMARY KEY AUTO_INCREMENT, visitor_id BIGINT NOT NULL COMMENT '访客ID,关联 visitor 表', host_user_id BIGINT NOT NULL COMMENT '被访人ID,关联内部用户表', visit_date DATE NOT NULL, start_time TIME NOT NULL, end_time TIME NOT NULL, purpose VARCHAR(200), status TINYINT DEFAULT 0 COMMENT '0待审批 1已通过 2已拒绝 3已完成 4已过期', create_time DATETIME DEFAULT CURRENT_TIMESTAMP );这段建表语句只是最常见的设计参考,不是让你拿它替换源码里的脚本——源码包里sql目录下的初始化脚本才是真正要导入的。看的时候重点观察两点:表之间有没有外键关联,以及状态字段的取值范围。理解了表关系之后,再去看 Controller 和 Service 里的查询代码,会顺畅很多,不会出现「这个字段哪来的」的困惑。
2.3 初始化脚本导入前,先查默认账号、表前缀和字符集
初始化脚本导入之前,有三个位置必须肉眼扫一遍。第一,找INSERT语句里的管理员账号。很多项目会预置一个admin,密码是123456或admin123,但存储形式一定是 BCrypt 摘要而不是明文。如果你看到的是明文,说明这套代码的登录校验很可能不是标准的 Spring Security 密码校验,后面登录时要特别留意。
第二,看表前缀。有的项目习惯给表加t_前缀,比如t_visitor,如果 ORM 用的是 MyBatis-Plus,实体类上的@TableName注解会和这个前缀对应。嫌前缀丑想去掉的话,要同时改实体注解和 SQL 脚本,只改一边必然启动报错。
第三,确认脚本里的字符集设置。CREATE DATABASE语句如果带着DEFAULT CHARACTER SET utf8mb4最好;没有的话,导入后可能中文乱码,而且这种乱码不是前端能修的。导入命令我一般分两步走:
mysql -uroot -p -e "CREATE DATABASE IF NOT EXISTS visit_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;" mysql -uroot -p visit_system < sql/init.sql第一条-e参数表示执行后面的建库语句,utf8mb4是完整的 UTF-8 编码,能存下中文和 emoji;第二条命令把sql/init.sql的内容导入到visit_system库里。如果你用的是 Navicat 这类图形工具,直接运行脚本文件也可以,但记得在执行前把连接和库的字符集都切到utf8mb4,否则照样乱码。
3. 本地跑通最小路径:解压、改配置、启动、登录
把源码包解压后,你会看到一个标准的 Maven 工程结构:src/main/java放后端 Java 代码,src/main/resources放配置文件和可能打包好的前端静态页面。跑通它需要经过四个环节:环境对齐、配置修改、编译启动、登录验证。前一章花时间读 pom 和脚本的功夫,在这一步会全部兑现——环境对齐几乎不用试错。
3.1 环境对齐:JDK、Maven、MySQL 版本先过一遍
打开命令行或终端,按顺序敲这三条命令:
java -version mvn -v mysql --version三条命令的输出决定了整个环境能不能往下走。java -version看的不是有没有装 JDK,而是版本号是否落在 pom 对应范围内:Spring Boot 2.x 用 JDK 8/11 稳妥,3.x 必须 17 以上。mvn -v会连带显示 Maven 版本,3.6 以上基本够用。mysql --version只用来确认客户端存在,真正起作用的是你本地 MySQL 服务的版本——如果是 MySQL 8,连接 URL 里的驱动和参数就得按 8 的规矩来,这一点第 5 章细说。
版本差异在 Spring Boot 里表现得特别敏感。我见过一位同事的机器上装的是 JDK 17,手头的项目却是 Spring Boot 2.7 配合老版本 Lombok,编译时报java.lang.NoSuchFieldError,查了半天才发现是 JDK 版本把 Lombok 的注解处理器顶掉了。所以版本对齐这件事,宁可多花五分钟确认,不要等报错再返工。
3.2 配置改动最小化:只有数据库连接是必改项
打开src/main/resources/application.yml或application.properties,真正必改的只有一个数据源块。其它配置如端口、日志级别、上传大小,保持默认就能跑。以最常见的 YAML 写法为例:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/visit_system?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.DriverURL 里有三个参数很容易被人忽略,逐个解释:serverTimezone=Asia/Shanghai是必须的,MySQL 8 的驱动要求明确时区,不写就报错;useSSL=false关闭 SSL 握手,本地开发环境不需要这个开销;allowPublicKeyRetrieval=true解决 MySQL 8 的 caching_sha2_password 认证方式在非 SSL 连接下的公钥获取问题,不写的话某些驱动版本会直接拒绝连接。
username和password换成你本机的数据库账号。另外注意driver-class-name用的是com.mysql.cj.jdbc.Driver,带cj的是 MySQL 8 的驱动类名,老写法com.mysql.jdbc.Driver对应 MySQL 5.x。这里建议对照一下 pom 里 MySQL 驱动的版本,驱动 8 连数据库 5 也会出问题。如果项目里用了 JPA,还要看spring.jpa.hibernate.ddl-auto的值,建议设成none,防止 Hibernate 启动时自动改表结构——尤其是你已经导入了初始化脚本的情况下,update可能会把字段类型改得和预期不符。
3.3 启动、登录、调接口:三步确认项目活了
配置改完,启动方式有两种。用 IDEA 的话,直接找到带有@SpringBootApplication注解的主类,点 Run 即可;命令行方式更接近部署现场:
mvn spring-boot:run第一次跑会下载大量依赖,耗时几分钟到十几分钟不等,这是正常现象。看到类似Tomcat started on port(s): 8080的日志,说明后端已经起来了。这时候打开浏览器访问http://localhost:8080,应该能看到登录页面或首页。如果页面直接报 404,多半是前端静态资源没有打进resources/static目录,先别急着改代码,看第 5 章的排查思路。
登录这一步是验证系统是否真正工作的关键。用初始化脚本里查到的默认账号登录。如果想绕过页面直接验证后端接口活着,可以拿登录接口试一下:
curl -X POST http://localhost:8080/api/auth/login \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"123456"}'返回 JSON 里通常会带 token 或用户基本信息,说明整个链路——配置、数据库连接、MyBatis/JPA 映射、Controller 路由——都通了。如果返回 500,优先看控制台最底部的异常栈,Spring Boot 的日志会把具体是数据库连不上还是 SQL 语句写错说得非常清楚。
4. 改成自家能用:预约审批和门岗登记的核心改造
跑通只是起点,把这套系统改成自己公司能用的形态,才是投入精力的主要部分。三个场景最常被改:预约审批流程、门岗登记核销、默认参数配置。改之前先理解代码的分层逻辑,这套系统的后端按 Controller → Service → Mapper/Repository 三层组织,Controller 只负责收参数和返回结果,真正的业务判断都在 Service 里。
4.1 预约到审批的状态流转:别在 Controller 里堆业务
审批接口的常见实现是:前端把预约单 ID 和审批意见传给后端,后端查出预约单,校验当前状态是不是「待审批」,是的话改成「已通过」或「已拒绝」,并记录审批人和审批时间。用 MyBatis-Plus 的话,Service 核心方法大体长这样:
@Service public class AppointmentService { @Transactional public Appointment approve(Long appointmentId, Long operatorId) { Appointment app = appointmentMapper.selectById(appointmentId); if (app == null) { throw new BizException("预约单不存在"); } // 只有待审批状态才能流转,防止重复审批覆盖结果 if (!AppointmentStatus.WAITING.equals(app.getStatus())) { throw new BizException("当前状态不可审批"); } app.setStatus(AppointmentStatus.APPROVED); app.setApproverId(operatorId); app.setApproveTime(LocalDateTime.now()); appointmentMapper.updateById(app); return app; } }这段逻辑的核心在@Transactional和状态判断。@Transactional保证「查状态 → 改状态 → 更新库」三步要么全成功,要么全失败,不会出现更新到一半程序崩溃、数据停留在中间态的情况。状态判断放在 Service 而不是 Controller,是因为业务规则不应该依赖前端传参——哪怕有人绕过前端直接调接口,Service 里的校验依然有效。
这段代码的边界在于并发。两个审批人同时打开同一个待审批预约单,都执行了查询,都看到状态是「待审批」,然后都执行更新,后更新的会覆盖先更新的。公司内部访客量不大的话可以不管,但如果要严谨处理,需要给appointment表加一个version字段做乐观锁,这个放到最后一章展开。
4.2 门岗登记/核销的核心判断:状态、时间窗、黑名单
门岗核销是这套系统里业务逻辑最密的一段。访客到门口出示预约凭证(通常是一个二维码,内容一般是预约单 ID 或随机短码),门卫扫一下,后端要依次校验:预约单存在且状态是「已通过」、当前时间落在预约时间窗内、访客不在黑名单。三个条件全部通过,才写入一条visit_log:
public VisitLog checkIn(String qrCodeContent, Long guardId) { Long appointmentId = parseIdFromQr(qrCodeContent); // 从二维码内容解析预约单ID Appointment app = appointmentMapper.selectById(appointmentId); if (app == null || !AppointmentStatus.APPROVED.equals(app.getStatus())) { throw new BizException("二维码无效或预约未通过审批"); } LocalTime now = LocalTime.now(); // 允许提前/延后各30分钟,窗口值应该做成配置而不是写死 if (now.isBefore(app.getStartTime().minusMinutes(30)) || now.isAfter(app.getEndTime().plusMinutes(30))) { throw new BizException("当前不在可登记时间范围内"); } if (blacklistService.isBlocked(app.getVisitorId())) { throw new BizException("该访客已被列入黑名单,请联系安保处理"); } VisitLog log = new VisitLog(); log.setAppointmentId(app.getId()); log.setVisitorId(app.getVisitorId()); log.setCheckInTime(LocalDateTime.now()); log.setGuardId(guardId); visitLogMapper.insert(log); return log; }校验顺序是有讲究的。先查预约单状态,再查时间窗,最后查黑名单——把开销最小的数据库单行查询放前面,命中黑名单这种需要额外查询的放最后,大多数无效请求在第一步就被拦掉了。黑名单检查必须放在写入visit_log之前,否则就会出现「访客已经被拉黑,但仍然产生了一条到访记录」的脏数据,后面做统计报表时会非常难解释。
这里的时间窗口30分钟是写死的示例,实际开发中绝对不要这么干。不同企业对「允许提前多久到访」的容忍度完全不一样:写字楼可能严格卡点,园区可能允许提前半小时进。把这个值挪到配置文件里,是这套系统第一个值得做的改造。
4.3 上线前最该改的三处:默认账号、审批人、可预约时段
第一处是默认账号。初始化脚本里的admin密码是公开的,上线前要么在系统里改掉,要么直接生成一个新的 BCrypt 摘要替换脚本里的值。第二处是审批人。演示数据里的被访人往往是写死的用户 ID,你得替换成自己团队的成员,否则访客选了某个人,实际审批的却是另一个账号。第三处是预约规则参数。这类参数适合集中放在application.yml里:
visit: reserve: max-days-ahead: 7 # 最多允许提前7天预约 approve-required: true # 预约是否需要审批,false则提交后直接通过 gate: early-minutes: 30 # 允许提前到达的分钟数 late-minutes: 30 # 允许延后到达的分钟数要让这些配置生效,后端需要一个配置类来接收:
@Component @ConfigurationProperties(prefix = "visit") public class VisitProperties { private boolean approveRequired = true; private int maxDaysAhead = 7; // 省略 getter / setter;Spring 会自动把 yml 里的值绑定进来 }@ConfigurationProperties会把 YAML 里visit.reserve.max-days-ahead自动绑定到maxDaysAhead字段上。这样以后调整预约策略,只需要改配置文件再重启,完全不用动代码。这是 Spring Boot 里最值得学会的工程习惯:把业务策略和代码逻辑剥离,而不是让运维改一行参数都要翻代码。
5. 避坑记录:跑这套源码最容易翻车的4个地方
这一章全部来自实际操作里见过和踩过的坑。每一个都是「现象 → 原因 → 解决」的结构,你可以直接拿现象对照自己遇到的问题,如果匹配,照做就能解决。
5.1 MySQL 时区与 SSL 导致启动失败
现象:启动 Spring Boot 应用时,日志在数据源初始化阶段抛异常,出现The server time zone value '�й���ʱ��' is unrecognized或者Communications link failure这样的字样,应用启动直接失败。
原因:这是 MySQL 8 的两个独立问题叠加在一起。第一,MySQL 8 驱动要求连接 URL 里显式指定时区,本地数据库的时区设置无法被驱动正确识别,中文环境下还会出现乱码形式的时区名。第二,MySQL 8 默认开启 SSL 握手,本地开发库没有配置证书,握手失败就会报连接链路错误。
解决:在 JDBC URL 后补齐三个参数,一次同时解决两个问题:
url: jdbc:mysql://localhost:3306/visit_system?serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=trueserverTimezone=Asia/Shanghai是时区问题的标准答案,useSSL=false关掉 SSL 握手,allowPublicKeyRetrieval=true配合解决认证公钥获取的问题。如果改完还报错,再看一眼 pom 里的 MySQL 驱动版本,别用 5.x 的驱动连 8.x 的库。
5.2 Lombok 不生效,IDE 一直报找不到 getter/setter
现象:代码里明明写了@Data注解,Service 里调用app.getStatus()时 IDE 却红线报错cannot find symbol method getStatus()。最迷惑人的是,有时候命令行mvn compile能通过,IDE 里却一片红。
原因:Lombok 是一个编译期注解处理器,它不依赖运行时,而是在编译阶段生成 getter/setter 方法。IDE 里报错,要么是 Lombok 插件没装,要么是 IDE 的注解处理开关没打开。另一种情况是 JDK 版本过高,老版本的 Lombok 在新 JDK 上注解处理器直接失效,编译期不生成任何方法,于是所有调用点集体报错。
解决:安装 Lombok 插件;打开 IDEA 的设置,在Build, Execution, Deployment → Compiler → Annotation Processors里勾选Enable annotation processing;如果本机 JDK 在 17 以上,在 pom 里把 Lombok 的版本升到较新版本(去 Maven 仓库查当前可用版本)。改完记得点 Maven Reload 重新加载项目,再不行就 Rebuild Project。
5.3 接口 404 和浏览器跨域拦截
现象:登录页面能打开,但输入账号密码后一直转圈或报错,F12 开发者工具里看到请求/api/auth/login返回 404;或者控制台出现Access to XMLHttpRequest has been blocked by CORS policy的红色报错。
原因:404 通常是因为前端页面和后端接口不在同一个服务里。很多 Spring Boot 管理系统的前端是打包后放在resources/static下的,如果你跑的是独立前端工程(比如 Vite 起的 5173 端口),页面里的请求会打到前端自己的开发服务器上,这个服务器根本没有/api/auth/login接口,自然 404。跨域报错则是前后端分离开发时的典型问题,浏览器不允许 5173 端口的页面直接访问 8080 端口的接口。
解决:开发期最简单的方案是给前端加代理,把/api开头的请求转发到后端 8080。后端如果不想动前端,也可以加一个全局跨域配置:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE") .allowedHeaders("*") .allowCredentials(true); } }需要注意的是,allowedOriginPatterns("*")配合allowCredentials(true)只适合本地联调,正式上线时要把*换成具体的域名,否则谁都可以跨域调用你的接口。如果页面能打开但接口 404,先确认请求实际发到了哪个端口——看 Network 面板里的请求地址,比看代码快得多。
5.4 端口被占、Redis 没启动,项目起一半就退出
现象:启动日志打了几行后卡住或直接退出,报Port 8080 was already in use;或者扫到 Redis 相关依赖时报Connection refused: localhost/127.0.0.1:6379。
原因:8080 被占是最常见的,本机另一个服务或上次没关掉的应用还占着这个端口。Redis 的问题则是项目里引入了spring-boot-starter-data-redis,但本机根本没有启动 Redis 服务,Spring Boot 在初始化 Redis 连接池时连接被拒绝。有些项目即使业务没真正用到 Redis,只要引入了依赖,启动时也会建立连接。
解决:端口被占时,要么换端口启动:
mvn spring-boot:run -Dspring-boot.run.arguments=--server.port=8081要么找到占端口的进程关掉。Redis 的问题分两步判断:如果项目确实用到了 Redis(比如验证码存储、Token 缓存),就把本机 Redis 启动起来;如果只是引了依赖但业务没用,可以先把 pom 里的 Redis starter 临时注释掉,重启确认对业务没影响后再决定去留。注释依赖这种操作要谨慎,确认所有报错都和 Redis 无关再动,别把正常运行的功能改坏。
6. 进阶:二次开发前先验证这三件事
跑通了、也改造成自家能用的样子了,接下来别急着加功能,先做三件验证,每一件都能暴露潜在问题。
第一件,压测预约接口。用 JMeter 或 Apache Bench 对「提交预约」接口做一轮并发测试,20 个并发用户同时提交预约,看看会不会出现状态覆盖、重复预约或者数据库连接池耗尽。预约系统在真实场景里很少出现高并发,但一旦出现,问题往往出在你在第 4 章看到的那种「先查后改」没有加锁的代码上。如果测出问题,给appointment表加一个version字段,更新语句改成UPDATE appointment SET status = ?, version = version + 1 WHERE id = ? AND version = ?,这是代价最小的乐观锁方案。
第二件,验证配置外置是否彻底。把application.yml里的数据库密码、预约窗口参数这些和环境相关的配置,拆成application-dev.yml和application-prod.yml两套,启动时用--spring.profiles.active=prod指定环境。这样部署到现场时就不用改代码,只改启动命令或环境变量。
第三件,做一次完整的业务回归。我习惯写一张简单的检查表:访客提交预约 → 被访人审批通过 → 门岗在时间窗口内登记 → 离场签退 → 在记录查询里能看到完整链路。每一步都核对数据库里对应的状态字段。有一次我在改造审批流程时,漏掉了「预约已过期」的状态判断,结果访客拿着一个上周的预约单也进了门。回归检查的目的就是把这些状态边界一个个敲实。
最后说个习惯:以前我拿到源码包,第一反应都是直接运行,跑不起来再四处问人。后来养成的习惯是——先读配置、先导数据、先看状态字段设计,再点启动。有一回因为没核对初始化脚本里账号密码的加密方式,我一直用普通加密去登录,折腾了半个下午才发现数据库里存的是 BCrypt 摘要。这种坑本来五分钟就能避免。希望帮到你。
本文还有配套的精品资源,点击获取