
这两年做过的项目里让我印象最深的一个就是这套基于 Spring Boot 微信小程序的小动物救助领养系统。为什么印象深因为它的业务逻辑并不复杂但涉及的角色、状态流转和前后端协同问题非常典型用户端要轻、管理端要全、接口要稳、图片要能传能显。今天这篇就把这个项目的完整思路、核心实现、调试实录和一些踩坑经验全部拆开来讲给正在做类似前后端分离小程序项目的朋友一个可以直接参考的版本。这个项目本质上是给救助站、个人志愿者和领养人搭一座桥。C 端微信小程序负责浏览待领养小动物、提交领养申请、收藏和查看公告后台管理端用 Vue 实现负责动物信息录入、审核申请、管理用户、发布公告Spring Boot 作为统一 API 服务层把两端串起来。无论你是学生做毕业设计、接外包项目还是想给本地救助组织做一套公益工具这套架构和实现思路都值得认真过一遍。1. 项目定位与需求拆解这个系统到底要解决什么问题1.1 救助站/志愿者的真实痛点我在调研需求的时候跟好几个做流浪动物救助的朋友聊过。他们的日常状态基本是微信群里发照片、朋友圈发领养信息、Excel 表登记意向、聊天记录翻到天荒地老。信息分散、状态不同步、领养人资质没有统一审核标准。这个项目要解决的恰恰就是这三件事信息集中化所有待领养动物统一录入后台小程序端按分类、品种、城市展示。流程标准化从“看到信息”到“提交申请”到“后台审核”到“领养成功”每一步都有明确状态。管理后台化救助站管理员不用再对着手机截图直接在 Vue 管理后台批量处理。这里有一个很关键的认知救助领养类小程序的核心不是“炫技”而是把信任链条做完整。所以数据模型设计上用户、动物、申请、公告四个主体之间的关联关系比界面好不好看重要得多。1.2 目标用户、角色划分与核心价值这个系统涉及三类角色权限边界必须清晰角色使用端核心操作游客/普通用户微信小程序浏览动物、查看详情、提交领养申请、收藏救助站管理员Vue 管理后台动物信息录入、上下架、审核领养申请、公告管理超级管理员Vue 管理后台管理员账号管理、数据统计、系统配置还有一个容易被忽略的点用户在小程序端第一次进入时是通过微信授权登录的后台需要存储 openid 作为唯一标识。这个 openid 是后续申请领养、收藏、查看进度所有操作的凭证。所以登录逻辑不能只做一个“登录成功”的假象token 的签发和校验必须在一开始就规划好。实际做下来我的体会是把角色和状态机先画清楚后面写代码会顺畅得多。动物有“待审核/已上架/已下架/已被领养/已下架”这些状态申请有“待审核/已通过/已拒绝/已完成”不同角色在不同状态下能做的操作完全不一样提前理清能少改很多 bug。2. 整体架构与技术选型为什么是 Spring Boot 微信小程序 Vue2.1 前后端分离到底分的是什么项目标题里明确写了“前后端分离”这不仅是技术架构的选择更是团队协作模式的转变。在这个项目里分离体现在三个层面工程分离小程序端是一个独立工程Vue 管理后台是一个独立工程Spring Boot 后端是一个独立工程三个目录互不干扰。部署分离后端 API 部署在云服务器小程序端通过微信开发者工具上传发布Vue 后台打包成静态文件用 Nginx 托管。职责分离前端只负责视图渲染和用户交互后端只负责数据处理和业务逻辑通信全靠 RESTful API。如果你之前习惯写 JSP 那种前后端不分的架构刚切到这种模式时最明显的感觉是联调成本变高了但代码维护成本降得很明显。改前端不用重启后端改接口不用动页面各改各的只要把接口约定做好就行。2.2 Spring Boot 在这个项目里为什么够用且合适很多人在技术选型时会纠结要不要用 Spring Cloud要不要上微服务我的建议是一个面向救助站的小系统单体应用 Spring Boot 是最务实的选择。Spring Boot 的优势在这个项目里体现得非常具体自动配置极大减少了 XML 和配置文件的维护。一个 Spring Initializr 生成的工程直接就能跑起来。内置 Tomcat打 jar 包即可部署。配合 Maven 打包一条命令就能出产物。Spring Data JPA 或 MyBatis 的选择上我用的是 MyBatis-Plus因为 CRUD 操作多、条件查询复杂MyBatis-Plus 的 LambdaQueryWrapper 写起来非常顺手。生态成熟集成微信登录、文件上传、参数校验都有现成方案。有一个细节必须提醒Spring Boot 版本不要盲目追求最新。2.7.x 或者 3.x 具体怎么选要看你的 JDK 版本和依赖兼容性。我遇到过 Spring Boot 3 和某些第三方依赖不兼容导致启动报错的情况后来回到 2.7.x 就一切正常。新手建议先用 2.7.x 起步稳。2.3 微信小程序为什么比 App 更适合这个场景救助领养这东西有很强的地域性和即时性。领养人往往是刷朋友圈时看到某只流浪狗的照片才动了恻隐之心让他专门下载一个 App 再注册一遍转化率会低很多。微信小程序“扫码即用、用完即走、转发方便”的特点和这个场景天然匹配。小程序端使用的原生框架配合微信开发者工具进行调试。页面结构上分为首页、动物列表、动物详情、我的、申请记录等几个模块。微信小程序的 WXML 语法本质上和 HTML 有相似之处但又有自己的组件体系和事件绑定方式上手的时候需要注意区分。视频播放也是一个常见需求比如给待领养动物拍一段动态视频。小程序端的 video 组件天然支持播放但要注意是 mp4 格式。如果后端给的是流媒体格式比如 m3u8微信小程序原生不支持直接播需要做特殊处理或转码这点后面调试部分会细说。2.4 Vue 管理后台轻量但必须完整管理后台我选了 Vue 2 Element UI 这个组合。虽然现在 Vue 3 Element Plus 已经很成熟但考虑到项目稳定性和大量现成示例Vue 2 生态在这个体量的项目中非常够用。管理后台的核心页面包括登录页管理员账号密码JWT 鉴权动物管理列表分页查询、筛选、上架/下架、编辑、删除动物录入表单图片上传、多图展示、品种/年龄/性别/疫苗状态等字段领养申请审核列表查看申请人信息、审核通过/拒绝用户管理列表查看用户基础信息、openid、注册时间公告管理发布公告、展示在小程序端首页数据统计简单展示每天新增动物数、申请数、领养成功数Vue 后台不是给普通用户用的是给救助站工作人员用的所以功能宁可多而全不要花哨但缺按钮。我做的版本里批量操作、状态筛选这类“易用性”功能比图表可视化更实用。3. 数据模型设计与功能模块拆解3.1 数据库表设计四张核心表与两张辅助表整个系统的数据表我整理成下面这个结构表名用途关键字段user微信用户id, openid, nickname, avatar, phone, city, create_timeadmin_user后台管理员id, username, password(BCrypt加密), role, statusanimal待领养动物信息id, name, type(猫/狗/其他), breed, gender, age, health_status, vaccine_status, description, cover_image, images, status, create_timeadopt_application领养申请id, animal_id, user_id, applicant_name, applicant_phone, applicant_address, reason, status, create_time, update_timefavorite收藏记录id, user_id, animal_id, create_timenotice公告id, title, content, status, create_time设计这几张表的时候有几个细节值得展开讲。第一animal 表的 images 字段我用了 JSON 字符串存储存的是图片 URL 数组前台展示时方便直接解析但这会牺牲一定的数据库规范化如果你对性能有极致要求或者后期要做图片维度统计建议拆成独立的 animal_image 表。第二adopt_application 表必须冗余申请人姓名、电话、地址。为什么因为如果只存 user_id后续用户改了头像昵称或者你想统计某个城市的领养数据就要多表关联查询。冗余这些字段换来的是列表查询少 join 一次值。第三BCrypt 加密密码是必须的。管理后台的 admin_user 表密码绝不能明文存储Spring Security 里的 BCryptPasswordEncoder 一行代码就能搞定。3.2 功能模块全景从用户端到管理端小程序端功能模块首页轮播公告 推荐动物列表 快速筛选入口猫/狗动物列表页分页加载、按品种性别筛选、搜索动物详情页轮播图、基本信息、救助站描述、收藏按钮、领养申请入口领养申请页表单填写姓名、电话、住址、养宠经验、申请理由我的页面我的收藏、我的申请记录、个人资料申请进度查询查看申请处于待审核/通过/拒绝/完成状态管理后台功能模块仪表盘核心数据概览动物总数、待审核申请数、用户数动物管理列表、新增、编辑、删除、上下架申请审核列表、详情、审核操作、联系用户用户管理列表、详情、禁用公告管理新增编辑删除、上下线管理员管理新增管理员、重置密码、停用整个功能清单看下来其实没有特别复杂的业务。但每一条链路都涉及到前端调用接口、后端操作数据库、再返回结果渲染的过程把这些链路做顺就是项目的核心工作。4. 系统实现过程从空工程到可运行的全流程记录4.1 Spring Boot 后端接口设计与核心代码逻辑后端这块我按照 controller-service-mapper 三层来组织。controller 只做参数接收和结果封装service 写业务逻辑mapper 负责数据库交互。以动物列表接口为例GetMapping(/animal/page) public ResultPageResultAnimalVO page(RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer size, RequestParam(required false) String type, RequestParam(required false) String keyword) { LambdaQueryWrapperAnimal wrapper new LambdaQueryWrapper(); wrapper.eq(Animal::getStatus, 1) // 只展示已上架 .eq(StringUtils.hasText(type), Animal::getType, type) .like(StringUtils.hasText(keyword), Animal::getName(), keyword) .orderByDesc(Animal::getCreateTime); PageAnimal p animalMapper.selectPage(new Page(page, size), wrapper); // 转换为VO隐藏敏感字段 return Result.success(new PageResult(p.getRecords(), p.getTotal())); }这里有两个习惯值得推荐返回统一结构 Result。无论成功失败code/message/data 三件套前端判断 code 即可不用每次写 try-catch。用 VO 对象返回前端需要的字段而不是直接返回实体。实体类里可能有多余的字段比如 status 表示内部状态前端不一定需要通过 VO 做一次字段收敛接口更干净。领养申请提交的接口稍微复杂一点因为要校验用户登录状态、校验动物是否可领养、校验是否重复申请PostMapping(/adopt/apply) public Result? apply(RequestBody ApplyDTO dto, RequestHeader(token) String token) { Integer userId userService.getUserIdByToken(token); if (userId null) return Result.error(401, 登录已过期); Animal animal animalMapper.selectById(dto.getAnimalId()); if (animal null || animal.getStatus() ! 1) { return Result.error(该动物不存在或暂不可领养); } // 防止重复申请 Long count applyMapper.selectCount(new LambdaQueryWrapperAdoptApplication() .eq(AdoptApplication::getAnimalId, dto.getAnimalId()) .eq(AdoptApplication::getUserId, userId) .in(AdoptApplication::getStatus, 0, 1)); // 待审核或已通过 if (count 0) return Result.error(您已申请过该动物请勿重复提交); // 业务合法性校验通过后插入申请记录 // ... return Result.success(); }这段代码里体现了一个重要原则后端不能信任前端传的任何状态所有校验必须在后端重做一遍。前端可能隐藏了按钮但人为构造请求也可以绕过后端接口是最后一道防线。4.2 微信小程序端登录、页面结构与关键交互微信小程序的登录逻辑是wx.login 获取 code传给后端后端拿着 code appid secret 去微信接口换 openid 和 session_key再生成自己的 token 返回前端。后续所有请求都在 header 里带 token。具体代码如下// 小程序端 app.js 里的登录逻辑 login() { wx.login({ success: (res) { wx.request({ url: https://api.example.com/user/login, method: POST, data: { code: res.code }, success: (response) { const { token } response.data.data; wx.setStorageSync(token, token); // 再拉取用户信息 this.getUserInfo(); } }); } }); }后端的处理逻辑核心就是调微信接口换 openid然后查询或创建用户再返回 tokenString url https://api.weixin.qq.com/sns/jscode2session?appid appid secret secret js_code code grant_typeauthorization_code; // 用 RestTemplate 发起 GET 请求 // 解析返回的 openid // 查 user 表有则更新昵称头像无则新建 // 生成 UUID 作为 token存 Redis 并设置过期时间这里有一个小坑微信接口返回的 session_key 是敏感信息绝对不能让前端拿到否则可能导致用户数据安全问题。后端只在内部使用token 用自生成的 UUID 就好不需要把 session_key 暴露出去。小程序端的核心页面交互我需要重点强调“状态同步”的体验。比如用户点击收藏后按钮立即变灰列表页的收藏状态也要同步。我的做法是收藏操作成功后更新本地缓存的收藏列表同时 store 里保存收藏的 animalId 集合详情页根据这个集合判断按钮状态。4.3 Vue 管理后台Element UI 表格、表单与图片上传管理后台最核心的页面是动物录入表单。这个表单字段多、类型杂用 Element UI 的 el-form 加上动态校验规则可以很好解决。图片上传这块必须多说一句。Vue 端用 el-upload 组件配置 action 指向后端的文件上传接口。后端接收 MultipartFile保存到服务器的指定目录然后返回一个可访问的 URL。这个 URL 写成绝对路径还是相对路径很有讲究。我在本地开发时用 http://localhost:8080 的绝对路径部署到服务器后就换了服务器公网 IP 或域名。如果前后端域名不同还要在 Nginx 或后端配置跨域策略保证图片能正常加载出来。图片存储方案的选择上本地存是最简单的。但如果图片量大、服务器带宽有限建议用对象存储加 CDN。这个项目因为量级不大本地存储完全够用关键是要把图片目录和上传接口设计好后期换对象存储只需要改上传接口的实现前端无感知。状态管理用 Vuex 或 Pinia 都行。管理后台我主要用 Vuex 存用户登录信息、菜单权限、路由状态。有个很实用的小技巧在 axios 拦截器里统一处理 token请求前在 header 里带 token响应时遇到 401 就跳回登录页。这样每个页面都不用单独处理登录过期。5. 调试过程实录联调、报错、排查的完整复盘5.1 前后端联调时的跨域问题CORS这几乎是前后端分离项目必经的一个坑。小程序端不存在跨域问题因为微信小程序的请求不遵循浏览器同源策略但 Vue 管理后台跑在浏览器里面跨域是必须处理的。我的处理方案是后端加一个全局 CORS 配置类Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }注意 allowCredentials(true) 的时候allowedOrigins 不能直接用 “*”要使用 allowedOriginPatterns这是很多新手的经典报错点。还有一种更常见的做法是走 Nginx 反向代理把 /api/ 路径代理到后端服务前端请求同源的 /api/xxx由 Nginx 转发并解决跨域。生产环境我强烈建议这种方式统一入口后还可以顺手做 HTTPS 和请求日志。5.2 小程序真机调试时图片加载不出来联调时遇到最诡异的问题是开发者工具里图片显示正常手机上一加载就空白。排查了半天才发现是因为后端开发服务器的 IP 是局域网地址手机和电脑不在同一网段访问不到。这个问题的标准解法是开发阶段让手机和电脑连同一个 Wi-Fi后端启动时设置--server.address0.0.0.0允许局域网访问。或者用内网穿透工具把本地端口映射到公网地址微信小程序后台配置合法域名手机就能访问了。上线阶段所有接口和图片 URL 必须换成 HTTPS 域名微信小程序要求所有 request 请求的域名必须在小程序后台配置为合法域名且必须是 HTTPS。还有一个小细节微信小程序里图片域名和 request 合法域名是分开配置的如果图片一直加载不出来先检查是否把图片域名配到了 downloadFile 合法域名里。5.3 微信登录与 token 失效问题排查出现过一次非常典型的 bug用户每天第一次打开小程序都要重新登录但明明 token 已经存储了。排查后发现是 token 过期时间设置得太短只有 2 小时用户隔天打开必然过期。解决思路把 token 过期时间设置为 7 天同时提供刷新接口。在用户每次操作时刷新 token 的过期时间保持活跃用户的登录态。前端在请求拦截器里遇到 401 不立即跳登录而是先去调刷新接口刷新失败再跳。另外一个容易踩的坑是wx.login 的 code 只能使用一次不能重复使用。如果前端在短时间内多次调用 wx.login后端处理时要每个 code 只换一次否则微信接口会报错。5.4 文件上传大小与类型限制上传图片一直失败排查半天发现是 Spring Boot 默认请求体大小限制在 1MB。用户用手机拍的照片动辄 3-5MB直接 413 报错。在 application.yml 里配置spring: servlet: multipart: max-file-size: 10MB max-request-size: 20MB同时在后端做文件类型校验只允许 jpg、png、gif、webp 格式避免有人传其他危险文件。这里和线上安全相关千万别省略。后端对上传的文件名最好做一次重命名用 UUID 原始后缀避免中文文件名乱码同时防止路径穿越攻击。这些细节不处理系统虽然能跑但隐患很多。6. 部署上线与后续扩展建议6.1 从本地到服务器部署实操记录这个项目的部署流程相对标准我在生产环境跑通并记录了步骤后端Maven 打包得到 jar上传到服务器用 nohup 启动或用 systemd 管理服务建议写一个小脚本处理启停和日志备份。前端 Vue 后台npm run build生成 dist 目录把静态文件放到 Nginx 的 html 目录配置反向代理/api到后端端口。小程序端在微信开发者工具中上传代码填好版本号和备注提交审核。审核通过后发布。域名与证书小程序要求所有 request 域名必须是备案过的 HTTPS 域名所以提前准备域名和 SSL 证书并配置到小程序后台的合法域名里。这里有一个经常被忽略的点管理后台的接口如果直接暴露到公网任何人拿到接口地址就可以绕过登录直接调用。我的处理方式是后端接口用自定义 token 校验拦截器统一处理管理后台接口要求必须携带合法的管理员 token并且对管理端接口做独立的权限校验保证普通用户 token 无法访问管理接口。6.2 这个系统还能怎么扩展如果这个项目不是毕业设计交付完就不再管而是想真正落地使用我建议按下面几个方向迭代第一增加志愿者模块。救助站往往缺的不是信息展示是志愿者管理。可以增加志愿者报名、排班、任务认领功能。第二增加回访机制。领养不是终点回访才能发现动物是否真的被善待。可以在系统中增加“领养回访记录”功能管理员定期回访后在后台登记形成完整的领养闭环。第三增加捐赠和物资众筹。救助站的资金压力是持续的小程序端增加“帮助它”按钮跳转捐赠页面让更多人参与进来。第四增加地图定位。用微信小程序的 wx.getLocation 获取用户所在城市根据经纬度推荐附近的待领养动物提高领养转化率。集成腾讯地图或高德地图的逆地址解析就能实现“附近的小动物”这个功能。第五消息通知。小程序的订阅消息非常适合申请审核结果通知。用户提交领养申请后管理员审核通过或拒绝后端调用微信订阅消息接口用户就能收到模板消息通知。这一块能明显提升用户体验。写在最后的经验我个人在实际开发中最大的体会是一套前后端分离的小程序项目真正难的从来不是某个技术点而是把登录态、状态流转、文件传输这三条链路接口设计得足够顺畅。如果你正在做类似项目我强烈建议先花一天时间把接口文档写清楚把每个接口的入参、出参、错误码定义好再开始写代码。前后端分离模式下接口是唯一的契约契约乱了联调阶段会浪费大量时间。最后再分享一个小技巧微信开发者工具里的“真机调试”功能建议从一开始就养成随手点开的习惯。很多样式和交互在模拟器里是正常的一到真机上就出现导航栏高度、底部安全区、图片加载速度这些差异。越早发现改起来越轻松。做公益类项目本身就是一件有温度的事代码写扎实一点也是对那些等待被领养的小生命负责。