首页/新闻资讯/正文详情

SpringBoot+Vue+MyBatis访客管理系统设计与部署实战

发布时间:2026/9/26 5:00:26 来源:云帆数科 栏目:资讯中心
SpringBoot+Vue+MyBatis访客管理系统设计与部署实战
前阵子朋友公司前台还在用纸笔登记访客字迹潦草、信息不全不说访客到底找谁、几点进来、几点离开、有没有预约过全靠一张纸根本查不出来。我问他为什么不换套系统他说市面上现成的要么太贵要么功能太“重”自己又不会开发。刚好我手头就有一套做过完整落地的企业级来访管理系统SpringBootVueMyBatisMySQL这套经典架构前后端分离源码完整可以直接跑。今天把系统从设计到部署的完整过程拆开讲一遍包括表结构怎么建、动态SQL怎么写、状态机怎么控制、前端路由权限怎么配、部署时Nginx要注意什么最后把项目里踩过的高频坑一起整理出来。这东西特别适合拿来当毕业设计、课程设计或者公司内部做轻量级行政管理系统的参考模板。1. 项目整体设计与技术选型1.1 这套技术栈背后是怎么取舍的先聊最核心的问题为什么用SpringBootVueMyBatis而不是SpringCloud微服务也不是JPA/Hibernate。来访管理系统听起来叫“企业级”但实际并发量真没那么高。一个几百人规模的企业高峰期一天来访登记可能也就几十到上百条这个量级用单体应用完全够。如果一上来就上微服务拆分服务、注册中心、配置中心、网关、分布式事务一套搞下来光维护成本就把项目拖垮了。技术选型不是越高级越好而是越匹配业务场景越好。SpringBoot的优势就是开箱即用、生态成熟、招人便宜配合内置的Tomcat跑一个单体服务轻松扛住这个量级。持久层选MyBatis而不选JPA核心原因是SQL可控性。来访管理涉及大量的多表联查和条件查询比如按访客姓名、手机号、访问部门、预约时间段、状态等多个条件组合查列表。MyBatis可以直接写原生SQL配合动态SQL标签可以精确控制每一条语句SQL调优的时候能清楚地看到执行计划。JPA虽然开发效率高但当查询条件组合复杂到一定程度自动生成的SQL往往不是最优的想优化还得回退到自定义QueryJPQL或者原生SQL绕一圈反而不如直接用MyBatis省心。前端用Vue是因为后台管理系统的开发效率需求非常明确Vue的双向绑定、组件化开发加上Element UI或Vue3的Element Plus这种现成的UI组件库表格、表单、弹窗、分页这些高频组件拖过来就能用开发速度极快。整个项目技术栈的匹配度很高后端一个SpringBoot应用前端一个Vue单页应用数据库一个MySQL实例部署简单维护直观。维度SpringBootMyBatis单体SpringCloud微服务JPA/Hibernate开发效率高适合业务CRUD密集场景低基础设施成本高较高但复杂查询控制力弱SQL可控性强原生SQL动态SQL强但跨服务联查麻烦弱自动SQL不一定最优部署运维一个Jar包搞定需要部署多个服务同左单体部署适用场景中小型后台管理系统大型复杂业务系统简单业务快速开发1.2 功能模块与业务流程怎么串系统整体可以拆成六个核心模块预约管理、审批管理、通行登记、黑名单、统计报表、系统管理。预约管理负责访客在线上填写来访信息、选择被访人、预约到访时间审批管理给被访人或者行政人员审批预约通行登记是门卫端操作支持直接登记临时访客也支持把已审批通过的预约转换为实际通行记录黑名单用于拦截有风险记录的访客统计报表用来分析每天、每周、每月的访问量、各部门接待量、访客平均滞留时间系统管理管用户、角色、菜单权限、基础数据字典。业务流程是这样的访客通过H5页面或者前台小程序提交预约申请填写姓名、手机号、身份证、来访事由、预计到访时间、被访人被访人在后台收到待审批记录通过或驳回审批通过后访客在约定时间到公司门口门卫在登记端输入手机号或者直接扫预约二维码调出预约信息核对后签入离开时再签离系统自动生成完整通行记录。如果预约了没来到第二天定时任务把状态更新为超时失效。临时来访不走预约流程门卫直接登记信息但也要过黑名单校验进入后给被访人发通知。这套流程最关键的设计思想是“状态可追踪”。纸质登记本的问题不只是记录麻烦更在于信息是静态的无法判断一个访客现在是不是还在公司里。系统引入状态流转之后每次状态变更都有操作人、操作时间出现安全事件时可以精确回溯访客的完整轨迹。这也是为什么我要在系统里专门设计一张状态流转相关的逻辑而不是简单在访客表上直接改字段。2. 数据库设计与MyBatis核心实现2.1 核心表结构设计与字段陷阱系统核心表包括访客信息表visitor、员工表staff、预约单表appointment、通行记录表visit_record、审批记录表approval_log、黑名单表blacklist、用户表sys_user、角色表sys_role等。这里给出一版可以直接用的核心建表SQL片段重点看字段类型和索引设计。CREATE TABLE visitor ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键ID, visitor_no varchar(32) NOT NULL COMMENT 访客编号格式YYYYMMDD6位序列, name varchar(50) NOT NULL COMMENT 访客姓名, phone varchar(20) NOT NULL COMMENT 手机号必须用varchar不能用int, id_card varchar(18) DEFAULT NULL COMMENT 身份证号, company varchar(100) DEFAULT NULL COMMENT 来访单位, black_flag tinyint(1) NOT NULL DEFAULT 0 COMMENT 是否命中黑名单 0否1是, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_phone (phone), KEY idx_name (name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT访客基础信息表;几个字段设计的经验手机号一定用varchar20而不要用int或bigint。可能有人觉得手机号是数字就顺手用了int但Java里int最大值21亿多手机号11位直接溢出bigint虽然不溢出但没法做手机号前几位模糊查询的索引优化而且也没有必要用数值类型去存一个不参与算术运算的字符串。身份证号同理必须用varchar而且还存在X结尾的情况。状态字段统一用tinyint并加注释比如visitor表里的black_flagvisit_record表里的status字段0待签入1已签入2已签离3已超时4已取消。数据库注释写清楚了后端起项目的人不需要翻代码才知道这个字段含义。外键不要物理建。我曾经见过有人把预约单、通行记录之间全部建了物理外键结果业务调整要改状态的时候各种约束冲突。逻辑外键就够了通过联查来保证数据完整性应用层控制业务关系这也是大多数企业项目的常见做法。关于索引通行记录表要按访客手机号和访问时间高频查询所以建议给visit_record表建(phone, plan_time)联合索引。统计报表经常按天、按部门聚合可以给record表加create_date字段冗余一个小字段避免在大表上直接用函数DATE(create_time)否则索引会失效。2.2 多条件查询的动态SQL写法来访管理列表查询可以说是典型的“多条件可选”场景查询条件可能是姓名、手机号、部门、状态、日期范围而且条件不固定。这种情况下最忌讳的方式是在Java代码里手工拼SQL字符串拼错了多一个AND调半天才发现是SQL语法问题。MyBatis的动态SQL就是专门解决这个问题的。核心就是where标签配合ifwhere会自动处理掉第一个条件前的AND或者OR不用自己操心。下面给一段访客记录联查的XML写法这是整套系统里最常用的一个查询。select idselectVisitRecordPage resultTypecom.example.vo.VisitRecordVO SELECT vr.id, vr.visitor_no, v.name AS visitor_name, v.phone, v.company, s.name AS staff_name, s.department, vr.plan_time, vr.sign_in_time, vr.sign_out_time, vr.status FROM visit_record vr LEFT JOIN visitor v ON vr.visitor_id v.id LEFT JOIN staff s ON vr.staff_id s.id where if testname ! null and name ! AND v.name LIKE CONCAT(%, #{name}, %) /if if testphone ! null and phone ! AND v.phone #{phone} /if if testdeptId ! null AND s.department_id #{deptId} /if if teststatus ! null AND vr.status #{status} /if if teststartTime ! null AND vr.create_time gt; #{startTime} /if if testendTime ! null AND vr.create_time lt; #{endTime} /if /where ORDER BY vr.create_time DESC /select这里有一个很容易踩的坑像和这种字符在XML里不能直接写必须转义成lt;和gt;或者用![CDATA[ ... ]]包裹。我自己早期写的时候忘了转义XML解析直接报错找了好半天。再一个就是多参数Mapper方法一定要加Param注解。比如上面的查询对应接口ListVisitRecordVO selectVisitRecordPage(Param(name) String name, Param(phone) String phone, Param(deptId) Long deptId, Param(status) Integer status, Param(startTime) LocalDateTime startTime, Param(endTime) LocalDateTime endTime);不加Param的时候MyBatis虽然会用arg0、param1这类默认名字绑定但可读性极差多个参数时稍不注意就报“Parameter name not found”错误。老老实实全部用Param标注XML里面引用的名字和注解名字保持一致这个类错误基本可以杜绝。还有全局配置里一定要开驼峰映射在application.yml里配置map-underscore-to-camel-case: true这样数据库的create_time才能自动映射到Java属性createTime。如果不配置返回的对象时间字段全为null前端列表空白一片。2.3 分页插件和MyBatis缓存的使用边界分页是列表查询的标配。我用的是PageHelper用法非常固定在Mapper查询前调用PageHelper.startPage(pageNum, pageSize)紧接着执行查询插件会自动给SQL追加limit语句。代码长这样PageHelper.startPage(pageNum, pageSize); ListVisitRecordVO list visitRecordMapper.selectVisitRecordPage(name, phone, deptId, status, startTime, endTime); PageInfoVisitRecordVO pageInfo new PageInfo(list);这里有个使用铁律startPage后面必须紧跟第一条要执行的查询中间不能夹带其它查询语句否则分页会作用到错误的查询上导致数据错乱。不要在循环里调用startPage如果循环里每查一次都调用分页参数会被覆盖查出来的数据也是乱的。关于MyBatis缓存我的建议就一句话默认配置够用就行不要轻易开二级缓存。一级缓存是SqlSession级别的同一个Session内多次查询相同SQL会走缓存正常解决问题。二级缓存是namespace级别的默认关闭。看着好像能提升性能但在多表联查场景下隐患很大比如查询visit_record的Mapper开启二级缓存另一个Mapper联查了visitor表并修改了visitor的数据visit_record的缓存是不会自动失效的查出来的结果可能是脏数据。项目里真正需要加速的热点数据比如首页统计数字直接用Redis做业务缓存比用MyBatis二级缓存可控得多。还有一个面试常问也经常在项目中遇到的问题Update执行特别慢。排查思路很固定第一步先用日志打印出执行的SQL放到数据库客户端里执行并用EXPLAIN看执行计划重点看是不是没有走索引造成全表扫描。有一个很典型的场景根据访客姓名更新记录name字段没建索引更新几条数据触发全表扫描解决方案是给name建索引或者改造成根据id更新。另一个场景是事务范围过大一个更新方法里先查了一堆数据又更新了许多行事务迟迟不提交导致行锁冲突解决方法是缩小事务边界只把必要的更新包在事务里。3. 后端业务实现的关键环节3.1 登录鉴权与权限控制后台系统肯定要登录我用的是JWT拦截器的方案没有直接上Spring Security原因很简单这套系统权限模型不算复杂用Spring Security需要配置SecurityFilterChain、UserDetailsService、PasswordEncoder等一堆组件学习成本高而且不直观。JWTHandlerInterceptor的方案写起来快逻辑也清晰登录成功后签发token前端每次请求把token放到Authorization头里后端写一个拦截器校验token并解析出当前用户。核心代码如下token生成我用的jjwt库密码加密用的BCrypt。// 登录接口 PostMapping(/login) public Result login(RequestBody LoginDTO dto) { SysUser user sysUserService.getByUsername(dto.getUsername()); if (user null || !BCrypt.checkpw(dto.getPassword(), user.getPassword())) { return Result.error(用户名或密码错误); } String token Jwts.builder() .setSubject(user.getId().toString()) .claim(username, user.getUsername()) .setExpiration(new Date(System.currentTimeMillis() 7 * 24 * 3600 * 1000L)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact(); return Result.success(token); } // 拦截器校验 public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { throw new BizException(401, 未登录); } Claims claims Jwts.parser().setSigningKey(secretKey) .parseClaimsJws(token.replace(Bearer , )).getBody(); request.setAttribute(userId, Long.valueOf(claims.getSubject())); return true; } }BCrypt这里多说一句为什么不用MD5。MD5加盐虽然也能用但BCrypt自带随机盐、计算速度可控抗暴力破解能力明显更强。密码长度相同的两个用户即使密码一样BCrypt生成的密文也不同安全性高一个量级。鉴权这块还要补充一个思路前端菜单和按钮的显示可以由后端返回权限标识控制但后端接口也必须做权限校验不能只靠前端隐藏按钮。我在项目里用一个自定义注解RequirePermission(visit:approve)加在审批接口上拦截器里检查当前用户的权限集合是否包含这个标识。只在前端控制而不在后端校验使用Postman直接调接口就能绕过菜单限制这是很多初学项目常见的安全漏洞。3.2 访客流程的状态机与并发控制业务状态流转是整套系统的核心逻辑。预约单的状态包括待审批、已通过、已驳回、已取消、已签入、已签离、超时失效。通行记录状态在预约单的基础上再拆分。写状态流转的时候我特别强调一个规范所有状态变更必须用“CAS式更新”也就是在SQL的where条件里带上当前期望状态更新成功返回1才代表状态变更成功。举例// 审批通过只有当前状态是“待审批”的预约单才能变更为“已通过” int rows appointmentMapper.updateStatusByIdAndStatus(appointmentId, AppointmentStatus.APPROVED.getCode(), AppointmentStatus.PENDING_APPROVAL.getCode()); if (rows 0) { throw new BizException(预约单状态已变更请刷新后重试); }UPDATE appointment SET status #{newStatus}, approve_time NOW(), approver_id #{approverId} WHERE id #{id} AND status #{expectStatus}这个写法的价值在并发场景下特别明显。假设同一个预约单审批人A和审批人B同时打开审批页面A点了通过B也点了通过。如果代码是先查状态、判断、再更新中间没有锁保护可能出现两个请求都判断“当前待审批”产生重复审批。用update ... where status 待审批数据库的行锁和条件更新天然保证了只有一个请求能更新成功第二个请求更新0行直接提示操作失败。这比在Java代码里写synchronized或者分布式锁简单得多也不容易出错。超时失效采用定时任务批量处理。项目里用Spring的Scheduled注解每半小时执行一次把到访时间已经过去且状态仍为“已通过”的预约单批量更新为“超时失效”。这个批量更新不要一次性UPDATE全表建议带上id范围分批处理每批限制几百条避免长事务锁住大量行影响正常的登记操作。3.3 文件上传、通知与黑名单辅助功能来访登记经常需要上传访客的身份证照片或者人脸照片这个用SpringBoot的MultipartFile接口基本是标配。上传后的文件建议单独放在一个目录比如/data/visit/upload/然后通过配置虚拟路径映射访问而不是把图片放到项目的resources目录下。项目目录里的文件会在重新部署时被清理掉到时候历史照片全部丢失这是真踩过的坑。spring: servlet: multipart: max-file-size: 10MB max-request-size: 20MB # 自定义上传目录映射 file: upload-dir: /data/visit/upload/再写一个WebMvcConfigurer把磁盘路径映射成URLConfiguration public class WebConfig implements WebMvcConfigurer { Value(${file.upload-dir}) private String uploadDir; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceLocations(file: uploadDir); } }通知功能我抽象了一个MessageSender接口实现类可以接短信网关、也可以测试环境直接打日志。生产环境接短信服务的时候只需要新增一个实现类不需要改动业务代码。黑名单模块的关键逻辑在登记和预约时都会执行根据访客手机号或身份证号去blacklist表比对命中后直接阻断操作并提示。手机号黑名单的判断优先级高于姓名原因很好理解身份证号可能记错但手机号是预约和登记的核心关联键。4. 前端Vue实现要点4.1 项目初始化与前端依赖前端用Vue构建后台管理界面项目初始化我建议直接使用Vue CLI或者Vite创建。Vite启动速度确实比Webpack快不少但在Vue 2生态里还是Vue CLI更稳如果选Vue 3直接用Vite就行配合Element Plus组件库开发体验很好。依赖安装阶段最容易踩的坑是Node版本不匹配。老一点的Vue 2项目装node-sass的时候如果Node版本过新node-sass编译会直接失败。解决办法是用nvm管理Node版本并在项目里固定engines字段或者直接用sass新版的Dart Sass替代node-sass安装基本不会再出问题。安装依赖时遇到卡顿先换镜像源npm config set registry https://registry.npmmirror.com npm install初始化完成之后建议先搭好目录结构按views页面、router路由、api接口请求、store状态管理、utils工具分层。后台管理系统页面一多如果全部堆在一个文件里后期维护成本非常高。4.2 路由守卫与路由参数传递登录鉴权在前端也有一套配合逻辑。路由配置使用全局前置守卫beforeEach每次路由跳转前检查当前用户token是否存在。没有token且目标路由不是登录页就重定向到登录页有token但是访问登录页就重定向到首页。import { createRouter, createWebHistory } from vue-router const router createRouter({ history: createWebHistory(), routes: [ { path: /login, component: () import(/views/Login.vue) }, { path: /, component: () import(/layout/Index.vue), meta: { requiresAuth: true } } ] }) router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next(/login) } else { next() } })动态路由这块如果是给不同角色分配不同的菜单权限可以在登录后从后端拉取菜单数据再用router.addRoute()动态注册。注意动态添加路由后刷新页面会丢失需要把菜单权限存到localStorage或者Pinia里刷新时重新根据权限重建路由。这个功能如果做不好就会出现“登录后进入页面刷新就404”的经典问题。路由参数传递也是一个容易被忽略的点。跳转详情页时用query传参比如router.push({ path: /visit/detail, query: { id: record.id } })刷新页面参数还在URL上不会丢如果用params传参刷新后参数直接丢失页面拿不到ID就渲染空白。这里建议统一用query传业务参数。4.3 Axios封装与跨域拦截前端所有HTTP请求必须走一个统一的Axios实例便于统一处理token注入、错误提示、超时等逻辑。我在utils/request.js里写了一套简单的封装import axios from axios import { ElMessage } from element-plus import router from /router const request axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }) request.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) } ElMessage.error(error.response?.data?.message || 网络异常) return Promise.reject(error) } ) export default request开发环境跨域问题我建议直接用Vite或Vue CLI的proxy配置后端不需要额外处理CORS。Vite在vite.config.js里配置server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这样前端请求/api/login开发服务器会自动转发到http://localhost:8080/api/login浏览器看到的还是同源的请求不存在跨域。生产环境再交给Nginx做反向代理这是前后端分离项目最常见的部署模式。5. 部署上线与生产环境配置5.1 MySQL初始化与JDBC连接配置项目使用的数据库是MySQL开发环境可以本地安装生产环境一般用云数据库或者自建MySQL。安装完成后第一件事不是建表而是确认数据库字符集。建议统一使用utf8mb4它比utf8多支持emoji等四字节字符访客信息里万一有人名字里带生僻字或者特殊符号utf8mb4都不会乱码。CREATE DATABASE visit_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;执行项目的init.sql建表脚本生成基础数据和默认管理员账号。SpringBoot的数据库连接配置重点在JDBC URL参数不同版本驱动参数略有差异我用的MySQL 8.0驱动配置如下spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/visit_system?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: yourpassword hikari: maximum-pool-size: 10 minimum-idle: 5 connection-timeout: 30000serverTimezoneAsia/Shanghai和useSSLfalse这两个参数建议务必写上。不写时区Java 8的LocalDateTime和数据库datetime之间的转换会差8小时所有记录时间都会不对不关SSL测试环境连接时经常报证书相关错误调试体验很差。5.2 后端打包与Linux服务器部署后端打包在项目根目录执行mvn clean package -Dmaven.test.skiptrue打包好的jar在target目录下名称类似visit-system-1.0.0.jar。生产环境用application-prod.yml配置文件启动的时候通过--spring.profiles.activeprod指定。生产配置和开发配置的区别主要是数据库地址、日志级别、上传目录地址、以及是否开启Swagger等调试功能。启动命令我一般这么写nohup java -Xms256m -Xmx512m -jar visit-system-1.0.0.jar \ --spring.profiles.activeprod \ --server.port8080 \ logs/app.log 21 JVM内存参数根据服务器配置调整小型项目256M到512M足够了不要盲目给到1G或2G反而会造成资源浪费。日志输出重定向到文件方便排查问题。这里有个常见问题开发环境连接数据库正常部署到服务器后报Communications link failure或者连接超时。排查顺序是先ping通不通再telnet ip 3306端口通不通最后看MySQL配置里的bind-address是不是绑定了127.0.0.1只允许本机连接。云服务器还要检查安全组是否放行了3306端口。顺序不要乱很多新人一上来就怀疑代码问题实际上八成是网络和端口问题。5.3 前端构建与Nginx反向代理前端构建命令npm run build构建产物在dist目录里面是纯静态文件。部署时我用Nginx托管静态文件并把/api路径反向代理到后端服务。Nginx配置如下这是前后端分离项目的标准模板server { listen 80; server_name visit.example.com; # 前端静态文件 root /opt/visit/dist; index index.html; # 解决Vue history路由刷新404 location / { try_files $uri $uri/ /index.html; } # API反向代理 location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 静态资源缓存 location /assets/ { expires 7d; add_header Cache-Control public; } }try_files $uri $uri/ /index.html这一行是用来解决Vue使用history路由时刷新子页面404的问题。原理是Nginx发现磁盘上找不到对应的静态文件就回退到index.html由前端路由接管渲染。如果前端用了hash路由这一行不加也可以但后台管理系统从URL美观和SEO角度来说用history还是更常规的选择。注意location /api/的proxy_pass末尾带了/表示转发时去掉/api前缀。比如前端请求/api/login后端实际收到的是/login。如果后端接口本身没有context-path这种写法是对的如果后端controller里写的是RequestMapping(/api/login)那proxy_pass末尾就不要带/。这个斜杠问题容易导致接口404前后端联调时要注意一致性。6. 常见问题排查与避坑实录6.1 高频问题速查表项目开发和部署过程中我整理了一张高频问题速查表。这些问题的出现频率非常高几乎是每个做这套架构的人都会遇到至少一两项问题现象可能原因解决办法后端启动报端口被占用上一次进程没有完全停止或端口被其它程序占用lsof -i:8080查占用进程kill 掉后重启前端请求接口404代理路径配置错误、后端context-path不一致核对 Nginx/api/斜杠规则和 proxy_pass 结尾斜杠接口返回数据中文乱码数据库字符集不是 utf8mb4、JDBC 未指定 UTF-8统一在 URL 加characterEncodingutf8数据库改 utf8mb4分页查询结果重复或丢失PageHelper.startPage 与查询之间夹了其它 SQL保证 startPage 后紧跟目标查询循环中禁止调用 startPage列表查询报SQL语法错误if条件组合导致多了不必要的 AND使用where标签包裹由框架自动处理第一个 AND时间字段相差8小时数据库和应用的时区配置不一致JDBC URL 加serverTimezoneAsia/Shanghai上传文件过大被拒绝SpringBoot 默认单文件限制 1MB配置spring.servlet.multipart.max-file-sizeVite 启动后页面访问失败Node 版本过高或依赖缓存异常用 nvm 切到指定 Node 版本清理 node_modules 重装MySQL 连接报 SSL 或公钥检索错误驱动版本与数据库配置不匹配URL 加useSSLfalseallowPublicKeyRetrievaltrue登录接口调通但页面仍跳登录token 没被 Axios 拦截器正确注入检查 request.js 中 Authorization 头格式和后端解析是否一致6.2 几条靠经验换来的开发建议除了速查表里的具体问题还有几条开发层面的建议是在实际项目里踩过不少坑总结出来的。第一列表查询不要写select *写全字段。可能有人觉得这样省事但后续表结构增加字段时select *会把新字段也查出来如果VO里没有对应字段不管是Map映射还是JDBC驱动都会出现各种奇怪的问题。更严重的是select *会让查询带上很多不必要的大字段数据量上来后网络传输和内存开销都很难看。第二警惕N1查询问题。比如查询通行记录列表需要显示被访人姓名最直观的做法是查完记录后循环查员工表一条记录查一次。记录多的时候数据库会被打爆。正确做法是联查一次性查出关联字段或者用IN批量查询。MyBatis的foreach标签配合IN查询是常见的优化手段。foreach collectionstaffIds itemstaffId open( separator, close) #{staffId} /foreach第三前端隐藏按钮不等于安全。权限控制必须以后端为准前端只是提升用户体验。项目里出现过开发人员通过浏览器控制台手动调用接口修改审批状态的测试如果后端不做权限校验这种行为完全能绕过界面限制。第四事务不要乱加。我在审批通过逻辑里一开始给整个方法加了Transactional后来发现方法里调用了短信通知接口短信服务响应慢的时候事务一直挂着不提交数据库连接被白白占用。现在的做法是事务只包裹状态变更相关的数据库操作外部通知放在事务提交后异步发送拆分成两个独立方法。第五定时任务记得批处理。批量更新超时预约单时如果一次UPDATE影响几万行数据行锁范围太大会影响正常业务的写入。我把定时任务改成了分批循环处理每批500条执行完一批提交一次耗时和锁粒度都可控。最后分享一个实用的小技巧黑名单校验在登记接口里用AOP做统一拦截。定义一个BlacklistCheck(phone)注解标注在切换控制器的方法参数上AOP切面自动从参数里取出手机号执行黑名单比对命中就抛异常阻断流程。这样每个需要校验的接口不用重复写判断逻辑后续要调整黑名单校验规则也只需要改一个切面。我这套系统做完之后最直观的感受是访客管理这种业务看着简单但把状态流转、权限校验、数据联动这几个点做扎实还是需要不少细致功夫的。希望这篇拆解能帮到正在做类似系统或者准备拿这个方向做毕设的朋友少走弯路。

相关推荐

Linux运维三剑客:ps、ss、lsof搞定进程与端口排查
Linux运维三剑客:ps、ss、lsof搞定进程与端口排查

深夜两点,业务群里一条消息炸开了锅:“后台服务是不是挂了?端口怎么连不上?”运维人碰到这种场景,第一反应往往不是猜,而是“看”。看什么?看进程,看端口。进程决定服务活着没有&… · 2026/9/26 5:00:26

PostgreSQL uuid-ossp 扩展安装与排错实战指南
PostgreSQL uuid-ossp 扩展安装与排错实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 5:00:26

认识眼睛调节力:比视力更关键的近视防控指标与训练指南
认识眼睛调节力:比视力更关键的近视防控指标与训练指南

1. 先聊点扎心的:查视力单,你究竟在看什么?你有没有遇到过这种情况:带孩子查完视力,电脑验光单上写着“近视 -1.00D”,医生说“注意控制”,于是你回家每天恨不得拿戒尺盯着孩子别趴着写字&#… · 2026/9/26 5:00:20

Agent工具调用实战:从Function Calling原理到代码实现
Agent工具调用实战:从Function Calling原理到代码实现

做 Agent 做到第八篇,前面几篇我们聊了框架、规划、状态流转,甚至给 Agent 加了点简单的记忆。但如果你真的动手把项目跑起来,大概率会遇到同一个问题:不管它在对话里怎么能说会道,一旦要它“干点实事”——查一下实时… · 2026/9/26 5:33:32

基于Paimon+StarRocks的轨迹数据统一底座架构实践
基于Paimon+StarRocks的轨迹数据统一底座架构实践

如果你在高德这类体量的业务里碰过轨迹数据,大概会有同样的感受:GPS 点本身不复杂,复杂的是它的下游。同一个经纬度坐标,既要支持实时位置查询,又要做历史轨迹回放,还要喂给热力图、ETA、OD 分析、偏航纠偏… · 2026/9/26 5:33:32

Kafka vs RabbitMQ:核心概念模型全面拆解与选型指南
Kafka vs RabbitMQ:核心概念模型全面拆解与选型指南

最近有个朋友问我:"你们系统用的Kafka还是RabbitMQ?听说Kafka比RabbitMQ好用,是不是该换?"我听完愣了一下,因为这两个东西虽然都叫消息队列,但底层模型完全是两码事,压根不是"谁… · 2026/9/26 5:33:32

Python就业推荐系统毕业设计:爬虫、TF-IDF与协同过滤完整实现
Python就业推荐系统毕业设计:爬虫、TF-IDF与协同过滤完整实现

1. 为什么毕业设计选这个题:就业推荐系统不是"简单"是"稳"1.1 一个Python毕设题目的自我修养每年毕业季我都会被问到一个问题:"毕设选什么题目能又好过又不掉头发?" 说实话,选"基于Python的大… · 2026/9/26 5:33:32

CAD新手3天实战指南:破解安装、字体、坐标、协作五大生死节点
CAD新手3天实战指南:破解安装、字体、坐标、协作五大生死节点

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 5:33:32

Jev爆火背后:开源AI对话模型本地部署与照片修复实战
Jev爆火背后:开源AI对话模型本地部署与照片修复实战

最近我的社交圈被一个词刷屏了:Jev。从技术群到小红书,从CSDN到GitHub Trending,到处都在问“Jev是什么”。作为一个常年蹲在开源模型圈子里的人,我一开始以为又是某个换皮大模型,结果仔细扒了一圈才发现,这… · 2026/9/26 5:33:25

数据库课后习题答案别硬背:当测试用例集刷,效率翻倍
数据库课后习题答案别硬背:当测试用例集刷,效率翻倍

简介:万常选版《数据库原理与设计》课后习题答案资源,覆盖第2至6章及第9章,适合正在学习关系模型、数据库建模、关系数据理论与模式求精的本科生、自学者作为复习与自测材料。压缩包共7个文件,含3个doc参考答案、2个sql示例脚本、… · 2026/9/26 0:00:21

OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置
OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 0:00:40

向下兼容与向上兼容:接口设计中的兼容性策略与工程实践
向下兼容与向上兼容:接口设计中的兼容性策略与工程实践

一次版本升级事故,是很多团队绕不过去的坎。线上环境里,服务端明明已经上线了新版接口,老的移动端还在照着旧文档传参数。请求一到网关,校验直接拒绝,用户操作失败,客服群炸了锅,开发群里开始互… · 2026/9/26 0:00:46

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码