基于Spring Boot和微信小程序的灾情救助系统设计与实现
2026/9/17 4:43:19 网站建设 项目流程

简介:基于微信小程序与 Java 后端的灾情救助系统毕业设计项目包,面向高校毕业设计、课程设计与项目实战练习。系统分管理员与会员两类角色:管理员可维护会员、发布灾情公告与救助政策、管理论坛咨询及轮播图;会员可查看灾情视频公告、在线申请救助并上传材料、发布求助信息。包内共 1250 个文件,压缩包约 124.54MB,主要文件类型涵盖 png 图片资源、js/vue/java 前后端代码、json 配置、wxss/wxml 小程序样式与页面结构,并附带 sql 数据库脚本、mp4 演示录像、docx 说明文档和 bat 快捷运行脚本。已有 224 人学习/下载。借助完整源码、数据库及演示录像,可系统理解小程序端与 Java 后端、MySQL 数据库的交互逻辑,掌握救助申请、资讯发布、后台管理等模块的设计思路,便于二次开发和论文撰写。

1. 灾情救助系统的技术选型逻辑与适用边界

灾害发生后的最初几个小时,求助信息往往散落在微信群里,接龙消息被新信息冲掉,电话热线占线,纸质登记表无法同步给后台审核人员。把灾民的救助申请、材料提交、管理员审核、志愿者响应放进同一个闭环,需要的不只是一张在线表单,而是一套能承载角色权限、状态流转和数据留痕的系统。基于微信小程序加 Java 后端的灾情救助系统解决的就是这个问题:小程序端提供灾民侧的轻量入口,Java 后端承载数据校验、审核流转和管理端操作,MySQL 把申请记录、公告内容和用户信息固化成可追溯的结构化数据。

这套系统的典型场景是计算机类毕业设计和数据库课程设计,也可以作为应急管理信息化改造的参考原型。前后端分离的接口对接、状态字段的流转控制、文件上传的存储策略,都能在源码里找到对应的实现位置。后面按数据建模、后端接口、小程序端、验证改造四层推进。

2. MySQL核心表结构:灾民信息到救助申请的数据建模

2.1 核心表职责划分与关联关系

系统里「会员」对应的就是灾区群众,也是灾民。管理员在后台查看会员的基本信息、地址、手机和邮箱时,实际读取的就是 member 表。字段设计时要把「身份信息」和「联系方式」分开,因为救助申请的材料审核引用的是身份部分,求助信息的分发依赖的是联系方式部分,两类字段的更新频率和敏感级别不同。

业务主表是 member,救助申请表 relief_apply 和求助信息表 help_info 都通过 user_id 逻辑关联到它。admin_user 管理员表独立存在,不参与业务外键,因为管理员账号是初始化写入的,与灾民注册没有业务交集。轮播图表 carousel 和灾情公告表 disaster_notice 属于内容类表,由管理员在后台维护,小程序端只读。核心表的职责边界如下:

表名业务职责核心字段
member灾民注册信息与联系方式real_name, phone, address, id_card
admin_user管理员账号与角色username, password, role
relief_apply救助申请与审核材料user_id, apply_type, status
help_info求助信息与响应状态user_id, content, status
disaster_notice灾情公告与自救政策title, content, create_time
carousel首页轮播图配置image_url, sort_order, status

外键约束在课程设计阶段建议保留,但要注意一个边界:当管理员删除某个灾民账号时,救助申请记录不能被级联删除,否则审核留痕就断了。常见做法是表结构里保留 user_id 字段但不建物理外键,删除前先检查该用户是否已有申请记录,有记录就把账号标记为禁用而不是物理删除。

拿到压缩包后先执行 sql 目录下的建表脚本,再改后端 application.yml 里的数据库连接和密码,最后用微信开发者工具导入小程序端源码,才能保证三端跑的是同一套数据。

2.2 救助申请表的状态机字段与审核留痕

救助申请表是系统里状态流转最复杂的表。灾民提交申请、管理员审核、灾民查看结果,每一步都要留下记录。字段设计上,状态用 TINYINT 存状态码而不是字符串,原因有两个:一是列表页按状态过滤时整数索引扫描更快,二是避免中文状态在不同客户端出现编码差异导致查询匹配不上。

