☰
SpringBoot+Vue来访管理系统:可直接运行的完整项目实战
2026/9/30 12:30:38 网站建设 项目流程

1. 为什么我会做一个“可直接运行”的来访管理系统

先说说背景。我在单位里经常要接待外部人员:供应商、客户、面试者、临时维修师傅,一来二去就得登记姓名电话、找谁、什么事、几点走。传统的做法是前台递一张纸质登记表,访客自己手写,字迹好不好认先不说,信息查起来非常痛苦,翻一叠纸才能找到昨天的记录。更麻烦的是,被访人经常在开会,前台得打电话确认,访客就在那儿干等。

我当时就想,能不能做一个轻量级的来访管理系统,把“登记—通知—审批—签入—签离—查询”这条链路全部数字化?市面上确实有成套的访客机,但硬件贵,而且大多绑定特定厂商,数据又封闭。我更想要一套自己能完全掌控的源码,随时改需求、扩功能。所以就动手用 SpringBoot + Vue + MySQL 这套主流组合,搭了一个前后端分离的完整项目。

这里为什么要特意强调“可直接运行”?因为我见过太多开源项目,名字写着“完整源码”,拉下来一跑,缺依赖、缺配置、数据库脚本缺失、前端接口地址不对,光修环境就得半天。我做的时候给自己立了个规矩:克隆下来、初始化数据库、改个配置文件、两条命令启动,前后端就能连起来。所以全文能直接参照复现,不需要横跳各种博客去补环境。

这套系统适合谁?三类人:一是被访客登记问题困扰的行政、前台、办公室人员,需要一个现成的方案;二是正在做 Java 课程设计、毕业设计的学生,需要一个功能完整、技术栈主流的项目作参考;三是想学习 SpringBoot 和 Vue 前后端分离实践的开发者,代码量适中,业务场景贴近真实,比纯粹练习 CRUD 的 Demo 有营养得多。

后续的思路我就按“业务设计—数据建模—后端实现—前端实现—部署上线—排坑建议”这条线往下走,每一段都尽量说清楚“为什么这么设计”,而不是只丢代码。

2. 先理清业务:来访管理到底管哪几件事

很多人在写管理系统的时候容易犯一个错:上来就建表建接口,做了一半才发现业务逻辑没想透。做来访管理也一样,得先角色分析。系统里实际有三类角色在协作:

  • 访客:外部人员,提交来访预约,到访后签到和签离。
  • 被访人:内部员工,收到预约通知,决定同意或拒绝。
  • 管理员:前台或行政人员,查看所有来访记录、统计访客数据、管理黑名单。

围绕这三个角色,核心业务流程是一条状态链路:待审批 → 已通过/已拒绝 → 已签入 → 已签离 → 已取消。

我最初理解大家最常要的两个能力是:把纸质的“登记本”变成线上表单,把“被访人知不知道有人来找他”这个信息同步给打通。但实际和前台聊下来,发现还有三个隐藏需求很容易被忽略:

  1. 访客的到访次数记录。同一个供应商可能每周都来,如果每次都重新录一遍身份证、电话、公司信息,前台会疯掉。所以系统要有“历史访客”概念,按手机号或身份证号识别老访客,自动带出基础信息。

  2. 审批的时效体验。访客在前台等着,被访人在开会,这时候审批如果卡住,现场体验极差。所以设计上要有“超时提醒”,预约提交后超过 30 分钟未审批,系统自动给被访人再发一次提醒。

  3. 安全留痕。访客签入时记录人脸照片或身份证照片(我这里是预留了图片上传接口),万一发生纠纷,有据可查。

基于这些,系统的功能模块最终定为五个大块,对应的页面也好规划:

功能模块使用者核心操作对应页面
访客预约访客(可由前台代录)填写来访信息、选择被访人、提交预约访客预约页 / 预约记录页
审批管理被访人查看预约申请、同意/拒绝、填写备注审批中心页
签入/签离访客、前台扫码或按预约单号签到、签离签入签离页
来访记录管理员多条件检索、导出 Excel、删除异常记录记录列表页
系统设置管理员部门管理、员工管理、黑名单管理设置页

这套功能设计下来,作为一个“可直接运行”的版本已经足够了。审批、签到、查询这三条线都通了,后续无论朝“门禁联动”“公众号预约”“访客车牌识别”哪个方向扩展,都不用推翻重来。

3. 数据库设计:五张核心表撑起整个业务闭环

后端开发有句老话:“数据库设计决定了业务能走多远。”SpringBoot 项目数据表不宜过多,但每一张都要能把关系理顺。这套系统我最后保留了visitor_info(访客基础信息)、visit_appointment(来访预约)、sys_user(内部用户)、sys_department(部门)、blacklist(黑名单)这五张核心表,外加一张visit_record(访问记录)作为签入签离的流水表,总共六张表。

