周末在家码了两天,把同城上门喂遛宠物系统从零完整落地到部署上线。这套系统用了最经典的前后端分离组合:SpringBoot负责后端接口,Vue搭前端页面,MyBatis管理数据库操作,MySQL存业务数据。源码和部署教程我都同步整理出来了,这篇把核心的设计思路、表结构、接口逻辑和部署踩坑一次性讲透,适合正在做前后端分离项目练手、或者打算接私单做本地生活类产品的朋友参考。
先说清楚这套系统是干什么的。宠物主人出差、加班、回老家的时候,猫要喂、狗要遛,靠朋友不现实,找宠物店寄养又贵。同城上门喂遛平台就是把有喂养需求的主人和愿意接单的服务人员匹配起来——主人发布任务,服务人员接单上门,完成之后线上结算、互相评价。业务模型不复杂,但麻雀虽小五脏俱全:从用户认证、宠物档案、任务发布、订单流转到支付结算、评价体系,一整套闭环走下来,几乎能把前后端分离项目的所有核心知识点都覆盖到。拿它当学习项目练手,或者作为本地生活类产品的最小可行版本去扩展,都很合适。
1. 项目全景:这套系统到底解决什么问题
1.1 业务角色与核心闭环
整个系统有三类角色,对应三种不同的操作视角。
宠物主人是需求的发起方。他们注册登录之后,先建宠物档案(名字、品种、体重、性格、喂养注意事项),然后发布喂遛任务:选好服务类型(上门喂猫、遛狗、半天陪护),填地址、时间、频次和备注,设置预算价格,等接单员来。主人这边关注的核心是服务过程透不透明、服务人员靠不靠谱、财物和宠物安全问题怎么保障。
接单员是服务的执行方。他们要做实名认证和技能资质登记,浏览附近的任务大厅,抢单接单,按约定时间上门服务,服务过程中拍照回传(喂食照片、遛狗轨迹截图),结束后确认完成等待打款。接单员关注的核心是任务密度、单价水平、结算周期和平台抽成。
管理员负责平台治理。审核服务人员的实名认证、处理用户投诉、处理异常订单(比如接单员失联、主人临时取消)、查看平台运营数据(订单量、成交额、服务人员活跃度)。有时候还要介入退款、赔付这类纠纷场景。
这三类角色形成一条完整业务闭环:主人发布任务 -> 服务人员接单 -> 上门履约 -> 回传凭证 -> 确认完成 -> 资金结算 -> 双方互评。后续想加会员体系、宠物寄养、商城周边,都是在这条主链路上做扩展。
1.2 技术选型背后的实际考虑
SpringBoot放在今天依然是后端开发的默认选择,因为它的自动配置机制确实省事。内置Tomcat,依赖管理直接引入starter,不用自己去配一堆样板代码,尤其是做这种中小体量的业务系统,快速把接口撑起来比什么都重要。版本我用的SpringBoot 2.7.x,不是最新版,因为大部分教程、插件和中间件的客户端版本都和2.x兼容性更好,没必要为了尝鲜把自己坑进去。
前端选了Vue(如果按Vue3 + Vite的组合),核心原因是这个项目要频繁操作表单、弹窗、状态管理,Vue的响应式开发体验和组件化思维非常适合。加上Element Plus组件库,后台管理界面和移动端适配都能快速搞定。
MyBatis在这套系统里负责数据库操作。选它不选JPA,是因为上门喂养这类业务里会有大量动态SQL场景——任务大厅的多条件筛选(服务类型、价格区间、距离范围)、订单列表的复合查询(加状态过滤、时间排序、分页)、统计报表的聚合查询。MyBatis对SQL的可控性强,写复杂SQL心里有底,JPA在简单CRUD上写着爽,但一旦业务查询复杂起来,又得回去写原生SQL,两头不讨好。
MySQL存数据,这里不多解释,稳定、开源、生态成熟,中小项目的首选存储。数据库字符集统一utf8mb4,排序规则utf8mb4_general_ci,这个一定要在建库时就定好,不然后面存emoji或者生僻字符直接乱码。
整套技术栈的优势在于:每个组件都是国内招聘市场的主流要求,学完能直接迁移到真实工作场景;部署成本低,一台2核4G的云服务器就能跑全套;组件之间配合成熟,遇到问题随便一搜就有答案,不用在踩坑上浪费太多时间。
2. 后端架构与数据库设计
2.1 项目分层与目录结构
后端工程按标准的三层架构拆包,不做过度设计,保持结构清晰。我习惯的目录组织方式:
com.petcare.server ├── config // 配置类:跨域、拦截器、MyBatis分页插件 ├── controller // 接口层:接收参数、调用service、返回统一响应体 ├── service // 业务层:核心业务逻辑、事务管理 │ └── impl // service实现类 ├── mapper // MyBatis数据访问接口 ├── entity // 数据库实体类 ├── dto // 前端接收参数对象(避免直接暴露实体字段) ├── vo // 返回给前端的视图对象 ├── common // 统一响应体、异常处理、常量定义、工具类 └── interceptor // JWT拦截器、管理员权限拦截器分层的原则只有一条:Controller不写业务逻辑,Mapper不写业务逻辑,业务逻辑统一收拢在Service层。比如“接单”这个动作,Controller只负责接收订单ID和用户ID,真正的校验(订单是否存在、是否已被抢、用户是否已认证、是否冲突接单)全部放在Service层。这样单测好写,出问题也好定位。
2.2 核心表设计:七张表撑起整个业务
整个系统的表数量控制在十几张左右,最核心的七张表如下:
用户表(sys_user):主键id、手机号(唯一索引)、密码(BCrypt加密存储)、昵称、头像、角色标识(主人/服务人员/管理员)、状态(正常/封禁)、注册时间。手机号登录是这套系统的主登录方式,验证码走阿里云短信接口。
宠物档案表(pet_profile):关联用户id、宠物昵称、品种、体重、生日、是否绝育、性格标签(温顺/胆小/好斗)、喂养注意事项(安全用药提醒)、疫苗记录。这部分字段在任务发布时会被自动复制到任务详情里,方便接单员在接单前了解宠物情况。
服务任务表(service_task):核心业务表。关联主人id、接单员id(可空,未接单时为空)、宠物id、任务类型(1喂猫、2遛狗、3陪护)、服务日期、开始时间、时长、服务地址(经纬度加详细地址)、任务金额、状态(0待接单、1已接单、2服务中、3已完成、4已取消、5已退款)、支付状态、备注。这条表有一条任务状态机要特别维护好,后面细说。
认证审核表(worker_auth):关联用户id、真实姓名、身份证号(存密文加脱敏处理)、服务技能标签、服务区域、接单次数、星级评分、审核状态。服务人员必须通过实名认证才能出现在任务大厅的接单候选列表里。
订单结算表(settlement_order):关联任务id、主人id、接单员id、任务金额、平台抽成比例、实际结算金额、结算状态、结算时间。平台抽成比例可以在系统配置表里设置,默认10%,支持动态调整。
评价表(task_review):关联任务id、评价人id、被评价人id、评分(1-5分)、评价内容、评价类型(对主人/对服务人员)。订单完成后双方各有一轮评价机会,评价数据参与服务人员星级计算。
系统配置表(sys_config):参数键值对,存放平台抽成比例、单笔任务金额上限、接单超时时间(分钟)等参数。把业务规则参数化是后期运营的刚需,不要写死在代码里。
建表时优先保证核心关系清晰:任务表用主人id和接单员id关联用户表,用宠物id关联档案表;结算表用任务id做逻辑关联,不用外键强制约束。生产环境我一般不用物理外键,逻辑外键就够了——MySQL外键约束在业务量上来后容易成为性能瓶颈,而且删除数据时各种限制也很烦人。数据一致性靠Service层控制事务来保证。
2.3 核心接口设计:状态机比CRUD更重要
后端接口设计遵循一个核心原则:写操作接口只传业务标识,不要传整张表的字段。例如“发布喂遛任务”是一个独立的事务动作,而不是简单的前端把表单POST给后端存进数据库。
任务状态机是整个系统最需要注意的地方:
待接单 -> 已接单 -> 服务中 -> 已完成 -> 已评价 | | | v v v 已取消 已取消 已退款状态流转做进Service层,用状态机方法统一管理,不能出现“前端随便传status字段就能改动订单状态”的情况。每个状态变更接口都带原状态参数校验,比如从“待接单”变成“已接单”时,后端要校验任务确实处于待接单状态,这是保证并发抢单安全的关键。
抢单并发处理这块我单独说一下。任务表里有一个字段接单员ID默认为空,抢单的本质是多个用户同时执行“UPDATE task SET worker_id=? WHERE id=? AND worker_id IS NULL”,受影响行数为1说明抢单成功,为0说明已经被别人抢了。这就够了,不用加Redis分布式锁。但如果用了MyBatis的固化和缓存机制,有可能会出现读取到旧数据的问题,所以抢单SQL一定要用实时查询,可以在Mapper上标记刷新缓存。
任务大厅列表用的是多条件筛选加分页查询,MyBatis里用动态SQL拼接查询条件。距离计算对经纬度做球面距离计算,用Haversine公式,MySQL里可以写成SQL函数直接在排序时用。我建议对城市和任务状态字段建联合索引,高并发场景下任务大厅的响应速度差异非常明显。
所有管理端统计接口(订单量、成交额、活跃接单员数)走独立统计Mapper,用聚合SQL一次查出结果,循环查询统计这种写法在数据量上来后会把数据库拖垮。
3. 前端结构与核心页面实现
3.1 工程结构与路由设计
前端使用Vue3加Vite构建,工程目录按业务模块划分:
src ├── api // 接口请求封装(axios实例 + 各模块接口定义) ├── assets // 静态资源 ├── components // 公共组件(地图选择器、宠物卡片、任务状态标签) ├── router // 路由配置 ├── store // Pinia状态管理(用户信息、任务筛选条件) ├── views // 页面视图 │ ├── home // 首页 + 任务大厅 │ ├── task // 任务发布、任务详情、我的任务 │ ├── profile // 宠物档案管理 │ ├── order // 订单与结算 │ └── user // 登录注册、个人中心、实名认证 └── utils // 工具函数(时间格式化、距离计算、图片上传)路由设计上加了一层登录守卫:没有登录态直接访问任务详情页、个人中心页、接单页的,统一重定向到登录页。Vue Router配置全局前置守卫,从Pinia里读token,没有就跳登录页。这个守卫要配合后端接口拦截,前端只是体验优化,真正的权限校验必须在后端做。
3.2 核心页面逻辑:任务发布与任务大厅
任务发布页面是这个项目里交互逻辑最复杂的页面。发布流程分三步:选择服务类型(喂猫/遛狗/陪护,不同服务的表单字段不同)-> 选择宠物档案(从已创建的宠物列表里勾选)-> 填写服务详情(日期时间、地址、备注、预算金额)。
地图选择地址是任务发布的关键交互。接单员需要精确知道上门地址,所以发布任务时强制要求用户在地图上选点,记录经纬度加结构化地址。我用高德地图JavaScript API,选点组件封装成一个独立模块,经纬度和地址描述一起回填给表单。这块注意申请自己的Api Key,并发量大了公司Key容易被封。
任务大厅列表页用卡片流展示任务概览:宠物照片、任务类型、服务时间、地址(部分脱敏,只展示到小区级别)、预算价格、距离距离。距离展示基于当前用户定位,定位需要浏览器授权,权限拒绝时兜底显示“全国”并提示开启定位。
接单员点击“去接单”按钮时,前端弹出二次确认框,提示确认后不可随意取消,确认后调用接单接口。接单成功跳转任务详情页,接单失败(被别人抢先)弹出提示并刷新列表。
任务详情页按状态区分内容区域。待接单状态:任务详情加主人侧信息(脱敏手机号);已接单状态:显示接单员信息和联系号码;服务中状态:显示服务进度拍照回传;已完成状态:显示评价入口和服务总结。整个页面结构用条件渲染,v-if套状态判断,代码结构清晰且维护性好。
3.3 状态管理与接口请求封装
Pinia全局状态库里主要管理三块数据:用户登录态(token、用户信息、角色)、任务列表筛选条件(地图中心点、选中的服务类型、价格区间)、全局配置(平台抽成比例、服务价格区间)。
axios二次封装要做三件事。第一,请求拦截器里给所有接口加上授权头(Bearer token);第二,响应拦截器统一处理错误码,401跳登录页、403提示无权限、500弹错误提示;第三,封装get/post/put/delete方法,返回值统一解构为response.data.data,这样业务代码里就不用反复处理外层响应壳。
前端页面要接受的教训:不要为了省事直接用任何开源后台管理模板,这个项目的页面结构不算复杂,手写组件反而更灵活,不会被模板自带的代码约束。如果是从零到一练项目,手写一遍比套模板学到的东西多得多。
4. 从开发到部署的完整路径
4.1 环境准备与本地联调
后端环境:JDK 1.8或更高(我用JDK8),Maven 3.6+,IDE用IntelliJ IDEA。数据库用MySQL 5.7或8.0,先建库再导入初始化SQL脚本(含基础表结构、初始管理员账号、配置参数)。
工程导入后先改application.yml配置文件:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/petcare?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=Asia/Shanghai username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.petcare.server.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl以下三个配置细节容易被坑:
数据库连接串里serverTimezone必须设成Asia/Shanghai,否则日期查询可能出现差8小时的问题。MyBatis的驼峰映射必须开启,否则数据库下划线字段和Java驼峰属性对应不上,属性全空。log-impl设置成StdOutImpl,开发阶段能在控制台打印SQL日志,定位问题时非常香,部署阶段再关掉。
本地联调建议先把后端跑起来,再用接口文档(Swagger或者YApi)确认所有接口正常,再启动前端。Swagger的依赖配置一下,访问/swagger-ui/index.html就能看到接口列表。前后端联调最常见的坑是跨域问题,开发环境下前端Vite端口是5173,后端8080,跨域是必然的。我直接在后端加全局CORS配置统一放行,前端不用配置Vite代理,代码更少更直观。
4.2 服务器部署:jar包加Nginx的方案
部署方案我在阿里云ECS上实操过,配置2核4G,操作系统CentOS 7.9,用jar包加Nginx的经典组合。购买、装环境这些不细说了,重点讲和传统应用不同的部署细节。
后端打jar包之前先改配置:数据库连接串换成云数据库或宝塔装的本地MySQL地址,日志级别改成只输出WARN,Swagger在prod环境关闭。然后执行mvn clean package,等它打完生成target目录下的petcare-server.jar。
jar包部署我习惯用脚本管理,创建启动脚本start.sh:
#!/bin/bash nohup java -jar petcare-server.jar \ --spring.profiles.active=prod \ --server.port=8080 \ > logs/app.log 2>&1 & echo $! > app.pid这里有一个关键点:记得预留内存配置。2核4G的服务器,Java进程分配Xmx1G就够了,留出内存给MySQL、Redis和Nginx。如果启动时看到“OutOfMemoryError: Metaspace”,就是默认内存配置不够,可以在java命令后面追加-Xmx1024m -XX:MaxMetaspaceSize=256m。
前端部署要先npm run build生成dist目录,然后配置Nginx。我的Nginx配置长这样:
server { listen 80; server_name yourdomain.com; root /var/www/petcare/dist; index index.html; location / { 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; } location /upload/ { proxy_pass http://127.0.0.1:8080/upload/; expires 30d; } }location /里写try_files是关键,Vue是单页应用,路由跳转时刷新页面会404,必须把所有路径请求都交给index.html去处理。这里很容易踩坑。接口走/api前缀,Nginx把/api/开头请求反向代理给后端,后端接口路径统一以/api前缀开头。文件上传走/upload/,图片缓存30天。图片上传是高频操作,宠物照片、评价图片都会触发,Nginx缓存能显著降低后端压力。
数据库初始化脚本在首次部署时执行一次,部署前记得修改init.sql里的管理员默认密码。整个部署流程跑通之后,访问域名能看到首页,本地IP加端口也能访问。为了安全起见,服务器上建议把MySQL和Redis端口只对本地开放,通过防火墙只暴露HTTP接口。
5. 常见问题与排查技巧实录
5.1 数据库与后端相关问题
数据查出来全是null,接口返回字段不完整。大概率是驼峰映射没开,或者MyBatis别名没配置。检查mybatis.configuration.map-underscore-to-camel-case是否为true,检查Mapper XML里的resultType是否写成了基本类型而不是包装类。
时间查询结果差8小时,数据库里的时间是对的,查出来少了8小时。这在本地轻松复现的问题在部署环境里特别隐蔽。三种常见原因:连接串没写serverTimezone、JVM默认时区不是东八区、MySQL连接器版本和数据库版本不匹配。最有效的统一方式是改连接串加serverTimezone=Asia/Shanghai,再在JVM启动参数加-Duser.timezone=GMT+8。
抢单接口出现并发问题,两个人接同一个单。排查这个场景时要先看自己的SQL有没有加条件。用UPDATE task SET worker_id=? WHERE id=? AND worker_id IS NULL这条SQL是原子操作,多线程并发下只有一个能成功。如果你写的SQL是先查出来判断是否为空再更新,就会产生并发安全问题。写代码时把“查改”变成“改”,利用数据库行锁天然保证并发安全。
接口返回401但登录状态没变,前端拦截器跳到了登录页。一般是JWT过期时间太短,或者后端拦截器配置的放行路径不对。开发环境JWT过期时间我设置24小时,生产环境2小时加自动续期。拦截器放行路径包含登录、注册、验证码、Swagger文档、接口文档页面,其余全部拦截。
5.2 前端与部署相关问题
前端打包后访问页面白屏,控制台报Uncaught SyntaxError。这个几乎都是静态资源路径问题——vue.config.js或vite.config.ts里的publicPath配成了项目名路径,打包后资源引用路径不对。改成'/'或'./'重新打包即可。
刷新页面404,路由跳回首页。Nginx配置里location /没有try_files,或者配置了但是路径写错。按前文的模板把try_files $uri $uri/ /index.html;加到location /里就能解决。
图片上传失败,上传到本地目录但页面不显示。分两种情况:后端没配置静态资源映射(要在SpringBoot里加WebMvcConfigurer映射/upload/**到本地磁盘目录),或者Nginx没有给/upload/路径配置代理。要么后端直接返回完整路径,要么在Nginx/upload/代理转发,两边保持一致。
部署环境启动时提示端口被占用,8080端口起不来。先lsof -i:8080看是什么进程占用了端口,如果是之前部署的应用没有杀干净,用kill -9杀掉再重启。遇到过因为服务器面板自带Nginx占用80端口导致前端页面访问不了的,把面板代理端点换掉就行。
最后分享几个心得
从立项到上线,这个项目让我对前后端分离的真实开发节奏有了更直观的认知。最有价值的部分其实不在写了几百个接口,而是把“状态机怎么设计才能不乱”“并发抢单怎么做才安全”“部署的环境问题有哪些隐蔽点”这些隐性知识通过实操沉淀下来。当你在自己的机器上把jar包跑起来、Nginx转发打通、手机浏览器访问域名看到完整页面的时候,整个链路的知识才会真正内化。
如果你打算把这套系统用于毕业设计或者个人作品集,我建议在此基础上至少扩展两三个亮点模块:服务人员GPS轨迹回传、聊天IM集成、订单超时自动取消的任务调度。任何一个模块的深度做进去,项目含金量都会明显提升。这套系统本身是个非常扎实的基础骨架,真正值钱的是基于业务流动而不断生长的能力。