尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
SpringBoot+Vue宠物健康顾问系统实战:从数据库设计到部署
1. 这个宠物健康顾问系统到底做了什么宠物健康这件事做过的都知道有多琐碎。疫苗下次该哪天打、体重一个月涨了多少、最近食欲和精神状态有没有异常、上次驱虫是不是又过期了全靠人脑去记十有八九要掉链子。所以当有朋友让我帮他把“宠物健康顾问系统”做成一个完整的SpringBootVue前后端分离项目时我的第一反应是这需求太真实了它不是那种为了凑功能硬拼出来的CRUD而是能真正解决宠物主人和健康顾问日常痛点的业务系统。这个项目的核心链路很简单宠物主人注册登录后给自家宠物建立档案日常记录体重、体温、食欲、精神状态等健康指标同时维护疫苗接种和驱虫时间节点系统把这些数据沉淀下来形成可视化的健康趋势曲线如果需要专业意见用户可以发起在线咨询健康顾问在后台查看档案、回答问题管理员则负责用户和内容的整体管理。整条链路覆盖了“记录—分析—咨询—建议”的闭环非常适合拿来练手、改造毕业设计或者直接部署出去做一个小规模SaaS。技术选型方面后端用SpringBoot做接口服务、MyBatis管理数据访问、MySQL承载业务数据前端用Vue 3 Vite Element Plus ECharts做页面和图表。这套组合是当前国内前后端分离项目里最主流的搭配之一相关的工程规范、部署方案和排坑经验在网上沉淀也最多你遇到问题时基本都能搜到解决方案。下面我会把从零搭建到部署上线的完整过程拆开讲包含数据库设计、核心代码逐段解释、联调细节以及我实际踩过的各种坑。不管你现在是刚学完SpringBoot和Vue想找项目练手还是需要交一份课程设计/毕业设计这篇文章都能直接当参考手册用。2. 技术栈选型与工程搭建2.1 为什么是SpringBoot 2.7而不是3.x先说后端。这个项目我用的Spring Boot 2.7.6 JDK 8/11没有盲目追新。原因很实际Spring Boot 3.x要求JDK 17起步并且把包名从javax.*换成了jakarta.*很多网上教程、老版本的MyBatis starter、第三方工具都还在用javax你如果直接上3.x光改import就能折腾半天。网上搜“springboot版本太高”搜出来一堆问题本质都是生态没完全跟上版本节奏。而2.7.x是目前兼容性最稳的长期维护分支既能配JDK 8也支持JDK 11/17运行绝大多数教学项目、开源脚手架都基于它。如果你确实想用3.x那建议子依赖也全部对齐Spring Boot 3的版本比如mybatis-spring-boot-starter至少要选2.3.x以上JDK用17遇到javax.servlet找不到的情况就换成jakarta.servlet。但在这个项目里我的建议是选2.7.6把时间花在业务实现上。依赖管理用Maven这是SpringBoot项目的标准构建方式。核心依赖如下dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/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 /dependencies注意这里用的是JJWT 0.11.5API和老的0.9.x完全不同。如果你搜教程看到Jwts.parser()这种写法那是老版本新版本要写成Jwts.parserBuilder()下面后端部分我会给完整示例。2.2 为什么用原生MyBatis而不是MyBatis-Plus很多人在这个环节会纠结既然MyBatis-Plus写起来那么省事为什么还要用原生MyBatis这个项目我刻意选了原生MyBatis理由是双重的。第一项目标题写的就是“MyBatis”用Plus会让整个数据访问层的实现思路完全变样面试或者答辩时问你“MyBatis的Mapper映射原理”你如果只会Plus的BaseMapper很容易露馅第二原生MyBatis的XML动态SQL能让你真正掌握where、if、foreach这些标签的用法这是很多一线岗位实际会考的东西。Plus当然是好工具它把单表CRUD封装到了极致适合快速交付。但作为练手项目核心里就应该是“手写SQL 手工配置映射”把MyBatis的工作机制吃透之后再上Plus就只是半天功夫的事。所以这个项目的Mapper层全是XML文件实体属性和数据库字段的映射也手工配置我会在下文详细展示。2.3 前端工程Vue 3 Vite的搭建细节前端我用的Vue 3.2 Vite 4 Vue Router 4 Pinia Element Plus Axios ECharts。Vite的启动速度比Webpack的Vue CLI快一个量级开发时热更新几乎是秒级对调试体验提升非常明显。如果你以前用的是Vue 2 Vue CLI那套切过来只需要注意三点生命周期写法变了setup语法、路由API变了createRouter而不是new VueRouter、状态管理从Vuex换成了Pinia当然Vuex也能用。创建工程的命令很简单npm create vitelatest pet-health-web -- --template vue cd pet-health-web npm install npm run dev装完基础依赖后再补几个核心包npm install element-plus element-plus/icons-vue npm install vue-router4 pinia npm install axios echarts sass到这里前后端的工程骨架都出来了。我的习惯是先不急着写业务代码而是把目录结构定清楚。后端按controller/service/mapper/entity/common/config分层前端按api/views/router/store/components/utils组织。目录清晰的项目后面写任何模块都不容易乱。3. 数据库设计五张核心表怎么建3.1 表结构设计与字段取舍数据库是这类管理系统的地基设计得好不好直接决定后面功能好不好写。宠物健康顾问系统的核心表一共五张用户表、宠物档案表、健康记录表、疫苗记录表、在线咨询表。先看用户表。这里有个细节这个系统有三种角色——宠物主人、健康顾问、管理员我没有拆三张表而是用一个role字段区分。理由是这个项目的角色功能差异没有大到需要独立表维护的程度一个数字字段就够用了。如果以后要加细粒度权限再引入permission相关表也不迟。密码字段要注意必须存加密后的密文不能存明文。我这里用的BCrypt加密Spring Security单独引入太重就直接用spring-security-crypto这个轻量包或者自定义一个加盐哈希工具类。CREATE TABLE user ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 用户ID, username varchar(50) NOT NULL COMMENT 用户名, password varchar(100) NOT NULL COMMENT 密码(BCrypt加密), phone varchar(20) DEFAULT NULL COMMENT 手机号, avatar varchar(255) DEFAULT NULL COMMENT 头像地址, role tinyint(4) NOT NULL DEFAULT 1 COMMENT 角色 1-主 2-顾问 3-管理员, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;宠物档案表挂一个user_id外键表示这只宠物属于哪个用户。注意这里我刻意没有建真正的物理外键约束只用普通索引idx_user_id。原因是在实际业务中物理外键会影响删除效率和扩展灵活性互联网项目里普遍采用逻辑外键靠代码保证一致性。这个习惯建议你现在就养成。CREATE TABLE pet ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL COMMENT 所属用户ID, name varchar(50) NOT NULL COMMENT 宠物名称, breed varchar(50) DEFAULT NULL COMMENT 品种, species tinyint(4) DEFAULT 1 COMMENT 物种 1-犬 2-猫 3-其他, gender tinyint(4) DEFAULT 1 COMMENT 性别 1-公 2-母, birthday date DEFAULT NULL COMMENT 出生日期, avatar varchar(255) DEFAULT NULL COMMENT 宠物头像, note varchar(500) DEFAULT NULL COMMENT 备注, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT宠物档案表;3.2 健康记录和疫苗记录历史数据才是宝健康记录表是整个系统的核心。在设计时我特意让它和宠物表分开存而不是在宠物表里放一个“当前体重”字段。原因很简单健康顾问真正需要的是体重、体温的变化趋势如果只记录一个当前值那图表就只能画一个点完全没有分析价值。把每次测量的数据作为一条独立记录保存后面才能用DATE_FORMAT按月份分组画出体重曲线。CREATE TABLE health_record ( id bigint(20) NOT NULL AUTO_INCREMENT, pet_id bigint(20) NOT NULL COMMENT 宠物ID, record_date date NOT NULL COMMENT 记录日期, weight decimal(5,2) DEFAULT NULL COMMENT 体重kg, temperature decimal(4,2) DEFAULT NULL COMMENT 体温℃, heart_rate int(11) DEFAULT NULL COMMENT 心率 次/分, appetite tinyint(4) DEFAULT NULL COMMENT 食欲 1-差 2-一般 3-良好 4-旺盛, spirit tinyint(4) DEFAULT NULL COMMENT 精神 1-萎靡 2-一般 3-正常 4-活泼, symptom varchar(500) DEFAULT NULL COMMENT 症状描述, note varchar(500) DEFAULT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_pet_date (pet_id, record_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT健康记录表;这里有两个容易忽略的点。一是weight和temperature用decimal而不是float/double浮点数在MySQL里做比较和统计时有精度问题金额、体重、体温这类数据一律用decimal。二是联合索引idx_pet_date它让“按宠物查某段时间的记录”走覆盖索引后面查询趋势数据时会快得多。疫苗记录表多了一个next_date字段这是为“下次接种提醒”预留的。有了它系统可以写一个定时任务提前一周扫描快到期或者已过期的疫苗记录给用户推送提醒。这个功能虽然不算复杂但用到定时任务和日期比较是很加分的亮点。CREATE TABLE vaccine_record ( id bigint(20) NOT NULL AUTO_INCREMENT, pet_id bigint(20) NOT NULL, vaccine_name varchar(100) NOT NULL COMMENT 疫苗名称, vaccine_date date NOT NULL COMMENT 接种日期, next_date date DEFAULT NULL COMMENT 下次接种日期, hospital varchar(100) DEFAULT NULL COMMENT 接种医院, note varchar(500) DEFAULT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_pet_id (pet_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT疫苗接种记录表;在线咨询表我放在最后说。它除了基础的问题和答案字段外还有一个status状态字段0待回复、1已回复、2已关闭。这是典型的工作流状态设计顾问端靠它筛选待办列表用户端靠它判断咨询是否有人跟进。如果后面要扩展成多轮的问答可以把这张表拆成consult主表和consult_message明细表但就目前这个业务规模一张表完全够用。CREATE TABLE consultation ( id bigint(20) NOT NULL AUTO_INCREMENT, pet_id bigint(20) NOT NULL, user_id bigint(20) NOT NULL COMMENT 发起咨询的用户, consultant_id bigint(20) DEFAULT NULL COMMENT 顾问ID, title varchar(100) NOT NULL COMMENT 咨询标题, content text COMMENT 咨询内容, answer text COMMENT 顾问回复, status tinyint(4) DEFAULT 0 COMMENT 0-待回复 1-已回复 2-已关闭, create_time datetime DEFAULT CURRENT_TIMESTAMP, reply_time datetime DEFAULT NULL, PRIMARY KEY (id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT在线咨询表;建表语句全部在MySQL 8.0下测试通过字符集统一utf8mb4这和utf8的最大区别是能存表情符号。别小看这一点用户昵称、宠物名里出现emoji是很常见的事用utf8会直接报错。4. 后端核心逻辑与实现细节4.1 基础配置与MyBatis驼峰映射后端的application.yml是整个服务能不能跑起来的关键。我见过太多新手项目启动失败都是这里出了问题。先给一份能直接用的配置server: port: 8080 servlet: context-path: /api spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/pet_health?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.pethealth.entity configuration: map-underscore-to-camel-case: true jwt: secret: pet-health-secret-key-please-change-in-production-2024 expire-minutes: 120逐项说一下为什么这么写。数据源URL里的serverTimezoneAsia/Shanghai是必须的MySQL 8.0的驱动对时区非常敏感不配的话经常报The server time zone value is unrecognized的错。useSSLfalse是为了避免本地开发时SSL握手导致的连接警告allowPublicKeyRetrievaltrue是针对MySQL 8.0 caching_sha2_password认证插件的不加它连远一点的数据库会报Public Key Retrieval is not allowed。context-path: /api等于给所有接口加了个统一前缀这样前端Vite代理和后面Nginx反向代理都只需要转发/api这个路径省掉一层rewrite配置。map-underscore-to-camel-case这个配置很重要数据库字段是user_idJava实体属性是userId打开这个开关后MyBatis自动完成映射否则你查出来的userId永远是null而且这种Bug特别难发现因为正常SQL不报错、接口也不报错就是数据不对。4.2 JWT登录认证拦截器注册与Token解析系统里除了登录注册接口外其他操作都应该要求身份认证。这里用的是JWT方案流程是登录成功后端签发一个Token前端存在localStorage里每次请求在Authorization请求头带上后端拦截器解析校验。JJWT 0.11.5的工具类写法如下注意别和老版本的parser()混淆Component public class JwtUtil { Value(${jwt.secret}) private String secret; Value(${jwt.expire-minutes}) private int expireMinutes; public String createToken(Long userId, String username, Integer role) { return Jwts.builder() .setSubject(username) .claim(userId, userId) .claim(role, role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() expireMinutes * 60 * 1000L)) .signWith(Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8))) .compact(); } public Claims parseToken(String token) { return Jwts.parserBuilder() .setSigningKey(Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8))) .build() .parseClaimsJws(token) .getBody(); } }拦截器里要做的事有三件放行登录注册接口、校验Token合法性、把当前用户信息塞进请求上下文。Tomcat的拦截器对OPTIONS预检请求要特殊处理否则前端跨域时会直接被拦住Component public class AuthInterceptor implements HandlerInterceptor { Autowired private JwtUtil jwtUtil; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行预检请求 if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (token null || token.isEmpty()) { return reject(response, 未登录或Token为空); } try { Claims claims jwtUtil.parseToken(token); request.setAttribute(userId, claims.get(userId, Long.class)); request.setAttribute(role, claims.get(role, Integer.class)); return true; } catch (Exception e) { return reject(response, Token无效或已过期); } } }注册拦截器时记得把登录、注册接口排除掉同时配一个跨域配置。跨域这块是前后端分离项目必踩的坑我在第7节会详细讲。4.3 Pet模块MyBatis动态SQL实战宠物档案模块就是一个标准的单表CRUD但我在Mapper里特意用了动态SQL方便后面做筛选。先看接口定义Mapper public interface PetMapper { ListPet selectByCondition(Param(userId) Long userId, Param(species) Integer species, Param(name) String name); Pet selectById(Param(id) Long id); int insert(Pet pet); int update(Pet pet); int deleteById(Param(id) Long id); }XML里的where和if是MyBatis最常用的两个标签。where会自动处理掉第一个条件前面多余的AND不然你拼SQL时总得想办法处理“要不要加where”这个逻辑select idselectByCondition resultTypecom.pethealth.entity.Pet SELECT * FROM pet where if testuserId ! null AND user_id #{userId} /if if testspecies ! null AND species #{species} /if if testname ! null and name ! AND name LIKE CONCAT(%, #{name}, %) /if /where ORDER BY create_time DESC /select这里有一个关于SQL注入的坑要提醒你千万别在Java代码里直接拼SELECT * FROM pet WHERE name LIKE % name %一定要用CONCAT(%, #{name}, %)。#{}是预编译占位符MyBatis会把它转成?由数据库驱动做参数绑定注入代码没机会执行。${}才是字符串拼接虽然有些场景如表名动态传入不得不用但只要你传的是用户输入永远优先用#{}。4.4 健康趋势查询分组聚合的Mapper写法健康顾问最关心的功能是趋势分析。前端要画一条“最近六个月体重变化曲线”后端接口就需要返回按月分组的数据。SQL层面用DATE_FORMAT(record_date, %Y-%m)把日期归并到月份再配合AVG/MAX/MIN聚合函数select idselectWeightTrend resultTypejava.util.Map SELECT DATE_FORMAT(record_date, %Y-%m) AS month, ROUND(AVG(weight), 2) AS avgWeight, MAX(weight) AS maxWeight, MIN(weight) AS minWeight FROM health_record WHERE pet_id #{petId} AND record_date BETWEEN #{startDate} AND #{endDate} GROUP BY DATE_FORMAT(record_date, %Y-%m) ORDER BY month ASC /select返回值直接用ListMapString, Object不要为了这种查询结果去专门建一个实体类。这里必须提一下Java端对日期的处理前端传startDate和endDate时我建议统一用yyyy-MM-dd字符串格式后端用LocalDate接收别用Date。原因是java.util.Date在不同时区下容易出现前后端各差8个小时的问题而LocalDate就是单纯的日期没有时区概念处理起来干净得多。Controller层再包一层统一返回结构。我习惯写一个ResultT类里面固定放code、msg、data三个字段接口全部返回它而不是裸对象。这么做的好处是前端Axios拦截器可以统一判断code不用每个接口单独处理错误。配合RestControllerAdvice全局异常处理器业务异常和参数校验失败都能统一转换成标准格式返回避免一报错就弹出Tomcat默认的错误页面。5. 前端页面与接口联调5.1 请求封装与开发环境代理前端这层我建议第一件事不是写页面而是把Axios实例和Vite代理配好。开发阶段最大的痛点是跨域前端跑在http://localhost:5173后端跑在http://localhost:8080两个端口不同直接请求必然触发CORS。解决办法有两个要么后端开CORS配置要么用Vite的代理转发。我推荐后者因为开发时走代理最接近生产环境的Nginx转发能提前暴露问题。Vite配置代理// vite.config.js export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })因为后端已经配了context-path: /api所以这里无需rewrite前端请求/api/pet/list代理会原样转发到后端后端刚好也是/api/pet/list。这是我最推荐的写法比“前端写/api、后端不带前缀、代理再rewrite去掉/api”要少很多容易错的环节。Axios实例封装成统一入口import axios from axios import { ElMessage } from element-plus import router from /router const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization token } return config }) request.interceptors.response.use( res { if (res.data.code ! 200) { ElMessage.error(res.data.msg || 请求失败) return Promise.reject(new Error(res.data.msg)) } return res.data.data }, err { if (err.response err.response.status 401) { localStorage.removeItem(token) router.push(/login) ElMessage.error(登录已过期请重新登录) } else { ElMessage.error(网络异常请稍后重试) } return Promise.reject(err) } ) export default request这样封装的收益是所有页面的业务代码只关心数据本身不用管Token怎么带、错误怎么提示。后来加接口的时候新页面里几行代码就能调用团队协作时也不会出现“有人忘了带Token”这种低级问题。5.2 路由守卫与Pinia状态管理前端路由用Vue Router 4。createWebHistory()是HTML5 History模式URL干净没有#号但部署后需要服务器配合Nginx配try_files这个我到部署环节再讲。如果嫌麻烦可以直接用createWebHashHistory()上线后永远不出现刷新404的问题代价是URL里多个#看个人取舍。const router createRouter({ history: createWebHistory(), routes: [ { path: /login, component: () import(/views/Login.vue) }, { path: /, component: Layout, redirect: /pets, meta: { requiresAuth: true }, children: [ { path: pets, component: () import(/views/PetList.vue) }, { path: pets/:id, component: () import(/views/PetDetail.vue), props: true }, { path: consultations, component: () import(/views/ConsultationList.vue) } ] } ] }) router.beforeEach((to) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) return /login if (to.path /login token) return /pets return true })用户信息这种全局数据我用Pinia存。不用Vuex的原因很简单Pinia去掉了mutations改个数据不用再写一堆样板代码TypeScript支持也好。在这里存一份当前用户信息页面里要用用户名、角色的时候直接useUserStore()取比每次从接口重新拉要高效。5.3 健康图表页ECharts把数据变成曲线宠物详情页是这个系统的门面。进入页面后左侧是宠物基础信息和疫苗时间线右侧是一个大图表区展示体重的月度趋势。前端拿到后端返回的月度和平均体重数组填充ECharts配置import * as echarts from echarts import { onMounted, ref } from vue import request from /utils/request const months ref([]) const avgWeights ref([]) onMounted(async () { const data await request.get(/health/trend/${petId}, { params: { months: 6 } }) months.value data.map(item item.month) avgWeights.value data.map(item item.avgWeight) renderChart() }) function renderChart() { const chart echarts.init(document.getElementById(healthChart)) chart.setOption({ tooltip: { trigger: axis }, legend: { data: [平均体重] }, grid: { left: 50, right: 20, top: 40, bottom: 40 }, xAxis: { type: category, data: months.value }, yAxis: { type: value, name: 体重(kg) }, series: [{ name: 平均体重, type: line, smooth: true, data: avgWeights.value, areaStyle: { opacity: 0.15 }, markPoint: { data: [ { type: max, name: 最高体重 }, { type: min, name: 最低体重 } ] } }] }) }ECharts默认是按数据项无限渲染的但如果你在图表组件里用了v-for循环渲染多个图表记得在组件卸载时调用chart.dispose()否则会内存泄漏页面长时间挂着会越来越卡。另外echarts.init的DOM容器必须已经有实际高度很多新手图表不显示检查一下多半是容器高度为0。5.4 疫苗提醒与咨询列表的交互细节疫苗模块的交互点在于“下次接种日期”的状态展示。我会在卡片上根据next_date和当前日期做颜色区分距离超过30天显示灰色“未到期”30天内显示橙色“即将到期”已过期显示红色“已过期”。这个逻辑放前端做就行后端只需要返回原始日期字段前端用dayjs比较。function getVaccineStatus(nextDate) { if (!nextDate) return { text: 未记录, type: info } const days dayjs(nextDate).diff(dayjs().startOf(day), day) if (days 0) return { text: 已过期${Math.abs(days)}天, type: danger } if (days 30) return { text: 还有${days}天, type: warning } return { text: 还有${days}天, type: success } }咨询列表则是典型的“角色决定视图”用户看自己发起的问题和回复状态顾问看所有待回复问题。前端根据Pinia里的用户角色动态渲染不同的标签页。接口层面我直接给咨询列表加了status和consultantId两个筛选参数复用一个接口避免给前端写两套API。6. 从本地到线上完整部署流程6.1 后端打包与运行环境准备先跑后端。在项目根目录执行mvn clean package -DskipTests构建产物在target目录下是一个可执行的jar包。启动之前确保服务器上有Java运行环境和MySQL。MySQL如果还没装Windows下就是下载安装包一路下一步注意三点选utf8mb4字符集、设置好root密码、记下端口号。Linux下用RPM包安装的话装完后记得先systemctl start mysqld然后从日志里找初始密码再修改。把数据库脚本导入mysql -u root -p pet_health.sql然后把jar包传到服务器直接启动java -jar pet-health-backend-1.0.0.jar --server.port8080 --spring.datasource.password你的数据库密码这里留了个实用小技巧外部通过命令行参数传配置优先级高于application.yml里的值。所以敏感信息比如数据库密码可以不在配置文件里写死上线时用--spring.datasource.password传入或者用环境变量SPRING_DATASOURCE_PASSWORD指定。这样即使代码仓库泄露数据库密码也不会被看到。如果后端在服务器上容易被关掉建议用nohup后台运行nohup java -jar pet-health-backend-1.0.0.jar app.log 21 日志输出到app.log出了问题直接tail -f app.log看比在终端死等要靠谱得多。6.2 前端打包与两种部署方式前端构建npm run build产物在dist目录里。接下来有三种部署路径我按推荐程度排序。方式一Nginx独立部署全套生产环境首选。前端静态文件丢给Nginx后端接口通过/api反向代理。好处是前端静态资源和后端服务完全解耦前端更新不用重启后端反向代理还顺便解决了跨域问题。server { listen 80; server_name your-domain.com; root /home/pet-health/dist; index index.html; 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 /里的try_files $uri $uri/ /index.html。没有这一行Vue Router用History模式时刷新/pets页面会直接404因为服务器上根本没有pets这个文件。这行配置的作用是所有匹配不到真实文件的请求都回退到index.html由前端路由接管。方式二把dist放进SpringBoot的static目录。直接把构建出的dist目录内容复制到src/main/resources/static下重新打包然后一个jar包同时提供前端页面和后端接口。这种方式适合个人小项目、成本极低的场景不需要额外装Nginx。但要注意如果前端用了createWebHistory()刷新子路由还是会404这时要么改回createWebHashHistory()要么在后端加一个WebMvcConfigurer的转发配置把未知路径转发到/index.html。这个做法原理和Nginx的try_files是一样的。方式三Docker Compose编排MySQL、后端、Nginx三容器。适合想顺便学容器化的朋友但这不是必须第一次跑项目一般不推荐直接上Docker容易把问题都搅在一起。6.3 部署时的配置检查和验证清单部署完最怕的是“页面出来了但接口全挂”。我整理了一份自己的验证清单按顺序检查能省很多排查时间。先验证数据库mysql -u root -p -e show databases;看看库在不在。再验证后端浏览器直接访问http://服务器IP:8080/api/user/loginPOST一条测试数据看能不能正常返回JSON。再验证前端静态资源访问http://服务器IP/能看到登录页就说明静态资源OK。最后验证代理登录一次如果登录成功说明/api代理、数据库连接、JWT签发整条链路都是通的。还有一个经常被忽略的坑服务器安全组的端口放行。云服务器的防火墙和安全组是两层有时候你本地访问不通不是程序问题而是8080端口根本没在安全组里放行。Nginx占用80端口的话也要确认80是放行的。7. 常见问题排查实录7.1 跨域报错CORS的三种表现前后端分离项目里跨域问题保守估计占新手联调困难的50%以上。常见报错形式有浏览器控制台出现Access-Control-Allow-Origin字样、请求状态变成CORS error、以及“请求都发出去了但response里拿不到数据”。如果你选择了“前后端都单独跑、前端不配代理”的方式那后端必须开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(*)和allowedOrigins(*)的区别当allowCredentials(true)时Spring不允许使用allowedOrigins(*)因为带Cookie的跨域请求不能把来源设为通配符。用allowedOriginPatterns才能在允许凭证的同时放行所有来源。我实际开发中更推荐走代理或Nginx转发后端完全不配置CORS因为生产环境用Nginx反代本来就不存在跨域CORS配置只会在Debug时多一层变量。7.2 MySQL连接异常全家桶这个项目最常见的启动失败异常信息五花八门本质就几个原因。第一个是Public Key Retrieval is not allowed解决方案是在JDBC URL里加allowPublicKeyRetrievaltrue。第二个是时区问题The server time zone value CST is unrecognized加serverTimezoneAsia/Shanghai。第三个是SSL警告刷屏加useSSLfalse。这三个参数我建议直接写进默认URL不要等报错再一个个补。还有一个隐藏坑是MySQL驱动版本。老教程里写的驱动类是com.mysql.jdbc.DriverMySQL 8.0要用com.mysql.cj.jdbc.Driver。如果你用的是Spring Boot 2.7.xmysql-connector-j依赖会自动管理版本不用手动指定但注意确认坐标是mysql:mysql-connector-java还是com.mysql:mysql-connector-j不同Spring Boot版本默认的坐标不一样。Spring Boot 2.7.6默认是mysql:mysql-connector-java:8.0.33如果你跟着新教程写了com.mysql:mysql-connector-j注意版本号要能解析到否则也会启动失败。7.3 MyBatis查询结果全是null这个坑我至少帮人排查过十几次。症状是接口返回200数据条数也对但每个字段都是null或者只有id有值。绝大多数情况是实体字段名和数据库列名对不上或者没有开启驼峰映射。先确认application.yml里有没有map-underscore-to-camel-case: true。如果开了还不行看XML里的resultType是不是写错了包名路径或者数据库列名确实是petname这种和实体不一致的。再不行就显式写resultMap做字段映射。排查思路是先在MySQL客户端里执行SQL确认数据库返回的列名再用mybatis.configuration.log-impl打印SQL日志看看MyBatis实际执行的语句和参数。我习惯在application.yml的开调试阶段加上mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl logging: level: com.pethealth.mapper: debug这样控制台会打印出每条SQL的参数和返回能快速定位SQL写错还是映射写错。上线前再把这个日志关掉避免刷屏。7.4 Vue路由刷新404与接口404刷新404的问题在第6节已经讲过Nginx加try_files即可解决。接口404则有另一个常见原因后端context-path和前端代理路径不对齐。比如前端请求/api/pet/list但后端配了server.servlet.context-path: /api而Controller又是RequestMapping(/pet/list)那就刚好。如果后端没配context-pathController直接是RequestMapping(/api/pet/list)那前端代理就要配rewrite把前缀去掉或者baseURL直接指向完整路径。总之前后端的路径规划要在动手写代码前就统一好我见过太多项目做到一半发现路径对不上到处改注解非常浪费时间。另外一个容易被忽略的接口404是Spring Boot拦截器把请求拦住了。如果后端日志里根本没打印到这个请求的Controller方法多半是拦截器返回了false。判断方法很简单看响应体是不是{code: 401, msg: 未登录...}这样自定义的JSON如果是说明请求其实到后端了只是被拦截器挡了不是404。7.5 Spring Boot版本太高导致的老项目移植问题网上很多老教程的代码是Spring Boot 1.x/2.x时代的你如果新建项目时选了最新的Spring Boot 3.x经常会遇到编译错误程序包javax.servlet不存在、无法解析javax.validation、RabbitMQ/Redis等Starter版本不兼容。这些都是因为Spring Boot 3把javax迁移到了jakarta命名空间。我的建议是做这种教学型和课程设计型项目锁定Spring Boot 2.7.x这是目前能找到最多教程和答案的版本。如果你一定要用3.x就从头把依赖全部对齐Spring Boot 3的BOM不要混着用老教程的依赖版本。还有Tomcat的变化Spring Boot 3内置的是Tomcat 10很多老的手写过滤器代码也要从javax.servlet改成jakarta.servlet。7.6 端口占用与本地联调杂项后端启动时报Port 8080 was already in use要么是上一个进程没关掉要么是别的服务占了端口。Windows下用netstat -ano | findstr 8080查进程PID再到任务管理器结束进程Linux下用lsof -i:8080。临时换个端口也可以直接java -jar xxx.jar --server.port8081。还有个IDE相关的小细节在IDEA里Run配置的VM options里加上-Dspring.profiles.activedev可以让本地开发和线上分离。如果只是想单独改端口在Program arguments里填--server.port8081也行。这个小操作很多人不知道改配置文件来回切很麻烦。最后分享一个我自己的习惯项目全部跑通之后我强烈建议你做一件事回到数据库删掉所有测试数据然后从用户注册开始完完整整走一遍业务流程。造两只不同的宠物录一个月的健康数据发起一次咨询再以顾问身份登录回复。这一步能帮你发现很多“代码能跑但逻辑不对”的问题——比如体重记录居然能选未来日期、顾问能看到所有用户的宠物、删除宠物后健康记录变成孤儿数据等等。我个人的体会是项目开发最花时间的往往不是写代码而是业务边界的设计。宠物健康顾问系统这种项目业务逻辑不算复杂但涉及角色、状态流转、时间的处理每一处都有细节。把细节抠到位这个项目的完成度就不只是一个“能交差”的水平而是可以真正拿出去用的水平。如果你在此基础上再扩展一个定时任务做疫苗提醒推送或者用ECharts把体温、心率也画成多指标联动图表项目整体质量又能上一个台阶。
RELATED

