1. 项目背景与核心价值
去年冬天,我在参与本地流浪动物救助站志愿活动时,亲眼目睹了工作人员用Excel表格手动记录上百只流浪动物信息的场景。领养者需要翻遍十几个微信群聊历史记录才能找到心仪宠物的资料,而捐款明细则零散地记录在不同志愿者的笔记本上。这种低效的管理方式直接导致了领养率低下和资源分配不均的问题。
这正是我们开发这套流浪动物领养系统的初衷。作为一套基于Spring Boot的全栈解决方案,系统实现了从动物信息管理、领养流程追踪到公益捐款的完整闭环。特别值得一提的是,我们在用户权限设计上采用了RBAC(基于角色的访问控制)模型,确保普通用户和管理员的操作边界清晰。比如普通用户只能修改自己的领养申请,而救助站工作人员可以管理整个宠物数据库,这种细粒度的权限控制在实际运营中至关重要。
2. 技术架构设计解析
2.1 整体技术选型
在技术栈选择上,我们经过多次论证最终确定了以下组合方案:
- 前端:Thymeleaf模板引擎 + Bootstrap5
- 后端:Spring Boot 2.7 + Spring Security
- 数据库:MySQL 8.0
- 部署:Tomcat 9.0
选择Thymeleaf而非主流前后端分离架构的考虑在于:救助站工作人员多为非技术人员,需要直接打印页面信息。我们实测发现,使用Vue等框架生成的页面在救助站的旧款打印机上经常出现格式错乱。而Thymeleaf生成的静态页面兼容性更好,同时减少了接口开发工作量。
2.2 核心架构设计
系统采用经典的三层架构,但针对动物领养业务做了特殊优化:
code复制表现层(Web)
↑↓
业务逻辑层(Service)
↑↓
数据访问层(DAO)
在数据访问层,我们没有采用JPA而是选择MyBatis,主要考虑到:
- 复杂的领养状态查询需要手写SQL优化(如联合查询疫苗状态和领养进度)
- 动物照片存储采用BLOB字段,需要精细控制内存占用
- 报表统计功能涉及多表关联计算
3. 数据库设计与优化
3.1 关键表结构设计
核心的pet_info表设计经历了三次迭代:
sql复制CREATE TABLE `pet_info` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(20) NOT NULL COMMENT '宠物名称',
`category_id` bigint NOT NULL COMMENT '分类ID',
`age` int DEFAULT '0' COMMENT '年龄(月)',
`gender` char(1) DEFAULT '0' COMMENT '性别(0未知 1雄 2雌)',
`vaccination` tinyint DEFAULT '0' COMMENT '疫苗状态',
`adoption_status` tinyint DEFAULT '0' COMMENT '领养状态',
`story` text COMMENT '救助故事',
`cover_img` longblob COMMENT '封面图',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_category` (`category_id`),
KEY `idx_status` (`adoption_status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
特别说明几个设计要点:
- 将图片直接存储为BLOB而非URL路径,避免救助站网络不稳定导致的图片加载失败
- 使用
tinyint枚举值而非字符串存储状态,节省存储空间 - 为分类和状态字段建立索引,提升查询效率
3.2 业务关联设计
领养业务涉及的核心关联关系:
mermaid复制erDiagram
USER ||--o{ ADOPT_RECORD : has
PET_INFO ||--o{ ADOPT_RECORD : has
USER ||--o{ DONATION : "donates"
PET_INFO {
bigint id PK
varchar(20) name
bigint category_id FK
}
ADOPT_RECORD {
bigint id PK
bigint user_id FK
bigint pet_id FK
datetime apply_time
tinyint status
}
重要提示:在实际部署时,建议将图片等大字段拆分到单独的表,我们测试发现当宠物记录超过5000条时,全表扫描速度会下降约30%
4. 核心功能实现细节
4.1 领养状态机设计
领养流程我们实现了完整的状态机控制:
java复制public enum AdoptionStatus {
PENDING(0, "待审核"),
INTERVIEW(1, "面试安排"),
HOME_CHECK(2, "家访中"),
APPROVED(3, "已批准"),
REJECTED(4, "已拒绝"),
COMPLETED(5, "已完成");
// 状态校验逻辑
public static boolean isValidTransition(int from, int to) {
switch (from) {
case 0: return to == 1 || to == 4; // 待审核→面试/拒绝
case 1: return to == 2 || to == 4; // 面试→家访/拒绝
case 2: return to == 3 || to == 4; // 家访→批准/拒绝
case 3: return to == 5; // 批准→完成
default: return false;
}
}
}
4.2 捐款支付集成
我们对接了支付宝当面付API实现捐款功能,关键代码片段:
java复制@Transactional
public String createDonation(DonationDTO dto) {
// 校验用户有效性
User user = userRepository.findById(dto.getUserId())
.orElseThrow(() -> new BizException("用户不存在"));
// 生成捐款单号:日期(8)+用户ID(6)+随机数(4)
String donateNo = DateUtil.format(new Date(), "yyyyMMdd")
+ String.format("%06d", user.getId())
+ RandomUtil.randomNumbers(4);
// 调用支付接口
AlipayTradePrecreateResponse response = alipayClient.execute(
new AlipayTradePrecreateRequest()
.setBizModel(new AlipayTradePrecreateModel()
.setOutTradeNo(donateNo)
.setTotalAmount(dto.getAmount().toString())
.setSubject("流浪动物救助捐款")));
if (!response.isSuccess()) {
throw new BizException("支付创建失败: " + response.getSubMsg());
}
// 保存捐款记录
Donation record = new Donation();
record.setDonateNo(donateNo);
record.setUserId(user.getId());
record.setAmount(dto.getAmount());
record.setStatus(0); // 待支付
donationRepository.save(record);
return response.getQrCode(); // 返回支付二维码
}
5. 部署与性能优化
5.1 服务器配置建议
根据压力测试结果(JMeter模拟100并发),推荐配置:
- 开发环境:2核4G内存,50G SSD
- 生产环境:4核8G内存,100G SSD + 独立MySQL实例
我们发现性能瓶颈主要出现在图片加载环节,解决方案:
- 启用Tomcat的sendfile特性:修改server.xml
xml复制<Connector port="8080" sendfile="true" ... />
- 配置Spring静态资源缓存:
properties复制spring.resources.cache.cachecontrol.max-age=86400
spring.resources.cache.cachecontrol.cache-public=true
5.2 安全配置要点
在Spring Security配置中特别注意以下设置:
java复制@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable() // 因部分救助站使用老旧浏览器
.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/api/donate").authenticated()
.antMatchers("/static/**").permitAll()
.and()
.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/", true)
.failureHandler((req, res, e) -> {
// 记录登录失败日志
log.warn("登录失败: {}", req.getParameter("username"));
res.sendRedirect("/login?error");
});
}
6. 典型问题解决方案
6.1 领养信息搜索优化
初期采用LIKE查询导致性能问题:
sql复制-- 问题写法
SELECT * FROM pet_info
WHERE name LIKE '%小白%'
AND adoption_status = 0;
优化方案:
- 添加全文索引
sql复制ALTER TABLE pet_info ADD FULLTEXT INDEX ft_idx_name (name);
- 使用MATCH AGAINST语法
sql复制SELECT * FROM pet_info
WHERE MATCH(name) AGAINST('小白' IN BOOLEAN MODE)
AND adoption_status = 0;
实测查询速度从1200ms提升到80ms
6.2 并发领养冲突处理
使用乐观锁解决多人同时申请同一宠物的问题:
java复制@Transactional
public boolean applyAdoption(Long userId, Long petId) {
// 检查宠物是否可领养
Pet pet = petRepository.findById(petId)
.orElseThrow(() -> new BizException("宠物不存在"));
if (pet.getAdoptionStatus() != 0) {
throw new BizException("该宠物已被申请");
}
// 乐观锁更新
int updated = petRepository.updateStatusWithLock(
petId, 0, 1, pet.getVersion());
if (updated == 0) {
throw new BizException("申请冲突,请重试");
}
// 创建申请记录
AdoptRecord record = new AdoptRecord();
record.setUserId(userId);
record.setPetId(petId);
record.setStatus(0);
adoptRecordRepository.save(record);
return true;
}
7. 扩展功能建议
7.1 智能推荐功能
基于用户浏览历史实现简单的协同过滤推荐:
java复制public List<PetInfo> recommendPets(Long userId) {
// 获取用户最近浏览的宠物类别
List<Long> viewedCategories = browseHistoryRepository
.findTopCategoriesByUser(userId, 3);
// 获取同类别中待领养的宠物
return petRepository.findByCategoryInAndStatus(
viewedCategories,
AdoptionStatus.PENDING.getValue(),
PageRequest.of(0, 6));
}
7.2 微信小程序集成
建议后续开发小程序端,需要注意:
- 使用WxJava SDK处理微信登录
- 小程序端采用分页加载,每页不超过10条数据
- 图片使用CDN加速,建议开启WebP格式转换
这套系统在实际部署到某省会城市动物救助中心后,六个月内将领养率提升了40%,同时减少了工作人员约30%的数据处理时间。特别在疫苗到期提醒功能上线后,宠物疫苗接种率从65%提升到了92%。