为什么要拆成这么多表?核心原则是区分静态数据和动态数据。

访客的姓名、手机号、身份证号、公司名称是相对静态的信息,如果每次预约都塞进预约表里,会造成大量冗余,而且老访客再次来访时,你很难从几十万条记录里准确地把他“认出来”。所以访客身份单独存一张表,预约单只存一个visitor_id外键,这是第一层拆分。

第二层拆分是把“预约单”和“签到流水”分开。这在最初版本里被我合并成一张表,但后来发现一个实际问题:如果访客预约了多次,其中一次签入又签离了,另一次还没来,合并设计就得改来改去,状态字段一多就容易乱。拆开后,预约单表管状态流转,签到记录表管每一次实际的进出时间,互不干扰,查询效率也更高。

看一下最核心的visit_appointment表结构,字段设计基本就能反映系统逻辑:

CREATE TABLE `visit_appointment` ( `id` bigint NOT NULL AUTO_INCREMENT, `appointment_no` varchar(32) NOT NULL COMMENT '预约单号,例如 V20250612001', `visitor_id` bigint NOT NULL COMMENT '访客ID,关联 visitor_info 表', `staff_id` bigint NOT NULL COMMENT '被访人ID,关联 sys_user 表', `visit_reason` varchar(200) DEFAULT NULL COMMENT '来访事由', `expected_time` datetime DEFAULT NULL COMMENT '预计到访时间', `status` tinyint DEFAULT '0' COMMENT '状态:0待审批 1已通过 2已拒绝 3已签入 4已签离 5已取消', `approval_remark` varchar(200) DEFAULT NULL COMMENT '审批备注', `sign_in_time` datetime DEFAULT NULL COMMENT '实际签入时间', `sign_out_time` datetime DEFAULT NULL COMMENT '实际签离时间', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_visitor_id` (`visitor_id`), KEY `idx_staff_id` (`staff_id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='来访预约表';

这里有几个细节值得展开说一下。

第一,状态字段用整数不用字符串。很多新手设计时喜欢用status存"PENDING"、"APPROVED"这种字符串,表面上可读性好,但实际开发和接口联调时很容易因为大小写不一致出问题。用整数配合枚举类,后端代码里定义常量,前端通过字典翻译,既省存储又避免脏数据。

第二,索引设计要跟着查询走。最常发生的查询是“某个访客的历史预约记录”(按visitor_id查)和“某个员工的待审批列表”(按staff_id和status查),所以这两个字段必须建立索引。生产环境数据量上来了,再考虑联合索引。

第三,时间字段统一使用datetime,在 Java 侧配合LocalDateTime使用。千万不要用timestamp,否则一旦系统部署到不同时区的服务器上,很容易出现“登录一看签到时间是八小时前”的诡异问题,这个我在后面踩坑部分会专门讲。

visitor_info表相对简单,核心字段就是手机号、身份证号、公司名称和来访人次计数。身份证号建议加密存储,至少不能用明文,否则后期做等保合规审核时非常被动。手机号可以加唯一索引,作为识别老访客的关键依据。visit_record表则是在预约单签入签离时落一条流水,记录操作人、操作时间、操作类型,作为审计留痕。

4. 后端实现核心:SpringBoot 接口怎么落地才不烂尾

表设计好了,后端代码的组织方式很大程度上决定了项目好不好维护。我用的标准 SpringBoot 分层结构:Controller → Service → ServiceImpl → Mapper,工程分包清晰,模块和模块之间借接口通信,实现类隔离在 impl 包里。这样改动某个模块的内部实现,不会影响到引用它的上层。

项目结构是这样的:

com.example.vms ├── VmsApplication.java ├── controller │ ├── AppointmentController.java │ ├── VisitorController.java │ ├── UserController.java │ ├── DepartmentController.java │ └── RecordController.java ├── service │ ├── AppointmentService.java │ └── impl │ └── AppointmentServiceImpl.java ├── mapper │ ├── AppointmentMapper.java │ └── xml/AppointmentMapper.xml ├── entity │ ├── VisitAppointment.java │ ├── VisitorInfo.java │ └── SysUser.java ├── common │ ├── Result.java │ ├── PageResult.java │ └── GlobalExceptionHandler.java └── config ├── WebConfig.java └── MybatisPlusConfig.java

上面用到了 MyBatis-Plus,这一点是刻意的:它内置了通用 CRUD,简单的单表操作不需要手写 SQL,开发效率极高。复杂的业务查询,比如“按时间段 + 访客姓名 + 被访人 + 状态组合筛选”这种动态条件,就在 Mapper XML 里写好 SQL,交给数据库执行。两种方式结合在一起,项目既不会因为全手写 SQL 而代码量大,也不会因为过度依赖框架而失去对复杂查询的掌控。

核心接口设计上,预约模块是全系统的重头戏。画几个关键接口出来,想得越清楚,后面写代码越快:

// 提交来访预约 @PostMapping("/appointment") public Result<Long> createAppointment(@RequestBody AppointmentCreateDTO dto) { return Result.success(appointmentService.createAppointment(dto)); } // 被访人查询待审批列表 @GetMapping("/appointment/pending") public Result<PageResult<AppointmentVO>> pendingList(@RequestParam Long staffId, @RequestParam int page, @RequestParam int size) { return Result.success(appointmentService.pendingList(staffId, page, size)); } // 审批操作(同意/拒绝) @PutMapping("/appointment/{id}/approve") public Result<Void> approve(@PathVariable Long id, @RequestBody ApproveDTO dto) { appointmentService.approve(id, dto); return Result.success(); } // 签入 @PutMapping("/appointment/{id}/sign-in") public Result<Void> signIn(@PathVariable Long id) { appointmentService.signIn(id); return Result.success(); } // 签离 @PutMapping("/appointment/{id}/sign-out") public Result<Void> signOut(@PathVariable Long id) { appointmentService.signOut(id); return Result.success(); }

createAppointment里面有一段逻辑值得展开,因为它是整个系统的首个业务判断点。流程是这样的:

  1. 根据手机号查visitor_info,如果存在就复用该访客 ID,并把来访次数加 1;如果不存在就新建一条访客信息。
  2. 生成预约单号,规则是V + yyyyMMdd + 三位流水号,保证当天可读性强且不重复。
  3. 检查被访人是否存在、是否在黑名单中。
  4. 设置预约状态为待审批,落库返回预约单 ID。

生成单号的时候,多个用户同时提交会出现并发场景,直接用synchronized锁不可靠(集群下会失效),更稳妥的做法是利用数据库唯一索引保证单号唯一,生成时带一个随机短码。虽然是单机部署也能用,但还是建议在一开始就把这种细节处理好,后面上集群不用返工。

审批接口中我加了乐观锁防重复处理,最关键的点在于状态变更时用update ... where status = 0的方式判断,如果更新行数为 0,说明记录已经被处理过了,直接抛异常“该预约已被处理”。这样不管前台双击多次,还是两个端同时点击,都不会把同一单审批两次,业务数据不会错乱。

参数校验方面,DTO 上直接加@NotBlank、@Pattern注解,配合全局异常处理器GlobalExceptionHandler,把所有异常兜底成统一的 JSON 结构返回。前端拿到的永远是{ code, message, data }这个格式,不用写一大堆 if-else 去猜后端返回什么。这是分摊联调成本很重要的一步,前后端约定好这套格式之后,沟通效率明显提高。

再单独说下安全。初期版本我没有引入 Spring Security 而是选择了轻量的拦截器方案,原因有两点:一是系统角色固定、权限模型简单,Security 在这个项目里属于过度设计;二是很多学生朋友看 Security 的过滤器链容易一头雾水,项目“可直接运行”的目标会受影响。我在拦截器里校验前端传来的 token,再把登录用户信息放进 ThreadLocal 供全局使用。如果你后续要扩展多角色细粒度权限,把这块替换成 Spring Security 或 Sa-Token 都不难。

5. 前端页面实现:Vue 如何组织页面和数据交互

前端用的是 Vue 2 生态 + Element UI。为什么选这套组合而不是 Vue 3 + Element Plus?一个很现实的理由:Element UI 对 Vue 2 的组件封装最成熟,网上问题和答案最多,遇到 bug 时几乎都能搜到现成解决方案。等码栈熟悉了,迁移到 Vue 3 和 TypeScript 是水到渠成的事。

页面结构按路由拆分,主视图是左侧菜单 + 顶部导航栏 + 右侧内容区的经典后台管理布局:

src ├── api │ ├── appointment.js │ ├── visitor.js │ └── user.js ├── router │ └── index.js ├── store │ └── modules │ └── user.js ├── views │ ├── appointment │ │ ├── AppointmentForm.vue // 访客预约表单 │ │ ├── AppointmentList.vue // 预约记录列表 │ │ ├── ApprovalCenter.vue // 审批中心 │ │ └── SignInOut.vue // 签入签离操作台 │ ├── record │ │ └── VisitRecordList.vue // 来访记录 │ └── setting │ ├── DepartmentManage.vue │ └── UserManage.vue └── utils └── request.js // axios 实例封装

utils/request.js是前后端联调的咽喉部位。我在这里做了四层统一封装:统一请求路径前缀/api;请求拦截器自动附加 token 到 Header;响应拦截器统一处理业务错误码并弹出 Message;HTTP 状态码 401 时自动跳回登录页。这样做的好处是业务组件里不用反复写错误处理,代码干净很多:

import axios from 'axios' const service = axios.create({ baseURL: '/api', timeout: 10000 }) service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = token } return config }) service.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { this.$message.error(res.message || '请求失败') return Promise.reject(new Error(res.message)) } return res.data }, error => { if (error.response && error.response.status === 401) { this.$router.push('/login') } this.$message.error('网络异常,请稍后重试') return Promise.reject(error) } )

