☰
SpringBoot+Vue+MyBatis+MySQL宠物上门服务系统解析
2026/10/7 5:12:00 网站建设 项目流程

我前阵子接了个同城上门喂遛宠物的项目,最终选型就是标题里这套组合:SpringBoot扛后端、Vue做前端、MySQL存数据、MyBatis负责持久层,Java作为主力语言把整条链路串起来。这套系统做完之后,从宠物主下单、服务人员接单、上门喂养/遛狗、到订单完成和评价结算,整个业务流程都跑通了,代码也完整整理成了可复现的源码包。这篇文章就把这套系统的设计思路、技术选型逻辑、核心模块实现、数据库设计、部署流程和踩坑记录全部聊透,给打算做同城服务类管理系统或正在学前后端分离项目的人一个可以照着抄的完整参考。

1. 项目概述与需求拆解

1.1 这个系统到底在解决什么问题

做项目之前,我特意去看了几家用过上门喂遛服务的宠物主的反馈。出差三天、回老家过节、加班到深夜,家里猫主子没人铲屎、狗子没人遛,这是非常真实且高频的痛点。线下找熟人帮忙总欠人情,找宠物店寄养又贵又不放心,所以同城上门喂遛这个细分服务这几年涨得很快。

但这类服务想要规模化运营,光靠微信群接单完全不够。宠物主人需要看到服务人员的资料和评价,服务人员需要知道今天几点去谁家、宠物有什么习惯、门锁密码是多少,平台方需要管理订单状态、跟踪服务进度、处理售后纠纷。这背后就是一套完整的管理系统:用户端操作界面、服务端的业务逻辑、管理后台的审核与统计。

所以这套系统的定位很清晰:面向同城宠物服务平台的业务管理系统,核心价值是把"下单-接单-服务-结算-评价"这条链路数字化,让每个角色都在系统里完成自己的动作,所有数据可查、可追溯。

1.2 角色与业务链路拆解

这类系统涉及三类核心角色,我在设计的时候先画了一张角色权限地图:

角色核心诉求关键操作
宠物主(用户端)快速找到靠谱服务、随时查看服务进度注册登录、维护宠物档案、下单、支付、评价
服务人员(接单端)高效接单、规划路线、获取服务详情接单/抢单、查看服务订单、提交服务记录
平台管理员(管理端)审核资质、处理纠纷、掌握平台运营数据用户管理、订单管理、服务分类管理、数据统计

业务链路我梳理成一条主线:宠物主创建宠物档案,选择需要的服务类型(上门喂养/遛狗/两者组合),填写服务时间和地址,系统根据距离和档期分配服务人员,服务人员上门后拍摄照片和记录服务情况,宠物主确认完成,平台完成结算和评价闭环。

这里有个容易被忽略的点:上门喂遛跟普通外卖订单不一样,它涉及"进入他人住宅"这个动作,所以订单详情里需要包含宠物生活习惯、紧急联系人、门锁密码等敏感信息。这些信息在订单完成后要做脱敏处理,不能长期明文保存。我在项目里给这些字段加了加密存储和定时清理的机制,这也是这类垂直场景跟通用电商系统拉开差距的地方。

1.3 功能模块清单

把这套系统的功能做一次完整拆解,按端划分:

用户端:

  • 账号体系:手机号注册登录、微信授权登录(预留)、个人资料维护
  • 宠物档案管理:多宠物维护、品种/年龄/体重/性格标签、疫苗接种记录
  • 服务下单:选择服务类型、预约时间、填写地址、选择服务人员或自动分配
  • 订单追踪:实时查看订单状态、服务人员位置(预留)、服务动态
  • 支付与评价:在线支付(预留或模拟)、服务完成后的打分和文字评价

服务端(服务人员使用的端):

  • 接单管理:查看可接订单列表、一键抢单、档期管理
  • 工作台:今日待服务列表、路线规划(预留)、服务记录上报
  • 收益中心:已完成订单收入汇总、提现记录

管理后台:

  • 用户管理:宠物主和服务人员的审核、封禁/解封
  • 订单管理:全平台订单查询、异常订单介入、退款处理
  • 内容管理:服务类型配置、城市区域配置、价格系数设置
  • 数据统计:订单量趋势、服务完成率、用户增长、营收报表