CREATE TABLE relief_apply ( apply_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '申请ID', user_id INT NOT NULL COMMENT '灾民ID,关联member表', apply_type VARCHAR(32) NOT NULL DEFAULT '物资' COMMENT '救助类型:物资/安置/医疗', materials_url VARCHAR(512) DEFAULT NULL COMMENT '申请材料附件地址', status TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0待审核,1通过,2驳回', audit_remark VARCHAR(255) DEFAULT NULL COMMENT '审核意见', audit_admin_id INT DEFAULT NULL COMMENT '审核管理员ID', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '提交时间', audit_time DATETIME DEFAULT NULL COMMENT '审核时间', INDEX idx_user (user_id), INDEX idx_status (status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='救助申请表';

status 的注释里已经把状态值定死,后续所有代码都按 0、1、2 三个数字判断,不允许在业务代码里出现三个数字之外的取值。audit_remark 和 audit_time 就是审核留痕,缺了这两个字段,灾民被驳回时不知道原因,管理员也无法追踪是谁在什么时间做的审核。课程设计的答辩环节,这两列的设计意图基本必问。

materials_url 只存文件路径,不存文件内容。小程序端上传材料时,后端先接收 MultipartFile 保存到本地 upload 目录或者对象存储,再把访问地址返回给前端,前端随申请表单一起提交,后端写入这条记录。这么做让表体积可控,以后要切换 MinIO 或 OSS 也不用改表结构。

2.3 求助信息表与轮播图的初始化数据要点

求助信息表虽然和救助申请表同属灾民操作,状态语义却完全不同。求助信息发布后进入公开列表,其他用户看到后响应并联系发布者,状态从「待响应」变成「已响应」,发布者确认得到帮助后变成「已解决」。这里要特别注意联系方式保护:列表页接口返回的 phone 应该是脱敏后的,完整号码只能通过详情接口按需获取。

CREATE TABLE help_info ( help_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '求助ID', user_id INT NOT NULL COMMENT '发布者ID', title VARCHAR(100) NOT NULL COMMENT '求助标题', content VARCHAR(1000) NOT NULL COMMENT '求助内容', phone VARCHAR(20) NOT NULL COMMENT '联系电话', status TINYINT DEFAULT 0 COMMENT '0待响应 1已响应 2已解决', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '发布时间', INDEX idx_status_time (status, create_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='求助信息表';

这里给 status 和 create_time 建了联合索引,因为列表页最常见的查询是「当前待响应的求助按发布时间倒序」,联合索引能同时过滤状态和排序,避免文件排序。初始化轮播图数据时,image_url 必须和项目里 upload 目录下的示例图片对应,否则首页渲染时图片 404。公告初始化数据可以放一条防灾自救指南,登录后直接能看到前台展示效果。admin_user 表初始化时写入管理员账号,密码字段存散列值,明文只出现在说明文档里。

3. Java后端:救助申请审核与求助分发的接口实现

3.1 Controller-Service-Mapper 分层与接口路径约定

后端基于 Spring Boot,接口层按业务域拆成 MemberController 和 AdminController 两个入口,分别对应小程序端和后台管理端。路径统一以 /api 开头,/api/member/** 带用户 token 访问,/api/admin/** 带管理员 token 访问,拦截器按路径前缀区分角色权限。

三层结构里,Controller 层只做参数接收和结果封装,Service 层处理业务状态流转,Mapper 层负责 SQL 操作。以救助申请提交接口为例,完整链路是小程序 POST 参数到 Controller,Controller 把 DTO 转成实体,Service 里补上 create_time 和初始 status,再调 Mapper 写入 relief_apply 表。把 SQL 写在 Controller 里的做法在课程设计里很常见,但会拖累后续加审核逻辑时的可维护性。

@RestController @RequestMapping("/api/member") public class MemberController { @Autowired private ReliefApplyService reliefApplyService; @PostMapping("/apply/submit") public Result submitApply(@RequestBody @Valid ApplySubmitDTO dto, @RequestHeader("token") String token) { Long userId = tokenService.getUserId(token); if (userId == null) { return Result.error(401, "登录状态已失效"); } return reliefApplyService.submit(userId, dto); } }

@Valid 触发 DTO 里的字段校验注解,比如 materialsUrl 不能为空、applyType 长度不超过 32,校验失败由全局异常处理器统一返回提示,不用在每个接口里写 if else。token 参数从请求头取,小程序端在 request 封装里已经塞进 header,后端拦截器解析出 userId 和角色。DTO 里不要直接放数据库实体类,否则前端多传一个 status 字段就能伪造审核状态。

3.2 救助申请审核的状态校验与幂等更新

管理员点击通过或驳回时,后端要做的不只是 UPDATE 状态,还要确认当前状态允许流转以及审核操作只能执行一次。否则两个管理员同时打开审核列表、先后提交审核操作,后一个会覆盖前一个的审核意见。演示环境里出现一次覆盖,整套系统的可信度就崩了。

@Transactional public Result audit(AuditRequest req) { ReliefApply apply = reliefApplyMapper.selectById(req.getApplyId()); if (apply == null) { return Result.error("申请记录不存在"); } if (apply.getStatus() != 0) { return Result.error("当前状态不可审核,请刷新列表"); } apply.setStatus(req.getAuditResult()); apply.setAuditRemark(req.getRemark()); apply.setAuditAdminId(req.getAdminId()); apply.setAuditTime(new Date()); reliefApplyMapper.updateById(apply); return Result.success(); }

逻辑顺序是先查出来看状态,再更新。这里有一个并发边界:两次审核请求同时进来,都读到 status=0,都会执行更新。课程设计层面可以接受,想做得严谨就在 UPDATE 语句里带上 status=0 条件,用数据库行锁兜底:

UPDATE relief_apply SET status = #{newStatus}, audit_time = NOW() WHERE apply_id = #{applyId} AND status = 0;

受影响行数为 0 说明状态已被其他管理员更新,事务回滚即可。注意 @Transactional 必须加在业务方法上,同时 Service 层调用 Mapper 的 update 方法时不能把返回结果丢掉,否则无法根据行数判断是否冲突。auditResult 的取值在 DTO 里用注解限定只能是 1 或 2,0 和 null 都会在进入方法前被拦截。

3.3 求助信息列表的脱敏查询与分页参数

求助信息列表是高频读接口,首页、发现页、搜索都会调用。分页参数 pageNo 从 1 开始,pageSize 固定 10,后端用 PageHelper 或者手写 LIMIT 都可以。列表数据里的手机号必须脱敏,完整号码只能在用户点击「提供帮助」后通过详情接口按需返回。

SELECT help_id, title, content, CONCAT(LEFT(phone, 3), '****', RIGHT(phone, 4)) AS masked_phone, create_time FROM help_info WHERE status = 1 ORDER BY create_time DESC LIMIT #{offset}, #{pageSize};

offset 由后端通过 (pageNo-1) * pageSize 计算,前端不要直接传 offset,否则页码和条数不一致时会出现重复或漏页。status=1 表示当前可响应的求助,已解决的求助不再出现在列表里,避免堆满历史数据。脱敏在 SQL 里做不会影响查询性能,因为 phone 没有参与 WHERE 条件,只在 SELECT 投影列里用函数处理。

搜索场景可以加 keyword 参数,WHERE 条件改成 title LIKE CONCAT('%', #{keyword}, '%')。演示数据几百条没问题,真实数据量大时 LIKE 前导通配符会让索引失效,需要换成全文索引或者 Elasticsearch,这一点到第五章的生产化改造里展开。

4. 微信小程序端:轮播图加载、公告渲染与材料上传联动

4.1 app.js 全局配置与 request 请求封装

小程序端用微信开发者工具打开项目后,先看 app.js 里的 globalData。baseURL 决定所有接口的请求地址,本地联调时是 localhost,真机预览必须改成电脑的局域网 IP,否则手机访问不到 Java 后端。token 在登录成功后写入 globalData,同时通过 wx.setStorageSync 持久化一份,冷启动时从 Storage 恢复。

// app.js App({ globalData: { baseURL: 'http://localhost:8080/api', token: '' }, request(path, method, data) { return new Promise((resolve, reject) => { wx.request({ url: this.globalData.baseURL + path, method: method || 'GET', data: data || {}, header: { 'Content-Type': 'application/json', 'token': this.globalData.token }, success: res => resolve(res.data), fail: err => reject(err) }); }); } });

request 封装把 wx.request 的细节收敛到一个函数里,页面里调用 getApp().request('/notice/latest') 就能拿到 Promise 结果。header 里的 token 是身份凭证,后端拦截器靠它区分管理员和会员。注意 wx.request 的 Content-Type 只影响请求体的序列化方式,上传文件要换成 wx.uploadFile 走 multipart/form-data,没法直接复用这个 request 函数。

4.2 首页轮播图和公告列表的并行加载

首页是小程序的门面,轮播图来自 carousel 表,按 sort_order 升序展示;公告列表来自 disaster_notice 表,按时间倒序取最新五条。两个接口相互独立,用两个 Promise 并行请求,不要串行 await,否则一次网络往返变成两次,弱网环境下感官差异明显。

onLoad() { const app = getApp(); app.request('/carousel/list', 'GET').then(res => { if (res.code === 200) { this.setData({ banners: res.data }); } }); app.request('/notice/latest?limit=5', 'GET').then(res => { if (res.code === 200) { this.setData({ notices: res.data }); } }); }

分两次 setData 是有意的,banners 和 notices 的数据来源和更新时机不同,合并成一个对象反而让后续局部刷新变得困难。注意首页如果有 tab 页签,onLoad 只在首次进入时触发一次,切后台再回前台要用 onShow 重新拉取公告列表,否则公告更新后首页还显示旧数据。

模板里 swiper 组件的 autoplay 控制自动播放,interval 建议 3000 到 5000 毫秒,太短用户来不及阅读。image 组件的 mode 设成 aspectFill 防止图片被拉伸变形,加载失败时要在 binderror 事件里替换成默认兜底图。首页也是项目答辩时第一眼看到的页面,轮播图空数据或者破图会直接影响评委印象。

4.3 救助申请材料上传与表单提交

申请救助页是交互最重的页面。用户先选材料图片,再填申请类型,最后提交。上传动作发生在表单提交之前,而不是随表单一起提交,这样用户上传失败时可以单独重试,不用重新填写整个表单。

uploadMaterial() { const app = getApp(); wx.chooseMedia({ count: 3, mediaType: ['image'], success: (res) => { const filePath = res.tempFiles[0].tempFilePath; wx.uploadFile({ url: app.globalData.baseURL + '/upload', filePath: filePath, name: 'file', formData: { token: app.globalData.token }, success: (uploadRes) => { const data = JSON.parse(uploadRes.data); this.setData({ materialUrl: data.url }); } }); } }); }

count 限制 3 张,对应后端接口的单次接收数量,材料太多会拖慢审核效率。name 字段值 file 必须和后端 @RequestParam("file") 的参数名一致,不一致时直接报 400。token 通过 formData 传递而不是 header,是因为 wx.uploadFile 的 header 只能放字符串且无法自定义 Content-Type,multipart 表单里 token 放 formData 最稳。

上传成功拿到 materialUrl 后,表单提交时把它一并 POST 到 /api/member/apply/submit。提交按钮要加 disabled 状态,materialUrl 为空时按钮置灰,防止用户不传材料就提交后,后端再返回「材料不能为空」的提示。前端先拦截,比后端报错再回显体验好得多。

5. 权限边界、审核闭环与系统改造验证

5.1 接口越权拦截与会话保持

小程序端登录后拿到的 token 只代表「当前用户是会员」,不代表能访问所有接口。后端拦截器按路径前缀做角色判断是最直接的方式:/api/admin/** 必须有管理员角色,/api/member/** 只要有有效 token 即可。容易漏掉的是 /api/upload 文件上传接口,如果放在 admin 路径之外,会员可以上传任意文件,攻击面就打开了。文件类型校验不能只在前端做,后端要按扩展名和文件头双重校验,只认 jpg、png、pdf。

5.2 全链路验证的三个关键节点

拿到源码后先按演示录像跑通完整流程,重点验证三个节点。第一,小程序端注册新账号后,member 表新增记录且密码是密文。第二,申请救助时上传材料,upload 目录出现文件,relief_apply 表写入记录且 status 为 0。第三,管理员后台审核通过后,小程序端个人中心能看到状态变成已通过。

提示:演示录像里的登录账号和数据库初始化脚本里的 admin 密码是对应的,改密码之前先确认登录页的说明文档。

验证时直接查库:

SELECT apply_id, user_id, status, audit_time FROM relief_apply ORDER BY apply_id DESC;

如果审核操作之后 audit_time 还是 NULL,说明 UPDATE 没执行成功,优先检查事务是否回滚、updateById 是否把 null 字段也写进去了。MyBatis-Plus 默认只更新非空字段,audit_time 必须显式 set 再更新,否则这条记录永远没有审核时间。

5.3 生产化改造的优先级建议

三处改造按性价比排序。文件存储换成 MinIO 或 OSS,解决本地 upload 目录在服务器重启后文件丢失的问题,实现上只改上传接口的存储逻辑,数据库不用动。公告和轮播图加 Redis 缓存,key 用 notice:latest 和 carousel:index,失效时间 5 分钟,灾情公告是低频变更数据,缓存命中率接近 100%。求助状态变更加订阅消息通知,审核通过时主动触达灾民,不用用户反复刷新页面。

这三个方向分别对应存储、缓存和消息,任何一个做完都可以写进论文的创新点里,而且不改变现有表结构和接口路径,改造风险可控。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询