相关推荐

Unity消融效果实战:基于噪声贴图与透明度裁剪的Shader实现

Unity消融效果实战:基于噪声贴图与透明度裁剪的Shader实现

在游戏里,角色死亡后飘散的灰烬、怪物被法术命中后燃烧殆尽、场景中冰块慢慢融化成水——这些效果大部分都是同一类技术:Unity里的消融效果。简单说,消融就是让一个物体从完整状态逐渐消失,而实现的核心手段,就是对贴图…

📅 2026/10/6 3:04:46
Unity消融效果Shader实现:动态着色与像素裁剪实战

Unity消融效果Shader实现:动态着色与像素裁剪实战

做技能特效和战斗表现的人,最常被问到的一个需求就是:怎么让一个模型在游戏里像被高温熔化一样,从某个位置开始一点点碎掉、烧掉,边缘还带一圈发光。这个需求在动作游戏、卡牌大招、甚至场景解密里都很常见。今天要聊的Unity消融效…

📅 2026/10/6 3:04:46
轴承寿命预测实战:从时域变换到健康指标构建

轴承寿命预测实战:从时域变换到健康指标构建

简介:面向轴承寿命预测的MATLAB源码包,聚焦时域变换与状态特征提取,适用于工业物联网、设备健康管理与故障诊断领域的研究者、工程师及相关专业学生。资源内共3个m文件,压缩包仅1KB,三个脚本分工明确,分别对…

