ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Spring Boot + Android 宠物领养信息管理系统设计与实践

Spring Boot + Android 宠物领养信息管理系统设计与实践 做宠物领养类的管理系统市面上案例不少但大多是纯网页后台真正把Java Spring Boot后端和Android客户端串成一个完整闭环的其实没那么多。这个项目标题里带“宠物中心信息管理系统 宠物领养”还配套源码、文档、运行视频和讲解视频基本就是一套可以直接上手改、直接答辩用的毕业设计或课程设计级项目。我拿到这种标题的时候第一反应不是“功能多不多”而是“链路全不全”用户从Android端浏览宠物、提交领养申请后台管理员审核、管理宠物信息、处理申请记录这条链路如果能完整跑通那这个项目的含金量就到位了。这套东西适合谁一个是正在做毕设、需要快速落地一个前后端分离式应用的学生另一个是想练手Spring Boot Android联调的在职开发者。对我个人来说我更喜欢把它当成一个“麻雀虽小五脏俱全”的案例来拆有用户体系、有宠物信息管理、有领养申请流程、有图片上传、有列表分页几乎每个模块都能单独摘出来讲一段面试题。下面我就从项目拆解、后端设计、Android端实现、运行调试到扩展方向把这类系统该有的细节一次讲清楚。1. 项目定位与整体拆解1.1 宠物中心管理系统到底解决什么问题线下宠物领养最常见的问题就是信息不透明宠物收容所或宠物店有多少待领养动物健康状况如何领养条件是什么这些信息散落在纸质表格或微信群聊天记录里想要查清楚非常费劲。宠物中心信息管理系统要解决的就是把这个过程线上化让领养人能在手机端浏览宠物档案让管理员能在后台维护宠物状态、审核领养申请最终形成一条“浏览、申请、审核、领养完成”的可追踪记录。从功能上看这个项目默认会包含几个核心角色普通用户领养人、管理员。用户端在Android App里可以看到宠物列表、宠物详情、领养申请入口、个人申请记录管理端通常在Web页面或同一个App内部实现可以发布宠物信息、上下架宠物、处理领养申请、查看统计等。这里的“宠物中心”既可以是流浪动物救助站也可以是一个宠物店的门店管理系统标题里特意强调“信息管理”而不是单纯“领养”说明系统还承担着基础的数据维护职责。这类项目我建议不要一开始就追求功能多先把核心链路做扎实。很多同学拿到源码喜欢先加一堆“宠物医疗记录”“疫苗提醒”之类的扩展模块结果主流程还没跑通就乱了。正确思路是先确认“用户能不能在App里看到宠物并申请领养”“管理员能不能审核并更新状态”这两条命门通了剩下的都是锦上添花。1.2 技术选型为什么是Spring Boot AndroidJava Spring Boot做后端Android做移动端这个组合在高校项目里几乎成了标准答案原因很实际Spring Boot极大降低了Java后端开发门槛内置Tomcat、自动配置、Starter机制写一个RESTful接口通常只需要一个Controller、一个Service、一个Mapper不需要像传统SSH那样写一堆XML配置。Android原生客户端和Spring Boot后端的配合是天然友好的都是Java系语言数据交换走JSON图片上传走MultipartToken认证用Interceptor拦截器就能搞定不存在跨语言沟通成本。部署简单Spring Boot打一个jar包就能跑MySQL建库执行SQL脚本就能起服务这对演示和答辩非常友好。有人会问为什么不直接用小程序或者纯Web原因倒不是小程序不好而是很多学校毕业设计选题时明确要求“必须有客户端开发”或者“必须体现移动端技术”Android原生App在这种场景下是最稳妥的选择。而且从学习角度讲Android端的生命周期、网络请求、本地缓存、适配问题本身就是一套完整的知识点比套壳WebView能学到的多得多。这个项目的技术栈通常还会涉及Spring Boot 2.x、MyBatis或MyBatis-Plus、MySQL 5.7/8.0、Android Studio中的Java或Kotlin、OkHttp/Retrofit、RecyclerView、Glide图片加载。如果有Redis做缓存、JWT做登录态那一句话就能把项目的技术亮点拔高不少。1.3 拿到源码之后先看哪几个文件我接触过不少拿到源码就急着启动项目的同学结果卡在环境上半小时然后开始怀疑代码有问题。其实拿到源码后正确的打开顺序不是先运行而是先看目录和文档。首先看项目根目录下的README或部署文档里面通常会写JDK版本、MySQL版本、数据库脚本位置、启动步骤然后看后端源码里的application.yml或application.properties确认数据库连接配置、端口配置、文件上传路径再看数据库SQL脚本清楚有哪些表、哪些初始数据最后看Android端的AndroidManifest.xml确认有没有申请网络权限、文件权限以及启动Activity是哪一个。建议按“数据库脚本 → 后端配置 → 后端启动 → Android端配置 → Android端启动”这个顺序来。每走一步都用日志确认状态不要一口气全开。很多看起来“项目跑不起来”的问题其实只是端口被占用、数据库密码不对、App端IP写成了localhost这类低级错误。2. 后端Spring Boot核心设计与实现2.1 分层结构与包结构设计一个标准的Spring Boot宠物管理系统后端包结构通常是这样的com.pet.adoption ├── controller // 接口层接收请求、返回JSON ├── service // 业务层处理核心逻辑 │ └── impl ├── mapper // 数据访问层MyBatis接口或JPA Repository ├── entity // 实体类对应数据库表 ├── dto // 数据传输对象用于接收参数和返回结果 ├── config // 配置类比如跨域、拦截器、文件上传配置 ├── common // 通用类统一返回结果、异常处理 └── utils // 工具类JWT、MD5等这种分层不是随便分的它的意义在于每一层各司其职。Controller层只负责接参数和返回结果不写SQLService层处理业务规则比如“只有登录用户才能提交领养申请”“同一只宠物只能被一个用户成功领养”Mapper层只对数据库打交道。这样你在答辩时被问到“如果加一个管理员审核功能应该改哪里”能很清晰地定位到Service和Controller而不是满项目乱搜。实体类建议用Lombok注解Data简化getter/setter用TableName或Table映射数据库表。很多源码里的实体类和数据库字段对不上就是因为命名不一致比如数据库字段是pet_nameJava属性是petName如果开启了驼峰映射还好没开启就麻烦了。所以拿到代码后第一步就是检查MyBatis的map-underscore-to-camel-case配置以及MyBatis-Plus的全局配置是否打开了驼峰转换。2.2 实体建模与数据库表设计我以“宠物领养”这个核心场景为例把表结构设计思路过一遍。最基本的是这几张表表名关键字段说明userid, username, password, phone, avatar, role, create_time用户表role区分管理员和普通用户petid, name, category, breed, age, gender, health, description, image, status, create_time宠物表status表示待领养/已领养/下架adoption_applicationid, pet_id, user_id, reason, status, apply_time, approve_time领养申请表status表示待审核/通过/驳回apply_record 或 messageid, user_id, content, create_time可选留言或咨询记录宠物表里的status字段很关键建议用整数类型比如0表示待领养、1表示已被申请、2表示已领养、3表示下架。为什么不直接用字符串因为整数状态在做前端展示映射时更灵活而且MySQL查询效率更高。领养申请表里的status则建议用枚举或字符串比如PENDING、APPROVED、REJECTED方便一眼看出业务含义。这里有一个实战中容易忽略的点用户提交领养申请后宠物的状态要跟着变。比如宠物还在“待领养”状态时用户可以申请一旦有用户提交申请且还没被驳回宠物就应该被置为“申请中”或“已被申请”避免其他用户继续重复申请。这个逻辑必须在Service层用事务控制先查宠物状态再插入申请记录再更新宠物状态三步必须同时成功或同时失败。数据库脚本里最好附带初始化数据不然App一打开宠物列表是空的演示效果大打折扣。我会在SQL里预置10只宠物覆盖猫、狗、兔子几类图片地址尽量用项目内的相对路径比如/uploads/pet1.jpg这样换电脑后只要把上传目录一起拷走图片还能正常显示。2.3 核心接口领养流程的状态机设计后端接口要围绕业务流程来设计而不是一个表配一套增删改查就完事。宠物领养系统的核心流程是用户浏览宠物列表 → 用户查看宠物详情 → 用户提交领养申请 → 管理员审核 → 状态更新。对应到接口上大致是这样POST /api/user/login、POST /api/user/register用户登录与注册GET /api/pet/list?page1size6categorycat分页查询宠物列表支持按分类、状态筛选GET /api/pet/{id}宠物详情POST /api/adoption/apply提交领养申请参数包含petId、reasonGET /api/adoption/my当前用户查看自己的申请记录GET /api/admin/pet/list管理端宠物管理列表POST /api/admin/pet/save新增或修改宠物信息POST /api/admin/adoption/approve审核领养申请参数包含applicationId、approveResult接口路径的设计建议采用RESTful风格同时用统一返回体包裹比如{ code: 200, message: success, data: ... }。这样Android端解析JSON时只需要关注code不等于200就走错误提示等于200再取data逻辑非常统一。领养申请审核这个状态机是项目亮点。我的实现思路是新建申请时状态为PENDING管理员审核通过时先检查宠物是否还是“申请中”状态如果是则把宠物状态改成“已领养”申请单状态改成APPROVED如果驳回则把宠物状态退回“待领养”申请单状态改成REJECTED。这里一定要避免“管理员连续审核同一份申请、宠物状态被重复修改”的情况方案是给申请单加一个version字段或者直接在Update SQL里带上状态条件用MyBatis写一句UPDATE adoption_application SET statusAPPROVED WHERE id#{id} AND statusPENDING根据受影响行数判断是否重复操作。2.4 文件上传与图片访问路径实操细节宠物信息里必须有图片这就绕不开文件上传。Spring Boot里接收文件很简单用MultipartFile即可但真正麻烦的是前端怎么把图片传上来、后端怎么存、访问路径怎么映射。后端配置里我一般会在application.yml中单独定义一个上传路径file: upload-dir: ./uploads access-path: /uploads/**再写一个静态资源映射配置类Configuration public class WebConfig implements WebMvcConfigurer { Value(${file.upload-dir}) private String uploadDir; Value(${file.access-path}) private String accessPath; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(accessPath) .addResourceResolver(new PathResourceResolver()) .addResourceLocations(file: uploadDir /); } }这样文件保存到./uploads目录后浏览器或App就能直接通过http://IP:8080/uploads/pet1.jpg访问。这里有个坑必须提醒Windows下路径分隔符是\Linux下是/如果你在配置里写死路径部署到服务器上很可能图片404。建议用Paths.get(uploadDir).toAbsolutePath().toString()来动态拼接。保存文件时还要注意文件名处理。直接用用户上传的原始文件名有安全风险比如路径注入或者重名覆盖。我习惯用UUID.randomUUID().toString().replace(-, )生成新文件名再拼接上原始文件的后缀名比如.jpg、.png。后缀白名单校验也很简单用StringUtils.endsWithIgnoreCase判断后缀是否是允许的类型就行。如果图片比较大建议在Android端先压缩再上传。很多源码里没有这个处理用户拍照一张5MB的图片直接传后端接收是没问题但列表页加载会明显卡顿。更合理的方式是用Glide的InputStream配合后端生成缩略图或者在上传前用Android的BitmapFactory计算inSampleSize做一次采样压缩把图片压到几百KB再上传。这块在我后面讲Android端时会再展开。3. Android端交互与界面实现3.1 项目结构从Activity到MVVMAndroid端拿到手之后先看包名和目录结构。一个可维护的宠物领养App至少应该有这样几个模块视图层Activity/Fragment、适配器Adapter、网络层Retrofit接口、响应体、模型Java Bean、工具类SharedPreferences封装、图片加载。老项目可能还停留在每个Activity里直接写网络请求的时代——能用但维护起来很痛苦。比较务实的建议是如果你已经熟悉MVP那MVP在面试中更好讲如果你想体现“较新技术”可以直接上MVVM用ViewModel LiveData管理数据。但要注意很多网上项目的MVVM只是挂个名字真正用LiveData监听数据变化的少之又少。答辩时老师问一句“你的ViewModel和Activity之间怎么通信”如果答不上来反而扣分。所以我更推荐一个折衷方案核心页面用简单清晰的MVP或直接使用ViewModel把网络请求结果通过回调更新UI不用搞得太抽象但逻辑要说得通。Android端的主要页面大致是登录注册页、首页宠物列表RecyclerView、宠物详情页、申请领养表单页、我的申请记录页、个人中心页。如果是管理员用的App还会加宠物发布页、申请审核页。3.2 网络层封装Retrofit/OkHttpAndroid端请求后端接口我强烈建议直接用Retrofit而不是每页手写OkHttp。原因很简单Retrofit把接口定义、请求参数、响应解析全部用注解声明出来代码量能少一半以上而且配合Gson自动解析JSON基本不需要手写JSON解析逻辑。一个典型的Retrofit接口定义长这样public interface ApiService { POST(api/user/login) CallResultUser login(Body LoginRequest request); GET(api/pet/list) CallResultPageResultPet getPetList(Query(page) int page, Query(size) int size, Query(category) String category); POST(api/adoption/apply) CallResultString applyAdoption(Body ApplyRequest request); Multipart POST(api/admin/pet/save) CallResultString savePet(Part(name) RequestBody nameValue, Part MultipartBody.Part imageFile); }然后通过Retrofit.Builder创建实例baseUrl记得写Android设备能访问到的后端地址10.0.2.2对应模拟器访问宿主机真机则要填电脑的局域网IP。这块我会在联调章节细讲但这里先记住一句话不要在App代码里写localhost否则模拟器上可能侥幸通真机上必挂。登录状态管理建议用Token机制。后端登录成功后返回一个tokenAndroid端存到SharedPreferences里之后每次请求都通过OkHttp的Interceptor在请求头里加上Authorization: Bearer token。后端用拦截器校验token校验通过才放行。这种方案比在每个Activity里手动传token干净得多而且换页面不需要反复登录。3.3 列表、详情与申请表单宠物列表页是整个App的门面也是技术展示的重点。使用RecyclerView CardView是标配列表项展示宠物图片、名字、品种、状态标签。这里有一个实用性技巧列表滑动时图片加载频繁建议用Glide配合skipMemoryCache(true)控制缓存策略同时在Adapter里对status字段做映射0显示“待领养”绿色标签1显示“申请中”灰色标签2显示“已领养”蓝色标签比直接拼字符串规范得多。宠物详情页要展示的字段较多包括分类、年龄、性别、健康状况、描述、宠物图片。拿到后端返回的Pet对象后用findViewById或ViewBinding逐一赋值即可。图片URL如果后端返回的是相对路径比如/uploads/pet1.jpgGlide加载前一定要拼接完整地址baseUrl pet.getImage()。这个坑非常常见单独访问图片没问题放App里就白屏多半是URL拼接漏了域名部分。申请领养表单页至少要包含这些输入项申请理由多行文本、联系电话、居住情况可选填提交前要做非空校验。点击提交时按钮要置灰并显示“提交中”防止用户重复点击造成多条申请。请求成功后不要直接finish掉页面而是提示“申请成功等待管理员审核”之后跳转到“我的申请记录”页面让用户看到自己的申请单状态变化。3.4 图片加载与缓存踩坑图片这块我再多说两句。Android端加载图片常用的Glide用法很简单Glide.with(context).load(url).into(imageView)。但实际操作中会遇到几个经典问题Android 9API 28及以上默认禁止明文HTTP请求。如果后端接口是http://而不是https://在不加配置的情况下所有网络请求都会直接报CLEARTEXT communication to ... not permitted。解决方案是在AndroidManifest.xml的application节点加android:usesCleartextTraffictrue或者在network_security_config.xml里专门为后端IP放开明文流量。我以前帮人排查过半天最后就是这个问题模拟器里白屏、日志报错一脸懵。图片加载导致的OOM如果后端返回的图片过大没有压缩RecyclerView快速滑动时容易内存暴涨。Glide本身有内存缓存但还是建议后端在上传时做一次压缩或者Glide加载时用override(400, 400)限制解码尺寸。列表滚动图片错位这个一般是ViewHolder复用导致的旧图片还没加载完成新图片就覆盖上去了。解决方法是Glide的.placeholder()先占位同时每个Item的图片位置在onBindViewHolder时先置为默认占位图再异步加载。这些小问题在源码里如果已经处理了说明项目质量不错如果没处理正好是你在文档和讲解视频里可以补充的“优化点”答辩效果反而更好。4. 运行环境搭建与调试排错4.1 本地环境准备JDK、MySQL、Android Studio这个项目要跑起来环境版本必须对齐否则会出现各种玄学报错。我建议按以下组合准备基础环境组件建议版本说明JDKJDK 8 或 JDK 11Spring Boot 2.x需要部分新版Spring Boot 3需要JDK 17先看源码pom.xmlMaven3.6用于后端依赖管理IDEA自带也可MySQL5.7 或 8.0记得设置utf8mb4字符集避免中文乱码Android StudioHedgehog 或较新版本需要能下载SDK、配置模拟器Android SDKAPI 28编译版本看项目的build.gradle后端启动步骤通常是这样用IDEA打开后端项目等待Maven下载依赖修改application.yml里的数据库用户名密码在Navicat或命令行里执行数据库脚本最后运行主类。看到控制台输出Tomcat started on port(s): 8080字样后端就起来了。可以用浏览器或Postman先调一下/api/pet/list接口返回JSON说明接口正常。这里有一个常见心态问题如果你用的MySQL 8.0而项目依赖的mysql-connector-java版本是5.x启动时可能会报Public Key Retrieval is not allowed或者时区错误。前者在JDBC URL加allowPublicKeyRetrievaltrue即可后者加serverTimezoneAsia/Shanghai即可。这些小坑在运行视频里一般会演示但自己遇到时要知道根因在哪。4.2 常见启动报错与排查实录我把实际运行中最高频的前5个问题整理成表格遇到时可以先对号入座现象原因解决方案后端启动即报数据库连接失败数据库密码错误或MySQL服务没启动检查application.yml确认账号密码Windows下用net start mysql确认服务接口返回404请求路径不对或Controller没扫描到确认启动类SpringBootApplication的位置Controller必须在启动类的同包或子包Android请求失败timeout模拟器/真机访问不到后端IP模拟器用10.0.2.2真机用电脑局域网IP并关闭防火墙图片显示不出来访问路径没映射或URL拼接错误后端访问/uploads/xxx.jpg测试App端检查baseUrl拼接中文乱码数据库字符集不是utf8建库SQL用DEFAULT CHARSETutf8mb4JDBC URL加characterEncodingutf8排查问题的方法论比具体答案更重要。我自己的习惯是用Postman绕过Android端先测后端。如果Postman接口通、Android端不通问题大概率在Android端网络配置如果Postman都不通那就是后端或数据库的事。千万别在Android Studio里反复点运行然后看一堆日志猜浪费时间。4.3 模拟器真机联调网络配置Android模拟器访问宿主机后端有两点很容易踩Android Studio自带模拟器中10.0.2.2专门指代宿主机不是127.0.0.1。所以Retrofit的baseUrl应该写成http://10.0.2.2:8080/。如果后端跑在远程服务器或另一台电脑上baseUrl要写服务器公网IP或局域网IP如http://192.168.1.100:8080/同时确保手机和电脑在同一WiFi下电脑防火墙放行8080端口。真机调试时Android设备的网络命名空间和电脑是不同的localhost对设备而言指向它自己。这一点我在帮人看代码时反复强调过项目源码里如果写死了http://localhost:8080模拟器上可能恰好能用因为模拟器root环境里localhost一般映射到模拟器自身而不是宿主机其实通不通取决于实现严格说是不通的你跑到真机上百分之百连不上后端。所以拿到源码第一件事就是全局搜一下localhost、127.0.0.1看到就改成10.0.2.2或局域网IP。另外Android 9明文HTTP限制很容易被忽略。就算你接口地址写对了如果没在AndroidManifest.xml声明usesCleartextTraffictrue网络层依然会被拦。这个我在3.4节已经提过这里再强调一次两个条件必须同时满足一是地址可达二是明文请求被允许。5. 这套源码/文档的利用方式与扩展思路5.1 文档、运行视频、讲解视频怎么配合看标题里写了“源码文档运行视频讲解视频”很多同学不知道这些东西该怎么配合容易一会儿翻文档一会儿看视频效率很低。我的建议是分三轮使用第一轮先看运行视频只看“最终效果”建立对系统的整体感知。你不需要关心代码怎么写的就看页面长什么样、功能有哪些心里有个底。第二轮再看文档中的部署章节按照文档把项目跑起来。这一步的重点是“复现运行效果”遇到环境问题优先对照文档的FAQ部分。第三轮再看讲解视频结合已经跑起来的代码去理解技术点比如Controller怎么接受参数、Service怎么做事务管理、Android端怎么封装请求。特别提醒文档如果你手头这份是PDF或Word不要指望里面全是“原创内容”多数情况下它就是代码的注释版说明。真正有价值的是数据库设计说明和接口文档这两部分能帮你快速回答答辩老师关于“你们系统怎么设计的”这类问题。我建议拿到源码之后自己花半天时间画一张简单的业务流程图把角色、页面、接口、表串起来这张图比文档里任何一张图都更能体现你确实理解了项目。5.2 从“能跑”到“能答辩”功能增强点很多项目初始能跑但功能比较寡淡答辩时容易冷场。我列出几个成本不高、但收益明显的增强点你可以按需选择验证码登录或注册后端用Redis存验证码Android端填写手机号获取验证码前端倒计时60秒。这个功能能体现Redis的应用是加分项。宠物收藏/关注功能在宠物详情页加一个“收藏”按钮用户收藏过的宠物出现在个人中心的收藏列表。只需要一张favorite表接口加两个就够但会让系统显得更像真实产品。申请审核的消息通知管理员审核通过或驳回后用户端“我的申请记录”页面状态刷新即可但如果你实现了一个简单的轮询或本地通知体验会好很多也能讲“为什么用轮询而不用WebSocket”这类问题。宠物筛选与搜索列表页增加按品种、年龄、性别筛选的下拉框后端加几个可空参数即可。用户找宠物时体验完全不同。这些增强不需要重构代码都是在现有Controller和Activity上加点东西。但注意不要一个功能改完又重新跑老半天建议每加一个功能就编译一次避免最后一起改出几十个编译错误没法定位。5.3 加分项代码规范、日志、权限安全从“完成项目”到“高质量项目”差别往往不在功能而在工程素养。代码层面要做到Controller里不写业务逻辑、Service层接口和实现分离、方法命名用动词名词如getPetById、approveAdoption、统一返回对象不要出现裸的Map或JSONObject。日志层面至少要在关键业务节点打日志比如用户注册、登录、提交申请、管理员审核用log.info或log.error记录关键参数和结果方便答辩现场演示排查问题过程。权限安全这块尤其值得关注。这个项目里管理员接口必须做角色校验不能让普通用户直接调用/api/admin/pet/save。最简单的实现是后端拦截器拦截/api/admin/**路径从Token里解析用户角色如果角色不是ADMIN就返回403。同时密码存储不要用明文至少用MD5或BCrypt加密。很多源码偷懒存明文答辩时老师一眼就能看出问题。你把密码改成BCrypt加密再将一句“实际项目中密码切不可明文存储所以这里用BCrypt”写进讲解视频里项目层次立刻就不一样了。最后一个实际的项目维护建议把后端上传的uploads目录加入.gitignore不要随着代码一起提交否则每次拉代码都可能覆盖别人的图片。数据库脚本里也应包含一组清晰的初始化测试账号比如admin/admin123、user/user123并在文档中注明这样评委或同事体验系统时不用先去注册效率高很多。我从这类项目里最大的体会就是源码能不能跑是底线而你能不能讲清楚“为什么这么设计”才是分水岭。比如为什么要用状态机管理领养流程、为什么文件访问要单独映射路径、为什么Android端要封装网络层这些都不是题干上的字却是整个项目真正值得复盘的地方。如果你打算把这个项目当毕业设计用我建议在跑通之后自己动手把领养申请的事务逻辑重新写一遍能写完一遍这个项目才是真正属于你的。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进