
做这个系统之前我先说个背景。我接触过好几个动保组织他们的日常管理基本靠微信群加Excel表格来完成谁家狗被领养了、哪只猫在治疗中、钱花了多少、志愿者排班是几号……数据散落在各个人手里想查个信息得来回翻聊天记录。这个宠物爱心组织管理系统就是冲着这种痛点去做的。整个项目技术栈很清晰SpringBoot2负责后端业务逻辑Vue3做前端页面MyBatis-Plus处理数据库操作MySQL8.0作为底层存储前后端分离代码和文档齐整适合刚学完Java Web想找个完整项目练手的人也适合公益组织拿去做二次开发。先说这个系统能干什么。核心就是五个字管人、管宠物。系统里有三种主要角色管理员、志愿者、普通用户。管理员维护宠物档案、审核领养申请、管理捐献记录志愿者报名活动、填写服务日志用户可以浏览宠物列表、提交领养申请、查看公示公告。整个项目从后端接口到前端页面从数据库表到部署脚本都有对应文档说明复现起来不需要太多额外摸索。我的评价是这是一个标准的、能跑起来的、有完整业务闭环的后台管理型项目。1 项目整体设计与技术选型思路1.1 宠物爱心组织管理需要解决的核心问题动保组织的信息管理和普通商业后台不一样。宠物不是商品领养不是下单整个业务流程掺杂着大量非标操作比如档案杂乱。每只宠物的来源渠道不一样——有流浪救助、走失寻主、弃养接收、执法移交。每只宠物的健康状态也在变待驱虫、治疗中、康复、可领养、已领养、已回访。如果没有系统这些信息基本靠记忆和口头交接。领养流程无闭环。常规领养流程是浏览宠物 → 提交申请 → 管理员初审 → 家访/聊聊 → 签协议 → 接宠物 → 定期回访。很多组织走到第二步就断档了申请表发出去没有回调机制到底批没批、卡在哪个环节全凭运气。数据统计难。管理者和捐助人最关心的几个数字——每月收容多少只、成功领养多少只、治疗花了多少钱、各类捐款渠道占比——纯靠人工汇总一个月底就要熬几个晚上。所以系统设计的首位目标不是界面多漂亮而是把“流程”固化下来。宠物有状态机领养申请有流转状态志愿者活动有报名和签到钱和物资有台账。所有数据落到表里随时可以查、可以统计、可以追溯。1.2 技术选型背后的逻辑选这套技术栈不是拍脑袋每一层都有明确理由。SpringBoot2业内用得最广、资料最多、踩坑解决方案最全的企业级Java框架。SpringBoot2相比SpringBoot1最大的好处是自动配置和starter机制以前写SpringMVC Spring MyBatis整合要配N个XML现在一个spring-boot-starter-web就搞定内置Tomcat打jar包直接跑。Vue3前端目前的主流版本。相比Vue2Vue3的Composition API解决了复杂组件逻辑复用难的问题性能上也有提升——更小的打包体积、更快的渲染速度。对于这个项目的后台管理页面用setup语法糖写起来比Options API清爽得多代码量直接少三分之一。MyBatis-Plus有人叫它“增强版MyBatis”。MyBatis本身已经够灵活但用起来老是写重复的增删改查SQL。MyBatis-Plus把这些通用的CRUD方法全部封装在BaseMapper和IService里实体类加个TableName注解单表操作基本不用手写SQL。这个项目里大部分基础接口都是直接调用封装方法。MySQL8.0目前社区最稳定的开源关系型数据库。相比5.78.0的默认字符集升到utf8mb4emoji表情不会乱码、支持窗口函数、支持WITH查询对索引优化也有改进。更重要的是8.0是长期支持版本网上教程多、云数据库默认版本也是它跟项目环境匹配度高。1.3 系统角色与功能模块划分整个系统按角色权限分成三个入口管理员后台、志愿者端、普通用户端。权限控制虽然没上Spring Security那一套重量级方案但也做到了基于拦截器和角色字段的接口级控制。模块管理员志愿者普通用户核心功能说明登录注册是是是JWT令牌鉴权角色区分宠物管理是只读只读宠物档案增删改查、状态变更领养管理是查看申请申请提交、审核、状态流转志愿者活动是报名查看活动发布、报名管理、签到捐赠管理是查看查看资金和物资台账、统计报表公告管理是查看查看信息发布、置顶这里我特别想强调的是不要把权限控制做成前端按钮隐藏就完事。前端隐藏只是体验上的优化真正的安全性必须放后端。这个项目里每个Controller方法都加了自定义拦截器校验管理员接口会检查用户角色普通用户拿到了接口地址也调不通。2 数据库设计详解MySQL8.0)2.1 核心表结构与关系设计整个库的表不算多但每张表都用得扎实。最核心的有七张用户表、宠物信息表、领养申请表、志愿者表、活动表、捐赠记录表、公告表。用户表sys_user因为系统里管理员、志愿者、普通用户共用一张表所以有一个role字段区分角色取值1管理员、2志愿者、3普通用户。表结构长这样CREATE TABLE sys_user ( id bigint NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录账号, password varchar(100) NOT NULL COMMENT 加密后的密码, nickname varchar(50) DEFAULT NULL COMMENT 昵称, phone varchar(20) DEFAULT NULL COMMENT 联系电话, email varchar(100) DEFAULT NULL COMMENT 邮箱, role tinyint NOT NULL DEFAULT 3 COMMENT 角色1管理员2志愿者3普通用户, status tinyint NOT NULL DEFAULT 1 COMMENT 状态1启用0禁用, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;宠物信息表pet_info这是全系统业务量最大的表字段比较细CREATE TABLE pet_info ( id bigint NOT NULL AUTO_INCREMENT, pet_name varchar(50) NOT NULL COMMENT 宠物名字, pet_type varchar(20) NOT NULL COMMENT 种类犬/猫/其他, breed varchar(50) DEFAULT NULL COMMENT 品种, age_month int DEFAULT NULL COMMENT 月龄, gender tinyint DEFAULT NULL COMMENT 性别1公2母0未知, color varchar(30) DEFAULT NULL COMMENT 毛色, health_status varchar(100) DEFAULT NULL COMMENT 健康状况描述, status tinyint NOT NULL DEFAULT 0 COMMENT 状态0待领养1审核中2已领养3治疗中4已离世, avatar varchar(255) DEFAULT NULL COMMENT 照片URL, description text COMMENT 详细介绍, source varchar(50) DEFAULT NULL COMMENT 来源流浪救助/走失寻主/弃养接收/执法移交, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_status (status), KEY idx_type (pet_type) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT宠物信息表;为什么单独建idx_status索引因为系统里使用频率最高的一类查询是“当前状态为待领养的宠物”如果宠物总量上万没有索引的情况下MySQL会全表扫描。这种高频查询字段建索引查询速度能快一个量级。2.2 关键表关系与业务约束领养申请表adoption_apply是连接用户与宠物的核心业务表CREATE TABLE adoption_apply ( id bigint NOT NULL AUTO_INCREMENT, pet_id bigint NOT NULL, user_id bigint NOT NULL, apply_reason varchar(500) DEFAULT NULL COMMENT 领养理由, contact_phone varchar(20) NOT NULL, address varchar(200) DEFAULT NULL COMMENT 居住地址, has_yard tinyint DEFAULT NULL COMMENT 是否有院子/阳台1有0没有, family_agree tinyint DEFAULT NULL COMMENT 家人是否同意1同意0不同意, status tinyint NOT NULL DEFAULT 0 COMMENT 状态0待审核1已通过2已拒绝3已取消4已完成, audit_remark 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_pet_id (pet_id), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT领养申请表;这里要特别注意业务约束的取舍。从纯数据库设计角度应该给pet_id和user_id加外键确保引用完整性但实际开发中外键我建议不加。原因很简单外键约束虽然能保数据一致性但会让删除操作变复杂、在高并发插入时产生额外的锁开销。这个项目采用逻辑外键也就是表里的关联字段在代码层面维护数据库层面不建FOREIGN KEY。比如领养审核通过时要同时把宠物状态改成已领养这个原子操作放在一个事务方法里做靠代码保证一致性效果等同外键但灵活得多。还有一个细节adoption_apply表里我用idx_pet_id加idx_user_id分别建索引而不是建联合索引。原因是我既可能需要按宠物查看所有申请列表管理员查某只宠物被哪些人申请了也可能按用户查看申请历史用户看自己的申请记录两种情况都是独立查询分别建索引覆盖度更好。如果你确定只有“按宠物查申请”这一个场景那建idx_user_pet联合索引(pet_id, user_id)更省空间。这块没有绝对标准取决于业务查询模式。2.3 MySQL8.0带来的实际收益开发这个项目时用的是MySQL8.0.36有几个切切实实的好处值得说utf8mb4默认字符集。MySQL8.0的默认字符集是utf8mb4而不是5.7的utf8utf8mb3。这意味着你不需要在建表时手动加DEFAULT CHARSETutf8mb4宠物昵称里就算有emoji表情也能正常存储。在5.7时代很多人建表忘了指定字符集插入一个表情直接报Incorrect string value错误这个坑在8.0里从源头堵上了。窗口函数。做统计报表时非常香。比如要查“每个月的领养成功数量及占总量的百分比”5.7得写子查询嵌套8.0直接SELECT DATE_FORMAT(create_time, %Y-%m) AS month, COUNT(*) AS adoption_cnt, ROUND(COUNT(*) / SUM(COUNT(*)) OVER() * 100, 2) AS percent FROM adoption_apply WHERE status IN (1, 4) GROUP BY DATE_FORMAT(create_time, %Y-%m);SUM(COUNT(*)) OVER()这个窗口函数直接算出全局总数不需要自连接SQL可读性高得多。更好的JSON支持。如果后续要给宠物档案增加自定义体检指标直接加一个JSON类型的字段就可以无需要求每只宠物都有体检数据。这里JSON字段的好处是字段可以缺失MySQL不会像普通字段那样要求每行都有值。3 后端开发SpringBoot2 MyBatis-Plus实战3.1 项目初始化与依赖配置后端工程结构用标准的三层架构controller/service/mapper额外加了一个config包放配置类和拦截器一个common包放统一返回结果和异常处理。这也是我个人习惯适合中小型项目。pom.xml里的核心依赖一定要说清楚新手最容易在这个地方被各种版本冲突折磨parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.7/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies这里有个细节值得强调SpringBoot2.7.18是2.x系列的最后一个版本选它是因为网上大部分教程和经验帖子都基于2.x遇到问题能搜到参考。如果你自己开新项目且不依赖老旧代码我更推荐直接上SpringBoot3.x JDK17但那个组合对JDK版本有硬性要求必须17如果你的服务器上一堆老项目用的JDK8SpringBoot2 JDK8反而是最稳的选择。这个项目定位就是兼容性优先所以我锁的2.7.18。另外一个重要变化是驱动的groupId。MySQL官方从Connector/J 8.0.31开始把groupId从mysql:mysql-connector-java改成了com.mysql:mysql-connector-j。如果你在pom里同时引入了两个坐标或者是老坐标可能遇到ClassNotFound异常。这个项目直接用的新坐标。对应的application.yml配置如下server: port: 8080 servlet: context-path: /api spring: datasource: url: jdbc:mysql://localhost:3306/pet_org?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: root123 driver-class-name: com.mysql.cj.jdbc.Driver jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: autoserverTimezoneAsia/Shanghai这行一定要写。不写的话如果数据库地区设置和JDK默认时区不一致查时间字段会整体偏移8小时插入时间更是错得离谱。我在排查时见过太多新手在这个小地方阴沟翻船。map-underscore-to-camel-case属性一定要设为true这样数据库字段pet_name才能自动映射到Java属性petName。MyBatis-Plus默认是开启的但如果你在application.yml里覆盖了configuration一定要把它加上。3.2 通用CRUD服务的设计MyBatis-Plus最省心的地方就在这里。实体类写完之后基础增删改查不写一行SQLData TableName(pet_info) public class PetInfo { TableId(type IdType.AUTO) private Long id; private String petName; private String petType; private String breed; private Integer ageMonth; private Integer gender; private String color; private String healthStatus; private Integer status; private String avatar; private String description; private String source; private LocalDateTime createTime; private LocalDateTime updateTime; }Mapper接口三行搞定Mapper public interface PetInfoMapper extends BaseMapperPetInfo { }Service接口和实现类public interface PetInfoService extends IServicePetInfo { PagePetInfo pageWithFilter(int page, int size, String keyword, Integer status); } Service public class PetInfoServiceImpl extends ServiceImplPetInfoMapper, PetInfo implements PetInfoService { Override public PagePetInfo pageWithFilter(int page, int size, String keyword, Integer status) { LambdaQueryWrapperPetInfo wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.hasText(keyword), PetInfo::getPetName, keyword) .eq(status ! null, PetInfo::getStatus, status) .orderByDesc(PetInfo::getCreateTime); return this.page(new Page(page, size), wrapper); } }这段代码的精髓是LambdaQueryWrapper的条件拼装。like方法的第一个参数是个boolean值当keyword为空时整个条件直接不参与SQL拼接不需要你手动写if判断。这种写法比MyBatis的XML动态SQL直观得多也避免了字符串拼接SQL注入风险。RestController RequestMapping(/pet) public class PetController { Autowired private PetInfoService petInfoService; GetMapping(/page) public ResultPagePetInfo page(RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer size, RequestParam(required false) String keyword, RequestParam(required false) Integer status) { return Result.success(petInfoService.pageWithFilter(page, size, keyword, status)); } GetMapping(/{id}) public ResultPetInfo detail(PathVariable Long id) { return Result.success(petInfoService.getById(id)); } }Controller返回统一封装类Result结构是{code: 200, message: success, data: {...}}。这样前端axios拦截器只需要判断code不需要为每个接口写异常处理。3.3 领养申请业务流实现领养申请是整个系统业务复杂度最高的部分因为涉及状态流转和数据一致性。流程是用户提交申请 → 管理员审核通过/拒绝 → 通过后宠物状态变为已领养 → 用户确认接收后状态变为已完成。核心方法长这样Service RequiredArgsConstructor public class AdoptionApplyServiceImpl extends ServiceImplAdoptionApplyMapper, AdoptionApply implements AdoptionApplyService { private final PetInfoService petInfoService; Override Transactional(rollbackFor Exception.class) public boolean submitApply(AdoptionApply apply) { PetInfo pet petInfoService.getById(apply.getPetId()); if (pet null) { throw new BizException(宠物不存在); } if (pet.getStatus() ! 0) { throw new BizException(该宠物当前不可申请领养); } // 同一个人不能对同一宠物重复申请 long count this.count(new LambdaQueryWrapperAdoptionApply() .eq(AdoptionApply::getPetId, apply.getPetId()) .eq(AdoptionApply::getUserId, apply.getUserId()) .ne(AdoptionApply::getStatus, 3)); // 3为已取消 if (count 0) { throw new BizException(您已申请过该宠物的领养); } apply.setStatus(0); return this.save(apply); } Override Transactional(rollbackFor Exception.class) public boolean audit(Long applyId, Integer status, String remark) { AdoptionApply apply this.getById(applyId); if (apply null || apply.getStatus() ! 0) { throw new BizException(申请不存在或已处理); } if (status 1) { // 审核通过先更新申请状态再锁定宠物 PetInfo pet petInfoService.getById(apply.getPetId()); if (pet.getStatus() ! 0) { throw new BizException(宠物已被其他人领养请重新确认); } pet.setStatus(1); // 审核中状态相当于预订 petInfoService.updateById(pet); } else if (status 2) { // 拒绝原状态不变 } apply.setStatus(status); apply.setAuditRemark(remark); return this.updateById(apply); } }这个实现里有几个关键点Transactional(rollbackFor Exception.class)必须是默认策略的补充。Spring默认只对RuntimeException回滚事务如果你在业务方法里catch了业务异常又抛出Exception事务不会回滚。这里显式指定所有异常都回滚防止脏数据。状态校验放事务内做。尤其在audit方法里先查申请状态是否为待审核再查宠物当前状态两个检查都在同一个事务里避免并发审核导致同一只宠物被两个人同时领养。乐观锁没有硬上。这个系统的用户并发极低用事务加状态判断足够。如果以后要对接真实场景、领养申请并发暴涨就需要在pet_info表加version字段配合MyBatis-Plus的Version注解做乐观锁防止ABA问题。3.4 登录鉴权与权限控制登录这块没有引入Spring Security因为对这个体量的项目来说Spring Security的学习成本和配置复杂度都偏高。我自己写了一个轻量级JWT 拦截器方案Component public class AuthInterceptor implements HandlerInterceptor { private final JwtUtil jwtUtil; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { token token.substring(7); Long userId jwtUtil.parseToken(token); if (userId ! null) { request.setAttribute(userId, userId); return true; } } response.setStatus(401); return false; } }再定义一个RequireRole注解用于管理员接口Target({ElementType.METHOD, ElementType.TYPE}) Retention(RetentionPolicy.RUNTIME) public interface RequireRole { int[] value(); }在拦截器里读取用户token后进一步从UserService查出角色和RequireRole里的要求比对不匹配直接返回403。这套方案虽然简陋但对这个项目够了——它的核心价值在于让你理解“token校验”和“权限判断”是两个独立环节前端也许会漏配后端拦截是最后一道防线。4 前端开发Vue3实战记录4.1 Vue3项目结构与Composition API前端我用Vite作为构建工具创建相比vue-cli的webpackVite的开发服务器启动速度快得肉眼可见一个稍微大点的项目webpack冷启动可能要十几秒Vite基本秒开。项目结构如下src/ ├── api/ # 封装axios请求 │ ├── pet.js │ ├── apply.js │ └── user.js ├── components/ # 公共组件 ├── layout/ │ └── AdminLayout.vue ├── router/ │ └── index.js ├── store/ # 全站状态 ├── views/ │ ├── admin/ # 管理员页面 │ ├── volunteer/ # 志愿者页面 │ └── public/ # 公共页面 ├── utils/ │ ├── request.js # axios实例 │ └── auth.js └── main.jsVue3最核心的写法变化就是script setup语法糖配合Composition API。拿宠物列表页举例script setup import { ref, reactive, onMounted } from vue import { fetchPetPage } from /api/pet const loading ref(false) const petList ref([]) const total ref(0) const queryParams reactive({ page: 1, size: 12, keyword: , status: null }) async function loadPets() { loading.value true try { const { data } await fetchPetPage(queryParams) petList.value data.records total.value data.total } finally { loading.value false } } function handleSearch() { queryParams.page 1 loadPets() } onMounted(loadPets) /script这里有一个新手极容易踩的坑reactivevsref的选择。上面代码里queryParams我用reactive包petList和total我用ref包。原因是ref适合包装基本类型和独立的对象读取时要写.value模板里自动解包。reactive适合包装整个“响应式状态对象”比如表单数据、查询参数因为不需要每次queryParams.value.page这样写。如果你用ref包一个本来要用.访问内部属性的复杂对象代码里全是obj.value.xxx繁琐不说还容易忘记。反过来用reactive包基本类型值是不允许的比如let count reactive(0)是不生效的必须ref(0)。明白这两者的边界Vue3写起来就顺了。4.2 核心页面实现要点宠物卡片列表页是访客第一眼看到的页面设计成卡片流式每个卡片上突出显示宠物照片、名字、性别、年龄、状态标签。状态标签我直接对接后端返回的status字段在前端做一个映射const statusMap { 0: { text: 待领养, type: success }, 1: { text: 审核中, type: warning }, 2: { text: 已领养, type: info }, 3: { text: 治疗中, type: danger }, 4: { text: 已离世, type: danger } }然后在模板里{{ statusMap[pet.status].text }}配上Element Plus的Tag组件的type属性颜色和语义一次对应。领养申请表单要特别注意校验。宠物领养不是简单的表单提交有几个字段对审核决策影响很大居住环境、家人是否同意、养宠物经验。前端用了Element Plus的form组件的rules做必填和格式校验比如手机号必须11位、地址必填。提交前调formRef.validate()通过后才发送请求。管理后台表格页是管理员使用频率最高的页面。要关心的问题不是样式而是数据加载效率和操作便捷性。我在宠物管理页用了分页表格 顶部筛选区筛选条件有种类、状态、来源、关键字搜索全部条件通过queryParams对象传给后端后端用MyBatis-Plus动态拼SQL。这里我有一个经验表格的搜索按钮和重置按钮必须分开搜索是重新请求第一页重置是清空所有条件后重新请求。很多新手把重置写成queryParams {}直接导致分页页码等字段丢失。志愿者活动报名页比较简单清晰活动列表、活动详情、报名按钮三件套。报名时后端会校验是否已经报过前端只做友好提示。4.3 前后端数据交互与跨域处理所有请求统一走utils/request.js里封装的axios实例import axios from axios const http axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || /api, timeout: 10000 }) http.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) http.interceptors.response.use( response { const res response.data if (res.code ! 200) { // 业务错误统一提示 return Promise.reject(new Error(res.message)) } return res }, error { if (error.response?.status 401) { // 登录过期跳回登录页 localStorage.removeItem(token) window.location.href /login } return Promise.reject(error) } ) export default http跨域问题在开发环境用Vite的proxy解决不需要后端单独开放CORS// vite.config.js export default { server: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }这个配置的意思是前端页面请求/api/pet/page时Vite开发服务器会把这个请求转发到后端http://localhost:8080/api/pet/page。注意changeOrigin: true是必须的否则某些情况下请求头里的Host还是前端域名后端过滤器和框架可能会判断为跨域拒绝。生产环境一般用Nginx做同源代理把前端静态资源放在80端口、后端接口通过/api路径反向代理到8080这样浏览器只认一个域名天然没有跨域。部署细节下一节详细说。5 环境搭建、联调与部署实录5.1 MySQL8.0环境搭建Docker方式最省心项目文档里写了两套MySQL8.0环境搭建方案本机安装和Docker。我的建议是开发环境直接用Docker生产环境再用本机安装或云数据库。本地敲两行命令一个干净的MySQL8.0就出来了docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDroot123 \ -e MYSQL_DATABASEpet_org \ -v /home/user/mysql-data:/var/lib/mysql \ mysql:8.0解释一下参数-d后台运行--name mysql8容器名-p 3306:3306把容器内3306映射到宿主机3306-e MYSQL_ROOT_PASSWORDroot123设置root密码-e MYSQL_DATABASEpet_org启动时自动创建数据库-v挂载持久化目录这个必须加。不加的话容器删了数据全没了。启动后用docker exec -it mysql8 mysql -uroot -proot123进入容器验证能进说明环境OK。MySQL8.0的密码认证方式是caching_sha2_password跟MySQL5.7的mysql_native_password不同。如果你的JDBC驱动版本太老5.x会报Public Key Retrieval is not allowed。解决办法就是本项目这样用最新的com.mysql:mysql-connector-j驱动或者连接URL里加allowPublicKeyRetrievaltrue。5.2 后端打包启动与前端构建后端项目是标准Maven工程打包执行mvn clean package -DskipTests在target目录下会生成pet-org-system-0.0.1-SNAPSHOT.jar。启动命令就一行java -jar pet-org-system-0.0.1-SNAPSHOT.jar --spring.profiles.activeprod这里我建议所有配置项都走application.yml外部化——用application-prod.yml放生产配置启动时通过--spring.profiles.active指定环境。千万不要把数据库密码直接写在代码或配置文件里提交到git仓库用环境变量引用更安全比如spring: datasource: password: ${DB_PASSWORD}前端构建npm run build生成dist目录里面是纯静态文件。部署到Nginxserver { listen 80; server_name pet.example.com; # 前端静态文件 root /var/www/pet-frontend/dist; index index.html; # 解决Vue Router history模式刷新404 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; } }try_files $uri $uri/ /index.html;这行不能省。Vue Router用history模式时刷新一个内部路由比如/admin/pet如果Nginx找不到对应文件会返回404这行配置让Nginx把这种情况回退到index.html由前端路由接管。5.3 初始数据导入与功能验证项目自带一个db/pet_org.sql初始化脚本建表 插入初始数据一次性搞定mysql -uroot -proot123 pet_org db/pet_org.sql验证系统是否完整跑通我建议按下面这个清单走一遍步骤操作预期结果1管理员账号登录跳转管理后台token写入localStorage2新增一只宠物刷新列表能查到状态为待领养3普通用户注册并提交领养申请管理后台“待审核”列表出现该申请4管理员审核通过宠物状态自动变为审核中5用户确认接收宠物状态变为已领养记录完成6管理员发布活动志愿者端能看到可报名7捐赠记录录入统计页数据变化正确走完这七步说明系统的核心业务闭环是通着的。如果卡在中间某一步基本问题都出在接口联调或数据库状态机不一致排查思路放到下一节。6 常见问题与排查技巧实录6.1 新手最容易踩的坑Top6坑1MySQL8.0驱动类名错误。老教程里写的com.mysql.jdbc.Driver在8.0里已经废弃必须用com.mysql.cj.jdbc.Driver。如果不小心用错启动项目报ClassNotFoundException。排查方法先确认pom依赖是mysql-connector-j而非老坐标再核对配置。坑2MyBatis-Plus查询字段自动填充失效。如果实体字段有createTime、updateTime你希望它插入时自动填值需要在字段上加TableField(fill FieldFill.INSERT)注解并实现MetaObjectHandler处理器。否则你会发现新增记录后create_time是NULL。项目里我建议直接放到数据库层用DEFAULT CURRENT_TIMESTAMP兜底代码层也做了处理器双保险。坑3前端请求跨域报CORS错误。开发环境配了Vite proxy还报CORS绝大概率是你请求的URL没有走Vite代理——比如直接把http://localhost:8080/api/xxx写死在代码里前端实际是发到3000端口代理的两者不一致。确认baseURL是/api而不是完整的后端地址。坑4Vue3响应式丢失。最常见的是从接口拿到的数据直接赋值给ref对象却在模板里不显示。大概率是你用reactive包了一个数组然后整个数组重新赋值state.list res.data.records这在Vue3里会丢失响应式。正确做法是state.list.splice(0, state.list.length, ...res.data.records)或者干脆用ref。坑5时间格式8小时偏移。后端查出来的时间是UTC前端显示比北京时间早8个小时。排查方向数据库、JDBC连接URL、Jackson配置三处必须统一Asia/Shanghai。见3.1节配置三个地方我都写了。坑6Nginx部署刷新404。按5.2节配置try_files那行就是解药。忘记加的人前端路由切换没事一刷新白屏或404非常典型。6.2 几个排查思路的实操记录有一次用户反馈管理后台审核领养申请时明明点了通过但宠物状态没变。我第一时间怀疑是事务问题后来发现是updateById的乐观锁字段没配好。MyBatis-Plus的Version注解如果放在一个值为null的字段上更新时生成的SQL会带WHERE version ?但set语句没把version1导致第二次更新永远成功不了。排查方式是在控制台打开SQL日志log-impl: StdOutImpl看到输出的UPDATE语句就能立刻发现。另一个记录是前端志愿者活动页面搜索关键字后点重置表格不见了。原因是重置时把queryParams对象整个置空了后端收到size为nullSQL分页参数异常。后来我把重置逻辑改成只重置筛选字段保留page和sizefunction handleReset() { queryParams.keyword queryParams.status null queryParams.page 1 loadActivities() }6.3 项目后续扩展方向如果这个系统需要真正投放到动保组织使用我建议往三个方向扩展文件存储。目前宠物照片是用URL字符串存的部署时得自己准备静态资源服务器或把图片丢到Nginx目录。更好的方案是接入对象存储服务OSS或MinIO后端提供预签名上传接口前端直传能大幅减轻后端带宽压力。消息通知。领养审核结果、活动报名成功、公告发布这些场景目前只能靠用户主动刷新看。扩展一个通知中心模块或者在关键状态变更时调用邮件/短信接口体验会好很多。移动端适配。很多动保组织的志愿者是用手机工作的。前端Vue3代码可以复用业务逻辑用uni-app或直接把现有页面做成响应式让手机浏览器访问体验更好。管理后台的复杂表格在手机上还是受限可以考虑单独做一个小程序端。我个人在做这个项目时最大的体会是一个系统能不能被真正用起来不是看技术多花哨而是看业务流程有没有梳理清楚。宠物状态机、领养审核流转、捐赠台账这些业务规则的合理性直接决定了使用者愿不愿意长期用。技术栈只是工具真正花时间的还是把动保场景下的非标需求一点点结构化成代码逻辑。这个项目提供了一个相对完整的起点剩下的就是在真实使用中不断打磨了。