Spring Boot + Android宠物领养管理系统开发实战:从需求到联调避坑
做这个宠物中心信息管理系统的动机其实有点私人——我课余时间在本地动物救助站做过志愿者最头疼的不是没人领养而是信息登记靠纸笔领养人有没有按时反馈谁也说不清。所以就想着用Java Spring Boot做后端Android做客户端把宠物领养这件事从“发一条朋友圈”变成“一个能追踪状态的系统”。前后断断续续写了一个多月配套整理了源码、文档、运行视频和讲解视频。这篇博客就把完整的开发思路和绕过坑的经验倒出来给正在开题或者准备动手做一个Spring Boot Android毕设的同学一点参考。1. 为什么是“宠物领养”而不是“宠物商城”需求与功能边界宠物领养系统和普通的宠物商城在业务上完全是两条路。商城关注的是商品、购物车、订单、支付核心是“交易”而领养关注的是申请人资格、宠物去向、流程审核核心是“责任”。如果选题只做宠物列表加一个“我要领养”按钮那本质还是商城思维领养场景里最有价值的“审核——流转——追踪”完全没有被体现。所以我在设计这个系统时第一件事就是先把业务边界划清楚不做支付、不做物流、不做积分商城甚至不做聊天只做从“宠物上架”到“用户申请”再到“管理员审核”这条闭环。这样一个项目周期内可以把主流程做得足够深而不是堆一堆没做完的功能。1.1 领养流程天然带着“审核”属性线下领养机构的要求一般很严格领养人需要有稳定收入、有养宠经验、家里同意还要接受定期回访。放到线上系统里这些约束就变成了“领养申请”和“管理员审核”两个核心环节。我最初的需求讨论阶段和几个同学还争论过要不要做“一键领养”即用户点击按钮就直接绑定宠物。后来想想这个方案在真实业务里是不可能的甚至有点不负责任——系统如果不做任何筛选任何人都能随便领走一只猫狗那不是领养平台是宠物批发市场。因此领养申请必须包含申请表申请人填写领养理由、居住情况、联系方式管理员在后台逐条审核审核通过后才进入“已领养”状态。这个决策其实也决定了数据表的结构因为申请链路会牵出至少两张表宠物表和领养申请表两张表之间还要维护一个状态机。如果一开始没想清楚这一点后面很容易做成“商城式”的直来直去。1.2 功能列表的取舍哪些做了哪些故意不做系统一共涉及两类角色普通用户和管理员。权限设计上不搞RBAC那套复杂模型直接在User表里加一个role字段0表示普通用户1表示管理员后端接口通过一个简单的拦截器去校验权限就足够了。角色功能模块是否实现一句话理由普通用户注册、登录实现所有领养申请必须绑定真实账号普通用户浏览宠物列表、查看详情实现核心入口游客也能看普通用户申请领养实现最核心的业务节点普通用户查看我的申请进度实现用户需要知道自己是否通过普通用户收藏宠物实现加大用户回访意愿成本低普通用户发布送养宠物不实现会引入送养人角色审核复杂度翻倍管理员登录、用户管理实现后台基本能力管理员宠物信息管理、上下架实现保证宠物数据可控管理员领养申请审核实现系统核心业务管理员公告管理实现简单的内容发布丰富后台页面我当时想过把“送养人直接发布宠物”也做进去但后来发现这个功能一旦放开管理员就要同时审核“人”和“宠物”等于一个人要干两种审核工作量直接翻倍。为了在有限的项目周期里把主流程跑通我把送养发布收敛成了管理员在后台录入宠物信息。这样虽然少了点“众包”的味道但数据质量和审核链路都干净很多也更容易向答辩老师讲清楚。2. 为什么技术栈是Spring Boot Android MyBatis Plus选型心路历程这个项目属于“后端接口 Android客户端”的典型组合。选型的时候不是没纠结过尤其在后端框架和前端形态上我前后推翻了好几次。这里把这套技术栈定下来的思路写出来给后来人一个参考。2.1 后端框架Spring Boot 2.x 比 3.x 更省心我后端最终用的是Spring Boot 2.3.4.RELEASE配套JDK 8。并不是说Spring Boot 3不好而是当时主流毕业设计相关博客、开源项目以及第三方依赖的兼容情况大多还停留在2.x时代。比如MyBatis Plus、一些文件处理库和连接池的适配在3.x上要额外折腾为了几个新特性去填兼容性的坑性价比很低。对中小型项目来说稳定压倒一切。Spring Boot 2.x最大的优势就是“自动配置 内嵌Tomcat”我只需要写一个带有SpringBootApplication注解的启动类再配一个application.yml就能把Web项目跑起来不像以前SSM要手动配一堆XML。整个工程我用Maven拆成了common、system、business三个子模块common放通用返回类和工具类system放用户、权限相关business放宠物和领养申请业务。模块拆分的好处是依赖关系一眼能看懂后面打包部署也不用东找西找。后端启动跑通之后配置文件里最容易被忽略的几个点也顺便说一下MySQL连接串要带serverTimezoneAsia/Shanghai否则驱动8.x会报时区异常MyBatis Plus要打开驼峰映射也就是map-underscore-to-camel-case: true否则数据库下划线的字段映射不到实体类的驼峰属性上。2.2 Android 原生 vs 小程序为什么会纠结题目里明确写了“基于Android”所以客户端最终选了Android原生开发。但真正自己选型的时候我确实在小程序和Web页面之间犹豫过。小程序开发效率高组件现成真机调试也方便H5跑在手机浏览器里也省去安装包的麻烦。但它们都有一个共同的问题在答辩展示和评分维度上“原生App”往往被认为更有“客户端工作量”。另外还有一个私心后端已经是Java了Android端也用Java整个项目语言统一出问题的时候排查逻辑相对顺畅不用在一个项目里切换两三种语言思维。实际用到的Android组件不算多核心是RecyclerView、CardView、BottomNavigationView、Fragment、SharedPreferences这套。网络请求选了OkHttp图片加载选了Glide。这两个库算是Android开发的事实标准用的人多遇到问题比较容易搜到答案。如果你现在刚开始做建议不要用传统的AsyncTask去请求接口而是一步到位用ViewModel LiveData Retrofit代码会清爽很多也更有现代Android开发的调性。2.3 数据持久化MyBatis Plus 带来的真实效率提升持久层用MyBatis Plus而不是原生MyBatis最重要的原因是省时间。宠物领养这种业务单表CRUD占了大概七成而MyBatis Plus的BaseMapper接口已经把insert、delete、update、selectById这些方法全部内置了。我不用写一行SQL就能完成用户表、宠物表、申请表的增删改查剩下真正需要手写SQL的只有一些联表查询和统计。分页也是一个痛点。原生MyBatis要写limit还要自己数pageNum、pageSizeMyBatis Plus提供了分页插件PaginationInnerInterceptor配置之后在Service里调用page(new Page(pageNum, pageSize), wrapper)就能拿到带总数的结果集接口返回分页数据非常省事。顺手提一个网上很火的话题MyBatis Plus能不能根据Java实体类自动生成创建表的SQL语句。答案是可以它有字段填充、自动建表的相关扩展能在应用启动时扫描实体类并生成表。但我的建议是正式项目或者课程设计里还是用SQL脚本管理表结构更好因为建表语句、索引、字段注释这些内容放在SQL文件里别人拿到项目后能直接看到数据库设计了。实体类自动建表适合快速原型验证不适合反复迭代跑几次数据库结构就不稳定了。3. 字段和接口先把数据地基打牢后面才能少翻工很多同学做这种全栈项目习惯先写代码再补数据库表等接口写完发现表结构对不上了又回头改表、改实体、改SQL非常折磨人。我的做法是先把字段定下来再开始写任何Controller。3.1 表结构设计先把状态字段定好后面少改代码这个系统里最关键的表有四张用户表、宠物表、领养申请表、收藏表。如果再算上公告就是五张足够撑起一个完整项目。用户表核心字段id、username、password、nickname、phone、avatar、role、create_time。password一定不要存明文哪怕毕设项目也要有最基本的加密意识我用的是MD5加盐实际工作里建议用BCrypt。宠物表核心字段id、name、category猫/狗/其他、breed、age、gender、description、images、status、create_time。这里最难定义的就是status我用四个数字来表达流转状态0待领养、1审核中、2已领养、3已下架。为什么需要“审核中”这个状态因为当用户提交领养申请后这只宠物要被“锁定”不能让另一个人同时申请。如果只有一个“待领养/已领养”两个状态两个用户同时提交时后端很难拦截会出现同一只宠物被多人申请成功的冲突。领养申请表核心字段id、pet_id、user_id、apply_reason、contact、status、apply_time、audit_time、audit_remark。申请表的status也有自己的状态流0待审核、1通过、2拒绝、3已完成。这里的“已完成”是为以后回访功能预留的如果领养成功后长期无人跟进管理员可以把申请标记为已完成代表一次领养生命周期结束。收藏表就简单多了id、user_id、pet_id、create_time唯一索引直接打在user_id pet_id上防止重复收藏。3.2 RESTful 接口怎么设计 Android 端调用最顺手因为客户端是Android接口设计就不能太复杂。我统一了后端返回格式用Result 包装里面固定三个字段code、message、data。成功时code200业务失败时code500token失效时code401Android端拿到code直接提示用户不用写一堆try/catch。具体接口路径遵循RESTful风格见下表功能请求方式路径注册POST/api/user/register登录POST/api/user/login宠物分页列表GET/api/pet/list?pageNum1pageSize6keyword猫宠物详情GET/api/pet/{id}提交领养申请POST/api/adoption/apply我的申请记录GET/api/adoption/my?userId1管理员审核POST/api/adoption/audit收藏/取消收藏POST/api/favorite/toggle接口里有几个约定对Android端很友好分页参数统一叫pageNum和pageSize返回结构体里直接包含total、pages、records时间格式统一是yyyy-MM-dd HH:mm:ss图片URL返回完整地址例如http://192.168.x.x:8080/uploads/xxx.jpgAndroid端拿过来可以直接交给Glide加载不用再拼路径。3.3 建表脚本与代码生成器双轨并行实际动手时我先写了database.sql脚本把所有表和初始数据一次性建好。比如管理员账号admin/admin123、几个示例宠物、几条初始公告。然后我用MyBatis Plus的代码生成器扫描这些表生成对应的实体类、Mapper接口、Service和Controller。生成器的好处是实体类里字段类型、注解都是准的省去手写一堆TableField的时间。但生成完代码不代表完事还要自己改逻辑Controller里加参数校验Service里加事务一些查询条件需要在Wrapper里手工拼。双轨并行是我权衡之后觉得最适合毕设的方式SQL脚本让别人拿到项目后能直接看懂表结构代码生成器让开发速度不至于太慢。后续如果加了字段我会同时更新SQL脚本和实体类尽量保持两边一致。4. 从“能跑”到“好用”核心业务逻辑的落地细节数据库和接口约定好之后项目其实已经能跑了但离“好用”还有一段路。这段路主要靠几个核心功能的细节打磨来完成。4.1 宠物图片上传Android端与后端的配合方式图片上传是这类管理系统的标配功能不接云服务的话技术路线就一条后端用MultipartFile接收保存到本地上传目录再把可访问的URL返回给客户端。我在Spring Boot里配置了静态资源映射把/webapp/uploads/映射到http://ip:8080/uploads/**这样图片URL就是一个普通的HTTP链接Android端用Glide直接可以加载。后端保存图片时文件名我用时间戳UUID生成尽量避免中文名和重名。Android端处理图片上传有几步先用系统相册选择图片然后通过约束压缩图片大小再用OkHttp的MultipartBody进行上传。实际操作中最重要的限制是Spring Boot默认的单文件上传大小只有1MB宠物照片随便一张就是2MB以上所以必须在application.yml里调大参数。我当时设置了max-file-size: 20MB、max-request-size: 20MB不然后端会报“文件大小超过限制”的错。4.2 领养状态流转为什么需要状态机而不是if/else领养业务是所有功能里最容易写乱的部分。用户的申请、管理员的审核、宠物状态的改变这三者必须保持同步任意一步丢了一致性系统就会出现“申请通过了但宠物还能被其他人看到”之类的诡异现象。我的做法是把状态流转写成一个逻辑清晰的方法。用户提交申请时后端需要在一个事务里完成两步第一步校验宠物status必须为0第二步插入领养申请表同时把宠物status改成1。这里用Transactional注解保证两步要么同时成功要么同时回滚。管理员审核时分两种路径审核通过则把申请记录status改成1宠物status改成2审核拒绝则把申请记录status改成2宠物status恢复成0。这两组操作同样需要在事务里完成不然很容易产生脏数据。写代码时我推荐把状态定义成枚举或者常量类不要到处写魔法数字。比如public class PetStatus { public static final Integer WAIT 0; // 待领养 public static final Integer APPLYING 1; // 审核中 public static final Integer ADOPTED 2; // 已领养 public static final Integer OFF_SHELF 3; // 已下架 }用这种方式后续如果新增“待回访”“已完成”之类的状态只需要在常量类和对应流转方法里扩展不用在几十个Controller里翻找数字维护成本会低很多。4.3 用户登录态与列表加载的简化实现登录这块我没有用JWT而是做了一个简单的Token方案用户登录成功后后端生成一个UUID字符串保存到token表并把它返回给Android端。Android端拿到token后存进SharedPreferences之后每次请求在Header里带Authorization字段后端用一个拦截器统一校验。为什么不用JWT不是因为JWT不好而是这个项目的体量不需要JWT带来的无状态复杂性简单的token表更容易让刚接触开发的同学理解整个登录链路的每一步。列表加载是Android客户端最常见的场景。宠物列表用RecyclerView展示我在Adapter里绑定Glide加载宠物图片。分页加载一开始我用了一个简单的onScrollListener每当滚动到底部就请求下一页但很快发现重复调用的问题。后来在Adapter里加了一个isLoading标记请求期间屏蔽下一次触发才算稳定下来。后来想想这种小坑非常值得写进开发笔记里因为几乎每个列表分页的项目都会遇到。5. 联调阶段最折磨人的几个坑网络权限与跨域这部分是我实际开发过程中浪费时间最多的部分本来以为代码写完就功成名就了结果Android端根本连不上后端。如果你也是前后端联调的新手以下三个坑至少会踩中两个。5.1 模拟器连不上后端10.0.2.2 与局域网 IP 的烦恼我第一次启动Android模拟器测试App时接口地址写的是http://localhost:8080结果怎么请求都是超时。后来才反应过来Android模拟器里的localhost指向的是模拟器自己而不是开发电脑。想访问电脑本机上运行的后端必须用特殊地址http://10.0.2.2:8080这是Android模拟器为宿主机保留的默认路由地址。如果你是拿真机测试问题更直观手机和电脑必须在同一个局域网内接口地址要改成电脑的局域网IP比如http://192.168.1.101:8080不能用localhost也不能用10.0.2.2。我建议在Android工程里把baseUrl集中放在一个常量类里切换模拟器和真机时只改一行配置不然你会在一堆Activity里找IP地址找得想摔手机。5.2 Android 9 之后不允许明文 HTTP一个小配置搞定模拟器连上之后又弹出一个报错Cleartext HTTP traffic not permitted。这是Android 9也就是API 28开始的一项默认安全策略系统默认禁止App使用明文HTTP流量。如果你的后端还是http协议而不是https请求会被直接拦截。解决办法是一行配置在AndroidManifest.xml的application节点加上android:usesCleartextTraffictrue。如果想做得更严谨可以通过network_security_config.xml只允许特定IP或域名走明文流量其余域名继续走https。对于开发阶段直接把usesCleartextTraffic改为true最省事但上架前一定记得收紧这个配置。5.3 Spring Boot 的 CORS 与文件上传大小限制原生Android App的请求其实不依赖浏览器同源策略所以跨域问题理论上只影响WebView或者你调试时打开的网页控制台。但我在项目中还是配置了一个简单的CorsFilter允许所有来源和常见方法这样做的好处是万一以后加一个H5管理端不用再回头补配置。除了CORS还有两个隐藏配置容易坑人。一个是前面说过的文件上传大小限制另一个是数据库连接时区。如果你用MySQL 8.0以上JDBC连接串不带serverTimezone参数每次启动后端都会报空指针或者时区异常。虽然这几个问题看起来都是“配置一下就解决”但往往排查时间比写代码还长。6. 拿到“源码文档视频”之后别急着解压这个项目打包交付的时候带了源码、文档、运行视频和讲解视频很多同学拿到手会习惯性地把压缩包解压后直接往IDEA里一拖然后被各种报错劝退。这里分享一下我觉得最高效的学习顺序。6.1 文档应该怎么读从数据库脚本开始拿到项目后第一件事不是看代码而是先打开文档里的“环境配置说明”和“数据库脚本”。先按文档要求安装JDK、Maven、MySQL和Android Studio再把SQL脚本执行一遍确保数据库能起来。后端的application.yml里通常写好了数据库连接地址和密码改成本地配置后启动启动类看到Spring Boot的启动日志出现“Started Application”才算环境通了。文档里一般会写明默认账号我这个项目准备的是管理员/admin123普通用户可以自己注册。先用管理员账号在浏览器里访问几个接口比如宠物列表接口能返回JSON就说明后端链路是通的然后再去打开Android工程。为什么要先看数据库脚本因为所有功能都挂在字段上你如果不知道宠物表有哪些状态看代码时会非常迷茫。数据库一旦建好再打开实体类去对照很容易就理解后端整体结构。6.2 运行视频的观看顺序先看效果再对着日志找代码运行视频的价值在于让你知道系统“应该长什么样”。我的建议是完整看一遍视频里的操作流程管理员登录后台、添加宠物、用户注册、提交申请、管理员审核边看边记下每个页面的功能点。之后在Android模拟器里自己跑一遍看到和视频差不多的效果你就有底了。下一步才是把IDE的同步日志和Android的Logcat同时打开对照着后端Controller和Android Activity去读代码。比如你看到视频里用户点击“申请领养”后宠物状态变成“审核中”就可以去代码里搜索宠物状态被修改的位置看看后端Service是不是加了事务。这种“现象驱动”的阅读方式比从头到尾读一遍代码效率高得多。6.3 讲解视频帮你理解业务但二次开发还要靠自己讲解视频通常会把项目的背景、数据库设计、核心接口和部署流程讲一遍适合快速建立整体认知。但注意看视频不等于学会了动手写代码才算。建议在现有基础上挑一个小功能做二次开发比如把“用户收藏”改成“用户收藏收藏分类”或者给宠物增加体重字段从数据库到后端口再到Android界面完整走一遍。做二次开发最大的好处是可以把整套技术栈真正串联起来。很多同学看别人的源码看得懂一旦自己动手就不知道从哪里开始就是因为还没有形成“数据库字段变更——实体类/接口变更——Android解析变更”的完整反射链路。你哪怕只改一个字段把这条链路走通收获都会比刷十遍讲解视频大。做完这个项目最大的感触是一个看起来简单的领养系统真正落到代码层面要考虑的细节远比想象多。状态流转、并发申请、图片存储、跨端联调每一个环节都值得单独写一篇踩坑记录。如果你准备拿这个题目做毕设或者练手我建议先把本文提到的状态流和数据表结构画清楚再动手写代码能少走很多弯路。最后再说一个我自己当时没早点意识到的小技巧联调时一定要同时打开后端控制台和Android的Logcat盯着两侧的日志对时间点很多问题其实在日志对齐之后一眼就能定位省下的时间足够你再把几个页面细节打磨一遍。