1. 项目概述
去年参与开发了一个流浪动物领养平台,采用前后端分离架构,前端使用Vue3+Element Plus,后端基于SpringBoot+MyBatis实现。这个系统解决了传统动物救助站信息不透明、领养流程繁琐的问题,让领养者可以线上查看动物信息、提交申请,同时方便救助站管理动物档案。
在实际开发中,我们遇到了不少技术挑战,比如如何设计高效的动物信息检索接口、如何处理高并发的领养申请、如何确保系统安全性等。通过这个项目,我积累了一套完整的解决方案,下面就把关键实现细节和踩坑经验分享给大家。
2. 系统架构设计
2.1 技术选型考量
选择Vue3+SpringBoot这套技术栈主要基于以下几点考虑:
- 开发效率:Vue3的Composition API和SpringBoot的自动配置能显著减少样板代码
- 生态成熟:Element Plus提供了丰富的UI组件,MyBatis-Plus简化了数据库操作
- 性能需求:Pinia的状态管理比Vuex更轻量,SpringBoot的内嵌Tomcat足够应对初期流量
注意:如果预计访问量很大(日活10万+),建议后端考虑Spring Cloud微服务架构,但会增加部署复杂度。
2.2 前后端分离实践
我们采用了典型的前后端分离架构:
- 前端:部署在Nginx,通过443端口提供HTTPS服务
- 后端:SpringBoot应用运行在8080端口
- 通信:使用HTTPS+JWT保证安全性
这种架构的优势在于:
- 前后端可以并行开发
- 前端资源可以CDN加速
- 后端服务可以独立扩展
3. 核心模块实现
3.1 用户认证模块
采用JWT+SpringSecurity实现认证流程:
java复制// JWT生成过滤器
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) {
String token = request.getHeader("Authorization");
if (token != null && token.startsWith("Bearer ")) {
String jwt = token.substring(7);
String username = jwtUtil.extractUsername(jwt);
if (username != null && SecurityContextHolder.getContext()
.getAuthentication() == null) {
UserDetails userDetails = userDetailsService
.loadUserByUsername(username);
if (jwtUtil.validateToken(jwt, userDetails)) {
// 验证通过后设置认证信息
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
userDetails, null,
userDetails.getAuthorities());
authToken.setDetails(
new WebAuthenticationDetailsSource()
.buildDetails(request));
SecurityContextHolder.getContext()
.setAuthentication(authToken);
}
}
}
filterChain.doFilter(request, response);
}
}
关键配置项:
- token有效期:设置为2小时(生产环境建议更短)
- 密钥长度:至少256位HS512算法
- 刷新机制:通过refreshToken实现无感刷新
3.2 动物信息模块
3.2.1 数据库设计
sql复制CREATE TABLE `animal` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
`type` enum('DOG','CAT','OTHER') COLLATE utf8mb4_unicode_ci NOT NULL,
`age` int DEFAULT NULL COMMENT '月份',
`health_status` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`vaccination` tinyint(1) DEFAULT '0',
`sterilization` tinyint(1) DEFAULT '0',
`description` text COLLATE utf8mb4_unicode_ci,
`cover_image` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`shelter_id` bigint DEFAULT NULL,
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_type` (`type`),
KEY `idx_shelter` (`shelter_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
3.2.2 高效查询实现
使用MyBatis-Plus的动态SQL实现多条件筛选:
java复制public Page<AnimalVO> queryAnimals(AnimalQueryDTO queryDTO) {
return lambdaQuery()
.eq(queryDTO.getType() != null, Animal::getType, queryDTO.getType())
.ge(queryDTO.getMinAge() != null, Animal::getAge, queryDTO.getMinAge())
.le(queryDTO.getMaxAge() != null, Animal::getAge, queryDTO.getMaxAge())
.eq(queryDTO.getVaccination() != null,
Animal::getVaccination, queryDTO.getVaccination())
.like(StringUtils.isNotBlank(queryDTO.getKeyword()),
Animal::getName, queryDTO.getKeyword())
.page(new Page<>(queryDTO.getPageNum(), queryDTO.getPageSize()))
.convert(this::toVO);
}
性能优化点:
- 为常用查询字段建立索引
- 使用DTO接收查询参数,避免Map导致SQL注入风险
- 分页查询一定要指定合理的pageSize(我们设置为10)
4. 领养申请流程
4.1 状态机设计
领养申请的状态流转采用状态机模式:
code复制[PENDING] → [APPROVED] → [COMPLETED]
↘ [REJECTED]
实现代码:
java复制public enum ApplicationStatus {
PENDING("待审核"),
APPROVED("已通过"),
REJECTED("已拒绝"),
COMPLETED("已完成");
@JsonCreator
public static ApplicationStatus from(String value) {
for (ApplicationStatus status : values()) {
if (status.name().equalsIgnoreCase(value)) {
return status;
}
}
throw new IllegalArgumentException("无效状态");
}
}
4.2 并发控制方案
为防止同一动物被重复领养,我们采用两种方案:
-
数据库层面:添加唯一索引
sql复制ALTER TABLE adoption_application ADD UNIQUE INDEX idx_animal_status (animal_id, status) WHERE status IN ('PENDING', 'APPROVED'); -
应用层面:分布式锁
java复制public boolean submitApplication(ApplicationDTO dto) { String lockKey = "animal:apply:" + dto.getAnimalId(); try { if (redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS)) { // 执行业务逻辑 return applicationService.createApplication(dto); } throw new BusinessException("当前动物正在被其他用户申请"); } finally { redisTemplate.delete(lockKey); } }
5. 前端关键实现
5.1 权限控制方案
基于Vue Router的全局守卫实现:
javascript复制router.beforeEach((to, from, next) => {
const authStore = useAuthStore()
if (to.meta.requiresAuth && !authStore.isAuthenticated) {
next({ name: 'Login', query: { redirect: to.fullPath } })
} else if (to.meta.roles && !to.meta.roles.includes(authStore.user.role)) {
next({ name: 'Forbidden' })
} else {
next()
}
})
5.2 文件上传组件
使用Element Plus的Upload组件配合后端接口:
vue复制<el-upload
action="/api/upload"
:headers="{ Authorization: `Bearer ${token}` }"
:on-success="handleSuccess"
:before-upload="beforeUpload"
>
<el-button type="primary">上传动物照片</el-button>
<template #tip>
<div class="el-upload__tip">
只能上传jpg/png文件,且不超过2MB
</div>
</template>
</el-upload>
后端接口实现:
java复制@PostMapping("/upload")
public Result<String> upload(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
throw new BusinessException("请选择文件");
}
if (file.getSize() > 2 * 1024 * 1024) {
throw new BusinessException("文件大小不能超过2MB");
}
String contentType = file.getContentType();
if (!"image/jpeg".equals(contentType) && !"image/png".equals(contentType)) {
throw new BusinessException("只支持JPEG/PNG格式");
}
String filename = UUID.randomUUID() +
Objects.requireNonNull(file.getOriginalFilename())
.substring(file.getOriginalFilename().lastIndexOf("."));
Path path = Paths.get(uploadPath, filename);
Files.copy(file.getInputStream(), path, StandardCopyOption.REPLACE_EXISTING);
return Result.success("/uploads/" + filename);
}
6. 部署与监控
6.1 Docker部署方案
后端Dockerfile配置:
dockerfile复制FROM openjdk:17-jdk-slim
VOLUME /tmp
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
使用docker-compose编排:
yaml复制version: '3'
services:
app:
build: .
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
- DB_URL=jdbc:mysql://mysql:3306/adoption
- DB_USER=root
- DB_PASSWORD=123456
depends_on:
- mysql
- redis
mysql:
image: mysql:8.0
environment:
- MYSQL_ROOT_PASSWORD=123456
- MYSQL_DATABASE=adoption
volumes:
- mysql_data:/var/lib/mysql
redis:
image: redis:6
ports:
- "6379:6379"
volumes:
mysql_data:
6.2 监控配置
使用Spring Boot Actuator暴露健康检查端点:
yaml复制management:
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
show-details: always
prometheus:
enabled: true
配合Prometheus+Grafana实现可视化监控:
code复制scrape_configs:
- job_name: 'spring'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['host.docker.internal:8080']
7. 踩坑经验分享
-
MyBatis缓存问题:在更新操作后,查询结果没有及时更新
- 解决方案:在Mapper上添加
@CacheNamespace(flushInterval = 60000)控制刷新间隔
- 解决方案:在Mapper上添加
-
Vue响应式丢失:使用解构赋值导致响应式失效
- 正确做法:使用
toRefs保持响应式
javascript复制const state = reactive({ list: [] }) const { list } = toRefs(state) - 正确做法:使用
-
时间格式问题:前端显示的时间与数据库存储不一致
- 解决方案:统一使用UTC时间传输,前端使用dayjs处理时区
javascript复制dayjs.utc(timeStr).local().format('YYYY-MM-DD HH:mm') -
文件上传大小限制:SpringBoot默认只允许1MB文件
- 配置调整:
yaml复制spring: servlet: multipart: max-file-size: 10MB max-request-size: 10MB
这个项目从技术选型到最终上线历时3个月,最大的收获是理解了如何根据业务特点选择合适的技术方案。比如在动物搜索功能上,初期使用简单的LIKE查询,后来改用Elasticsearch实现了更高效的全文检索。