说实话,看见标题里写着【可直接运行】的时候,我是有点怀疑的。这些年接手过的遗留系统、拿来改的毕设源码不少,十个号称“解压就能跑”的项目,起码有八个要先跟环境配置打一架。但这套民宿租赁系统确实是个少见的老实项目:后端是主流的Spring Boot,前端用Vue,数据库扔给MySQL,代码结构规整,SQL脚本也备得齐全,前后端分工非常清晰。把整套流程跑通之后,你会发现它覆盖了一个信息管理系统最典型的闭环——房源维护、用户下单、订单状态流转、后台管理、统计查询,每一环都有真实业务逻辑,而不是那种只会在页面上摆两张表的空壳子。
如果你正打算找一套带完整前后端的项目来交作业、做毕设,或者想自己动手把一套前后端分离的系统从零跑通,这篇内容应该能帮上忙。我会按自己实际跑项目的顺序,从技术栈选型、环境配置、项目启动,讲到核心业务实现、数据库设计,再把那些最容易卡壳的端口、版本、精度问题一并列出来。你照着走一遍,基本不会再被“明明能运行却启动失败”这种事折磨。
1. 一套能跑通的民宿租赁系统:先看清项目全貌
1.1 三类角色与业务闭环
民宿租赁系统表面上看起来就是个“房源信息管理”项目,但真正跑起来会发现,它至少把三类用户的使用路径串在了一起。
第一类是系统管理员。管理端关心的是平台整体数据:民宿入驻了多少家、订单总量、用户注册量、评论内容是否合规。对应的功能包括基本信息管理、民宿审核、订单总览、统计报表、系统用户维护。这套系统里的管理后台通常用Vue搭一个独立的界面,用表格展示数据,通过后端接口拉取数据并操作。
第二类是房东,也就是商家的角色。房东需要维护自己的民宿信息,比如民宿名称、介绍、封面图、地址、城市,还要管理具体房间和房型的价格、库存。在订单流转中,房东负责接单、确认入住、处理退订,同时需要看到自己的收入情况。
第三类是用户,也就是住宿的游客。用户侧的业务是从前端页面开始的:注册登录、浏览房源、按城市和日期搜索、查看民宿详情、提交订单、支付、查看订单状态、发表评论。这部分交互做得越顺手,整套系统看起来就越完整。
三类角色合起来组成了民宿租赁的基本业务闭环:房东上架民宿和房间,用户搜索浏览并下单,管理员监督管理全流程,订单完成后用户评价,房东和管理员都能看到评价内容。这套闭环里包含了基础的CRUD,也包含了稍复杂的订单状态切换、库存扣减、日期重叠校验、统计聚合,所以它比很多“纯管理后台”项目有嚼头得多。
1.2 前端页面与后端接口怎么对应
很多初学者拿到一套前后端分离项目会先发懵:“前端代码在Vue里,后端代码在Spring Boot里,两个项目是怎么对上的?”其实你只需要抓两条线,整栋楼就通了。
一条线是页面路由。前端Vue项目里有router配置,对应着地址栏的路径,例如首页/、民宿详情页/homestay/:id、登录页/login、后台管理页面/admin。每个页面加载时会发请求取数据。另一条线是后端接口。Spring Boot后端通过Controller暴露RESTful API,例如GET /api/homestay/list、POST /api/order/create、PUT /api/order/cancel。前端通过axios发请求,把返回的JSON渲染到页面上。
前后端交互的关键在于接口约定。比如前端提交一个预订请求,发送的JSON里有roomId、checkInDate、checkOutDate、contactName,后端用对应的实体类或DTO接收,校验之后返回订单号、状态码和提示信息。前端拿到结果后决定跳转支付页还是弹窗提示错误。只要这条链路清楚,你在读代码时就不会迷路。
前后端之间的权限控制也很常见:用户登录后拿到token,存到localStorage,axios拦截器每次请求自动带上token,后端拦截器解析token后再放行接口。整个流程一看就懂,也是面试里经常会被追问的地方。
2. 技术栈选型:Spring Boot + Vue + MySQL为什么这么搭
2.1 后端为什么是Spring Boot而不是老Spring MVC
如果是五年前,这个项目很可能叫SSM(Spring + Spring MVC + MyBatis)整合项目,光是配置一堆XML就够折腾半天。Spring Boot把绝大多数配置变成了自动装配和约定优先,你引入一个spring-boot-starter-web,内嵌的Tomcat就已经准备好了,启动类写个main方法就能起服务。
我在实际使用中觉得,Spring Boot对齐这套民宿租赁系统最大的优势是:它让团队成员把注意力放在业务接口上,而不是放在环境折腾上。比如你要写一个民宿列表接口,直接建Controller、Service、Mapper三层,类上打@RestController、@Service、@Mapper注解,业务就串起来了。数据库操作Spring Boot也给了两条主流路线:MyBatis写SQL灵活可控,Spring Data JPA省去大量模板代码。这套系统用的是MyBatis或MyBatis-Plus的话,SQL都在mapper.xml里,排查问题的时候一眼能看到最终执行的语句。
另外,Spring Boot的生态太成熟了,集成Redis、MQ、定时任务、文件上传都有现成的starter。民宿租赁系统这种体量的项目用Spring Boot,以后想扩展也不至于推倒重来。
2.2 前端选Vue的务实理由
民宿租赁系统的前端如果还拿JSP来渲染,会特别痛苦:页面里嵌套Java代码,前后端工程师没法并行开发,交互改起来费劲。Vue作为渐进式框架,最大的好处是组件化和数据驱动。
你在Vue里写一个民宿卡片组件,包含标题、价格、封面图、城市标签,数据通过props传进去,父组件只要循环数组就能渲染出整个房源列表。页面交互变成了“数据变,视图自动变”,不用手动操作DOM。这对信息管理系统来说体验提升非常明显,因为后台管理页面的公共逻辑——表单校验、弹窗、表格分页、状态标签——都能封装成组件复用。
配合Element UI或Element Plus这类组件库,后台管理的表格、表单、日期选择器、分页控件都不用手搓,开发效率很高。Vue的工程化生态也成熟,用Vite或Webpack管理依赖,用vue-router管理路由,用Pinia或Vuex管理状态,整体项目结构清晰,新接手的人看一眼目录就知道代码放在哪。
2.3 MySQL在这套系统里的角色
MySQL在这套系统里承担的是“所有业务数据的最终归宿”。用户信息、民宿和房间资料、订单流水、评论内容,全部落库。
选中MySQL不是因为它新潮,而是因为它足够可靠、免费、文档丰富,而且和Spring Boot的配合最顺滑。InnoDB引擎支持事务和外键约束,像下单这种“扣库存加订单”的操作必须放在同一个事务里,MySQL能保证要么都成功、要么都回滚。数据量在十万级对MySQL来说是轻松活,民宿租赁这类垂直领域的业务量,单库单表完全够用。
有些项目会为了“显得厉害”硬塞Redis做缓存、MongoDB存评论,但就这套系统而言,没有足够的性能瓶颈之前,任何中间件都只是在增加维护成本。先把MySQL用好,学会建表、加索引、写聚合SQL,这才是后期上任何新组件的基础。
3. 从下载到跑通:本地环境配置与项目启动全流程
3.1 环境版本怎么选:别让版本差异成为第一个坑
拿到源码的第一件事不是双击打开,而是先看版本。我看到太多人卡在环境上的原因只有一个——版本对不上。
你先打开后端pom.xml看Spring Boot的parent版本,再看前端package.json里的Vue和构建工具版本,然后倒推需要的JDK和Node版本。我建议直接按这个组合来:
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 8 或 17 | Spring Boot 2.x用JDK 8,Spring Boot 3.x要求JDK 17以上 |
| Maven | 3.6+ | 用于后端依赖下载和打包 |
| Node.js | 16 LTS | 跑Vue项目最稳,Vue 3 + Vite也可用18 LTS |
| MySQL | 8.0 或 5.7 | 8.0记得调整密码认证方式,字符集统一utf8mb4 |
| IDE | IDEA + VSCode | IDEA跑后端,VSCode写前端 |
| 数据库工具 | Navicat 或 Workbench | 用来导入SQL脚本、调试数据 |
这里特别提醒:不要看到新的就装最新的。Node 17以上运行旧版Webpack项目经常会报OpenSSL错误,JDK版本太新跑旧版Spring Boot也会出现一堆不兼容。先按项目实际依赖来装,之后再考虑升级。
3.2 后端启动细节:配置文件与Maven依赖
后端项目用IDEA打开,等它识别为Maven项目后,首次会自动下载依赖。这一步在国内经常很慢甚至失败,建议先配置阿里云Maven镜像,在maven的settings.xml的mirrors节点里加:
<mirror> <id>aliyunmaven</id> <mirrorOf>*</mirrorOf> <url>https://maven.aliyun.com/repository/public</url> </mirror>依赖下载完成后,找到src/main/resources里的application.yml,重点看数据源配置。我第一次跑这种项目都会先确认数据库地址、账号密码是否对得上本地环境。典型的配置是这样的:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/homestay?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver密码改成你自己的数据库密码。如果项目用到Redis或其他中间件,配置文件里也会写,先确认本机服务有没有启动。
然后处理SQL脚本。项目里通常会放一个homestay.sql或database.sql,用Navicat新建一个数据库,字符集选utf8mb4,导入执行。执行完检查一下表是否建立成功、是否已有几条测试数据,这些数据能让你启动后立刻看到页面效果。
最后,找到启动类(一般是项目名加Application后缀,标着@SpringBootApplication),点击运行。控制台出现“Started Application in...”说明后端起来了。验证方法很简单,浏览器访问 http://localhost:8080 看到错误页也说明端口已经通,再访问一个具体的接口路径看看能否返回JSON。
3.3 前端启动细节:依赖安装与跨域代理
前端项目单独放在一个目录里,用VSCode打开。先执行依赖安装:
npm install如果下载慢,换成国内镜像:
npm config set registry https://registry.npmmirror.com安装完成后看package.json里的scripts,通常是npm run serve或npm run dev,运行即可。Vue 2 + Vue CLI项目启动后默认端口是8080,容易和后端冲突,Vue 3 + Vite项目默认通常是5173。
前后端联调还有一道重要配置:跨域代理。前端页面访问 /api 开头的接口时,需要通过代理转发到后端。Vue 2项目在vue.config.js里配:
module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } }Vue 3 + Vite项目在vite.config.js里配:
server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }前端启动后打开页面,能注册、能登录、能看到民宿列表,说明前后端已经打通,整套系统就算真正跑起来了。这个节点非常关键:一旦通了,后面任何问题你都能通过“是前端调用问题还是后端接口问题”来快速定位。
4. 核心业务拆解:预订流程的状态流转与关键实现
4.1 下单这个动作背后发生了什么
一个用户在前端点了“立即预订”,看到的反应是“订单创建成功”,但后端在这个接口里做了不止一件事。
第一步,接收参数校验。房间ID、入住日期、离店日期、入住人数、联系人电话,这些参数只要有缺漏或格式不对,后端会直接拒绝。第二步,检查房态和库存。这个房间在目标日期段内是否可订,有没有被其他人占用,当前剩余可售数量是否大于0。第三步,计算价格。单价乘以入住天数,得到总价,必要时加上清洁费或平台服务费。第四步,扣减库存并生成订单。库存减一,订单表插入一条待支付记录。第五步,返回结果给前端,同时启动“超时未支付自动取消”的计时。
这个流程里最容易出错的就是第四步,因为涉及两个操作:扣库存和插订单。如果中间任何一步失败,必须保证两边都回滚,否则会出现“订单生成了但库存没减(超卖)”或“库存减了但订单没生成(幽灵单)”的问题。
解决方式就是给方法加事务注解:
@Transactional(rollbackFor = Exception.class) public OrderResult createOrder(CreateOrderRequest req) { Room room = roomMapper.selectById(req.getRoomId()); if (room == null || room.getStock() <= 0) { throw new BizException("房间不存在或已下架"); } int affected = roomMapper.deductStockIfEnough(req.getRoomId(), req.getDays()); if (affected == 0) { throw new BizException("库存不足,请更换日期或房型"); } Booking order = buildOrder(req, room); bookingMapper.insert(order); return OrderResult.success(order); }代码里那个deductStockIfEnough很有讲究,它对应的SQL是“扣减库存但限定库存大于0,如果影响行数为0就说明扣减失败”。这种方式比“先查库存再扣库存”更安全,避免了并发时两个人同时抢最后一间房的问题。如果是更高并发场景,还可以考虑版本号乐观锁或者数据库行锁,但对这套民宿系统的体量来说,条件更新已经足够。
4.2 订单状态机:六个状态怎么串起整个流程
订单状态是民宿租赁系统里最值得研究的点。这六个状态把用户和房东的每一次操作都串了起来:
| 状态码 | 状态 | 触发条件 | 后续动作 |
|---|---|---|---|
| 0 | 待支付 | 用户提交订单 | 锁定库存,等待支付 |
| 1 | 已支付 | 支付回调成功 | 通知房东,生成入住凭据 |
| 2 | 已确认 | 房东后台确认 | 用户收到确认通知 |
| 3 | 已入住 | 用户办理入住 | 房间占用中 |
| 4 | 已完成 | 退房结算完成 | 开放评论入口 |
| 5 | 已取消 | 用户取消或超时 | 释放库存并退款 |
状态机的核心是“不允许非法跳转”。比如已取消的订单不能变成已支付,已完成的订单不能再次入住。很多项目在状态流转上只用if-else硬写,订单一多就变成一团乱麻。更清晰的做法是维护一张“状态到状态”的流转表:
private static final Map<Integer, Set<Integer>> ALLOWED_TRANSITIONS = new HashMap<>(); static { ALLOWED_TRANSITIONS.put(0, new HashSet<>(Arrays.asList(1, 5))); ALLOWED_TRANSITIONS.put(1, new HashSet<>(Arrays.asList(2, 5))); ALLOWED_TRANSITIONS.put(2, new HashSet<>(Arrays.asList(3, 5))); ALLOWED_TRANSITIONS.put(3, new HashSet<>(Arrays.asList(4))); ALLOWED_TRANSITIONS.put(4, Collections.emptySet()); ALLOWED_TRANSITIONS.put(5, Collections.emptySet()); }每次修改状态前,先检查当前状态是否允许目标状态,不允许就抛异常。这样哪怕以后加了新状态,也只需要改这一张表。
超时未支付的取消逻辑,在单体项目里我用的是Spring自带的定时任务。每隔几分钟扫描一次:当前时间减去订单创建时间超过30分钟、状态还是0的订单,统一改成已取消,并把库存加回去。这个方案简单、能跑、也够用,比引入MQ做延迟消息要轻量得多。
4.3 房态与库存:可订、被锁定、已售出的判断逻辑
民宿系统里“库存”和普通商品不一样。普通商品库存是一个恒定的总数,民宿房间的库存却和日期强相关:7月1日到7月3日被订了,7月5日到7月7日还可以订。
所以库存判断的逻辑不能只看房的stock字段,还要考虑订单表的日期占用情况。每次用户查询某天能不能订,系统要做的判断是:目标入住日期和离店日期之间,和现有订单的入住、离店日期有没有重叠。
SQL可以这样写:
SELECT COUNT(*) FROM booking WHERE room_id = #{roomId} AND status IN (0, 1, 2, 3) AND check_in_date < #{newCheckOutDate} AND check_out_date > #{newCheckInDate}这段SQL的逻辑就是“别人还没走的日期和你准备来的日期重叠了,就不能再订”。只要查询结果的数量大于等于该房间的可用数量,就说明已经满了。这个日期重叠判断是预订类系统最常见的业务点,面试时也值得多提一句。
下单成功时把stock扣掉,订单取消或完成后stock加回,配合每天定时清理过期订单,这套逻辑就能保证房态数据大致正确。如果要做更严格的时段级房态,可以引入房态日历表,但那是后话。
5. 数据库表设计:几张核心表如何撑起整套业务
5.1 六张核心表的结构与设计意图
民宿租赁系统的数据库表数量不需要多,但每一张表都得撑起一段业务流程。我梳理了最核心的六张表:
| 表名 | 用途 | 关键字段 |
|---|---|---|
| sys_user | 系统用户 | id、username、password、phone、role、status |
| homestay | 民宿表 | id、owner_id、name、city、address、cover_img、description |
| room | 房间表 | id、homestay_id、room_type、bed_info、price、stock、status |
| booking | 订单表 | id、order_no、user_id、homestay_id、room_id、check_in_date、check_out_date、total_price、status |
| comment | 评论表 | id、order_id、user_id、homestay_id、content、rating、reply_content |
| favorite | 收藏表 | id、user_id、homestay_id、create_time |
这张表组合基本覆盖了所有业务页面。用户表管登录注册和角色权限,民宿表和房间表管房源展示,订单表管交易流转,评论表管用户反馈,收藏表管用户个人行为。没有多余的冗余表,功能边界清楚。
以订单表为例,实际建表SQL大概长这样:
CREATE TABLE `booking` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '订单号', `user_id` bigint(20) NOT NULL COMMENT '下单用户ID', `homestay_id` bigint(20) NOT NULL COMMENT '民宿ID', `room_id` bigint(20) NOT NULL COMMENT '房间ID', `homestay_name` varchar(100) DEFAULT NULL COMMENT '民宿名称冗余', `room_type` varchar(50) DEFAULT NULL COMMENT '房型名称冗余', `price_snapshot` decimal(10,2) NOT NULL COMMENT '下单时单价快照', `total_price` decimal(10,2) NOT NULL COMMENT '订单总价', `check_in_date` date NOT NULL COMMENT '入住日期', `check_out_date` date NOT NULL COMMENT '离店日期', `guest_count` tinyint(4) NOT NULL COMMENT '入住人数', `contact_name` varchar(50) NOT NULL COMMENT '联系人', `contact_phone` varchar(20) NOT NULL COMMENT '联系电话', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '0待支付 1已支付 2已确认 3已入住 4已完成 5已取消', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`), KEY `idx_room_time` (`room_id`, `check_in_date`, `check_out_date`), KEY `idx_create_time` (`create_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';看到这里你可能注意到我对订单表做了冗余设计,人为存了homestay_name和room_type。原因很直接:订单列表页面需要显示民宿和房型信息,如果全靠联表查询,每次查询都要join民宿表和房间表,数据量大的时候性能会越来越慢。订单表里冗余这几个展示字段,列表页一次单表查询就出来了。代价是民宿改名时订单表里的冗余字段不同步,但对订单这种历史记录来说,保留下单时的名称其实更合理。
5.2 金额、时间、状态这些字段为什么这样定义
新手建表最容易踩的坑有三个:金额用float、时间乱选类型、状态用字符串。
金额字段必须用decimal(10,2),不要用float。float在计算机里是二进制浮点数,0.1加0.2可能是0.30000000000000004,而金额计算出现这种误差是绝对不允许的。decimal是定点数,适合做货币、价格、百分比这类需要精确计算的场景。单价、总价、退款金额,全部用decimal。
时间字段,我建议用datetime而不是timestamp。timestamp有一个2038年溢出的问题,而且会受MySQL时区设置影响,同一个时间在不同时区配置下读出来可能变了;datetime存的就是字面值,不管时区怎么切换,它代表的都是你写入的那一刻。再加上配置里的serverTimezone=Asia/Shanghai,基本能避开乱码和时间错乱的问题。
状态字段我用tinyint加注释,很少直接用varchar存“待支付”“已支付”这种中文。整型占用空间小,后端用枚举或常量类来定义含义,不容易出现“待支付”和“待付款”这种同一业务两种叫法的混乱。如果你希望数据库更可读,可以用MySQL的ENUM类型,但之后想加状态值就要改表结构,所以我更推荐tinyint + 代码枚举的方式。
order_no字段值得一提。这张表给order_no加了唯一索引。订单号由后端生成,常见做法是日期加时间戳加随机数,例如2025070110304512345678。唯一索引的意义在于接口层面万一出现重复提交,数据库兜底会拒绝第二条相同订单号,避免产生重复订单。
5.3 索引与外键:实操中我倾向于怎么做
索引这块,实践比理论更重要。民宿系统的查询场景很有规律:用户查自己的订单(user_id)、房东查自己民宿的订单(homestay_id或owner_id关联)、按城市搜民宿(city)、日期段查房间占用(room_id + check_in_date + check_out_date)。这些字段建索引之后,慢查询基本消失。
组合索引的原则是“最左前缀”,比如(room_id, check_in_date, check_out_date)这个索引可以覆盖“先定位房间、再过滤日期段”的查询。你用EXPLAIN看一下SQL执行计划,有没有走索引一目了然。
外键这里我想多说一句。很多教材都强调外键能保证数据一致性,但实际项目里,我见过的大多数业务系统在代码层面控制关联关系,不建物理外键。原因有几个:物理外键在删除父表数据时会因为子表约束报错,导致你无法灵活地做逻辑删除;分库分表时物理外键基本没用;高并发写入时外键校验会增加额外的锁开销。更推荐的做法是:用逻辑外键,也就是存peer_id、user_id这样的字段,但约束由Service层代码保证。比如删除一个民宿前,先检查它有没有未完成的订单,有就拒绝删除,不需要数据库来拦。如果你正在学的课程强行要求外键,那按课程来就行,工作里可以务实一点。
6. 实操中绕不开的坑:端口、版本、精度与时区问题
6.1 端口冲突与后端启动失败排查
后端启动后控制台直接飘红,最常见的现象是“Port 8080 was already in use”。这时候很多人会反复重启项目,其实问题根本不在代码层面。
在Windows上,打开命令提示符输入:
netstat -ano | findstr 8080能看到占用8080端口的进程PID,然后去任务管理器找到对应进程结束掉。如果这台机器上有多个Java项目要一起跑,更省事的办法是直接改Server端口,application.yml里把8080改成8081。注意,改了后端端口之后,前端的代理配置也要同步改,否则前端转发还是指向8080,照样连不上。
排查启动失败还有一条原则:看控制台第一行错误,不要只盯着最后一行异常。Spring Boot启动失败往往是一连串日志,真正的原因通常在“Error creating bean with name...”“Unable to connect to database”这类早期信息里。数据库连不上就去看账号密码和URL,Redis连不上就去看Redis有没有启动,先把外部依赖解决,再去抠代码逻辑。
6.2 Node版本过高导致的构建失败
这套系统是前后端分离的,前端启动阶段有一类错误非常典型:跑npm run serve的时候,构建到一半报编译错误,日志里带着“Error: error:0308010C:digital envelope routines::unsupported”。
这个错误的根源是Node 17及以上版本默认启用了OpenSSL 3.0,而项目里的Webpack版本用的是旧的OpenSSL 1.1 API,两边不兼容。解决方案有两个:一是降Node版本,直接用16 LTS最省心;二是不想换版本的话,在package.json的scripts里给启动命令加上:
"serve": "NODE_OPTIONS=--openssl-legacy-provider vue-cli-service serve"这行配置的意思是让Node使用旧版OpenSSL提供者,绕开兼容性报错。不过我建议能降Node就降Node,因为后面你还会遇到其他ESLint、依赖版本的问题,一个16 LTS能帮你躲开一大片雷。
顺带一提,很多人安装依赖失败是因为npm registry源太慢,运行npm config set registry https://registry.npmmirror.com之后重新安装,基本立竿见影。
6.3 数据库连接报错与中文乱码
数据库这块有两个高频问题。第一个是MySQL 8.0连接时报“Public Key Retrieval is not allowed”,这是因为MySQL 8.0的caching_sha2_password加密方式需要先获取服务器的公钥。解决方式是在JDBC URL后面加上allowPublicKeyRetrieval=true,同时配合useSSL=false把SSL校验关掉:
jdbc:mysql://localhost:3306/homestay?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true第二个是页面和数据库都出现中文乱码。排查链路是:先看数据库表字符集是不是utf8mb4,执行SHOW CREATE TABLE看看字符集;再看JDBC连接URL有没有characterEncoding=utf8;最后看后端返回JSON时有没有统一设置UTF-8。三步都检查一遍,乱码基本消失。建库时养成习惯,直接指定utf8mb4,不要把决定权交给数据库默认配置。
6.4 Long类型精度丢失与跨域问题
后端订单表主键是Long,前端拿到后却经常出现ID后几位变成0的情况。原因在于JavaScript的Number类型最大安全整数是2的53次方,而数据库自增主键生成的bigint一旦超过这个范围,精度就丢了。
常见的解决办法是把ID序列化为字符串返回给前端。Spring Boot项目里配置一个Jackson自定义序列化器,或者直接在ID字段上加注解:
@JsonSerialize(using = ToStringSerializer.class) private Long id;如果项目统一使用Jackson,也可以全局注册Long转String的序列化配置,这样所有Long类型字段都对前端友好。前端拿到的ID变成字符串后,传给后端的请求参数通常也要相应调整,后端Controller的入参类型用String接收再转Long,或者直接按字符串处理。
跨域问题在前后端分离项目里迟早会遇到。浏览器直接访问前端页面,前端向http://localhost:8080发请求,会报CORS错误。开发环境最优雅的解决方式就是前面说的代理转发,配置好之后前端请求看起来是同源的,浏览器不再拦截。生产环境通常用Nginx做反向代理,把 /api 转发到后端服务,同样一劳永逸。
最后一个我自己实操中的习惯建议:整套系统跑通之后,先打开git把当前状态打一个初始提交。之后不管你怎么改代码、折腾新功能,随时能回退到“能跑”的状态。这个习惯让我少掉了很多头发,写代码时心里特别有底。拿到这套源码后,你顺着订单状态机和数据库表结构这两条主线读一遍,再去动任何功能,会觉得整个系统变得越来越顺手。