我去过很多做前后端项目的同学,大家最容易卡住的不是某个语法点,而是“一个完整项目到底应该长什么样”。尤其是当项目里还要叠加 AI 能力时,很多人会陷入两种极端:要么把 AI 当成一个炫技的聊天框,要么完全不知道 AI 应该嵌在业务的哪个环节。
宠物领养管理系统,恰好是一个适合把“前后端分离 + AI 能力落地”讲透彻的业务场景。它不像电商那样商品逻辑复杂,也不像后台管理系统那样纯 CRUD 枯燥。它的业务闭环清晰:宠物信息展示、领养申请、后台审核、领养记录跟踪,每一环都可以被 AI 增强,但又不会因为 AI 的加入把项目复杂度推高到没法落地。
这篇文章会围绕一个完整的 AI 宠物领养管理系统展开,从前端页面到后端接口,从数据库设计到 AI 能力的接入方式,把整个项目的核心链路拆开讲清楚。内容偏工程实践,适合已经掌握 Spring Boot 和 Vue 基础语法、但还没有完整做过前后端分离项目的开发者。读完你可以照着思路把一个带 AI 能力的业务系统跑起来,也能理解前后端联调时那些“看起来没问题但就是报错”的坑到底出在哪。
1. 宠物领养系统为什么适合作为前后端实战项目
先给一个明确判断:宠物领养系统是练习前后端分离项目非常理想的中等复杂度业务模型,它比图书管理系统多了一层业务状态流转,又比电商系统少了很多价格、库存、支付等重逻辑。
一个典型的领养系统中,核心业务是“用户浏览宠物 → 提交领养申请 → 管理员审核 → 记录领养结果”。这个过程天然包含多个角色、多种状态、多张数据表,以及前后端之间大量的接口交互。你会在里面用到用户认证、分页查询、文件上传、表单校验、状态变更这些最常见也最实用的开发技能。
更重要的是,这个业务场景非常适合引入 AI。宠物领养中有一个很实际的痛点:领养人并不清楚这只宠物是否适合自己。比如一个住在单身公寓、每天加班到很晚的年轻人,是否适合领养一只需要大量运动量的边牧?这不是简单的关键词匹配能回答的问题,它需要对宠物特征和用户条件做综合判断。这种“非结构化决策”正是 AI 能力擅长的地方。
所以这个项目的技术亮点不是把 AI 包装成一个花哨的功能模块,而是让 AI 真正参与到业务决策链路里,比如智能宠物匹配和领养前的 AI 问答咨询。这才是在真实项目中引入 AI 的正确姿势。
2. 系统功能模块与技术架构设计
在设计一个前后端分离项目时,第一步不是写代码,而是把功能模块画清楚。宠物领养管理系统的功能可以拆成三个端:
用户端:注册登录、浏览宠物列表、查看宠物详情、提交领养申请、查看申请进度、使用 AI 助手咨询养宠问题。
管理端:宠物信息管理(上架/下架/编辑)、领养申请审核(通过/拒绝)、领养记录管理、用户管理、内容审核(对用户提交的领养理由做 AI 辅助判断)。
AI 能力模块:智能宠物匹配(基于用户条件推荐宠物)、领养问答助手(回答养宠常识和领养政策)、申请理由辅助审核(识别异常内容或高风险描述)。
技术架构采用目前主流的 Spring Boot + Vue 前后端分离方案。后端负责业务逻辑、数据持久化和 AI 接口的聚合,前端负责页面渲染和用户交互,通过 RESTful API 通信。
后端核心组件:
- Spring Boot 作为应用框架
- Spring Security + JWT 做用户认证与权限控制
- MyBatis-Plus 或 Spring Data JPA 做 ORM 映射
- MySQL 存储业务数据
- Redis 缓存热点数据(如宠物列表)
- 大模型 API 提供 AI 能力
- Docker 用于部署
前端核心组件:
- Vue 3 + Vite 作为基础框架
- Element Plus 作为 UI 组件库
- Axios 封装 HTTP 请求
- Pinia 管理前端状态
- Vue Router 实现页面路由
这种架构的好处是前后端职责边界清晰。前端只关注页面展示和用户交互,后端只关注业务逻辑和数据安全,AI 能力作为后端的一个服务被统一封装,前端不需要关心具体调用的是哪家模型接口。
3. 数据库设计与核心表结构
数据库设计是很多初学者容易忽略的环节。领养系统的表结构设计直接决定了后续业务逻辑的复杂程度。下面给出核心表的建表 SQL,你可以直接复制到 MySQL 中执行。
-- 用户表 CREATE TABLE `user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID', `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(100) NOT NULL COMMENT '加密后的密码', `nickname` varchar(50) DEFAULT NULL COMMENT '昵称', `phone` varchar(20) DEFAULT NULL COMMENT '手机号', `role` tinyint(4) NOT NULL DEFAULT '1' COMMENT '角色:1-普通用户 2-管理员', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; -- 宠物信息表 CREATE TABLE `pet` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '宠物ID', `name` varchar(50) NOT NULL COMMENT '宠物名称', `category` varchar(20) NOT NULL COMMENT '类别:dog/cat/other', `breed` varchar(50) DEFAULT NULL COMMENT '品种', `age` int(11) DEFAULT NULL COMMENT '年龄(月)', `gender` tinyint(4) DEFAULT NULL COMMENT '性别:1-公 2-母', `weight` decimal(5,2) DEFAULT NULL COMMENT '体重(kg)', `vaccinated` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已打疫苗', `sterilized` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已绝育', `health_status` varchar(500) DEFAULT NULL COMMENT '健康状况描述', `personality` varchar(500) DEFAULT NULL COMMENT '性格特征描述', `adoption_requirements` varchar(500) DEFAULT NULL COMMENT '领养要求', `cover_image` varchar(255) DEFAULT NULL COMMENT '封面图URL', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态:0-待审核 1-可领养 2-已被领养 3-下架', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='宠物信息表'; -- 领养申请表 CREATE TABLE `adoption_application` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '申请ID', `pet_id` bigint(20) NOT NULL COMMENT '宠物ID', `user_id` bigint(20) NOT NULL COMMENT '申请人ID', `reason` text COMMENT '领养理由', `has_experience` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否有养宠经验', `home_type` varchar(20) DEFAULT NULL COMMENT '居住类型:apt/house', `family_members` int(11) DEFAULT NULL COMMENT '家庭成员数', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态:0-待审核 1-已通过 2-已拒绝', `ai_review_result` text COMMENT 'AI辅助审核结果', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '申请时间', `review_time` datetime DEFAULT NULL COMMENT '审核时间', `reviewer_id` bigint(20) DEFAULT NULL COMMENT '审核人ID', `review_comment` varchar(500) DEFAULT NULL COMMENT '审核意见', PRIMARY KEY (`id`), KEY `idx_pet_id` (`pet_id`), KEY `idx_user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='领养申请表';三个核心表的设计有几个值得注意的点。pet表中的status字段用来控制宠物在整个生命周期中的状态,这个字段是后续业务流转的关键。adoption_application表添加了ai_review_result字段,用来保存 AI 审核的结果文本,这样做的好处是可以追溯“AI 当时为什么给出这个判断”,方便人工复核。
一个新手容易犯的错误是把枚举值直接写成字符串存到数据库,比如 status 字段存 “待审核” 而不是 0。这种做法在展示时确实方便,但后续做统计、条件查询时非常痛苦,而且容易因为中英文不一致产生脏数据。正确的做法是存数字编码,在代码层定义枚举常量,前端展示时再映射成对应的文本。
4. Spring Boot 后端核心接口实现
后端接口设计采用统一返回结构,这是前后端联调时减少沟通成本的关键。所有接口的返回格式统一为:
{ "code": 200, "message": "success", "data": {} }对应的通用返回类定义如下。
// 文件路径:src/main/java/com/example/adoption/common/Result.java package com.example.adoption.common; import lombok.Data; @Data public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("success"); result.setData(data); return result; } public static <T> Result<T> error(Integer code, String message) { Result<T> result = new Result<>(); result.setCode(code); result.setMessage(message); return result; } }4.1 宠物列表分页接口
宠物列表是用户端访问最频繁的接口,使用 MyBatis-Plus 的分页插件可以简化实现。接口需要支持按类别筛选、按状态筛选和分页参数,这里要注意只能查询状态为“可领养”的宠物,不能把待审核和下架状态的宠物暴露给普通用户。
// 文件路径:src/main/java/com/example/adoption/controller/PetController.java @RestController @RequestMapping("/api/pet") public class PetController { @Autowired private PetService petService; @GetMapping("/list") public Result<Page<Pet>> list( @RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, @RequestParam(required = false) String category, @RequestParam(required = false) String keyword) { Page<Pet> page = petService.queryAdoptablePets(pageNum, pageSize, category, keyword); return Result.success(page); } @GetMapping("/{id}") public Result<Pet> detail(@PathVariable Long id) { Pet pet = petService.getPetDetail(id); return Result.success(pet); } }这里真正容易踩坑的地方是分页参数的命名。有些团队用page和limit,有些用pageNum和pageSize,本身没有对错,但前后端必须约定一致。如果你的前端是 Element Plus 的el-pagination组件,它默认回调参数是page和limit,如果后端接口定义的是pageNum和pageSize,就需要在前端做转换,否则会出现“点击第二页没有反应”这类问题。
4.2 提交领养申请接口
提交领养申请是业务的核心操作之一,涉及数据校验、状态判断和 AI 辅助审核三个环节。用户提交申请时,后端需要校验三件事:宠物当前是否可领养、该用户是否已经申请过这只宠物、申请表必填字段是否完整。
// 文件路径:src/main/java/com/example/adoption/service/impl/AdoptionServiceImpl.java @Service public class AdoptionServiceImpl implements AdoptionService { @Autowired private AdoptionApplicationMapper applicationMapper; @Autowired private PetMapper petMapper; @Autowired private AiReviewService aiReviewService; @Override @Transactional(rollbackFor = Exception.class) public void submitApplication(AdoptionApplication application) { // 1. 校验宠物状态 Pet pet = petMapper.selectById(application.getPetId()); if (pet == null || pet.getStatus() != 1) { throw new BusinessException("该宠物当前不可领养"); } // 2. 校验是否已申请过 Integer count = applicationMapper.countByPetIdAndUserId( application.getPetId(), application.getUserId(), 0); if (count > 0) { throw new BusinessException("您已申请过该宠物,请勿重复提交"); } // 3. AI辅助审核申请理由 String aiResult = aiReviewService.reviewApplication(application); application.setAiReviewResult(aiResult); application.setStatus(0); // 4. 保存申请记录 applicationMapper.insert(application); } }这段代码里加了一个@Transactional注解,目的是保证 AI 审核结果和申请记录要么同时保存成功,要么同时回滚。AI 接口调用有可能超时或者返回异常,如果 AI 审核失败,不能让申请记录残留一半在数据库里。
4.3 AI 智能匹配服务
AI 能力在这个项目里的核心落点是智能匹配服务。用户提交自己的居住条件、养宠经验、家庭成员等资料后,系统调用大模型 API,结合宠物档案信息生成匹配建议。这里要注意,不要把用户的敏感信息直接拼进提示词后发给外部模型接口,而应该先做字段筛选和脱敏。
// 文件路径:src/main/java/com/example/adoption/service/impl/AiMatchServiceImpl.java @Service public class AiMatchServiceImpl implements AiMatchService { @Value("${ai.api-key}") private String apiKey; @Value("${ai.model}") private String model; @Autowired private RestTemplate restTemplate; @Override public String matchPet(UserCondition condition, Pet pet) { // 构造提示词 String prompt = String.format( "你是一个宠物领养顾问。请根据以下领养人条件评估是否适合领养这只宠物。" + "领养人情况:居住类型=%s,家庭成员=%d人,有养宠经验=%s," + "每天可陪伴宠物时长=%d小时,已养宠物=%s。" + "宠物信息:品种=%s,年龄=%d个月,性格=%s,活动需求=%s。" + "请给出结论(适合/需要谨慎/不适合)及理由,不超过150字。", condition.getHomeType(), condition.getFamilyMembers(), condition.getHasExperience() ? "有" : "无", condition.getDailyCompanionHours(), condition.getExistingPets(), pet.getBreed(), pet.getAge(), pet.getPersonality(), pet.getExerciseNeeds() ); // 调用模型API Map<String, Object> requestBody = new HashMap<>(); requestBody.put("model", model); requestBody.put("messages", List.of(Map.of("role", "user", "content", prompt))); requestBody.put("temperature", 0.3); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setBearerAuth(apiKey); HttpEntity<Map<String, Object>> request = new HttpEntity<>(requestBody, headers); ResponseEntity<Map> response = restTemplate.postForEntity( "https://api.example.com/v1/chat/completions", request, Map.class); // 提取模型返回内容 Map<String, Object> body = response.getBody(); List<Map<String, Object>> choices = (List<Map<String, Object>>) body.get("choices"); Map<String, Object> firstChoice = choices.get(0); Map<String, Object> message = (Map<String, Object>) firstChoice.get("message"); return (String) message.get("content"); } }这段代码中的 API 地址和模型名称是示例,实际接入时替换为你所使用的模型服务即可。这里要特别提醒的是temperature参数,匹配建议类任务希望输出稳定、可解释,所以温度值应该调低,一般建议在 0.2 到 0.4 之间。如果是 AI 聊天问答类的任务,可以适当调高温度让回答更灵活。
5. Vue 前端页面与交互实现
前端部分使用 Vue 3 + Element Plus。页面结构上,用户端主要包含首页、宠物列表、宠物详情、领养申请、个人中心,管理端包含宠物管理、申请审核、用户管理。这里重点讲宠物列表页和领养申请表单的实现。
5.1 宠物列表页
宠物列表页是用户进入系统后看到的第一个核心页面。使用el-card网格布局展示宠物卡片,点击卡片跳转到详情页。
<!-- 文件路径:src/views/pet/PetList.vue --> <template> <div class="pet-list-container"> <div class="filter-bar"> <el-radio-group v-model="categoryFilter" @change="loadPets"> <el-radio-button value="">全部</el-radio-button> <el-radio-button value="dog">狗狗</el-radio-button> <el-radio-button value="cat">猫咪</el-radio-button> <el-radio-button value="other">其他</el-radio-button> </el-radio-group> <el-input v-model="keyword" placeholder="搜索品种或名称" clearable style="width: 240px; margin-left: 16px" @keyup.enter="loadPets" /> </div> <el-row :gutter="20" v-loading="loading"> <el-col :span="6" v-for="pet in petList" :key="pet.id"> <el-card class="pet-card" :body-style="{ padding: '0' }" @click="goDetail(pet.id)" > <el-image :src="pet.coverImage" fit="cover" style="width: 100%; height: 200px" /> <div class="pet-info"> <div class="pet-name"> <span>{{ pet.name }}</span> <el-tag size="small" type="warning">{{ categoryMap[pet.category] }}</el-tag> </div> <div class="pet-meta"> <span>{{ pet.breed }}</span> <span>{{ pet.age }}个月</span> </div> </div> </el-card> </el-col> </el-row> <el-empty v-if="!loading && petList.length === 0" description="暂无待领养的宠物" /> <el-pagination class="pagination" background layout="prev, pager, next, total" :total="total" :page-size="pageSize" v-model:current-page="pageNum" @current-change="loadPets" /> </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 loading = ref(false) const pageNum = ref(1) const pageSize = ref(10) const total = ref(0) const categoryFilter = ref('') const keyword = ref('') const categoryMap = { dog: '狗狗', cat: '猫咪', other: '其他' } const loadPets = async () => { loading.value = true try { const res = await getPetList({ pageNum: pageNum.value, pageSize: pageSize.value, category: categoryFilter.value, keyword: keyword.value }) petList.value = res.data.records total.value = res.data.total } finally { loading.value = false } } const goDetail = (id) => { router.push(`/pet/${id}`) } onMounted(() => { loadPets() }) </script>5.2 Axios 请求封装与拦截器
前后端分离项目中最容易出问题的环节就是请求封装。部门同事之间经常出现“为什么我请求成功了但拿不到数据”“为什么提示没有登录”这类问题,大多数都出在请求头、Token 处理或错误码解析不一致上。推荐统一封装 Axios 实例,在请求拦截器里自动携带 Token,在响应拦截器里统一处理错误。
// 文件路径:src/utils/request.js import axios from 'axios' import { ElMessage } from 'element-plus' import { useUserStore } from '@/stores/user' import router from '@/router' const request = axios.create({ baseURL: '/api', timeout: 30000 }) // 请求拦截器:自动携带Token request.interceptors.request.use( (config) => { const userStore = useUserStore() if (userStore.token) { config.headers.Authorization = `Bearer ${userStore.token}` } return config }, (error) => { return Promise.reject(error) } ) // 响应拦截器:统一处理业务错误 request.interceptors.response.use( (response) => { const res = response.data if (res.code !== 200) { ElMessage.error(res.message || '请求失败') if (res.code === 401) { const userStore = useUserStore() userStore.logout() router.push('/login') } return Promise.reject(new Error(res.message)) } return res }, (error) => { ElMessage.error(error.message || '网络异常') return Promise.reject(error) } ) export default request这里要注意一个细节:后端统一返回结构是{ code, message, data }三层结构,所以响应拦截器里拿到的res是完整结构,真正要给页面的数据在res.data上。前端 API 函数里返回的res,页面调用时要记得再取一层.data。如果后面发现页面数据始终是 undefined,优先检查这里是不是少了一层。
5.3 领养申请表单
领养申请表单是用户端操作频率最高的表单,也是 AI 能力介入的入口。表单包含领养理由、养宠经验、居住类型等字段,提交时调用后端接口。
<!-- 文件路径:src/views/pet/PetApply.vue --> <template> <div class="apply-container"> <el-card class="apply-card"> <template #header> <div class="card-header"> <span>申请领养 {{ pet?.name }}</span> </div> </template> <el-form ref="formRef" :model="form" :rules="rules" label-width="120px" > <el-form-item label="领养理由" prop="reason"> <el-input v-model="form.reason" type="textarea" :rows="4" placeholder="请说明您想领养这只宠物的理由" /> </el-form-item> <el-form-item label="养宠经验" prop="hasExperience"> <el-radio-group v-model="form.hasExperience"> <el-radio :value="true">有经验</el-radio> <el-radio :value="false">没有经验</el-radio> </el-radio-group> </el-form-item> <el-form-item label="居住类型" prop="homeType"> <el-select v-model="form.homeType" placeholder="请选择"> <el-option label="自有住房" value="house" /> <el-option label="租房公寓" value="apt" /> </el-select> </el-form-item> <el-form-item label="家庭成员数" prop="familyMembers"> <el-input-number v-model="form.familyMembers" :min="1" :max="10" /> </el-form-item> <el-form-item> <el-button type="primary" :loading="submitting" @click="handleSubmit"> 提交申请 </el-button> <el-button @click="goBack">返回</el-button> </el-form-item> </el-form> </el-card> </div> </template> <script setup> import { ref, onMounted } from 'vue' import { useRoute, useRouter } from 'vue-router' import { ElMessage } from 'element-plus' import { getPetDetail } from '@/api/pet' import { submitApplication } from '@/api/application' const route = useRoute() const router = useRouter() const formRef = ref(null) const pet = ref(null) const submitting = ref(false) const form = ref({ petId: route.params.id, reason: '', hasExperience: false, homeType: 'house', familyMembers: 1 }) const rules = { reason: [ { required: true, message: '请填写领养理由', trigger: 'blur' }, { min: 10, message: '领养理由至少10个字', trigger: 'blur' } ] } const handleSubmit = async () => { await formRef.value.validate() submitting.value = true try { await submitApplication(form.value) ElMessage.success('申请提交成功,请等待审核') router.push('/my-applications') } finally { submitting.value = false } } onMounted(async () => { const res = await getPetDetail(route.params.id) pet.value = res.data }) const goBack = () => { router.back() } </script>提交表单时要注意validate方法的使用。Element Plus 的formRef.value.validate()返回的是一个 Promise,如果校验失败会 reject。这里使用await时,后续的提交逻辑不会执行,但需要在调用处捕获异常,否则控制台会有未处理的 Promise rejection 提示。更好的做法是包一层 try-catch,或者使用回调函数的写法。
6. 管理端审核流程与 AI 辅助审核
管理端是这个系统的另一块核心。管理员登录后可以看到待审核的领养申请列表,对每条申请进行通过或拒绝操作。这里的业务亮点在于,每条申请的详情页里会展示 AI 对领养理由的分析结果,帮助管理员快速判断。
AI 辅助审核的实现逻辑并不复杂,就是把领养理由、用户养宠经验和宠物特点拼成提示词,让模型输出结构化判断,再用正则或 JSON 解析提取关键词。
// 文件路径:src/main/java/com/example/adoption/service/impl/AiReviewServiceImpl.java @Service public class AiReviewServiceImpl implements AiReviewService { @Autowired private RestTemplate restTemplate; @Value("${ai.api-key}") private String apiKey; @Override public String reviewApplication(AdoptionApplication application) { String prompt = String.format( "你是一个宠物救助站审核员。请对以下领养申请进行风险评估," + "要求输出JSON格式结果:{\"riskLevel\":\"low/medium/high\",\"reason\":\"判断理由\"}。" + "领养理由:%s。养宠经验:%s。居住类型:%s。家庭成员数:%d。", application.getReason(), application.getHasExperience() ? "有" : "无", application.getHomeType(), application.getFamilyMembers() ); // 调用模型API并解析返回结果 String response = callModel(prompt); return response; } }用 AI 审核替代人工审核目前还不现实,但作为辅助工具价值很大。管理员看到 AI 标记为高风险且理由合理的申请,可以优先重点审核;看到低风险的申请,可以加快处理速度。这个设计思路在真实项目中很常见,AI 不是替代决策者,而是提高决策效率。
审核通过后,系统应该自动更新宠物状态为“已被领养”,并记录审核操作日志。这里涉及多个表的更新,必须放在同一个事务里。
@Transactional(rollbackFor = Exception.class) public void reviewApplication(Long applicationId, Integer status, String comment) { AdoptionApplication application = applicationMapper.selectById(applicationId); if (application == null) { throw new BusinessException("申请不存在"); } // 更新申请状态 application.setStatus(status); application.setReviewComment(comment); application.setReviewTime(new Date()); applicationMapper.updateById(application); // 如果审核通过,更新宠物状态为已被领养 if (status == 1) { Pet pet = new Pet(); pet.setId(application.getPetId()); pet.setStatus(2); petMapper.updateById(pet); } }7. 前后端联调与 Docker 部署
前后端分离项目做到最后,最花时间的往往不是功能开发,而是联调和部署。联调阶段最常见的问题可以归为三类:跨域问题、接口路径不一致、字段命名不一致。
跨域问题的解决方案有两种。一种是在后端配置 CORS 允许跨域,另一种是通过 Nginx 反向代理将前端请求转发到后端,让浏览器认为所有的请求都来自同一个域名。生产环境推荐使用第二种方案,配置示例如下。
# 文件路径:nginx/conf.d/adoption.conf server { listen 80; server_name localhost; # 前端静态资源 location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } # 后端接口反向代理 location /api/ { proxy_pass http://backend: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; } }后端接口路径统一以/api开头,Nginx 将/api开头的请求转发到后端服务的 8080 端口。这里有个细节要注意,proxy_pass http://backend:8080;这行,URL 末尾没有斜杠,和后端接口的映射关系是/api/pet/list→http://backend:8080/api/pet/list,后端 Controller 的 RequestMapping 也写成/api/pet,这样可以保持路径一致,避免额外做路径重写。
如果使用 Docker Compose 来编排前端、后端和数据库,编排文件可以参考下面这个骨架。
# 文件路径:docker-compose.yml version: '3.8' services: mysql: image: mysql:8.0 container_name: adoption-mysql environment: MYSQL_ROOT_PASSWORD: root123456 MYSQL_DATABASE: adoption ports: - "3306:3306" volumes: - mysql-data:/var/lib/mysql - ./sql/init.sql:/docker-entrypoint-initdb.d/init.sql backend: build: ./backend container_name: adoption-backend depends_on: - mysql environment: SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/adoption?useUnicode=true&characterEncoding=utf8 SPRING_DATASOURCE_USERNAME: root SPRING_DATASOURCE_PASSWORD: root123456 ports: - "8080:8080" frontend: build: ./frontend container_name: adoption-frontend ports: - "80:80" depends_on: - backend volumes: mysql-data:一个关键点是容器间的网络通信。在 Docker Compose 网络中,服务名就是主机名,所以后端连接数据库的地址写的是mysql:3306,而不是localhost:3306。很多人在本地能跑通,一到 Docker 部署就连不上数据库,多半是把配置文件里的数据库地址还写成 localhost,导致容器内访问不到宿主机。
8. 常见问题与排查思路
前后端分离项目的坑,很多不是代码写不出来,而是出了问题不知道怎么高效率地定位。下面整理几个高频问题。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 前端请求接口返回 404 | 接口路径写错或 Nginx 转发路径不匹配 | 打开浏览器开发者工具查看 Network 中的请求 URL,和后端 Controller 的 RequestMapping 逐一比对 | 统一接口路径规范,保持前端调用地址、后端接口地址、Nginx 转发规则三者一致 |
| 前端能打开但页面没有数据 | 后端返回结构解析错误,或存在跨域问题 | 查看 Network 面板中接口响应内容,确认是 cross origin 错误还是数据格式问题 | 检查 Axios 响应拦截器、跨域配置,确认返回数据层级是否正确 |
| 提交申请提示 401 | Token 过期或未携带 Token | 查看请求头中是否有 Authorization 字段,检查 Token 过期时间 | 刷新 Token 或在请求拦截器中重新登录 |
| 本地启动后端时报数据库连接失败 | 数据库未启动、账号密码错误、数据库名不存在 | 检查 MySQL 服务状态,使用客户端工具手动连接确认信息正确 | 核对 application.yml 中的连接配置,确认数据库已初始化 |
| AI 接口调用超时 | 网络原因或模型响应时间过长 | 查看后端日志中 AI 调用耗时,测试直连 AI 服务的响应时间 | 增加 AI 接口调用超时时间,或改用异步任务处理 AI 审核流程 |
| 图片上传成功但页面显示不出来 | 图片保存路径与访问路径不一致 | 检查上传文件的保存路径,确认返回给前端的 URL 是否能够直接访问 | 配置静态资源映射,或用对象存储保存图片并返回完整的访问链接 |
| 编译前端时依赖冲突 | npm 依赖版本不兼容 | 查看 npm install 时的错误日志,使用 npm ls 检查依赖树 | 删除 node_modules 和 package-lock.json 后重新安装,或锁定精确版本号 |
开发阶段如果遇到前后端字段对不上的问题,建议用接口文档工具进行约束。不需要非常重型的方案,直接使用 Apifox、Postman 或者后端的 Swagger 都行,核心是让前端能清楚看到后端返回的字段名和类型,避免因为userId和user_id这样的命名差异反复调试。
9. 最佳实践与工程建议
项目能跑起来只是第一步,要让它具备真实项目的工程质量,下面几点建议值得注意。
第一,接口返回结构必须统一。不要在某个接口返回{code: 200, data: [...]},另一个接口返回{success: true, result: {...}},前后端沟通成本会急剧上升。从项目一开始就定义好统一的 Result 包装类,所有接口强制使用。
第二,敏感信息必须过滤。用户的手机号、住址等信息,不应该在宠物列表接口中返回。建议在 DO 和 VO 之间做区分,DO 对应数据库表结构,VO 对应前端展示结构,不要直接把数据库实体类返回给前端。
第三,AI 相关的配置项不要硬编码。API Key、模型名称、接口地址、超时时间都应该放在配置文件中,通过@Value或@ConfigurationProperties注入。生产环境更推荐使用配置中心统一管理,方便在不重新部署的情况下修改配置。
第四,AI 调用要做降级处理。如果模型接口挂了,系统不能跟着挂。最佳实践是捕获 AI 调用异常后返回兜底结果,比如匹配服务可以返回“当前 AI 服务暂不可用,请查看宠物详情页的领养要求”,同时通过日志和告警机制提醒开发人员介入。
第五,注意数据库索引设计。领养申请列表经常按状态和时间查询,应该在status和create_time上建立联合索引。宠物列表按类别和状态查询,也需要评估是否建立合适的索引。小项目可能看不出差别,数据量上来后,一个合适的索引能让查询效率提升几十倍。
第六,前端环境变量管理。开发环境的后端地址和生产环境的后端地址往往不同,建议在.env.development和.env.production文件中分别配置VITE_API_BASE_URL,而不是在前端代码里写死。
# 文件路径:.env.development VITE_API_BASE_URL=/api # 文件路径:.env.production VITE_API_BASE_URL=/apiAxios 的 baseURL 改成读取import.meta.env.VITE_API_BASE_URL,这样在不同环境下构建时不需要修改业务代码。开发时如果后端端口是 8080 而前端端口是 5173,可以在 Vite 配置中设置代理,将/api代理到后端地址,既避免了跨域,又保持了和生产一致的接口路径。
10. 总结与后续学习方向
这个宠物领养管理系统虽然不是特别大的项目,但把前后端分离开发中最重要的环节基本都覆盖到了。从数据库表设计,到 Spring Boot 后端接口实现,再到 Vue 前端页面交互,最后到 Nginx 反向代理和 Docker 部署,完整走完一遍之后,你对项目整体架构的理解会和只做练习题完全不一样。
项目中的 AI 能力部分,建议不要停留在“调一个 API 返回文本”的层面。可以继续思考如何把 AI 能力产品化,比如用流式输出提升问答助手的交互体验,用向量数据库实现宠物特征的语义检索,或者把匹配建议从一段文本升级成打分和标签。这些都是目前企业实际会用到的大模型应用方向。
下一步的实践建议是先把项目基础功能完整跑通,包括用户注册登录、宠物列表和详情、领养申请提交、管理员审核这几个主链路。然后再把 AI 匹配和 AI 审核两个模块接进去,观察整个系统在引入外部模型服务后,在超时、异常、成本方面会带来哪些新的工程问题。这些实践中遇到的问题,才是最值得记录和分享的内容。