功能齐了之后,项目才有资格说是一个"系统",而不是一个简单的前后端页面拼凑。

2. 技术选型解析:为什么是SpringBoot+Vue+MyBatis+MySQL

2.1 后端框架选型

后端框架我几乎没有纠结,直接选了SpringBoot。理由很简单:开发效率最高,生态最成熟。相比传统的SSM(Spring+SpringMVC+MyBatis)需要手动配置一大堆XML,SpringBoot用自动配置和起步依赖把框架整合的成本降到了最低。

举个例子,想在项目里用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而不是JPA

MyBatis和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 整体架构组合的逻辑

SpringBoot+Vue+MyBatis+MySQL这个组合不知道被多少项目验证过了,它最大的优势不是单点技术最强,而是组合以后的学习成本低、招聘市场认可度高、出问题能找到的人多。

前后端分离架构下,前端和后端通过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)业务订单号,前端展示和客服沟通用,不能直接用自增ID
user_idbigint下单宠物主ID
server_idbigint服务人员ID,接单后写入
pet_idbigint宠物ID
pet_snapshotvarchar(500)宠物信息快照,下单时的宠物名/品种/体重等
address_snapshotvarchar(500)服务地址快照
service_type_idbigint服务类型ID
service_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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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 Result<T> { private Integer code; private String msg; private T data; // 省略 getter/setter public static <T> Result<T> success(T data) { return new Result<>(200, "success", data); } public static <T> Result<T> 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跨域共享非常麻烦。所以我采用了JWT(JSON Web Token)方案做无状态鉴权。

流程是:

  1. 用户登录成功后,后端用秘钥生成一个JWT Token,包含用户ID、角色、过期时间
  2. 前端拿到Token后存在localStorage里,每次发请求时放到Authorization请求头
  3. 后端写一个拦截器,拦截需要登录的接口,校验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.jsonwebtoken(JJWT)库,秘钥放到配置文件里不要硬编码。过期时间我设为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 id="listOrdersByCondition" resultType="com.petcare.entity.Order"> select * from `order` <where> <if test="userId != null"> and user_id = #{userId} </if> <if test="serverId != null"> and server_id = #{serverId} </if> <if test="status != null"> and status = #{status} </if> <if test="startTime != 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要注意时区设置,连接串里需要加serverTimezone=Asia/Shanghai,否则会报时区相关的异常。

6.2 后端启动配置

后端启动前只需要改一个文件:application.yml。把数据库链接的用户名密码改成你自己的:

server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/petcare?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver

然后通过IDE(IDEA或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?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/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没传或拦截器顺序错误检查前端请求拦截器,检查后端拦截器注册路径
数据库中文乱码连接串没加字符集参数连接串加characterEncoding=utf8
查询结果字段为null没开启驼峰映射map-underscore-to-camel-case: true
前端接口数据为undefined后端返回字段和前端取用字段不一致对比JSON结构,检查DTO字段命名
Vue启动报Node版本不支持本地Node版本过低或过高用nvm切换Node 16/18
服务人员接单后赔款订单状态并发保护缺失参考4.3里的乐观锁更新方式
打包后前端样式丢失静态资源路径配置错误SpringBoot部署时static目录层级核对

尾声:一些个人实践后的体会

这套系统从订单状态机设计到前后端联调,完整走完一遍之后,我最大的收获不是哪些技术点学会了,而是对一个业务系统"从0到1"的整体掌控感。做之前我以为难点在后端接口,做的时候才发现大部分时间花在需求梳理、数据库设计、联调排错上——这跟面试题里刷的算法完全不同,它是真实的工程问题。

如果你准备拿这套源码做毕业设计或者面试项目,我建议你拿到后不要急着跑,先花两天时间把表结构读懂,再把订单创建到完成的状态流转走一遍,最后挑一个自己感兴趣的模块改改加加,比如接单时加个短信通知、给订单列表加个筛选导出功能。能说出"我改了什么、为什么这么改",项目就真正变成你自己的了。有时间的话,还可以顺着服务完成后打赏小费、按月统计服务人员收益排行这些思路继续扩展,这套架构完全撑得住。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询