ARTICLE DETAIL

资讯详情

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

SpringBoot+微信小程序流浪动物救助系统:状态流转与并发控制实战

SpringBoot+微信小程序流浪动物救助系统:状态流转与并发控制实战 简介面向高校计算机专业毕业设计的流浪动物救助数据库项目使用SpringBoot框架与微信小程序技术构建覆盖后端Java服务、小程序前端展示、数据库表结构及完整部署说明。系统围绕流浪动物信息登记、领养申请与救助记录等核心功能展开能帮助开发者深入理解前后端分离架构、小程序生命周期及接口联调流程。压缩包共681个文件以97个Java源码、96个Vue页面、61个JavaScript脚本、22个WXSS样式与21个WXML模板等小程序开发文件为主附有演示视频、一键启动脚本、SQL初始化文件和多种图标素材整体体积约49.97MB目录组织层次分明便于按模块检索学习。项目已在Windows10/11环境严格调试答辩评审获得97分配套使用文档、部署教程及全部项目资料下载配置后即可运行既适用于毕业设计参考也能作为期末课程作业的完整示例。目前已有208人学习下载对于正在寻找全栈实战项目或入门小程序开发的Java学习者这套项目能提供从代码到部署的完整参考。1. 流浪动物救助程序难点不是CRUD而是状态流转拿到这个压缩包第一眼看到的是五样东西SpringBoot后端、微信小程序端、救助数据库、使用文档、演示视频。毕业设计做成这个标题真正要回答的问题不是“有多少张表”而是当一只动物同时有三个人提交领养申请时系统怎样保证只有一个人能领养成功。我第一次做这类项目也会先把动物档案、领养申请、救助记录各建一张表跑起来之后才发现状态字段不一致会带来一堆脏数据。这篇文章会把数据库怎么设计、SpringBoot接口怎么把状态流转锁住、小程序端怎么把列表和表单串起来讲清楚适合准备SpringBoot框架面试题时想找一个完整业务链路的读者也适合开始接触微信小程序开发想要一个能答辩的项目样板的人。2. 拆解救助数据库从ER图到SpringBoot实体与表结构做数据库课程设计的时候习惯先从ER图开始。流浪动物救助这个业务域第一版只需要四张表动物档案、领养申请、救助记录、小程序用户。实体之间的关系不复杂真正的复杂度来自“状态联动”领养申请审核通过时动物要从待领养变成已领养同时该动物其他待审核的申请必须全部作废。这个动作如果只在页面里改一个字段数据很快就会脏。表名业务含义关键字段animal动物档案id, name, species, gender, status, cover_url, locationadopt_application领养申请id, animal_id, user_id, applicant_name, phone, statusrescue_record救助记录id, animal_id, rescuer_name, phone, address, descriptionsys_user小程序用户id, openid, nickname, phone, role救助记录和动物档案是逻辑上的多对一一只动物可能被同一个人救助过多次救助记录只负责留痕不参与领养状态判断。领养申请和动物档案是多对一和用户也是一对多。这个项目不建物理外键用逻辑外键加索引就够了否则小程序端删除种子数据的时候会频繁触发外键约束演示反而卡住。2.1 救助域的三类核心实体与关系实体关系理清之后第二件事是给状态字段定边界。动物状态和申请状态不能混在一个枚举里否则业务逻辑会越来越绕。我的建议是动物表单独维护 status领养申请表单独维护 status两个状态通过业务规则同步。2.1.1 动物状态与申请状态的边界动物状态建议用四个值0 待审核、1 待领养、2 已领养、3 已下架。领养申请状态用四个值0 待审核、1 已通过、2 已拒绝、3 已取消。救援人员提交救助记录后默认进入待审核管理员审核通过后变成待领养用户提交领养申请后管理员审核通过动物状态变成已领养同时把其他待审核申请置为已拒绝。这里最容易错的是认为“动物状态 最新一条申请状态”。实际上申请状态只是流程单据状态动物状态才是业务状态。比如动物体检不过被下架状态变成 3这时它的待审核申请仍然可能是 0需要服务层在下架时统一处理。把状态边界写在表注释里比写在代码注释里更有效因为建表脚本会被多人反复导入。2.2 用Navicat建表的字段与索引设计建表脚本可以直接导入Navicat执行。我一般会关闭外键检查避免初始化顺序导致导入失败。下面是核心的建表语句已经去掉了不必要的冗余字段CREATE TABLE animal ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL DEFAULT COMMENT 动物昵称, species VARCHAR(20) NOT NULL COMMENT cat/dog/rabbit, gender TINYINT NOT NULL DEFAULT 0 COMMENT 0未知 1公 2母, age_months INT NOT NULL DEFAULT 0, health_status VARCHAR(100) NOT NULL DEFAULT 待体检, location VARCHAR(120) NOT NULL DEFAULT COMMENT 发现地或收容地址, cover_url VARCHAR(255) NOT NULL DEFAULT , status TINYINT NOT NULL DEFAULT 0 COMMENT 0待审核 1待领养 2已领养 3已下架, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_status_create (status, create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT动物档案; CREATE TABLE adopt_application ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, animal_id BIGINT UNSIGNED NOT NULL, user_id BIGINT UNSIGNED NOT NULL, applicant_name VARCHAR(50) NOT NULL, phone VARCHAR(20) NOT NULL, reason VARCHAR(500) NOT NULL DEFAULT , housing_info VARCHAR(500) NOT NULL DEFAULT COMMENT 居住情况, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待审核 1已通过 2已拒绝 3已取消, apply_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, audit_time DATETIME DEFAULT NULL COMMENT 审核时间, KEY idx_animal_status (animal_id, status), KEY idx_user_apply (user_id, apply_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT领养申请表;id 使用 BIGINT UNSIGNED避免以后数据量超过 INT 上限。状态字段用 TINYINT 而不是 INT节省行空间InnoDB 行越小单页能存的记录越多列表查询越占优势。create_time 和 update_time 交给数据库默认值维护Java 实体里只做读取不手动 set。索引方面首页列表最常见的查询是where status ? order by create_time desc所以建联合索引 idx_status_create领养审核页最常见的查询是where animal_id ? and status ?所以建 idx_animal_status。这里不建议在 animal_id 和 status 上建唯一索引因为业务规则要求的是“同一动物只有一个状态为 0 的待审核申请”MySQL 普通唯一索引做不到只对某个状态值唯一。要不依赖数据库约束把这个校验放到 Service 层事务里逻辑更直观。2.3 MyBatis-Plus还是Spring Data JPA选型与代码落地做 SpringBoot 项目ORM 选 MyBatis-Plus 最常见。理由很实际单表 CRUD 完全不用写 SQL继承 BaseMapper 就有 selectById、selectPage、updateById联表查询写 Xml 也能接住。Spring Data JPA 的派生查询写起来也省事但面试时导师更容易追问代理对象、懒加载、N1 问题毕业设计时间紧的话没必要给自己加戏。pom 里引入 mybatis-plus-boot-starter 就行版本选你之前跑通过的一个 3.5.x不要追最新SpringBoot 版本太高反而会出现分页插件拦截器不兼容的情况。实体类和表字段做映射注意 MyBatis-Plus 默认把驼峰转下划线所以 Java 属性写 createTime表字段写 create_time不需要额外配置。TableName(animal) public class Animal { TableId(type IdType.AUTO) private Long id; private String name; private String species; private Integer gender; private Integer ageMonths; private String healthStatus; private String location; private String coverUrl; private Integer status; private LocalDateTime createTime; private LocalDateTime updateTime; }接口层再写一个 Mapper 接口继承 BaseMapper。TableName指定表名TableId(type IdType.AUTO)对应自增主键。animal 表里有 localDateTime 字段数据库 URL 参数里记得配 serverTimezoneAsia/Shanghai否则本机时间和服务器时间对不上演示视频里会出现“刚提交的申请时间比当前时间晚 8 小时”这种尴尬。3. SpringBoot后端救助状态机与小程序端API实现后端接口设计最怕两种极端一种是按页面拆接口比如 getIndexData、getDetailData另一种是每个表单独暴露 CRUD小程序端可以直接改任意字段。正确做法是把接口按业务流程拆小程序端只能看到自己需要的操作比如提交领养申请、查询申请记录、上传救助信息不能直接跨表改状态。3.1 REST API 的路径设计与统一返回体接口路径建议这样划分方法路径说明GET/api/animals?statusspeciespage1size10分页查询动物列表GET/api/animals/{id}查询动物详情POST/api/adopt/apply提交领养申请PUT/api/adopt/{id}/audit?passtrue管理员审核申请GET/api/rescue/records查询救助记录统一返回体使用一个 Result 泛型类code 为 0 表示成功非 0 表示业务失败。不要直接用 HTTP 状态码做业务判断因为小程序端和后端之间的网关可能会重写状态码而且 200 之外的响应在 wx.request 里也能拿到但处理逻辑会碎。public class ResultT { private int code; private String msg; private T data; public static T ResultT ok(T data) { ResultT r new Result(); r.code 0; r.msg ok; r.data data; return r; } public static T ResultT error(int code, String msg) { ResultT r new Result(); r.code code; r.msg msg; return r; } }写 Controller 的时候方法参数用 RequestParam 给默认值分页参数 page 和 size 一定不能让用户传负数。MyBatis-Plus 的 Page 对非法值会做修正但业务上最好在入口就挡住。GetMapping(/animals) public ResultPageAnimal list(RequestParam(defaultValue 1) long page, RequestParam(defaultValue 10) long size, RequestParam(required false) Integer status) { LambdaQueryWrapperAnimal wrapper Wrappers.lambdaQuery(); wrapper.eq(status ! null, Animal::getStatus, status) .orderByDesc(Animal::getCreateTime); return Result.ok(animalMapper.selectPage(new Page(page, size), wrapper)); }eq 方法第一个参数是布尔条件status 为 null 时不拼这个条件避免出现where status null。orderByDesc 直接用 Lambda 属性引用不会因为字段改名而漏改字符串。3.2 领养申请的状态流转实现事务与行锁审核领养申请是整个项目最核心的业务。很多人只写了普通 select再 update并发时会出大问题。两个管理员同时审核同一只动物的不同申请都查到动物状态是待领养于是两个都通过动物就被“双领养”了。解决办法是先锁行再判断。Transactional(rollbackFor Exception.class) public void auditApply(Long applyId, boolean pass) { AdoptApplication apply adoptApplicationMapper.selectById(applyId); if (apply null || apply.getStatus() ! 0) { throw new BusinessException(申请不存在或已审核); } Animal animal animalMapper.selectByIdForUpdate(apply.getAnimalId()); if (animal null || animal.getStatus() ! 1) { throw new BusinessException(该动物当前不可领养); } if (pass) { animal.setStatus(2); animalMapper.updateById(animal); apply.setStatus(1); apply.setAuditTime(LocalDateTime.now()); adoptApplicationMapper.updateById(apply); LambdaUpdateWrapperAdoptApplication update Wrappers.lambdaUpdate(); update.eq(AdoptApplication::getAnimalId, apply.getAnimalId()) .eq(AdoptApplication::getStatus, 0) .ne(AdoptApplication::getId, applyId) .set(AdoptApplication::getStatus, 2); adoptApplicationMapper.update(null, update); } else { apply.setStatus(2); apply.setAuditTime(LocalDateTime.now()); adoptApplicationMapper.updateById(apply); } }selectByIdForUpdate 不是 MyBatis-Plus 自带方法直接写在 AnimalMapper 上即可Select(SELECT * FROM animal WHERE id #{id} FOR UPDATE) Animal selectByIdForUpdate(Param(id) Long id);这段逻辑有四个关键点。第一先锁 animal 行再查申请和更新申请锁顺序固定避免两个事务各自锁了不同行然后互相等待的死锁。第二FOR UPDATE会把 animal 这一行锁到当前事务提交第二个并发事务执行同样的 selectForUpdate 会阻塞直到第一个事务提交后重新读取此时 animal.status 已经是已领养进入“不可领养”分支。第三Transactional 默认只对 RuntimeException 回滚这里指定 rollbackFor Exception.classBusinessException 也是继承 RuntimeException所以业务异常同样触发回滚。第四批量把其他申请置为 2 时用 ne 排除当前审核通过的这一条避免把自己的申请重新写成已拒绝。3.2.1 为什么不直接用乐观锁乐观锁也可以做比如 animal 表加 version 字段update 时带 version 条件影响行数为 0 就失败。但这种情况需要前端重试或者后端循环重试逻辑分发到多个入口后很容易漏。单机部署的毕业设计悲观锁反而清晰锁的那一行代码就代表了“这只动物同时只有一个审核事务能操作”。如果你面试被问到高并发可以补充说生产环境会把锁上移到 Redis但演示时不用。3.3 SpringBoot配置、yml密文与上传参数从 zip 里拿到项目后最常改的就是 application.yml。数据库密码、文件上传大小、MyBatis-Plus 映射配置都在这一个文件里。建议密码用 Jasypt 加密不要明文提交到压缩包。spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/animal_shelter?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: ENC(这里放密文) servlet: multipart: max-file-size: 5MB max-request-size: 20MB mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 jasypt: encryptor: password: ${JASYPT_PASSWORD}启动时指定环境变量java -jar app.jar --JASYPT_PASSWORD你的密钥。这里的密码不是数据库密码是 Jasypt 加密用的盐配置文件里的 ENC() 密文必须用它才能解开。multipart 配置只控制 SpringBoot 接受的文件大小如果项目前端用 Nginx 转发上传请求Nginx 的 client_max_body_size 也要同步放开否则前端报 413但后端日志里什么都没有。SpringBoot 版本如果特别新jasypt-spring-boot-starter 要用适配 SpringBoot 3 的分支否则启动会直接报自动配置类不存在。这种问题很典型属于“springboot版本太高”导致的依赖兼容性踩坑。不想引 Jasypt 的话另一个更简单的方案是数据库密码不写进 yml用环境变量password: ${DB_PASSWORD}部署时在系统环境变量里设置同样能避免明文入库。3.4 接口调试从Postman脚本到Burp Suite抓取小程序请求后端接口写完先用 Postman 过一遍最基础的 CRUD确认返回结构是 Result 的 JSON 格式。然后在小程序开发者工具里把 BASE_URL 指向本机局域网 IP真机预览时手机和电脑连同一个 WiFi后端要在 application.yml 里加server.address0.0.0.0否则真机访问不到。如果需要查看小程序实际发出去的请求体、响应体、请求头可以用 Burp Suite。常见做法是Burp Suite 里新建一个监听 127.0.0.1:8080 的项目微信开发者工具右上角“详情 - 本地设置”里把请求流量指向这个地址再安装并信任 Burp 的 CA 证书就能在 Burp 里看到 wx.request 从构建请求到收到响应的完整过程。这里要注意抓包工具只能帮你看请求不能帮你改数据库调试状态联动还是要在后端日志里打关键节点。4. 微信小程序端列表、详情、状态更新与表单校验小程序端更接近一个展示和录入层业务规则尽量不放前端。列表页从 /api/animals 拉数据详情页打开后展示动物档案和一个“申请领养”按钮申请页提交表单个人中心查看我的申请记录和审核状态。做到这四个页面项目演示就已完整。4.1 小程序目录结构与导航栏适配小程序端目录结构建议这样组织miniprogram/ app.js app.json pages/ index/ detail/ apply/ mine/ utils/ request.js images/app.json 里 pages 数组的第一项是启动页。很多新手遇到每次编译都进错页面问题就出在这里。要修改刚进入的加载页面直接调整 pages 数组顺序而不是去 project.config.json 里找入口。顶部导航栏高度是小程序开发里的高频问题。如果 app.json 里设置了自定义导航navigationStyle: custom页面顶部状态栏会空出一块直接写死 px 会在不同机型上错位。正确做法是拿系统状态栏高度和胶囊按钮位置计算function getNavInfo() { const windowInfo wx.getWindowInfo(); const menu wx.getMenuButtonBoundingClientRect(); const navBarHeight (menu.top - windowInfo.statusBarHeight) * 2 menu.height; return { statusBarHeight: windowInfo.statusBarHeight, navBarHeight: navBarHeight, menuHeight: menu.height }; }这个公式把胶囊按钮到状态栏的距离翻倍加上胶囊自身高度就是自定义导航栏的完整高度。用它去设置容器 paddingTop页面内容就不会被刘海屏和状态栏遮挡。4.2 用Promise封装wx.request并处理加载页wx.request 默认回调风格页面一多会出现多层嵌套。先封装一层 Promise后续页面统一调 request 方法返回体已经帮终端把 Result 拆好了。const BASE_URL http://192.168.1.100:8080/api; function request(url, method GET, data {}) { return new Promise((resolve, reject) { wx.request({ url: ${BASE_URL}${url}, method: method, data: data, header: { Content-Type: application/json }, success: (res) { if (res.data.code 0) { resolve(res.data.data); } else { wx.showToast({ title: res.data.msg || 请求失败, icon: none }); reject(res.data); } }, fail: (err) { wx.showToast({ title: 网络异常, icon: none }); reject(err); } }); }); } module.exports { request };加载页的体验问题是列表页首次进入容易白屏。原因很简单onLoad 发请求是异步的页面 onReady 先执行数据回来后 setData 才触发渲染。处理办法是在 onLoad 里 wx.showLoading在请求结束后的 finally 里 wx.hideLoading。如果用了自定义导航栏wx.showNavigationBarLoading 不显示所以不要依赖它。4.3 表单提交与radio-group单选框的坑申请页的表单至少要包含姓名、电话、申请理由、居住情况。物种和性别筛选在列表页用 radio-group 实现WXML 写法radio-group bindchangeonSpeciesChange label wx:for{{speciesList}} wx:keyvalue classspecies-item radio value{{item.value}} checked{{item.checked}} color#07c160 / text{{item.label}}/text /label /radio-group对应的 JSdata: { speciesList: [ { value: cat, label: 猫, checked: true }, { value: dog, label: 狗, checked: false } ] }, onSpeciesChange(e) { const selected e.detail.value; this.setData({ selectedSpecies: selected }); }这里有个典型坑radio 的 checked 必须是布尔值如果后端返回的 0/1 直接放进 WXML字符串false在部分基础库版本里会被当作选中态导致两个单选框同时高亮。正确做法是在接口返回后手动转换成布尔值。表单提交时手机号校验用正则const phoneReg /^1[3-9]\d{9}$/; if (!phone || !phoneReg.test(phone)) { wx.showToast({ title: 请输入正确的手机号, icon: none }); return; }校验通过后再调 request 提交提交按钮加 disabled 状态防止用户连续点击产生两条申请这样后端就算没加防重前端也挡住了大部分重复请求。4.4 小程序与web-view内嵌H5的页面坑有的项目会把救助协议或多页面的免责声明用 web-view 内嵌 H5。常见问题有两个一个是“微信小程序内嵌H5工具栏左侧返回箭头没有了”还有一个是 H5 页面内跳转后返回层级错乱。出现返回箭头消失先检查承载 web-view 的那个页面的导航栏配置。如果这个页面设置了自定义导航返回箭头会被自己的自定义按钮盖住或覆盖web-view 全屏时原本由宿主提供的导航 UI 就不稳定。解决办法是不对 web-view 页面开自定义导航如果必须自定义就在 H5 页面用 wx.miniProgram.postMessage 告诉小程序原生层手动控制返回。另一个做法是 H5 内部用 history.back() 自己维护返回而不依赖小程序的导航栈。web-view 和业务页通信时建议把参数通过 URL 查询串传递比如/pages/webview/webview?urlxxxanimalId1这样 H5 侧原生 JS 拿到的参数和后端日志里的请求参数能一一对上排查问题也容易。5. 用演示视频反推验收清单从数据库脚本到高分答辩一个高分项目的演示视频不是随便点两下就行的它其实暴露了完整验收路径。拿到 zip 后先看演示视频里做了什么再反过来检查自己能不能复现这个顺序比直接看代码更高效。5.1 演示视频里必须出现的4个场景演示场景操作步骤答辩可说点首页列表进入小程序按状态筛选动物分页、状态过滤、数据库索引提交领养申请打开详情填写表单提交参数校验、申请写入审核通过后端或管理端调用审核接口行锁、事务、状态联动数据复查Navicat 查询申请和动物两张表索引设计、逻辑外键取舍演示视频里如果有管理端页面那么管理端代码也必须在 zip 中存在。很多资料包会混入两个版本的后端演示视频里调用的接口和当前代码对不上答辩时现场操作直接露馅。5.2 数据库初始化脚本的三个检查点先执行命令导入数据库mysql -u root -p animal_shelter init.sql如果导入报错优先看三个检查点。第一init.sql 里是否有 CREATE DATABASE 和 USE保证库存在第二表是否都用了 InnoDB 和 utf8mb4整库导出的脚本常见 DEFINER 绑定原用户需要手动去掉第三演示数据覆盖 status 的 0、1、2 三种状态没有已领养数据就没法演示审核通过后的联动效果。5.3 从zip到能run的最后一公里使用文档如果照着做还是起不来大概率卡在三个本地差异application.yml 里的数据库密码不对、小程序 appid 不是自己的、BASE_URL 写的 localhost。逐项改成自己的环境后后端执行mvn spring-boot:run前端在微信开发者工具里导入项目本地开发时勾选“不校验合法域名”真机预览再改成局域网 IP。把这一套动作在答辩前亲手走一遍从数据库脚本到演示视频里最后一个点击能全部复现项目基本不会卡在半路。本文还有配套的精品资源点击获取
返回列表