你正在做毕业设计或项目实战,打算用前后端分离的方式写一个管理系统,但最怕的不是"不会写接口",而是"写完不知道下一步干什么"。如果你刚完成一个Spring Boot项目,接着要写前端页面,然后还要把AI能力接进去,这篇文章应该能帮你把整条线串起来。
这篇文章围绕一个完整的业务系统展开:AI宠物领养管理系统。它不是一个普通的CRUD练习,而是一个包含用户端、管理端、宠物档案、领养流程、图片识别等完整业务闭环的全栈项目。核心判断是:真正让你从"会写接口"进步到"能交付项目"的,不是某个框架的新特性,而是你把前端、后端、数据库、AI服务、前后端联调这五件事串起来的工程能力。
读完你会有三个收获:第一,理解这类前后端分离项目的模块划分方式;第二,拿到一套可以直接抄作业的宠物领养业务表结构和核心接口;第三,弄明白AI能力(宠物品种识别、相似宠物推荐)应该放在哪个环节、怎么和业务代码对接。
1. 宠物领养管理系统,难点从来不是CRUD
很多人拿到"XX管理系统"第一反应是:不就是用户表、商品表、订单表,然后写增删改查吗?这话对了一半。增删改查确实是基础,但一个能上台演示、能写进简历、能在答辩时讲出亮点的系统,真正难的是三层东西。
第一层是业务状态流转。宠物领养不是用户点一下"我要领养"就结束的。一只宠物要经历"待领养 - 申请中 - 审核通过 - 已领养 - 已下架"这些状态。每个状态由谁触发、在哪个接口触发、触发后其他模块要做什么,这些规则才是业务系统的灵魂。很多新手项目死在"状态是字符串,到处随便改",最后代码里到处都是if else,逻辑根本没法看。
第二层是角色权限。领养系统里至少有三类人:游客可以浏览宠物、注册用户可以提交领养申请、管理员可以审核申请并维护宠物档案。如果你把所有接口都公开,或者所有页面都不做权限控制,项目演示时面试官追问一句"怎么防止普通用户直接调用管理员接口",你就很难回答。Spring Boot里的拦截器、JWT、Vue里的路由守卫,这些不是八股文,而是真实系统必须有的东西。
第三层是AI怎么落地。既然叫"AI宠物领养管理系统",AI不能只是标题里的装饰词。比较自然的落地方式是:用户上传宠物照片时,系统自动识别宠物品种,辅助用户填写档案;或者根据用户填写的居住条件、养宠经验,推荐匹配度高的宠物。这不是炫技,而是真实业务里能提效的功能点。
所以这篇文章不是给你堆一堆代码,而是用宠物领养这个场景,把"业务设计 -> 接口开发 -> 前端对接 -> AI接入 -> 项目验证"全部走一遍。你把这个项目做完,比背十套模板项目都有用。
2. 系统功能设计与技术选型
2.1 你要实现的完整模块清单
先看整体功能边界,做项目最忌讳上来就写代码,先把模块画清楚。
| 模块 | 子功能 | 面向角色 |
|---|---|---|
| 用户认证 | 注册、登录、个人信息 | 游客、用户、管理员 |
| 宠物管理 | 宠物列表、宠物详情、发布宠物、上下架 | 用户、管理员 |
| 领养流程 | 提交申请、审核、状态查询 | 用户、管理员 |
| AI 能力 | 品种识别、宠物相似推荐 | 用户、系统 |
| 后台管理 | 用户管理、宠物审核、领养审核 | 管理员 |
| 消息通知 | 申请结果通知、宠物上新提醒 | 系统 |
这里需要说明一个设计取舍:宠物发布和领养申请,现实中可能是管理员统一录入,但在教学项目里,我们可以把"发布宠物"和"提交领养申请"都开放给用户,管理员只做审核。这样做的好处是,项目的所有角色都会深度参与整个流程,演示起来也更完整。
2.2 技术栈选型
这套系统的技术栈选择原则是:前端主流、后端主流、AI能力最低成本接入。你不用追求最新版本,但每项选型都要知道为什么选它。
| 层次 | 技术选型 | 说明 |
|---|---|---|
| 前端 | Vue 3 + Vite + Element Plus + Pinia | 中后台项目的成熟组合,组件生态丰富 |
| 后端 | Spring Boot + MyBatis Plus + MySQL | Java项目实战的主力组合,资料多、排错容易 |
| 认证 | JWT + 拦截器 | 无状态认证,适合前后端分离 |
| AI | 图像识别API + 相似度推荐 | 不在本地训练模型,调用现成能力,门槛低 |
| 部署 | Docker(可选) | 后阶段打包部署,项目实战加分项 |
如果你是Python方向,后端也可以换成FastAPI或Django,但本文代码示例按Spring Boot来写。前后端分离的思想是通用的,业务设计更是可以照搬的。
3. 数据库设计:领养系统的表结构怎么规划
数据库设计是项目实战的地基。很多翻车的项目,不是代码写错了,是表结构设计有缺陷,导致后面每个接口都写得很难受。宠物领养系统的核心表我建议分为五张。
3.1 核心表结构清单
第一张是用户表user,字段包括id、username、password(加密存储)、phone、role(USER/ADMIN)、create_time。
第二张是宠物表pet,这是系统的核心业务表,字段包括宠物名称、品种、年龄、性别、健康状况、描述、图片地址、状态、发布人ID等。
第三张是领养申请表adoption_apply,记录谁在什么时间申请了哪只宠物,审核状态、审核意见、申请时的居住情况、养宠经验等。
第四张是宠物图片表pet_image。为什么要单独建?因为一只宠物通常有多张照片,而且AI识别功能需要在图片上传后做异步处理,单独建表方便记录每张图的识别状态和识别结果。
第五张可以是消息表notification,记录系统给用户发送的申请结果通知。
3.2 宠物表与领养申请表的DDL参考
下面是宠物表的核心DDL,字段类型和注释写清楚,这是基本功:
CREATE TABLE `pet` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `pet_name` varchar(50) NOT NULL COMMENT '宠物名称', `species` varchar(50) NOT NULL COMMENT '物种,如猫/狗', `breed` varchar(50) DEFAULT NULL COMMENT '品种,如英短/金毛', `age` int(11) DEFAULT NULL COMMENT '年龄,单位月', `gender` tinyint(1) DEFAULT NULL COMMENT '性别:0-母 1-公', `health_status` varchar(255) DEFAULT NULL COMMENT '健康状况描述', `description` text COMMENT '宠物介绍', `main_image` varchar(255) DEFAULT NULL COMMENT '主图地址', `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态:0-待审核 1-待领养 2-申请中 3-已领养 4-已下架', `user_id` bigint(20) NOT NULL COMMENT '发布人ID', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='宠物信息表';领养申请表同样要重视:
CREATE TABLE `adoption_apply` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `pet_id` bigint(20) NOT NULL COMMENT '宠物ID', `user_id` bigint(20) NOT NULL COMMENT '申请人ID', `reason` varchar(500) DEFAULT NULL COMMENT '领养理由', `has_experience` tinyint(1) DEFAULT '0' COMMENT '是否有养宠经验', `home_type` varchar(50) DEFAULT NULL COMMENT '居住类型:租房/自有住房', `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态:0-待审核 1-已通过 2-已拒绝', `audit_comment` varchar(255) DEFAULT NULL COMMENT '审核意见', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='领养申请表';这里要特别解释一下pet.status和adoption_apply.status的关系。当领养申请提交成功时,pet.status要同步从"待领养"改成"申请中";当审核拒绝时,pet.status要恢复成"待领养";审核通过则改成"已领养"。这两个状态不能靠前端传值随意修改,一定要在后端Service层通过事务保证一致性。
4. 后端接口开发:从登录认证到领养流程
4.1 项目结构与认证设计
后端项目按模块分包,推荐结构如下:
src/main/java/com/example/petadopt/ ├── controller/ # 接口层 ├── service/ # 业务逻辑层 ├── mapper/ # 数据访问层 ├── entity/ # 实体类 ├── dto/ # 接口入参出参对象 ├── config/ # 配置类 ├── interceptor/ # 登录拦截器 ├── util/ # 工具类 └── common/ # 通用返回结果、异常处理认证用的是JWT。用户登录成功后,后端生成token返回给前端。前端把token存在localStorage里,每次请求在请求头携带Authorization字段。后端写一个拦截器统一校验,校验通过才放行。
这里是JWT工具类的核心逻辑,重点看token的生成和解析方式:
// 文件路径:src/main/java/com/example/petadopt/util/JwtUtil.java public class JwtUtil { private static final String SECRET = "your-secret-key-change-in-production"; private static final long EXPIRE_TIME = 7 * 24 * 60 * 60 * 1000L; // 生成token public static String generateToken(Long userId, String role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim("role", role) .setExpiration(new Date(System.currentTimeMillis() + EXPIRE_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } // 解析token public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token) .getBody(); } }注意加密密钥在生产环境必须放到配置中心或环境变量里,不能写死在代码中。这是项目实战里一个非常重要的安全习惯。
4.2 领养申请的状态流转逻辑
这部分的代码是整个后端业务的核心,比单纯的CRUD要值得写得多。提交领养申请的Service方法,逻辑是这样的:
- 校验宠物存在且状态是"待领养"。
- 校验该用户没有正在审核中的申请(避免重复申请)。
- 插入一条领养申请记录。
- 把宠物状态改为"申请中"。
- 所有操作在一个事务里,任何一步失败都要回滚。
// 文件路径:src/main/java/com/example/petadopt/service/impl/AdoptionApplyServiceImpl.java @Transactional(rollbackFor = Exception.class) public Result submitApply(AdoptionApplyDTO dto, Long userId) { // 1. 查宠物 Pet pet = petMapper.selectById(dto.getPetId()); if (pet == null) { return Result.error("宠物不存在"); } if (pet.getStatus() != PetStatus.WAIT_ADOPT.getCode()) { return Result.error("该宠物当前不可领养"); } // 2. 查重复申请 Integer count = adoptionApplyMapper.countByUserAndPet(userId, dto.getPetId(), AdoptionStatus.WAIT_AUDIT.getCode()); if (count > 0) { return Result.error("你已提交过申请,请等待审核"); } // 3. 插入申请记录 AdoptionApply apply = new AdoptionApply(); apply.setPetId(dto.getPetId()); apply.setUserId(userId); apply.setReason(dto.getReason()); apply.setHasExperience(dto.getHasExperience()); apply.setHomeType(dto.getHomeType()); apply.setStatus(AdoptionStatus.WAIT_AUDIT.getCode()); adoptionApplyMapper.insert(apply); // 4. 同步宠物状态 pet.setStatus(PetStatus.APPLYING.getCode()); petMapper.updateById(pet); return Result.success("申请提交成功"); }这里特别强调一下状态值为什么要用常量类或枚举,而不是散落的魔法数字。因为前后端对接、AI状态同步、将来加状态记录表,都依赖一个统一的约定。写代码的时候每个数字都写注释,不然三天之后你自己都分不清1到底代表"已通过"还是"申请中"。
4.3 管理员审核接口
管理员的审核接口同样要考虑状态一致性。审核拒绝时,除了更新申请状态,还要把宠物的状态恢复为"待领养";审核通过时,宠物状态改为"已领养"。这样才能保证用户看到的宠物状态永远是准确、一致的。
@Transactional(rollbackFor = Exception.class) public Result auditApply(Long applyId, Integer status, String comment) { AdoptionApply apply = adoptionApplyMapper.selectById(applyId); if (apply == null) { return Result.error("申请记录不存在"); } if (apply.getStatus() != AdoptionStatus.WAIT_AUDIT.getCode()) { return Result.error("该申请已处理过"); } Pet pet = petMapper.selectById(apply.getPetId()); apply.setStatus(status); apply.setAuditComment(comment); adoptionApplyMapper.updateById(apply); if (status == AdoptionStatus.APPROVED.getCode()) { pet.setStatus(PetStatus.ADOPTED.getCode()); } else { pet.setStatus(PetStatus.WAIT_ADOPT.getCode()); } petMapper.updateById(pet); // 给用户发送站内信 sendNotification(apply.getUserId(), "你的领养申请已" + (status == AdoptionStatus.APPROVED.getCode() ? "通过" : "被拒绝")); return Result.success("审核完成"); }这段代码直接体现了"事务 + 状态机"的编程思想。你写完这两个接口,整个领养业务的主链路就已经通了。
5. 前端页面开发:Vue3 + Element Plus实战
5.1 前端项目结构与路由
前端用Vue3 + Vite + Element Plus。项目结构如下:
src/ ├── api/ # 接口请求封装 ├── views/ │ ├── home/ # 首页和宠物列表 │ ├── pet/ # 宠物详情、发布宠物 │ ├── apply/ # 我的申请 │ ├── admin/ # 管理后台 │ └── login/ # 登录注册 ├── router/ # 路由配置 ├── store/ # Pinia状态管理 ├── components/ # 公共组件 └── utils/ # 请求封装、token处理路由配置里要做两件关键事:给需要登录的页面加meta.requiresAuth,给管理员页面加meta.requiresAdmin,然后在路由守卫里做跳转判断。
// 文件路径:src/router/index.js const routes = [ { path: '/login', component: Login }, { path: '/', component: Home }, { path: '/pet/:id', component: PetDetail }, { path: '/publish', component: PublishPet, meta: { requiresAuth: true } }, { path: '/my-apply', component: MyApply, meta: { requiresAuth: true } }, { path: '/admin', component: AdminDashboard, meta: { requiresAuth: true, requiresAdmin: true } } ]; router.beforeEach((to, from, next) => { const token = localStorage.getItem('token'); if (to.meta.requiresAuth && !token) { next('/login'); return; } const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}'); if (to.meta.requiresAdmin && userInfo.role !== 'ADMIN') { next('/'); return; } next(); });5.2 宠物卡片列表页
宠物列表页是门面功能,设计得好看直接影响项目整体印象。用Element Plus的el-card组件展示宠物卡片,每个卡片展示主图、品种、年龄、状态标签。这个页面的核心在于把后端的status字段映射成用户能看懂的状态标签。
<!-- 文件路径:src/views/home/Home.vue --> <template> <div class="pet-grid"> <el-card v-for="pet in petList" :key="pet.id" class="pet-card"> <img :src="pet.mainImage" class="pet-image" alt="宠物图片" /> <div class="pet-info"> <div class="pet-name">{{ pet.petName }}</div> <div class="pet-breed">{{ pet.species }} · {{ pet.breed || '未知品种' }}</div> <el-tag :type="statusType(pet.status)">{{ statusText(pet.status) }}</el-tag> <el-button type="primary" size="small" @click="goDetail(pet.id)"> 查看详情 </el-button> </div> </el-card> </div> </template> <script setup> import { ref, onMounted } from 'vue'; import { useRouter } from 'vue-router'; import { getPetList } from '@/api/pet'; const router = useRouter(); const petList = ref([]); const statusText = (status) => { const map = { 0: '待审核', 1: '待领养', 2: '申请中', 3: '已领养', 4: '已下架' }; return map[status] || '未知状态'; }; const statusType = (status) => { const map = { 0: 'warning', 1: 'success', 2: 'primary', 3: 'info', 4: 'danger' }; return map[status] || 'info'; }; const goDetail = (id) => router.push(`/pet/${id}`); onMounted(async () => { const res = await getPetList(); petList.value = res.data; }); </script>这段代码已经把"列表页"需要掌握的知识点都覆盖了:接口调用、状态映射、路由跳转。真正到你的项目里,可以在这个基础上加分页、搜索筛选、图片懒加载。
5.3 领养申请表单与提交
申请表单要注意的不是表单控件本身,而是提交成功后如何处理。因为一个用户只能对同一只宠物申请一次,所以提交成功后,按钮应该变成"已申请",并且不能再重复提交。
<el-form :model="applyForm" label-width="100px"> <el-form-item label="领养理由"> <el-input v-model="applyForm.reason" type="textarea" :rows="4" placeholder="请简单介绍你的领养计划和条件" /> </el-form-item> <el-form-item label="养宠经验"> <el-switch v-model="applyForm.hasExperience" active-text="有" inactive-text="无" /> </el-form-item> <el-form-item label="居住情况"> <el-radio-group v-model="applyForm.homeType"> <el-radio value="self_owned">自有住房</el-radio> <el-radio value="rent">租房</el-radio> </el-radio-group> </el-form-item> <el-form-item> <el-button type="primary" :loading="submitting" @click="submitApply"> 提交申请 </el-button> </el-form-item> </el-form>前端提交逻辑要注意异常处理。后端返回的错误信息要原样展示给用户,比如"该宠物当前不可领养""你已提交过申请"。这些提示直接决定用户体验,不要在前端写死错误文案。
6. AI能力接入:宠物品种识别与相似推荐
6.1 AI能力在整个系统中的位置
传统的宠物领养系统,宠物品种是用户或管理员手动填写的,漏填、错填非常常见。AI接入之后,发布宠物时用户上传照片,系统自动识别出品种,用户确认或修正后保存。这个流程既真实又不过度复杂,是项目实战里最合适的AI落地点。
同时可以做一个"相似宠物推荐":用户看中一只金毛后,系统根据品种、年龄、健康状况计算相似度,在详情页底部推荐其他宠物。这个功能不一定要用深度学习模型,基于标签匹配和简单的相似度算法就能实现,但演示效果很好。
6.2 接入方式的推荐与代码示例
AI能力不建议自己在本地训练模型,真实项目中也是调用现成的视觉识别API,或者调用自己团队部署的模型服务。实践中通常的做法是:后端把宠物图片地址传给AI服务,AI服务返回识别结果,后端再更新宠物记录。
// 文件路径:src/main/java/com/example/petadopt/service/AiRecognizeService.java @Service public class AiRecognizeService { private RestTemplate restTemplate; public AiRecognizeService(RestTemplate restTemplate) { this.restTemplate = restTemplate; } /** * 调图像识别服务识别宠物品种 * imageUrl 是上传后的图片地址,由文件服务返回 */ public RecognizeResult recognizeBreed(String imageUrl) { String url = "http://ai-service:8081/api/recognize"; Map<String, String> requestBody = new HashMap<>(); requestBody.put("imageUrl", imageUrl); ResponseEntity<RecognizeResult> response = restTemplate.postForEntity( url, requestBody, RecognizeResult.class); if (response.getStatusCode().is2xxSuccessful()) { return response.getBody(); } // 降级处理:AI服务不可用时返回空结果,不影响主流程 return new RecognizeResult(null, 0.0, false); } }这里要特别强调降级处理的重要性。真实项目里AI服务可能超时、可能限流。如果AI挂了整个发布流程就走不下去,这是不能接受的。所以代码里当AI服务不可用时,返回空结果,前端提示"自动识别失败,请手动填写品种",而不是报错。
AI识别结果更新宠物字段的业务代码:
public void recognizeAndUpdate(Long petId) { Pet pet = petMapper.selectById(petId); if (pet == null || pet.getMainImage() == null) { return; } try { RecognizeResult result = aiRecognizeService.recognizeBreed(pet.getMainImage()); if (result.isSuccess() && result.getBreed() != null) { if (StringUtils.isBlank(pet.getBreed())) { pet.setBreed(result.getBreed()); petMapper.updateById(pet); } } } catch (Exception e) { // AI识别失败不能影响主流程,记录日志后人工处理 log.error("AI recognize failed for petId: {}", petId, e); } }AI服务是否可用的一个最简单的判断方式:发布宠物成功后,前端调"开始识别"按钮,后端调用这个识别方法,然后返回识别结果。识别失败时用户手动填写,流程始终能走通。
6.3 相似宠物推荐的后端实现
相似推荐的实现逻辑比较简单,可以按品种和年龄区间筛选。这里不追求算法复杂度,重点在于你理解了"推荐也是一种查询,而不是玄学"。
public List<Pet> recommendSimilarPets(Long petId, int limit) { Pet currentPet = petMapper.selectById(petId); if (currentPet == null) { return Collections.emptyList(); } LambdaQueryWrapper<Pet> wrapper = new LambdaQueryWrapper<>(); wrapper.ne(Pet::getId, petId) .eq(Pet::getStatus, PetStatus.WAIT_ADOPT.getCode()) .eq(StringUtils.isNotBlank(currentPet.getBreed()), Pet::getBreed, currentPet.getBreed()) .last("LIMIT " + limit); return petMapper.selectList(wrapper); }这段代码的逻辑是:先排除当前宠物自己,只推荐"待领养"状态的宠物,优先匹配同品种。如果同品种不够,再放宽条件按同物种查询。这样实现简单,但勉强能支撑演示和文档说明,后续要优化也能有扩展方向。
7. 前后端联调:从跨域配置到如何区分前后端Bug
7.1 环境配置层面
前后端分离项目的第一个拦路虎就是跨域。开发环境里Vite启动在5173端口,Spring Boot启动在8080端口,两者端口不同,直接请求必然跨域。解决方案有两种:前端Vite代理,或者后端配置CORS。生产环境建议后端统一配置CORS。
后端CORS配置:
// 文件路径:src/main/java/com/example/petadopt/config/CorsConfig.java @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); } }注意:allowCredentials(true)时,如果允许了具体的域名,要小心安全边界;用allowedOriginPatterns("*")是演示环境方便。生产环境应该写成明确的域名列表。
7.2 联调时最容易被问的:怎么区分是前端Bug还是后端Bug
这是让很多新手头疼的问题。这里给你一套排查思路。
| 现象 | 排查位置 | 判断方法 |
|---|---|---|
| 页面报404 | 后端 | 看后端接口路径和前端的API请求路径是否一致,优先用Postman直接调接口,能通就是路径问题 |
| 页面报500 | 后端 | 看后端控制台异常日志,一般能直接定位到代码位置 |
| 页面数据为空 | 后端 | 先用Postman请求接口,确认返回JSON是否正常,再确认前端字段名是否和后端实体字段名一致 |
| 页面有报错但网络Status 200 | 前端 | 大概率是响应格式解析问题,比如后端返回{code: 1, data: [...]},前端却取了res.data.data |
你会发现核心思路是:用接口调试工具和前端Network面板把问题卡到某一端。后端能直接连数据库验证,前端能直接改代码调试,只有左右隔离才能快速定位。
7.3 联调最佳实践:统一响应格式
联调时最痛苦的事情是字段名不一致。比如后端叫petName,前端写成了name,页面就是显示不出来。这个问题的根治方案是:后端定义统一返回体Result,接口对齐后前端直接用res.data.petName,不做过多的额外转换。
// 文件路径:src/main/java/com/example/petadopt/common/Result.java @Data public class Result<T> { private Integer code; // 0-成功 非0-失败 private String message; private T data; public static <T> Result<T> success(T data) { Result<T> r = new Result<>(); r.setCode(0); r.setMessage("success"); r.setData(data); return r; } public static <T> Result<T> error(String message) { Result<T> r = new Result<>(); r.setCode(500); r.setMessage(message); return r; } }前端的请求封装也要统一处理这个结构,避免每个页面重复写错误判断:
// 文件路径:src/utils/request.js import axios from 'axios'; import { ElMessage } from 'element-plus'; const request = axios.create({ baseURL: '/api', timeout: 10000 }); request.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }); request.interceptors.response.use( response => { const res = response.data; if (res.code !== 0) { ElMessage.error(res.message || '请求失败'); return Promise.reject(new Error(res.message)); } return res; }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token'); window.location.href = '/login'; } ElMessage.error('网络异常,请稍后重试'); return Promise.reject(error); } );前后端统一了响应格式,联调效率会高很多。
8. 项目本地运行与全功能验证路径
项目写完之后,要能从头到尾跑通并演示。下面给出完整的运行步骤和验证路径。
8.1 后端启动
# 1. 准备MySQL,创建数据库 mysql -uroot -p CREATE DATABASE pet_adopt DEFAULT CHARSET utf8mb4; # 2. 执行src/main/resources/sql/init.sql初始化表结构 # 3. 修改application.yml中的数据库连接信息 spring: datasource: url: jdbc:mysql://localhost:3306/pet_adopt?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your-password # 4. 启动Spring Boot应用 mvn spring-boot:run后端启动成功的标志是控制台出现Started PetAdoptApplication in xxx seconds,同时访问http://localhost:8080/api/pet/list能看到JSON数据。
8.2 前端启动
# 1. 进入前端目录 cd pet-adopt-frontend # 2. 安装依赖 npm install # 3. 启动开发服务器 npm run dev前端启动成功后,访问http://localhost:5173,能看到宠物列表首页。
8.3 全功能验证清单
建议按下面的路径完整走一遍,这也是演示时的脚本:
| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 1. 注册 | 点击"注册",填写信息 | 注册成功,自动登录 |
| 2. 发布宠物 | 上传宠物照片,填写信息 | 点击AI识别能识别出品种(或允许手动填写),宠物进入待审核状态 |
| 3. 管理员审核 | 用管理员账号登录后台,审核宠物 | 宠物状态变成"待领养",首页可见 |
| 4. 提交领养申请 | 用另一个用户账号申请领养 | 宠物状态变成"申请中",管理员后台出现待审核申请 |
| 5. 管理员审核申请 | 通过或拒绝申请 | 宠物状态变为"已领养"或恢复"待领养",用户收到通知 |
| 6. 相似推荐 | 进入宠物详情页 | 页面底部展示相似宠物推荐 |
这六步跑通,整个系统的核心业务闭环就完成了。
9. 常见问题与排查方法
项目实战中最让人焦虑的就是"程序跑不起来也不报错"。这里整理几个高频问题:
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 前端请求接口报跨域错误 | 后端CORS未配置或配置错误 | 打开浏览器Network面板,确认跨域报错信息 | 检查CorsConfig,确认allowedOriginPatterns正确 |
| 登录成功但刷新页面后登录态丢失 | token未存或路由守卫未正确读取 | F12看localStorage是否有token | 登录成功后localStorage.setItem('token', res.data.token),刷新后重新读取 |
| 启动报数据库连接失败 | MySQL未启动或账号密码错误 | 用Navicat/命令行测试连接 | 检查application.yml的连接URL、用户名、密码 |
| MyBatis Plus查询结果字段全部为null | 实体类字段与数据库字段驼峰映射关闭 | 查看MyBatis Plus配置 | 开启map-underscore-to-camel-case: true |
| 发布宠物后列表页看不到 | 宠物状态是"待审核",列表页只查"待领养" | 用SQL直接查询pet表确认status值 | 确认列表查询接口的status过滤条件正确 |
| 管理员审核时提示"申请已处理过" | 同一申请被重复提交 | 查看adoption_apply表status是否已被更新 | 审核接口做好幂等判断,前端按钮同时禁用 |
10. 工程化建议与部署提醒
项目写完后如果要往简历写、往生产部署,这里有几点建议。
第一,不要使用明文密码。用户的注册密码必须经过BCrypt加密存储,数据库中不能出现明文密码。这是安全底线,面试官一定会看。
第二,图片上传必须做类型和大小校验。只允许jpg、png等图片格式,限制文件大小上限,防止恶意上传。上传路径最好使用对象存储,本地磁盘只适合开发环境。
第三,日志要分级。业务操作记录info日志,异常记录error日志并输出完整堆栈。生产环境排查问题全靠日志。
第四,部署要自动化。前后端分离项目可以用Docker Compose编排,一个命令启动MySQL、后端、前端。这一步是项目实战的加分项。
第五,删除操作要谨慎。真实系统里的宠物列表和领养申请,尽量用状态字段做逻辑删除,不要物理删除。逻辑删除的好处是随时能恢复数据,也方便审计。
11. 总结
回到文章开头的问题:完成一个AI宠物领养管理系统,真正要掌握的不是某一个框架的语法,而是从业务设计到前后端联调再到AI接入的完整闭环能力。你需要能把领养状态机的流转讲清楚,能把JWT认证和前端路由守卫配对使用,能在AI服务不可用时做降级处理,能在跨域报错时五分钟定位到问题。
这些能力,光看文章练不出来。建议你拿到这篇文章后,先照着一张表把模块设计画出来,再建库、写后端、写前端,最后接AI。跑通全流程后,再把代码重构一遍,注意异常处理和代码风格。
如果你的下一步是深入学习,重点可以放在几个方向:一是将推荐模块升级为基于用户行为数据的真实推荐算法;二是把AI识别替换成自己微调的模型;三是加入消息队列处理领养申请的通知异步发送;四是用Docker Compose完成整套系统的容器化部署。
建议把本文收藏备用。祝你的宠物领养系统项目一次跑通。