1. 先去场馆坐半天预约系统的本质是人、场地、时间的绑定去年帮朋友所在的社区体育馆做内部管理系统我特地去前台坐了半天观察他们的工作流。前台小姑娘的日常就是接电话、翻一个共享Excel表、拿笔登记遇到重复预订、临时取消、场地维护全凭人肉记忆周末高峰期常常自己都分不清哪个场子到底空没空。所以这类体育馆预约平台表面上是个技术项目本质上是在解决一个非常具体的业务问题把谁在哪个场地占用哪个时间段这三件事稳定地记录下来并且保证互不冲突。技术栈已经明确是前后端分离的SpringBootVueMyBatisMySQL意味着这个系统天然分成两块Vue负责页面展示和交互SpringBoot负责业务逻辑和接口MyBatis负责数据库操作MySQL负责最终存储。这个组合在目前中小型项目中非常常见做毕设、做课设、或者给单位内部搭一套预约系统都很合适。下面我会从业务建模一直讲到部署上线把完整链路中值得注意的地方全部过一遍。1.1 业务痛点与系统目标先想清楚一个事情预约系统要解决的核心矛盾是场地有限、时段有限、需求随机。体育馆里的羽毛球馆可能只有6片场地每个场地每天有固定的开放时段用户想在周五晚上7点约一块场地系统要做的就是在这个时段被占掉之前允许用户锁定它。基于这个目标系统至少要具备以下能力用户注册、登录区分普通用户和管理员两种角色。场馆信息展示包括场地名称、类型、位置、时段价格和当前可用状态。用户按日期和时段在线预约能查看自己的预约记录也能取消未开始的预约。管理员维护场馆基础信息查看所有订单必要时强制取消某条预约。所有预约操作必须避免同一场地同一时段被重复预约的问题。很多新手拿到这类需求会直接开写代码结果写到订单表的时候才想起来要处理冲突写到管理端的时候发现前期表结构没有状态字段写到部署的时候才发现数据库连接串配错了时区。所以我的建议是第一步不是建项目而是先把业务规则一条条列出来哪怕写在纸上都行。1.2 角色权限和预约规则怎么定角色权限这里不需要设计得过于复杂。普通用户就是注册、登录、选场地、预约、取消、看自己的记录管理员负责场馆维护和订单管理。前后端分离的结构下权限控制通常在两个层面做前端通过路由守卫控制页面入口后端通过拦截器校验接口权限。光靠前端隐藏按钮是不够的后端接口必须自己校验角色否则别人模拟一个请求就能把管理接口扫一遍。预约规则建议在项目初期就定死否则后期改起来牵一发动全身。以我做的这个场馆为例开放时间是每天9点到22点最小预约单位是半小时用户每次最多预约2小时可以提前7天预约开场前2小时不允许取消。你可以根据自己场馆的情况调整但这些参数最好集中放在后端配置里不要散落在代码的各个角落不然上线的第一个周末就会有人打电话投诉。1.3 核心业务名词先统一开发前把名词统一后面沟通会省很多事。这套系统里最常见的几个概念是场馆Venue物理场地比如羽毛球1号场。时段TimeSlot预约的时间片比如2024-05-20 19:00-20:00。预约单Reservation用户和场馆时段的一次绑定关系。状态Status预约单的状态常见有待支付、已确认、已完成、已取消。这一段看起来不像技术但很多项目做砸了就是因为场馆场地预约订单这些词大家理解不一致前后端联调的时候接口字段名叫法五花八门平白增加沟通成本。把这几个词在需求文档里固定好后面的表设计和接口路径都会清晰很多。2. 技术选型不跟风这四件套为什么够用且好维护很多初学者喜欢追新一上来就要上微服务、上Docker、上Redis集群。我理解追求学习新技术的心情但这类预约系统本质上属于CRUD密集型业务系统核心难点在于并发控制和状态流转不在于架构炫技。SpringBootVueMyBatisMySQL这套组合是当前中小型项目里非常主流、资料最全、排错最容易的搭配对个人学习、毕设展示和小型真实项目都足够。2.1 SpringBoot负责后端服务省去大量配置SpringBoot最直观的价值是自动配置。打个比方以前用SSMSpringSpringMVCMyBatis搭项目要写web.xml、spring配置、mybatis配置、数据库连接池配置一堆XML看得人头皮发麻。SpringBoot把这些约定成俗的东西都自动配好了你只需要引入对应的starter依赖它自己在启动时帮你装配好。放到预约系统这个场景里SpringBoot提供的核心能力有这些内置Tomcat打成一个Jar包直接运行部署时不用单独装Tomcat。spring-boot-starter-web提供RESTful接口支持前后端分离靠的就是这一层Json传输。spring-boot-starter-validation做参数校验比如手机号格式、预约时段是否为空。配置项集中在application.yml数据库、端口、日志、文件上传大小全在这里改。生态成熟遇到问题搜一下基本都有解决方案。如果你看到若依框架这类快速开发脚手架也别觉得项目落后很多前后端分离的预约系统都基于类似RuoYi二次开发核心还是SpringBoot这一套只是提前集成了权限、代码生成、定时任务等通用功能。新手自己从零搭一遍流程收获会比直接套脚手架更大。2.2 Vue负责前端交互Vue 2还是Vue 3要想清楚Vue的核心是组件化开发。页面拆成一个个组件每个组件管理自己的状态和模板数据变化时页面自动更新开发体验比操作DOM的老写法舒服太多。现在的问题是该选Vue 2还是Vue 3。如果你是自己从零开始新写项目我建议直接Vue 3 ViteVue 3的性能更好Composition API写起来逻辑复用更清晰而且Vite冷启动快得让人感动npm install之后启动基本上秒开。但如果你下载的是网上现成的源码很多是Vue 2 Vue CLI Element UI的组合运行之前先看清楚package.json里的版本再装依赖不然一个npm install装出一堆高版本依赖前端直接起不来。在这套预约系统里Vue主要负责四件事路由跳转、接口请求、状态管理、组件渲染。实际开发中vue-router、axios、vuex/pinia是跑不掉的三个库我后面会逐个讲它们是怎么配合的。2.3 MyBatis加MySQLSQL自由是这个组合的核心优势MyBatis和JPA/Hibernate的最大区别在于MyBatis把SQL牢牢握在开发者手里SQL怎么写由你决定。预约系统里会有大量复杂的查询比如查询某个场馆在某天已经预约的时段列表统计每个场馆的周预约量这类语句用MyBatis的XML文件写起来非常直观也方便DBA后期帮你调优。MySQL则是这套组合里最稳定的一环。部署简单一个MySQL 8.0的安装包解压配好就能跑官方文档和中文资料都极其丰富。对于预约系统这种数据量在百万以内的项目正确建索引之后MySQL性能完全不是瓶颈。值得提醒的是MyBatis分页不要自己用limit去算页码直接用PageHelper插件ThreadLocal方式传递分页参数一行代码搞定分页。我见过最惨的写法是手动拼limit、count两套SQL改个页码还要维护两个查询后期痛点非常明显。2.4 没上Redis、MQ这类中间件的取舍逻辑我理解很多人会问预约系统不需要Redis缓存吗不需要消息队列削峰吗答案是看规模。如果场馆只有十几块场地一天几千次访问MySQL加正确索引完全扛得住强行引入Redis反而增加了缓存一致性、过期策略、缓存穿透这些新问题。消息队列更是没有必要预约这个动作本身是同步操作用户点了提交就需要立刻知道成功还是失败异步化之后反而让用户困惑。但这不代表完全不用中间件。如果项目明确要求处理高并发秒杀式预约一个务实的升级路径是先在预约表上加唯一约束挡住并发再用Redis做分布式锁兜底最后把短时高峰流量缓冲到MQ里排队落库。这是后话初学者先把单体版本跑通理解每一条SQL的真实执行计划比盲目堆中间件收获大得多。3. 数据库先建模预约冲突从源头就要堵死数据库设计是预约系统的地基。地基没打好后面写再多业务代码都是拿泥巴糊墙。我见过很多人的订单表连唯一索引都不知道加最后并发测试一压全是重复预约记录只能手动删数据。这一节我从三张核心表讲起再重点讲如何用数据库设计从源头拦截冲突。3.1 三张核心表与字段设计一个最简可用的预约系统至少需要三张表用户表、场馆表、预约表。再扩展的话可以加场馆类型表、支付订单表、操作日志表但核心业务就围绕这三张表转。用户表sys_user的常见字段CREATE TABLE sys_user ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, username VARCHAR(50) NOT NULL COMMENT 登录名, password VARCHAR(100) NOT NULL COMMENT BCrypt加密后的密码, nickname VARCHAR(50) DEFAULT NULL COMMENT 昵称, phone VARCHAR(20) DEFAULT NULL COMMENT 手机号, role TINYINT NOT NULL DEFAULT 1 COMMENT 1-普通用户 2-管理员, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;场馆表venue主要存物理场地信息比如名称、类型、每小时价格、所在位置、状态。注意这里不要想太多不需要把每一天、每一个时段都预先拆成一条记录存在表里那是过度设计。场馆表就是静态的基础资料动态的可约状态由预约表反向推导出来。预约表reservation是整个系统的核心字段设计决定系统上限CREATE TABLE reservation ( id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT 预约人, venue_id BIGINT NOT NULL COMMENT 场馆id, reserve_date DATE NOT NULL COMMENT 预约日期, start_time VARCHAR(10) NOT NULL COMMENT 开始时间 HH:mm, end_time VARCHAR(10) NOT NULL COMMENT 结束时间 HH:mm, status TINYINT NOT NULL DEFAULT 0 COMMENT 0-已确认 1-已完成 2-已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_venue_slot (venue_id, reserve_date, start_time), KEY idx_user_date (user_id, reserve_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT预约表;注意到没有预约表里有一个唯一索引uk_venue_slot这就是承担防止同一场地同一开始时间被重复预约的数据库层防线。后面我会详细说它是怎么在并发下起作用的。3.2 时段粒度为什么用半小时而不是精确到分钟时段的粒度选择很关键它直接影响整个系统的复杂度和用户体验。如果粒度精确到分钟用户看场馆日历会看到无数个可选时间点数据库里的冲突判断也要变成区间重叠判断逻辑复杂很多。如果粒度太粗比如固定2小时一场用户想订1小时就很尴尬。折中方案是使用固定时隙Time Slot比如30分钟为一个槽位。用户预约19:00-20:00实际上就是占用19:00、19:30两个槽位。实现时用VARCHAR存start_time和end_time格式是HH:mm比较时按字符串排序即可不需要转成DateTime去算。这个方案的好处是前端展示时段列表直接按槽位渲染清晰明了。后端冲突判断从区间重叠判断退化为槽位精确匹配非常高效。数据库唯一索引可以直接覆盖不需要写复杂条件锁。代价是灵活性下降用户无法预约19:10到19:50这种任意时段。对体育馆来说这个代价完全可以接受现实中场馆预约本来就是按整点或半小时排期。3.3 防止重复预约的双保险唯一索引加事务重查解决了时段粒度问题重复预约还有最后一个漏洞两个用户同时提交同一个场地的同一个时段。这时候只靠应用层先查再插是不够的因为两个请求可能同时查到当前没有冲突然后同时插入最终重复。数据库层的唯一索引就是在这个场景中起作用的核武器。当服务端同时收到针对同一venue_id、reserve_date、start_time的两条插入请求时数据库唯一索引会让第二条插入直接报DuplicateKeyException不会产生两行脏数据。应用层还要再加一道保险在事务内先使用SELECT ... FOR UPDATE锁定场馆当天相关记录再判断是否冲突最后插入。这种悲观锁唯一索引的组合开销不高正确性却非常有保障。完整代码我在第四章给出。3.4 RESTful接口清单提前规划建表之后接口文档也建议一开始就列个清单。这套系统最核心的接口大概如下模块方法路径说明用户POST/api/user/register注册用户POST/api/user/login登录返回Token用户GET/api/user/info获取当前用户信息场馆GET/api/venue/list分页查询场馆列表场馆GET/api/venue/detail查询场馆详情预约GET/api/reservation/available查询某场馆某天的已约时段预约POST/api/reservation/create创建预约预约GET/api/reservation/my我的预约列表预约POST/api/reservation/cancel取消预约管理POST/api/admin/venue/save新增或修改场馆管理GET/api/admin/reservation/list管理员查看全部预约路径命名统一为/api开头方便Nginx做反向代理转发。开发阶段前后端端口不同也需要通过这个前缀做跨域和代理设置这一块到部署章节再展开。4. 后端SpringBoot实现的关键链路JWT鉴权、事务与MyBatis动态SQL进入代码阶段。这一节我只挑三个最有代表性的链路来讲登录鉴权、预约下单、以及MyBatis分页和动态SQL。这三块覆盖了后端70%的工作量剩下的就是普通的CRUD照着写就行。4.1 项目骨架与源码结构如果你拿到的是完整源码第一步先看目录结构。一个合理的SpringBoot前后端分离项目后端目录应该长这样src/main/java/com/example/gym/ ├── config/ │ ├── CorsConfig.java # 跨域配置 │ ├── WebMvcConfig.java # 拦截器注册 │ └── MybatisPlusConfig.java # 分页插件配置 ├── controller/ │ ├── UserController.java │ ├── VenueController.java │ └── ReservationController.java ├── service/ │ ├── UserService.java │ ├── VenueService.java │ └── ReservationService.java ├── mapper/ │ ├── UserMapper.java │ ├── VenueMapper.java │ └── ReservationMapper.java ├── entity/ │ ├── User.java │ ├── Venue.java │ └── Reservation.java ├── common/ │ ├── Result.java # 统一返回体 │ └── JwtUtil.java # JWT工具 ├── exception/ │ └── GlobalExceptionHandler.java └── interceptor/ └── AuthInterceptor.java # 登录拦截器关键点统一返回体Result一定要有它保证所有接口无论成功失败都返回code、message、data三件套前端axios拦截器只需要按这个结构解包即可不用每个接口单独判断。4.2 JWT登录鉴权的实现思路JWTJSON Web Token是目前前后端分离项目最常用的登录态方案。用户登录成功后后端生成一个Token字符串返回给前端前端存在localStorage或状态管理里之后每次请求在请求头带上Authorization字段后端拦截器校验Token的合法性和有效期。Token本身携带了用户基本信息比如userId和role后端解码就能拿到当前身份不需要每次查数据库。生成Token的简化代码public String generateToken(Long userId, String username, String role) { return Jwts.builder() .setSubject(username) .claim(userId, userId) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() 7 * 24 * 3600 * 1000)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); }拦截器里面对OPTIONS请求直接放行否则前后端分离跨域预检请求会被拦截导致前端报一堆CORS错误。这个坑我至少见过十个人踩过。管理员接口的权限校验可以在拦截器里拿到role字段后判断role不等于2就返回403。不要只在前端路由守卫里判断角色后端必须把权限边界守住。4.3 预约下单Service事务、锁与唯一索引的组合预约创建是整个系统技术含量最高的地方。核心逻辑分四步对预约表和场馆记录加锁悲观锁查询该场馆当天指定时段是否已经被预约没冲突则插入预约记录库表唯一索引兜底提交事务响应结果参考代码如下Transactional(rollbackFor Exception.class) public Result createReservation(ReservationCreateDTO dto, Long userId) { // 1. 锁住场馆记录防止同一个场馆并发处理 Venue venue venueMapper.selectByIdForUpdate(dto.getVenueId()); if (venue null) { return Result.error(场馆不存在); } // 2. 校验时段是否合法 if (!validateTimeSlot(dto.getStartTime(), dto.getEndTime())) { return Result.error(预约时段不合法); } // 3. 检查该时段是否已被约走 int count reservationMapper.countConflict( dto.getVenueId(), dto.getReserveDate(), dto.getStartTime(), dto.getEndTime()); if (count 0) { return Result.error(该时段已被预约请选择其他时间); } // 4. 插入唯一索引兜底 try { Reservation reservation new Reservation(); reservation.setUserId(userId); reservation.setVenueId(dto.getVenueId()); reservation.setReserveDate(dto.getReserveDate()); reservation.setStartTime(dto.getStartTime()); reservation.setEndTime(dto.getEndTime()); reservation.setStatus(0); reservationMapper.insert(reservation); } catch (DuplicateKeyException e) { throw new BusinessException(手慢了该时段刚刚被抢走了); } return Result.success(预约成功); }这段代码最关键的是两个点selectByIdForUpdate一定要存在它把当前场馆的行锁住了后面另一个并发请求只能排队等待countConflict这个SQL里查询的条件依赖前面的行锁这样在并发场景下不会出现两条请求同时查到count0的脏数据。再加上表里唯一的索引兜底才算完整闭环。4.4 MyBatis动态SQL和分页插件预约系统里会大量用到动态查询。比如场馆列表支持按名称模糊搜索、按类型过滤预约列表支持按时间范围、状态过滤。这些用MyBatis的if标签组合起来非常方便。select idselectReservationPage resultTypecom.example.gym.entity.Reservation SELECT * FROM reservation where if testuserId ! null AND user_id #{userId} /if if testvenueId ! null AND venue_id #{venueId} /if if teststatus ! null AND status #{status} /if if teststartDate ! null AND reserve_date gt; #{startDate} /if if testendDate ! null AND reserve_date lt; #{endDate} /if /where ORDER BY reserve_date DESC, start_time ASC /select这里有个经典小坑写日期比较时和是XML的保留字符必须换成gt;和lt;否则XML解析直接报错。很多新手第一跑就是挂在这里。分页直接用PageHelper三步走引入依赖、配置插件、业务代码里调用PageHelper.startPage紧接着的下一条select查询就会自动拼接limit。注意startPage只管它后面第一条查询如果你在中间插入了其他Mapper查询分页条件就会被吞掉。public PageResultReservation pageMyReservations(int page, int size, Long userId) { PageHelper.startPage(page, size); ListReservation list reservationMapper.selectReservationPage( new ReservationQuery(userId, null, null, null, null)); PageInfoReservation pageInfo new PageInfo(list); return PageResult.of(pageInfo.getTotal(), pageInfo.getList()); }5. Vue前端落地路由守卫、axios封装与场馆卡片后端把接口都提供齐了前端就是一个取数据、渲染数据、传数据的过程。但要想做得像样以下几个基础工程必须搭好。5.1 前端工程结构与依赖一个合理的前端项目目录src/ ├── api/ │ ├── request.js # axios统一封装 │ ├── user.js # 用户相关接口 │ ├── venue.js # 场馆相关接口 │ └── reservation.js # 预约相关接口 ├── assets/ ├── components/ │ ├── VenueCard.vue # 场馆卡片组件 │ └── ReserveDialog.vue # 预约弹窗组件 ├── router/ │ └── index.js # 路由配置 路由守卫 ├── store/ │ └── user.js # 用户状态管理 ├── views/ │ ├── Login.vue │ ├── Register.vue │ ├── Home.vue │ ├── VenueDetail.vue │ ├── MyReservation.vue │ └── admin/ │ ├── AdminLayout.vue │ ├── VenueManage.vue │ └── ReservationManage.vue └── App.vue前端目录的划分核心原则是api目录统一放接口请求views目录按页面模块组织components目录放公共组件。这样即使项目变大也还能保持结构清晰。5.2 axios封装请求拦截器与响应拦截器axios如果不封装每个页面都要写一套请求逻辑代码会非常碎。统一封装的核心有两个拦截器请求拦截器把localStorage里的Token取出来放到请求头Authorization里。没登录就跳登录页。响应拦截器后端返回的Result统一包成了code、message、data这里统一处理code不为200的情况比如Token过期就清除本地用户信息并跳回登录页。封装代码大致长这样// api/request.js import axios from axios import router from /router import { ElMessage } from element-plus const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization token } return config }) request.interceptors.response.use( response { const res response.data if (res.code ! 200) { if (res.code 401) { localStorage.removeItem(token) router.push(/login) } ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res }, error { ElMessage.error(error.message || 网络异常) return Promise.reject(error) } ) export default request这里用baseURL/api而不是写死后端IP是因为本地开发时可以通过Vite代理解决跨域部署时通过Nginx转发。把这个习惯养成就不会出现本地改一个IP、线上又改一个IP的窘境。5.3 路由守卫页面访问的看门人前端路由守卫主要做两件事判断是否登录、判断角色有没有权限。Vue Router 4提供的beforeEach钩子里拿到目标路由meta里配置的requiresAuth和role信息再和本地存储的登录状态比对。router.beforeEach((to, from, next) { const token localStorage.getItem(token) const role localStorage.getItem(role) if (to.meta.requiresAuth !token) { next(/login) return } if (to.meta.role to.meta.role ! Number(role)) { next(/403) return } next() })记住前面说过的一句话路由守卫只是用户体验层面的拦截不是安全边界。服务器的接口权限校验才是真正的防线两边都要做不要二选一。5.4 场馆卡片与预约弹窗的交互细节前端技术含量最高的地方在我看来是动态获取某天某场馆的可约时段。用户选了一个日期前端就要请求后端拿到该场馆当天已经被预约的时段列表然后渲染卡片时把已约时段置灰把可约时段亮出来。这个交互的效果决定了这个系统看起来是能用的工具还是花架子。实现时用户切换日期时调用 /api/reservation/available 接口。返回的List是已约时段的字符串数组比如 [19:00-20:00]。前端生成从9:00到22:00的连续时段列表与已约集合比对动态生成可点击状态。用户点击某个时段后弹窗展示场地名称、日期、时段、单价确认后调reservation/create接口。这个流程简单直接前端不需要自己维护太复杂的逻辑。还有一个小细节用户取消预约后场馆卡片的可用时段应该立刻刷新。处理方式是在取消接口返回成功之后再调一次available接口刷新数据不要让前端凭感觉改状态避免数据不一致。6. 从本地跑到服务器完整部署教程源码在手、本地能跑只是第一步很多人挂在部署上线这一步。前后端分离项目部署其实流程固定掌握一次就能举一反三。6.1 本地开发环境版本对照不同的SpringBoot、Node、MySQL版本组合表现差异很大。最稳的一套版本组合建议如下组件推荐版本说明JDK1.8目前大部分SpringBoot 2.x源码运行最稳的版本Maven3.6依赖管理和打包工具SpringBoot2.7.x成熟稳定资料最多Node.js16或18Vite 4/Vue 3项目可用MySQL8.0使用utf8mb4字符集Nginx1.24部署前端和反向代理如果你的项目是SpringBoot 3.x版本JDK必须换成17及以上MyBatis依赖坐标也有变化很多老旧教程不适用。源码是什么版本就用对应的环境不要拿SpringBoot 3.x项目配JDK 8启动时直接报UnsupportedClassVersionError。6.2 后端打包与配置外置本地开发时数据库连接直接写在application.yml里但部署到服务器时配置最好外置这样改配置不需要重新打Jar包。做法是在Jar包同目录放一个application-prod.yml启动命令指定使用它mvn clean package -DskipTests java -jar gym-server.jar --spring.profiles.activeprodapplication-prod.yml里需要注意的几个点server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/gym?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 你的数据库密码 servlet: multipart: max-file-size: 10MB max-request-size: 20MB mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: trueserverTimezoneAsia/Shanghai必须加否则数据库连接会报时区错误。map-underscore-to-camel-case必须开否则数据库下划线字段和Java驼峰属性对不上查询出来全是null。6.3 MySQL初始化和数据导入部署时数据库初始化的标准做法是在服务器MySQL中创建数据库导入项目提供的sql脚本创建一个专门的业务账号而不是直接用root远程连数据库。mysql -u root -p CREATE DATABASE gym DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; GRANT ALL PRIVILEGES ON gym.* TO gym_userlocalhost IDENTIFIED BY 强密码; FLUSH PRIVILEGES; exit mysql -u gym_user -p gym gym.sql数据库账号权限最小化这个习惯值得养成万一应用被攻击攻击者拿到的也只是单库权限影响范围可控。6.4 Nginx反向代理与前端发布前端构建产出一堆静态文件扔给Nginx托管就行。但有个核心配置前端页面的接口请求都走/api前缀Nginx要把/api转发到后端8080端口这样前端浏览器只和Nginx一个端口打交道不存在跨域问题。参考配置server { listen 80; server_name 你的域名或服务器IP; # 前端静态文件 root /opt/gym-web/dist; index index.html; # 解决Vue history路由刷新404 location / { try_files $uri $uri/ /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; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }注意location /api/里的proxy_pass末尾是否带路径带不带结果完全不同。代理到http://127.0.0.1:8080时客户端请求/api/venue/list会被完整转发给后端如果写成http://127.0.0.1:8080/则/api前缀会被替换成/同样能通但容易混淆。我的习惯是原来有什么路径就原样转发后端Controller统一用/api开头。6.5 上线后的基础验证清单部署完成后不要直接宣布大功告成按以下清单快速过一遍打开前端首页确认场馆卡片正常加载图片不裂。注册一个新账号确认能收到验证码或能正常注册。登录后预约一个时段然后换一个账号登录确认同样时段变灰不可约。退出登录手动访问需要登录的页面确认被路由守卫拦截。直接用Postman调管理端接口不带Token确认返回401带普通用户Token确认返回403。模拟提交一个体积较大的文件上传确认在配置的10MB限制下返回友好提示。这套清单十分钟就能跑完能提前发现至少80%的部署问题。7. 实测中踩过的坑你最好提前知道项目做完只是一半真正让人掉头发的是测试和上线阶段。这一节我把实测中踩过、以及帮别人排查过的高频坑都列出来每个坑都附上排查思路希望你能绕开。7.1 并发预约产生的重复记录唯一索引不是装饰有次压测时我用JMeter同时发50个请求预约同一个场馆同一个时段结果数据库里出现了多条记录。查了半天发现那套代码的表结构里压根没有唯一索引只有应用层先查再插。光靠应用层判断在高并发下根本不严谨两个请求可能同时通过检查再插入于是重复记录诞生了。修复方案就是我前面说的双保险数据库加唯一索引uk_venue_slot(venue_id, reserve_date, start_time)事务里对场馆行加锁。加了索引之后你再怎么并发压数据库只会保留一条记录后到的直接报DuplicateKeyException。排查这类问题的时候先看表结构再看事务隔离级别别上来就怀疑是代码逻辑写错了。7.2 跨域问题排查完整链路前后端分离开发时跨域问题几乎人人会碰到。现象是前端请求后端接口浏览器控制台报CORS policy错误。不是所有跨域都要配CORS我的排查链路是先确认前端用的代理配置。开发阶段用Vite代理Vite配置proxy把/api转发到后端这样可以绕过跨域生产阶段用Nginx反向代理同样可以避免跨域。如果确实需要跨域请求后端配置CorsConfig允许前端来源、允许Authorization请求头、允许GET/POST/PUT/DELETE方法。如果CORS配置了还是不生效检查拦截器是不是把OPTIONS预检请求拦截了。预检请求不做业务处理拦截器里第一行就需要放行OPTIONS。跨域报错有80%是第三步导致的先看后端日志有没有打印OPTIONS请求相关异常一抓一个准。7.3 日期时间格式与MySQL时区问题MySQL 8.0连接串不配serverTimezoneAsia/Shanghai启动直接报The server time zone value Öйú±ê׼ʱ¼ä is unrecognized。这串乱码看着吓人本质就是数据库和驱动之间的时区对不上手动指定即可。还有更隐蔽的问题Java实体里用LocalDateTime前端传过来的日期字符串格式稍微不对反序列化就报错。建议统一在application.yml里配置全局时间格式spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8或者更简单一点统一使用String类型接收日期时间参数入库前再转换。对预约系统来说reserve_date用LocalDate类型就够了因为用户选的永远是某一天不需要时分秒。7.4 Vue history路由打包后404前端路由用了history模式即URL里没有#号直接部署到服务器后刷新某个子页面会404。原因是Nginx找不到对应的真实文件路径比如用户访问/venue/3服务器去查找/venue/3这个文件当然找不到。前面Nginx配置里那句try_files $uri $uri/ /index.html就是专门解决这个问题的。它告诉Nginx这个路径没找到就回退到index.html然后交给Vue Router去匹配路由。凡是使用history模式的项目这句配置不能省。7.5 容易被忽略的小细节最后列几个容易让系统显得不专业的小问题密码不要明文存库用BCrypt加密Spring Security里的BCryptPasswordEncoder可以单独用不用引入全套Security。后端返回错误信息不要直接把异常堆栈抛给前端用全局异常处理器统一封装成Result。取消预约要校验时间开场前2小时禁止取消这个状态在前后端都要判断。数据库字段默认值要设置好比如status默认0create_time默认当前时间能少写很多重复代码。管理员删除场馆前要检查该场馆是否还有未完成的预约不能直接物理删除。按我个人的经验把这些细节打磨好整个项目的完成度会明显上一个档次。编码只是把功能实现把边界情况和异常流程处理好才是真正能上线的系统。这套体育馆预约系统虽然不算复杂但它覆盖了前后端分离、登录鉴权、事务并发、部署上线这一整条主线认真走一遍之后再接触更大规模的业务系统很多思路都能复用得上。
企业数字化 ERP 产品动态
相关推荐
HTTP/HTTPS稳跑指南:从状态码到连接池与超时重试策略 1. HTTP 请求完整链路:先把“跑起来”这件事拆明白1.1 一次请求里到底藏了哪些信息很多开发者写 HTTP 调用的时候,代码大概是这样的:拼个 URL、拿到响应、读 body、完事。这种写法在本地自测没问题,但是一旦上到生产环境ÿ… · 2026/9/23 2:26:02
ExcelVBA与WordVBA跨应用自动化实战指南 简介:本资源是面向Office自动化开发初学者与进阶用户的VBA核心概念精讲教程,聚焦Excel与Word双平台对象模型的统一理解与差异化实践。内容系统解析Application、Document/Workbook、Range、Selection等关键对象,深入讲解集合(Docu… · 2026/9/23 2:25:56
SEO优化四大支柱体系与实战指南 1. SEO优化方案的核心价值与行业现状在数字营销领域,SEO(搜索引擎优化)始终是企业获取自然流量的核心渠道。根据最新行业数据,搜索引擎结果页第一位的点击率高达27.6%,而第二页之后的点击量则呈现断崖式下跌。这种流量… · 2026/9/23 2:25:56
蜗轮梯形丝杆升降机原理与应用全解析 1. 蜗轮梯形丝杆升降机的基本构造手摇蜗轮梯形丝杆升降机主要由以下几个核心部件组成:蜗轮蜗杆传动机构:这是整个系统的动力转换核心,通常采用铸铁或钢制材料制造。蜗杆的螺旋角经过精密计算,与蜗轮的齿形完美匹配,确保… · 2026/9/23 3:58:07
IIS管理器实战:从服务引擎到配置入口,一文搞定网站部署与排错 前阵子帮朋友收拾一台 Windows Server 2019 的服务器,站点挂了,网站访问直接 503。他打开服务器桌面上的 IIS 管理器,一脸懵地问我:这个“文件夹树 中间一堆图标 右边操作栏”的窗口到底是干嘛的?我为什么在里边找不… · 2026/9/23 3:58:07
自适应与自组织系统构建实战:从理念到分布式落地 做出一套会自己调整、自己组织的东西,这事我琢磨了好几年。从最开始用规则引擎硬编码,到后来接触自适应控制、自组织网络,再到实际在分布式系统里跑通一套具备自愈和自调节能力的架构,整个过程踩了不少坑,也积累了很多… · 2026/9/23 3:58:07
测网速 联通原理详解 测网速联通避坑指南:3个致命误区与修复方案 官方文档里关于TCP连接建立和带宽测量的描述,往往长篇大论,堆砌着IETF的术语,让人看完还是不知道代码该怎么写。很多开发者在实现“测网速”功能时,盯着RFC文档里的状态机看半天,结果代码一跑,测… · 2026/9/23 3:58:01
钢材缺陷检测实战:1000张图YOLOv8训练与避坑指南 简介:本资源为YOLO谢韦尔钢材缺陷检测数据集,面向从事工业质检、目标检测算法学习与竞赛实践的学生、工程师及研究者,帮助解决钢材表面缺陷识别任务中数据获取难、标注格式不统一的问题。压缩包共2000个文件,约12.38MB,… · 2026/9/23 3:57:49
3招搞定手机怎么下载微信面试难题实战项目解析 3招搞定手机怎么下载微信面试难题实战项目解析 面试被问“手机怎么下载微信”背后的原理,90%的人答不上来。别笑,这看似弱智的问题,实则是考察你对移动应用分发机制、安全校验及网络协议理解的试金石。我带过不少校招新人,他们背了八股文,却连一个A… · 2026/9/23 0:00:03
你有新短消息请注意查收:3个新手避坑指南搞定消息系统选型 你有新短消息请注意查收:3个新手避坑指南搞定消息系统选型 面试被问“高并发下如何保证消息不丢失”,你张口就是“用Redis”,结果面试官追问“如果Redis宕机了怎么办”,你瞬间卡壳。这种场景太常见了,很多新手在背八股文时,只记住了技术名词… · 2026/9/23 0:00:29