
简介这是一份基于微信小程序的精准扶贫数据收集平台毕业设计项目适合计算机相关专业毕业生、Java后端开发者及小程序学习者参考。系统围绕贫困对象精准画像、帮扶记录与收入管理、扶贫新闻发布等核心模块展开通过小程序端完成数据采集与政策宣传后端采用Java提供接口支持完整呈现互联网精准扶贫业务流程。压缩包共503个文件包含Java源码、编译文件、小程序前端页面、数据库脚本、图片及演示视频等整体约106.62MB目录结构清晰便于按功能模块逐一学习。已有229人学习浏览。除源码和数据库外还附有演示视频与项目文档可帮助理解系统设计、运行流程与接口调用逻辑适合用于二次开发、功能扩展及毕业设计答辩准备。1. 微信小程序做数据收集先想清楚断网和重复提交“入户调查时信号不好数据怎么交”这是数据收集类小程序最真实的需求起点。毕业设计题目写成「基于微信小程序的精准扶贫数据收集平台」时技术栈基本是固定答案小程序原生前端、JavaSpring Boot后端、MySQL 数据库再加上源码压缩包里附的建表脚本和演示视频。但代码怎么组织、接口怎么设计、数据如何保证不丢不重才是答辩时真正会被追问的几个点。下文按一条能复现的路径展开从选型和表结构到小程序端表单、图片上传再到 Java 后端接口与校验最后给出部署与演示时的常见坑。2. 微信小程序 Java 后端的技术选型与整体架构边界2.1 原生小程序还是 uniapp数据收集场景的取舍「基于微信小程序」这个措辞其实没限定必须用原生语法uniapp 编译成小程序同样是路径。但数据采集高度依赖相机、上传、本地缓存三类 API我更倾向原生小程序wx.chooseMedia、wx.uploadFile、wx.setStorage在原生环境里的返回结构最直接调试时不用再隔一层编译产物。uniapp 的写法对 Vue 熟练的同学友好uni.chooseImage和uni.setStorageSync也能做到同样业务但一旦在真机上出现图片上传 401排查顺序要多跳一层。不是非此即彼毕业设计要的是演示稳定和源码易读原生更省事。2.2 功能模块与数据流谁在录、谁在审、落到哪张表模块小程序端服务端数据落库登录wx.login 获取 code/api/loginsys_user家庭档案表单新增/api/householdhousehold走访记录文本图片/api/visitvisit_record、attachment统计页面列表/聚合数据/api/stats/region聚合查询这个结构把业务边界划得很细家庭档案是基础底档走访记录是动态变化数据附件单独成表。数据流是「调查员在户外录档案 → 现场拍照 → 文本和图片分别提交 → 汇总页面按地区/月份查询」。图片先传拿到 URL文本再提交避免一次请求负载过大反过来会容易超时尤其弱网环境。2.3 源码包怎么编排目录与职责分离poverty-data-collect ├── miniprogram │ ├── pages │ │ ├── login/ │ │ ├── household/ │ │ ├── visit/ │ │ └── stats/ │ ├── utils/request.js │ ├── utils/upload.js │ └── app.json ├── server │ ├── src/main/java/com/example/collect │ ├── src/main/resources │ │ ├── application.yml │ │ └── sql/schema.sql │ └── pom.xml └── doc ├── 演示视频.mp4 └── 系统使用说明.md源码包解压后我通常采用这样的结构SQL 单独放resources/sql不散落在手写笔记里演示视频和说明放 doc答辩现场不需要满磁盘找。server内部按 controller/service/mapper 分包数据收集项目的数据量不大过度设计 DDD 反而会让评审老师问住。miniprogram/utils/request.js统一封装 Promise后端返回统一 JSON后续加权限拦截时只改一处。提示压缩包里带「演示视频」不等于答辩时直接播放视频只能作为补充现场需要把工程跑起来演示真实操作所以工程启动步骤务必写进 README。3. 小程序端实现表单采集、图片上传与离线暂存3.1 页面注册与首屏加载页面改造app.json 的 pages 数组第一项就是进入时的首页。数据收集类小程序不要先把登录页放第一页否则每次进入都闪一下白屏再跳转。{ pages: [ pages/visit/index, pages/household/index, pages/stats/index, pages/login/login ], window: { navigationBarTitleText: 数据采集平台, backgroundTextStyle: dark } }visit/index作为启动页onLoad里检查本地 token有 token 直接加载走访列表没有 token 时用wx.reLaunch到登录页。这里不能用redirectTo因为启动页还在页面栈里用户返回时会回到空白启动页。首屏请求慢导致白屏时用wx.showLoading挡在onLoad的请求发起前回调里wx.hideLoading。这对应了常被搜的「修改刚进入的加载页面」的诉求本质不是改动画而是控制页面栈和请求时序。3.2 走访表单的控件组合与前端校验走访记录表单字段通常有家庭档案编号、走访日期、收入情况、住房状况、帮扶项目多选、备注。WXML 片段如下view classform-item text classlabel收入来源/text picker modeselector range{{incomeTypes}} bindchangeonIncomeTypeChange view{{form.incomeType || 请选择}}/view /picker /view view classform-item text classlabel帮扶项目/text checkbox-group bindchangeonHelpChange label wx:for{{helpOptions}} wx:keyvalue classhelp-item checkbox value{{item.value}}/{{item.label}} /label /checkbox-group /view view classform-item text classlabel备注/text textarea value{{form.remark}} maxlength500 bindinputonRemarkInput auto-height/ /viewpicker 的 range 是字符串数组bindchange 里e.detail.value是下标要映射回字符串再写入form.incomeTypecheckbox-group 的e.detail.value是选中项的 value 数组。前端校验放在 submit 里收入为空或用isNaN判断、帮扶项目不能是空数组、备注可选。前端校验从来不是安全手段因为可以绕过小程序直接调接口它的价值是让操作人员少一次无效提交。3.3 wx.chooseMedia 选图与 wx.uploadFile 上传图片是数据收集里最容易出问题的部分。选择图片用wx.chooseMedia上传用wx.uploadFile两者分开处理。封装上传// utils/upload.js const uploadImage (filePath) new Promise((resolve, reject) { wx.uploadFile({ url: http://192.168.1.10:8080/api/visit/images, filePath, name: file, header: { Authorization: wx.getStorageSync(token) }, success: (res) { try { const data JSON.parse(res.data); if (data.code 0) resolve(data.data); else reject(new Error(data.msg)); } catch (e) { reject(new Error(响应解析失败)); } }, fail: (err) reject(err) }); }); const submitVisit async (formData, images) { const urls await Promise.all(images.map(uploadImage)); return request(/api/visit, POST, { ...formData, images: urls }); };name: file要和 Spring Boot 里RequestParam(file)完全一致header 里的 Authorization 给后端做登录态校验。Promise.all并发上传时其中一张失败会整体 reject所以实际操作里我会在 fail 回调里提示「第几张上传失败」而不是整单失败否则用户很难定位。wx.uploadFile 每次只传一个文件这是 API 限制9 张以内一般不用额外队列超过 9 张需要在选图时就截断。3.4 弱网下的本地暂存与联网后同步户外调查的网络条件不如办公室。常用做法是先判断网络类型无网络时把表单 JSON 写入本地缓存并提示用户已暂存有网络时从缓存里取出逐条提交。wx.getNetworkType({ success: (res) { if (res.networkType none) { const pending wx.getStorageSync(pending_visits) || []; pending.push(formData); wx.setStorageSync(pending_visits, pending); wx.showToast({ title: 已离线保存, icon: none }); } else { submitVisit(formData, images); } } });离线状态下图片处理要单独设计不能把图片转 base64 塞进 storage单个 key 只有 1MB 左右空间几张图就满了。稳妥的简化方案是离线只暂存文本表单图片留在用户相册联网后进入「待补图」列表让操作员重新选图上传。这虽然不算自动续传但对毕业设计的演示场景已经足够也避免了大文件占用缓存导致小程序异常。数据收集常用 API 的注意点整理如下场景API必须注意的点选图wx.chooseMediacount 不超过 9真机与模拟器相册展示顺序不同上传wx.uploadFilename 与后端参数名对齐返回体是一段字符串离线缓存wx.setStorageSync别存 base64 图片容量限制明显网络探测wx.getNetworkType返回 none/unknown 时要当作不可用处理4. Java 后端接口设计与数据库落库4.1 Spring Boot 工程与依赖选择后端用 Spring Boot 2.7.x 配 MyBatis-Plus对这种 CRUD 为主的数据收集平台足够。pom.xml 里的核心依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.32/version /dependencyMyBatis-Plus 的 BaseMapper 自带增删改查省去大量重复 XML。controller/service/mapper 三层拆开即可数据收集项目不需要把领域模型分得特别复杂。如果换成 Spring Data JPA 也可以但动态条件查询在列表页筛选场景下MyBatis-Plus 的 LambdaQueryWrapper 写起来更直接。4.2 数据库表结构与防重复约束用户表、家庭档案表、走访记录表三张核心表。走访记录重点看唯一键CREATE TABLE household ( id BIGINT PRIMARY KEY AUTO_INCREMENT, household_no VARCHAR(32) NOT NULL COMMENT 户编码, householder VARCHAR(32) NOT NULL COMMENT 户主姓名, region_code VARCHAR(12) NOT NULL COMMENT 行政区划编码, income_monthly DECIMAL(12,2) DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_household_no (household_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE visit_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, household_id BIGINT NOT NULL, visitor_id BIGINT NOT NULL, visit_date DATE NOT NULL, income_monthly DECIMAL(12,2) DEFAULT NULL, remark TEXT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_visit_same_day (household_id, visit_date, visitor_id), KEY idx_visitor_date (visitor_id, visit_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;uk_visit_same_day是防重复的关键同一个调查员在同一日期对同一家庭只能提交一条走访记录。这个约束写在数据库层即使接口被并发调用重复数据也进不来比只在应用层判断可靠。收入字段用 DECIMAL 而不是 FLOAT因为金额计算不允许浮点误差。4.3 小程序登录凭证与 token 发放登录流程用wx.login得到 code后端拿 code 调微信接口换 openid。code 一次性有效服务端不能缓存 code。Controller 示例PostMapping(/login) public ResultLoginVO login(RequestBody LoginDTO dto) { String openid wechatService.code2Session(dto.getCode()); if (openid null) { return Result.error(登录凭证无效); } User user userMapper.selectOne( new LambdaQueryWrapperUser().eq(User::getOpenid, openid)); if (user null) { user new User().setOpenid(openid).setRoleCode(surveyor); userMapper.insert(user); } String token UUID.randomUUID().toString().replace(-, ); tokenService.cache(token, user.getId()); return Result.success(new LoginVO(token, user)); }openid 是用户标识token 用一个随机串缓存起来即可不必引入完整 JWT。答辩被问到「为什么不用 JWT」时可以说无状态 token 在这个项目里没有分布式要求存 Redis 或内存 Map 更简单如果用 JWT 要自行处理吊销问题。4.4 走访记录提交与图片上传接口走访记录接口接收 JSON图片接口接收 multipart 文件。两个接口分开是数据流设计的结果RestController RequestMapping(/api) public class CollectController { PostMapping(/visit) public ResultLong submitVisit(RequestBody Valid VisitDTO dto) { // 这里会做幂等检查同一调查员同一天不能重复走访同一家庭 Long id visitService.save(dto); return Result.success(id); } PostMapping(/visit/images) public ResultListString uploadImages( RequestParam(file) MultipartFile[] files) { ListString urls Arrays.stream(files) .map(file - fileStorage.save(file)) .collect(Collectors.toList()); return Result.success(urls); } }VisitDTO 里incomeMonthly用BigDecimal避免金额浮点误差householdId 加NotNullremark 限制Size(max500)。上传接口的 MultipartFile 要和前端 wx.uploadFile 的 name 保持一致否则拿到 null。fileStorage 把文件写到本机 upload 目录返回相对路径上线时用 Nginx 把 /files 映射到该目录图片 URL 就能直接访问。4.5 接口清单与统一返回结构前后端约定统一返回{ code, msg, data }。主要接口接口路径方法请求参数说明/api/loginPOSTcode登录返回 token/api/householdPOSTJSON新增家庭档案/api/visitPOSTJSON images提交走访记录/api/visit/imagesPOSTmultipart file图片上传/api/stats/regionGETregionCode, month地区统计code 用 0 表示成功401 表示未登录400 表示参数错误500 表示系统异常。小程序端 request.js 只认 code 是否为 0其余情况弹 toast 展示 msg。这样接口层最薄排错也最直观。5. 统计聚合、数据质量检查与接口权限收敛5.1 按行政区域和月份的聚合 SQL数据收集的最后一步是统计。按地区和月份聚合走访数据核心 SQLSELECT v.region_code, DATE_FORMAT(v.visit_date, %Y-%m) AS visit_month, COUNT(DISTINCT v.household_id) AS household_cnt, ROUND(AVG(v.income_monthly), 2) AS avg_income FROM visit_record v WHERE v.visit_date #{startDate} AND v.visit_date #{endDate} GROUP BY v.region_code, DATE_FORMAT(v.visit_date, %Y-%m) ORDER BY v.region_code, visit_month;COUNT(DISTINCT household_id)统计户数而不是记录数因为同一家庭在当月可能被走访多次但统计关心的是覆盖户数。小程序的统计页只需把这个查询结果渲染成列表图表库可以不用或只画最简单的柱状条。如果评审老师追问性能这张表加上idx_visitor_date索引数据量在十万级以内没有压力。5.2 重复数据与异常数据拦截前面的uk_visit_same_day数据库唯一键已经兜住重复但用户直接撞上 SQLIntegrityConstraintViolationException 会感觉像是程序崩溃。所以应用层要先查一遍Long count visitMapper.selectCount(new LambdaQueryWrapperVisitRecord() .eq(VisitRecord::getHouseholdId, dto.getHouseholdId()) .eq(VisitRecord::getVisitDate, dto.getVisitDate()) .eq(VisitRecord::getVisitorId, currentUserId)); if (count 0) { return Result.error(该家庭当日已有走访记录); }数据质量上收入字段用BigDecimal.compareTo(BigDecimal.ZERO) 0判断负数不能用运算符直接比较两个 BigDecimal 对象。region_code 加正则校验只允许 6 位数字避免手输脏数据。走访日期要限制不能晚于当前日期否则统计口径会出现负数月份。5.3 接口权限矩阵与最小暴露一个简化但完整的权限矩阵接口村级调查员乡镇管理员POST /api/household允许允许POST /api/visit允许允许GET /api/stats/region仅本村 region_code全部实现方式用 HandlerInterceptor 拦截/api/**放行/api/login其余检查 token 并加载用户角色。统计数据接口在 service 层判断调查员传入的 region_code 必须等于本人所属 region_code否则拒绝。这个权限由 sys_user.region_code 控制演示时初始化两个账号分别验证即可。5.4 request.js 封装与列表分页小程序端统一请求封装避免每个页面重复写 wx.request// utils/request.js const request (url, method, data) new Promise((resolve, reject) { wx.request({ url: http://192.168.1.10:8080 url, method, data, header: { Authorization: wx.getStorageSync(token) }, success: (res) { if (res.data.code 0) resolve(res.data.data); else reject(new Error(res.data.msg)); }, fail: reject }); }); module.exports { request };走访列表的加载更多用onReachBottom触发页面 data 里维护 page 和 hasMore 两个字段每次请求成功追加数组返回条数小于 pageSize 就把 hasMore 置 false阻止无效请求。后台分页用 MyBatis-Plus 的 Page 对象传 pageNum 和 pageSize。token 过期时后端返回 401request.js 里统一清除本地 token 并跳转登录页避免每个页面各自处理。6. 部署与演示从源码包到现场跑通6.1 本地启动顺序缺一不可先初始化数据库再启动后端最后打开小程序。mysql -uroot -p server/src/main/resources/sql/schema.sql cd server mvn spring-boot:runapplication.yml 里的数据库地址、账号、密码改成自己本机的端口保持 8080。如果演示时 MySQL 没启动后端日志里会出现Communications link failure但小程序端只看到请求失败所以先看后端控制台再定位前端。6.2 微信开发者工具的域名与真机调试开发阶段在微信开发者工具的「详情 - 本地设置」勾选「不校验合法域名」。不勾选的话 wx.request 会被拦截。真机预览时手机和电脑必须在同一局域网接口地址不能写 localhost要写电脑的局域网 IP例如http://192.168.1.10:8080。用手机热点演示时必须改 IP这也是现场翻车最多的地方。6.3 用抓包和录屏验证演示效果演示视频最好由两段组成正常提交一段断网保存后再同步一段。录制前用微信开发者工具的 Network 面板或 Charles 抓包确认每次提交返回 code0response 里能看到业务数据。接口返回 401 时先看 token 是否存在返回 400 时把请求体 JSON 和 DTO 字段逐一比对多半是字段名不一致或日期格式传错。提示演示现场最常翻车的是数据库没初始化管理员账号导致无法登录以及接口地址写成本机 localhost 导致真机连不上。开机后先在浏览器直接访问后端接口确认服务在跑再打开小程序。如果上面的顺序都检查过走访列表仍然空白先看 visit_date 是否被前端传成字符串而数据库字段是 DATE再看统计 SQL 的日期参数范围。这类问题的根因基本在类型转换和时区按接口响应、控制台日志、数据库记录三层顺序排查就能收口。本文还有配套的精品资源点击获取