1. 为什么选这套组合:SpringBoot+Vue3+MyBatis的真实理由
宠物领养系统这个选题,几乎是Java后端学习路上的"标准动作"。不管是毕业设计、课程项目,还是想充实简历的练手项目,它都有个非常典型的特点:业务逻辑不复杂,但覆盖面很全——用户体系、内容发布、状态审核、数据检索、前后端交互,该有的环节一个不少。而这种体量的系统,恰好就是SpringBoot+Vue3+MyBatis这套组合最擅长的战场。
先说后端。SpringBoot把SSM时代那一堆繁琐的XML配置砍掉之后,一个宠物领养系统从零搭建到跑通业务,效率比传统方式高出一大截。它内置的自动配置机制能把数据源、事务、Web容器这些基础设施全部托管起来,我们只需要关心业务代码。MyBatis则负责把SQL从Java代码里剥离出去,尤其是领养系统里那些涉及多表联查、条件筛选、状态统计的查询,写在XML里比写在注解里清晰得多,也更容易调整。
再说前端。Vue3的Composition API对这类管理后台+展示页混合型项目非常友好。宠物列表的筛选逻辑、领养申请的表单校验、管理端的审核操作,这些功能用ref、reactive和computed组织起来,代码比Options API更紧凑,逻辑复用也方便。配合Vite的极速冷启动,前后端联调时改一行代码几十毫秒就能看到效果,这种体验感在Vue2+Webpack时代是可望不可及的。
加上MySQL做数据持久化,这套"SpringBoot+Vue3+MyBatis+MySQL"的组合,本质上就是当前中小型Web项目的主流配置。选它来做宠物领养系统,不是因为它是某个大厂的高端架构,而是因为它最贴近真实企业项目的开发模式——前后端分开部署、接口文档驱动联调、Git各管各的分支。从这个角度说,做完这个项目学到的不是"怎么写一个宠物领养系统",而是"一个真实Web项目是怎么组织起来的"。
2. 数据库设计:领养业务的核心在状态流转
2.1 四张核心表和一对多关系的梳理
宠物领养系统听起来业务简单,但数据库设计上有一个容易犯的错误:只盯着"宠物表"和"用户表",忽略了领养这个行为本身是要被跟踪和记录的。
我实际做的表结构是这样的:
user(用户表):id、username、password(加密存储)、phone、email、avatar、role(区分管理员和普通用户)、status(是否封禁)、create_time。管理员和普通用户用同一个表加角色字段区分,而不是拆成两张表,是因为两者的公共字段远多于差异字段,拆表带来的查询复杂度完全不值得。
pet(宠物表):id、name、category(猫/狗/其他)、breed(品种)、age、gender、is_neutered(是否绝育)、vaccine_status(疫苗情况)、description(详细描述)、images(图片URL,多张用逗号分隔)、status(0-待审核,1-已上架,2-已领养,3-已下架)、publisher_id(发布人)、create_time、update_time。
adoption_application(领养申请表):id、pet_id、applicant_id、reason(领养理由)、home_condition(居住情况)、experience(养宠经验)、status(0-待审核,1-已通过,2-已拒绝,3-已完成)、apply_time、audit_time、auditor_id。
favorite(收藏表):id、user_id、pet_id、create_time。
这四张表的关系是:用户一对多发布宠物,宠物一对多接收领养申请,用户一对多发起领养申请,用户和宠物通过收藏表形成多对多关系。看起来简单,但真正运行起来后你会发现,adoption_application这张表才是整个系统的核心——它是状态流转的主战场,也是并发问题最容易爆发的地方。
2.2 领养状态机的设计思路
领养系统的业务规则核心是一个状态机:宠物从发布到被领养,中间要经过"发布→审核→上架→申请→审核通过→完成"这六个节点。
很多初学方案把状态设计得非常随意,想到哪写哪,导致后端的if/else越来越乱。我的做法是先把状态流图画清楚:宠物发布后进入待审核状态,管理员通过后变为上架,此时用户才能看到并提交领养申请;管理员在收到申请后可以选择通过或拒绝,通过后宠物状态改为已领养,用户的申请记录标记为已完成。
这个设计里有一个容易被忽略的点:宠物状态和领养申请状态必须分开管理,但又需要保持联动。举个例子,管理员在审核领养申请时,需要同时更新adoption_application.status和pet.status两块数据。如果不加控制,就会出现"申请状态是已通过,但宠物状态还是上架中"这种数据不一致的问题。解决办法有两个层面:数据库层面,在审批时先检查宠物当前是否处于可领养状态,用UPDATE pet SET status = 2 WHERE id = ? AND status = 1来保证原子性;代码层面,把状态变更封装在同一个事务方法里,任何一步失败就整体回滚。
提示:状态字段建议用数字而不要用字符串。一来数据库存储和索引更高效,二来避免了字符串拼写不统一的问题。前端展示层再通过枚举或字典把它们映射成对应的中文文案。
3. 后端落地:SpringBoot分层架构与MyBatis的实战用法
3.1 项目初始化与目录结构设计
我初始化的项目用的是SpringBoot 2.7.x版本,这个版本是个"稳定舒适区"——既兼容JDK 8/11,又保留了javax命名空间,网上绝大多数教程和踩坑资料都适配它。如果选SpringBoot 3.x,就要求JDK 17以上,而且所有依赖里的javax.servlet会变成jakarta.servlet,很多第三方整合的示例代码直接跑不通,对新手来说就是个无底洞。
后端目录结构我按功能包划分:
com.example.adoption ├── config // 配置类:跨域、拦截器、WebMvc ├── controller // 控制层:接收请求、返回结果 ├── service // 业务层:核心业务逻辑 │ └── impl ├── mapper // MyBatis持久层接口 ├── entity // 实体类 ├── dto // 请求/响应对象(用于参数校验和结果封装) ├── vo // 视图对象(聚合多表数据) ├── common // 通用类:统一返回结果、异常处理、工具类 └── interceptor // JWT拦截器每一层各司其职:Controller只做参数接收和结果封装,不写任何业务代码;Service负责业务逻辑和事务管理;Mapper只负责数据库操作。这样分层的好处是当前项目规模不大时感觉有点"冗余",但一旦领养申请、宠物审核、用户收藏这些功能之间开始互相调用,分层清晰的收益就会立刻显现出来。
3.2 MyBatis的XML映射和动态SQL
MyBatis在这一项目里主要处理三类SQL:单表CRUD、多表联查、动态条件筛选。我全程用XML方式写Mapper,没有用注解。原因是宠物列表页的查询条件太多了——按分类、品种、年龄、性别、绝育状态、发布时间排序,这些条件组合起来可能是十几种情况,用动态SQL的<where>和<if>标签管理起来清晰直观,而注解里拼字符串会疯掉。
举个例子,宠物列表页的典型查询:
<select id="selectPetList" resultType="com.example.adoption.vo.PetVO"> SELECT p.*, u.username AS publisher_name FROM pet p LEFT JOIN user u ON p.publisher_id = u.id <where> p.status = 1 <if test="category != null and category != ''"> AND p.category = #{category} </if> <if test="gender != null and gender != ''"> AND p.gender = #{gender} </if> <if test="keyword != null and keyword != ''"> AND (p.name LIKE CONCAT('%', #{keyword}, '%') OR p.description LIKE CONCAT('%', #{keyword}, '%')) </if> <choose> <when test="sort == 'newest'"> ORDER BY p.create_time DESC </when> <otherwise> ORDER BY p.create_time ASC </otherwise> </choose> </where> </select>这里有个细节:<where>标签会自动去掉第一个多余的AND,所以所有条件都可以统一写成AND xxx,不用担心拼接问题。新手容易犯的错误是手动拼SQL时忘了处理最后一个条件后面的AND,换用MyBatis动态标签之后,这类问题从根上被消灭了。
3.3 MyBatis条件判断里那个"0被吞掉"的经典坑
这个坑我在实际开发里踩过一次,也是很多面试题会问的点。
假设要按宠物状态筛选,前端传过来status = 0(待审核状态),你写的XML判断是:
<if test="status != null and status != ''"> AND p.status = #{status} </if>看起来天衣无缝对不对?但实际执行时会发现,status = 0这条过滤条件根本没被拼上,查询结果把待审核之外的宠物全查出来了。
原因是MyBatis的OGNL表达式里,0会被解析成Integer类型,而''是空字符串。OGNL在做0 != ''比较时,会尝试把空字符串''转成数字,转换结果就是0,于是0 != ''这个表达式返回false,整个<if>条件被跳过。
解决办法有两种。第一种是把判断拆开写,只判断null:
<if test="status != null"> AND p.status = #{status} </if>第二种更精准,用toString()显式声明类型:
<if test="status != null and status.toString() != ''"> AND p.status = #{status} </if>这个坑如果不在还开发阶段发现,等到项目上线后管理员筛选列表数据出错,排查起来会非常痛苦——因为问题出在"看不见"的动态SQL拼接阶段,而不是明显的报错信息。类似的坑在MyBatis里其实不少,后面我会专门列一个"踩坑清单"。
3.4 领养审核流程中的事务与并发控制
领养审核操作是系统中最需要谨慎处理的接口。管理员点击"通过"按钮,后端要做的事情是:校验申请状态、更新申请状态为已通过、更新宠物状态为已领养、给申请者留一条状态变更记录。这个过程必须包在事务里。
我用@Transactional注解处理事务:
@Transactional(rollbackFor = Exception.class) public void approveAdoption(Integer applicationId, Integer adminId) { // 1. 查询申请记录并校验状态 AdoptionApplication app = adoptionApplicationMapper.selectById(applicationId); if (app == null || app.getStatus() != 0) { throw new BusinessException("申请记录不存在或已被处理"); } // 2. 原子性更新申请状态 int rows = adoptionApplicationMapper.updateStatus(applicationId, 1, adminId); if (rows == 0) { throw new BusinessException("申请已被其他人处理,请刷新后重试"); } // 3. 原子性更新宠物状态:确保宠物仍处于可领养状态 int petRows = petMapper.updateStatusByCondition(app.getPetId(), 1, 2); if (petRows == 0) { throw new BusinessException("该宠物已被领养,请刷新后重试"); } }这里最关键的是updateStatus和updateStatusByCondition这两个用了条件更新的SQL。它们不是先查后改(那样在并发场景下会有竞态条件),而是直接把"当前状态必须是待处理/上架中"作为WHERE条件写进UPDATE语句,通过数据库行锁来保证并发安全。两个管理员同时审核同一申请时,只有一个能更新成功,另外一个拿到rows = 0,直接提示"已被处理"。这种写法比加SELECT FOR UPDATE锁更轻量,也比在Java代码层用synchronized更可靠——因为集群部署时synchronized只能锁住单个实例,而数据库条件更新是全局生效的。
4. Vue3前端:从登录到领养申请的双角色工程
4.1 Vite初始化与工程结构
前端我用Vite创建Vue3项目,这一步比Vue CLI快得不是一星半点:
npm create vue@latest # 根据提示选择需要的特性:TypeScript选No,Router选Yes,Pinia选Yes工程结构按模块划分:
src ├── api // 接口请求封装 ├── assets // 静态资源 ├── components // 通用组件 ├── router // 路由配置 ├── stores // Pinia状态 ├── views // 页面视图 │ ├── home // 前台展示 │ ├── admin // 管理后台 │ └── user // 用户中心 ├── utils // 工具函数 └── App.vue一个容易忽略的细节是alias配置。默认情况下Vite里需要用相对路径../../来找组件,一旦目录嵌套深了非常痛苦。在vite.config.js里配置好@别名后,就可以用@/components/PetCard.vue这样的绝对方式导入了:
import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import path from 'path' export default defineConfig({ plugins: [vue()], resolve: { alias: { '@': path.resolve(__dirname, 'src') } } })4.2 跨域问题:前后端分离的第一个拦路虎
前后端分离开发,第一个遇到的问题必然是跨域。前端跑在http://localhost:5173,后端跑在http://localhost:8080,两者端口不同,浏览器直接请求就会触发CORS拦截。
我在后端配置类里统一处理跨域:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }开发环境这样配完全够用。但生产环境上线后要注意,allowedOriginPatterns("*")配合allowCredentials(true)时,不能使用*通配具体域名,否则部分浏览器会拦截携带Cookie的请求。更严谨的做法是把前端域名明确写出来,比如https://www.example.com。
4.3 路由守卫与动态侧边栏
宠物领养系统有前台用户和管理员两类角色,两者的权限差异很大:普通用户看到的是"首页、宠物列表、领养申请、个人中心",管理员看到的是"宠物审核、领养审核、用户管理、数据统计"。我用路由守卫在前端做了拦截控制。
路由配置里给每个页面标记需要的角色:
{ path: '/admin', component: () => import('@/views/admin/AdminLayout.vue'), meta: { requiresAuth: true, role: 'ROLE_ADMIN' }, children: [ { path: 'pet-audit', component: () => import('@/views/admin/PetAudit.vue') }, { path: 'adoption-audit', component: () => import('@/views/admin/AdoptionAudit.vue') } ] }然后在全局前置守卫里检查:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}') if (to.meta.requiresAuth && !token) { next('/login') return } if (to.meta.role && to.meta.role !== userInfo.role) { next('/403') return } next() })这套方案足够应对当前项目规模。但要说清楚一个事实:前端路由守卫只是优化体验的手段,真正的权限控制必须后端接口层也做。因为前端所有的JS代码用户都能看到,绕过路由守卫直接调后端接口是很容易的事情。所以我的后端在每个需要权限的接口上都加了@RequireRole类似的注解校验,前端守卫只是让普通用户看不到入口而已。
4.4 领养申请表单:一个实战级的表单校验案例
领养申请表单是整个前端交互最复杂的部分。它不仅字段多(领养理由、居住情况、养宠经验),而且提交后要处理三种状态:待审核、已通过、已拒绝。已拒绝时还要显示原因。
我用Element Plus的el-form做校验,规则配置如下:
const rules = { reason: [ { required: true, message: '请填写领养理由', trigger: 'blur' }, { min: 10, max: 500, message: '领养理由需在10到500字之间', trigger: 'blur' } ], homeCondition: [ { required: true, message: '请选择居住情况', trigger: 'change' } ], experience: [ { required: true, message: '请选择养宠经验', trigger: 'change' } ] }提交前检查是否重复申请过同一只宠物——这个逻辑我放在后端接口里做了,因为前端只能做显性校验,防不了恶意请求。后端查询逻辑是:
public void applyAdoption(AdoptionApplicationDTO dto, Integer userId) { // 校验:同一用户不能重复申请同一只宠物 Integer count = adoptionApplicationMapper.countByPetAndApplicant(dto.getPetId(), userId); if (count > 0) { throw new BusinessException("您已申请过该宠物,请勿重复申请"); } // 校验:宠物必须处于上架状态 Pet pet = petMapper.selectById(dto.getPetId()); if (pet == null || pet.getStatus() != 1) { throw new BusinessException("该宠物暂不可领养"); } // 插入申请记录 }4.5 宠物卡片列表的渲染与图片懒加载
前台首页的宠物列表用栅格布局展示宠物卡片,每张卡片包含宠物照片、名字、品种、性别、年龄标签。图片加载方面,我用Vue3自定义指令实现了懒加载,只有图片进入视口才真正加载数据,避免首屏加载几十张图把带宽吃满。
const lazyLoad = { mounted(el, binding) { const observer = new IntersectionObserver((entries) => { if (entries[0].isIntersecting) { el.src = binding.value observer.unobserve(el) } }) observer.observe(el) } }5. 图片上传、分页查询与MyBatis分页插件的整合
5.1 图片上传:本地存储还是对象存储
宠物领养系统里图片是刚需,一只待领养的宠物至少需要上传3-5张照片。我用的是本地磁盘存储方案:后端接收MultipartFile,保存到服务器指定目录,然后把访问路径返回给前端存到数据库。
public String uploadImage(MultipartFile file) { // 校验文件类型和大小 if (file.isEmpty() || file.getSize() > 5 * 1024 * 1024) { throw new BusinessException("图片大小不能超过5MB"); } String originalFilename = file.getOriginalFilename(); String suffix = originalFilename.substring(originalFilename.lastIndexOf(".")); List<String> allowedSuffix = Arrays.asList(".jpg", ".jpeg", ".png", ".gif"); if (!allowedSuffix.contains(suffix.toLowerCase())) { throw new BusinessException("不支持的图片格式"); } // 生成唯一文件名,避免重名覆盖 String fileName = UUID.randomUUID().toString().replace("-", "") + suffix; String dateDir = new SimpleDateFormat("yyyyMMdd").format(new Date()); File dir = new File(UPLOAD_DIR + dateDir); if (!dir.exists()) { dir.mkdirs(); } file.transferTo(new File(dir.getAbsolutePath() + File.separator + fileName)); // 返回访问URL return "/upload/" + dateDir + "/" + fileName; }在WebMvcConfig里把/upload/**映射为静态资源:
@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceHandler("file:" + UPLOAD_DIR); }本地方案的好处是简单、零成本,适合开发和单机部署。但坦白说,项目真要上线到云服务器上,我建议换成云存储——本地磁盘扩容是运维噩梦,而云存储自带CDN加速,访问速度也更快。不过对练手项目,本地完全够用。
5.2 分页查询:MyBatis分页插件的正确用法
宠物列表和领养申请列表都必须分页,否则数据量一上来页面会卡死。我用的PageHelper分页插件,整合步骤极简:
第一步,引入依赖:
<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.4.7</version> </dependency>第二步,业务方法里直接使用:
public PageResult<PetVO> getPetList(PetQueryDTO query) { PageHelper.startPage(query.getPageNum(), query.getPageSize()); List<PetVO> list = petMapper.selectPetList(query); PageInfo<PetVO> pageInfo = new PageInfo<>(list); return new PageResult<>(pageInfo.getTotal(), pageInfo.getList()); }PageHelper.startPage()之后紧跟着的第一条SQL会被自动加上LIMIT语句。这里有个必须牢记的规矩:startPage之后必须紧跟一个Mapper查询方法,中间不要插入任何其他数据库操作,否则分页会作用到错误的SQL上。
分页插件还有一些高级用法,比如PageInfo里自带pageNum、pageSize、pages(总页数)、isFirstPage、isLastPage等字段,前端直接用就行,不用自己算。
5.3 数据统计与首页大屏
管理后台我加了一个简单的数据统计页面,展示宠物总数、上架宠物数、待审核宠物数、本月领养成功数。这些统计用一条聚合SQL完成:
<select id="selectDashboardStats" resultType="com.example.adoption.vo.DashboardVO"> SELECT (SELECT COUNT(*) FROM pet) AS totalPetCount, (SELECT COUNT(*) FROM pet WHERE status = 1) AS availablePetCount, (SELECT COUNT(*) FROM pet WHERE status = 0) AS pendingPetCount, (SELECT COUNT(*) FROM adoption_application WHERE status = 3 AND MONTH(create_time) = MONTH(CURRENT_DATE)) AS monthlyAdoptedCount </select>这种仪表盘的统计一般不需要实时性要求很高的数据,偶尔缓存一下也没问题。
6. 开发过程中踩过的坑:从环境搭建到上线部署
6.1 IDEA创建SpringBoot项目:版本陷阱与选型建议
用IDEA的Spring Initializr创建项目时,默认选择的SpringBoot版本很可能是3.x。如果本机JDK是8或11,这时要么手动改成2.7.x,要么升级JDK到17。我实际开发时选了2.7.x,原因很实际:很多组件(比如某些工作流引擎、支付SDK)对SpringBoot 3的支持还不够成熟,网上可查的资料也大量集中在2.x时代。
如果确实想用SpringBoot 3.x,一定要提前确认几个配套组件的版本兼容性:MyBatis的mybatis-spring-boot-starter要用2.3.x以上版本,PageHelper要用1.4.6以上版本,并且注意javax到jakarta的包名变更。这些细节都确认清楚再开始,不然开发到一半被依赖冲突卡住,非常折磨。
6.2 MySQL安装配置:免安装版的正确姿势
MySQL的安装是这套项目里最容易卡住新手的一环。我用的免安装版(zip解压版)比msi安装包更可控,具体步骤:
- 从官网下载zip压缩包,解压到指定目录(路径不要有中文和空格)。
- 在解压目录下创建
my.ini配置文件,设置basedir和datadir路径。 - 以管理员身份打开命令行,执行
mysqld --initialize-insecure初始化(这样root用户默认空密码)。 - 执行
mysqld --install注册为Windows服务,然后net start mysql启动。 - 用
mysql -u root -p登录,执行ALTER USER 'root'@'localhost' IDENTIFIED BY '你的密码';设置密码。
配置数据库连接信息时,SpringBoot的application.yml里要设置时区和字符集:
spring: datasource: url: jdbc:mysql://localhost:3306/adoption?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver特别注意serverTimezone必须配,不配的话连接会报时区错误。useSSL=false是因为本地开发不需要SSL加密,不加访问时会有一大堆警告日志。
6.3 JWT登录鉴权:拦截器里的白名单
登录鉴权我用JWT实现。用户登录成功后,后端签发一个带用户ID和角色信息的Token返回前端;前端每次请求在Authorization头带上Token;后端通过拦截器解析Token,把当前用户信息放到ThreadLocal里供业务代码使用。
拦截器里一个容易遗漏的点是白名单配置。登录、注册、宠物列表、宠物详情这些接口不应该需要登录才能访问,否则游客连首页都看不了。我在拦截器里维护了一个白名单数组:
private static final String[] WHITE_LIST = { "/api/auth/login", "/api/auth/register", "/api/pet/list", "/api/pet/detail/**", "/upload/**" };/upload/**也必须放行,不然宠物列表页的图片会因为没带Token而加载失败——这个坑我实际遇到过,排查半天才发现是拦截器把静态资源也拦截了。
6.4 打包部署:前端Nginx + 后端Jar的经典组合
项目开发完成后部署上线,我采用的标准策略是:前端用Nginx托管,后端打包成Jar用java -jar运行,MySQL用系统服务。
前端打包命令:
npm run build打包完成后,dist目录里的静态文件上传到服务器,Nginx配置如下:
server { listen 80; server_name example.com; # 前端静态文件 root /usr/share/nginx/html; index index.html; # 前端路由history模式需要配置try_files location / { try_files $uri $uri/ /index.html; } # 反向代理后端接口 location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 上传的图片 location /upload/ { proxy_pass http://127.0.0.1:8080; } }try_files $uri $uri/ /index.html这一行必须配,否则前端路由用了history模式时,用户在非首页刷新会报404。
后端Jar启动命令,我一般用:
nohup java -jar -Xms256m -Xmx512m adoption-system.jar > app.log 2>&1 &nohup保证SSH断开后进程不退出,-Xms和-Xmx控制JVM堆内存大小。日志输出到app.log,出问题时方便排查。
6.5 我在实际开发中最后悔没早点做的事
做完这个项目复盘,有一个心得想分享:一定要从第一周就把接口文档写好,不要等联调的时候再补。
我一开始图快,接口参数改了又改,前后端通过聊天工具反复确认,浪费了大量时间。后来用类似Apifox或者直接在后端Swagger加注解的方式把接口文档生成出来,前端照着文档调接口,后端照着文档做自测,效率至少提升一倍。哪怕是一个人同时写前后端,这个习惯也值得养成——因为一周后你自己回头看自己写的接口,也会忘记当时设计时的细节。
另外一个建议是单元测试。领养审核这种核心流程,我用JUnit写了几条测试用例,包括正常审批、重复审批、宠物状态异常等场景,每次改完代码跑一遍就能确认核心逻辑没被改坏。虽然写测试短期内看起来"浪费时间",但如果项目稍后还要继续迭代,这笔投资回报率极高。
7. 功能演示与项目扩展:这套架构还能怎么玩
宠物领养系统跑通之后,可以沿着几个方向扩展,既是简历亮点也是技术深度的体现:
消息通知模块:领养申请状态变化后,给用户发送站内信或邮件通知。技术实现上可以在申请状态变更的地方发布一个Spring事件(ApplicationEvent),监听器里异步处理通知逻辑,这样核心审核逻辑不会因为通知发送失败而回滚。
全文检索:宠物列表页的关键字搜索目前只是SQL的LIKE模糊查询,数据量大时性能会遇到瓶颈。可以引入Elasticsearch把宠物信息建立索引,检索速度和相关性都会大幅提升。这个扩展对简历的加分效果非常明显。
Redis缓存热点数据:宠物详情页通常是访问最频繁的接口,可以把热点宠物的详情数据缓存到Redis里,设置过期时间,减少数据库压力。用SpringCache+Redis整合,改造成本很低。
图片懒加载再优化:目前粗粒度查看了正确的本地实现,如果换用CDN+缩略图方案:上传时同时生成缩略图,列表页加载缩略图,详情页加载原图,页面性能还会有明显改善。
从一个技术练手项目的角度,这套"SpringBoot+Vue3+MyBatis+MySQL"的组合已经足够让你完整经历一个Web应用从需求分析、数据库设计、前后端开发到打包部署的全过程。认真做完,再针对上面的扩展方向挑一个深入研究,无论是继续做毕设还是写进简历做项目经验,都完全拿得出手。