作为一名在Java领域摸爬滚打多年的开发者,我最近完成了一个宠物管理系统的全栈项目——云宠之家。这个系统的诞生源于几个现实的痛点:我身边越来越多的朋友开始养宠物,但经常遇到宠物信息管理混乱、领养流程不透明、健康记录丢失等问题。传统的Excel记录方式已经无法满足现代宠物主的需求,而市面上的商业软件要么功能过剩,要么价格昂贵。
这个系统采用SpringBoot+Vue的主流技术栈,主要解决三类核心问题:
技术选型思考:为什么选择SpringBoot+Vue?
- 后端需要快速构建RESTful API → SpringBoot的自动配置和起步依赖能极大提升开发效率
- 前端需要响应式界面 → Vue的组件化开发适合构建动态用户界面
- 团队技术栈匹配 → 团队成员对这两个框架最熟悉,降低学习成本
系统采用典型的前后端分离架构,分为四个逻辑层:
code复制[前端展示层] Vue3 + Element Plus
↑
[API网关层] Spring Cloud Gateway
↑
[业务逻辑层] SpringBoot 2.7 + MyBatis Plus
↑
[数据持久层] MySQL 8.0 + Redis 6.2
关键设计决策:
宠物管理系统的数据模型有几个特殊设计:
sql复制CREATE TABLE `pet` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`name` VARCHAR(50) NOT NULL COMMENT '宠物名称',
`qr_code` VARCHAR(100) UNIQUE COMMENT '唯一二维码标识',
`genetic_info` JSON COMMENT '基因信息(结构化存储)',
`behavioral_traits` SET('friendly','shy','aggressive') COMMENT '行为特征多选',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
避坑指南:宠物行为特征字段为什么用SET类型?
- 传统方案是用逗号分隔的字符串,但查询效率低
- 改用SET类型后可以直接使用FIND_IN_SET()函数优化查询
- 注意:SET类型有64个成员的限制,适合固定选项的场景
宠物信息采集界面包含以下几个关键组件:
后端接口实现示例:
java复制@PostMapping("/pets")
@Idempotent(expireTime = 30, timeUnit = TimeUnit.SECONDS)
public Result addPet(@Valid @RequestBody PetDTO petDTO) {
// 二维码生成逻辑
String qrCode = QRCodeUtil.generate(petDTO.getName());
petDTO.setQrCode(qrCode);
// 基因信息压缩存储
if(petDTO.getGeneticInfo() != null) {
String compressed = GzipUtil.compress(petDTO.getGeneticInfo());
petDTO.setGeneticInfo(compressed);
}
return Result.success(petService.save(petDTO));
}
采用Spring Schedule实现定时提醒:
配置示例:
properties复制# application.properties
spring.task.scheduling.pool.size=5
pet.vaccine.remind.days=7
sms.template.code=VACCINE_REMINDER
领养流程采用状态机模式管理,核心状态包括:
mermaid复制stateDiagram-v2
[*] --> APPLIED: 提交申请
APPLIED --> REVIEWING: 初审通过
REVIEWING --> INTERVIEWING: 资料审核通过
INTERVIEWING --> APPROVED: 面试通过
APPROVED --> SIGNED: 签约完成
SIGNED --> [*]
状态转换通过PetAdoptionService实现:
java复制public void changeStatus(Long adoptionId, AdoptionStatus newStatus) {
Adoption adoption = getById(adoptionId);
AdoptionStatus current = adoption.getStatus();
if(!statusMachine.canTransfer(current, newStatus)) {
throw new BusinessException("状态转换不合法");
}
// 记录状态变更日志
statusLogService.recordChange(adoptionId, current, newStatus);
// 触发相关事件
eventPublisher.publishEvent(new StatusChangeEvent(adoptionId, current, newStatus));
}
采用法大大电子签约SDK实现:
集成关键点:
java复制public class FadadaCallbackController {
@PostMapping("/api/fadada/callback")
public String handleCallback(@RequestBody CallbackData data,
HttpServletRequest request) {
// 验证签名
if(!FadadaSignUtil.verify(request)) {
throw new SecurityException("签名验证失败");
}
// 处理不同类型的回调
switch(data.getEventType()) {
case "SIGN_COMPLETE":
adoptionService.updateContractStatus(data.getContractId());
break;
case "SIGN_REJECT":
adoptionService.notifyRejection(data.getContractId());
break;
}
return "success";
}
}
原始方案的问题:
优化后的解决方案:
二级缓存策略:
分页优化:
java复制public Page<PetVO> queryPets(PetQuery query) {
// 使用基于游标的分页替代传统LIMIT分页
if(query.getLastId() != null) {
return petMapper.selectPageByCursor(query);
}
// 小数据量时使用常规分页
Page<Pet> page = new Page<>(query.getPageNum(), query.getPageSize());
return petMapper.selectPageWithAssociations(page, query);
}
前端实现的懒加载方案:
vue复制<template>
<div class="pet-gallery">
<img
v-for="img in images"
:key="img.id"
:data-src="img.url"
class="lazy-img"
:alt="img.alt"
/>
</div>
</template>
<script>
import { useIntersectionObserver } from '@vueuse/core'
export default {
mounted() {
const images = document.querySelectorAll('.lazy-img')
images.forEach(img => {
useIntersectionObserver(img, ([{ isIntersecting }]) => {
if(isIntersecting) {
img.src = img.dataset.src
}
})
})
}
}
</script>
Docker Compose文件关键配置:
yaml复制version: '3.8'
services:
backend:
image: pet-home-backend:${TAG:-latest}
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
- REDIS_HOST=redis
depends_on:
- redis
- mysql
frontend:
image: pet-home-frontend:${TAG:-latest}
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf
mysql:
image: mysql:8.0
environment:
- MYSQL_ROOT_PASSWORD=${DB_PASSWORD}
volumes:
- mysql_data:/var/lib/mysql
volumes:
mysql_data:
Prometheus监控指标示例:
yaml复制# application.yml
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
metrics:
tags:
application: pet-home
export:
prometheus:
enabled: true
关键监控指标:
http_server_requests_seconds_count:接口调用次数jvm_memory_used_bytes:JVM内存使用hikaricp_connections_active:数据库连接池状态开发中遇到的CORS问题及解决路径:
java复制@Bean
public CorsWebFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOriginPattern("*");
config.addAllowedMethod("*");
config.addAllowedHeader("*");
config.setAllowCredentials(true);
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
宠物信息并发更新解决方案:
java复制public class Pet {
@Version
private Integer version;
}
java复制@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
vue复制<script setup>
const handleUpdateError = (error) => {
if(error.response?.data?.code === 'OPTIMISTIC_LOCK_ERROR') {
ElMessage.error('数据已被修改,请刷新后重试')
fetchLatestData()
}
}
</script>
这个项目从零开始到上线历时3个月,最大的体会是:技术方案没有绝对的好坏,关键是找到最适合当前团队和业务场景的平衡点。比如在状态机实现上,我们最初考虑过Activiti这样的专业工作流引擎,但考虑到学习成本和系统复杂度,最终选择了自研轻量级方案,事实证明这个决定让后续的迭代维护变得简单许多。