首页 / 资讯中心 / 文章详情

SpringBoot2+Vue3家政平台实战:从订单设计到部署避坑

SpringBoot2+Vue3家政平台实战:从订单设计到部署避坑 ★ FEATURED ARTICLE
做家政服务平台这类项目最怕的就是把代码堆完就扔结果业务逻辑说不清技术选型也看不出门道。标题里这个组合——SpringBoot2 Vue3 MyBatis-Plus MySQL8.0乍看是常规技术栈但真落地的时候光是一套能跑通预约下单到支付完成的完整闭环就够忙活一阵子。今天我把这个项目的拆解思路、核心表设计和踩过的坑一并整理出来给准备做类似系统或者正在钻研前后端分离的同学当个参考。1. 项目整体设计与技术选型思路1.1 为什么是SpringBoot2 Vue3而不是其他组合前端用Vue3现在基本是共识了组合式API带来的逻辑复用能力提升是实打实的。特别是在家政服务平台这种场景服务卡片、预约表单、订单状态流转、评价组件这四大块要同时处理大量交互状态。拿服务卡片来说不同家政服务类型保洁、保姆、月嫂、维修有不同的服务项配置和定价策略如果用Vue2的选项式API逻辑分散在data、methods、computed里代码会很快膨胀到500行以上维护起来很痛苦。Vue3的setup语法配合Composition API可以把一个服务卡片的拉取、筛选、收藏、弹窗下单预约整个流程封装成一个独立的组合式函数useServiceCard不只这个项目能复用后续开新模块也能直接搬过去。后端框架选择SpringBoot2而不是SpringBoot3按我个人的经验来看是因为目前主流的中小企业线上环境稳定运行版本以及大量第三方中间件的兼容性适配仍然以SpringBoot2为主而且很多企业内部封装好的公共组件库和文档都是基于SpringBoot2做的如果选用SpringBoot3光处理各种starter的新版兼容性就会耗费不少时间。这个项目选SpringBoot2.6.x版本刚好能兼顾稳定性和特性需求。1.2 MyBatis-Plus在业务开发中的真正价值MyBatis-Plus大部分人只拿它当单表CRUD工具用其实放在家政平台这种业务场景里它的价值远不止这点。先看用户订单统计这个需求你要统计每个用户在不同状态下的订单数量以及按服务类型分组统计收入这种聚合操作如果手写JDBC代码不仅代码量大而且容易在字段映射层面出问题。用MyBatis-Plus的Wrapper机制加上自定义SQL注解可以在保持SQL可读性的前提下把聚合逻辑集中管理。持久层设计还得考虑分页插件、自动填充、逻辑删除这三个痛点。分页插件配置好之后前端传页码和页大小后端统一封装分页结果集自动填充负责插入更新时自动写入create_time、update_time字段避免每个Mapper都重复设置逻辑删除加在订单和用户表上防止直接物理删除导致后续对账排查问题。这三块配置合并下来至少能省下200行以上的重复代码。1.3 为什么数据库必须选MySQL8.0MySQL8.0相比5.7对开发友好度提升是显著的。最直观的是窗口函数和公共表表达式做排名统计、分组TOP-N这类需求写一套SQL就能解决不需要在业务代码里做多次查询和内存计算。比如平台要推“金牌阿姨榜”按照评分、接单量、好评率三个维度排序用窗口函数一条SQL就能算出来。MySQL8.0的默认字符集是utf8mb4天然支持emoji表情和特殊字符存储。家政服务评价和留言里用户会发各种表情如果数据库用的是5.7且没改字符集入库直接乱码。此外MySQL8.0的事务处理能力也有了明显提升特别是在高并发场景下对订单创建、支付回调这种需要强一致性的操作稳定性更有保障。不过要注意8.0的内存占用比5.7高一些如果是小型服务器特别是1G内存的实例需要适当调整缓冲池大小亲测有效。2. 数据库设计与核心表结构2.1 用户、阿姨、订单三大核心主体的表设计先看用户表的设计除了基本的id、用户名、密码、手机号之外我额外加了两个字段user_type用户类型和status账号状态。user_type用于区分是普通雇主还是平台管理员status则是控制账号是否被封禁。密码字段用的不是明文而是BCrypt加密后的密文长度设置成64位避免后期加密算法升级导致字段不够用。阿姨表是整个平台的核心资产。除了基础信息以外我还专门设计了两个体重磅字段service_categories和skills。service_categories存的是阿姨能提供的服务类型ID列表比如101代表日常保洁102代表育儿嫂103代表养老护理skills存的是技能标签比如会说普通话、会做辅食、有健康证。这种设计初期查询靠MySQL的LIKE和FIND_IN_SET等平台阿姨量级到十万以上就需要把这些字段单独拆成标签关联表这一点初期设计要留好空间。价格策略上按服务类型分别存price字段比如保洁按小时付费月嫂按月付费育儿嫂按天付费。订单表是业务核心中的核心。订单号用年月日时分秒用户ID后四位随机数生成保证全平台唯一订单状态用tinyint表示1待支付、2待开始、3服务中、4待评价、5已完成、6已退款、7已取消服务地址直接冗余了一份在订单表里而不是通过关联地址表ID去查询。这个冗余设计是经验之谈订单生成后用户可能修改地址或者地址记录被清理如果订单只是存ID后续打印服务工单或做纠纷仲裁时需要翻原来的地址数据万一原记录被用户删除了后台就啥也看不到了。2.2 订单流程设计中的状态机与并发控制订单状态流转是整个系统里最容易出bug的地方。比如支付回调到达时订单已经取消了、派单时阿姨已经被别人预定了这些都是并发和状态不一致问题。我采用的方案是状态机乐观锁结合。具体做法是在订单表加一个version字段每次更新订单状态时都在WHERE条件里带上version当前版本号比如“UPDATE order SET status2, versionversion1 WHERE idxxx AND status1 AND version2”。这样即使两个请求同时更新同一个订单也只有一个能成功另一个更新后返回受影响行数为0直接抛出异常提示操作失败从源头阻止了超卖和状态错乱。预约时间冲突是另一个必须处理的并发问题。一位家政阿姨同一时间段只能接一单至少在创建订单的时候就得用SQL查询检查该阿姨在所需时间段内是否已有重叠订单。但严格来说仍存在并发窗口如果同一秒有两个请求同时查都发现阿姨空闲然后都创建订单就会超卖。稳妥的办法是在阿姨排期表上加一个唯一索引比如UNIQUE KEY(worker_id, service_date, service_time_slot)插入排期时如果主键冲突就说明已经被抢定了直接提示用户换一个时间段。数据库层面我用一张单独的服务排期表存阿姨在某一天是否可预约可预约则生成时段记录为了支持跨天服务的阿姨额外设计了start_date和end_date检测冲突时就判断两个区间是否有交集用SQL条件或者时间范围扫描都能实现。2.3 数据库设计踩过的3个坑坑一int类型存手机号导致查询慢。手机号是11位数字超过int的取值范围实际13位长的数字类型存进去尾部会被替换成0查询不出正确结果。后来统一改成char(11)查询效率和存储都变好了。坑二时间字段用datetime还是timestamp。一开始图省事全用datetime但datetime不处理时区如果前端做定时提醒时需要传UTC时间转换起来很麻烦。后来把涉及提醒的字段改成timestamp它存储时自动转为UTC读取时又按会话时区转换彻底杜绝了时区转换问题。坑三订单表忘记建复合索引。上线第一周订单量小没感觉第二周单量上来之后查询“某用户的历史订单”和“某阿姨的待接订单”时响应时间直接翻倍。加了userIdstatus复合索引和workerIdstatus复合索引之后两条核心查询降到毫秒级。3. 后端核心功能落地与实现3.1 SpringBoot2 项目初始化与关键配置创建一个SpringBoot2项目现在很方便Spring官方脚手架就能拉到一个基础工程。但有两处关键配置需要特别注意。第一处是yml文件里关于Jackson的配置。Vue3前端默认接收和返回的JSON数据格式是标准驼峰命名而数据库表字段习惯用下划线命名这中间的映射由MyBatis-Plus的map-underscore-to-camel-case自动处理但JSON序列化方面必须设置spring.jackson.date-format和spring.jackson.time-zone否则传到前端的时间会是时间戳格式页面还得单独做转换。我实际配置如下spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8第二处是MyBatis-Plus的分页插件配置这个很容易被忽略因为代码里不加也不会报错但传了分页参数不生效。分页插件作为MybatisPlusInterceptor的一个内部拦截器要手动装配到配置类里。Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }3.2 用户登录鉴权与JWT接入用户鉴权选的是JWT方案无状态、支持跨域、适合前后端分离。用户登录成功后后端生成一个token里面带上userId和userType前端存到localStorage每次请求在请求头里带上Authorization: Bearer token。后端写一个拦截器对所有需要登录的接口先校验token解析出来的userId放到ThreadLocal里当前操作的业务代码直接从中取用户身份省去每次从数据库查用户的动作。具体实现时我用了2.6.x版本的spring-security框架配合jjwt库自定义了一个TokenAuthenticationFilter正常流程是用户提交用户名密码走登录接口用户名密码校验通过后调用JwtUtils生成token后续访问受保护接口时过滤器解析token拿到userId后注入到SecurityContext。需要注意token过期时间不能设太长我一般是2小时过期后前端捕获到401状态码清除本地token并跳转登录页让用户重新登录。如果要更顺滑的体验可以额外设计一个refresh_token实现无感刷新但会增加复杂度小项目先不做。3.3 预约下单流程逻辑推演预约下单是业务最核心的接口。前端传过来服务类型、预约开始时间、服务时长、阿姨ID、地址ID、备注后端要做的事情按顺序来校验阿姨在指定时间是否空闲查排期表如果时间段已经被别的订单占了就直接返回“该时段已被预约”。根据服务类型和时长算总价后台黑名单用户或优惠券活动期间计算优惠券抵扣金额。创建订单状态置为待支付订单号按前面说的规则生成。锁定阿姨的排期在排期表插入一条记录公司用唯一索引守护防止其他用户再订。记录一条订单操作日志内容如“用户xxx下单成功待支付”。这里我特别强调一下幂等性问题。如果前端用户手快一秒钟点了两次“提交预约”就会产生两条一模一样的订单。加一个前端按钮loading只是基础方案后端兜底方案是在创建订单前先查一下在当前用户、当前阿姨、当前服务时间范围内是否已有待支付的同内容订单有就直接返回已有订单信息不再重复创建。更严格的方案是引入事务消息或分布式锁但对单机应用来说上述的查重唯一索引已经能够应对绝大部分场景了。3.4 支付模块集成经验这部分会引入第三方支付做出来之后效果很直观。我的方案是先调用支付接口生成一个支付二维码把二维码内容返回给前端展示用户拿手机扫码完成支付码支付。后端需要一个回调接口接收支付结果通知回调里验签成功后同步修改订单状态为待开始给阿姨推送“您有新订单”的短信。支付模块的三个关键点注意回调接口需要用普通HTTP而不是HTTPS很多内网联调环境没有域名证书调试期来回切换比较困扰但有条件还是得HTTPS。回调处理要求幂等回调通知可能触发多次要根据订单号和交易号去重重复通知不能重复改状态。回调必须在极短时间内返回“成功”否则支付平台会重试多次通知导致系统资源被刷爆。可以先收到通知后快速返回成功然后异步更新订单状态。但注意异步更新前最好做一次订单当前状态判断避免出现已取消订单又被支付成功。4. Vue3 前端实现与接口联调4.1 前端工程结构与路由设计前端工程用Vite Vue3 Pinia Vue Router Element Plus搭建。Vite的启动速度比Webpack快好几倍开发体验提升非常明显。脚手架创建项目之后按照功能模块划分目录结构对于家政平台比较清晰的做法是src/api所有与后端交互的接口定义集合按模块拆分成user.js、order.js、worker.js、comment.jssrc/views页面组件首页、服务列表、阿姨详情、订单中心、个人中心、后台管理等src/components公共组件比如服务卡片、预约表单、订单状态标签、星级评价组件src/storePinia状态管理store/user.js存用户信息token、登录状态store/order.js存下单流程中的临时数据路由设计需要考虑的是权限控制。普通用户只能访问首页、服务列表、阿姨详情、自己的订单中心平台管理员要能访问后台管理页面比如阿姨审核、订单管理、用户管理、数据统计。前后端权限校验都做前端通过路由守卫控制页面可见性后端通过JWT和拦截器控制接口可访问性。双保险的意义在单靠前端控制用户直接在地址栏输入路径都能绕过单靠后端控制前端会暴露一堆无权限按钮体验太差。路由守卫的具体写法是在router.beforeEach里取当前用户的userType如果目标路由的meta.requiresAdmin为true但用户不是管理员直接重定向到首页并给出无权限提示。4.2 Pinia在订单状态管理中的应用订单中心页面的交互复杂度高比如订单列表展示、订单详情抽屉、取消订单弹窗、支付弹窗、评价弹窗多个弹窗之间还会有联动。如果用组件自己管理数据每个弹窗都会请求一遍订单详情再展示会有多个请求在同时进行状态容易错乱。用Pinia定义了一个useOrderStorestate里放当前选中的订单详情、操作类型cancel/pay/comment、操作是否进行的loading状态。操作方法里定义cancelOrder、payOrder、submitComment三个action组件里只负责调用store里的action不在组件内部写具体业务逻辑。这样做的好处是一个弹窗提交完状态后所有用到该订单数据的组件都会同步更新展示避免了兄弟组件之间的状态同步地狱。登录状态也放在store里维护每次接口请求在axios拦截器里带上token如果响应401就清理store里的用户信息并跳转登录页。axios请求封装是每个前端项目的必修基础课需要统一处理错误码统一展示后端返回的msg字段比如“阿姨已被预约请选择其他时段”并且统一控制loading。4.3 前后端联调环境配置与跨域处理开发环境前后端分离跑在两个端口上前端是5173后端是8080必然出现跨域问题。我常用两种方案一种是后端配置CorsFilter允许指定来源跨域另一种是前端Vite配置devServer.proxy代理让前端请求时路径带/api前缀Vite帮转发到8080端口。实际项目里我更倾向于用第二种nginx方向的方案因为生产环境部署时前端和后端在Nginx里做反向代理仍然需要配置/api转发规则开发环境和生产环境统一了路径设计切换环境只改代理地址不容易因为跨域遗漏导致上线后连续报错。联调过程中最容易出问题的不是接口数据格式错误而是前后端对响应格式的约定不一致。我的习惯是用统一的结构下表列出了前后端约定字段名类型说明codeInteger状态码200成功401未登录403无权限500服务器错误messageString信息描述成功或错误的具体文本dataObject/Array业务数据成功时返回具体对象或列表后端用RestControllerAdvice统一捕获业务异常和系统异常前端在axios响应拦截器里统一处理确保无论从哪个接口返回错误前端提醒弹窗是一致的。4.4 移动端适配与小程序方向的延伸家政平台的用户很大一部分会从手机浏览器或微信内打开页面。所以页面设计一开始就考虑移动端适配Element Plus的栅格系统加响应式断点服务列表在手机上是一列卡片在PC上是三列或四列。特别是预约时间选择手机上要展示成可横向滚动的日历组件PC上则用传统的下拉选择框这两种交互维度体验差异很大需要共用同一套接口但前端做不同适配。如果有在小程序端扩展的打算后端接口在设计RESTful风格时就保证全部走HTTPJSON前端只需换一个适配层业务逻辑可以复用。我做的订单状态流转和评价接口天然适配小程序不需要改后端SQL逻辑。5. 常见问题排查与避坑指南5.1 后端启动报错的5个高频问题这里汇总了我实操时遇到最多、搜索引擎提问最多的情况。报错现象根本原因解决办法启动直接退出提示Failed to configure a DataSource没有配置数据库连接信息或yml里配置了但URL写错检查application.yml确保spring.datasource.url、username、password都正确Access denied for user数据库账号密码错误或该账号没有远程访问权限MySQL命令行登录后执行授权授完FLUSH PRIVILEGESUnknown database数据库还没有创建CREATE DATABASE并保证服务名不写错Table xxx doesn‘t exist没有执行sql脚本用项目自带的sql文件导入导入时确认选了正确的数据库端口被占用8090端口被某个进程占用了lsof -i:8090或netstat -ano找到对应PID杀掉即可5.2 前端运行时的几个经典问题第一个是跨域配置好了但还是请求失败。检查浏览器的Network面板看请求有没有真正发出如果请求是OPTIONS开头说明后端CORS配置没有正确允许预检请求。需要设置corsConfiguration.addAllowedMethod()和addAllowedHeader()。第二个是token失效后页面还在展示旧数据。原因是接口报401后前端没有清理本地token和用户信息Store里还存着旧状态。务必在axios响应拦截器的401处理里同时调用logout方法清理数据并跳转登录页。第三个是打包部署后刷新页面404。Vue3是SPA应用路由是history模式刷新时Nginx要配置try_files否则真实路径找不到解决方法是Nginx配置里加try_files $uri $uri/ /index.html。第四个是Element Plus组件按需引入报错。如果没有用unplugin-vue-components自动导入手动按需引入时容易漏了样式文件导致组件没样式。我建议直接把Element Plus全量引入做后台管理系统完全够用省心省时。5.3 数据一致性保障策略家政平台的订单和阿姨排期是一组强一致数据。事务切面需要覆盖从创建订单到锁定排期的完整链路任一步操作失败前面所有步骤全部回滚避免出现订单存在但阿姨排期没锁上的状态或排期锁上了但订单创建失败的情况。用Transactional注解之前先确认两点第一事务方法是public方法并且是通过对象调用的不能是本类方法自调用否则事务会失效第二事务里不要包含feign调用或外部RPC等同步阻塞操作容易挂死。5.4 其他高频小坑MyBatis-Plus逻辑删除字段的坑逻辑删除字段加TableLogic注解后查询时还会默认自动带上deleted0条件这在统计表里几乎无效需要在SQL里手动指定别名或忽略该字段。自动填充生效时机createTime是insert时自动填充updateTime是insert和update都自动填充如果只配置了insert策略更新记录时就不会刷新时间。大文本字段查询慢评价表里content是长文本查询列表时select *会把大字段全部加载出来拖慢速度列表查询时只select id, orderId, userId, score等必要字段详情接口再完整查询。6. 项目部署与上线清单6.1 前端Nginx部署配置前端Vue项目打包后是dist目录部署步骤如下服务器安装Nginxroot路径指到dist目录。配置server块监听80端口server_name写域名或服务器IP。location /样式处理try_files配置解决history路由刷新404问题。location /api/配置反向代理转发到后端8080接口地址。给一个可以直接改的Nginx配置参考server { listen 80; server_name your-domain.com; location / { root /usr/share/nginx/html/dist; index index.html; 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; } }6.2 后端Jar包部署SpringBoot2项目打出来的Jar包用java -jar命令启动即可。上线环境我建议做三件事把yml配置文件放入外部config目录用多环境配置方式区分dev和prod方便日后修改配置不需要重新打包。配置开机自启动用systemd管理服务日志重定向到单独的log文件。配置MySQL字符集utf8mb4排序规则utf8mb4_unicode_ci避免中文和表情乱码。外部config目录的具体做法是把application.yml放在jar包同级目录下新建的config文件夹里SpringBoot启动时外部配置文件优先级高于内置配置覆盖内部的相同配置项非常实用。6.3 HTTPS证书与访问安全线上环境务必配置HTTPS证书现在申请免费证书很方便。Nginx配置好证书之后再把HTTP请求301重定向到HTTPS用户输入http://域名会自动跳到https://域名。还有就是REST接口的访问控制后台管理接口要增加IP黑白名单限制支付回调接口要验签日志文件要定期轮转避免日志把磁盘打满。7. 写在最后这套项目从零到一跑通我最有体感的一点是千万别为了炫技引入复杂框架。SpringBoot2 Vue3 MyBatis-Plus MySQL8.0这套组合扎实可靠能处理数据量在百万级以下的家政业务场景而且每个组件都有大量成熟文档和社区经验可用遇到问题查起来有抓手。如果你正在照着这个项目学习建议不要停留在“能跑就行”可以去思考几个进阶方向订单表的分库分表策略、阿姨推荐算法的引入、评价内容的敏感词过滤、服务价格的动态调整。把这些点做深才真正吃透一个项目。最后分享一个我踩过的小坑开发阶段一定要用UTF-8编码保存代码文件否则注释里的中文会乱码排查起来极其痛苦。祝大家都能顺利完成自己的项目有技术上的问题欢迎交流讨论。
阅读完成 · 觉得有帮助?
咨询建站