ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

流浪宠物救助系统Spring Boot实战:从业务设计到部署

流浪宠物救助系统Spring Boot实战:从业务设计到部署 1. 先想清楚需求流浪宠物救助系统到底在管理什么很多人拿到流浪宠物领养救助管理系统这类Spring Boot项目第一反应是不就是宠物信息的增删改查嘛。但真正动手之后会发现宠物信息只是最表层的东西。这个系统的核心不是记录一只猫而是管理一条生命从被发现到进入家庭的全过程这里的业务复杂度远比你想象得高。1.1 管理对象的本质宠物、救助人、领养人、志愿者四条业务线我先把这个系统的业务角色拆开你才知道数据库要建哪些表、接口要分哪些模块。宠物这是系统的主体。但一只流浪宠物从被救助那一刻起它身上就挂着三个状态待救助、在救助站或寄养家庭、已领养。这三个状态背后还有健康记录、疫苗接种记录、绝育记录等信息需要持续更新。很多初学者把宠物表设计成只有一个status字段结果后期想查这只猫打过几次疫苗都得现写SQL这种表结构撑不起真实业务。救助人发现流浪动物的人。现实中这个角色往往是普通市民他们最核心的需求是快速提交求助信息包括位置、照片、动物状态描述。救助人不需要登录以游客身份提交即可。这就意味着系统的暴露面要比一般管理系统大必须考虑提交频率限制和图片安全审核。领养人想领养宠物的人。领养人需要实名信息因为领养不是拿走而是承诺所以系统要记录领养人的身份信息、居住条件、养宠经验。审核过程可能需要管理员线下联系核实系统里要留好联系方式。志愿者/管理员救助站的日常运营者。志愿者负责审核救助信息、更新宠物健康档案、处理领养申请、回访已领养宠物。管理员则额外兼顾用户管理、数据统计、公告发布。这四条线不是独立的它们之间的交互才是系统最有价值的地方救助人提交求助 → 志愿者核实并建档 → 宠物信息展示 → 领养人提交申请 → 审核 → 领养成功 → 后续回访。每一步都在产生业务数据这些数据合起来构成了完整的救助闭环。1.2 救助-领养闭环中容易被忽略的状态流转我见过不少实现把宠物状态做成一个简单的下拉框改了就直接保存。这种做法和业务真实流程严重脱节——现实中一只流浪猫从被救起那天起状态变化是有严格顺序限制的。举例一只受伤的猫进入系统初始状态是待救助。志愿者核实后状态变为在站同时建档包括照片、预估年龄、健康初评。有人看中它提交领养申请后状态不能直接从在站跳到已领养中间必须经过一个审核中或待回访的阶段。领养人把猫接走之后也不是结束几周后的回访结果会影响最终状态是已领养还是退回退回到在站。这个状态机如果靠if-else硬写在Controller里后期维护真的很痛苦。我的建议是单独维护一张状态流转配置表或者至少用一个枚举类把允许的流转路径写清楚。比如public enum PetStatus { PENDING(待救助, Arrays.asList(IN_SHELTER)), IN_SHELTER(在站, Arrays.asList(ADOPTING, RETURNED)), ADOPTING(审核中, Arrays.asList(ADOPTED, IN_SHELTER)), ADOPTED(已领养, Arrays.asList(RETURNED)), RETURNED(已退回, Arrays.asList(IN_SHELTER)); // ... }这样设计的好处是状态流转逻辑集中管理前端拿到的状态永远合法后期加状态比如治疗中只需改动一处而不是全项目搜常量。1.3 管理员的日常工作流决定了权限模型说到权限设计很多毕设就是简单地分管理员和普通用户然后管理员全部权限、普通用户只能看。但对救助站来说真实场景要细得多。志愿者需要能更新宠物健康档案但不应该能删除发布记录审核领养申请是站长的活志愿者只有查看权数据统计分析页面普通志愿者也不应该看到。我用Spring Security JWT做三种角色ADMIN、VOLUNTEER、USER领养人。注意救助人的角色其实可以并入USER因为救助人提交信息注册之后也可能变成领养人没必要单开。这个权限模型在后端实现上建议用Spring Security的方法级注解而不是在Controller里手动判断role。比如审核领养申请的方法PreAuthorize(hasRole(ADMIN)) PostMapping(/adoptions/{id}/approve) public Result? approveAdoption(PathVariable Long id) { // 审核逻辑只有管理员能调用 }方法级注解的好处是权限规则和业务代码在一个地方读代码的时候一眼就明白谁有资格调用。配合全局异常处理权限不足时返回统一的JSON结构前端弹提示也更方便。提示权限校验不要依赖前端隐藏按钮来实现安全。前端隐藏只是用户体验优化真正的安全边界一定在后端方法级拦截否则用Postman直接调接口就能越权。2. 技术选型与项目结构Spring Boot生态下怎么组合最省心技术选型这一步决定你后面一个月是轻松还是痛苦。我的原则很简单主流技术栈 不整花活。主流的定义是文档多、社区问答多、你自己或者同学踩过坑之后有现成解决方案。花活指那些小众框架、非主流版本组合。2.1 前端方案Vue Element UI拆开还是聚合这个系统我推荐用Spring Boot Vue Element UI的前后端分离方案但部署时有两种选择。第一种选择是完全分离前端独立部署在Nginx后端Spring Boot打包成Jar跑在服务器上通过配置跨域访问。这种方案胜在结构清晰前后端各自独立更新适合团队分工。但大多数人做毕设是单人开发本地开发要同时起一个后端通常8080端口和一个前端通常8081端口联调时还要处理跨域多少有点浪费精力。第二种选择是我更推荐的开发时分离部署时合并。具体做法是后端和前端目录放在同一个仓库里开发时用IDEA同时跑后端和前端前端用devServer代理接口到后端打包时用Maven把Vue构建出来的dist目录复制到Spring Boot的static目录下最终只产出一个Jar包。这样做部署和演示都轻松演示时只要启动一个Spring Boot进程浏览器打开一个端口就能看全站。Vue打包放进Spring Boot中的具体操作也不复杂在pom.xml里加一个插件配置plugin groupIdcom.github.eirslett/groupId artifactIdfrontend-maven-plugin/artifactId version1.12.1/version configuration workingDirectoryfrontend/workingDirectory installDirectorytarget/installDirectory /configuration executions execution idinstall-node-and-npm/id goals goalinstall-node-and-npm/goal /goals configuration nodeVersionv16.20.0/nodeVersion npmVersion8.19.4/npmVersion /configuration /execution execution idnpm-build/id goals goalnpm/goal /goals configuration argumentsrun build/arguments /configuration /execution /executions /plugin然后把Vue的dist目录拷贝到Spring Boot的src/main/resources/static下这样打了包之后前端页面和后端接口就在同一个进程里了。2.2 后端核心组件真的用得上热搜词里的这些技术吗我在搜索引擎看到一堆热搜词MinIO、Redis、ActiveMQ、HanLP、Kettle、金仓数据库、Mosquitto全都有人和Spring Boot绑在一起。但落到流浪宠物救助系统这个场景里不是每个都该上。MyBatis Plus必须上。它的BaseMapper让你不用写基础的增删改查SQL单表操作效率提升一大截。宠物信息、领养申请这种典型单表查询用它的QueryWrapper直接搞定只有复杂统计才需要手写XML。Redis强烈建议上。用法有几个一是给领养人的手机验证码做存储5分钟有效期配合过期时间自动失效二是缓存首页宠物列表和公告三是做领养申请提交的频率限制同一个IP一分钟只能提交一次。这些都是Redis的典型场景面试官问起来你也有话说。MinIO建议上。宠物照片上传是刚需MinIO是开源的S3兼容对象存储10分钟能部署起来比你本地磁盘保存文件再配置虚拟路径映射省心几十倍。后面单开一节细说。ActiveMQ / Mosquitto / Kettle在这个项目里属于加分但不是必需品。ActiveMQ可以做领养申请审核通过后的站内信通知但用小规模场景里Redis的发布订阅就够用了Mosquitto是MQTT Broker除非你要做物联网喂食器之类的IoT扩展否则没必要Kettle是ETL工具和数据抽取转换有关这个系统和它关系不大。金仓数据库如果是国产化工控场景才需要适配正常毕设直接用MySQL就行。热搜里出现springboot 金仓读写分离配置说明确实有人在项目里用国产库但那是另一类需求不是流浪宠物项目的主要矛盾。这套选型思路背后有一个通用判断标准技术选型由业务场景驱动而不是由热门词驱动。聊天、搜索、消息队列都很热但你的业务里根本没有海量消息要异步处理硬上ActiveMQ只会增加部署复杂度和答辩风险。2.3 分包结构避免Controller写成上帝类我用了一年多Spring Boot之后总结出一个比较舒服的分包方式com.example.petadoption ├── controller # 接口层只做参数接收和返回不写业务逻辑 ├── service # 业务层事务边界在这里 │ └── impl # 业务实现 ├── mapper # MyBatis Plus的Mapper接口 ├── entity # 数据库实体类 ├── dto # 前端传输对象避免直接把实体暴露给前端 ├── vo # 视图对象组合多表查询结果时用 ├── config # 配置类安全配置、跨域、Redis、MinIO客户端 ├── common # 通用返回结构、异常处理、常量、工具类 └── aspect # AOP切面如日志记录我特别想强调dto和vo的使用因为很多人图省事直接把entity返回给前端。这样做短期看着方便后面只要实体字段一变前端接口返回结构就变了前后端联调全是坑。而且实体类里如果加了TableField(exist false)的冗余字段会被意外序列化给前端带来数据安全问题。正确做法是为关键接口定义清晰的VO。比如宠物详情页需要返回宠物基本信息 健康记录列表 救助人称呼脱敏。那就在service里组装好再返回前端只负责渲染不用自己调三个接口拼数据体验也好不少。3. 数据库设计几张核心表和那段关键的状态机代码数据库设计是这类系统最容易暴露水平的地方也是答辩老师最爱深挖的环节。我直接把这套系统的核心表结构和设计思路整理出来。3.1 宠物信息表字段设计决定了后期开发的效率宠物信息表是核心中的核心字段设计的好坏直接影响查寻的便利性。先看我的建议表结构简版CREATE TABLE pet ( id bigint NOT NULL AUTO_INCREMENT COMMENT 宠物ID, name varchar(50) DEFAULT NULL COMMENT 宠物昵称, species varchar(20) NOT NULL COMMENT 物种CAT/DOG/OTHER, breed varchar(50) DEFAULT NULL COMMENT 品种如橘猫、金毛, gender tinyint DEFAULT NULL COMMENT 性别1公 2母 0未知, age_month int DEFAULT NULL COMMENT 预估月龄便于按年龄筛选, status varchar(20) NOT NULL COMMENT 状态PENDING/IN_SHELTER/ADOPTING/ADOPTED/RETURNED, health_status varchar(255) DEFAULT NULL COMMENT 健康概况如健康/治疗中, sterilized tinyint DEFAULT NULL COMMENT 是否已绝育1是 0否, vaccinated tinyint DEFAULT NULL COMMENT 是否已接种疫苗, avatar_url varchar(255) DEFAULT NULL COMMENT 封面图URL, description text COMMENT 详细描述、性格特点, rescue_time datetime DEFAULT NULL COMMENT 救助时间, found_location varchar(255) DEFAULT NULL COMMENT 发现地点方便定位流浪高发区域, admin_id bigint DEFAULT NULL COMMENT 负责志愿者ID, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, deleted tinyint DEFAULT 0 COMMENT 逻辑删除标记, PRIMARY KEY (id), KEY idx_status (status), KEY idx_species (species) ) ENGINEInnoDB COMMENT宠物信息表;有几个容易被忽略但很重要的细节age_month用数字而不是字符串。我见过有人用3个月1岁这种文本框存年龄导致后端没法按年龄区间搜索只能全表查出来在内存里过滤。用数字存月龄按年龄筛选就是一秒的事。逻辑删除不是物理删除。救助站运营过程中难免有宠物信息登记错误需要删除。但如果用物理删除这张表的历史记录就没了后面统计分析比如这个季度救助了多少只猫数据就会对不上。所以加一个deleted字段默认0删除时置为1表面上看是删了数据其实还在。MyBatis Plus里这个字段配TableLogic注解即可。3.2 领养申请表一条申请记录的完整生命周期CREATE TABLE adoption_apply ( id bigint NOT NULL AUTO_INCREMENT, pet_id bigint NOT NULL COMMENT 申请的宠物ID, user_id bigint NOT NULL COMMENT 领养人用户ID, apply_reason varchar(500) COMMENT 领养理由, has_pet_experience tinyint DEFAULT NULL COMMENT 是否有养宠经验, has_children tinyint DEFAULT NULL COMMENT 家中是否有儿童, housing_type varchar(20) DEFAULT NULL COMMENT 住房类型RENT/OWN, status tinyint DEFAULT 0 COMMENT 0待审核 1通过 2拒绝 3已取消, audit_remark varchar(255) COMMENT 审核意见, audit_time datetime DEFAULT NULL, adopt_time datetime DEFAULT NULL COMMENT 实际接回宠物的时间, follow_up_status tinyint DEFAULT NULL COMMENT 回访状态0待回访 1通过 2退回, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_pet_id (pet_id), KEY idx_user_id (user_id) ) ENGINEInnoDB COMMENT领养申请表;这里的关键是follow_up_status字段。领养不是人把猫带走就算结束救助站要做回访确认宠物在新家的生活状态。我在实际设计时加了一个待回访队列管理员能看到所有超过两周还没回访的领养记录提醒志愿者去联系领养人。这个小设计在答辩时是个很好的加分项充分体现你对领养概念理解得比一般人深。更有意思的是当回访发现领养人无法继续抚养时领养记录状态变为退回宠物表的状态要自动回到IN_SHELTER。这种跨表状态联动用MySQL的事务在Service层实现Transactional(rollbackFor Exception.class) public void returnPet(Long applyId, String reason) { AdoptionApply apply adoptionApplyMapper.selectById(applyId); apply.setStatus(2); // 已退回 apply.setAuditRemark(reason); adoptionApplyMapper.updateById(apply); Pet pet petMapper.selectById(apply.getPetId()); pet.setStatus(PetStatus.IN_SHELTER.name()); petMapper.updateById(pet); }3.3 求助、救助记录和志愿者值班安排除了领养系统的另一半价值在救助侧。这个模块至少有这三张表rescue_request求助信息表记录救助人游客提交的求助信息字段包括位置、宠物类型描述、照片路径、受伤程度描述、联系方式。管理员或志愿者审核这类信息需要及时性所以设计上要有未处理清单功能。rescue_record救助记录表志愿者实际出勤的记录。字段包括宠物ID可空因为有些求助经评估不需要救助、志愿者ID、处理时间、处理结果、消耗物资记录。这张表的价值在于月底统计每个志愿者的出勤次数。volunteer_schedule志愿者排班表如果救助站有线下值班需求可以用这个表管理每周的值班安排。表字段是志愿者ID、值班日期、班次上午/下午/全天。求助信息审核建议加一个简单的内容安全校验比如检查描述文本里是否包含联系方式以外的敏感信息防止有人借平台发布广告。在Spring Boot里可以用AOP或者过滤器在提交接口上统一做敏感词过滤把命中敏感词的内容自动转为待人工审核状态。4. 项目搭建实战从IDEA配置到第一个接口跑通这部分我尽量还原真实的搭建过程包括那些容易卡壳的版本问题和配置项。这篇完全是一步步操作照着走就能跑起来。4.1 环境准备IDEA、JDK、Maven版本怎么搭才不折腾先把环境版本说清楚因为我踩过版本不匹配的坑JDK用了17但项目编译级别还停留在8结果各种奇奇怪怪的报错。我的建议组合JDK 17或21但17最稳妥Maven 3.8用IDEA自带的也行但建议单独装一个配好镜像源IDEA 2023社区版就够了MySQL 8.0别用5.7了8.0是现在的主流Redis 6.x/7.xNode.js 16Vue 2或Vue 3看个人习惯如果是新项目建议直接Vue 3关于IDEA 2026怎么配置Spring Boot服务这类问题其实就是Run Configuration里的设置新建Spring Boot启动配置选择主类设置JVM参数不需要特殊参数端口在application.yml里配置server.port不需要在IDEA里单独设置启动端口。如果你用的是Maven打包的可执行Jar就更不需要IDEA配置端口了直接改yaml文件。Maven的settings.xml要配置阿里云镜像不然Spring Boot依赖下载速度会让人怀疑人生mirror idaliyunmaven/id mirrorOfcentral/mirrorOf name阿里云公共仓库/name urlhttps://maven.aliyun.com/repository/public/url /mirror4.2 初始化项目Spring Initializr和手工添加依赖用IDEA的Spring Initializr创建项目时先勾这几个依赖Spring WebSpring Security后面权限用得上先加了以防后期难集成MyBatis Plus Framework在Initializr里可能搜不到需要去Maven中央仓库复制最新版本MySQL DriverSpring Data Redis创建完成后pom.xml里关键依赖如下dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency dependency groupIdio.minio/groupId artifactIdminio/artifactId version8.5.7/version /dependency然后配置application.ymlserver: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/pet_adoption?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 timeout: 3000ms mybatis-plus: global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpllogic-delete-field配置是MyBatis Plus实现逻辑删除的关键配置好之后所有deleteById操作都会自动变成update ... set deleted 1。4.3 第一个接口宠物列表分页查询把启动类跑起来之后做一个最简单的宠物分页查询接口来验证整个链路RestController RequestMapping(/api/pets) public class PetController { Resource private PetService petService; GetMapping(/page) public ResultIPagePetVO page( RequestParam(defaultValue 1) Integer current, RequestParam(defaultValue 10) Integer size, RequestParam(required false) String species, RequestParam(required false) String keyword) { PagePet page new Page(current, size); PagePetVO result petService.queryPetPage(page, species, keyword); return Result.success(result); } }这段代码看着简单但有个细节分页对象泛型从Pet转成了PetVO。如果你直接在Service里返回PagePet前端拿到的是数据库原始字段一些不该暴露的字段比如deleted标记会漏出去。我通常在Service实现里用BeanUtils.copyProperties做转换后返回VO。到这里项目的基础骨架已经能跑了能启动、能连库、能返回JSON。下一步要做的是把核心业务逐个接进来。5. 关键技术难点拆解图片上传、搜索、消息通知怎么落地跑通CRUD只是热身真正让这个系统像个产品的是下面这几个功能模块。每个模块都有隐藏的坑我逐个说明。5.1 MinIO对象存储宠物照片怎么存、怎么访问、怎么防越权宠物照片的上传和访问是救助系统最直接影响用户体验的功能。救助人提交求助时往往在户外手机网络不好照片上传如果设计得不好失败重试会让人崩溃。先解决存储选型。本地磁盘存储的问题是后续扩展麻烦服务器磁盘满了怎么办多台服务器部署时照片不同步怎么办MinIO是S3协议的对象存储部署简单一个二进制文件就能启动社区版免费而且和Spring Boot集成几乎是标配操作。启动MinIO开发环境用Docker最快docker run -d --name minio \ -p 9000:9000 -p 9001:9001 \ -e MINIO_ROOT_USERminioadmin \ -e MINIO_ROOT_PASSWORDminioadmin \ minio/minio server /data --console-address :9001Spring Boot里的MinIO配置类Configuration public class MinioConfig { Value(${minio.endpoint}) private String endpoint; Value(${minio.accessKey}) private String accessKey; Value(${minio.secretKey}) private String secretKey; Bean public MinioClient minioClient() { return MinioClient.builder() .endpoint(endpoint) .credentials(accessKey, secretKey) .build(); } }上传接口的核心逻辑public String uploadFile(MultipartFile file) throws Exception { // 校验文件类型和大小 String originalFilename file.getOriginalFilename(); String suffix originalFilename.substring(originalFilename.lastIndexOf(.)); if (!Arrays.asList(.jpg, .jpeg, .png, .gif).contains(suffix.toLowerCase())) { throw new BizException(仅支持图片格式); } if (file.getSize() 5 * 1024 * 1024) { throw new BizException(图片大小不能超过5MB); } // 生成不重复的文件名 String datePath LocalDate.now().format(DateTimeFormatter.ofPattern(yyyyMMdd)); String fileName datePath / UUID.randomUUID() suffix; // 上传到MinIO minioClient.putObject(PutObjectArgs.builder() .bucket(pet-images) .object(fileName) .stream(file.getInputStream(), file.getSize(), -1) .contentType(file.getContentType()) .build()); return fileName; }这里有几个关键点值得说。文件命名用UUID加日期路径避免中文文件名乱码问题也方便日志排查。桶名要预先创建好权限设置成私有。那么问题来了私有桶的文件前端怎么访问答案是预签名URL。给前端返回的不是直接的图片地址而是一个带有效期的临时访问链接String url minioClient.getPresignedObjectUrl( GetPresignedObjectUrlArgs.builder() .method(Method.GET) .bucket(pet-images) .object(fileName) .expiry(7, TimeUnit.DAYS) .build());过期时间设置一周基本上够用户浏览和领养人查看图集了。这样设计的好处是别人即使拿到了图片地址过期后链接直接失效防止图片被爬虫批量抓取。这个细节在答辩时可以展开讲讲说明你考虑了文件访问安全问题。5.2 搜索和标签推荐用笨办法实现90分的体验热搜词里出现了HanLP分词在Spring Boot这类需求。如果你的系统要做到宠物搜索的模糊匹配比如搜橘猫能查出所有品种为橘猫或描述里含有橘猫的宠物最朴素的做法是SQL的LIKE查询SELECT * FROM pet WHERE description LIKE CONCAT(%, #{keyword}, %) OR breed LIKE CONCAT(%, #{keyword}, %)这个方案在数据量几千条时完全够用根本不需要引入Elasticsearch或者HanLP分词。真实救助站一个城市的存量宠物数据可能也就几百到几千条硬上搜索引擎属于过度设计。不过搜索体验还有一个升级空间标签系统。给宠物打上标签#亲人、#已绝育、#适合有经验家庭、#需长期照顾领养人根据标签筛选效果和搜索一样甚至更好。实现方式也简单宠物表加一个tags字段存逗号分隔的标签字符串列表页按标签筛选时用SQL的FIND_IN_SETSELECT * FROM pet WHERE FIND_IN_SET(亲人, tags) 0这个方案的性能在万条数据内没问题足够覆盖这个项目的真实场景。5.3 领养进度通知用Redis实现简单的消息通道领养人提交申请之后最关心的是我的申请有没有被看到、有没有通过。我见过不少系统做一个我的申请列表让用户刷新页面看状态。这种方案不是不能用但体验不够好——如果救助站志愿者审核信息慢领养人盯着页面看半天没变化体验很差。考虑到不是每个项目都需要引入完整的WebSocket套件但Spring Boot整合WebSocket也只是加个依赖的事我这里提供两档方案方案一轻量Redis发布订阅做站内信提醒。审核通过或拒绝时管理员操作时发一条消息到Redis的channel后端监听channel后把消息写入站内信表。用户下次登录拉取未读消息时看到新通知。这个方案不需要维持长连接实现成本低且Redis你反正已经引入了没有额外运维负担。方案二进阶WebSocket推送。如果希望用户在页面上实时收到通知比如审核一通过页面立即弹窗就使用Spring Boot的WebSocket。实现细节是用户登录成功后前端建立WebSocket连接后端维护一个用户ID到Session的映射。审核状态变化时推送消息给指定用户。关于这两组方案我实际推荐方案一原因很现实WebSocket在Nginx后面要配代理升级协议万一部署环境没配好连接半天建不上排查成本高。Redis订阅方案走正常的HTTP轮询或请求拉取链路简单、容易排查。提示消息通知的核心价值是用户不需要频繁刷新页面但实现时别把后端做成轮询地狱。在Redis方案里可以用前端定时拉取比如每30秒请求一次未读消息这个频度对服务器压力很小完全在可接受范围。5.4 手机验证码登录和防刷领养人、救助人注册时用手机验证码登录是比较安全的方案也符合现在的主流习惯。流程是用户提交手机号 → 后端生成6位随机码存入Rediskey为sms:code:{phone}TTL 5分钟→ 调用短信服务商接口阿里云短信、腾讯云短信都行测试时可以接一个Mock→ 用户输入验证码 → 后端校验并把Redis里的值删除一次性成功则发放JWT。防刷机制别漏了。我在/api/auth/sendCode这个接口上加了两层限制一是同一个手机号60秒内只能发送一次Redis记录上次发送时间二是同一个IP一天最多发送10条验证码Redis的INCR实现计数器。这两层防线能挡住99%的恶意刷短信行为答辩讲出来也很亮眼。6. 部署与上线打包、Docker、以及那些坑不得的事项目做到这一步功能上已经完整了。但如果你想把项目部署到服务器上或者至少在演示时显得更专业下面这些内容就很重要。6.1 打包细节Jar包体积优化和启动配置前端构建产物塞进Spring Boot的static目录后整体打出来的Jar包可能会在60MB到100MB之间含依赖。这个体积对云服务器没压力但每次上传到服务器会比较慢。我建议用Maven的spring-boot-maven-plugin做分层打包。默认配置打出来的是胖Jar结构是BOOT-INF/lib下面全是依赖方便是方便但改一点代码就要全量上传。分层打包之后lib层和classes层分开上传时Docker能复用不变的层部署速度提升明显。实际上很多同学到部署这步都直接放弃Docker改在服务器上裸跑Jar。不是不行但我想说这个项目用Docker Compose编排来跑才是既省心又加分的做法——在服务器上把所有服务MySQL、Redis、MinIO、Spring Boot应用打包成一组容器一个命令全部起环境问题几乎不会出。Docker本身在这个系统里非必需但加入工程化元素能显著提升整体完成度。6.2 Docker Compose编排一键启动全栈环境docker-compose.yml大致长这样关键部分version: 3.8 services: mysql: image: mysql:8.0 container_name: pet-mysql environment: MYSQL_ROOT_PASSWORD: root123456 MYSQL_DATABASE: pet_adoption ports: - 3306:3306 volumes: - ./mysql-data:/var/lib/mysql redis: image: redis:7 container_name: pet-redis ports: - 6379:6379 minio: image: minio/minio container_name: pet-minio command: server /data --console-address :9001 ports: - 9000:9000 - 9001:9001 volumes: - ./minio-data:/data app: build: . container_name: pet-app ports: - 8080:8080 depends_on: - mysql - redis - minio environment: SPRING_PROFILES_ACTIVE: prod配合项目根目录的DockerfileFROM maven:3.8-openjdk-17 AS builder WORKDIR /app COPY . . RUN mvn clean package -DskipTests FROM openjdk:17-jre-slim WORKDIR /app COPY --frombuilder /app/target/*.jar app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, app.jar]多阶段构建能把最终镜像的体积控制住这个细节踩坑之后我强烈建议所有做毕设的都这么写。第一次构建能成功不代表后续每次都成功——Docker缓存、镜像源不稳定都可能卡住建议先把镜像源配置成国内源再开始。6.3 实际部署中最容易遇到的5个问题我总结一下常见问题你部署时先排查这些能省很多时间。数据库连不上检查数据库容器的网络模式。如果Spring Boot应用也在容器里localhost指向的就不是MySQL容器而是应用自己的容器。需要在application-prod.yml里把数据库地址改成服务名比如jdbc:mysql://mysql:3306/pet_adoption。Redis端口通但连接被拒确认Redis容器是否绑定了正确的端口映射并且Spring Boot里没有配置错误的密码。如果Redis容器没设密码且对外暴露6379端口会有被入侵风险部署环境建议加密码和保护模式。图片上传后访问404大概率是MinIO桶的访问策略问题。私有桶要走预签名URL但如果代码里生成URL的endpoint写的是localhost而前端从远程访问那么生成的URL对用户来说就是无效的。解决方式把MinIO的访问地址配置成服务器对外IP或域名。Jar包启动后页面白屏检查static目录下是不是有index.html以及Spring Boot是否配置了页面跳转。一般把Vue构建的dist内容放到static后访问根路径就能看到页面。如果不行加一个Controller跳转到forward:index.html。内存不够云服务器建议至少2G内存。MySQL、Redis、MinIO、Spring Boot四个服务同时跑起来1G内存会非常紧张甚至OOM。本地开发电脑跑这套组合倒是没压力但云服务器确实要关注这个指标。7. 我从这个项目里学到的做完之后的复盘与扩展方向到这里整个流浪宠物领养救助管理系统从业务设计到技术选型、从数据库设计到部署上线核心内容和注意事项都讲完了。最后聊几句我的真实体会。做这类管理系统最容易犯的毛病是只做CRUD不思考业务。代码写得再熟练如果不知道流浪宠物救助站是怎么运转的——谁先审核、谁负责回访、状态怎么流转、领养为什么要有审核流程——做出来的东西就只是个花架子。真正能打动答辩老师和使用者的永远是你对业务场景的理解深度而不是用了多少技术名词。这个项目后续如果要扩展我建议优先考虑两个方向一是地图可视化接入地图组件把流浪动物发现地点和救助站位置标出来方便志愿者规划出勤路线二是自动化报表用定时任务每天统计领养率、救助成功率、退回率等指标给救助站运营做决策参考。这两个方向都是从实际需求出发加不了几行代码但能显著提升系统价值。最后说一个我觉得很实用的技巧把项目的设计思路和关键决策写成开发文档。不用写多正式就记录为什么用MinIO不用本地存储为什么领养状态要设计成状态机为什么审核权限只开放给管理员这些决策理由。这份文档是你答辩时的最强辅助也是你将来面试时讲述项目经历的底稿。别忘了代码只能证明你会写设计决策才能证明你会思考。这套流程走下来这个项目就不再是一个平平无奇的毕设了而是一个有完整业务闭环、有技术亮点、有工程化思考的Spring Boot完整项目了。
返回列表