ARTICLE DETAIL

资讯详情

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

基于Spring Boot和Vue 3的AI宠物领养管理系统全栈项目实战

基于Spring Boot和Vue 3的AI宠物领养管理系统全栈项目实战 在实际的开发学习中很多人对“前后端完整项目”的理解停留在“会写接口、会写页面”这一层但真正动手做一个像 AI 宠物领养管理系统这样带用户体系、业务流转、AI 能力的全栈项目时才会发现数据库设计、接口规范、状态管理、前后端联调和 AI 接口接入是一整套需要串联起来的问题。这篇文章会围绕一个可运行的全栈项目主线展开后端使用 Spring Boot 提供 RESTful 接口前端使用 Vue 3 搭建管理页面MySQL 负责持久化AI 能力通过大模型接口实现宠物推荐和领养问答。文章会从数据表设计开始讲到 JWT 登录认证、宠物管理、领养申请状态机、AI 助手封装再到前端页面、联调验证、常见报错排查和生产部署建议。读完以后你可以照着这个项目结构完成一个属于自己的全功能演示项目也能直接把里面的模块设计思路迁移到其他管理系统里。1. 先明确这个项目的技术主线和功能边界1.1 业务场景从“宠物列表”到“领养成功”的完整链路宠物领养管理系统不是单纯的宠物信息展示站核心业务是把“浏览宠物”到“提交领养申请”再到“管理员审核”这一整条链路数字化。普通用户登录后可以查看待领养宠物列表、进入详情页查看宠物信息和领养条件然后填写申请表单管理员在后台看到申请列表可以审核通过或拒绝用户在自己的“我的申请”里查看进度。AI 在这个系统里承担两个角色。第一个是智能推荐用户填写居住面积、养宠经验、是否有小孩、每天可陪伴时间等条件后系统调用大模型接口生成推荐宠物的理由列表。第二个是问答助手用户可以直接和助手对话问“幼猫到家第一周要注意什么”“柯基适合公寓养吗”这类问题助手结合系统里宠物列表和通用养宠知识给出回答。这样设计的好处是 AI 能力不是炫技而是真正插入到用户决策的环节中。这个完整链路决定了项目不能只写 CRUD至少需要认证授权、业务状态流转、第三方接口调用和前端交互状态管理这几个关键模块。1.2 技术选型Spring Boot Vue 3 MySQL Redis 的组合逻辑常见的管理系统技术栈有两类一类是 SSM 或 Spring Boot 后端加 JSP/Thymeleaf一类是前后端分离。本项目需要支撑多用户同时在线、页面交互复杂、后续要扩展微信小程序或移动端所以选择前后端分离。后端选择 Spring Boot 是因为生态成熟、配置简单、和 MyBatis-Plus 配合写 CRUD 非常快。前端选择 Vue 3 加 Element Plus 是因为组件库覆盖了表格、表单、弹窗、消息提示这些后台系统高频组件开发效率高适合作为教学项目。技术栈选型如下表所示层级技术选型主要作用后端Spring Boot 3.x提供 RESTful 接口、依赖注入、事务管理持久层MyBatis-Plus简化单表 CRUD内置分页插件数据库MySQL 8.x存储用户、宠物、申请等业务数据缓存Redis缓存验证码、Token 黑名单等临时数据认证JWT无状态登录认证前端Vue 3 Vite构建前端单页应用UI 组件Element Plus表格、表单、弹窗、消息提示HTTPAxios前端请求后端接口AI大模型 HTTP 接口宠物推荐和智能问答要注意如果原始项目没有明确指定版本号落地前先确认依赖之间的兼容性。Spring Boot 3.x 要求 JDK 17 以上MyBatis-Plus 需要选择支持 Spring Boot 3 的mybatis-plus-spring-boot3-starter版本这两个点在实际项目中经常因为版本不匹配导致启动失败。1.3 功能模块划分和演示范围为了让项目适合“全功能演示”功能模块按照角色划分为两部分。普通用户可以完成注册登录、宠物浏览、领养申请、查看我的申请、AI 推荐、AI 问答。管理员可以完成宠物信息管理、领养申请审核、用户管理。演示路径建议从前端注册开始走完“提交申请 - 管理员审核 - 查看状态更新”这一圈再把 AI 推荐和问答功能演示一遍。模块划分如下用户模块注册、登录、个人信息、JWT 拦截。宠物模块宠物列表分页、宠物详情、管理员新增和上下架宠物。领养模块提交申请、审核、用户查询申请状态。AI 模块智能匹配推荐、对话问答。管理端模块宠物管理、审核管理。后面所有代码实现都围绕这五个模块展开不会额外引入无关功能。2. 数据库设计先定表结构再写代码2.1 核心表设计数据库是整个项目最先确定的部分。表结构设计不清晰后面写接口和页面都会反复返工。本项目的核心表有四张用户表sys_user、宠物表pet、领养申请表adoption_application、AI 对话记录表ai_message。用户表保存账号、密码摘要加密后存储、角色、联系方式。宠物表保存宠物名称、种类、年龄、性别、疫苗状态、描述、图片地址、状态待领养/已被领养/下架。领养申请表保存用户 ID、宠物 ID、申请理由、状态待审核/已通过/已拒绝、处理人、处理时间。AI 对话记录表保存用户 ID、用户提问、AI 回答便于后续查看历史。设计时一定要注意状态字段不要用无意义的数字建议用可读字符串比如PENDING、APPROVED、REJECTED。字符串在日志排查时一眼能看懂数字还要去映射表查含义。2.2 建表 SQL以下是完整的建表 SQLMySQL 8.x 可以直接执行。字符集统一使用utf8mb4否则存入 emoji 或生僻字会报错宠物描述里很可能出现特殊字符。CREATE DATABASE IF NOT EXISTS pet_adoption DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; USE pet_adoption; CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键, username VARCHAR(50) NOT NULL UNIQUE COMMENT 用户名, password VARCHAR(100) NOT NULL COMMENT 加密后的密码, nickname VARCHAR(50) DEFAULT COMMENT 昵称, phone VARCHAR(20) DEFAULT COMMENT 手机号, role VARCHAR(20) NOT NULL DEFAULT USER COMMENT 角色ADMIN/USER, avatar VARCHAR(255) DEFAULT COMMENT 头像地址, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; CREATE TABLE pet ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键, name VARCHAR(50) NOT NULL COMMENT 宠物名称, species VARCHAR(20) NOT NULL COMMENT 种类猫/狗/兔等, breed VARCHAR(50) DEFAULT COMMENT 品种, age INT DEFAULT 0 COMMENT 年龄单位月, gender VARCHAR(10) DEFAULT UNKNOWN COMMENT 性别, vaccinated TINYINT(1) DEFAULT 0 COMMENT 是否已疫苗, description TEXT COMMENT 宠物描述, image_url VARCHAR(255) DEFAULT COMMENT 宠物图片地址, location VARCHAR(100) DEFAULT COMMENT 所在城市或救助站, status VARCHAR(20) NOT NULL DEFAULT AVAILABLE COMMENT AVAILABLE/ADOPTED/OFFLINE, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT宠物表; CREATE TABLE adoption_application ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键, user_id BIGINT NOT NULL COMMENT 申请用户ID, pet_id BIGINT NOT NULL COMMENT 宠物ID, reason VARCHAR(500) DEFAULT COMMENT 申请理由, status VARCHAR(20) NOT NULL DEFAULT PENDING COMMENT PENDING/APPROVED/REJECTED, handler_id BIGINT DEFAULT NULL COMMENT 处理人ID, handle_time DATETIME DEFAULT NULL COMMENT 处理时间, reject_reason VARCHAR(200) DEFAULT COMMENT 拒绝原因, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, KEY idx_user_id (user_id), KEY idx_pet_id (pet_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT领养申请表; CREATE TABLE ai_message ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键, user_id BIGINT NOT NULL COMMENT 用户ID, user_content TEXT COMMENT 用户提问, ai_content TEXT COMMENT AI回答, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENTAI对话记录表;2.3 状态字段的设计要点状态字段虽然简单却是整个项目的业务核心。领养申请的状态流转是单向推进的从PENDING到APPROVED或REJECTED不允许已经通过的申请重新变成待审核。宠物状态则会在领养审核通过时从AVAILABLE变成ADOPTED这个联动建议放在同一个事务方法里避免出现“申请通过了但宠物还是待领养”的数据不一致。这种设计在生产项目里非常常见状态枚举、事务、状态流转校验三者缺一不可。后面写领养接口时会用代码体现这个约束。另外不要在数据库里直接用enum类型存状态一是修改枚举值需要改表结构二是 MyBatis 映射时容易踩坑用VARCHAR配合 Java 枚举更灵活。3. 后端实现认证、宠物、领养、AI 四个模块依次打通3.1 工程结构和 Maven 依赖后端工程推荐使用标准的 Maven 单模块结构包名按职责划分。项目根目录下是pom.xml源码放在src/main/java配置文件放在src/main/resources。包结构如下com.example.petadoption ├── config # 配置类如跨域、拦截器、MyBatis-Plus 分页插件 ├── controller # 接口层接收前端参数 ├── service # 业务逻辑层 ├── mapper # MyBatis-Plus Mapper 接口 ├── entity # 数据库实体 ├── dto # 请求和响应对象 ├── common # 统一响应、异常、常量、枚举 ├── util # JWT、密码加密等工具 └── ai # AI 接口调用封装pom.xml里的核心依赖如下。这里使用 Spring Boot 3.xJDK 使用 17。需要特别说明的是 MyBatis-Plus 在 Spring Boot 3 下必须引入mybatis-plus-spring-boot3-starter如果误引入旧版 starter会导致 Mapper 扫描不到。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.2.5/version relativePath/ /parent properties java.version17/java.version mybatis-plus.version3.5.7/mybatis-plus.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version${mybatis-plus.version}/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version0.11.5/version scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-jackson/artifactId version0.11.5/version scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependenciesapplication.yml配置数据库连接、Redis、JWT 密钥和 AI 接口参数。生产环境里数据库密码和 AI 密钥不要写死在配置文件中要放到环境变量或配置中心。server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/pet_adoption?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver data: redis: host: localhost port: 6379 mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 jwt: secret: please-change-this-secret-to-a-long-random-string-in-production expire-hours: 24 ai: base-url: https://your-llm-endpoint.example.com/v1 api-key: ${AI_API_KEY} model: your-model-name3.2 统一响应和 JWT 登录认证接口返回结构必须统一前端才能用 Axios 拦截器统一处理。统一响应类ResultT包含状态码、消息和数据。这里定义成功返回码 200业务失败返回码 500未登录返回码 401。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; } }密码不能明文存储。使用 BCrypt 加密Spring Security 的BCryptPasswordEncoder可以直接单独引入使用不需要因此引入整套 Spring Security。登录成功后签发 JWT前端把 Token 存放在localStorage或pinia中每次请求时放进Authorization请求头。JWT 工具类负责生成和解析 Token。密钥长度必须足够jjwt在密钥小于 256 位时会报错这是一个常见坑建议直接放一个长字符串。Component public class JwtUtil { Value(${jwt.secret}) private String secret; Value(${jwt.expire-hours}) private int expireHours; public String generateToken(Long userId, String username, String role) { Date now new Date(); Date expireDate new Date(now.getTime() expireHours * 3600 * 1000L); return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(username, username) .claim(role, role) .setIssuedAt(now) .setExpiration(expireDate) .signWith(SignatureAlgorithm.HS256, secret.getBytes(StandardCharsets.UTF_8)) .compact(); } public Claims parseToken(String token) { return Jwts.parser() .setSigningKey(secret.getBytes(StandardCharsets.UTF_8)) .parseClaimsJws(token) .getBody(); } }登录接口如下。用户提交用户名和密码后端查出用户后用 BCrypt 校验成功则返回用户信息和 Token。RestController RequestMapping(/api/auth) public class AuthController { private final SysUserMapper userMapper; private final JwtUtil jwtUtil; private final PasswordEncoder passwordEncoder; public AuthController(SysUserMapper userMapper, JwtUtil jwtUtil, PasswordEncoder passwordEncoder) { this.userMapper userMapper; this.jwtUtil jwtUtil; this.passwordEncoder passwordEncoder; } PostMapping(/login) public ResultMapString, Object login(RequestBody LoginDto dto) { SysUser user userMapper.selectOne( new LambdaQueryWrapperSysUser().eq(SysUser::getUsername, dto.getUsername())); if (user null || !passwordEncoder.matches(dto.getPassword(), user.getPassword())) { throw new BusinessException(用户名或密码错误); } String token jwtUtil.generateToken(user.getId(), user.getUsername(), user.getRole()); MapString, Object data new HashMap(); data.put(token, token); data.put(user, user); return Result.success(data); } }这里要提一个关键设计登录校验失败时不要返回 500而是返回带明确提示的业务错误。前端在拦截器里根据code判断是弹提示还是跳登录页。3.3 宠物管理接口宠物接口是典型的 CRUD 加分页查询。列表接口接收页码、每页条数、种类参数返回分页结果。MyBatis-Plus 的分页插件要在配置类中注册否则Page参数不生效返回的total永远是 0。Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }宠物列表接口实现如下。这里用LambdaQueryWrapper构造查询条件避免魔法值和字符串拼接。RestController RequestMapping(/api/pet) public class PetController { private final PetService petService; public PetController(PetService petService) { this.petService petService; } GetMapping(/page) public ResultIPagePet page(RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer size, RequestParam(required false) String species) { return Result.success(petService.page(page, size, species)); } GetMapping(/{id}) public ResultPet detail(PathVariable Long id) { return Result.success(petService.getById(id)); } }列表查询默认只返回AVAILABLE状态的宠物这个逻辑最好放在 Service 层而不是让前端传状态参数。原因很简单业务规则应该在后端控制前端传参只能影响查询条件不能决定权限边界。3.4 领养申请流程和状态机领养申请是整个项目业务最重的部分。用户提交申请时后端要做三个校验宠物必须存在且状态为AVAILABLE当前用户不能重复申请同一只宠物申请理由长度不能为空。校验通过后插入一条PENDING记录。管理员审核时要做状态流转校验核心代码如下Transactional public AdoptionApplication approve(Long applicationId, Long handlerId) { AdoptionApplication application applicationMapper.selectById(applicationId); if (application null) { throw new BusinessException(申请不存在); } if (!PENDING.equals(application.getStatus())) { throw new BusinessException(只有待审核状态才能执行此操作); } Pet pet petMapper.selectById(application.getPetId()); if (pet null || !AVAILABLE.equals(pet.getStatus())) { throw new BusinessException(宠物不可领养); } application.setStatus(APPROVED); application.setHandlerId(handlerId); application.setHandleTime(new Date()); applicationMapper.updateById(application); pet.setStatus(ADOPTED); petMapper.updateById(pet); return application; }Transactional保证更新申请状态和更新宠物状态在同一个事务中任何一步失败都会回滚。拒绝申请时只更新申请状态和拒绝原因不动宠物状态。用户查询“我的申请”时关联查询宠物名称和图片。建议用 VO 对象返回给前端而不是直接把数据库实体暴露出去避免把不必要字段传给前端。3.5 AI 助手接口封装AI 模块是项目的亮点。实现思路是封装一个大模型 HTTP 客户端统一处理请求构造、响应解析、超时和异常。目前很多大模型服务都提供 OpenAI 兼容的/chat/completions接口所以可以基于这个通用协议封装然后通过配置切换base-url和api-key。接口定义如下用户传入prompt后端构造系统提示词和用户消息请求大模型接口返回回答文本。Service public class AiChatService { Value(${ai.base-url}) private String baseUrl; Value(${ai.api-key}) private String apiKey; Value(${ai.model}) private String model; private final RestTemplate restTemplate; public AiChatService(RestTemplate restTemplate) { this.restTemplate restTemplate; } public String chat(String userMessage) { MapString, Object body new HashMap(); body.put(model, model); body.put(temperature, 0.7); ListMapString, String messages new ArrayList(); MapString, String systemMsg new HashMap(); systemMsg.put(role, system); systemMsg.put(content, 你是宠物领养管理系统的智能助手回答要简洁、实用、安全。 涉及医疗建议时提醒用户咨询专业兽医。); MapString, String userMsg new HashMap(); userMsg.put(role, user); userMsg.put(content, userMessage); messages.add(systemMsg); messages.add(userMsg); body.put(messages, messages); HttpHeaders headers new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setBearerAuth(apiKey); HttpEntityMapString, Object request new HttpEntity(body, headers); try { ResponseEntityMap response restTemplate.exchange( baseUrl /chat/completions, HttpMethod.POST, request, Map.class); Map result response.getBody(); if (result null || result.get(choices) null) { throw new BusinessException(AI 服务响应异常); } ListMap choices (ListMap) result.get(choices); Map firstChoice choices.get(0); Map message (Map) firstChoice.get(message); return (String) message.get(content); } catch (BusinessException e) { throw e; } catch (Exception e) { throw new BusinessException(AI 服务调用失败请稍后重试); } } }RestTemplate的默认实现不设置超时会一直阻塞等待生产环境必须配置连接超时和读取超时。建议在配置类里统一创建带超时设置的RestTemplateBean。智能推荐功能可以复用同一个chat方法只是把系统提示词改成“根据用户条件推荐宠物并以 JSON 数组返回”然后让用户在 AI 助手里填写一份简短的条件描述即可。这种方式实现成本低演示效果好。如果想要更稳定可以单独写一个recommend方法用结构化提示词约束输出格式。要让 AI 输出稳定可解析的 JSON建议在提示词中明确要求“只输出 JSON不要输出其他文字”并且在后端做好解析兜底解析失败时提示用户重新描述条件。4. 前端实现Vue 3 页面和组件4.1 前端工程初始化前端使用 Vite 创建 Vue 3 项目并安装 Element Plus、Axios、Vue Router、Pinia。命令如下npm create vitelatest pet-adoption-web -- --template vue cd pet-adoption-web npm install npm install element-plus axios vue-router4 piniamain.js里注册 Element Plus 和全局组件。Element Plus 的样式需要单独引入否则组件渲染出来没有样式这是一个常见问题。import { createApp } from vue import { createPinia } from pinia import ElementPlus from element-plus import element-plus/dist/index.css import App from ./App.vue import router from ./router const app createApp(App) app.use(createPinia()) app.use(router) app.use(ElementPlus) app.mount(#app)4.2 Axios 封装和路由守卫Axios 封装是所有请求的入口。要在请求拦截器里统一携带 Token在响应拦截器里根据后端返回的code做统一处理401跳转登录页业务错误弹出消息提示网络错误给出友好提示。import axios from axios import { ElMessage } from element-plus import router from ../router const request axios.create({ baseURL: /api, timeout: 30000 }) 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 200) { return res } if (res.code 401) { localStorage.removeItem(token) router.push(/login) } ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) } else { ElMessage.error(网络异常请稍后重试) } return Promise.reject(error) } ) export default request路由守卫用于控制页面访问权限。未登录用户访问需要登录的页面时自动跳转到登录页管理员页面还要判断角色。router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next(/login) return } if (to.meta.role ADMIN) { const role localStorage.getItem(role) if (role ! ADMIN) { next(/) return } } next() })4.3 宠物列表、详情与领养申请宠物列表页是前端最核心的页面。使用 Element Plus 的el-card和el-row栅格布局展示宠物卡片每张卡片包含图片、名称、种类、年龄、状态和“查看详情”按钮。分页使用el-pagination。template div classpet-list el-row :gutter20 el-col :span8 v-forpet in petList :keypet.id el-card classpet-card :body-style{ padding: 0px } img :srcpet.imageUrl classimage / div classpet-info h3{{ pet.name }}/h3 p{{ pet.species }} / {{ pet.breed }} / {{ pet.age }}个月/p el-button typeprimary clickgoDetail(pet.id)查看详情/el-button /div /el-card /el-col /el-row el-pagination layouttotal, prev, pager, next :totaltotal :current-pagequery.page :page-sizequery.size current-changeloadData / /div /template script setup import { ref, onMounted } from vue import { useRouter } from vue-router import request from ../util/request const router useRouter() const petList ref([]) const total ref(0) const query ref({ page: 1, size: 9 }) async function loadData(page) { query.value.page page const res await request.get(/pet/page, { params: query.value }) petList.value res.data.records total.value res.data.total } function goDetail(id) { router.push(/pet/${id}) } onMounted(() loadData(1)) /script领养申请页是一个表单组件用户选择宠物后填写申请理由提交到/api/adoption/apply。这里要注意设置validate-on-rule-change和合理的表单校验规则避免用户提交空理由。4.4 AI 助手页面AI 助手页面做成聊天窗口样式左边是历史消息列表底部是输入框。用户发送消息后前端调用接口后端请求大模型返回结果后追加到消息列表。关键点是消息列表要使用scrollIntoView自动滚动到底部否则聊天体验很差。template div classai-chat div classchat-messages refmessageBox div v-for(msg, index) in messages :keyindex :class[message, msg.role] div classbubble{{ msg.content }}/div /div /div div classchat-input el-input v-modelinputText placeholder输入你的问题比如幼猫到家需要准备什么 keyup.entersendMessage / el-button typeprimary clicksendMessage发送/el-button /div /div /template script setup import { ref, nextTick } from vue import request from ../util/request const messages ref([ { role: assistant, content: 你好我是宠物领养助手。可以问我宠物饲养、领养条件等问题。 } ]) const inputText ref() const messageBox ref(null) async function sendMessage() { const text inputText.value.trim() if (!text) return messages.value.push({ role: user, content: text }) inputText.value scrollToBottom() const res await request.post(/ai/chat, { message: text }) messages.value.push({ role: assistant, content: res.data }) scrollToBottom() } function scrollToBottom() { nextTick(() { if (messageBox.value) { messageBox.value.scrollTop messageBox.value.scrollHeight } }) } /script发送按钮要加loading状态防止用户在大模型响应期间重复发送请求。也可以用disabled控制输入框和按钮等响应返回后再恢复。5. 前后端联调代理配置、接口自测和全流程演示5.1 Vite 代理配置解决跨域问题前后端分离开发模式下前端运行在 5173 端口后端运行在 8080 端口直接请求会产生跨域。开发环境推荐使用 Vite 的代理把/api开头的请求转发到后端这样前端代码里不用写完整地址生产环境也更容易切换到 Nginx 反向代理。vite.config.js配置如下import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })配置后前端请求/api/pet/page会被转发到http://localhost:8080/api/pet/page浏览器侧没有跨域问题。如果后端也希望通过 CORS 支持跨域可以在后端添加CorsFilter但开发环境下优先使用代理生产环境优先使用 Nginx 反向代理这两者都比在后端开放跨域更符合规范。5.2 核心接口自测和预期返回启动后端后再做接口自测。先注册普通用户并登录拿到 Token然后查询宠物列表提交领养申请最后用管理员账号登录审核。使用 curl 测试登录接口curl -X POST http://localhost:8080/api/auth/login \ -H Content-Type: application/json \ -d {username:demo,password:123456}预期返回示例{ code: 200, message: success, data: { token: eyJhbGciOiJIUzI1NiJ9.xxxxxxxxx, user: { id: 1, username: demo, role: USER, nickname: 演示用户 } } }带 Token 查询宠物列表curl http://localhost:8080/api/pet/page?page1size10 \ -H Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.xxxxxxxxx提交领养申请curl -X POST http://localhost:8080/api/adoption/apply \ -H Content-Type: application/json \ -H Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.xxxxxxxxx \ -d {petId:1,reason:家里有稳定住所想给它一个温暖的家}自测时重点检查三个点第一没有 Token 时请求受保护接口是否返回 401第二重复申请同一宠物是否被拦截第三管理员审核通过后宠物状态是否变成ADOPTED。5.3 全流程演示路径全功能演示建议按照下面的路径执行每步都检查页面和数据库这样能完整展示系统价值注册新用户登录进入首页。在宠物列表中挑选一只状态为AVAILABLE的宠物。进入详情页查看宠物基本信息和描述。打开 AI 助手输入“帮我想想住在 70 平公寓、每天上班 8 小时的上班族适合养什么宠物”观察 AI 回答。返回宠物详情点击“申请领养”填写申请理由提交。切换到管理员账号进入审核管理看到新增的PENDING申请。点击通过刷新用户端“我的申请”状态从PENDING变成APPROVED。回到宠物列表确认该宠物已经不下发到普通用户列表状态已变为ADOPTED。这个演示路径覆盖了用户、宠物、领养、AI 四个模块是项目验收时最有说服力的完整链路。6. 常见问题排查6.1 本地跑不起来的排查清单很多初学者拿到项目跑不起来问题集中在环境、依赖和配置三层。下面给出排查顺序按顺序检查能快速定位大部分问题。排查项检查方式常见原因JDK 版本运行java -versionSpring Boot 3 需要 JDK 17MySQL 连接用客户端连接数据库密码错误、服务未启动、时区参数缺失Redis 连接运行redis-cli pingRedis 未启动导致启动失败Maven 依赖查看 IDEA 右侧 Maven 面板依赖未刷新、版本冲突MyBatis-Plus 版本检查 pom 中 starter 名称引入了旧版 starterMapper 扫描不到前端依赖运行npm install后看是否报错Node 版本过低或依赖安装不完整6.2 联调阶段的重点问题跨域请求被拦截。现象是浏览器控制台出现CORS error接口在 Postman 里正常。原因通常是前端请求的地址和后端不在同一个域名端口下又没有配置代理或 CORS。解决方式是优先使用 Vite 代理或 Nginx 反向代理。如果使用代理要确认代理把/api路径完整转发后端接口也要保持/api前缀。请求 401 或 Token 失效。现象是页面刷新后请求全部 401。原因可能是 Token 没有在后端解析、前端没有在拦截器里带Authorization头、或者 Token 过期时间太短。排查步骤是先看浏览器 Network 面板里的请求头是否带了 Token再看后端日志里 JWT 解析是否抛出异常。JWT 的secret如果太短jjwt 会直接抛出异常这也是常见问题。AI 接口超时。现象是前端发送问题后长时间无响应或者接口返回 500。原因是大模型接口响应慢前端 Axios 和后端 RestTemplate 超时时间设置不合理。解决方式是在后端把 RestTemplate 连接超时设为 10 秒、读取超时设为 60 秒前端 Axios 超时设为 60 秒同时给发送按钮加 loading 状态。AI 接口属于第三方依赖生产环境还必须做降级兜底比如接口失败时返回固定提示不能让用户一直等待。分页数据不生效。现象是接口返回的total为 0但列表有数据。原因是 MyBatis-Plus 分页插件没有注册配置类。检查配置类中是否添加了PaginationInnerInterceptor。数据库出现中文乱码。现象是中文存储后变成问号。原因是数据库连接 URL 缺少characterEncodingutf8或者建表时字符集不是utf8mb4。统一在 JDBC URL 和建表语句中指定字符集。7. 生产部署建议和扩展方向7.1 从学习环境到生产部署的差距学习环境能跑通和生产环境可运行之间还有很大差距。至少要考虑以下几个方面配置外置数据库密码、AI 密钥不要写在application.yml里改用环境变量或配置中心。HTTPS登录接口和 Token 传输必须走 HTTPS避免 Token 被截获。日志生产环境把日志接入文件或日志平台至少把错误堆栈单独记录方便排查。数据库备份定期备份 MySQL领养申请和用户数据丢失不可接受。资源限制AI 接口调用要考虑限流防止用户恶意刷请求产生额外费用。反向代理前端静态资源用 Nginx 托管/api路径通过 Nginx 转发到后端服务。如果使用 Docker Compose 部署可以一次启动 MySQL、Redis、后端镜像、前端 Nginx 容器。下面是一个简化的部署文件示例实际服务镜像需要先通过 Dockerfile 构建version: 3.8 services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: pet_adoption ports: - 3306:3306 volumes: - mysql_data:/var/lib/mysql redis: image: redis:7 ports: - 6379:6379 backend: image: pet-adoption-backend:latest environment: SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/pet_adoption?useUnicodetruecharacterEncodingutf8 SPRING_DATASOURCE_USERNAME: root SPRING_DATASOURCE_PASSWORD: root AI_API_KEY: ${AI_API_KEY} ports: - 8080:8080 depends_on: - mysql - redis frontend: image: pet-adoption-frontend:latest ports: - 80:80 depends_on: - backend volumes: mysql_data:7.2 项目复盘时可复用的检查清单写完一遍全栈项目后对照下面的清单复盘能发现很多隐藏问题。这个清单也是以后做其他管理系统的通用自查表。[ ] 数据库表是否有统一的主键策略和创建时间字段。[ ] 状态字段是否使用可读字符串并且有明确的状态流转规则。[ ] 密码是否使用 BCrypt 加密没有明文入库。[ ] 后端接口是否统一返回Result结构异常是否被全局处理器捕获。[ ] 需要登录的接口是否都经过 JWT 拦截器校验。[ ] 领养申请和宠物状态变更是否在同一个事务中。[ ] AI 接口是否设置了超时、异常降级和用户限流。[ ] 前端 Axios 是否统一处理了 Token 携带和 401 跳转。[ ] 前端页面是否处理了加载中、空数据和请求失败三种状态。[ ] 配置文件里是否有明文密码或密钥。[ ] 生产环境的日志、备份、监控是否已经考虑。7.3 后续可以扩展的方向这个项目的扩展空间很大按优先级排列主要有四个方向。第一图片识别用户上传宠物照片后调用图像识别接口自动识别宠物种类和毛色减少人工录入成本。第二检索增强宠物数量多了以后在 AI 问答里把宠物列表和用户问题结合起来让 AI 回答基于真实数据而不是仅靠通用知识。第三消息通知申请审核通过后通过邮件或短信通知用户可以在审核接口里加一个消息队列异步发送。第四多端支持后端接口已经按 RESTful 规范设计小程序和移动端可以直接复用同一套接口只需要增加新的前端工程。对于想通过这个项目积累经验的人来说最重要的不是把每个功能都做完而是理解一条数据从用户输入经过接口校验、业务处理、数据库持久化再回到页面渲染的完整路径以及在这个路径上每一层应该承担的职责。把这一点想清楚这个项目才真正变成你自己的能力。
返回列表