尧图精选

Spring Boot民宿系统源码解析:事务边界、MyBatis映射与Vue权限拦截

🕒 发布时间:2026/9/16 14:38:27 📁 来源:尧图网络
简介这是一套基于 JavaSpringBootVue 前后端分离架构的民宿预订管理系统完整源码适合作为毕业设计、课程设计或团队协作项目参考。资源共 381 个文件约 10.17MB包含 79 个 Java 后端源文件、38 个 Vue 组件、24 个 TypeScript 与 20 个 JavaScript 前端文件以及 XML 配置、SQL 脚本、字体文件和说明文档其中 125 个 JPEG 与 19 个 PNG 用于界面展示和民宿图片39 个 SVG 用于图标资源整体按 web 前端与 server 后端分目录组织结构清晰。系统覆盖民宿信息管理、用户认证授权、订单处理、支付接口对接等核心模块既能展示 SpringBoot 的服务端分层设计也能体现 Vue 组件化与 TypeScript 类型约束带来的可维护性。已有 475 人学习浏览压缩包内附带代码说明.txt、readme.txt、表结构文档和 doc 文档可快速理解项目结构、启动方式与数据表设计是一份便于对照学习和二次开发的全栈实战案例。1. 拆开 382 个文件的民宿预订系统之前先看这几个 Java 类拿到这套基于 Java Springboot Vue 的民宿预订管理系统源码别急着启动先读一遍 server 目录下 79 个 Java 文件的分布。一个 Springboot 项目如果控制器和业务逻辑混在一起、事务注解乱加后续改需求就是灾难而这里 ThingController、AccessInterceptor、ThingServiceImpl 三个类的职责划分已经能看出它对 Springboot 框架的使用方式。无论你拿它做毕业设计、课程设计还是当前后端分离工程的教学案例先把这几个类摸清楚后面才走得顺后面排查问题时也少走弯路。2. Springboot 后端ThingController、AccessInterceptor 与 MyBatis 持久化链路后端代码集中在 server 目录src/main/java 下的包结构按 controller、service、mapper、entity、config 分层。没有把业务逻辑堆进控制器也没有让 Service 直接拼接 SQL 操作数据库而是由 Mapper 接口负责数据访问这说明项目遵循 Springboot 的标准分层习惯。自动装配解决了大部分配置工作你只需要关注业务类的划分、事务边界和请求链路上的拦截逻辑。包路径职责代表类controller接收 HTTP 请求、参数绑定、返回统一 ResultThingController、OverViewControllerservice / service.impl业务逻辑编排、事务控制ThingServiceImplmapperMyBatis 数据访问接口ThingMapper、OrderMapperentity与数据库表对应的实体House、Order、Userconfig / interceptor拦截器注册、Web 配置AccessInterceptor、WebConfig接手这套源码时你会遇到一个命名细节核心控制类叫 ThingController实体却是 House服务层叫 ThingServiceImpl几个名字没有对齐。这种问题在老项目和课程设计里很常见理解它比抱怨它更有价值——你在重命名时能体会到 MyBatis 的 resultMap 映射、前端 API 路径、路由菜单之间其实是隐性关联的。2.1 ThingControllerREST 接口定义与参数校验先从控制器切入。ThingController 是整个民宿模块的对外入口前后端分离项目里控制器只做三件事接参、调服务、包返回。RestController RequestMapping(/api/house) public class ThingController { private final ThingServiceImpl thingService; public ThingController(ThingServiceImpl thingService) { this.thingService thingService; } GetMapping(/list) public Result list(RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer size, String keyword) { // keyword 可空service 层要处理模糊查询的 null 判断 return Result.ok(thingService.pageQuery(page, size, keyword)); } GetMapping(/detail/{id}) public Result detail(PathVariable Long id) { return Result.ok(thingService.getHouseDetail(id)); } }这个控制器写法里有三个细节。第一构造函数注入 ThingServiceImpl 而不是字段直接 Autowired便于单元测试时替换 mock 对象第二list 接口的 keyword 参数允许缺省Spring 把它解析为 nullService 层必须处理这个空值否则 MyBatis 拼接 LIKE 条件时会直接报错第三Result.ok 统一包装了响应结构前端 Axios 拦截器拿到 { code, data, msg } 后只看 code 就能判断业务是否成功。如果这是 java 面试场景大概率会被追问为什么 Controller 不能写业务逻辑因为 Spring 的 Transactional 依赖 AOP 在运行时生成容器对象来拦截 service 方法Controller 内部的自调用不会触发这层 AOP 拦截事务就会静默失效数据写到一半不报错也不回滚。这是 Springboot 框架里最典型的事务边界问题也是 springboot 面试题里反复出现的考点。2.2 AccessInterceptor请求鉴权与登录态校验民宿系统涉及下单和支付这些接口不能裸奔。AccessInterceptor 实现的是 Spring MVC 的 HandlerInterceptor核心逻辑在 preHandle 里完成 token 校验。Component public class AccessInterceptor implements HandlerInterceptor { private final TokenService tokenService; public AccessInterceptor(TokenService tokenService) { this.tokenService tokenService; } Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token null || !tokenService.verify(token)) { response.setStatus(401); return false; } // 把当前用户放进 ThreadLocalservice 层可以直接取 UserContext.set(tokenService.getUserId(token)); return true; } }写完拦截器必须在配置类里注册否则它永远不生效。常见做法是实现 WebMvcConfigurer在 addInterceptors 里指定拦截和放行路径。放行逻辑是这里的难点注册、登录、民宿列表、民宿详情属于公开接口不能拦下单、个人中心、后台管理必须拦。Configuration public class WebConfig implements WebMvcConfigurer { private final AccessInterceptor accessInterceptor; public WebConfig(AccessInterceptor accessInterceptor) { this.accessInterceptor accessInterceptor; } Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(accessInterceptor) .addPathPatterns(/api/**) .excludePathPatterns(/api/auth/login, /api/auth/register, /api/house/list, /api/house/detail/**); } }有一个容易忽略的坑拦截器校验后会把 userId 放进 UserContext而 UserContext 通常用 ThreadLocal 实现如果不重写 afterCompletion 清除Tomcat 线程池复用时下一个请求会继承上一个用户上下文产生越权。提示只要用了 ThreadLocal就必须在请求结束时清理这是并发场景的基本要求也是这个源码里最容易出安全问题的位置。2.3 ThingServiceImpl业务编排与 MyBatis 持久化Service 层的核心是事务边界和业务校验。ThingServiceImpl 里关于下单的逻辑等价于一个完整业务入口它要在一个事务里完成检查房源、创建订单、扣减库存三个动作Service public class ThingServiceImpl implements ThingService { private final ThingMapper thingMapper; private final OrderMapper orderMapper; public ThingServiceImpl(ThingMapper thingMapper, OrderMapper orderMapper) { this.thingMapper thingMapper; this.orderMapper orderMapper; } Transactional(rollbackFor Exception.class) Override public Long createOrder(OrderCreateDTO dto) { House house thingMapper.selectById(dto.getHouseId()); if (house null || house.getStatus() ! 1) { throw new BusinessException(房源不存在或已下架); } if (house.getStock() 1) { throw new BusinessException(库存不足); } Order order new Order(); order.setHouseId(house.getId()); order.setUserId(dto.getUserId()); order.setAmount(house.getPrice()); order.setStatus(0); // 0 待支付 orderMapper.insert(order); return order.getId(); } }这里最值得讲的是rollbackFor Exception.class。Spring 默认只回滚 RuntimeException如果你抛的是 checked exception事务不会回滚这会导致房源已扣减但订单没写入之类的数据不一致。MyBatis 的影子体现在 ThingMapper 上。XML 配置里一般用 resultMap 做表字段和实体属性的映射动态查询用 where、if 标签拼条件。如果你读过 mybatis 源码会记得mapUnderscoreToCamelCase开启后才能把 create_time 自动映射为 createTime否则查询结果里这一列永远是 null。3. Vue 3 TypeScript 前端38 个组件拆解、路由守卫与 Axios 拦截前端代码在 web 目录38 个 Vue 组件加 24 个 TypeScript 文件的组合说明这不是一个简单套壳项目而是按可维护性组织过的 Vite Vue 3 工程。组件按页面、公共、布局三种角色拆分src/views、src/components、src/router、src/api 四个目录基本定义了前端的模块边界。3.1 从 38 个 Vue 组件反推页面架构打开 src/views 先扫一遍列表页和详情页通常占一半以上。民宿预订的用户路径是“浏览房源 → 查看详情 → 选日期下单 → 支付 → 管理订单”每个步骤对应一个或几个组件。组件类别代表组件职责页面组件HouseList.vue、HouseDetail.vue、OrderConfirm.vue路由直接渲染的顶级页面公共组件HouseCard.vue、DateRangePicker.vue、StarRating.vue跨页面复用的展示与交互单元布局组件MainLayout.vue、AdminLayout.vue导航、侧边栏、内容区布局HouseList.vue 这类列表页是典型的“搜索条件 卡片列表 分页”结构。DateRangePicker 选择了起止日期后query 参数会带着 startDate 和 endDate 传给后端。常见问题是很多课程设计只做了日期展示没有校验日期区间内房源是否被占用于是同一时间段能重复下单。这种校验应该落在后端 service 层前端只是减少无效请求的手段。3.2 Vue Router 路由守卫登录才能下单vue 路由负责前端权限控制的前半段。路由表里用 meta 标记哪些页面需要登录组件按路由懒加载拆分// src/router/index.ts import { createRouter, createWebHistory } from vue-router const router createRouter({ history: createWebHistory(), routes: [ { path: /, component: () import(/views/HouseList.vue) }, { path: /house/:id, component: () import(/views/HouseDetail.vue) }, { path: /login, component: () import(/views/Login.vue) }, { path: /order/confirm, component: () import(/views/OrderConfirm.vue), meta: { requiresAuth: true } }, { path: /admin, component: () import(/views/admin/AdminDashboard.vue), meta: { requiresAuth: true, role: admin } } ] }) router.beforeEach((to) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { return /login } if (to.meta.role to.meta.role ! localStorage.getItem(role)) { return / } })这里的import()是函数式导入首屏只加载当前页面而不是把 38 个组件全部打包进 bundle。Vue 项目优化首屏性能最直接的手段就是拆路由和拆组件这一点在 vue 面试题里也经常出现。路由守卫做了两件事未登录用户被拦截回登录页非管理员不能进入后台看板。3.3 Axios 请求拦截Token 注入与 401 跳转前端和后端的配合重点在 axios 封装上。TypeScript 的引入让请求方法的入参和返回值具备类型约束不再靠记忆确认字段// src/api/request.ts import axios from axios import router from /router const request axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器每个请求自动带上 token request.interceptors.request.use((config) { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) // 响应拦截器401 时清理身份信息并跳转登录页 request.interceptors.response.use( (resp) resp.data, (error) { if (error.response?.status 401) { localStorage.removeItem(token) router.push(/login) } return Promise.reject(error) } ) export default request这个封装有两个关键点。请求拦截器从 localStorage 读取 token统一写到 Authorization 头后端 AccessInterceptor 也从同一个请求头解析前后端约定必须一致响应拦截器直接把 resp.data 返回业务代码就不用到处写response.data.data。调用后端接口时模块化写法如下export function getHouseDetail(id: number) { return request.get(/house/detail/${id}) }baseURL: /api配合 Vite dev server 的转发配置使用/api 开头的请求被转发到 Springboot避免开发环境出现跨域问题。这里有个常见误区如果转发没生效浏览器 Network 里会看到请求地址仍是 localhost:5173/api而后端日志没有对应记录这时候要核对 vite.config.ts 里的 target 端口。4. 民宿预订系统的核心流程房源状态、订单状态机与后台聚合统计把三块代码串起来看才能真正理解 79 个 Java 文件在做什么。预订系统最核心的不是单表 CRUD而是房源、订单、支付三个对象之间的状态变化。下面从房源上下架开始推到订单状态机最后看后台统计看板怎么把数据聚合起来。4.1 房源上下架与库存管理民宿表 t_house 的字段通常包括 id、name、location、price、stock、status、images、create_time。上下架的本质就是状态字段的一次 UPDATEUPDATE t_house SET status #{status} WHERE id #{id}只改状态是不够的。下架前需要检查该房源是否存在未完成订单。如果一栋民宿已经有人支付了明天的房间却直接下架订单就悬空了。所以严谨的做法是先查订单表里 status IN (0,1) 的记录存在就拒绝下架并返回提示。这类业务校验写在 service 层属于 MyBatis 数据访问之上的一层保护时机要选在事务开启之后。4.2 订单状态机从待支付到已入住订单表是系统的主动脉。状态字段用 int 表示可读性交给枚举或常量类下面是典型的状态转移表状态含义触发动作可转向状态0待支付用户下单1 已支付 / 4 已取消1已支付支付成功2 已确认 / 4 已取消2已确认房东确认3 已入住3已入住用户入住完成4已取消用户取消或超时终态状态机用 if 判断也能写但路径一多就容易漏。把所有合法转移关系收进 Map调用时查一次即可public class OrderStateMachine { // 键当前状态值允许迁往的状态集合 private static final MapInteger, SetInteger TRANSITIONS Map.of( 0, Set.of(1, 4), 1, Set.of(2, 4), 2, Set.of(3), 3, Set.of() ); public boolean canTransit(int from, int to) { return TRANSITIONS.getOrDefault(from, Set.of()).contains(to); } }这套源码里的支付是演示性质的真实项目要接入微信或支付宝 SDK还要处理回调验签。课程设计里模拟支付即可但模拟支付也必须经过状态机校验否则前端直接改请求参数把待支付改成已入住订单系统就失去了约束力。4.3 OverViewController后台看板的数据聚合OverViewController 对应管理后台的数据看板接口路径一般是 /api/admin/overview。需要聚合的数据包括订单总数、今日订单量、累计营业额、房源总数、入住中订单数。RestController RequestMapping(/api/admin) public class OverViewController { private final OrderMapper orderMapper; private final ThingMapper thingMapper; GetMapping(/overview) public Result overview() { MapString, Object data new HashMap(); data.put(totalOrders, orderMapper.countAll()); data.put(todayOrders, orderMapper.countToday()); data.put(totalIncome, orderMapper.sumPaidAmount()); data.put(totalHouses, thingMapper.countAll()); return Result.ok(data); } }聚合统计有两种实现方式一条多表 GROUP BY 查询或者多个 count 查询组合。对这个体量的后台看板多个 count 更直观也方便单独排查某个指标异常。但要注意统计计算务必放在 SQL 里而不是 Java 里把整表查出再循环求和订单数据量过万后接口响应时间会明显上升。要在这个项目上扩展优惠券或积分功能只需在 OrderCreateDTO 里加抵扣字段再在创建订单的 service 方法里插入折扣计算状态机和订单表结构都不需要动这就是分层边界清晰带来的扩展性。5. 把源码跑起来环境配置、数据库初始化和前后端联调排错5.1 环境准备与依赖安装先确认本机工具链JDK 1.8、Node 16 以上、MySQL 5.7 及以上。命令行分别执行java -version、node -v、mysql --version版本对不上就先处理环境。Java 环境变量配置不检查的话IDE 启动 Springboot 时经常报 unrecognized option 这类错误。5.2 修改后端数据库配置打开 server/src/main/resources/application.yml按本机数据库信息修改 url、username、password。如果资源带了表结构.docx就按文档里的表结构建库建表没有就找 sql 脚本导入。spring: datasource: url: jdbc:mysql://localhost:3306/homestay?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 你的数据库密码 mybatis: mapper-locations: classpath:mapper/*.xmlurl 里加上serverTimezoneAsia/Shanghai可以避免 MySQL 8 时区行为导致的 8 小时时间偏移这在订单统计和入住日期计算上很容易查半天查不出原因。5.3 前端构建与转发配置web 目录下执行npm install npm run dev如果 npm install 卡在依赖下载换 registry 源再试。Vite 的转发写在 vite.config.ts 的 server 字段里export default defineConfig({ server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })5.4 联调排错顺序联调按固定顺序来先看后端控制台再看浏览器 Network。后端报 500优先查 Mapper SQL 和表名是否匹配前端报 401检查请求头 Authorization 是否带上 token前端报 404先分清是路由 404 还是接口 404接口 404 多半是转发目标的端口和后端 server.port 不一致。最后一个小技巧把 application.yml 里的日志级别临时调到 debug观察 MyBatis 打印的 SQL 和实际表结构是否一致大多数字段映射问题在这一眼就能定位。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →