资讯详情

资讯详情

SpringBoot+Vue数字乡村旅游景点预约平台实战解析

SpringBoot Vue 做数字乡村旅游景点预约平台这件事我前前后后完整地做过一版从需求梳理到上线部署都走了一遍。这个项目放到现在来看技术栈不算新但胜在覆盖面全——后端用 SpringBoot 提供 RESTful API前端用 Vue 全家桶做单页应用数据库设计、权限控制、预约状态流转、支付对接如果要做的话都能练到。更关键的是乡村旅游景点预约这个场景本身很有代表性它不像大型景区那样有成熟的票务系统很多乡村景点还在用Excel甚至纸质登记做一个轻量级的预约平台既解决实际问题又非常适合拿来当毕设、个人项目或者小团队接单的参考。这篇文章我把整个项目的设计思路、核心实现和踩坑记录都拆开讲一遍适合正在做类似选题的学生也适合想把前后端分离项目落地成产品的开发者。1. 项目整体设计思路与架构拆解1.1 为什么选 SpringBoot Vue 这套组合先说选型。市面上做Web应用的前后端方案很多但SpringBoot Vue几乎成了中小型管理系统的默认搭配。我选它不是因为“大家都在用”而是这套组合的实际开发效率确实高。后端用SpringBoot最核心的价值是自动配置和起步依赖。你不需要像传统的SSM项目那样手动去配置大量的XML文件引入一个spring-boot-starter-web就能把内嵌Tomcat、Spring MVC、JSON序列化这些基础能力全部带起来。这对快速开发一个预约平台来说非常重要——项目核心逻辑在预约流程和数据处理上而不是浪费在环境搭建上。另外SpringBoot的生态非常成熟后面要接MySQL、Redis、微信支付、对象存储都有现成的starter可以拉进来用。前端选Vue最大的优势是渐进式框架带来的开发灵活性。你可以只用它做页面的数据绑定和组件化也可以引入Vue Router做路由管理、Pinia/Vuex做状态管理把它当成一个完整的SPA框架来用。对于预约平台这种交互较多的场景——用户选日期、选时段、填信息、提交订单——Vue的响应式数据模型天然适合页面状态管理起来非常清晰。前后端分离还有一个隐性的好处部署灵活。前端构建出来的静态文件可以扔到Nginx上后端打包成Jar包单独跑两边可以独立扩容。对于乡村景点这种访问量有波峰波谷的场景节假日人多、平时人少这种架构可以根据实际情况弹性调整资源控制成本。1.2 系统功能模块划分我一开始做这个项目时踩了个坑——上来就写代码结果写到一半发现功能边界模糊各种逻辑互相纠缠。后来老老实实先把模块划分清楚实现起来才顺畅得多。数字乡村旅游景点预约平台按角色分核心是三端游客端前端预约、景点管理端审核和处理预约、系统管理端平台运维。游客端的核心功能是注册登录、浏览景点列表、查看景点详情图片、介绍、开放时间、可预约时段、提交预约申请、查看我的预约记录、取消预约。这里有一个容易被忽略但是很重要的功能——预约规则说明。乡村景点的预约规则往往比大型景区复杂比如“周末需提前1天预约”“单日限额200人”“雷雨天可能临时关闭”这些信息必须在前端清晰展示否则后期客服压力会很大。景点管理端的核心功能是维护景点基本信息、设置每日可预约人数上限、设置可预约时段比如上午8:00-12:00、下午13:00-17:00、查看该景点的预约列表、审核或核销预约。这里我建议把“审核”和“核销”区分开——审核是提前确认用户预约是否有效核销是用户到现场时凭二维码或预约码核验入场。对于免费开放的乡村景点审核这步可以简化甚至去掉但核销必须有。系统管理端的功能是管理所有注册用户、管理所有景点包括上下架、查看全平台的预约统计数据、处理投诉或异常预约。这一端对于平台运营方来说是刚需因为你需要知道哪些景点受欢迎、哪些时段预约最集中、哪些用户有异常行为。1.3 数据库设计思路与E-R模型数据库设计是这类系统最见功力的部分因为预约业务涉及状态流转和时间校验表结构设计不好后面写SQL会非常痛苦。我按照实际开发经验把核心表结构拆一下。用户表sys_user主键id、用户名、密码BCrypt加密存储、手机号、真实姓名预约需要实名、身份证号部分景区要求可做成选填、角色游客/景点管理员/系统管理员、创建时间。景点表scenic_spot主键id、景点名称、所在地区省市区、详细地址、景点介绍长文本、封面图片URL、开放时间如08:00-17:00、所属管理员id关联用户表、状态上架/下架、创建时间。预约配置表reservation_config这个表很容易被忽略但非常关键主键id、景点id、可预约日期、可预约时段如上午/下午、该时段最大预约人数、已预约人数。为什么要单独一张表因为每个景点的每日限额和时段设置都可能变化单独建表可以灵活配置而且查询某个日期是否可预约时直接查这张表效率更高。预约订单表reservation_order主键id、订单编号唯一业务编号不是主键、用户id、景点id、预约日期、预约时段、预约人数、联系人姓名、联系人手机号、备注、状态待审核/已确认/已取消/已完成/已过期、预约码核销时用、创建时间、更新时间。我还加了两张辅助表公告表notice发布景区临时关闭、预约规则变更等通知和操作日志表operation_log记录关键操作方便排查问题。日志表看着不起眼但在生产环境里出了问题它能帮你回溯整个链路。这里有一个设计要点必须说时间相关的字段一定要用合适的数据类型。预约日期用DATE预约时段我用的是VARCHAR存“上午/下午”或具体时间段字符串。之前有人用DATETIME存时段结果跨天逻辑写得非常痛苦。2. 核心细节解析与实操要点2.1 预约流程的状态机设计预约系统的核心不是CRUD而是状态流转。我把预约订单的状态设计成五种待审核、已确认、已取消、已完成、已过期。每种状态之间的转换有条件约束不是随便什么状态都能跳到什么状态。正常流程是游客提交预约 → 状态为“待审核”如果无需审核则直接“已确认”→ 游客到景区后管理员核销 → 状态为“已完成”。游客也可以在“待审核”或“已确认”状态下主动取消状态变为“已取消”。如果预约日期已经过了但状态仍是“待审核”或“已确认”定时任务把状态更新为“已过期”。这个状态机模型看起来简单但实现时有几个容易出错的点。第一取消预约的时限控制——有些景区规定预约当天不能取消这个规则需要在后端接口里做校验不能只在前端判断否则用户绕过前端直接调接口就能把当天的预约取消掉。第二状态变更的并发控制——用户和管理员同时操作同一订单时要用乐观锁在订单表加version字段或行级锁防止状态覆盖。第三状态变更的记录——每次状态变更都要写日志方便用户和管理员追溯。我在实现时用了一个简单的枚举类OrderStatus来管理状态并且在Service层写了一个changeStatus方法统一处理状态校验和日志记录。这样比在Controller里散落地写各种if判断要清晰得多。2.2 并发预约的超卖问题与数据库锁机制预约平台最核心的一个技术难点就是超卖——也就是多人同时预约同一个景点的同一个时段导致实际预约人数超过限额。这个问题我在做第一版的时候确实遇到过用Postman同时发10个预约请求结果10个都成功了但限额只有5个——因为每个请求都先查剩余名额再插入订单两个操作之间有时间窗口后面的请求读到的还是旧数据。解决这个问题有几种方案我按实际推荐程度排一下。最简单、最稳妥的是在数据库层面加约束。在reservation_config表里用UPDATE ... SET booked_count booked_count 1 WHERE id ? AND booked_count max_count这样的原子操作来扣减名额如果影响行数为0说明已经满了直接返回预约失败。这条SQL是原子性的数据库的锁机制会保证同一时刻只有一个事务能成功执行。我把这个方案放在第一位因为乡村景点的预约量通常不大QPS撑死几十数据库原子更新完全够用不需要引入Redis分布式锁把架构搞复杂。如果预约量真的很大比如黄金周的知名乡村景点可以引入Redis做库存预扣但要注意Redis和数据库的一致性问题。我建议的做法是Redis存剩余名额预约请求先扣减Redis库存扣减成功后再异步写数据库订单。如果数据库写入失败需要补偿回滚Redis库存。这套方案的复杂度比纯数据库方案高出一个量级对于中小项目未必划算。2.3 接口设计规范与权限控制预约平台的接口设计要遵循RESTful风格但也别死板该简化的就简化。我按业务模块梳理一下核心接口。用户模块POST /api/auth/register注册、POST /api/auth/login登录、GET /api/user/info获取当前用户信息、PUT /api/user/info修改个人信息。景点模块GET /api/spot/list景点列表支持分页和关键词搜索、GET /api/spot/{id}景点详情、GET /api/spot/{id}/slots?date2025-06-01查询指定日期可预约时段和剩余名额。预约模块POST /api/reservation提交预约、GET /api/reservation/my我的预约列表、GET /api/reservation/{orderNo}订单详情、PUT /api/reservation/{orderNo}/cancel取消预约、GET /api/reservation/{orderNo}/qrcode获取预约二维码。管理端接口POST /api/admin/spot新增景点、PUT /api/admin/spot/{id}修改景点、PUT /api/admin/spot/{id}/status上下架、GET /api/admin/reservation/list分页查询预约列表支持日期和状态筛选、PUT /api/admin/reservation/{id}/confirm确认预约、PUT /api/admin/reservation/{id}/checkin核销预约。权限控制这里有一个经验之谈不要在前端做鉴权前端只是控制显示真正的权限校验必须在后端。我用了Spring Security JWT的方案定义了三种角色ROLE_USER、ROLE_SPOT_ADMIN、ROLE_ADMIN。景点管理员只能操作自己管理的景点数据这个归属关系在接口层要校验不能只检查角色。举个例子景点管理员A试图核销景点B的预约订单后端必须通过订单查询出景点id再比对当前管理员的管辖景点id不一致直接返回403。JWT的设计也有讲究。我把token的过期时间设为2小时看实际使用场景游客逛景区流程没那么长同时实现了一个token刷新机制——前端在收到401时用refreshToken去换取新的accessToken避免用户玩着玩着突然被强制登录。但要注意refreshToken必须要做过期保护不能让一个refreshToken永远有效。2.4 前端Vue架构与路由设计Vue前端这块我用的是Vue 3 Vite Vue Router 4 Pinia的组合。选Vite是看中了它的冷启动速度——比Webpack不知道快到哪里去了开发体验提升非常明显。项目结构上我按views页面、components公共组件、api接口请求封装、stores状态管理、router路由配置来划分职责清晰后期维护不头疼。路由设计上我用了路由守卫做登录校验和角色权限控制。router.beforeEach里做三件事判断访问的页面是否需要登录、判断用户角色是否有权限访问、根据登录状态自动跳转登录页或首页。这里有一个非常容易踩的坑——路由守卫里不要做太重的异步操作比如每次都去请求用户信息否则页面切换会有明显的卡顿。正确做法是登录后把用户信息和角色存到Pinia里路由守卫只读内存数据如果刷新页面导致内存数据丢失再通过/api/user/info接口恢复。预约提交页面是前端交互最复杂的部分。用户需要选择景点、选择日期、选择时段、填写人数和联系人信息。这里我用了一个联动逻辑日期变化后重新请求该日期的可约时段和剩余名额时段存在嵌套关系某些景区如果上午段满了还会推荐下午段人数要校验不能超过该时段剩余名额。这个逻辑涉及两个异步请求要用async/await串行处理处理好loading态避免用户快速切换导致数据显示错乱。3. 实操过程与核心环节实现3.1 SpringBoot后端核心代码实现先说项目初始化。我使用的是Spring Boot 2.7.x版本JDK 1.8。为什么不用3.x因为3.x要求JDK 17以上而且很多第三方starter的兼容性还没跟上对于做项目来说没必要冒险追新。依赖这块核心引入下面几个dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency dependency groupIdcn.hutool/groupId artifactIdhutool-all/artifactId version5.8.22/version /dependency我用MyBatis-Plus做持久层框架主要是看中它的代码生成能力和条件构造器不用写太多XML。Hutool是个工具库生成预约码、日期处理、加密解密都能用到省不少事。提交预约的核心Service方法我直接贴关键代码并解释每一段的作用。这是整个系统最重要的方法没有之一Service public class ReservationServiceImpl implements ReservationService { Resource private ReservationConfigMapper reservationConfigMapper; Resource private ReservationOrderMapper reservationOrderMapper; Override Transactional(rollbackFor Exception.class) public ReservationSubmitVO submitReservation(ReservationSubmitDTO dto) { // 1. 校验景点是否存在且上架 ScenicSpot spot spotMapper.selectById(dto.getSpotId()); if (spot null || spot.getStatus() ! 1) { throw new BizException(景点不存在或已下架); } // 2. 查询预约配置锁定对应日期的时段记录 // 这里用了数据库行锁防止并发扣减名额 ReservationConfig config reservationConfigMapper .selectConfigForUpdate(dto.getSpotId(), dto.getReservationDate(), dto.getTimeSlot()); if (config null) { throw new BizException(该日期时段不可预约); } // 3. 校验剩余名额 if (config.getBookedCount() dto.getVisitorCount() config.getMaxCount()) { throw new BizException(预约名额不足剩余 (config.getMaxCount() - config.getBookedCount()) 个名额); } // 4. 原子扣减名额这一步和上面的行锁是双重保险 int updated reservationConfigMapper.deductBookedCount( config.getId(), dto.getVisitorCount(), config.getMaxCount()); if (updated 0) { throw new BizException(预约名额已满); } // 5. 生成预约订单状态为待审核或已确认取决于景点配置 ReservationOrder order new ReservationOrder(); order.setOrderNo(generateOrderNo()); order.setUserId(dto.getUserId()); order.setSpotId(dto.getSpotId()); order.setReservationDate(dto.getReservationDate()); order.setTimeSlot(dto.getTimeSlot()); order.setVisitorCount(dto.getVisitorCount()); order.setContactName(dto.getContactName()); order.setContactPhone(dto.getContactPhone()); order.setStatus(spot.getNeedReview() 1 ? PENDING : CONFIRMED); order.setReservationCode(generateReservationCode()); reservationOrderMapper.insert(order); // 6. 返回预约结果包含预约码 return new ReservationSubmitVO(order.getOrderNo(), order.getReservationCode(), order.getStatus()); } }其中第4步deductBookedCount对应的SQL是关键它是一个原子操作UPDATE reservation_config SET booked_count booked_count #{count} WHERE id #{id} AND booked_count #{count} max_count这个方法我之前专门用JMeter做了并发测试设置最大并发50个线程同时预约同一个时段名额限制10个最终只有10个请求成功其余全部返回“预约名额已满”。数据库行锁 原子更新双保险下数据一致性完全没问题。3.2 预约日期与时段校验的细节处理预约系统里还有一个很容易被忽略的细节日期的可预约性校验。不是随便选一天就能预约的需要满足几个条件日期不能是过去的日期、日期不能超过可预约的最远日期比如只能提前7天预约、该日期必须是景区开放日排除周一闭园等、该日期的某个时段还剩余名额。这些校验规则如果全写在Controller里会非常乱我建议抽一个ReservationValidator组件统一处理Component public class ReservationValidator { public void validateReservationDate(LocalDate date, ScenicSpot spot) { // 检查是否为过去日期 if (date.isBefore(LocalDate.now())) { throw new BizException(不能预约过去的日期); } // 检查是否超过可预约天数从系统配置读取 int maxAdvanceDays systemConfigService.getIntValue(reservation.max_advance_days, 7); if (date.isAfter(LocalDate.now().plusDays(maxAdvanceDays))) { throw new BizException(最多只能提前 maxAdvanceDays 天预约); } // 检查是否为闭园日比如每周一闭园 Integer closedDay spot.getClosedDay(); // 1-7 表示周一到周日 if (closedDay ! null date.getDayOfWeek().getValue() closedDay) { throw new BizException(该日期为景区闭园日请选择其他日期); } } }这里有一个坑来自时区问题。如果服务器时区没设置好LocalDate.now()拿到的时间可能和用户本地时间不一致。比如你的服务器部署在新加坡东八区但标准时间不一样或者数据库连接的时区参数没配好就有可能出现“今天中午预约了明天的票但系统认为明天已经过了”。我的解决方式是在启动类里统一设置时区PostConstruct public void init() { TimeZone.setDefault(TimeZone.getTimeZone(Asia/Shanghai)); }同时在数据库连接串上加上serverTimezoneAsia/Shanghai。双保险确保时间计算不会乱。3.3 Vue前端预约页面的交互设计前端的预约页面是整个用户体验的核心我花了很大心思在这个页面上。页面整体分为三个部分景点信息区、日期时段选择区、预约信息填写区。这三个区域从上到下依次排列引导用户按顺序操作。日期选择我用的是日历组件但做了一些定制过去的日期置灰不可选超过可预约天数的日期置灰不可选已约满的日期显示“已满”标签。这个效果需要动态计算每天的剩余状态前端通过一个接口批量拉取7天内的可预约状态再来渲染日历。// 获取近7天各日期的预约状态 async function loadWeekStatus(spotId) { const startDate formatDate(new Date()); const endDate formatDate(new Date(Date.now() 7 * 24 * 60 * 60 * 1000)); const res await getSpotWeekStatus({ spotId, startDate, endDate }); weekStatus.value res.data; }时段选择这部分我用卡片式布局展示“上午 08:00-12:00”和“下午 13:00-17:00”两个选项每个卡片上显示剩余名额。如果某个时段已满卡片置灰并显示“已约满”。这里有个小细节时段选择后个人信息填写区才变得可编辑使用el-form的disabled绑定避免用户先填完信息再发现时段已满需要重填体验很糟糕。提交预约时前端要做两轮校验。第一轮是Element Plus表单校验检查手机号格式、姓名不能为空、预约人数不能为0等基础规则。第二轮是后端返回的业务校验结果比如“该时段剩余名额不足”或“该日期不可预约”这些错误信息需要直接展示在表单上方用红色文字提示。我踩过一个坑一开始把后端错误信息用alert弹窗展示用户在手机端操作时弹窗非常烦人后来改成内联错误提示用户体验明显改善。3.4 管理端核销功能的实现要点管理端核销是景区工作人员每天都要用的功能设计得好能大幅提升工作效率。我做了两种核销方式一种是电脑端通过订单列表操作另一种是通过预约码扫码核销。预约码我用的是12位随机数字字母组合生成时保证唯一性。核销接口的逻辑是PutMapping(/api/admin/reservation/checkin) public ResultVoid checkin(RequestBody CheckinDTO dto) { LambdaQueryWrapperReservationOrder wrapper new LambdaQueryWrapper(); wrapper.eq(ReservationOrder::getReservationCode, dto.getReservationCode()); ReservationOrder order reservationOrderMapper.selectOne(wrapper); if (order null) { throw new BizException(预约码不存在); } // 校验该订单是否属于当前管理员管辖的景点 ScenicSpot spot spotMapper.selectById(order.getSpotId()); if (!spot.getAdminId().equals(currentAdminId())) { throw new BizException(无权核销该景点的预约); } // 校验订单状态 if (!CONFIRMED.equals(order.getStatus())) { throw new BizException(订单状态异常当前状态 order.getStatus()); } // 校验是否在有效核销时间内 if (!order.getReservationDate().equals(LocalDate.now())) { throw new BizException(只能核销当日预约该预约日期为 order.getReservationDate()); } order.setStatus(COMPLETED); order.setCheckinTime(LocalDateTime.now()); reservationOrderMapper.updateById(order); return Result.success(); }这里我加了一个“只能核销当日预约”的限制防止管理员提前或者过后核销。有些景区的实际需求可能不同比如允许核销未来日期的订单但标记为“提前核销”这个可以根据业务灵活调整。还有一个开发时需要注意的细节核销操作是不可逆的所以核销成功前一定要有二次确认弹窗。我在前端做了ElMessageBox.confirm让管理员确认预约人姓名和人数后再执行核销操作避免误操作。4. 常见问题与排查技巧实录4.1 跨域问题前后端联调第一道坎前后端分离项目联调时跨域几乎是100%会碰到的问题。症状很典型前端请求后端接口浏览器控制台报Access-Control-Allow-Origin相关的错误。我推荐用后端统一配置的方式解决SpringBoot里写一个CORS配置类Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }注意这里用的是allowedOriginPatterns而不是allowedOrigins因为后者在allowCredentials(true)时不能使用通配符allowedOriginPatterns就没有这个限制。这个细节坑了不少人我一开始也被Spring Security拦了一波——在SpringBoot和Spring Security同时存在的项目里CORS配置如果只写在WebMvcConfigurer中Spring Security的过滤器链会先拦截请求导致跨域失效需要在SecurityConfig里也放行CORSOverride protected void configure(HttpSecurity http) throws Exception { http.cors().and() .csrf().disable() // ... 其他配置 }4.2 并发场景下预约名额超卖问题实录前面提到过超卖问题的解决方案这里再分享一个实际排查的过程。第一版代码里扣减名额的逻辑是这样写的// 错误的写法 ReservationConfig config reservationConfigMapper.selectById(configId); if (config.getBookedCount() count config.getMaxCount()) { throw new BizException(名额不足); } config.setBookedCount(config.getBookedCount() count); reservationConfigMapper.updateById(config);这段代码在单线程下跑没问题但在并发场景下就是典型的“检查后再执行”竞态条件。排查方法我建议用JMeter压测线程组设置50个线程并发访问提交预约接口观察数据库里booked_count最终的值。我在本地测试时发现50个并发请求后booked_count可能只增加了30多因为后面插入订单失败回滚了但更严重的是可能超过max_count——因为多个请求同时读到了同一个旧值。定位到问题后我把扣减逻辑改成了前面贴的UPDATE ... WHERE booked_count count max_count原子操作同时加上了SELECT ... FOR UPDATE行锁双重保障。修改后再跑压测50个并发请求最终成功预约10个最大值数据库数据完全一致。这个案例建议所有做预约系统的朋友都自己复现一遍理解会非常深刻。4.3 Vue路由刷新404与部署配置问题前端路由用History模式在本地开发时一切正常但打包部署到Nginx后刷新页面就404。这个问题的原因是Nginx的location配置只匹配到了根路径而SPA的路由在刷新时请求了真实路径比如/reservation/myNginx找不到这个文件就返回404。解决方法是在Nginx配置里加一个try_files指令server { listen 80; server_name your-domain.com; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; } # 静态资源缓存 location /assets/ { expires 30d; } # 后端接口反向代理 location /api/ { proxy_pass http://localhost: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; } }try_files $uri $uri/ /index.html的意思是先尝试访问实际文件找不到文件再尝试访问目录还找不到就返回index.html由前端路由接管。还有一个小问题如果后端部署在另一台服务器前端通过Nginx代理访问后端接口时注意proxy_pass后面的URL要不要带/。带/表示替换路径前缀不带表示保留原路径。我这边后端接口统一以/api开头所以proxy_pass http://localhost:8080;不带/这样请求/api/reservation会被完整转发给后端。4.4 数据库连接池与性能优化建议预约平台在节假日会有明显的流量高峰平时可能没什么人访问。这种情况下数据库连接池的配置非常重要。我使用的是HikariCPSpringBoot 2.x默认核心配置如下spring: datasource: hikari: minimum-idle: 5 maximum-pool-size: 20 idle-timeout: 300000 connection-timeout: 20000 max-lifetime: 1800000maximum-pool-size不要设置得太大因为数据库的连接数是有限的连接池设置太大会拖垮数据库。对于乡村景点这个体量20个连接完全够用。如果遇到高峰期连接不够优先考虑优化SQL和加索引而不是无脑加大连接池。索引优化方面预约订单表我建了三个索引idx_user_id查询我的预约、idx_spot_id_date查询某景点某日期的预约列表、idx_reservation_code扫码核销时按预约码精确查询。特别注意idx_reservation_code要是唯一索引因为预约码必唯一唯一索引还能提供额外的一层数据完整性保护。4.5 安全防护基础配置Web安全这个话题范围很广预约平台至少要关注下面几个点。密码加密存储是必须的我用的BCrypt算法每次加密结果都不一样但校验时能通过算法比对。不要用MD5——MD5是可逆的有彩虹表即使是加了盐的MD5也不如BCrypt安全。Spring Security自带的BCryptPasswordEncoder直接就能用。接口防刷方面我在预约提交接口上用了简单的限流基于IP的滑动窗口限流每分钟同一IP最多提交5次预约。为什么限制这么宽松因为一个用户可能帮一家人同时预约多个景点限制太严格会误伤正常用户。如果想精细化控制可以改成基于用户IDIP的组合限流或者接入Sentinel这类限流组件。SQL注入防护主要靠MyBatis-Plus的参数绑定机制这个框架默认使用预编译语句基本杜绝了SQL注入。但要注意不要使用${}拼接SQL要用#{}。另外前端传过来的参数一定要做白名单校验比如排序字段只允许asc和desc不允许用户自定义传字段名。XSS防护方面前端Vue的模板语法默认会转义所有内容这个已经天然防护了大部分反射型XSS。后端建议加上一个全局的XSS过滤器对用户提交内容的特殊字符做转义。我在生产环境遇到过有人通过备注字段提交了一段脚本虽然Vue渲染时被转义了但存储的原始数据是脏的后面做数据导出时会出问题所以后端过滤不可省。5. 项目落地过程中的经验总结5.1 版本选择与兼容性心得版本选型这个问题我在项目开始前就反复确认过。SpringBoot 2.7.x JDK 1.8是当前最稳的组合能跑在便宜的1核2G云服务器上部署成本低运维简单。如果追求新特性SpringBoot 3.x JDK 17也成熟了但部分旧版依赖可能要升级比如javax.servlet改成jakarta.servletMyBatis-Plus也要用适配SpringBoot 3的版本迁移成本不是零。如果做毕设或者企业内部小工具有几点建议。第一不要过度设计分布式事务、消息队列这些不是乡村景点预约平台需要的东西引入了反而是负担。第二尽量使用国内使用面广的框架版本遇到问题搜索引擎能搜到答案。第三前端Node版本要注意Vue 3 Vite 5要求Node 18如果服务器上装的是Node 16本地开发环境也要保持一致或者用nvm切换避免本地能跑服务器跑不了的情况。5.2 后续功能扩展空间这个系统的架构让我觉得特别顺的地方在于它留了好的扩展点。如果想在这个平台基础上继续做至少有三个方向可以深入。方向一是支付对接。如果景点是收费的预约时就要完成支付。微信支付Native支付、支付宝当面付都有现成的SDK前端生成二维码用户扫码支付。需要注意的是支付回调的通知处理要幂等处理好“支付成功但订单状态更新失败”的边界情况。方向二是增加实时通信。比如景点发布临时闭园通知后通过WebSocket或者短信网关给已预约用户推送消息。这个功能对于乡村景区特别实用——山区天气多变突然下雨关闭景点是常事临时通知能减少大量客诉。方向三是大数据分析。通过预约数据可以分析热门景点的访问趋势、客流高峰时段、用户来源地分布帮助景区做运营决策。开发一个数据看板页面用ECharts画折线图、柱状图、地图热力图前端展示很直观。5.3 部署架构建议最后聊一下部署完整的部署链路是前后端分离项目落地最重要的一环。我实际的部署方案是前端打包成静态文件交给Nginx托管后端打成Jar包用systemd进程守护运行MySQL单独跑在云数据库上或者同一台服务器上用Docker跑MySQL。# 后端启动命令 java -jar reservation-platform.jar --spring.profiles.activeprod # systemd 服务文件 /etc/systemd/system/reservation.service [Unit] DescriptionReservation Platform Afternetwork.target [Service] Userwww ExecStart/usr/bin/java -jar /var/www/reservation-platform.jar Restartalways RestartSec15 [Install] WantedBymulti-user.target数据库备份我建议写一个定时脚本每天凌晨用mysqldump做一次全量备份保留最近7天的备份文件。预约数据虽然不涉及资金流水那么敏感但丢了以后再恢复非常麻烦而且属于用户数据不能丢。HTTPS现在已经是标配了用免费的Lets Encrypt证书就可以Nginx配置SSL证书后自动跳转HTTPS。这个对用户信任度提升非常明显——浏览器地址栏的“不安全”标识很影响游客的心理感受。整个项目做下来我最大的感受是技术选型不需要花哨把核心业务流程的每个细节都打磨好才是这个项目真正有价值的地方。预约平台看着简单但并发控制、状态流转、时间校验这些看似基础的点每一处都能看出开发者的功力。希望这篇实战记录能给正在做类似项目的人一些参考少踩一些我踩过的坑。如果大家在实现过程中遇到具体问题尤其是数据库并发和前端交互这两块欢迎多交流我在这些细节上积累了比较多踩坑经验。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →