ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringBoot2+Vue3来访管理系统:数据库设计、接口开发与部署全解析

SpringBoot2+Vue3来访管理系统:数据库设计、接口开发与部署全解析 1. 从纸质登记到在线流程这个来访管理系统要解决的真实问题做来访管理系统之前我得先承认一件事这套系统看着就是 SpringBoot2 Vue3 MyBatis-Plus MySQL8.0 的技术组合但真正决定项目成败的不是这些框架怎么配而是来访这个业务到底该怎么建模。我最早接到的需求很简单——前台每天手工登记访客信息来访人员在登记簿上写姓名、电话、单位、找谁、事由下班前整本拍照存档。流程看着完整实际执行全是漏洞字迹看不清、号码随意填、访客超时没走没人知道、领导要统计今天的来访情况只能翻纸。所以当这个 Java Web 项目决定从零搭建时我第一件事不是装环境而是把来访管理做什么这件事彻底想透。1.1 需求是从哪里来的这类系统最常见的应用场景是写字楼前台、园区大门、学校访客登记、工厂门卫本质上是同一件事外部人员进入一个受控区域需要被记录、被确认、被追踪。我归纳出的核心痛点有三个。第一是登记信息的可信度问题纸质登记完全靠自觉手机号写错一位、身份证号少一位事后根本联系不上人。第二是审批环节缺失传统做法是访客到门口前台打电话给被访人确认被访人在开会或者没接电话访客就只能干等。第三是离场不可控访客签入后有没有按时离开没有任何约束机制一旦出现安全事故调监控都无从下手。把这些痛点翻译成系统功能就是五件事访客线上提交预约 - 被访人审批 - 前台核实签入 - 到访结束后签离 - 管理员查记录和拉黑名单。这里每一环都是独立的接口每一环之间又有严格的状态约束。这也是为什么我说不能一上来就写 CRUD先把流程定清楚后端的表结构和前端的页面才能对得上。1.2 四个角色和一条完整的流程闭环我把系统里的角色分成四类访客是预约发起人被访人是审批人前台负责签入签离并核验身份管理员管理黑名单、查看统计。为了让权限简单清晰没有做复杂的 RBAC而是给用户表加了一个 role 字段通过后端拦截器判断角色可访问的接口。流程闭环用大白话描述就是访客填一个预约单包含姓名、手机号、身份证号、单位、被访人、事由、预计到访时间被访人看到待审批列表点通过或驳回访客到现场后前台根据预约单号或身份证号查到审批通过的记录核实后签入访客走的时候再签离记录离场时间。中途出现的特殊情况包括被访人驳回后访客再次发起、访客预约了但没来、访客在黑名单里被前台拒绝接入。这套业务规则直接决定了三件事预约表需要一个状态字段来承载审批流转访客资料和预约单必须分开存因为一个人可能预约很多次签入签离要有独立记录方便按时间段统计。把这几条想清楚之后我才开始进入技术选型环节。2. 技术选型的真实逻辑SpringBoot2、Vue3、MyBatis-Plus、MySQL8.0 各自解决什么问题很多应届生和初学者选技术栈习惯性追逐新版本哪套教程火就用哪套结果项目做一半卡在版本兼容上。这个项目最后敲定的组合是 SpringBoot2 做后端、Vue3 做前端、MyBatis-Plus 做持久层、MySQL8.0 做数据库每一步都是我基于实际场景做的取舍。2.1 SpringBoot2不是最新但是最稳SpringBoot2 选 2.7.x这是 2.x 分支的最后一个大版本生态成熟度极高。这个项目是单体应用不需要微服务不需要分布式事务一个内嵌 Tomcat 的 jar 包就能跑起来SpringBoot2 完全够用。为什么不选 SpringBoot33.x 强制要求 JDK17而大量公司生产环境和学校的实验环境还在 JDK8 上一套代码如果想在两种环境里都能跑SpringBoot2 JDK8 是兼容面最广的选择。另外 SpringBoot2 的 starter 体系让配置成本非常低引入 spring-boot-starter-web、spring-boot-starter-validation、spring-boot-starter-test 三个包就能覆盖这个项目的全部基础设施需求。开发调试阶段用 devtools 做热重启效率比手动重启 Tomcat 高出不少。2.2 MyBatis-Plus把 CRUD 的重复劳动压到最低选 MyBatis-Plus 而不是纯 MyBatis不是因为纯 MyBatis 不行而是这个项目的核心业务就是围绕几张表的增删改查加状态流转用 MyBatis-Plus 的 BaseMapper 可以直接继承出 insert、updateById、selectById、selectPage 这些基础方法省掉至少四成的手写 SQL。我最常用的是 LambdaQueryWrapper比如查询某个访客的待审批预约单Appointment appointment appointmentMapper.selectOne( new LambdaQueryWrapperAppointment() .eq(Appointment::getVisitorId, visitorId) .eq(Appointment::getStatus, 0) .orderByDesc(Appointment::getCreateTime) .last(LIMIT 1));.eq(Appointment::getStatus, 0)这种写法用的是方法引用字段名写错会在编译期就暴露比手写 XML 里拼字符串安全得多。不过有一点要心里有数MyBatis-Plus 只擅长单表 CRUD一旦涉及多表 join 的统计报表还是要老老实实写 XML SQL。这个项目的来访统计我保留了自定义 SQL 的入口后面文档部分会细说。2.3 Vue3 配 Element PlusMySQL8.0 配 utf8mb4前端选 Vue3 主要是为了 Composition API 和script setup语法。这个项目几乎全是表单和列表页面逻辑复用场景很多比如预约表单和访客编辑表单共用一套校验规则把校验逻辑抽成 hook 之后代码量明显下降。UI 层配 Element Plusel-form、el-table、el-pagination、el-dialog 这几个组件就能拼出全部核心页面不用自己造轮子。数据库选 MySQL8.0 是奔着 utf8mb4 和更好的时间处理能力去的。utf8mb4 是真正的全量字符集访客登记时填的生僻字、特殊符号都能正常存储不会出现 5.7 时代 emoji 存不进去的问题。8.0 默认的 utf8mb4_0900_ai_ci 排序规则对中文搜索也更友好。部署方面如果本地没有 MySQL 环境用 Docker 拉一个 mysql:8.0 镜像映射 3306 端口几分钟就能起一个实例。3. 数据库设计实战五张核心表、预约状态与索引取舍数据库设计是这个项目里最值得分享的部分。我见过太多同类项目把访客信息直接塞进预约表所有字段堆在一张表里后面加黑名单、加签离记录时只能继续往表里加列最后表结构乱到没人敢动。我的做法是先划清业务概念的边界再决定表怎么拆。3.1 五张表是怎么拆出来的整个系统一共五张核心表表名作用核心字段t_user系统登录用户username、password、real_name、rolet_visitor访客档案name、phone、id_card、companyt_appointment来访预约单visitor_id、host_name、purpose、expect_time、statust_visit_record到访记录appointment_id、visitor_id、check_in_time、check_out_timet_blacklist黑名单visitor_id、reason、operator_id访客和预约单为什么要分开因为同一个访客可能一个月来三次如果把访客信息每次都复制到预约单里身份证号、手机号会大量冗余而且后续要做某个人一共来过几次的分析时完全没有抓手。拆成两张表之后预约单只存 visitor_id 一个外键访客档案是唯一的黑名单也直接挂在访客维度上。到访记录单独拆出来是第二个关键决策。预约单表达的是意图是我打算去到访记录表达的是事实是我真的去了并完成了签入签出。两者混在一张表里状态字段会非常别扭——预约状态要存待审批、已通过、已驳回到访状态又要存已签入、已签离两个维度互相干扰。拆开之后逻辑清爽得多签入操作就是给 visit_record 插入一条记录并写时间签离操作就是 update 这条记录的 check_out_time。3.2 预约状态字段设计与 DDL 示例预约状态我用了 tinyint 数字枚举没有用字符串理由是存储空间小、查询快、代码里用常量类维护一目了然。状态定义如下状态值含义触发时机0待审批访客提交预约1已通过被访人同意2已驳回被访人拒绝3已取消访客或被访人主动取消4已过期超过预计到访时间且未签入以预约表为例DDL 长这样CREATE TABLE t_appointment ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, visitor_id BIGINT NOT NULL COMMENT 访客ID, host_name VARCHAR(64) NOT NULL COMMENT 被访人姓名, host_department VARCHAR(64) DEFAULT NULL COMMENT 被访人部门, purpose VARCHAR(255) NOT NULL COMMENT 来访事由, expect_time DATETIME NOT NULL COMMENT 预计到访时间, status TINYINT NOT NULL DEFAULT 0 COMMENT 状态0待审批 1已通过 2已驳回 3已取消 4已过期, audit_time DATETIME DEFAULT NULL COMMENT 审批时间, audit_remark VARCHAR(255) DEFAULT NULL COMMENT 审批备注, create_time DATETIME NOT NULL COMMENT 创建时间, deleted TINYINT NOT NULL DEFAULT 0 COMMENT 逻辑删除标记, PRIMARY KEY (id), KEY idx_visitor_id (visitor_id), KEY idx_expect_time (expect_time), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_ai_ci COMMENT来访预约表;deleted 字段配合 MyBatis-Plus 的TableLogic注解做逻辑删除所有删除操作自动变成 update查询自动带上 deleted0 条件。对来访数据这种可能有审计需求的场景我建议一律逻辑删除不要物理删行。3.3 查询场景和索引怎么匹配索引设计需要跟真实查询场景对齐。这个系统里的高频查询有三个按访客查预约记录、按时间范围查当天来访列表、按状态查待审批列表。所以预约表我建了 idx_visitor_id、idx_expect_time、idx_status 三个索引到访记录表建了 idx_check_in_time 用来支撑某天来了多少人的统计查询。这里有个很容易踩的坑不要给每一列都加索引。有些初学者看教程说索引能加速查询就把 where 里出现过的字段全建一遍结果写入变慢、索引维护成本高查询优化器还不一定用得上。这个项目的表都没超过几万行三五个索引就够了真到数据量大了再考虑联合索引和分区现在做这些属于过度设计。4. 后端核心接口的实现预约登记、审批、签入签离后端接口我按业务动作拆一个动作一个接口不搞那种一个通用 save 接口传类型参数的万能接口。虽然 MyBatis-Plus 有人提过通用 CRUD 的思路但在业务系统里接口必须表达业务语义调用方看到/api/appointment/audit就知道是审批看到/api/visit/checkIn就知道是签入维护成本低得多。4.1 预约登记访客去重 预约单落库预约登记接口的输入是一个 DTO包含访客姓名、手机号、身份证号、单位、被访人、事由、预计到访时间。核心逻辑分两步第一步根据身份证号去 t_visitor 表查访客查不到就新建访客档案第二步创建预约单状态置为 0。Transactional(rollbackFor Exception.class) public Appointment createAppointment(AppointmentDTO dto) { // 1. 按身份证号判断访客是否已存在 Visitor visitor visitorMapper.selectOne( new LambdaQueryWrapperVisitor() .eq(Visitor::getIdCard, dto.getIdCard())); if (visitor null) { visitor new Visitor(); visitor.setName(dto.getName()); visitor.setPhone(dto.getPhone()); visitor.setIdCard(dto.getIdCard()); visitor.setCompany(dto.getCompany()); visitorMapper.insert(visitor); } // 2. 创建预约单状态默认待审批 Appointment appointment new Appointment(); appointment.setVisitorId(visitor.getId()); appointment.setHostName(dto.getHostName()); appointment.setPurpose(dto.getPurpose()); appointment.setExpectTime(dto.getExpectTime()); appointment.setStatus(0); appointment.setCreateTime(LocalDateTime.now()); appointmentMapper.insert(appointment); return appointment; }事务注解必须加因为访客插入和预约单插入是两个写操作任何一个失败都要回滚否则会出现有预约单没访客档案的脏数据。返回值把预约单带回去前端可以用预约单号生成二维码后续签入时扫码调记录。4.2 审批操作条件更新避免并发覆盖审批逻辑最容易出问题的点是并发。想象一个场景访客在手机端看到预约被通过后取消了预约与此同时被访人在电脑端刚好点了通过两个请求同时到达如果代码写成先查出状态判断是 0再 update中间隔着两次数据库操作极容易把取消后的预约再改成通过。正确的做法是用一次条件更新的原子操作把判断和更新合并public boolean audit(Long id, Integer targetStatus, String auditRemark) { LocalDateTime now LocalDateTime.now(); int rows appointmentMapper.update(null, new LambdaUpdateWrapperAppointment() .eq(Appointment::getId, id) .eq(Appointment::getStatus, 0) .set(Appointment::getStatus, targetStatus) .set(Appointment::getAuditTime, now) .set(Appointment::getAuditRemark, auditRemark)); return rows 0; }关键在.eq(Appointment::getStatus, 0)这个条件。数据库行锁保证同一时刻只有一个事务能更新成功如果 rows 返回 0说明预约已经不是待审批状态直接提示该预约已被处理请刷新后再试。这种方案比乐观锁版本号更轻因为这里的冲突可能性不高没必要给每张表都加 version 字段。4.3 签入签离时间记录、黑名单拦截和超时判定签入接口做的事情比表面看起来多先校验预约单存在且状态为 1已通过再查黑名单命中就拒绝签入全部通过后往 t_visit_record 插一条记录check_in_time 设为当前时间状态置为已签入。签离就简单了按 appointment_id 找到未签离的记录update check_out_time。这里有个业务细节如果访客签入后没有签离就离开了系统怎么处理我的方案是不依赖定时任务去抓超时记录而是在查询列表时动态计算——凡是 check_out_time 为空且 check_in_time 超过设定安全时长比如 8 小时的记录在返回前标一个超时未签离的标记。这么做的理由是预约量级不大没必要引入 xxl-job 之类的定时框架查询时顺手算一下逻辑简单也不会漏。5. Vue3 前端落地的关键细节表单、列表、状态展示后端接口跑通之后前端的坑开始浮出水面。Vue3 项目里最容易被忽视的其实是工程化配置而不是组件写法。这个项目的页面不算多预约表单、访客列表、预约列表、黑名单管理、登录页五个页面就覆盖了全部功能但要把它们组织得清晰依赖的是一套好的项目骨架。5.1 项目初始化、目录拆分和请求层封装前端用 Vite 初始化npm create vitelatest选 vue 模板然后装 element-plus、axios、vue-router、pinia。目录结构我习惯这样分src/ ├── api/ # 每个模块一个文件统一放接口请求 ├── router/ # 路由配置 ├── views/ # 页面组件 ├── components/ # 复用组件 ├── utils/ # request.js 封装、校验工具 └── hooks/ # 表单校验等可复用逻辑request.js 封装值得多说两句。axios 实例统一设置 baseURL 为/api拦截器里统一挂 token、统一处理 401 跳登录页、统一把后端返回的 code/message/data 结构解包。这样业务代码里写接口就是纯粹的拿到数据错误处理全部收敛在拦截器不会每个页面都写一遍 try-catch。5.2 预约表单的校验规则与提交流程预约表单用的是 Element Plus 的 el-form。校验规则里最容易出问题的是日期校验和身份证正则。手机号和身份证号的正则建议直接写在规则里不要等后端返回错误再提示前端先拦一道用户体验会好很多const rules { name: [{ required: true, message: 请输入姓名, trigger: blur }], phone: [ { required: true, message: 请输入手机号, trigger: blur }, { pattern: /^1[3-9]\d{9}$/, message: 手机号格式不正确, trigger: blur } ], idCard: [ { required: true, message: 请输入身份证号, trigger: blur }, { pattern: /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/, message: 身份证号格式不正确, trigger: blur } ], expectTime: [{ required: true, message: 请选择预计到访时间, trigger: change }] }日期选择我用 el-date-picker 的 datetime 类型并把 disabledDate 设为早于今天的日期不可选防止访客把预约时间填到过去。提交按钮的回调里先调用formRef.value.validate()校验通过再走 submit 接口这个方法返回的是一个 Promise可以用 await 配合 try-catch 处理。5.3 列表查询、分页和状态标签渲染列表页是这类管理系统的主战场。预约列表和来访记录列表我都用了 el-table el-pagination 的组合。后端分页接口接收 pageNum、pageSize 和查询条件返回 records 和 total前端把这两项分别绑定到表格数据源和分页组件上。状态字段的展示建议统一封装成一个函数根据 status 映射到 Element Plus 的 tag 类型和文案const statusMap { 0: { text: 待审批, type: warning }, 1: { text: 已通过, type: success }, 2: { text: 已驳回, type: danger }, 3: { text: 已取消, type: info }, 4: { text: 已过期, type: info } }el-table-column label状态 width100 template #default{ row } el-tag :typestatusMap[row.status].type {{ statusMap[row.status].text }} /el-tag /template /el-table-column这样写的好处是状态文案和颜色只维护一份新增状态只需要改 statusMap不用满页面找魔法数字。6. 联调部署阶段踩过的坑时区、分页插件、跨域代理的完整排查前后端代码写完之后联调和部署阶段大概是整个项目里最容易让人血压升高的阶段。我把在这个项目里实际遇到的三类典型问题完整记录下来每个问题都按症状 - 排查链路 - 根因 - 解决的顺序讲。6.1 MySQL8.0 连接报时区错误完整排查链路第一次启动 SpringBoot 项目时控制台直接报错核心信息是The server time zone value xxx is unrecognized or represents more than one time zone。第一次遇到的人会以为数据库坏了其实问题出在 JDBC 连接串和驱动之间的时区协商上。我的排查链路是这么走的。第一步先确认 MySQL8.0 服务本身正常命令行能连接、能查询。第二步看 pom 里的驱动版本如果用的是 mysql-connector-java 5.x 连 MySQL8.0连接直接失败因为 8.0 的默认认证插件 caching_sha2_password 和 5.x 驱动不兼容。第三步检查连接串发现没带时区参数这就是报错直接原因。最终连接串配置如下spring.datasource.urljdbc:mysql://localhost:3306/visitor_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue spring.datasource.usernameroot spring.datasource.password你的密码 spring.datasource.driver-class-namecom.mysql.cj.jdbc.Driver这里面两个参数必须解释清楚。serverTimezoneAsia/Shanghai是告诉驱动用哪个时区去解释数据库返回的时间值不写就会拿 JVM 默认时区去猜猜不准就报错。allowPublicKeyRetrievaltrue是配合 caching_sha2_password 认证用的在非 SSL 连接下首次连接需要向服务器请求公钥不加这个参数会报 Public Key Retrieval is not allowed这个问题在 MySQL8.0 上极其常见。6.2 MyBatis-Plus 分页插件不生效插件根本没注册联调列表接口时遇到一个更隐蔽的问题接口传了 pageNum1、pageSize10返回的数据确实只有 10 条但 total 字段返回的是全表总行数怎么翻页都是同一批数据。我一度怀疑是前端分页参数传错了结果在控制台把 SQL 打出来一看压根没有 LIMIT 语句。根因总结起来一句话MyBatis-Plus 的分页能力不是引入依赖就自动生效的必须显式注册分页插件。很多教程只讲用法不讲前提导致大量项目卡在这一步。Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }注册之后selectPage才会生成带 LIMIT 的 SQLtotal 才会走 count 查询。还有一个相关细节分页必须调用selectPage或selectMapsPage这类分页方法如果自己构造了 Page 对象却调selectList分页插件并不会拦截结果还是全量数据。这就是为什么我在前文强调 BaseMapper 的方法语义要搞清楚。6.3 Vite 代理和跨域前端开发环境怎么配最省事前后端分离项目联调时跨域问题躲不开。浏览器里访问localhost:5173axios 请求打到localhost:8080端口不同就是跨域。我推荐的做法是开发阶段用 Vite 的代理而不是在后端加CrossOrigin。// vite.config.js export default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })前端所有请求都以/api开头Vite 开发服务器收到带/api前缀的请求后自动转发到http://localhost:8080浏览器看到的请求是同源的根本不会触发 CORS 预检。生产环境同理用 Nginx 把/api反向代理到后端服务即可。后端不需要单独配 CORS避免把跨域放宽到全域名安全性更好。这套方式踩过一次坑之后我是真的记住了排错一定要从请求链路的最前端看起先在浏览器 Network 面板确认请求到底发出去了没有、响应是什么状态码再决定是改前端代理还是改后端配置。7. 含文档源码怎么组织以及这个项目的后续扩展方向标题里特意写了【含文档】说明这套源码不是简单的代码压缩包而是可以作为毕业设计、课程设计或者团队协作起点来交付的完整项目。我在整理这个项目的时候把文档和源码的组织方式当成一个正经工作来做因为只有代码没有文档的项目接手的人工作效率至少打五折。7.1 源码包和文档的整理思路一个合格的 Java Web 项目源码包我建议至少包含四类内容。第一类是 README里面写清楚环境要求JDK8、Maven3.6、Node16、MySQL8.0、导入步骤后端 IDEA 打开、执行 sql 脚本、改配置文件前端 npm install 和 npm run dev、默认账号密码。第二类是数据库初始化脚本t_user 表里预置管理员、前台、被访人角色的账号访客不需要登录直接走预约页面。第三类是接口文档我用的方案是服务端引入 springdoc 的依赖项目启动后访问/swagger-ui.html就能看到全部接口的入参和返回结构比自己手写 Word 文档高效且不容易过时。第四类是部署说明包括后端打包成 jar 的命令、前端构建静态资源的命令、生产环境 Nginx 的反向代理配置示例。这里有一个容易被忽略的细节文档里的版本号必须和 pom.xml、package.json 里的实际版本一致。我见过太多项目的文档写的是 SpringBoot2.6代码里用的却是 2.7读者照着文档配了一晚上才找到问题。整理文档的时候顺手核对一遍依赖版本是成本极低但收益极大的动作。7.2 下一步能加的功能以及一点个人体会这个项目目前的状态是能完整跑通预约-审批-签入-签离-查询-黑名单全链路但说实话它离一个成熟的商业来访系统还有很长的路。我在规划里排了几个扩展方向访客预约成功后生成二维码前台用扫码枪核销签入这能把签入效率提一大截预约结果通过服务号模板消息推送给访客省得打电话通知管理员端加一个当日来访概览的大屏页面用 ECharts 画按小时来访趋势再往后可以对接门禁设备签入后闸机自动放行。我自己的体会是这类管理系统最大的价值不在技术难度而在于把不规范的线下流程变成有状态、可追溯的线上数据。在你写代码之前花一个下午跟实际用系统的人聊清楚流程搞清楚他们每天在哪一步最烦、最怕出什么错比纠结用哪个框架版本重要得多。这也是为什么我在整篇文章里反复强调业务建模、状态设计和流程闭环——框架技术到处都有教程而真正让一个系统好用的是那些散落在业务里的细节。如果你也正准备做一个类似的 Java Web 管理系统我的建议很直接先把数据表建对把状态字段定清楚再动手写第一行接口代码。表结构对了后面的代码和页面都会顺很多。
返回列表