前段时间接了个活儿,给本地一家流浪动物救助站梳理日常管理流程。去了现场才知道,情况比我预想的糟糕得多:动物登记靠纸质表格,领养申请全靠微信群里一条条聊天记录,物资出入库是用Excel记的,经常对不上账。最要命的是,领养人有没有定期回访、动物疫苗什么时候该打第二针,这些完全没有系统记录,全靠负责人脑子里记着。我调研完就决定,与其零敲碎打地补丁,不如直接做一套基于Spring Boot的流浪动物救助站系统,把动物档案、领养审批、回访记录、物资管理这些核心流程一次性搬到线上。这篇文章就从项目落地的完整视角,把需求梳理、技术选型、数据库设计、核心模块实现和实际踩坑的过程都过一遍,希望能给正在做同类毕设、课程设计,或者想用Spring Boot练手完整业务系统的朋友一些参考。
1. 救助站的真实痛点:纸质台账、微信群申请与Excel物资表
在动手写代码之前,我花了两天时间蹲在站里看他们怎么干活,顺便把过去大半年的纸质记录翻了一遍。很多做系统的人容易犯一个毛病:上来就画ER图、建表,根本不关心业务现场长什么样。实际上,救助站这种地方,业务流程有强烈的线下色彩,和电商、OA完全不是一回事。
1.1 一套系统真正要解决什么
先说救助站日常运转会遇到哪些让人头大的事。每周大概会接收十几只流浪动物,每只都要登记发现地点、体况描述、是否受伤、是否驱虫、疫苗打到第几针。纸质表格记完就往文件夹里一塞,三天后再想查这只狗打过狂犬疫苗没有,得翻十分钟资料。
领养环节更崩溃。有意向的人通常在微信里问“那只白猫还在吗”,负责人要手动翻聊天记录确认,再约线下见面。一旦同时有三四个人问同一只猫,基本必乱。领养人填的申请表、身份证复印件、领养协议,全部是纸质版,归档靠夹子,查一个人以前的领养记录得翻半天。
物资这块也不乐观。猫粮狗粮、驱虫药、消毒液入库出库,全在Excel里,谁领了多少、还剩多少,月底一盘点就发现账面和实物对不上。更别说志愿者值班排班,全靠一张贴在墙上的手写表。
所以这套系统的第一个设计原则就是:不追求大而全,先解决“信息分散、查询靠人、流程靠嘴”这三个核心问题。我当时把需求拆成了五大块——动物全生命周期档案、领养申请审批流程、领养后回访记录、物资出入库台账、志愿者值班管理。每一块都能在站里找到对应的“现有痛点”,而不是凭空想出来的功能。
1.2 从现场访谈得出的功能模块地图
有一件事值得单独说:需求访谈时不要只问负责人,还要问一线志愿者。负责人眼里的流程和志愿者眼里的流程经常不一样。比如负责人觉得“领养申请都是线上提交的,很方便”,但志愿者说“很多人不会填表,最后都是我们代为录入”。这个细节直接影响了用户权限设计——系统必须支持“工作人员代填申请”,而且代填的时候要记录操作人。
最终定下来的功能地图大致是这样:
- 动物管理:登记档案、上传照片、维护疫苗/绝育/驱虫状态、变更领养状态
- 领养管理:提交申请、资格初审、安排线下见面、签订协议、完成领养、拒绝申请
- 回访管理:按领养记录生成回访计划,记录回访结果和异常情况
- 物资管理:入库、出库、库存预警、领用记录
- 志愿者管理:值班排班、服务时长统计
- 首页看板:待审核申请数、在养动物数、库存不足提醒、本月领养完成数
模块之间不是孤立的,领养模块要读动物模块的状态,回访模块要挂到领养记录下面,物资模块又跟志愿者领用行为挂钩。这些联动关系,在设计数据库表结构之前必须想清楚,否则后面就是一堆if else硬凑。
2. 技术选型的取舍:为什么是Spring Boot 2.7而不是3.x
技术选型这块,我估计是很多朋友最关心的。毕竟网上Spring Boot 3的教程已经满天飞了,还有人拿它跟Python FastAPI对比,问到底哪个好。我的答案比较朴素:选型不是选“最新”,是选“能稳定跑起来且后续有人能维护”。
2.1 版本选择背后的环境约束
站里的电脑配置很一般,4GB内存的老机器,JDK 8是现成的。这就把Spring Boot 3直接排除了,因为3.x强制要求JDK 17,硬上不是不行,但老机器跑起来内存占用明显更大,而且团队里没人熟悉Java 17的新特性,出了问题排查成本高。
另一个原因是生态兼容性。救助站系统要做文件上传、Excel导出、WebSocket通知,这些周边依赖在Spring Boot 2.7时代已经非常成熟。2.3.x和2.6.x我也简单对比过,2.6之后配置项有调整,而2.7.18是官方维护的最后一个2.x版本,等于安全补丁吃到了最后一刻,性价比是最高的。
新旧版本的选择可以列个表,给还在纠结的人一个直观参考:
| 对比项 | Spring Boot 2.7.x | Spring Boot 3.x |
|---|---|---|
| JDK要求 | 8/11 | 17 |
| 内存占用 | 相对低,256MB能起步 | 相对高,建议512MB以上 |
| 生态成熟度 | 极高,大部分老教程适用 | 部分starter要升级,坑较多 |
| 长期维护 | 已停止新功能,但补丁维护最久 | 持续更新 |
| 适合场景 | 老旧环境、稳妥交付 | 新项目、团队熟悉JDK17 |
如果学校毕设硬性要求Spring Boot 3,那当然可以上3.2.x配上JDK 17,只要别在老服务器上跑就行。但从“稳妥交付一个能长期运行的系统”这个角度,2.7.18加JDK 8是我打心底推荐的高性价比组合。这套组合跑那个救助站的老电脑,内存占用稳定在300MB以内,非常舒服。
2.2 持久层选型:MyBatis-Plus的一站式体验
持久层我选了MyBatis-Plus 3.5.x,没选Spring Data JPA。原因很实际:救助站这类管理系统的业务表,单表增删改查占了八成,剩下的复杂查询主要是多条件筛选和连表统计。MyBatis-Plus用LambdaQueryWrapper写条件查询,几乎不用写SQL,而且分页插件一加,列表接口直接搞定。
JPA当然也写得爽,但它的级联关系和懒加载问题,对新手来说调试成本偏高。救助站系统的实体关系并不复杂,没必要引入一套重量级的ORM思维。反过来,如果用了JPA,一旦碰到N+1查询,日志刷屏的时候排查起来相当痛苦。
配合MyBatis-Plus,我在代码生成上也偷了个懒:用MyBatis-Plus Generator根据表结构自动生成实体类、Mapper接口和Service骨架,再手工补业务逻辑。这样建完表之后,基础CRUD几乎零成本,主要精力可以放在审批流程和状态流转上。
2.3 前端与开发工具的务实安排
前端没上Vue或者React,直接用了服务端渲染模板。原因同样和团队背景有关:救助站没有专职前端,懂一点HTML的人维护模板比维护Node构建链路的成本低得多。Spring Boot集成Thymeleaf或者一个简单的模板引擎,页面用Bootstrap框架,表格、表单、弹窗都够了,加载速度还快。
这里要提一个很多人卡住的问题:IntelliJ IDEA社区版怎么用Spring Boot。社区版确实没有Spring Initializr项目向导,但完全不耽误使用。正确做法是直接上start.spring.io网站,选好版本和依赖,下载zip压缩包,然后在IDEA里File -> New -> Project from Existing Sources,选下载解压后的目录,识别Maven工程,等着依赖下载完就能开发了。我用这个方法开了好几个工程,一点问题没有。
另外提一句接口拆分。好多人纠结第三方接口是不是要单独起一个服务,其实在项目初期完全没必要。我在工程里单独建了openapi包,路径统一用/api/open/开头,配合独立的鉴权拦截器,将来真要开放给合作宠物医院查询领养档案时,直接在这个包里加接口就行,不需要拆服务。
3. 数据库建模:动物档案表、领养申请表与状态流转
数据库设计是整个系统最不能省的部分。救助站业务有很强的线下属性,状态变化多,一个动物从一个状态到另一个状态之间还涉及真实世界里的见面、协议签署,这些都需要在表结构里反映出来。
3.1 两棵核心表怎么设计
第一张核心表是动物档案表animal。字段设计上,除了基础的名字、品种、性别、年龄,还要有救助信息、健康信息、状态信息。参考设计如下:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键,雪花ID |
| animal_no | varchar | 动物编号,如CAT20240001 |
| name | varchar | 动物名字 |
| species | varchar | 品种,猫/狗等 |
| gender | tinyint | 性别 |
| age_month | int | 月龄 |
| vaccine_info | varchar | 疫苗记录 |
| sterilized | tinyint | 是否绝育 |
| dewormed | tinyint | 是否驱虫 |
| health_status | varchar | 健康状态 |
| rescue_date | date | 救助日期 |
| rescue_address | varchar | 发现地点 |
| adopt_status | varchar | 领养状态 |
| description | text | 详细描述 |
| create_time | datetime | 创建时间 |
其中有几个字段值得单独说。vaccine_info不要只存一个“已打疫苗”,应该存“狂犬20240115、猫三联20240201”这种明细文本,方便后续核对。adopt_status是给动物用的状态,后面会详细讲。animal_no这个编号非常有用,线下交流时大家直接报编号就能定位,比报“那只黄色的猫”靠谱多了。
第二张核心表是领养申请表adoption_application。之所以单独立表,而不是在animal表上直接加几个领养人字段,原因很简单:一只动物可能会被多个人申请过,一个人也可能申请多只动物,而且每一次审核、拒绝、取消都是历史记录,需要被追溯。表设计大致是:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| animal_id | bigint | 申请的动物 |
| applicant_id | bigint | 申请人(关联用户表) |
| applicant_name | varchar | 姓名 |
| phone | varchar | 联系电话 |
| id_card | varchar | 身份证号 |
| address | varchar | 住址 |
| pet_experience | varchar | 养宠经验 |
| family_reason | text | 领养理由与家庭情况 |
| application_status | varchar | 申请状态 |
| meeting_time | datetime | 预约见面时间 |
| admin_id | bigint | 审核人员 |
| create_time | datetime | 申请时间 |
| update_time | datetime | 更新时间 |
申请表把申请人的核心信息都冗余了一份,而不是全部靠关联用户表去查。道理很简单:领养审核时工作人员要在列表页快速看到联系方式,每次join一张用户表没有任何必要。冗余字段在这种业务场景里不是设计坏味道,而是查询效率的来源。
3.2 状态机设计:为什么需要“线下见面”这个中间态
这是我认为整个系统最关键的设计决策。救助站工作人员审核通过领养申请之后,距离领养真正完成还差着一大截——领养人必须到站里看动物、确认合得来、签纸质领养协议,这一套流程在线上系统里必须有一个专属状态承接。
我定义了两个状态机,一个管动物,一个管申请。
动物的adopt_status流转是:AVAILABLE(可领养)-> APPLYING(申请处理中)-> RESERVED(已审核通过,预留)-> ADOPTED(已领养)。另外还有UNAVAILABLE(治疗中或暂不可领养)。
申请的application_status流转是:PENDING(待审核)-> APPROVED(审核通过)-> MEETING(线下见面)-> ADOPTED(完成领养),以及分支REJECTED(已拒绝)、CANCELLED(已取消)。
为什么审核通过之后还要拆一个MEETING出来?因为线下见面是一个真实存在且耗时较长的环节,很可能出现审核通过了、人却不来看了的情况。如果没有这个状态,系统里就会同时出现一堆“已审核通过但实际没领养成功”的数据,月底统计完成率时口径完全混乱。有了MEETING状态,管理员看一眼就知道有多少申请已经推进到了见面阶段,有多少是审核完了就没下文了的,可以安排人电话催访。
3.3 冗余统计字段与报表便利
除了业务表,我还在数据库层面做了几个方便统计的冗余设计。比如回访表专门加了回访计划日期和实际回访日期两个字段,这样首页看板可以直接查“今天该回访但没回访的记录”,SQL只有一条简单查询,不用在业务代码里做复杂计算。
动物表上也冗余了当前申请数量、领养次数这类字段。每次申请提交时,在事务里顺带更新动物表的申请数;每次完成领养时,更新累计领养次数。虽然这种字段严格来说属于可以算出来的数据,但救助站的管理后台需要频繁展示这类列表指标,与其每条记录都写子查询或者运行时统计,不如在写入时就把数字维护好,查询时直接用。这种“用存储换查询时间”的取舍,在这种规模的项目里是划算的。
4. 领养申请模块的实现:状态机、事务边界与幂等控制
领养申请是整个系统的核心业务,也是最值得讲清楚的一个模块。它虽然不复杂,却同时涉及状态机、事务和幂等三个问题,每一个在生产环境里都能单独逼疯人。
4.1 申请接口的完整实现逻辑
先看核心Service方法的实现:
@Transactional(rollbackFor = Exception.class) public void applyAdoption(ApplyAdoptionCommand command) { Animal animal = animalMapper.selectById(command.getAnimalId()); if (animal == null || !AnimaleStatus.AVAILABLE.equals(animal.getAdoptStatus())) { throw new BusinessException("该动物当前不可领养"); } Long count = applicationMapper.selectCount( new LambdaQueryWrapper<AdoptionApplication>() .eq(AdoptionApplication::getAnimalId, command.getAnimalId()) .eq(AdoptionApplication::getApplicantId, command.getApplicantId()) .in(AdoptionApplication::getApplicationStatus, Arrays.asList("PENDING", "APPROVED", "MEETING")) ); if (count > 0) { throw new BusinessException("您已提交过该动物的领养申请,请等待工作人员处理"); } AdoptionApplication application = new AdoptionApplication(); application.setAnimalId(command.getAnimalId()); application.setApplicantId(command.getApplicantId()); application.setApplicantName(command.getApplicantName()); application.setPhone(command.getPhone()); application.setIdCard(command.getIdCard()); application.setAddress(command.getAddress()); application.setPetExperience(command.getPetExperience()); application.setFamilyReason(command.getFamilyReason()); application.setApplicationStatus(ApplicationStatus.PENDING); applicationMapper.insert(application); animal.setAdoptStatus(AnimalStatus.APPLYING); animalMapper.updateById(animal); }这段代码里有两个顺序很关键。第一,先查动物状态,再校验重复申请,最后插入申请并更新动物状态。插入申请是主操作,更新动物状态是副操作,两者的顺序不能颠倒,不然可能出现申请没插进去、动物状态倒是被改成APPLYING了。第二,更新动物状态用的是updateById的全量更新,只改了adoptStatus一个字段,其他字段不会被覆盖,所以不需要担心并发下字段丢失。
4.2 事务为什么会“失效”:两类经典场景
很多人在本地测试时事务是好用的,一上线就发现数据写了一半。这里有两个最常见的坑,我在这个项目里都踩过。
第一个坑是同一类内部方法调用。假设上面的applyAdoption方法内部又调用了另一个事务方法,比如this.updateAdoptStatus(),直接通过this调用时,Spring代理是感知不到的,等于第二个方法上的@Transactional完全无效。解决办法是从Spring容器里注入自己,或者把需要事务控制的方法拆到另一个Service类里,通过容器调用。
第二个坑是异常被吞。如果业务代码里catch了异常然后返回一个错误对象,事务机制看到的是“方法正常结束了”,根本不会触发回滚。我见过不少项目出现“明明报错了,数据却写进去了”的诡异问题,最后都是这个原因。所以我自己在项目里定了一个规矩:业务异常统一继承RuntimeException,在事务方法里只抛出,不捕获,由全局异常处理器统一转成前端提示信息。这样事务边界绝对干净。
4.3 重复申请的幂等处理:一张查询解决
曾几何时系统上线第一天就出过一档子事:一个用户对同一只猫连续点了三次“申请领养”,结果表格里插入了三条申请记录,工作人员都看懵了。
事后我加了个接口层防重方案:根据申请人ID和动物ID查询状态为PENDING、APPROVED、MEETING的记录,只要存在就直接提示“您已提交过申请”。为什么不直接加唯一索引?因为不能用“申请人+动物”做唯一,人家确实可以对同一只动物申请两次——第一次被拒绝了再申一次是允许的,历史状态REJECTED/CANCELLED不算数。数据库唯一索引做不出“部分状态唯一”这种约束,所以业务层查一次是最直观的方案。
再配合前端的提交按钮置灰,双保险基本就能挡住用户手滑了。这里要补充的是:如果未来系统并发量大了,光靠业务层查询还不够,需要在应用层加分布式锁或者用数据库悲观锁,但对于救助站这种个位数并发的场景,一次select加事务隔离已经绰绰有余。
5. 踩坑实录:从本地联调到打包上线的排查链路
这部分跟大家仔细过一遍系统从开发到上线遇到的几个印象深刻的坑。每个问题单独看都不大,但连在一块的排查过程很能说明问题,也希望读者看完能少走点弯路。
5.1 主键策略冲突:MyBatis-Plus自增ID与雪花ID的错位
系统开发到第三天,第一个恶心的问题出现了。我用MyBatis-Plus生成实体类时,默认配置了@TableId(type = IdType.ASSIGN_ID),也就是雪花ID。但建表脚本里,主键字段我却写成了AUTO_INCREMENT自增。结果一跑插入接口,报错信息提示主键重复,或者数据库直接reject了显式写入的自增列值。
这个问题的本质是两套主键策略的冲突:数据库要求自增,框架却非要自己生成一个雪花长整型塞进去,两边各干各的,自然就打架了。
解决办法很简单,二选一:要么把表的主键改成非自增、bigint类型,配合ASSIGN_ID;要么把实体类改成@TableId(type = IdType.AUTO),完全交给数据库生成。我选了前者,理由是雪花ID在分布式场景下更通用,将来如果拆多个服务,主键冲突的概率为零。这里提醒一句,MyBatis-Plus的代码生成器默认策略不是AUTO,用之前务必跟表结构对齐一下,很多诡异报错都是这个引起的。
5.2 图片上传后404:路径映射问题的定位过程
上传宠物照片时本地一切正常,但部署到服务器之后,图片上传成功了,访问却一直404。这个问题的排查链路值得说一下。
我先确认了文件确实落盘了,然后发现上传返回的URL是/upload/xxx.jpg,这个路径在本地是IDEA里设置的静态资源映射路径,但服务器的Spring Boot工程默认没有映射/upload/**到服务器磁盘目录,所以404。本地正常是因为开发环境用了resources下的静态目录,服务器上没有对应的文件。
正确的做法是增加一个WebMvcConfigurer配置,直接把/upload/**映射到磁盘上的真实目录:
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Value("${file.upload-path}") private String uploadPath; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceLocations("file:" + uploadPath + "/"); } }如果生产环境前面还挂了Nginx,更好的方案是让Nginx直接处理静态资源请求,Spring Boot只管业务接口。这一步配置好之后,图片访问的问题彻底消失。现在回想,这类问题本地永远测不出来,因为根因就在环境和运行方式的差异上。
5.3 部署到服务器后的环境差异:内存、编码与日志
第一次打包部署到服务器时,我踩了一个特别经典的内存坑。服务器上Java进程跑了一会儿就挂了,查日志发现是OutOfMemoryError。看了下启动方式——java -jar adoption-server.jar,默认JVM堆内存按服务器物理内存的四分之一配置,2GB的服务器理论上能分到500MB,但救助站系统同时跑了MySQL和一些其他进程,内存直接被瓜分干净。
调整启动参数后问题解决:
java -Xms256m -Xmx512m -XX:+HeapDumpOnOutOfMemoryError \ -XX:HeapDumpPath=/var/log/adoption/ \ -jar adoption-server.jar --spring.profiles.active=prod除了内存,编码问题也出现过一次。服务器上MySQL默认字符集是utf8mb4,但连接串没有显式指定,导致从接口写入的中文在某些操作后成了乱码。解决方式是数据源URL统一加上characterEncoding=utf8mb4,同时在应用层用spring.datasource.hikari.connection-init-sql设置初始化SQL。这种问题在本地因为环境字符集碰巧一致看不出来,一到服务器就现原形。
总结一下这轮的几个坑,列个表方便大家对照自查:
| 问题 | 根因 | 解决方式 |
|---|---|---|
| 主键报错 | ID策略与自增冲突 | 统一用雪花主键,表去掉自增 |
| 图片404 | 静态资源未映射磁盘路径 | WebMvcConfigurer映射 /upload/** |
| 进程被系统杀 | JVM堆参数过大 | 显式设置-Xms -Xmx |
| 中文乱码 | 连接串未指定字符集 | URL加characterEncoding=utf8mb4 |
6. 上线后的监控与扩展:Spring Boot Admin和WebSocket通知
系统上线不是终点,救助站的值班人员能安稳用才是目标。上线一周后,我开始考虑怎么做监控和增强通知,毕竟服务器放在站里角落的机架上,没人会天天盯着终端窗口看。
6.1 接入Spring Boot Admin的完整步骤
我先引入了一个独立的Spring Boot Admin Server小服务,再把救助站系统作为Client注册上去。步骤很简单,Admin Server端只需要一个Spring Boot工程,加上依赖:
<dependency> <groupId>de.codecentric</groupId> <artifactId>spring-boot-admin-starter-server</artifactId> <version>2.7.15</version> </dependency>主类加@EnableAdminServer注解就完事。客户端这边,救助站系统加两个依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>de.codecentric</groupId> <artifactId>spring-boot-admin-starter-client</artifactId> <version>2.7.15</version> </dependency>然后在application.yml里配置:
spring: boot: admin: client: url: http://localhost:9080/admin-server management: endpoints: web: exposure: include: 'health,info,metrics,logfile' endpoint: health: show-details: always关键在于Actuator的端点默认只暴露health和info,必须手动把metrics和logfile端点开出来,Admin界面才能看到JVM内存、线程、HTTP请求统计这些信息。配好之后,仪盘表页面直接看得到当前堆内存使用量、垃圾回收次数、最近请求耗时,感觉比每天ssh上去敲jstat命令踏实多了。
6.2 关键监控指标与JVM参数配置
很多人在这个环节会问:监控到底要看哪些指标?我的经验是,对于救助站这种低并发的单体系统,盯四个就够:堆内存使用率、Full GC频率、数据库连接池活跃数、接口响应耗时。堆内存肉眼看着稳定增长说明可能有泄漏;Full GC太频繁说明堆太小或者代码里有大对象;数据库连接池被打满通常是慢SQL或者连接泄漏;响应耗时突然升高往往是外部依赖异常。
生产环境的JVM参数也按这个思路给的:
-Xms256m -Xmx512m -XX:MaxMetaspaceSize=128m -XX:+UseG1GC用G1收集器是因为它在低堆内存下的停顿控制优于ParallelGC,配合Admin跑起来,系统可以做到很平稳。另外,如果服务器内存确实紧张,可以削到-Xmx384m,前提是别同时开太多其他应用。
6.3 领养进度通知:WebSocket的接入与yml配置
救助站工作人员提了个需求:用户提交领养申请后,希望系统能主动推送进度变化,比如“您的领养申请已通过审核,请预约时间来看猫”。这个功能最自然是走WebSocket。
这里有个网上提问率很高的误区:很多人搜“spring boot 集成web socket yml 配置”,以为要通过yml配什么参数。其实Spring Boot集成WebSocket,核心靠Java配置类,yml基本不用专门配置。我用的是Spring内建的WebSocket支持,先写一个WebSocketHandler处理连接和消息,再注册到WebSocketConfigurer里:
@Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Resource private AdoptionProgressHandler adoptionProgressHandler; @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(adoptionProgressHandler, "/ws/progress") .setAllowedOrigins("*"); } }Handler里面收到前端传来的userId后,把WebSocketSession缓存起来。等后台更新申请状态时,通过userId找到对应Session推送消息。实践下来这套方案在救助站场景完全够用,比引入RabbitMQ或者Kafka轻得多,也省掉了运维负担。关于yml,唯一的注意点是如果接入了Spring Security,放行/ws/progress这个端点,否则握手阶段就会被拦截。这个坑我倒是没踩,因为前期设计时就把WebSocket路径放到了白名单里。
后端里还可以预留一个思路:如果将来合作宠物医院要查领养档案,可以单独开一个openapi模块,用服务鉴权的方式对接,内部Service层复用现有逻辑,不跟后台管理接口混在一起。这样既满足了对外开放的需求,也不会把内部结构暴露出去。
WebSocket加上之后,工作人员那边的工作量小了很多,用户也省心。我在实际运行中发现一个小技巧:推送消息的模板里一定要带上动物编号和当前状态,否则用户收到消息还要去翻详情页才能知道是哪只动物,体验会差一截。
最后再分享一点自己的体会
这套系统从调研、设计、开发到上线跑稳定,前后花了一个多月。最大的感悟是:技术选型不要追新,稳定压倒一切。Spring Boot 2.7 + JDK 8 + MyBatis-Plus这套组合,看起来平平无奇,但它把开发成本压到了最低,让救助站这种小场景也能轻松拥抱信息化。
站在写代码的角度,领养申请模块的状态机设计、事务边界控制以及幂等处理,可能才是这个项目真正的价值所在。如果只是照着教程做CRUD,你永远遇不到也解决不了这些问题,代码写再多都只是重复劳动。希望这篇实战复盘能帮准备做类似系统的人少踩几个坑,把精力放到真正需要思考的业务细节上去。