
1. 项目概述为什么我要做一套宠物爱心组织管理系统宠物爱心组织管理系统在程序员的项目清单里不算新奇但真正跑到救助站和爱心领养中心看过一圈的人才知道这套系统解决的全是切肤之痛。从宠物入所建档、疫苗驱虫记录、领养人资质审核到志愿者排班、物资捐赠入账、活动报名统计大量信息散落在Excel表格和微信聊天记录里月底对账能对到半夜。我最近完成的这套 Java Web 宠物爱心组织管理系统技术栈锁定 SpringBoot2 Vue3 MyBatis-Plus MySQL8.0源码和配套文档都齐把上述业务全部搬上系统适合正在找实战项目的Java开发者、刚入门Vue3的前端同学以及想推进信息化建设的爱心组织。这套系统不是那种堆了一堆CRUD就交差的练习项目。宠物爱心组织的业务比一般管理系统更杂既有常规的档案管理又有领养这种带审核流和回访周期的业务还涉及志愿者、捐赠、活动等多个参与方。把这些关系理清楚本身就是一个完整的业务建模训练。对开发者来说照着这套源码走一遍能同时掌握SpringBoot2后台、Vue3前端、MyBatis-Plus通用CRUD、MySQL8数据库设计四条线比零散看教程效率高得多。1.1 爱心组织的真实业务痛点先说线下场景。救助站收容的每一只流浪动物从进站那天起就要开始记录体况检查、治疗过程、体内外驱虫、疫苗首免和加强针、绝育手术时间、寄养位置、是否亲人、能不能和其他动物合笼。这些信息以前可能只存在于救助群里的聊天记录和一张手写登记表上。真到了领养人来看猫看狗志愿者得临时翻群聊记录才能说清楚这只动物的来历。领养审核更麻烦。救助站要把流浪动物交到陌生人手里心里那道坎必须过对方住在哪、是不是租房、家里有没有其他宠物、有没有养宠经验、能否接受回访。这些审核要素要是靠微信截图传来传去遗漏和扯皮是家常便饭。捐赠和物资对账也难现金捐、猫粮狗粮捐、药品捐时间跨度一拉长Excel里的数据结构就变形了格式也各不相同。这套系统把上述问题全部模块化宠物档案管全生命周期领养申请走完整审核状态流捐赠物资分开记账志愿者排班和活动报名都在线上完成。爱心组织负责人不再需要追着志愿者要表格所有操作留痕出了问题能追溯到人责任边界清楚很多。1.2 这套系统的学习价值与适用人群我之所以把源码和文档整理出来是因为这类业务真实但不复杂的管理系统是目前Java Web学习者最缺的中间态项目。太大的电商项目容易陷在分布式中间件里太小的学生作业又只有一个表跑通流程而宠物爱心组织管理系统刚好卡在中间单体能跑业务有深度数据库设计有讲究。如果你正在准备面试项目这套系统能讲的东西很多MyBatis-Plus 如何把单表CRUD工作量压到最低JWT 如何做无状态登录领养审核的状态机怎么设计Vue3 的响应式数据在复杂表单里的实际用法MySQL8 部署时那些坑是怎么排查的。如果你本身就是爱心组织的技术志愿者这套源码可以直接改改上线用文档里连数据库初始化和部署脚本都准备好了。2. 技术选型拆解为什么锁定这四件套选型这件事我吃过亏。早期做管理系统喜欢追新什么新框架都敢上结果做到一半官网文档不齐全卡在环境配置上折腾一周。这次做宠物爱心组织管理系统我一开始就定了三条纪律生态成熟、资料充足、团队上手成本低。最终锁定的 SpringBoot2 Vue3 MyBatis-Plus MySQL8.0恰好都满足。2.1 SpringBoot2 与 MyBatis-Plus稳定压倒一切SpringBoot2 很多人觉得过时了SpringBoot3 都出了好几年。但实际做业务项目稳定性比版本号更重要。SpringBoot2.7.x 搭配 JDK8是目前存量项目最多、网上解决方案最全的组合。你随便搜一个报错几乎都能找到前人踩坑的记录。SpringBoot3 默认要求JDK17不少老版本的依赖和插件跟不上迁移成本不低。所以这套系统我坚定用SpringBoot2.7.x开发效率和排错效率都高。MyBatis-Plus 是这套系统的效率担当。它的核心价值在于单表增删改查完全不用写SQL继承一个 BaseMapper 就有几十个现成方法。更实用的是条件构造器比如查询所有待领养的猫咪直接 LambdaQueryWrapper 拼接条件不用在XML里维护字符串SQL。它内置的分页插件、逻辑删除、字段自动填充、代码生成器每一个都是开发中最常遇到的硬需求。我选择 MyBatis-Plus 还有一个理由它的通用 CRUD 服务配合 SpringBoot 的泛型抽象可以实现无状态的增删改查服务层。简单说宠物、志愿者、捐赠等模块只要实体类定义好Service 层不需要每个模块重复写一遍 create、update、delete、page 方法一个通用基类全部搞定。后续新增一张表只需要写个实体类和Mapper接口业务代码量少到可以忽略。2.2 Vue3 Vite前端工程化的当下最优解Vue3 相比 Vue2 最大的变化是 Composition API。以前写一个页面逻辑数据、方法、计算属性分散在 options 里一个功能相关的代码被拆得七零八落用 Composition API 之后可以把某个业务的所有响应式变量和函数放在一起逻辑内聚性一下子提高了。配合 setup 语法糖代码读起来和普通 TypeScript 函数几乎没区别这也是Vue3学习曲线中最值得先掌握的差异点。组件生态我选了 Element Plus后台管理系统用它做表格、表单、弹窗、分页就是标准的搭积木流程开发速度极快。构建工具方面用了 Vite 而不是 vue-cli因为 Vite 冷启动快到毫秒级改代码浏览器热更新几乎无感。整个项目没有做复杂的微前端或服务端渲染一个典型的中后台管理端用 Vue3 Vite Element Plus 已经是效率最高的组合。很多刚从Vue2迁移过来的同学会纠结 reactive 和 ref 到底用哪个。我的经验很简单基础类型用 ref对象和数组如果不需要整体替换用 reactive但一旦涉及整表替换比如分页接口返回新数组直接用 ref 包一层更省心。这个细节我在前端部分还会专门展开。2.3 MySQL8.0版本升级的收益与成本MySQL8.0 已经发布多年生产环境占有率早就超过旧版本。它相比 MySQL5.7 有几个实打实的升级默认字符集是 utf8mb4emoji 和生僻字不会再乱码新增窗口函数和 CTE公用表表达式写排名、同比环比这类统计SQL方便很多JSON 类型的功能也增强存储灵活的扩展字段不用再单独建关联表。代价也有。MySQL8 的默认认证插件是 caching_sha2_password老版本的驱动和可视化工具直连会报错需要换 MySQL Connector/J 8.x或者手动把用户认证方式改回 mysql_native_password。这两条路我都走过后面部署章节会给出具体命令和配置。另外MySQL8 对服务器内存的占用明显比5.7高如果是1核2G的小服务器需要主动调低 innodb_buffer_pool_size不然跑起来很容易卡。数据库安装我强烈推荐一条 Docker 路线一条命令拉起 MySQL8数据目录挂载到宿主机删容器重建数据也不丢。这套系统附带的部署文档里就是这个方案省去了一堆 Linux 命令兼容性问题。2.4 整体架构与请求链路整套系统是标准的前后端分离单体架构。前端 Vue3 项目通过 axios 发起请求经过 Nginx 反向代理到后端 SpringBoot 应用的 8080 端口。后端由 Controller、Service、Mapper 三层构成MyBatis-Plus 负责数据库交互JWT 处理登录状态用一个统一的 Result 对象包装所有响应。请求链路大致是这样浏览器输入账号密码登录后端校验通过后签发 JWT Token前端把 Token 存在 localStorage每次请求在 Authorization 请求头带上后端拦截器解析 Token校验通过后放行到对应的 Controller。这套无状态设计的好处是服务端不用维护 session后面真要横向扩展多实例也不用额外处理会话同步。业务数据最终落到 MySQL8宠物、领养、志愿者、捐赠、活动、用户六类核心数据通过外键逻辑关联。3. 核心模块与数据库设计实战数据库设计是这套系统最花心思的部分。宠物爱心组织管理系统的数据模型和普通进销存还不太一样它有一个贯穿宠物档案、领养申请、回访记录的主业务线同时又有志愿者、捐赠、活动这些相对独立的辅助业务线。设计得好后面写业务代码就不会一直改表结构。3.1 六大模块的功能拆解我把系统拆成六个模块每个模块解决一类具体业务模块核心功能使用角色宠物档案管理入所登记、医疗记录、疫苗驱虫、绝育信息、状态变更管理员、志愿者领养管理领养申请、资质审核、家访排期、签约领养、回访跟踪管理员、领养人志愿者管理志愿者注册、值班排班、服务时长统计管理员、志愿者捐赠物资管理现金捐赠登记、物资入库、物资出库、库存盘点管理员活动管理活动发布、报名统计、签到管理、活动回顾管理员、志愿者系统管理用户管理、角色权限、登录日志管理员宠物档案可不是简单记个名字照片。我实际建模时给它挂了医疗记录子表一只流浪猫进站后治疗过程中的体温、用药、手术记录都要按时间排好领养人问起来能直接展示完整就医历史。这个细节对爱心组织的信任感提升很关键。捐赠模块我做了现金和物资分离。现金走金额字段精确到小数点后两位物资走独立明细表记录物品名称、数量、单位、有效期出库时对应到领养人或活动消耗。不做成一个混合表的好处是月底对账时现金账和实物账互不干扰也不会出现一条记录里既放金额又放数量导致统计混乱。3.2 表结构设计中的几个关键决策核心表设计上第一张是宠物表 pet字段包括id、pet_name、species猫/狗、breed、gender、age、health_status、adopt_status、photo_url、remark、create_time、update_time、deleted。其中 adopt_status 是领养状态用整数枚举表示0-待领养、1-审核中、2-已领养、3-已下架。用整数不用字符串是为了查询和统计效率配合状态字典表解释数字含义。第二张是领养申请表 adoption_application这是业务最复杂的表id、pet_id、adopter_id、apply_time、home_type自有/租房、has_pet_experience、has_other_pets、audit_status、reject_reason、audit_time、follow_up_record。审核状态我用单独字段 audit_status 而不是直接在 pet 表上改状态因为一次领养申请可能被驳回但宠物还是待领养状态两个概念不能混。设计时有几个决策特别想说。第一所有表都加了逻辑删除标志 deleted不用物理删除。救助站的数据有追溯价值志愿者手滑删错一条记录影响很大逻辑删除可以随时恢复。第二表之间不建物理外键只保留逻辑关联和索引。物理外键在并发写入时容易引发锁竞争而且以后拆库拆表都得先删外键业务代码里控制好引用关系就行。第三时间字段统一用 DATETIME 并设置默认 CURRENT_TIMESTAMP不用字符串存时间。字符串时间排序比较都别扭日期函数也用不上。第四涉及金额的字段一律 DECIMAL(10,2)用 DOUBLE 会出现0.10.2不等于0.3的精度问题做财务对账是绝对不能接受的。3.3 领养流程状态机让审核不乱套领养审核是整个系统业务流程最重的部分我设计了一个六步状态机待审核 → 初审通过 → 家访排期 → 复审通过 → 签约领养 → 回访跟踪 → 归档。驳回可以在多个环节发生但每次驳回必须填写原因原因会写入 reject_reason 字段并展示给领养人。为什么要把一个简单的通过/拒绝搞这么复杂因为真实救助站的审核过程就是分阶段的管理员先看申请资料是否完整这是初审志愿者上门看居住环境这是家访负责人综合评估双方匹配度这是复审签完领养协议后还要定期回访确认宠物在新家过得好不好。每个环节的责任人不同权限也不同。用状态机建模每个状态对应一个操作按钮集合系统自然就不会出现跳步审核或重复审核的情况。状态机的实现不复杂我在后端 Service 里封装了一个 audit() 方法传入当前状态、目标状态和操作人角色先校验状态转移是否合法再执行更新。SpringBoot 的事务注解保证审核记录和状态更新要么同时成功要么同时回滚。这套逻辑面试时也很好讲状态机 事务 权限校验三个点合在一起就是一个完整的业务闭环。4. 后端手把手实现SpringBoot2 MyBatis-Plus后端部分是这套系统的重头戏。我用 SpringBoot2.7.18 搭建工程JDK8打包方式为 jar。下面从初始化、通用CRUD封装、登录认证三个层面说清楚实现思路。4.1 项目初始化与关键依赖配置新建工程时我直接选了 Spring Initializr 生成Java 8、打包方式 jar、SpringBoot 版本 2.7.18。核心依赖在 pom.xml 里这样加dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.2/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency dependency groupIdcn.hutool/groupId artifactIdhutool-all/artifactId version5.8.25/version /dependency /dependencies这里特别提醒一点 MySQL8 对应的驱动依赖坐标是com.mysql:mysql-connector-j而不是以前5.7时代的mysql:mysql-connector-java。如果你用了旧坐标即使代码能跑起来也可能因为驱动版本太老导致认证方式不兼容。核心配置文件 application.yml 里数据源和 MyBatis-Plus 的配置是这样server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/pet_aid?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: 123456 mybatis-plus: global-config: db-config: id-type: auto logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 configuration: map-underscore-to-camel-case: true mapper-locations: classpath:/mapper/**/*.xmlurl 参数里有两个关键点一个是serverTimezoneAsia/Shanghai不配的话 JDBC 连接时会报时区异常另一个是allowPublicKeyRetrievaltrue这是 MySQL8 的 caching_sha2_password 认证插件需要的新驱动默认不主动获取服务端公钥不配置会报连接被拒绝。这两个参数几乎是 MySQL8 JDBC 的标配建议直接背下来。4.2 基于 MyBatis-Plus 的通用 CRUD 封装MyBatis-Plus 的高效用法不是每个模块写一套 CRUD而是做一次通用封装后续直接复用。我先定义 BaseEntity所有业务表对应的实体都继承它Data public class BaseEntity { TableId(type IdType.AUTO) private Long id; TableField(fill FieldFill.INSERT) private LocalDateTime createTime; TableField(fill FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; TableLogic private Integer deleted; }createTime 和 updateTime 是自动填充字段值由 MetaObjectHandler 统一生成不需要业务里手动 set。这里有一个容易踩的坑用了 TableLogic 逻辑删除后框架会自动在查询 SQL 后面追加AND deleted0这是对的但如果你在业务里又自己写了带 deleted 条件的 SQL就会造成条件重复属于典型的通用封装搭配问题。所以我后面自定义 SQL 时明确约定不再重复拼 deleted 条件。自动填充处理器这样写Component public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, createTime, LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } }由于所有实体都有 create_time、update_time、deleted 三个字段我直接用 BaseEntity 自动填充统一接管业务代码里完全不用碰时间字段。后续新建一张表实体继承 BaseEntityCRUD 能力就全有了这就是通用 CRUD 服务的基础。分页功能不需要每次手写 limit配置一个分页拦截器即可Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }有了这个配置Controller 里接 Page 参数Service 里 this.page(page, wrapper) 就能完成分页。这里要强调MyBatis-Plus 的分页插件不配置就不生效很多新手只引入了依赖却不注册这个 Bean结果分页查询始终返回全部数据排查半天才发现是拦截器没加。4.3 登录认证与权限控制管理系统的登录认证我选择 JWT 方案。不需要 Redis 存 session服务端无状态逻辑也简单。登录接口收到用户名密码后去 user 表查询并校验密码通过就生成一个有效期 2 小时的 Token把用户ID和角色编码塞进 claims 里。Token 工具类核心逻辑public class JwtUtil { private static final String SECRET pet-aid-system-secret; public static String generateToken(Long userId, String role) { return Jwts.builder() .claim(userId, userId) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() 2 * 60 * 60 * 1000)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody(); } }权限拦截器里面除了验证 Token 是否有效还要针对不同接口做角色校验。比如领养审核接口只有 ROLE_ADMIN 能调用志愿者排班接口 ROLE_VOLUNTEER 也能调用。判断逻辑简单直接从 Token 解析出的 role 字段和接口要求的角色做匹配。这里我建议用注解的方式实现写一个 RequireRole 注解挂在 Controller 方法上拦截器反射读取注解统一校验业务代码里就不用到处判断角色了。统一响应体和全局异常处理是我强烈建议加的。所有接口返回 Result 结构里面 code、msg、data 三个字段成功 code200。全局异常处理器把业务异常、参数校验异常、系统异常分开处理避免堆栈信息直接裸奔到前端。这一层做好了前端拿到任何响应都能用一个拦截器统一处理错误提示开发体验提升非常明显。5. 前端手把手实现Vue3 Element Plus 后台前端部分用 Vue3 Vite Element Plus 搭建。我按后台管理系统常用的组织方式views 放页面、router 管路由、api 层统一封装网络请求、stores 放 Pinia 状态。下面挑三个关键环节讲。5.1 Vite 初始化项目与基础设施搭建创建项目我用的命令是npm create vuelatest这个命令会生成一个带 TypeScript、Vue Router、Pinia 选项的基础工程。生成之后安装依赖npm install element-plus axios pinia vue-router npm install -D unplugin-auto-import unplugin-vue-componentsElement Plus 我按需自动导入用 unplugin-vue-components 里的 ElementPlusResolver这样打包体积比全量引入小很多。对后台项目来说这一步收益很明显首屏加载速度快不会因为把整个组件库打进去导致白屏时间长。axios 请求封装我这里给出一个可复用的雏形import axios from axios const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) service.interceptors.response.use( res { const { code, msg, data } res.data if (code 200) return data ElMessage.error(msg) return Promise.reject(new Error(msg)) }, err { if (err.response?.status 401) { localStorage.removeItem(token) router.push(/login) } return Promise.reject(err) } )注意 axios 拦截器里处理 401 跳转登录这是后台系统必须有的兜底逻辑。Token 过期、被篡改、权限不足后端统一返回 401前端收到后清理本地登录态并跳回登录页用户重新登录一次即可恢复不用等浏览器报错才反应过来。5.2 宠物信息管理页面的完整实现宠物管理页面是典型的搜索区 表格 分页 弹窗表单结构。搜索区按宠物名、物种、领养状态筛选表格展示主要字段行内操作点编辑弹出表单。关键代码片段template el-table :datatableData v-loadingloading el-table-column proppetName label宠物名 / el-table-column propspecies label物种 / el-table-column propbreed label品种 / el-table-column propage label年龄 / el-table-column label状态 template #default{ row } el-tag :typestatusMap[row.adoptStatus]{{ statusText(row.adoptStatus) }}/el-tag /template /el-table-column el-table-column label操作 template #default{ row } el-button typeprimary link clickopenEdit(row)编辑/el-button /template /el-table-column /el-table el-pagination v-model:current-pagequery.pageNum v-model:page-sizequery.pageSize :totaltotal changeloadData / /template表单校验用 el-form 的 rules注意编辑和新增共用同一个弹窗时打开弹窗后要用 nextTick 清空下旧的表单校验状态。我踩过这个坑编辑完一条数据关掉弹窗再点新增表单还残留着上次的校验红字。解决办法是在 open 方法里调用 formRef.value.resetFields()并且在 dialog 关闭后重置表单数据。图片上传这块我本地开发用的是后端接口接收 MultipartFile保存到配置的上传目录然后返回文件访问路径存入 photo_url。部署上线如果条件允许换成 OSS 或者 MinIO 都行代码上只是替换上传接口返回的 URL前端逻辑不用动。Vue3 数据这里有个细节值得说。加载列表数据时用const tableData ref([])接口返回后整体赋值tableData.value res.rows。如果用 reactive 包数组整体赋值会丢失响应性很多人第一次从 Vue2 转到 Vue3 都会在这个地方卡住。我的建议就是列表数据统一用 ref简单直接。5.3 领养审核交互的细节打磨领养审核页面是整个系统前端交互最复杂的地方。列表展示所有领养申请每行根据 audit_status 显示不同按钮待审核显示通过驳回初审通过显示安排家访复审通过显示签约领养。这些按钮不能一次性全显示否则志愿者误点跳步业务流程就乱套了。点通过前我加了一个二次确认弹窗文案写明当前环节和后续环节。比如初审通过后提示将由管理员安排家访让操作人明确知道下一步是什么。驳回时表单里必须有驳回原因内容不为空才允许提交这个校验用 el-form 的 rules 就能实现。审核详情我用了抽屉组件 el-drawer把领养人的住房信息、养宠经验、当前宠物情况汇总展示。为了避免审核时看不全信息还要跳转多个页面我在详情抽屉里做了个 Tab第一个面板显示申请资料第二个面板显示宠物档案摘要第三个面板显示历史审核记录。这样审核人一个页面看完所有上下文再做决定符合实际业务操作习惯。前端还有个细节就是状态变更后列表刷新。我统一在操作成功的回调里调用 loadData()而不是手动去改某一行数据。虽然多一次请求但保证列表和服务端数据绝对一致不会出现本地改了状态但漏刷新字段的问题。6. MySQL8.0 安装与项目部署一条 Docker 路线搞定这一节解决两个问题MySQL8.0 怎么装、系统怎么部署上线。两个问题都给出我实际验证过的步骤。6.1 Docker 安装 MySQL8.0 与初始化配置在 Linux 服务器上安装 MySQL8.0我最推荐 Docker 方式。命令如下docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD123456 \ -e MYSQL_DATABASEpet_aid \ -v /opt/mysql8/conf:/etc/mysql/conf.d \ -v /opt/mysql8/data:/var/lib/mysql \ mysql:8.0参数解释一下-p 3306:3306 把容器内 3306 端口映射到宿主机-e MYSQL_ROOT_PASSWORD 设置 root 密码-e MYSQL_DATABASE 自动创建 pet_aid 数据库-v 挂载目录最关键数据目录挂载到宿主机 /opt/mysql8/data以后容器删了重建数据还在。很多新手部署 MySQL8 后容器一删数据全没了就是因为没挂载数据卷。容器启动后执行数据库初始化脚本docker exec -i mysql8 mysql -uroot -p123456 init.sqlinit.sql 里面包含建表语句、初始管理员账号、状态字典数据。文档里整理了完整 SQL 脚本直接执行即可不需要手工逐条建表。MySQL8 跑在 Docker 里有几个配置要提前注意。第一时区。如果业务要按北京时间统计捐赠金额容器默认用 UTC 时区会差 8 小时要么在 run 命令加-e TZAsia/Shanghai要么在 MySQL 配置里写default-time-zone08:00。第二字符集。建表语句统一指定DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci避免中文乱码。第三内存。小服务器建议在挂载的配置目录里放一个 my.cnf把 innodb_buffer_pool_size 调到 128M 左右不然 2G 内存的机器跑 MySQL8 加上 SpringBoot容易 OOM。6.2 前后端部署与上线要点后端打包部署mvn clean package -DskipTests java -jar target/pet-aid-server-1.0.0.jar --spring.profiles.activeprod建议在 resources 下准备 application-prod.yml生产环境数据库地址、账号密码单独配置不要把开发环境的配置直接带到服务器上。正式跑服务不要用java -jar挂在前台用 systemd 或者 Docker进程挂了能自动重启。前端打包npm run build构建产物在 dist 目录把这个目录上传到服务器后配置 Nginx 反向代理即可server { listen 80; server_name your-domain.com; root /opt/pet-front/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }这个配置里 try_files 必须有因为 Vue3 用的 history 路由模式刷新非首页路径时 Nginx 会找不到对应文件必须回退到 index.html 让前端路由接管。这是前端部署最常见的坑没有之一。/api/前缀的请求反代到后端 8080这样前端请求就同源了也不会有跨域问题。7. 常见问题排查与避坑实录整套系统从开发到部署我踩过不少坑这里整理成速查表按后端、前端、数据库三个维度列出来。7.1 MyBatis-Plus 高频问题现象原因解决方案分页查询返回全部数据没注册分页拦截器添加 PaginationInnerInterceptor Bean逻辑删除后数据查询不到自定义SQL又拼了deleted条件自定义SQL不再重复写deleted自动填充字段为nullMetaObjectHandler 没注入或字段名不一致检查处理器类是否有Component条件构造器报字段不存在字段名写成了数据库下划线形式用 LambdaQueryWrapper 避免拼字符串批量插入效率极慢JDBC URL没开批处理url追加 rewriteBatchedStatementstrue批量插入这个点值得多说一句。MyBatis-Plus 的 saveBatch 默认走 JDBC 批处理但 MySQL 的驱动默认把批处理语句逐条发送加了rewriteBatchedStatementstrue后才能真正合并执行。我在导入志愿者名单时实测过5000条数据从几十秒优化到两三秒。7.2 Vue3 开发中的典型坑Vue3 和 Vue2 开发习惯差异不小。最典型的是响应式丢失问题用 reactive 定义的数组从接口拿到新数据后直接list res.data页面不更新。这是因为 reactive 里的数组引用没变改变的是它指向的地址。对策有两种数组用 ref 定义整体赋值用.value或者用list.splice(0, list.length, ...res.data)原数组原地更新。我所有列表查询统一用 ref 方案避免记错。Element Plus 表单校验也有坑。await formRef.value.validate()在校验失败时是抛异常的如果不用 try/catch 包住点击提交后控制台会报 Uncaught (in promise)。正确写法const submit async () { try { await formRef.value.validate() // 校验通过调用保存接口 } catch { // 校验失败什么都不用做el-form 已经显示红字 } }我还在一个地方栽过跟头就是 el-table 的多选。表格设置 typeselection 列后跨页勾选需要 row-key 属性配合selection-change事件维护已选数组不然翻页后前面勾选的记录不会被计入。对需要批量审核领养申请的场景这个细节必须处理。7.3 MySQL8.0 兼容问题速查现象原因解决方案连接报 Public Key Retrieval is not allowedcaching_sha2_password 插件机制url加 allowPublicKeyRetrievaltrue连接报 Unable to load authentication pluginJDBC驱动版本过旧使用 mysql-connector-j 8.x中文乱码字符集不是utf8mb4建库建表显式指定utf8mb4容器重启数据丢失没挂载数据卷-v 挂载 /var/lib/mysql服务器内存占用过高innodb_buffer_pool_size 默认值大调低到128M~256M关于 MySQL8 的认证插件如果团队里有老版本可视化工具连不上临时解决办法是执行ALTER USER root% IDENTIFIED WITH mysql_native_password BY 123456;这条命令把 root 的认证插件改成老版本的 mysql_native_password但新项目我建议不要这样做毕竟新驱动和工具都支持 caching_sha2_password保住默认认证方式的加密强度更稳妥。部署完成后记得做两件事第一给 root 设置强密码并创建专用业务账号不能所有环境都用 root第二配置数据库定期备份至少每天凌晨全量备份一次用 mysqldump 结合 crontab 就能实现。宠物爱心组织的数据积累起来之后每一笔领养、捐赠记录都是组织的宝贵资产备份这步不能省。