SpringBoot+Vue+MyBatis+MySQL宠物上门服务系统解析
我前阵子接了个同城上门喂遛宠物的项目最终选型就是标题里这套组合SpringBoot扛后端、Vue做前端、MySQL存数据、MyBatis负责持久层Java作为主力语言把整条链路串起来。这套系统做完之后从宠物主下单、服务人员接单、上门喂养/遛狗、到订单完成和评价结算整个业务流程都跑通了代码也完整整理成了可复现的源码包。这篇文章就把这套系统的设计思路、技术选型逻辑、核心模块实现、数据库设计、部署流程和踩坑记录全部聊透给打算做同城服务类管理系统或正在学前后端分离项目的人一个可以照着抄的完整参考。1. 项目概述与需求拆解1.1 这个系统到底在解决什么问题做项目之前我特意去看了几家用过上门喂遛服务的宠物主的反馈。出差三天、回老家过节、加班到深夜家里猫主子没人铲屎、狗子没人遛这是非常真实且高频的痛点。线下找熟人帮忙总欠人情找宠物店寄养又贵又不放心所以同城上门喂遛这个细分服务这几年涨得很快。但这类服务想要规模化运营光靠微信群接单完全不够。宠物主人需要看到服务人员的资料和评价服务人员需要知道今天几点去谁家、宠物有什么习惯、门锁密码是多少平台方需要管理订单状态、跟踪服务进度、处理售后纠纷。这背后就是一套完整的管理系统用户端操作界面、服务端的业务逻辑、管理后台的审核与统计。所以这套系统的定位很清晰面向同城宠物服务平台的业务管理系统核心价值是把下单-接单-服务-结算-评价这条链路数字化让每个角色都在系统里完成自己的动作所有数据可查、可追溯。1.2 角色与业务链路拆解这类系统涉及三类核心角色我在设计的时候先画了一张角色权限地图角色核心诉求关键操作宠物主用户端快速找到靠谱服务、随时查看服务进度注册登录、维护宠物档案、下单、支付、评价服务人员接单端高效接单、规划路线、获取服务详情接单/抢单、查看服务订单、提交服务记录平台管理员管理端审核资质、处理纠纷、掌握平台运营数据用户管理、订单管理、服务分类管理、数据统计业务链路我梳理成一条主线宠物主创建宠物档案选择需要的服务类型上门喂养/遛狗/两者组合填写服务时间和地址系统根据距离和档期分配服务人员服务人员上门后拍摄照片和记录服务情况宠物主确认完成平台完成结算和评价闭环。这里有个容易被忽略的点上门喂遛跟普通外卖订单不一样它涉及进入他人住宅这个动作所以订单详情里需要包含宠物生活习惯、紧急联系人、门锁密码等敏感信息。这些信息在订单完成后要做脱敏处理不能长期明文保存。我在项目里给这些字段加了加密存储和定时清理的机制这也是这类垂直场景跟通用电商系统拉开差距的地方。1.3 功能模块清单把这套系统的功能做一次完整拆解按端划分用户端账号体系手机号注册登录、微信授权登录预留、个人资料维护宠物档案管理多宠物维护、品种/年龄/体重/性格标签、疫苗接种记录服务下单选择服务类型、预约时间、填写地址、选择服务人员或自动分配订单追踪实时查看订单状态、服务人员位置预留、服务动态支付与评价在线支付预留或模拟、服务完成后的打分和文字评价服务端服务人员使用的端接单管理查看可接订单列表、一键抢单、档期管理工作台今日待服务列表、路线规划预留、服务记录上报收益中心已完成订单收入汇总、提现记录管理后台用户管理宠物主和服务人员的审核、封禁/解封订单管理全平台订单查询、异常订单介入、退款处理内容管理服务类型配置、城市区域配置、价格系数设置数据统计订单量趋势、服务完成率、用户增长、营收报表功能齐了之后项目才有资格说是一个系统而不是一个简单的前后端页面拼凑。2. 技术选型解析为什么是SpringBootVueMyBatisMySQL2.1 后端框架选型后端框架我几乎没有纠结直接选了SpringBoot。理由很简单开发效率最高生态最成熟。相比传统的SSMSpringSpringMVCMyBatis需要手动配置一大堆XMLSpringBoot用自动配置和起步依赖把框架整合的成本降到了最低。举个例子想在项目里用MySQL和MyBatis传统做法是引入一堆依赖、配置数据源、配置SqlSessionFactory、配置Mapper扫描稍有不慎就报错。SpringBoot里只需要引入spring-boot-starter-web、mybatis-spring-boot-starter和mysql-connector-java在application.yml里写几行配置就可以直接跑起来。版本上我建议SpringBoot 2.7.x不要盲目追最新的3.x。因为3.x基于JDK 17很多企业服务器和教学环境还在JDK 8而且很多第三方starter对3.x的适配还不到位。我这套源码就是基于SpringBoot 2.7.x写的在JDK 8环境下能直接编译运行兼容性最稳。2.2 持久层为什么用MyBatis而不是JPAMyBatis和JPA之争在Java圈吵了很多年我的态度很明确业务型管理系统、SQL逻辑复杂、需要精细优化的场景选MyBatis更顺手。这套系统里订单查询、服务人员接单统计、多表联查非常多MyBatis可以把SQL完全掌控在自己手里动态SQL用where、if、foreach标签就能优雅解决条件拼装问题。而JPA的自动建表和Hibernate的延迟加载行为在复杂业务下经常出现意外查询排查起来很痛苦。当然如果让我自己新起一个纯CRUD后台项目我大概率会用MyBatis-Plus单表操作几乎零SQL。但这套源码既然定位是学习型项目用原生MyBatis反而能让读者看清楚SQL是怎么写的、Mapper怎么跟XML对应学到的东西更多。2.3 前端框架选型前端选Vue也是基于同样的逻辑上手曲线平缓、中文社区资料多、中小型管理系统开发效率极高。Vue的单文件组件把HTML、CSS、JavaScript聚合在一个文件里维护起来比jQuery时代舒服太多。配合Vue Router做路由管理Vuex/Pinia做全局状态管理Element UI或Vant做UI组件库一个管理系统半个月就能成型。版本上我建议Vue 3如果读者电脑里还是老项目模板用Vue 2.6也能跑这套源码兼容两者。区别在于Vue 3彻底拥抱Composition API逻辑复用更清爽新项目没必要再抱着Options API不放。2.4 整体架构组合的逻辑SpringBootVueMyBatisMySQL这个组合不知道被多少项目验证过了它最大的优势不是单点技术最强而是组合以后的学习成本低、招聘市场认可度高、出问题能找到的人多。前后端分离架构下前端和后端通过JSON格式的RESTful API通信开发时两个人可以并行推进我负责后端接口另一个同学负责前端页面只要把接口文档定好两边几乎不需要来回扯皮。部署时前端打包成静态文件丢给Nginx或直接放进SpringBoot的static目录后端打成jar包跑在服务器上资源占用低一台小云服务器就能支撑一个小型平台的流量。这个组合还有一个隐形的价值对计算机专业的学生或转行开发者来说它是就业市场的基本盘技能。做毕业设计也好做个人作品集也罢这套技术栈写进简历面试官不用看代码就知道你做过什么。3. 数据库设计一张表都不能少3.1 核心实体与关系数据库设计我花的时间比写代码还多。这系统的核心业务链路是围绕订单展开的订单关联用户、宠物、服务类型、服务人员、地址、评价等多个实体。我把核心实体关系梳理成如下结构用户表user)平台所有账号的统称通过role字段区分宠物主和服务人员一表多用避免建两套账号体系。宠物表pet)归属于宠物主一个宠物主可以养多只宠物一对多关系。字段涵盖宠物基本信息、性格标签、生活习惯。服务类型表service_type)配置平台提供哪些服务如上门喂养、遛狗、喂养遛狗组合以及对应的计费规则。订单表order)核心业务表记录谁在什么时间、什么地点、需要什么服务、指派给谁、状态如何。订单服务明细表order_service_item)一个订单可能包含多个服务项目比如同一只猫需要喂养铲屎拆成明细方便核算。评价表comment)宠物主对服务人员和整个服务过程的评分与文字反馈。地址表address)宠物主维护常用地址下单时直接选用避免每次都手填。设计关系时有一个关键点订单表不要只存关联ID要存必要的信息快照。比如订单里要冗余一份宠物昵称、服务地址和宠物主手机号而不是全部通过join去查。原因是服务发生后用户可能修改了宠物档案或换了手机号如果订单表不存快照历史订单信息就会漂移——你看到的是用户现在的宠物名而不是那次服务时的。这在售后场景里非常致命。3.2 关键表字段设计拿订单表来举例我把核心字段列出来并说明设计理由字段名类型说明idbigint主键自增order_novarchar(32)业务订单号前端展示和客服沟通用不能直接用自增IDuser_idbigint下单宠物主IDserver_idbigint服务人员ID接单后写入pet_idbigint宠物IDpet_snapshotvarchar(500)宠物信息快照下单时的宠物名/品种/体重等address_snapshotvarchar(500)服务地址快照service_type_idbigint服务类型IDservice_timedatetime预约服务时间statustinyint订单状态用数字枚举amountdecimal(10,2)订单金额pay_statustinyint支付状态remarkvarchar(500)用户备注create_timedatetime下单时间update_timedatetime更新时间这里有个很容易犯的错订单号不要用数据库自增ID直接暴露给用户因为订单号会被用户用来咨询客服、打电话核对自增ID既短又容易被人试探出平台的日单量换成带时间戳和随机位的业务订单号更专业。3.3 订单状态机设计订单系统的核心难点是状态流转。我提前把整条链路上的状态定义清楚写代码时就不会东改一处西改一处状态枚举值触发动作说明待支付0用户提交订单订单创建后必须先支付才能进入后续流程待接单1支付成功平台展示给服务人员抢单已接单2服务人员接单锁定服务人员其他人不可抢服务中3服务人员开始服务到达现场开始执行喂/遛动作待确认4服务人员提交完成等待宠物主确认已完成5宠物主确认订单闭环进入结算和评价已取消6用户/平台取消取消原因记录备用退款中7售后发起管理员介入处理状态机设计的关键是每一步状态变更都需要记录操作人和操作时间。我在项目里加了订单日志表一笔订单从创建到完成的所有状态变化都会留痕这为后期处理纠纷提供了铁证。3.4 建表SQL示例这里给出订单表的核心建表SQL完整版本请参考源码里的sql目录CREATE TABLE order ( id bigint(20) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 业务订单号, user_id bigint(20) NOT NULL COMMENT 下单用户ID, server_id bigint(20) DEFAULT NULL COMMENT 服务人员ID, pet_id bigint(20) NOT NULL COMMENT 宠物ID, pet_snapshot varchar(500) DEFAULT NULL COMMENT 宠物信息快照, address_snapshot varchar(500) DEFAULT NULL COMMENT 地址快照, service_type_id bigint(20) NOT NULL COMMENT 服务类型ID, service_time datetime NOT NULL COMMENT 预约服务时间, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 状态0待支付 1待接单 2已接单 3服务中 4待确认 5已完成 6已取消 7退款中, amount decimal(10,2) NOT NULL COMMENT 订单金额, pay_status tinyint(4) NOT NULL DEFAULT 0 COMMENT 支付状态0未支付 1已支付 2已退款, remark varchar(500) DEFAULT NULL COMMENT 用户备注, create_time datetime NOT NULL COMMENT 创建时间, update_time datetime DEFAULT NULL COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id), KEY idx_server_id (server_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT宠物服务订单表;索引设计上order_no建唯一索引所有查询条件里常用的user_id、server_id、status都分别建普通索引。生产环境单表数据量大了之后再考虑分表或归档但现在这个规模完全够用。4. 后端SpringBoot核心实现4.1 项目分层结构后端工程我严格按照经典三层架构组织com.petcare ├── controller // 接口层接收请求、参数校验、返回统一结果 ├── service // 业务层核心业务逻辑都在这里 │ └── impl // service实现类 ├── mapper // MyBatis Mapper接口声明数据库操作方法 ├── entity // 数据库实体类 ├── dto // 前端交互数据传输对象 ├── config // 配置类拦截器、跨域、全局异常处理 ├── common // 公共类统一返回结果、状态码、常量、工具类 └── PetApplication.java // SpringBoot启动类这个分层的意义在于可维护性和可测试性。Controller只做参数接收入参校验不写任何业务代码Service层专注业务规则比如订单状态是否允许流转、接单时是否有并发冲突Mapper层只做数据库读写。这样改业务逻辑时不会动到接口定义换数据库实现时也不会动到业务代码。统一返回结果类用泛型定义public class ResultT { private Integer code; private String msg; private T data; // 省略 getter/setter public static T ResultT success(T data) { return new Result(200, success, data); } public static T ResultT error(Integer code, String msg) { return new Result(code, msg, null); } }这样前端拿到的所有接口响应格式都是统一的{code: 200, msg: success, data: {...}}前端axios拦截器只需要判断code是否为200无需每个接口单独处理异常。这个习惯一定要养成否则前后端联调时十有八九会乱。4.2 鉴权设计前后端分离下的JWT方案传统单体应用用Session保存登录态但前后端分离部署后前端可能跑在Nginx后端跑在另一台服务器Session跨域共享非常麻烦。所以我采用了JWTJSON Web Token方案做无状态鉴权。流程是用户登录成功后后端用秘钥生成一个JWT Token包含用户ID、角色、过期时间前端拿到Token后存在localStorage里每次发请求时放到Authorization请求头后端写一个拦截器拦截需要登录的接口校验Token是否有效解析出用户ID并放入请求上下文核心拦截器实现public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (StringUtils.isBlank(token)) { throw new BusinessException(未登录); } // 解析token校验签名和过期时间 Claims claims JwtUtil.parseToken(token.replace(Bearer , )); // 把用户ID放入request attribute后续从上下文取 request.setAttribute(userId, claims.get(userId)); return true; } }生成Token时用到了io.jsonwebtokenJJWT库秘钥放到配置文件里不要硬编码。过期时间我设为24小时后端还做了时间戳校验防止Token过期后还能通过修改前端时间绕过。这里提醒一个新手常踩的坑不要把用户的明文密码放进Token里。Token是在客户端存放的一旦被截获就等于泄露了用户身份。Token里只放用户ID、角色这些必要信息查询用户详情时再到数据库里取。4.3 订单核心流程实现订单是整个系统最核心的业务我以用户下单和服务人员接单两个关键动作为例拆解一下代码逻辑。用户下单的Service方法Transactional public Long createOrder(OrderCreateReq req, Long userId) { // 1. 校验用户是宠物主宠物属于当前用户 Pet pet petMapper.selectById(req.getPetId()); if (pet null || !pet.getUserId().equals(userId)) { throw new BusinessException(宠物信息不存在); } // 2. 生成业务订单号 String orderNo OrderNoGenerator.generate(); // 3. 组装订单实体写入宠物快照和地址快照 Order order new Order(); order.setOrderNo(orderNo); order.setUserId(userId); order.setPetId(req.getPetId()); order.setPetSnapshot(buildPetSnapshot(pet)); order.setAddressSnapshot(req.getAddress()); order.setServiceTypeId(req.getServiceTypeId()); order.setServiceTime(req.getServiceTime()); order.setStatus(OrderStatus.PENDING_PAYMENT.getCode()); order.setAmount(calculateAmount(req.getServiceTypeId(), req.getServiceTime())); // 4. 插入订单表 orderMapper.insert(order); return order.getId(); }Transactional注解保证创建订单过程中任何一个环节出错数据库操作全部回滚不会出现订单表有记录但日志表没有的脏数据。服务人员接单的逻辑要处理并发重点代码如下Transactional public void acceptOrder(Long orderId, Long serverId) { // 1. 查询订单 Order order orderMapper.selectById(orderId); if (order null || order.getStatus() ! OrderStatus.PENDING_ACCEPT.getCode()) { throw new BusinessException(订单不可接); } // 2. 更新订单状态把serverId写进订单 int rows orderMapper.updateStatusAndServer( orderId, OrderStatus.ACCEPTED.getCode(), serverId, OrderStatus.PENDING_ACCEPT.getCode() // 作为更新条件防止并发 ); if (rows 0) { throw new BusinessException(手慢了订单已被抢走); } }这段代码的精髓是乐观锁思路更新订单时把where条件加上当前状态值如果两个服务人员同时抢单只有一个人能更新成功另一个人更新的行数为0直接提示订单已被抢走。不用select后再update因为两步之间会有时间窗口并发下容易出问题。这里需要说明的是真正高并发环境下这种方案还能进一步优化比如引入Redis做分布式锁。但作为毕业设计或中小型系统数据库层面的乐观锁已经足够把复杂度控制在一个合理的范围内才是最佳实践。4.4 MyBatis动态SQL与常见坑点MyBatis最强大的能力就是动态SQL。以订单条件查询为例select idlistOrdersByCondition resultTypecom.petcare.entity.Order select * from order where if testuserId ! null and user_id #{userId} /if if testserverId ! null and server_id #{serverId} /if if teststatus ! null and status #{status} /if if teststartTime ! null and create_time gt; #{startTime} /if /where order by create_time desc /select这段SQL可以同时服务管理后台的订单列表、用户端我的订单、服务人员的待接单列表等多个场景只是传入条件不同。这就是MyBatis动态SQL的价值——一套SQL模板多场景复用。用MyBatis有几个坑必须提醒第一个坑是${}和#{}的区别。#{}是预编译参数占位符会生成?占位符防止SQL注入${}是字符串拼接会直接把内容拼进SQL里。动态排序、动态表名这些场景确实需要${}但绝对不能拿它拼接用户传过来的值否则就是SQL注入漏洞。第二个坑是Mapper接口和XML文件的绑定问题。如果出现Invalid bound statement (not found)报错九成是XML文件的namespace写错、Mapper接口的方法名和XML里的id对不上、或者接口和XML文件不在同一个包路径下。排查顺序先看namespace再看方法名最后看配置文件里的mapper-locations路径。第三个坑是查询结果映射。数据库字段是create_time下划线风格实体类字段是createTime驼峰风格必须在application.yml里开启全局配置mybatis: configuration: map-underscore-to-camel-case: true不开这个配置查询结果里凡是带下划线的字段全是null新手排查半天也找不到原因。5. 前端Vue实现要点5.1 页面结构与路由设计前端工程基于Vue CLI或Vite搭建目录结构如下src ├── api // 所有接口请求封装按模块拆分 ├── assets // 静态资源 ├── components // 公共组件 ├── router // 路由配置 ├── store // 全局状态管理 ├── views // 页面组件 │ ├── user // 宠物主端页面 │ ├── server // 服务人员端页面 │ └── admin // 管理后台页面 ├── utils // 工具函数 ├── App.vue └── main.js路由设计上核心是根据不同角色显示不同页面和菜单。宠物主端主要路由包括首页服务列表、宠物档案、下单页、订单列表、订单详情、个人中心服务人员端是工作台风格可接订单、我的接单、收益明细管理后台则是一套侧边栏布局。登录后根据用户角色用动态路由或路由守卫进行跳转控制。路由守卫的核心代码router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.meta.requiresAuth !token) { next({ path: /login, query: { redirect: to.fullPath } }); } else { next(); } });这个守卫的逻辑很简单凡是需要登录的页面本地没有Token就强制跳到登录页同时记录用户原本想访问的路径登录成功后跳回原路径体验更顺滑。5.2 核心组件设计与复用Vue开发的效率提升一半靠组件拆分。我在这套系统里抽了几个高频复用的组件宠物卡片组件PetCard.vue宠物主端的宠物档案列表、下单时的宠物选择、服务人员端的宠物信息展示都用它。props传入宠物对象内部展示宠物头像、昵称、品种、体重、性格标签点击事件通过emit抛给父组件。订单状态标签组件OrderStatusTag.vue订单状态在列表、详情、后台各处都会显示直接用一个组件封装状态枚举和标签颜色映射后续要调整状态文案改一个文件就搞定。倒计时组件CountDown.vue订单列表里等待支付的订单需要一个倒计时我写了一个纯前端的计时器组件用window.setInterval每秒刷新一次剩余时间时间到了自动触发父组件的取消订单动作。组件拆分的判断标准很简单这个结构块是否在多个页面出现如果出现了两次以上就值得抽成组件。不要为了抽组件而抽过度设计反而增加维护负担。5.3 axios封装与前后端联调整个前端的所有接口请求统一走一个axios实例封装在utils/request.js里import axios from axios; const service axios.create({ baseURL: /api, timeout: 15000, }); // 请求拦截器自动携带token 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) { if (res.code 401) { // 登录过期跳转登录页 localStorage.removeItem(token); window.location.href /login; } return Promise.reject(new Error(res.msg)); } return res; }, error { return Promise.reject(error); } ); export default service;突出的价值有两点第一每个接口不用重复写Token的代码第二后端返回统一状态码前端统一处理错误401跳登录、502提示服务器异常一个拦截器全部搞定。上面的baseURL写成/api配合前端的开发代理本地开发时把请求代理到后端服务避免跨域问题生产环境前端静态文件和后端jar在同一台服务器上Nginx直接把/api转发给后端端口接口路径天然一致。5.4 地图选点与时间选择交互上门喂遛服务的时间选择和地址选择是前端交互里体验提升最大的两个点。服务时间选择我用的是vue-calendar或者Element Plus的date-picker但限制了可选范围只能选未来7天内的时间按小时粒度选择。这个限制是业务决定的——服务人员需要提前安排档期用户也不能约太久远的时间。地址选择接入了高德地图JS API的定位组件用户在地址输入框里搜索地址选中的坐标回填到经纬度字段再逆解析出详细地址文本。这里有一个细节地图组件加载是异步的首次进入页面时容易出现地图还没加载完就调用方法的报错。我是用AMapLoader的方式在生命周期里先加载地图渲染完成后再绑定事件实测下来比较稳定。6. 源码启动与部署从0到1跑起来6.1 数据库初始化拿到源码后的第一步不是急着启动而是先把数据库建好。源码包里有一个sql目录里面是完整的建库建表脚本。在MySQL里执行CREATE DATABASE petcare DEFAULT CHARACTER SET utf8mb4;然后按顺序执行建表脚本和初始化数据脚本。初始化数据脚本里我放了几条测试数据两类测试用户宠物主和服务人员、几种服务类型、一条样例订单跑起来之后不需要自己手动造数据就能直接看到效果。MySQL版本建议用5.7或8.0实测都可以。8.0要注意时区设置连接串里需要加serverTimezoneAsia/Shanghai否则会报时区相关的异常。6.2 后端启动配置后端启动前只需要改一个文件application.yml。把数据库链接的用户名密码改成你自己的server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/petcare?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver然后通过IDEIDEA或Eclipse导入Maven项目等待依赖下载直接在PetApplication.java里右键Run启动。如果依赖下载慢建议配置阿里的Maven镜像几百兆的依赖包几分钟就能拉完。如果不想用IDE命令行也可以mvn clean package -DskipTests java -jar target/petcare-0.0.1-SNAPSHOT.jar启动成功后控制台会显示SpringBoot的Banner和Tomcat启动端口默认是8080。6.3 前端启动与代理配置前端的启动步骤更简单npm install npm run serve这里有个经验npm install时如果报权限或版本错误大部分是Node版本问题。Vue CLI 5建议Node 16以上Vite项目建议Node 18以上装一个nvm做Node版本管理能省掉大量折腾时间。本地开发时前端运行在localhost:8081通常是8081和8080的SpringBoot区分开访问后端接口会跨域。解决方案不是去后端写CORS虽然写了而是用vue.config.js里的devServer代理module.exports { devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } };这样前端页面所有/api开头的请求都会自动转发到后端8080端口浏览器看到的请求是同源的绕开了跨域问题。开发体验极其顺滑。6.4 打包部署两种主流方案项目开发完要部署上线有两种主流方案先说结论小项目直接用第二种省事。方案一前后端分开部署。前端npm run build生成dist目录丢到Nginx的html目录Nginx配置好静态文件服务并把/api请求反向代理到后端8080端口。后端打成jar包用nohup java -jar方式启动。这种方案适合前端、后端分开维护或各自独立扩容的场景是生产环境的标准姿势。方案二前端打包进SpringBoot。把前端dist目录里的文件整个拷贝到后端工程的src/main/resources/static目录下重新打jar包。启动jar后浏览器直接访问服务器的8080端口就能看到前端页面接口也能通。因为静态资源和API跑在同一个端口完全没有跨域问题。这是单体项目最简单的部署方式。我实际部署这套系统时用的是方案二一台2核4G的云服务器足够撑起日均百单的业务量运维只需要管一个jar进程省了Nginx的配置环节。如果后面流量上来了再拆成方案一也不迟。7. 常见问题与排查技巧实录7.1 前后端跨域问题现象前端页面在8081端口后端接口在8080端口浏览器控制台报Access-Control-Allow-Origin错误请求被拦截。排查思路跨域是浏览器同源策略导致的开发环境最优解是前端代理vue.config.js的proxy生产环境最优解是Nginx反向代理。如果后端确实需要支持跨域也可以写一个CORS配置类Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }但注意生产环境不要用allowedOriginPatterns(*)这种全放开配置指定可信来源更安全。7.2 Mapper接口与XML绑定失败现象项目启动或调用接口时抛org.apache.ibatis.binding.BindingException: Invalid bound statement (not found)。排查思路这类问题按顺序检查三个地方。第一application.yml里的mybatis.mapper-locations是否指向了正确的XML目录常见写法是classpath:mapper/*.xml第二XML文件里的namespace是否写成了Mapper接口的全限定名比如com.petcare.mapper.OrderMapper第三Mapper接口里的方法名和XML里的id是否完全一致。我在初学MyBatis时在这个问题上卡了一整天最后发现只是方法名一个字母的大小写不对。7.3 MySQL 8.x时报时区错误现象启动后端时控制台报The server time zone value Öйú±ê׼ʱ¼ä is unrecognized。原因MySQL 8.x的连接驱动要求明确指定时区不指定就用系统默认值容易乱码。解决在数据库连接串里加参数jdbc:mysql://localhost:3306/petcare?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai顺便提醒字符集参数也不能少否则中文存进去全是乱码这属于MySQL连接里最经典的三个参数。7.4 前端页面刷新后404现象把前端打包进SpringBoot静态目录后首页能打开但刷新某个子路由时返回404。原因前端路由用的history模式刷新时浏览器直接拿着/order/detail/3这样的路径去请求服务器SpringBoot的静态资源处理器里没有这个路径自然就404了。解决如果是方案一Nginx部署在Nginx配置里加一行try_files $uri $uri/ /index.html;。如果是方案二打进SpringBoot需要写一个转发规则把所有非api且非静态资源的路径转发到index.html。说实话如果不想折腾前端路由直接改hash模式最简单URL里多个#但刷新永远不会有问题小项目够用。7.5 并发场景下的重复接单现象两个服务人员同时抢同一笔订单两个人都显示接单成功然后用户被两个同时上门的服务人员堵在门口。原因常规的先查询状态再更新状态模式在并发下存在竞态条件。我在4.3里已经给了解法就是更新时把原状态作为where条件用受影响行数判断是否更新成功。这个方案在并发量不太高的小型系统里非常稳定代码也最好懂。7.6 问题速查表问题现象可能原因快速解决方案接口登录后仍提示未登录Token没传或拦截器顺序错误检查前端请求拦截器检查后端拦截器注册路径数据库中文乱码连接串没加字符集参数连接串加characterEncodingutf8查询结果字段为null没开启驼峰映射map-underscore-to-camel-case: true前端接口数据为undefined后端返回字段和前端取用字段不一致对比JSON结构检查DTO字段命名Vue启动报Node版本不支持本地Node版本过低或过高用nvm切换Node 16/18服务人员接单后赔款订单状态并发保护缺失参考4.3里的乐观锁更新方式打包后前端样式丢失静态资源路径配置错误SpringBoot部署时static目录层级核对尾声一些个人实践后的体会这套系统从订单状态机设计到前后端联调完整走完一遍之后我最大的收获不是哪些技术点学会了而是对一个业务系统从0到1的整体掌控感。做之前我以为难点在后端接口做的时候才发现大部分时间花在需求梳理、数据库设计、联调排错上——这跟面试题里刷的算法完全不同它是真实的工程问题。如果你准备拿这套源码做毕业设计或者面试项目我建议你拿到后不要急着跑先花两天时间把表结构读懂再把订单创建到完成的状态流转走一遍最后挑一个自己感兴趣的模块改改加加比如接单时加个短信通知、给订单列表加个筛选导出功能。能说出我改了什么、为什么这么改项目就真正变成你自己的了。有时间的话还可以顺着服务完成后打赏小费、按月统计服务人员收益排行这些思路继续扩展这套架构完全撑得住。