近两年一直在做各类 Java 后端项目,Spring Boot 几乎成了标配。前阵子接了一个比较特殊的单子——流浪狗智能救助系统,要求带完整的可运行源码,最终编号 23826。做完之后我觉得挺有代表性:它不是一个标准的管理系统,而是把救助站日常运作、领养流程、位置上报、通知触达这些场景全部揉进了一个 Spring Boot 项目里。今天把整套设计思路和关键实现拆开讲一遍,包括数据库怎么设计、领养状态机怎么流转、图片上传怎么落地、通知模块怎么选型,以及最后部署时踩过的一些坑,希望能给正在做同类项目的朋友一点参考。
1. 为什么需要一套“智能救助系统”:先想清楚要管理什么
很多人觉得流浪狗救助站不就是“登记狗、等人领养”嘛,做个 CRUD 就够了。但真实场景远比这个复杂。
一个普通救助站要管理的信息至少有这几类:
- 流浪狗基础信息:品种、年龄、性别、毛色、健康状态、疫苗记录、绝育状态、救助时间、救助地点。
- 救助记录:谁报的警、在哪个位置发现的、当时什么状况、有没有受伤。
- 领养流程:申请人信息、申请时间、审核状态、领养协议签署情况、回访记录。
- 捐赠和物资:很多人愿意捐狗粮、捐钱,需要流水记录和公示。
- 日常提醒:疫苗到期、驱虫时间、回访计划,光靠人脑记根本不现实。
这些信息如果放在 Excel 表格里,一旦数据量超过几百条,查询和协作效率就急剧下降。更重要的是,救助站往往有多个工作人员,甚至还有志愿者,信息分散在各人手上,很难形成统一的视图。
所以这个系统的定位就很清晰了:用 Spring Boot 做一个中心化的救助管理平台,让所有角色——管理员、救助人员、领养申请人、捐赠人——都通过一套系统协作。它解决的问题不是“做个网站”,而是“让救助站的每一条狗、每一个流程都有迹可循”。
实际开发中,我建议把系统拆成几个核心模块:
- 用户与权限模块:管理后台账号、领养人注册登录,角色区分管理员和普通用户。
- 流浪狗管理模块:狗的档案、状态流转、图片上传。
- 领养流程模块:申请、审核、协议、回访,全流程线上化。
- 救助上报模块:发现流浪狗后提交位置和描述,支持地图标注。
- 通知与提醒模块:邮件、短信或公众号模板消息触达。
- 数据统计模块:领养率、救助数量、区域分布等可视化报表。
模块划分清楚之后,后续所有代码写起来都有章法。这也是一开始最值得花时间的事情——先把边界定清楚,而不是上来就写代码。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 系统整体拆解:技术选型与核心数据表设计
这个项目底层用的是 Spring Boot 2.7.x,配套 MyBatis-Plus 做 ORM,MySQL 8 存储数据,Redis 做缓存和验证码存储,Knife4j 生成接口文档。这套组合在中小型管理系统中非常稳,团队上手成本低,出了问题社区资料也多。
选 Spring Boot 2.7.x 而不是 3.x,主要是考虑到 JDK 版本兼容。很多部署环境还是 JDK 8,Spring Boot 3 强制要求 JDK 17,对于救助站这种预算有限的场景,硬上高版本反而增加部署成本。当然如果你是新项目且环境完全可控,用 3.x 也没问题,但下面的代码示例我会基于 2.7.x 来写,方便大多数同学直接复用。
2.1 核心数据表设计
数据库是整个系统的地基,表设计直接决定后续开发效率和扩展空间。我按业务域拆成以下几张核心表:
| 表名 | 用途 | 关键字段 |
|---|---|---|
| user | 用户表 | id, username, password, phone, email, role, status |
| dog_info | 流浪狗档案表 | id, name, breed, age, gender, health_status, vaccine_status, sterilized, location, status, photo_url, create_time |
| rescue_record | 救助上报记录 | id, dog_id, reporter_name, reporter_phone, location, description, report_time, status |
| adoption_record | 领养记录 | id, dog_id, user_id, apply_time, audit_status, sign_status, follow_up_time, remark |
| donation_record | 捐赠记录 | id, user_id, amount, item_name, donate_time, remark |
| notification_log | 通知日志 | id, target, type, content, status, send_time |
拿 dog_info 表来说明一下状态管理。这张表里有一个 status 字段,我定义成枚举:
- 0:待救助(刚被上报,还没确认接回)
- 1:在站(已经收留在救助站)
- 2:待领养(健康检查完成,开放领养)
- 3:已领养(已经找到领养人)
- 4:医疗中(正在治疗)
为什么要用数字枚举而不是字符串?一方面存储空间更小,另一方面在代码里用枚举类约束,比散落的字符串判断更安全,不容易写错。MyBatis-Plus 可以直接用 EnumTypeHandler 映射枚举,非常方便。
领养记录表 adoption_record 是整个业务流的核心,状态流转比较讲究:
- audit_status:待审核、审核通过、审核拒绝
- sign_status:未签署、已签署
- 回访状态:未回访、已回访
这里我特意把审核和签署拆成两个字段,因为实际业务里,审核通过了不代表领养人一定会来签协议,有可能中途变卦。拆开之后状态组合更灵活,统计也更准确。
2.2 关键代码:状态流转的枚举设计
java复制@Getter
public enum DogStatus {
PENDING(0, "待救助"),
IN_STATION(1, "在站"),
ADOPTABLE(2, "待领养"),
ADOPTED(3, "已领养"),
IN_TREATMENT(4, "医疗中");
private final int code;
private final String desc;
DogStatus(int code, String desc) {
this.code = code;
this.desc = desc;
}
public static DogStatus fromCode(int code) {
for (DogStatus status : values()) {
if (status.code == code) {
return status;
}
}
throw new IllegalArgumentException("未知状态: " + code);
}
}
状态流转我集中放在一个 Service 里做,不散落在各处。好处是以后要加状态校验、加操作日志,只需要改一个地方。
提示:状态机的核心思想是明确“谁在什么条件下可以从 A 状态转到 B 状态”。比如“已领养”的狗不能被重复提交领养申请,这些业务约束需要在状态变更入口统一校验,而不是靠前端隐藏按钮。
3. 领养全流程的工程实现:从申请到回访的闭环
领养流程是这个系统业务价值最高的部分。没有流程管理,救助站就只能靠微信聊天记录来跟进,很容易漏掉回访、错失审核节点。
3.1 领养申请接口
用户在前端看到“待领养”状态的狗,点击申请领养,后端接口要做的事情不止是插入一条记录那么简单。我画一下核心逻辑:
- 校验用户是否登录。
- 校验该 dog 的状态是否为“待领养”。
- 校验该用户是否已经申请过这只狗(防止重复申请)。
- 创建 adoption_record,状态为“待审核”。
- 将 dog 状态改为“领养审核中”(可以新增一个状态,或者用单独字段标记)。
- 记录一条通知日志,通知管理员去审核。
第 3 步很容易被忽略。如果不做重复申请校验,用户手滑点了两次,就会出现两条待审核记录,管理员后台会看到重复数据,体验很差。
java复制@Service
@RequiredArgsConstructor
public class AdoptionService {
private final AdoptionRecordMapper adoptionRecordMapper;
private final DogInfoMapper dogInfoMapper;
private final NotificationService notificationService;
@Transactional(rollbackFor = Exception.class)
public Result applyAdoption(AdoptionApplyRequest request, Long userId) {
// 1. 校验狗是否存在且可领养
DogInfo dog = dogInfoMapper.selectById(request.getDogId());
if (dog == null) {
return Result.error("狗狗不存在");
}
if (dog.getStatus() != DogStatus.ADOPTABLE.getCode()) {
return Result.error("当前狗狗暂不可领养");
}
// 2. 校验是否重复申请
Long count = adoptionRecordMapper.selectCount(new LambdaQueryWrapper<AdoptionRecord>()
.eq(AdoptionRecord::getDogId, request.getDogId())
.eq(AdoptionRecord::getUserId, userId)
.eq(AdoptionRecord::getAuditStatus, 0));
if (count > 0) {
return Result.error("您已提交过申请,请勿重复操作");
}
// 3. 创建申请记录
AdoptionRecord record = new AdoptionRecord();
record.setDogId(request.getDogId());
record.setUserId(userId);
record.setApplyTime(new Date());
record.setAuditStatus(0);
record.setSignStatus(0);
adoptionRecordMapper.insert(record);
// 4. 更新狗狗状态
dog.setStatus(DogStatus.PENDING_ADOPTION_AUDIT.getCode());
dogInfoMapper.updateById(dog);
// 5. 通知管理员审核(异步)
notificationService.sendToAdmins("新的领养申请",
"用户 " + userId + " 申请领养狗狗 " + dog.getName());
return Result.success();
}
}
这里用到了 @Transactional:创建申请记录和更新狗状态必须放在同一个事务里,要么都成功,要么都失败。否则可能出现“申请记录建了,但狗状态没更新”的数据不一致问题。
事务失效是新人最容易踩的坑。最典型的情况是:方法内部调用同类中的另一个 @Transactional 方法,因为走的是 this 调用而不是代理对象,事务注解不生效。解决方案是注入自身代理或者把事务方法拆到另一个 Service 类里。
3.2 审核与协议签署
管理员审核通过后,系统要做几件事:
- 更新 adoption_record 的 audit_status 为 1(审核通过)。
- 将 dog 状态改为“已锁定领养”,避免其他用户再申请。
- 通过邮件或短信通知申请人,告知下一步签署协议的安排。
- 生成签署任务记录,关联到领养记录上。
审核拒绝的处理相对简单:更新状态为 2,同时把狗状态改回“可领养”,释放占位。
协议签署建议在线上完成电子签署,至少也要记录签署时间。如果条件有限,也可以在线下签署后由管理员在系统里手动登记,但一定要保留签署时间字段,后续回访和合规审查都用得到。
3.3 回访任务与闭环
领养不是“狗被带走”就结束了。正规救助站都会在领养后一两周安排回访,确认狗狗适应情况。系统里我用了一个简单的定时任务扫描:
java复制@Component
@Slf4j
public class FollowUpTask {
private final AdoptionRecordMapper adoptionRecordMapper;
private final NotificationService notificationService;
@Scheduled(cron = "0 0 9 * * ?")
public void checkFollowUp() {
// 查询已领养且已超过7天未回访的记录
List<AdoptionRecord> records = adoptionRecordMapper.selectList(
new LambdaQueryWrapper<AdoptionRecord>()
.eq(AdoptionRecord::getAuditStatus, 1)
.eq(AdoptionRecord::getSignStatus, 1)
.eq(AdoptionRecord::getFollowUpStatus, 0)
.lt(AdoptionRecord::getSignTime, DateUtil.offsetDay(new Date(), -7))
);
for (AdoptionRecord record : records) {
notificationService.sendToAdmins("回访提醒",
"领养记录 " + record.getId() + " 已到期,请安排回访");
}
}
}
@Scheduled(cron = "0 0 9 * * ?") 表示每天早上 9 点执行一次。部署的时候要注意服务器时区,我之前就遇到过因为时区设置成了 UTC,任务总是和预期时间差 8 个小时。
注意:定时任务如果部署了多个实例(集群),一定要加分布式锁或者用 xxl-job 之类的分布式调度平台,否则每个实例都会执行一遍,通知会重复发送。单机部署的话用 Spring 自带的
@Scheduled就够用。
4. “智能”体现在哪里:位置上报、标签推荐与状态提醒
“智能救助系统”听起来高大上,但实际落地不需要太复杂的人工智能。我在这个项目中实现的“智能”主要体现在三个点:位置服务、推荐逻辑、自动提醒。
4.1 位置上报与地图可视化
救助上报模块需要记录发现流浪狗的位置。用户端通过微信公众号或者小程序授权获取地理位置,后端保存经纬度,管理端用地图组件展示所有待救助狗的热力图。
数据库表 rescue_record 里的位置字段我建议直接存经纬度两个 decimal 字段:
java复制private BigDecimal longitude;
private BigDecimal latitude;
经纬度一般精确到小数点后 6 位,约 0.1 米精度,用 decimal(10, 6) 足够。如果要查询“某个坐标附近 3 公里内的上报记录”,可以用 MySQL 内置的 ST_Distance_Sphere 函数:
sql复制SELECT * FROM rescue_record
WHERE ST_Distance_Sphere(
POINT(longitude, latitude),
POINT(116.397, 39.908)
) <= 3000;
这个函数直接用球面距离计算,单位是米,不需要自己引入 GIS 插件,适合数据量不大的场景。数据量大了再考虑引入 Elasticsearch GEO 或者 PostGIS。
4.2 基于标签的狗狗推荐
用户查看待领养列表时,系统可以根据用户填写的偏好(品种、年龄、性别、毛色)做简单的匹配推荐。实现思路并不复杂:后台给每只狗打标签,用户选择偏好后,按匹配度排序。
我采用的方案是:给 dog_info 增加一个 tags 字段,用逗号分隔,比如“温顺,小型犬,适合公寓”。用户偏好表 user_preference 存同样的标签形式。计算匹配度时,取两个集合的交集数量排序。
java复制private int matchScore(String dogTags, String userPrefTags) {
if (StringUtils.isBlank(dogTags) || StringUtils.isBlank(userPrefTags)) {
return 0;
}
Set<String> dogSet = new HashSet<>(Arrays.asList(dogTags.split(",")));
Set<String> prefSet = new HashSet<>(Arrays.asList(userPrefTags.split(",")));
dogSet.retainAll(prefSet);
return dogSet.size();
}
这种方案肯定没有协同过滤算法专业,但优点是容易实现、解释性强,对小流量场景完全够用。用户看到“推荐度”三个字,体验就比纯列表好很多。
4.3 状态自动提醒
除了领养回访提醒,系统里还有几类典型提醒:
- 疫苗到期提醒:dog_info 里存 next_vaccine_date,定时任务扫描到期前 7 天的记录,通知管理员安排接种。
- 绝育提醒:录入时如果标记未绝育且已经成年,定期提醒安排绝育。
- 捐赠感谢:捐赠成功后自动发送感谢消息。
这些功能代码模式大同小异,核心就是 @Scheduled + 条件查询 + 通知发送。关键是提醒频率要合理,不能每天轰炸,我一般会加一个 notification_log 表做去重,每个业务对象在一个周期内只发一次。
5. 图片上传与文件存储:最容易低估的模块
流浪狗档案里必须要有照片,而且往往不止一张——救助现场、健康检查、生活近照。图片模块看着简单,实际做到好用有不少细节。
5.1 本地上传 vs 云存储
项目初期我用的本地上传,把文件写在服务器磁盘上,数据库存 URL。这是成本最低的方案,也适合演示环境。但如果要做生产部署,我建议直接上云 OSS 或者 MinIO 自建对象存储。原因有三点:
- 图片访问走云厂商 CDN,加载速度有保障。
- 本地磁盘容易满,清理和备份都不方便。
- 应用多实例部署时,本地上传会导致图片在两个实例间不一致。
5.2 上传接口实现
这里给一个用本地存储实现的完整示例,方便初学者跑通:
java复制@PostMapping("/upload")
public Result uploadImage(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
return Result.error("文件不能为空");
}
// 1. 校验文件类型
String originalFilename = file.getOriginalFilename();
String ext = StringUtils.getFilenameExtension(originalFilename);
List<String> allowedExt = Arrays.asList("jpg", "jpeg", "png", "gif", "webp");
if (!allowedExt.contains(ext.toLowerCase())) {
return Result.error("不支持的文件类型");
}
// 2. 校验文件大小,限制 5MB
if (file.getSize() > 5 * 1024 * 1024) {
return Result.error("文件大小不能超过5MB");
}
// 3. 生成唯一文件名,避免重名覆盖
String newFileName = UUID.randomUUID().toString().replace("-", "")
+ "." + ext;
// 4. 保存文件
String uploadDir = fileUploadProperties.getPath();
File dir = new File(uploadDir);
if (!dir.exists()) {
dir.mkdirs();
}
try {
file.transferTo(new File(uploadDir + File.separator + newFileName));
} catch (IOException e) {
return Result.error("文件上传失败");
}
// 5. 返回可访问的 URL
String url = "/files/" + newFileName;
return Result.success(url);
}
文件类型校验这里只做了后缀名校验,其实不够严谨。如果有人传一个伪装成 jpg 的恶意脚本,后缀名校验拦不住。更稳妥的做法是校验文件的 MIME type,或者直接用图片处理库尝试解析文件头。项目里我用了 Apache Tika 做内容探测,虽然增加了一点依赖,但安全性提升明显。
5.3 图片访问的静态资源映射
Spring Boot 默认不会把任意磁盘目录映射成静态资源,需要在配置里指定:
java复制@Configuration
public class WebConfig implements WebMvcConfigurer {
@Value("${file.upload.path}")
private String uploadPath;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/files/**")
.addResourceLocations("file:" + uploadPath + File.separator);
}
}
注意 addResourceLocations 必须以 file: 开头,且路径末尾要加 File.separator,否则映射不到。这个细节坑了很多人,我一开始也是 404 排查了半天才找到原因。
经验:如果上传的图片返回的 URL 是
/files/xxx.jpg这种相对路径,前端使用时要记得拼接服务器地址。也可以直接存完整 URL,但后期迁移域名会很痛苦,我更推荐存相对路径,前端统一加 baseURL。
6. 消息触达:通知模块的选型与工程实践
系统里所有“提醒”“审核通知”“感谢信”都是消息触达。这个模块虽然不起眼,但直接影响用户对系统的感知。
6.1 渠道选型:邮件、短信、公众号模板消息
不同场景适合不同渠道:
| 渠道 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 邮件 | 免费、内容可以很详细 | 打开率低、可能进垃圾箱 | 审核结果、回访报告 |
| 短信 | 到达率高 | 每条几分钱、字数限制 | 审核通过通知、验证码 |
| 公众号模板消息 | 免费、打开率高 | 需要认证服务号 | 领养进度、捐赠感谢 |
救助站预算有限,我建议优先用邮件 + 公众号模板消息。短信作为验证码的补充渠道,仅在必要时买一点量。
6.2 Spring Boot 整合 JavaMail
邮件发送是最容易上手的。配置好 SMTP 信息后,代码其实很简单:
yaml复制spring:
mail:
host: smtp.qq.com
port: 465
username: your-email@qq.com
password: your-auth-code
properties:
mail:
smtp:
ssl:
enable: true
注意:发件邮箱的 password 不是登录密码,而是邮箱服务商提供的授权码。用 QQ 邮箱的话,需要在设置里开启 SMTP 服务并生成授权码。
发送邮件的核心代码:
java复制@Service
@RequiredArgsConstructor
public class EmailNotificationService {
private final JavaMailSender mailSender;
@Async
public void sendSimpleEmail(String to, String subject, String content) {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom("your-email@qq.com");
message.setTo(to);
message.setSubject(subject);
message.setText(content);
mailSender.send(message);
}
}
这里我加了 @Async,把邮件发送变成异步操作。原因是邮件发送涉及网络 IO,可能耗时几百毫秒甚至更久,如果放在业务请求链路里同步调用,接口响应时间会被拖长。启动类上要记得加 @EnableAsync 注解,否则 @Async 不生效。
6.3 异步任务的重试与降级
异步发送也不是万无一失。邮件发送失败时需要重试,我用的方案是手动捕获异常并记录到 notification_log 表,由定时任务扫描失败记录做补偿。
java复制@Scheduled(cron = "0 0/30 * * * ?")
public void retryFailedNotifications() {
List<NotificationLog> logs = notificationLogMapper.selectList(
new LambdaQueryWrapper<NotificationLog>()
.eq(NotificationLog::getStatus, 0) // 失败状态
.lt(NotificationLog::getRetryCount, 3) // 重试不超过3次
);
for (NotificationLog log : logs) {
try {
sendEmail(log.getTarget(), log.getSubject(), log.getContent());
log.setStatus(1);
log.setSendTime(new Date());
} catch (Exception e) {
log.setRetryCount(log.getRetryCount() + 1);
}
notificationLogMapper.updateById(log);
}
}
这样一个“半异步 + 定时补偿”的方案,可以在不引入消息队列的前提下达到很可靠的投递效果。对于救助站这种体量,完全够用。
7. 部署交付:从源码到可演示的运行环境
项目做完不只意味着代码写完,还要能部署起来给用户看。这一节讲一下我最后交付时用到的部署方案和踩过的坑。
7.1 Maven 多环境配置
开发、测试、生产环境的数据库连接和文件上传路径都不一样。用 Maven profile 实现多环境配置是最常规的做法:
xml复制<profiles>
<profile>
<id>dev</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<activatedProperties>dev</activatedProperties>
</properties>
</profile>
<profile>
<id>prod</id>
<properties>
<activatedProperties>prod</activatedProperties>
</properties>
</profile>
</profiles>
对应的 application-dev.yml 和 application-prod.yml 分别放到 resources 目录下,主配置 application.yml 里用 spring.profiles.active: @activatedProperties@ 引用。打包时:
bash复制mvn clean package -DskipTests -Pdev # 开发环境
mvn clean package -DskipTests -Pprod # 生产环境
这样打出的 jar 包会自动带上对应环境的配置,不用手动改文件。
7.2 Docker 部署
救助站的服务器大概率不归专业运维管,最好把部署步骤做到极致简单。我的做法是写一个 docker-compose.yml,一条命令拉起 MySQL + Redis + 应用:
yaml复制version: "3"
services:
mysql:
image: mysql:8.0
container_name: rescue-mysql
environment:
MYSQL_ROOT_PASSWORD: root123
MYSQL_DATABASE: rescue_db
ports:
- "3306:3306"
volumes:
- ./mysql-data:/var/lib/mysql
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
app:
build: .
container_name: rescue-app
depends_on:
- mysql
ports:
- "8080:8080"
environment:
SPRING_PROFILES_ACTIVE: prod
DB_HOST: mysql
Dockerfile 就写标准的多阶段构建,先 Maven 打包再用 JDK 镜像运行:
dockerfile复制FROM maven:3.8-openjdk-8 AS builder
COPY . /app
WORKDIR /app
RUN mvn clean package -DskipTests
FROM openjdk:8-jre-alpine
COPY --from=builder /app/target/rescue-system.jar /app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app.jar"]
这里有个细节:Spring Boot 项目里数据库连接地址不要写死 localhost,要写成容器名 mysql。因为在 compose 网络里,应用容器通过服务名访问数据库容器,而不是回环地址。
7.3 部署后容易踩的坑
第一是数据库时区。MySQL 8 默认时区是 UTC,如果连接串里不加 serverTimezone=Asia/Shanghai,存进去的时间会比北京时间少 8 小时。我一般在连接串里显式指定:
code复制jdbc:mysql://localhost:3306/rescue_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
第二是端口冲突。救助站现场可能已经跑了其他服务,如果 8080 被占,要么改端口,要么用 server.port 配置。我在部署文档里明确写清楚了修改方式。
第三是静态资源路径权限。Linux 下如果应用用非 root 用户运行,上传目录必须给对应的写权限,否则上传文件时直接 Permission denied。
第四是前端跨域。这个系统如果前后端分离,后端要配置跨域过滤器,否则前端请求会被浏览器拦截:
java复制@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOriginPattern("*");
config.addAllowedMethod("*");
config.addAllowedHeader("*");
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
提示:生产环境不建议把 allowedOriginPattern 设为
*,最好限定为你的前端域名,避免接口被任意站点跨域调用。
7.4 初始化数据的重要性
交付源码后,如果用户本地启动起来是一张空表,体验非常差。我在 init.sql 里预置了一部分演示数据:
- 3 个测试账号(admin / adopter / rescuer),密码都用 BCrypt 加密好。
- 5 条流浪狗档案,覆盖在站、待领养、医疗中等不同状态。
- 2 条救助上报记录,带真实经纬度。
- 1 条领养申请记录,方便演示审核流程。
这样用户拿到源码,导入数据库,启动应用,登录就能看到完整业务效果,不用自己一步步造数据。对于“附源码”类的交付,这个细节特别加分。
8. 项目扩展思路:下一步还能往哪个方向做
从技术角度来说,当前这个系统还比较“传统”,离“智能”还有一定提升空间。如果后续要做二期,我觉得有三个方向很值得尝试。
第一个是引入图像识别做犬种辅助识别。救助人员上传流浪狗照片后,系统自动识别犬种、大致年龄,辅助填写档案。现在有一些开源的图像分类模型可以微调,Spring Boot 侧只需要调用一个 Python 推理服务或者 ONNX Runtime 加载模型即可,整体架构改动不大。
第二个是接入微信小程序端。目前我做的系统是后台管理 + H5 前端,如果做成小程序,领养人可以直接在微信里浏览待领养狗狗、提交申请、接收模板消息,触达效率会高很多。Spring Boot 后端只需要增加小程序登录的接口,复用现有的业务逻辑就行。
第三个是完善数据报表。救助站每年要向主管部门报送数据,需要统计救助了多少只、领养率多少、区域分布如何。用 ECharts 做可视化大盘,或者后端导出 Excel 报表,都是实用性很强的功能。
不过这些都是后话。当前这个 23826 版本已经覆盖了救助站日常运营的完整闭环,从流浪狗登记、救助上报、领养审核、协议签署到回访提醒,每一步都有明确的业务归属和技术实现,代码结构也保持了清晰的模块边界。
我个人在这个项目里最大的体会是:别小看“管理系统”四个字,真正把每个业务环节想透、把状态流转理清楚、把容易出错的边界条件处理好,工作量一点都不小。尤其是状态机设计和事务处理这两块,直接决定了系统背后的数据是否可信。如果你正在做类似的项目,建议在动手写代码之前,先把各业务状态画出来、把表关系理清楚,这比多写几个接口有价值得多。