审批中心是前端交互密度最高的页面,用到了el-tabs做待审批、已通过、已拒绝三个 Tab 的切换;每条数据右侧放“通过”“拒绝”按钮,点击后在弹窗里填审批备注;表格数据通过分页组件el-pagination拉取。页面组件不直接调用 axios,而是统一走api目录下封装好的函数,后续如果接口地址变动,只改一个文件即可。

有一个细节是前端表单校验必须和后端对齐。比如手机号校验,前端正则和后端@Pattern如果不一致,就会出现“前端提示格式正确,提交后被后端打回”的尴尬。我的做法是定义一个公共校验规则文件,前端复用同一份正则,保持两端的口径相同。

前后端联调时最容易出的问题就是跨域。因为后端跑在localhost:8080,前端跑在localhost:9528,端口不同必然产生跨域,这属于基本功,不算坑。解决办法是在后端写一个CorsConfig类,配置允许的来源、方法和请求头;注意不要粗暴地设置allowedOrigins("*"),至少把来源限定在前端实际地址列表,避免安全隐患。也可以在生产环境通过 Nginx 反向代理统一入口,从根上规避跨域问题,这一点部署篇会详说。

6. 环境准备与核心步骤:让你 10 分钟内跑起来

这个项目特意照顾了“可直接运行”这个目标,对环境要求可以说相当宽容:Node.js 14+、Maven 3.6+、JDK 1.8+、MySQL 5.7+即可,四个依赖都装上后,基本不会卡环境。

