这个项目是我上半年从零到一搭完的一套完整系统前后大概花了三周业余时间。技术栈就是标题里那套SpringBoot SpringCloud 做微服务后端Vue 做管理后台微信小程序做C端业务围绕流浪动物领养和捐赠两条主线展开。做完以后我最大的感受是这套系统的难点根本不在CRUD而在服务拆分的粒度怎么把握、领养这种长流程的状态怎么管理、以及捐赠这种涉及资金的链路怎么保证数据一致。这篇就按我实际落地的思路把架构设计、拆库拆表、核心流程、前端联动和排坑记录一次讲清楚给想拿微服务做实战项目的朋友做个参考。1. 项目定位流浪动物领养捐赠系统到底要解决什么问题1.1 业务链条与三方角色先把这个系统的业务背景捋清楚。流浪动物救助不是一个简单的“发帖-领养”模型它天然带一条长链路救助人发现流浪动物后要把动物收容、体检、驱虫、绝育然后发布领养信息领养人浏览动物、提交申请、经过审核和家访、签领养协议、后续还有定期回访。与此同时救助站点的日常运转需要资金这就有了捐赠这条线平台展示捐赠项目、用户在线捐款、资金去向公示。这套系统里有四类核心角色管理员负责审核动物信息、审核领养申请、管理捐赠项目救助人志愿者负责录入动物资料、上传体检记录领养人负责浏览、申请、签协议、填回访记录捐赠者负责捐款和查看资金流水。我在设计时把整个系统拆成了用户、动物、领养、捐赠、文件、通知六个核心域每个域对应一个微服务这是整个项目所有设计决策的起点。1.2 为什么选微服务而不是单体开发这是我在项目启动前最纠结的问题也是很多初学者拿到这类题目时第一个会问的问题。说实话如果一个流浪动物平台只有几千用户、几个救助站单体应用完全够用强行上微服务就是给自己找麻烦。但我做这个项目时有几个前置条件让微服务变成了合理选择第一未来动物信息、领养申请、捐赠流水都会持续增长按业务域独立扩展比整体扩容成本低第二领养和捐赠的并发特征不同领养集中在白天、捐赠在活动期间会有瞬时峰值拆开以后可以对不同服务设置不同的实例数第三这套系统本身是想做成一个可复用的公益平台底座后续可能接入多个救助站、多个小程序端服务独立部署更容易实现多租户改造。当然微服务的代价也实实在在地体验到了服务间调用要处理超时、重试、数据一致性问题本地调试要多启动好几个进程部署要写一堆Docker编排。我的建议很明确如果你只是想交个毕业设计或练手SpringBoot单体模块化就够了但如果你已经基本掌握单体开发想去理解服务拆分、注册中心、网关这些生产级概念这个项目的技术选型正好合适。2. 微服务拆分与技术选型2.1 服务边界怎么划按业务域拆分的核心思路微服务拆分的方法论有很多种有的按层拆controller拆一个、service拆一个有的按技术拆把Redis单独拆个服务这些都是反模式。我采用的是领域驱动里最经典的“按业务能力拆分”把业务上高内聚的功能聚类成一个服务服务之间通过接口通信不共享数据库。我最终拆成了6个服务加1个网关gateway-service统一入口负责路由转发、统一鉴权、跨域处理user-service用户注册登录、实名认证、角色权限、个人资料animal-service动物档案、图片视频、领养状态、健康记录adoption-service领养申请、审核流程、协议签署、回访记录donation-service捐赠项目、捐赠订单、微信支付回调、资金流水file-service文件上传下载对接MinIO对象存储message-service站内信、模板消息通知订阅业务事件拆分时我反复提醒自己一个原则宁可服务少而边界清晰不要服务多而耦合严重。比如“动物信息”和“领养状态”看着像一回事但动物信息是基础资料、领养状态是业务流程如果放在一个服务里领养审核时并发更新状态会和基础资料的读操作互相影响。分开以后动物服务只管档案读写领养服务只管流程流转中间用Feign调用同步状态逻辑清楚多了。2.2 技术栈版本与组件选型版本兼容性是我在这个项目里踩过最多的坑。Spring Boot 和 Spring Cloud 的版本对应关系非常严格用错了直接启动报错。我最终用的是Spring Boot 2.7.18 Spring Cloud 2021.0.8 Spring Cloud Alibaba 2021.0.5.0配套 Nacos 2.2.3 作为注册中心和配置中心。为什么不用 Spring Boot 3.x因为 Spring Cloud 2022 对 JDK 17 有硬性要求而我的服务器环境是 JDK 8团队其他项目也都是 JDK 8 体系。这里给所有要做微服务项目的朋友一个忠告开始建项目前先查版本兼容矩阵不要直接拉最新版。网上很多教程默认你用的最新版本跟着抄完才发现自己的 Spring Boot 版本太高或者太低一天时间就耗在排版本上了。网关我选的是 Spring Cloud Gateway没有选 Zuul。原因很简单Gateway 基于 WebFlux 响应式编程性能更好而且内置了断言和过滤器机制配合 Sentinel 做限流非常方便。服务间通信统一用 OpenFeignHTTP 调用加上负载均衡比直接写 RestTemplate 优雅得多。2.3 服务间注册发现与配置管理服务注册发现这块我用了 Nacos 而不是 Eureka。Nacos 除了注册中心还兼任配置中心可以把每个服务的数据库连接、Redis地址、业务开关都放在 Nacos 配置里以后改配置不用重新打包发版。我在每个服务里都引入了spring-cloud-starter-alibaba-nacos-discovery和spring-cloud-starter-alibaba-nacos-configbootstrap.yml 里配置 Nacos 地址和服务名启动后自动注册。有个细节值得提一下Nacos 做配置中心时一定要给配置文件名加后缀比如user-service.yaml然后在 bootstrap.yml 里指定spring.cloud.nacos.config.file-extension: yaml否则默认按 properties 解析容易踩坑。服务间的调用关系我用了一张表来管理后面排查问题时一目了然调用方被调用方调用场景接口gateway所有服务路由转发/api/{service}/**adoption-serviceuser-service校验领养人实名状态GET /user/{id}/realnameadoption-serviceanimal-service更新动物领养状态PUT /animal/statusdonation-serviceuser-service获取捐赠者昵称头像GET /user/{id}/baseanimal-servicefile-service获取图片访问链接POST /file/presign这套调用关系在开发阶段就用 Apifox 做了 mock 测试确保每个接口的出入参在联调前就对齐不然后面改一个字段要联动改三个服务。3. 数据库设计与核心表结构3.1 分库方案微服务的数据独立微服务最硬的规矩就是“数据不共享”。我在数据库设计上做了一个分布式改造按服务拆分为 user_db、animal_db、adoption_db、donation_db、file_db 五个业务库各自库里的表独立创建服务只能访问自己的库。这一刀切下去后面所有跨服务的“查一下用户信息”“查一下动物状态”都必须走Feign接口虽然多点网络开销但换来了数据维度的彻底解耦。拆分以后我发现一个好处备份和恢复变得非常方便。哪怕 animal_db 出问题需要回滚也不会影响已经产生的捐赠流水。坏处也明显跨库 join 全部消失以前一条 SQL 能查出来的“领养人姓名动物名称协议编号”现在要调两个服务的接口做数据组装。所以我在设计响应 DTO 时就预先规划好列表页需要的信息一次性查完再聚合避免前端调多次接口。3.2 领养链路的核心表设计领养链路是整个系统里业务状态最复杂的部分我设计了5张核心表。adoption_apply是领养申请表记录申请人、申请动物、申请理由、居住情况、是否租房、是否有养宠经验adoption_review是审核记录表每一轮审核人、审核结论、备注都留痕adoption_contract是领养协议表存协议编号、签署人、签署时间、协议PDF的存储路径adoption_followup是回访表按协议要求每3个月回访一次animal_status_log记录动物从“待领养”到“已领养”到“退养”的每一次状态变化。我特意把审核和回访都做成独立表而不是字段原因是为了可追溯。线下做领养审核时经常出现救助人和管理员对“这只看过两轮”争执不清的情况有记录表就能看到每一次操作日志。adoption_apply表里我加了current_status字段用一个状态机管理待初审、初审通过、待家访、家访通过、待签约、已完成、已拒绝、已取消。后面会详细说状态机的实现。3.3 捐赠链路的字段设计捐赠链路涉及资金表结构的设计严谨程度比领养高一个量级。我用donation_project存公益项目包括项目名称、目标金额、已筹金额、封面图、详情介绍、项目状态donation_order存每一笔捐赠订单订单号、捐赠人ID、项目ID、金额、支付渠道、交易状态、支付时间donation_refund存退款记录。这里有几个关键点第一donation_order的out_trade_no我用的是“服务ID日期雪花ID”拼出来的唯一字符串不能只用自增ID因为支付回调的幂等性要靠这个单号判断第二金额字段用 decimal(10,2)禁止用 float/double否则一分钱的误差会累积成大问题第三project_status和order_status在设计上从建立字段时就想到枚举常量订单状态我只允许这几种待支付、支付成功、已退款、支付失败、已关闭。另外我做了一张donation_publicity表每次捐款成功都会生成一条公示记录展示捐赠人昵称可匿名、金额、时间。这个设计是为了满足公益平台“善款透明”的诉求也给用户一个善举被看见的正反馈。这个表数据量会增长很快我在开发阶段就做了按月分表的预案。4. 关键业务实现领养与捐赠全流程4.1 领养状态机设计与审核流领养流程是整个系统最核心、最有业务深度的部分。如果只做成“提交申请→管理员通过”两态根本挡不住实际运营中各种异常情况。我在adoption-service里用状态机管理领养申请状态核心逻辑是状态只能按预设方向流转非法跳转直接抛异常。public enum AdoptionState { PENDING_REVIEW(0, 待初审), REVIEW_PASSED(1, 初审通过), HOME_VISITING(2, 待家访), HOME_VISIT_PASSED(3, 家访通过), CONTRACT_SIGNING(4, 待签约), COMPLETED(5, 已完成), REJECTED(6, 已拒绝), CANCELLED(7, 已取消); private static final MapAdoptionState, SetAdoptionState TRANSITIONS new HashMap(); static { TRANSITIONS.put(PENDING_REVIEW, Set.of(REVIEW_PASSED, REJECTED, CANCELLED)); TRANSITIONS.put(REVIEW_PASSED, Set.of(HOME_VISITING, REJECTED, CANCELLED)); TRANSITIONS.put(HOME_VISITING, Set.of(HOME_VISIT_PASSED, REJECTED, CANCELLED)); TRANSITIONS.put(HOME_VISIT_PASSED, Set.of(CONTRACT_SIGNING, REJECTED, CANCELLED)); TRANSITIONS.put(CONTRACT_SIGNING, Set.of(COMPLETED, REJECTED, CANCELLED)); TRANSITIONS.put(COMPLETED, Set.of()); // 终态 TRANSITIONS.put(REJECTED, Set.of()); // 终态 TRANSITIONS.put(CANCELLED, Set.of()); // 终态 } public boolean canTransitionTo(AdoptionState target) { return TRANSITIONS.getOrDefault(this, Set.of()).contains(target); } }执行状态流转时代码里会先校验canTransitionTo再检查操作者权限然后开启事务更新current_status和update_time同时写一条状态变更日志。这里我强烈建议状态流转方法必须加Transactional而且传入的乐观锁版本号要匹配否则并发状态下两条审核请求同时通过会有脏数据。审核流我做了两级第一级初审管理员看申请人的基本信息、养宠条件、居住情况第二级家访需要志愿者线下上门核实对应的操作员是“家访员”角色。这两级在权限设计上是分开的避免一个管理员走完全部流程导致审核形同虚设。4.2 微信支付对接与捐赠流水捐赠模块的支付我接了微信支付的 JSAPI 下单也就是从小程序端发起支付流程是用户在小程序点“捐赠” → 后端调微信统一下单接口拿到prepay_id→ 小程序用wx.requestPayment拉起收银台 → 用户输密码付款 → 微信异步回调通知后端支付结果。这里最核心的是回调处理也是防资损的第一道防线。PostMapping(/notify/pay) public String payNotify(RequestBody String xmlData, RequestHeader(Wechatpay-Signature) String signature) { // 1. 验签用微信支付平台证书验证签名防伪造回调 boolean valid wxPayService.verifyNotifySignature(xmlData, signature); if (!valid) { return fail(sign error); } // 2. 解析XML取出 outTradeNo商户订单号和 transactionId // 3. 查询本地订单校验金额是否与微信返回的 totalFee 一致 DonationOrder order donationService.getByOrderNo(outTradeNo); if (order null || !order.getAmount().equals(payAmount)) { return fail(order not match); } // 4. 幂等处理如果订单已经是支付成功状态直接返回成功防止重复处理 if (order.getStatus() OrderStatus.PAID) { return success(); } // 5. 事务内更新订单状态增加项目已筹金额写公示记录 donationService.markOrderPaid(order.getId()); projectService.increaseRaisedAmount(order.getProjectId(), order.getAmount()); return success(); }回调处理有几个容易忽略的死角验签一定要用证书或公钥验不能只校验回调里某个敏感字段金额必须比对而不是信任回调参数更新金额时要先加锁或使用数据库行锁否则微信有可能并发推送两条重复回调。我在压测环境专门模拟过重复通知发现幂等逻辑一旦写错项目已筹金额会双倍增加这个是资金安全级别的bug宁可多写几行判断也不能偷懒。4.3 分布式事务取舍与最终一致性微服务下最头疼的就是分布式事务。领养申请提交时逻辑上要同时改adoption_apply的申请记录和animal的“申请锁定”状态但这两个操作分属 adoption-service 和 animal-service单机事务管不了跨服务问题。我开始想用 Seata 的 AT 模式全局事务后来评估了一下放弃了。原因是领养申请这个场景对一致性的实时要求没那么高锁定动物状态晚几秒生效完全可以接受而引入 Seata 意味着每个业务库都要加 undo_log 表、每个事务都要有全局锁协调器复杂度和性能开销都不小。最终我采取的方案是先调 Feign 接口尝试把动物状态改为“锁定中”修改成功后再在本地事务里插入领养申请记录如果本地事务失败反向调用 animal-service 把状态改回“待领养”最后加一张operation_log表记录整个补偿过程这套“本地事务反向补偿”的方案虽然不能做到原子性但把不一致的时间窗口压缩到几百毫秒以内对于领养这种低频业务完全够用。如果是捐赠下单这种资金场景我会用 TCC 模式保证资金操作绝不双花。这里给个实操建议不是所有业务都需要强一致能容忍最终一致就尽量别上重型分布式事务框架这是微服务落地一个很重要的成本考量。5. 前端双端落地Vue管理后台 微信小程序5.1 管理后台Vue3搭建要点管理后台我用的 Vue3 Vite Element Plus Pinia工程结构按功能模块分目录dashboard数据看板、animal动物管理、adoption领养审核、donation捐赠管理、user用户管理、system角色权限。路由用 vue-router 的动态路由模式根据用户角色从后端拉取菜单权限后再addRoute注册实现真正的按钮级权限控制。后台开发有几个细节可以分享。第一接口请求统一封装在src/api下用 axios 实例统一设置 baseURL请求拦截器里从 Pinia 拿 token 塞到 header响应拦截器里统一处理 401 跳登录和 500 错误提示这样业务代码里就只管数据不用到处写错误处理。第二列表页我封装了一个通用的usePageList组合式函数把loading、分页参数、数据列表、刷新方法全部封装进去新页面写列表只要传接口函数就能跑起来省了特别多重复代码。第三动物图片上传用了 el-upload 的http-request自定义上传方法绕开默认的 action 方式改成调 file-service 的接口拿预签名 URL 后直传 MinIO大图上传速度明显提升。5.2 小程序端页面结构与分页加载小程序端我用的是原生微信小程序框架没有上 uni-app因为原生小程序对微信登录、支付、模板消息的兼容性最直接。页面结构是 TabBar 四页首页、领养、捐赠、我的首页上放动物轮播推荐和最近上新领养页是一个可筛选的动物列表。小程序端最容易写崩的是列表加载更多逻辑。我的做法是onLoad 时请求第一页数据onReachBottom 时请求下一页用一个loading标志位防止重复请求用hasMore判断是否还有下一页。这个逻辑看着简单但很多新手会写成onReachBottom里直接page然后无条件请求导致快速滚动时发出去一堆重复请求。// 小程序端列表分页加载的稳定写法 loadAnimals(reset false) { if (this.loading || (!reset !this.hasMore)) return; this.loading true; const page reset ? 1 : this.page 1; getAnimalList({ page, size: 10, type: this.filterType }) .then((res) { const list res.records || []; this.setData({ animalList: reset ? list : this.data.animalList.concat(list), page, hasMore: res.total page * 10 }); }) .finally(() { this.loading false; }); }关于调试联调我配合 Charles 做了小程序抓包主要是看请求的 URL、header 里的 token 和响应数据结构。这里注意小程序要勾选“不校验合法域名”本地开发时才能访问 http 接口。等上线前再把 request 合法域名配上 https 地址。5.3 文件上传与对象存储接入项目里的动物图片、体检PDF、领养协议都涉及文件存储。我一开始想直接存服务器本地目录后来发现微服务多实例部署下文件会散落在不同节点用户下次请求可能路由到没存这个文件的机器上果断换成了 MinIO 对象存储。MinIO 接入 Spring Boot 比较简单引入minioJava SDK配置 endpoint、accessKey、secretKey、bucketName然后封装一个上传接口。但我遇到过一个很大坑MinIO 默认返回的 URL 是 MinIO 服务自己的地址比如http://192.168.1.10:9000/bucket/xxx.jpg这个地址小程序端是访问不了的。我后来改成了“预签名URL”方案file-service 负责生成一个带签名的临时访问链接客户端直接访问这个链接上传前端拿到文件ID再提交表单。对于上传后文件的预览网站后台用 MinIO 的桶内地址拼到 Nginx 反向代理这样客户端统一走 8080 端口访问不会把内网地址暴露出去。这个设计也顺带处理了跨域问题不用在 MinIO 层面做各种奇怪的 CORS 配置。6. 实战踩坑记录与微服务排障速查6.1 开发期高频问题第一个高频问题是 Nacos 注册不上。现象是服务启动成功但 Nacos 控制台看不到实例排查半天发现是 bootstrap.yml 的配置没生效。原因是 Spring Cloud 2021 之后默认不再自动加载 bootstrap 配置需要在 pom 里额外引入spring-cloud-starter-bootstrap依赖否则 bootstrap.yml 里的 Nacos 地址根本读不到。这是我在项目中遇到的最隐蔽的坑之一。第二个高频问题是 OpenFeign 调用时报SocketTimeoutException。原因是服务 A 调服务 B 时B 处理耗时超过 Feign 默认的 60 秒读取超时但配合的业务接口通常很快真正出问题的是某些导出报表接口。我的解决方法是给 FeignClient 单独配置超时时间不用全局统一设置追求快接口和报表接口分开对待。第三个高频问题是跨域。Gateway 路由转发时前端请求报 CORS 错误我在 Gateway 全局配置了跨域过滤器允许来源、允许方法、允许 header 都显式设置。但要注意如果网关和应用层同时配置跨域会出现 OPTIONS 预检请求被拦截两次的情况所以我统一只在网关层处理跨域后端服务不做任何跨域配置。6.2 部署期常见故障部署时我踩过最痛的一个坑是服务编排启动后捐赠服务一直连不上数据库。排查后是因为我用了同一个 MySQL 实例的多个数据库但 MySQL 8 默认的wait_timeout和 Nacos 服务发现机制配合不好导致连接池里的空闲连接被 MySQL 主动断开应用不感知继续使用就报错。解决方法是给每个数据源配置 HikariCP 的keepalive-time和connection-test-query同时在 MySQL 端把wait_timeout调大。微服务部署后接口超时是另一个高发问题。我的排查顺序是先看网关日志有没有超时记录再查具体服务的接口耗时监控最后看数据库慢查询日志。有次用户反馈领养列表打开很慢查了半天发现是animal-service里一个关联查询没建索引全表扫描 12 万行数据加了一个联合索引后接口耗时从 3.2 秒降到 80 毫秒。给所有做微服务的朋友一个建议别一上来就怀疑框架90%的“慢”都是数据库慢不是服务慢。6.3 微服务监控与日志排查技巧微服务场景下日志分散在多个容器里排查问题如果一台台翻日志会崩溃。我用的是最轻量也实用的方案所有服务统一把日志输出到 JSON 格式包含 traceId、服务名、接口路径、耗时、调用方IP然后通过 Docker 日志驱动统一收集到 Loki。排查问题时在 Grafana 里按 traceId 搜索能把一次请求在网关、用户服务、动物服务之间的完整调用链路拉出来。分布式链路追踪这块我强烈建议引入 Sleuth Zipkin 或 SkyWalking。Sleuth 的使用成本非常低引入依赖、配置采样率、日志里自动带上 traceId。我第一次上线时就是靠 traceId 查出领养状态更新慢的根因——adoption-service 里循环调用了 user-service 查询申请人信息每个用户信息查询耗时 200 毫秒循环 15 次就是 3 秒。优化思路是改成批量查询接口一次传用户 ID 列表问题直接解决。版本升级这块也提醒一下Spring Boot 的小版本升级也要谨慎我从 2.7.14 升到 2.7.18 时Gateway 的一个路由匹配策略变了导致一个小程序接口 404排查半天才定位到是版本差异。生产环境的原则是“能用就不升要升先在测试环境跑一遍全链路回归”。写在最后这个项目做下来我最大的体会是微服务不是银弹它是一套用“复杂度换弹性”的技术方案。你享受了服务独立部署、独立扩展和高可用容灾的好处就必须接受服务间调用的网络开销、分布式数据一致性难题和运维复杂度。我踩过的坑里有一半是我自己前期设计时埋下的比如跨服务查询没提前设计好批量接口、Feign超时配置没区分场景、对象存储的访问链路规划晚了。这些经验希望读者能少走一次弯路尤其是分布式事务和数据一致性这两个点一定要在动手前想清楚。如果你正准备做类似的项目可以先把服务边界画出来再考虑怎么拆表、怎么定义接口最后才是写代码。想清楚再落地比边写边改省了不止一倍的时间。