📅 2026/10/6 2:59:45
MORE NEWS

更多资讯

📰

SAP PS模块快速指南:从项目定义到WBS的落地实践

简介:这份PDF资料面向SAP PS模块的初学者与项目管理人员,系统梳理了项目系统的核心概念与实操要点,帮助读者快速建立从项目创建、规划、执行到收尾的完整认知框架。内容涵盖SAP PS模块概述、项目分类与工作分解结构WBS、网络图与里程碑监控、…

📰

微信小程序目录结构怎么组织?从2048源码实战拆解

接手微信小程序项目,第一步不是看代码能不能跑,而是先把项目结构吃透。这几天交付了一个2048小游戏的微信小程序源码工程(2048-小程序.zip),不少朋友拿到压缩包后第一句话就问:这些文件夹和文件都是干什么的…

📰

Delphi客户端文件上传与PHP接收:multipart/form-data实战与避坑指南

简介:一套面向Delphi桌面端开发者与PHP服务端工程师的文件上传联调示例代码,解决客户端提交文件、服务端接收存储的完整链路问题。示例覆盖通过Indy组件构造HTTP POST请求、封装二进制文件流,以及在服务端PHP脚本中借助$_FILES与move_uploade…

📰

从缓存故障到数据库切换:混沌实验设计思路与实战指南

你有没有在半夜被一条告警短信叫醒过?我遇到的那一次,是缓存集群里一个节点悄悄退出,流量绕过缓存直击数据库,连接数瞬间被打满,P99延迟从几十毫秒飙到三秒多。事后复盘,结论很一致:我们为高可用…

📰

泉州樟脚村:不用滤镜的五彩石头古村拍照攻略与实用自驾指南

这两年泉州是真的火,西街、开元寺、蟳埔簪花围,一到假期全是人从众。但很多人不知道,从泉州市区往北走,泉港区涂岭镇的山坳里还藏着一个几乎没什么游客的石头村——樟脚村。我头一回知道它,是被一张清晨雾气里的五彩石…

📰

SIM卡引脚定义详解:硬件工程师必懂的7个触点电气逻辑与故障排查

1. 什么是SIM卡引脚定义?它为什么值得花时间搞清楚“SIM卡引脚定义”这六个字,乍看像教科书里的冷门术语,但只要你拆过手机、修过物联网设备、调试过POS机或车载终端,甚至只是好奇过“为什么插反了卡就识别不了”,你就…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