1. 项目概述:景区民宿预约系统到底在做什么
我最早接到这个需求时,对方给的原话是:“景区边上十几间民宿,想做个能在线看房、下单选房、后台管订单的小系统。”这听起来像是个典型的增删改查项目,但真正动手之后才发现,民宿预约的关键不在“预订”这个动作,而在房态、日期、库存、价格策略,以及后端 MyBatis 动态 SQL 之间的配合。整套系统选用 SpringBoot + Vue + MySQL + MyBatis 这个技术栈,刚好能覆盖大部分中小型民宿场景的诉求,也是国内 Java 全栈项目里非常主流的组合。
这个项目适合谁?一是准备做毕业设计或简历项目的同学,二是刚转 SpringBoot 全栈、想找一个完整业务闭环练手的开发者,三是真的想给景区民宿做一套线上预约系统的个人或小团队。它能解决的核心问题有三个:让游客不用打电话就能看房、选房、下单;让老板能在后台维护房型、价格、订单;让民宿和游客之间的确认流程从“加微信发消息”变成系统化、可追踪的状态流转。
下面我会从业务建模、数据库设计、后端接口、前端页面到部署排查,把整套系统的设计思路和落地细节完整拆一遍。重点是讲清楚“为什么这么做”,而不只是截图式地贴代码。
2. 系统架构与核心业务设计
2.1 业务模块怎么划分
景区民宿预约系统从使用角色上看,可以分成游客端、民宿管理端和系统端三层。游客端主要提供首页展示、民宿列表、房型详情、在线预约、我的订单这些功能;管理端负责房型维护、民宿信息编辑、订单审核、入住退房操作、价格日历设置;系统端则处理用户登录、管理员鉴权、统计报表这类基础能力。
从工程结构上,我采用的是经典前后端分离方案。后端只提供 JSON 接口,前端用 Vue 开发单页应用,通过 Axios 调用接口。这样做的直接好处是,前端可以做独立的页面路由和组件复用,比如同一个订单卡片组件,在游客端展示“待支付、已预约、已入住”不同状态,在管理端只多一个“确认入住”按钮,代码写一遍就能复用。
前后端分离也有个必须接受的成本:联调时跨域、token 传递、接口文档同步都要比传统 JSP 项目多花心思。所以在项目里我把接口地址统一放到了application.yml的server.servlet.context-path和前端.env.development环境变量里,后端统一走/api前缀,前端开发环境用 Vite 的 proxy 转发到 8080,生产环境再让 Nginx 做反向代理。这个设计后面部署时几乎不用改代码。
2.2 为什么选 SpringBoot + MyBatis,而不是 JPA
选 SpringBoot 没什么可犹豫的,它把配置、内嵌 Tomcat、依赖管理都收敛了,一个mvn spring-boot:run就能起来,适合快速落地。关键差别在数据访问层,我最终选了 MyBatis 而不是 Spring Data JPA,原因有三个:第一,民宿预约里有大量按日期、按房型、按状态组合查询的列表场景,用动态 SQL 表达比 JPA 的 Specification 或 QueryDSL 更直观;第二,团队里后来的成员都是写 SQL 出身,MyBatis 的 XML 文件让他们心里踏实;第三,库存扣减这种需要精细控制 SQL 的更新语句,MyBatis 能直接写出带条件的UPDATE ... WHERE,可控性更强。
MyBatis 也不是没有缺点,最明显的就是“样板代码多”。实体类、Mapper 接口、XML、Service 一层层手写很枯燥。所以我在项目里引入了 MyBatis-Generator 生成基础代码,再手工补充自定义查询,避免把时间浪费在 getter/setter 和无意义的selectById上。
2.3 数据库表结构和字段设计
数据库我用了 MySQL 8.0,字符集统一utf8mb4,排序规则utf8mb4_general_ci。核心表我拆成了六张:用户表t_user、民宿表t_homestay、房型表t_room_type、房态库存表t_room_inventory、预约订单表t_reservation、管理员表t_admin。
这里重点说一下t_room_inventory,它解决的是“民宿按天卖房”这个核心问题。传统酒店系统通常会为每个房间生成每天一条记录,但民宿房量少、房型固定,我用“房型 + 日期”作为唯一维度。表里字段大致是:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| room_type_id | bigint | 关联房型 |
| inventory_date | date | 营业日期 |
| total_count | int | 总房量 |
| booked_count | int | 已预约数量 |
| price | decimal(10,2) | 当日价格 |
| status | tinyint | 0可售,1停售 |
预约下单时,扣减库存就是一个UPDATE t_room_inventory SET booked_count = booked_count + 1 WHERE room_type_id = ? AND inventory_date = ? AND booked_count < total_count的原子操作,避免了先查后改的并发问题。这个设计后面所有核心接口都是围绕它展开的。
订单表t_reservation则记录了order_no、user_id、room_type_id、check_in_date、check_out_date、total_amount、status、create_time等字段。订单编号我没有用自增 id 直接暴露,而是用日期加随机数生成,比如2025060112345678,好处是用户报单号时不容易输错,也不会暴露系统真实订单量。
3. 后端核心实现:SpringBoot + MyBatis 的实战拆解
3.1 工程目录与依赖配置
后端工程我按常见的分层结构来组织:controller只做参数接收和结果封装,service写业务逻辑,mapper只负责数据库访问,entity对应表结构,vo放视图对象,common放统一返回结果、异常处理、工具类。这样分层的目的是当订单状态逻辑变复杂时,不会出现“控制器里写 SQL”这种后期维护噩梦。
依赖方面,最基础的是这些:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.2</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>如果你用的 SpringBoot 3.x,注意javax包已经变成jakarta,MyBatis 的 starter 版本也要选 3.0 以上的版本,否则启动时会报找不到org.springframework.boot.autoconfigure.condition.ConditionalOnClass之类的兼容错误。这是我第一次升级版本时踩过的坑,项目里我把 SpringBoot 版本固定为 2.7 系列,稳定优先。
3.2 登录鉴权与统一状态返回
民宿预约系统的权限模型比较简单,游客端用手机号加验证码登录,管理端用账号密码登录。为了不引入太重的东西,我用 JWT(JSON Web Token)做登录态。用户登录成功后,后端签发一个 token,前端每次请求在请求头里带上Authorization: Bearer xxx,后端通过拦截器解析 token 并注入当前用户信息。
这样做有个取舍:JWT 本身是无状态的,无法在服务端主动踢人,如果用户改了密码,旧 token 在到期前仍然有效。对于民宿这种低频业务来说完全够用,但如果后期要做“强制下线”,就得引入 Redis 做黑名单,或者干脆换成 Spring Security + Session 方案。我在源码里留了一个UserContext工具类,用 ThreadLocal 保存当前用户,这样 Service 层代码里直接UserContext.getUserId()就能拿到操作人,比每个接口都从参数传 userId 要清爽。
统一返回结构我用了一个泛型类Result<T>,字段是code、message、data。正常业务返回code=200,业务异常返回像400、401、403、409这类具体状态码,再由全局异常处理器统一转成 JSON。这里特别提一下:不要让后端在业务失败时返回 HTTP 500,因为前端 Axios 的拦截器默认会把非 2xx 当异常,业务里的“库存不足”其实是正常反馈,用code=409更合适。
3.3 民宿详情与房态查询
游客端首页进入某家民宿详情页后,需要看到两个信息:房型列表和未来几天的可订状态。房型列表可以简单SELECT * FROM t_room_type WHERE homestay_id = ?查出来,但“哪天有房、哪天满房”就必须用日期区间去查。
我在后端提供了一个接口,参数是roomTypeId、startDate、endDate,返回每天的库存情况。让人放心的是,这个查询不需要写循环,一条关联 SQL 就能实现:
<select id="selectInventoryRange" resultType="com.example.entity.InventoryVO"> SELECT inv.inventory_date, inv.total_count, inv.booked_count, inv.price, (inv.total_count - inv.booked_count) AS available_count FROM t_room_inventory inv WHERE inv.room_type_id = #{roomTypeId} AND inv.inventory_date >= #{startDate} AND inv.inventory_date < #{endDate} ORDER BY inv.inventory_date </select>注意 XML 文件里“<”号必须转义成<,否则 XML 解析会直接报错。这个坑太经典了,我在写动态日期查询时被卡了十分钟,后来才意识到问题出在 XML 转义上。更重要的是,这个查询结果会被前端用来画“价格日历”,所以哪怕某天status=1已停售,也要返回status字段让前端置灰,而不是直接把这条记录过滤掉。
3.4 预约下单:事务、锁与状态机
预约下单是整个系统最核心的接口,流程包括:校验登录状态、校验入住日期不能早于今天、计算房间总价、扣减房态库存、生成订单、返回订单号。我把它全部放在一个@Transactional方法里,任何一个环节失败都能回滚库存。
库存扣减我前面提到用原子 UPDATE,但如果想要防止“同一个用户的重复点击”造成的重复下单,还需要配合唯一约束或者业务幂等处理。我采用了最简单有效的方案:在前端点击“提交预约”后立即禁用按钮,并把按钮文字改成“提交中”;后端再对同一个用户同一天同一房型的入住日期加一个内存锁,用ConcurrentHashMap做短时间内的重复请求拦截。虽然分布式环境下这个方案有局限,但单机部署的民宿系统完全够用。
订单状态我设计成一个有限状态机:PENDING_PAYMENT待支付、PAID已支付/待确认、CONFIRMED已确认、CHECKED_IN已入住、CHECKED_OUT已退房、CANCELLED已取消、EXPIRED已过期。每个状态之间的流转都写在 Service 层的独立方法里,例如支付成功时调用confirmOrder(),而不是直接在任意地方改status字段。这样做的好处是,未来如果增加“游客已入住三天后自动确认完成”的定时任务,只需要在状态机里补一个触发条件,不会牵扯到订单创建逻辑。
订单价格计算我单独抽了一个PriceCalculator,它的输入是房型基础价和入住天数,输出是总价。民宿价格通常不是固定不变的,我在t_room_inventory里允许每天设置独立价格,节假日前台把某几天的价格调高,系统计算总价时直接累加每日价格,而不是“基础价乘以天数”。这块逻辑在需求评审时差点被漏掉,如果按简单方式实现,游客下单时看到的价格和实际结算价不一致,容易产生客诉。
3.5 MyBatis 细节:缓存、TypeHandler 与动态更新
网上搜 MyBatis 面试题时经常看到一级缓存、二级缓存的问题,实际项目里我对缓存一直比较谨慎。MyBatis 的一级缓存默认开启,作用域是同一个 SqlSession,在 Spring 管理的事务里通常没问题;二级缓存默认关闭,我建议不要开,因为一旦涉及到多表联查和自定义 SQL,缓存失效的时机很难把控,而且民宿系统的读多写少特性比较适合做 Redis 层面的缓存,而不是依赖 MyBatis 内置缓存。如果真要加性能优化,我会在selectInventoryRange这类高频查询上做 Redis 缓存,key 按“房型+日期区间”设计,库存扣减时再删缓存。
另一个用得上的点是 TypeHandler。订单表里有一个extend_info字段,用来存续住备注、发票信息等不确定结构的内容,我用 JSON 字符串存储。每次手动序列化、反序列化很烦,就写了一个JsonTypeHandler,继承BaseTypeHandler<String>,在setNonNullParameter里把对象转成 JSON 写入,在getNullableResult里把 JSON 转回对象。配置方式是在 Mapper XML 里指定:
<result column="extend_info" property="extendInfo" typeHandler="com.example.common.JsonTypeHandler"/>注意实体类里的对应字段要声明成String类型,这样类型处理器才不会在调用时因为泛型类型不一致而报ClassCastException。我把这个坑写进源码注释里,因为一开始我把字段声明成Object,结果查出来的 JSON 始终是字符串,后来查了半天才发现是 handler 泛型桥接方法的问题。
4. 前端核心实现:Vue 页面与前后端联调
4.1 Vue 工程搭建与目录组织
前端我用的 Vue 3 + Vite + Vue Router + Pinia,这套组合启动速度比 Webpack 时代快很多。创建工程就一行命令:
npm create vite@latest homestay-frontend -- --template vue安装依赖后,目录按功能组织:src/api放所有接口请求,src/router放路由表,src/views放页面组件,src/components放公共组件,src/store放全局状态。对于民宿预约系统,页面不算多,但“列表页、详情页、订单页、管理页”这四类页面都具备典型的前端交互逻辑,非常适合用来练 Vue 的核心能力。
如果你在安装 Vue 依赖时遇到npm install卡住不动,多半是网络源的问题,先执行npm config set registry https://registry.npmmirror.com再装。别小看这一步,很多新手在vue安装及环境配置这一关就被挡住了,其实不是 Vue 的问题,是 Node 生态的包管理工具使用习惯问题。
4.2 路由配置与页面参数传递
民宿列表到详情页的跳转,我用的是 Vue Router 的动态路由参数:
const routes = [ { path: '/', name: 'Home', component: Home }, { path: '/homestay/:id', name: 'HomestayDetail', component: HomestayDetail }, { path: '/order/confirm', name: 'OrderConfirm', component: OrderConfirm }, { path: '/orders', name: 'MyOrders', component: MyOrders }, ]在民宿列表页点击“查看详情”时,通过router.push({ name: 'HomestayDetail', params: { id: row.id } })跳转,详情页再用route.params.id获取民宿 id。这里我提醒一句:params传参在页面刷新后会丢失,如果参数必须持久化,比如从预约确认页跳转到订单支付页,建议用query传参或者把订单号放到 URL 的 path 里。我在项目中订单详情页用的是/order/detail/:orderNo,这样复制链接发给别人也能直接打开。
Vue 单页应用还有一层要注意:路由切换不会重新加载页面,所以详情页组件在beforeRouteUpdate或 watchroute.params时,要重新请求接口。不然从 A 民宿跳到 B 民宿,页面上可能还显示 A 的数据。这个问题的隐藏性很强,我第一次联调时排查了很久。
4.3 Axios 封装与统一鉴权
Axios 请求我封装成了一个request.js,统一做了三件事:带上 token、统一处理业务错误、处理 HTTP 异常。核心代码大致是这样:
import axios from 'axios' import { ElMessage } from 'element-plus' const service = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 10000 }) service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = 'Bearer ' + token } return config }) service.interceptors.response.use( response => { const res = response.data if (res.code === 200) { return res } if (res.code === 401) { localStorage.removeItem('token') window.location.href = '/login' } ElMessage.error(res.message) return Promise.reject(new Error(res.message)) }, error => { ElMessage.error('网络异常,请稍后重试') return Promise.reject(error) } )这样处理之后,业务组件里不需要每个接口都判断res.code,直接拿到res.data就认为是成功结果,代码干净很多。但要注意:如果某个接口需要处理“库存不足”的特殊逻辑,比如自动给用户换日期,这种情况不能只依赖全局拦截器,要在接口调用处单独 catch 并读取error.response.data.code来做分支。
4.4 民宿列表与预约表单的关键交互
民宿列表页用了 Element Plus 的卡片布局,每张卡片显示民宿主图、名称、最低价格和评分。这个页面的搜索条件包括“入住日期、离店日期、人数”,数据变化后重新请求/api/homestay/list?startDate=xx&endDate=xx。这里为了让游客能直观看到价格日历,我在详情页用了一个表格组件展示“未来 30 天每天可订房量”,可用房量为 0 的日期用红字置灰,点击日期后自动填充入住日期输入框。
预约表单是最容易写糙的地方。它包含姓名、手机号、入住日期、离店日期、备注、入住人数。表单校验用了 Element Plus 的 rules,但业务上有两个特殊校验不能只靠表单库:第一,离店日期必须晚于入住日期,且入住日期不能小于今天;第二,手机号要匹配真实的 11 位号段格式,而不是只判断长度等于 11。第一点我在后端也做了核心校验,因为前端校验只是用户体验,真正防止脏数据写入数据库的是后端接口。
前端表单提交前我还做了一个“二次确认”弹窗,展示用户选择的时间、房型、总价。这个交互虽然多了一步,但能有效减少民宿场景里常见的“日期选错、房型搞混”导致的售后问题。游客确认后点击“提交预约”,调用/api/reservation/create,成功后跳转到订单列表页,并弹出“预约成功,等待民宿确认”的提示。
5. 部署、联调与常见问题排查
5.1 本地环境准备与启动顺序
本地开发我建议按这个顺序准备:先装 JDK 8/11、Maven、MySQL、Node 14+,再启动后端,最后启动前端。MySQL 安装配置教程网上很多,但有几个容易踩的细节我一定要强调:MySQL 8 默认用caching_sha2_password认证插件,老版本的驱动连接会报错,所以项目里驱动必须选mysql-connector-j8.x;如果连接时报Public Key Retrieval is not allowed,在 JDBC URL 后面加allowPublicKeyRetrieval=true。
后端启动前,要确认数据库建好了,并且application.yml里账号密码正确:
spring: datasource: url: jdbc:mysql://localhost:3306/homestay_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.DriveruseSSL=false在本地很重要,如果 MySQL 服务端没有配置 SSL 证书,而这里不关闭,就会抛出 SSL 连接错误。生产环境如果数据库在内网,一般也不需要开 SSL,靠内网隔离保证安全,真要公网访问数据库就顺手把端口白名单和安全组配好。
前端启动前,确认.env.development里的VITE_API_BASE_URL是/api,然后 Vite 配置文件里的 proxy 指向后端地址:
server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }启动后访问http://localhost:5173,登录游客账号,走一遍搜索民宿、查看详情、提交预约的流程,再到后端管理端确认订单,基本就能跑通主链路。
5.2 联调中的典型问题与排查思路
第一类问题是跨域。如果你不走 Vite proxy,直接在前端请求http://localhost:8080/api/...,后端又没有配置跨域,浏览器就会拦截。我建议统一走 proxy,避免开发环境到处写@CrossOrigin,因为@CrossOrigin一旦写进代码,生产环境还要记得删除,否则等于给所有接口开了外域访问权限,属于安全隐患。
第二类问题是日期格式化不一致。Java 后端返回LocalDate时,如果不配置spring.jackson.date-format和JavaTimeModule,前端拿到的可能是一串数组或者 ISO 字符串。我在application.yml里做了统一配置:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai这样前端拿到2025-06-01 12:00:00就能直接用,不需要写new Date(parts[0] + 'T' + parts[1])这种绕来绕去的转换代码。
第三类问题是 MyBatis 返回的字段为 null。如果你实体类属性是checkInDate,数据库字段是check_in_date,而 MyBatis 没有开启驼峰映射,查询结果就是 null。解决办法是在配置里加:
mybatis: configuration: map-underscore-to-camel-case: true这个配置我几乎每次建项目都会写上,但是架不住换新项目时忘掉。排查时如果发现查出来的数据某个字段总是 null,先看是不是驼峰映射和 resultMap 的问题,再去看 SQL 是否真的查出了数据,因为 null 字段拦截 SQL 输出的日志优化后,很容易让人误以为数据库里没数据。
5.3 前端与后端常见开发辅助工具
前端 Vue 开发时一定要装 Vue Devtools 浏览器扩展,它能直接查看组件的 props、data、router 当前路由信息。有一次我发现页面跳转后组件不刷新,就是靠 Vue Devtools 里看路由参数变化定位到的。它能帮你把“数据流哪里断了”这个问题快速缩小到组件内还是组件间。
后端 SpringBoot 调试时,我习惯于给项目做一个 Banner,用在线 Banner 生成器自助生成,纯属调剂心情,但别小看这个细节,一个带版本号的 Banner 能让你同时开多个后端服务时,一眼区分当前跑的是哪个分支。另一个实用习惯是启动时打印关键配置项,比如当前激活的 profile,这样就不会出现“本地连了生产库”这种低级事故。
网上还经常有人问“怎么将 SpringBoot jar 反编译成项目”,我在接手别人遗留项目时也干过这事。需要说明的是,反编译只能帮你恢复 class 的业务逻辑,无法还原 XML 注释、多行可读性、以及 Maven 原始依赖树,所以花几个小时去反编译,往往不如找原仓库或让作者重新发一份源码。一个负责任的项目,源码管理比什么都重要。
5.4 与业务相关的安全细节
民宿预约系统免不了用户提交手机号、姓名、订单金额这些个人信息,安全设计不能只停留在“接口加个 token”。我在项目里做了三件事:第一,所有接口统一加一个全局过滤器,对请求头和可预期参数做 XSS 过滤,过滤掉<script>、javascript:等危险片段;第二,管理端接口额外校验管理员角色,写了一个@RequireAdmin注解,通过拦截器判断当前用户是否是管理员;第三,订单金额和库存数量绝不相信前端传参,后端在下单时重新从数据库的价格表读取并计算。
XSS 这块网上有直接用 Spring 的过滤器处理上传 PDF 文件的例子,说明大家确实关注这个问题。我这里更建议采用“输入校验 + 输出编码”的双层方案:写入数据库前过滤掉危险标签,前端 Vue 默认的{{ }}插值本身会做 HTML 转义,不会把<img onerror>当成元素执行,只要别用v-html渲染用户提交内容就行。如果你从后台管理表单提交了一段富文本,确实要支持v-html展示,那就必须用白名单过滤,只允许<p>、<br>、<img>这类安全标签。
6. 扩展空间:这套系统还能继续长出什么能力
系统跑起来之后,后续的扩展方向其实很清晰。第一是支付能力,当前项目是“线下支付或到店支付”,接入微信支付/支付宝小程序支付后,订单状态机的PENDING_PAYMENT -> PAID节点会变成一个回调接口,流程会增加支付回调验签、对账、退款,代码结构仍然可以沿用现有 Service 层,不需要重写。
第二是消息通知,民宿预约确认后,可以发短信告诉游客“您的房间已确认,入住时请报姓名和手机号”。这个可以用阿里云短信或者腾讯云短信服务,后端在状态机流转到CONFIRMED时触发异步通知接口。注意短信发送一定做成异步或消息队列,不要在主流程里同步等待供应商响应,否则下单接口的响应时间会被拖慢。
第三是民宿实拍视频展示,游客端详情页想展示房间 m3u8 视频流,前端需要引入hls.js播放器,视频源的跨域鉴权头也要在前面 Nginx 配置里处理好。这个和 Vue 本身关系不大,属于播放器与流媒体访问控制的范畴,但用 Vue 组件封装一下播放器是很好的实践。
第四是多民宿接入,现在的系统是单店模式。如果以后扩充到景区周边几十家民宿联合运营,数据库就得多一张t_merchant表,库存表也要加商户维度,订单结算会变成每个商户生成结算单。到那个阶段,当前这套表结构依然能演进,不需要推翻重来。
7. 个人经验总结
说实话,民宿预约系统的难点从来不在技术框架的新奇度,而在于把“卖房间”这个简单动作做对。日期重叠校验、库存原子扣减、订单状态流转、前后端日期格式一致、XSS 过滤,这些细节单独拎出来都不难,但组合到一起就会暴露很多“看起来没 bug 实际跑不通”的问题。
我自己印象最深的一次排查是:预约下单成功后,管理端订单列表始终不显示。后来发现是前端订单列表页调用的接口传了userId,而这个用户在游客端是正常用户,管理端查询却用了管理员 token 去解析用户身份,导致查的是管理员自己的订单,当然为空。这个问题的根源就是我没把“当前登录用户”和“被查询用户”两个概念区分开,后来把接口改成服务端只从 token 拿 userId,不接收前端传入的 userId,问题才消失。这也是我反复强调UserContext的原因,很多权限问题都出在接口信任了不该信任的前端参数上。
如果你是要拿这套系统做毕设或练手,我建议不要只满足于把流程跑通,抽时间把现货库存并发扣减的单元测试补上,用两个线程模拟同一时刻抢最后一间房的场景,会对你理解事务和并发控制特别有帮助。如果是要给真实民宿用,先让老板用 Excel 把未来一个月的价格维护好,再看着导入系统的价格日历做校验,比直接开放后台录入要稳得多。
这套 SpringBoot + Vue + MySQL + MyBatis 的组合,没有高深的概念,但它覆盖了 Java 全栈工程师日常开发中绝大部分基础能力。能把每一个细节都解释清楚,比用过再多框架都有价值。最后再分享一个小技巧:给所有接口的统一返回加一个traceId字段,每次请求在拦截器里用UUID生成并放进日志上下文,排查线上问题时会感激这个设计。