先说数据库准备。项目根目录下提供了db/visitor_management.sql初始化脚本,里面包含建库建表语句和默认管理员账号(默认密码是admin123,首次登入系统后建议立刻改掉)。在 MySQL 中执行:source /你的路径/visitor_management.sql,完成后检查一下核心表是否创建成功,如果报语法错误,优先检查 MySQL 版本是否太老或太新,utf8mb4相关的排序规则在旧版本可能不兼容。

在后端配置文件application.yml里,你需要改三处:数据源地址、数据库用户名、数据库密码。注意时区参数必须带上,建议写成:

spring: datasource: url: jdbc:mysql://localhost:3306/visitor_management?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: your_password

useSSL=false和allowPublicKeyRetrieval=true这两个参数,合理设置能直接避开很多新手的 MySQL 8.x 版本下的 SSL 连接报错问题(Public Key Retrieval is not allowed)。如果本地环境是 MySQL 5.7,问题更少。

启动后端方式有两种,任选其一:

# 方式一:直接用 Maven 插件启动 mvn spring-boot:run # 方式二:先打包再启动(推荐,更接近生产环境) mvn clean package -DskipTests java -jar target/visitor-management.jar

启动日志里看到Started VmsApplication in x.xxx seconds就是后端启动成功了,接着在浏览器访问http://localhost:8080/api/health,如果返回 JSON 格式的健康检查信息,说明后端链路正常。

前端启动更简单:

npm install npm run dev

依赖安装时间可能较长,如果npm install卡住,多半是网络问题,可以临时切到国内镜像源再重试。前端起来后访问http://localhost:9528,应该能看到登录页。用初始化脚本里的账号进去后,整个系统就跑起来了。

前后端联调的时候要确保一件事:前端.env.development文件里的VUE_APP_BASE_API指向后端地址,默认写的是http://localhost:8080,如果后端改了端口,记得同步改这里。很多项目跑不起来,不是代码问题,就是这个地址没配对。

生产部署我推荐一个轻量方案:后端用jar包加systemd守护进程,前端打包成静态文件交给 Nginx 托管,Nginx 同时承担反向代理和静态资源服务。核心配置示例:

server { listen 80; server_name your-domain.com; root /opt/visitor-web/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }

这样部署的好处是浏览器只和 Nginx 通信,不存在跨域问题;前端静态文件访问飞快;后端接口通过/api路径统一转发到 Java 服务,链路清晰,后续加 HTTPS 证书也很方便。

7. 实测排坑:我交付源码时踩过的四个典型坑

这个项目从第一版到稳定版,前前后后调试了一个多星期。有几个坑不是网上随便搜能搜到的,我详细说一下,帮大家省点时间。

坑一:时间差了八小时,根因却不是代码。

系统上线测试时,前台签到显示的时间比真实时间晚了 8 小时。排查了很久,发现问题根本不在 Java 代码,而是数据库连接串里没有加serverTimezone参数。MySQL 的datetime类型本身不带时区信息,如果连接参数里不指定时区,驱动会用 JVM 默认时区和数据库会话时区做换算,两边不一致就出岔子。解决办法看起来不起眼,就是连接串后加serverTimezone=Asia/Shanghai。这也是我为什么在前面强调务必加上这个参数的原因。另外一个隐含教训是,排查时间问题时不要一上来就看代码,先检查连接参数和数据库时区配置,往往事半功倍。

坑二:前后端联调时“不符合 CORS 策略”报错。

这个比较典型。前端登录请求发出后控制台直接报blocked by CORS policy。我第一时间想到跨域,所以写了一个WebMvcConfigurer配置类,放行所有跨域请求。结果不止前端报错,后端日志里也出现了重复的跨域响应头问题。原因是 Spring Boot 层面放行之后,Nginx 层(或者浏览器插件)又拦截了一次,产生了重复配置冲突。最后把 SpringBoot 的跨域配置拆成按需放行的白名单模式,在前端配好代理解决联调环境,生产上统一走 Nginx 反向代理,问题彻底解决。如果你也遇到跨域,先确认你请求链路上到底有几层服务,每一层都做了设置的话,重复配置就会很错乱。

坑三:MyBatis-Plus 更新字段时“逻辑删除”没生效。

管理后台的删除功能,前台点了“删除访客”,但实际上数据在数据库里还在。查来查去,发现是 MyBatis-Plus 的@TableLogic逻辑删除配置没匹配上。我用的是自定义删除标记字段deleted,但在实体类里没加注解,MyBatis-Plus 默认不启用逻辑删除特性,删除操作就真的变成了物理 DELETE。把@TableLogic配置到实体字段上,同时在配置文件中指定全局逻辑删除的值(未删除为 0,已删除为 1),问题就解决了。这里也提个建议:用户产生的业务数据,尽量用逻辑删除而不是物理删除,否则事后想恢复数据、审计追溯都无从谈起。

坑四:前端刷新页面后路由 404。

部署在 Nginx 上之后,访问首页没问题,但一旦点击详情页后按 F5 刷新,就会显示 404。原因是 Vue Router 用的是 history 模式,URL 路径是真实的浏览器地址,Nginx 默认只匹配磁盘上的静态文件,找不到对应的路径就返回 404。解决办法是历史经典的try_files配置,让所有未知路径都回退到index.html,由 Vue Router 接管路由。上面 Nginx 配置示例里已经写好了这一行。用 hash 模式虽然也能规避,但 URL 里会多一个/#/,不建议在这个项目里采用。

这四个坑的经验沉淀下来之后,这套源码我才敢放心标上“可直接运行”。事实上,后续有人照着文档部署,基本都能在半小时内跑通。

8. 下一步可以怎么扩展

项目现在已经能稳定运行,但离“产品化”还有一段距离。我自己规划了几个后续迭代方向,你可以根据自己的实际场景参考:

  1. 对接企业微信/钉钉通知:审批环节目前依赖被访人主动刷新页面,如果能对接企业微信或钉钉机器人,预约提交时自动推送提醒,审批后实时反馈给访客,体验会再上一个台阶。
  2. 人脸识别签入:在签入环节加一个摄像头调用,配合人脸比对服务,实现无感签到,减少前台人工核对身份证的操作。
  3. 访客预约小程序:前端扩展一个小程序端,让访客在来之前就用手机提交预约,到前台直接刷预约二维码签入,整套流程还能更顺滑一些。
  4. 数据大屏:管理员需要一个门口或前台的大屏,实时展示当前在访人数、今日来访趋势、各部门接待量等指标,SQL 层面已经支持了。

这套源码的边界从一开始就没打算只做一个“教学目标”,我更希望它是你来访管理场景的起点。代码本身不复杂,但该有的结构、规范和踩坑记录都在。你拿到手之后,最好先把整个流程自己跑一遍,再针对实际场景的差异去改,这就是最高效的学习路径。

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

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

立即咨询