ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Spring Boot+Vue+AI:宠物领养管理系统前后端分离实战

Spring Boot+Vue+AI:宠物领养管理系统前后端分离实战 我去过很多做前后端项目的同学大家最容易卡住的不是某个语法点而是“一个完整项目到底应该长什么样”。尤其是当项目里还要叠加 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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 ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(success); result.setData(data); return result; } public static T ResultT error(Integer code, String message) { ResultT 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 ResultPagePet list( RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, RequestParam(required false) String category, RequestParam(required false) String keyword) { PagePet page petService.queryAdoptablePets(pageNum, pageSize, category, keyword); return Result.success(page); } GetMapping(/{id}) public ResultPet 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 MapString, 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); HttpEntityMapString, Object request new HttpEntity(requestBody, headers); ResponseEntityMap response restTemplate.postForEntity( https://api.example.com/v1/chat/completions, request, Map.class); // 提取模型返回内容 MapString, Object body response.getBody(); ListMapString, Object choices (ListMapString, Object) body.get(choices); MapString, Object firstChoice choices.get(0); MapString, Object message (MapString, 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 classpet-list-container div classfilter-bar el-radio-group v-modelcategoryFilter changeloadPets el-radio-button value全部/el-radio-button el-radio-button valuedog狗狗/el-radio-button el-radio-button valuecat猫咪/el-radio-button el-radio-button valueother其他/el-radio-button /el-radio-group el-input v-modelkeyword placeholder搜索品种或名称 clearable stylewidth: 240px; margin-left: 16px keyup.enterloadPets / /div el-row :gutter20 v-loadingloading el-col :span6 v-forpet in petList :keypet.id el-card classpet-card :body-style{ padding: 0 } clickgoDetail(pet.id) el-image :srcpet.coverImage fitcover stylewidth: 100%; height: 200px / div classpet-info div classpet-name span{{ pet.name }}/span el-tag sizesmall typewarning{{ categoryMap[pet.category] }}/el-tag /div div classpet-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 classpagination background layoutprev, pager, next, total :totaltotal :page-sizepageSize v-model:current-pagepageNum current-changeloadPets / /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() }) /script5.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 classapply-container el-card classapply-card template #header div classcard-header span申请领养 {{ pet?.name }}/span /div /template el-form refformRef :modelform :rulesrules label-width120px el-form-item label领养理由 propreason el-input v-modelform.reason typetextarea :rows4 placeholder请说明您想领养这只宠物的理由 / /el-form-item el-form-item label养宠经验 prophasExperience el-radio-group v-modelform.hasExperience el-radio :valuetrue有经验/el-radio el-radio :valuefalse没有经验/el-radio /el-radio-group /el-form-item el-form-item label居住类型 prophomeType el-select v-modelform.homeType placeholder请选择 el-option label自有住房 valuehouse / el-option label租房公寓 valueapt / /el-select /el-form-item el-form-item label家庭成员数 propfamilyMembers el-input-number v-modelform.familyMembers :min1 :max10 / /el-form-item el-form-item el-button typeprimary :loadingsubmitting clickhandleSubmit 提交申请 /el-button el-button clickgoBack返回/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?useUnicodetruecharacterEncodingutf8 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 响应拦截器、跨域配置确认返回数据层级是否正确提交申请提示 401Token 过期或未携带 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 审核两个模块接进去观察整个系统在引入外部模型服务后在超时、异常、成本方面会带来哪些新的工程问题。这些实践中遇到的问题才是最值得记录和分享的内容。
返回列表