做养老院管理系统的时候,我在技术选型上纠结了很久。单体架构写着简单,但后面加需求、加并发、多团队协作都费劲,尤其养老院这种场景,涉及老人档案、膳食、床位、护理、健康档案、家属联动一大堆模块,全塞进一个工程里后期就是灾难。最终定了 SpringBoot + Vue + SpringCloud 这套微服务组合,把系统拆成多个独立服务来开发和部署。这篇就从头到尾聊聊这个"微服务分布式SpringBoot+Vue+Springcloud的养老院管理系统的设计与实现"项目怎么做、怎么落地、有哪些坑要躲。
不管你是正在做毕业设计,还是刚入行想拿一个完整的微服务项目练手,这篇内容都适合你。我会从需求拆解开始,逐步讲到服务划分、核心代码落地、膳食模块的详细设计,最后再给出我实际踩过的坑和排查思路。看完你不仅能复刻这套系统的骨架,还能把微服务架构里那些"看起来会了、一写就废"的环节理顺。
1. 系统定位与需求拆解
1.1 养老院管理系统解决什么问题
养老院管理系统本质上是一个典型的传统行业信息化项目。养老院日常工作中有几个核心痛点:老人基础档案管理混乱、膳食供应和营养搭配全靠人工记录、床位利用率不清楚、护工排班靠口口相传、家属想了解老人状态只能打电话。这个系统要解决的就是把线下人工流程搬到线上,让管理人员能实时看到养老院的运营状态。
具体到模块上,一个完整的养老院管理系统通常覆盖:老人信息管理(入住登记、健康档案、家属信息)、膳食管理(菜单制定、营养配比、订餐记录、送餐跟踪)、床位管理(房型、入住率、调房记录)、护理管理(护工排班、护理记录)、收费管理(月费、杂费、押金)、权限管理(不同角色的菜单权限和数据权限)。我做这个项目的时候,把目光重点放在了膳食管理模块上,因为它是养老院业务里最"日常"也最容易被忽略,但实际数据量最大的一个环节。
从使用者角度分层:院长/管理员关心全院数据看板;护理主管关心老人分配和护工安排;膳食专员每天要配菜单、记录老人用餐情况;护工要查看今日工作项。不同角色的操作频率和侧重点完全不同,这直接决定了微服务拆分时要从"业务角色"和"数据域"两个维度去思考,而不是简单按后端代码分层去拆。
1.2 为什么选微服务架构而不是单体架构
说实话,一个养老院管理系统如果只看业务量,单体架构完全能跑,SpringBoot 一个工程加个 MySQL 就够用了。但为什么还要上微服务?核心原因是这个项目承担了两个目标:第一是长期演进,养老院业务后续会接智能硬件(老人手环、紧急呼叫)、对接第三方支付、对接医保接口,单一系统越加越臃肿;第二是开发协作,微服务在小组内就能让前端、后端、算法、数据各管各的服务,互不阻塞。
用 SpringCloud 这套方案,本质上是借用它的生态组件来减少分布式开发的复杂度。比如 Nacos 解决服务注册发现,Gateway 解决统一入口和鉴权,Feign 解决服务间调用,Sentinel 做好服务保护。这些组件在单体里根本不需要,但一旦拆了服务就必须统一引入,否则服务之间相互调用一堆裸 HTTP 请求,地址写死、超时不管、挂了没人知道,那比单体还难受。
我在做选型评估时列了一张简单对比:
| 对比项 | 单体架构 | 微服务架构 |
|---|---|---|
| 开发初期效率 | 高,直接一个工程跑起来 | 低,需要搭注册中心、网关等基础设施 |
| 模块解耦 | 差,改一个模块要重新构建整个项目 | 好,独立构建、独立部署 |
| 扩容方式 | 整体水平扩容,资源浪费 | 按服务维度扩容,精准控制 |
| 故障影响范围 | 一个模块宕机等于全挂 | 故障隔离,部分服务仍可用 |
| 团队协作体验 | 代码冲突频繁 | 仓库独立,冲突少 |
| 运维成本 | 低 | 高(需要监控、日志、配置中心配合) |
结论很明确:如果你做的是纯 demo,单体没问题;但你要是拿它做毕设展示或是准备长期演进的商业系统,微服务这套技术栈的"展示价值"和"学习价值"都要高一档。而且现在的 SpringCloud Alibaba 已经把分布式开发的很多痛点做了封装,上手门槛比早期低很多。
1.3 微服务拆分:按业务域而不是按代码层拆
微服务拆分永远是先说清楚的问题。网上很多项目把服务拆成 "controller 服务、service 服务、mapper 服务",那种拆法完全错误,服务是垂直业务域的切片,不是水平分层的切片。
我在这个项目里按数据域来拆,拆了六个服务:
- 认证服务(auth-service):负责登录、令牌颁发、用户权限校验。
- 老人管理服务(elder-service):管理老人档案、入住信息、家属联系、健康档案。
- 膳食服务(diet-service):管理菜品库、菜谱、老人订餐、膳食配送记录、营养分析。
- 床位服务(bed-service):管理楼栋、房间、床位、入住调房记录。
- 护理服务(care-service):管理护工排班、护理任务、护理记录。
- 系统管理服务(system-service):管理后台用户、角色、菜单权限、操作日志。
每个服务拥有独立的数据库 schema,理论上数据库物理隔离,但在开发环境为了省资源,我用的是同一个 MySQL 实例里的多个数据库,比如db_elder、db_diet、db_bed这样分开。注意,微服务强调"数据独享",即使物理上共用一个 MySQL,逻辑上也绝不能直接跨库 join 查询。跨服务的数据需要通过接口调用获取,这是硬性约束,如果破了这个规矩,服务边界就形同虚设。
2. 核心技术栈选型与分析
2.1 后端选型:SpringBoot 3.x + SpringCloud Alibaba
后端框架我选的是 SpringBoot 3.2 + SpringCloud Alibaba 2023.x,搭配 JDK 17。很多人问为什么不用 SpringBoot 2.x,因为新项目的 springboot 版本太高的问题在网上比较多,但实际用下来 3.x 配 17 反而省心,JDK 17 的虚拟线程和容器友好特性对微服务部署很有价值,而且 SpringBoot 3.x 在启动速度上明显比 2.x 快。
SpringCloud 组件用的是 Alibaba 生态,这套在国内落地已经很成熟。注册中心和配置中心选 Nacos,因为它同时解决了服务发现和动态配置两个问题,而且自带中文控制台,对新手友好。网关用 SpringCloud Gateway,基于 WebFlux,性能比 Zuul 1.x 强很多。服务间调用用 OpenFeign,配合 Sentinel 做熔断降级。分布式事务用 Seata,后面详细展开。
这里有一个经验:不要一上来就把所有 SpringCloud 组件都引入。项目里只需要用到 Nacos、Gateway、OpenFeign、Sentinel、Seata 这几个核心件就够了。早期版本我用过 SpringCloud Bus + Config 做配置中心,后来发现和 Nacos 功能重复,果断删掉。微服务项目组件越多,排错链路越长,能少引入就少引入,这是务实原则。
2.2 前端选型:Vue 3 + Vite + Element Plus
前端用 Vue 3 组合式 API,构建工具选 Vite 而不是 Webpack。Vite 在开发环境热更新极快,启动项目从 Webpack 的十几秒降到两三秒,这对频繁调试前端的体验提升是巨大的。UI 组件库选 Element Plus,文档齐全,表单和表格场景做管理后台基本开箱即用。
状态管理用 Pinia 替代 Vuex。Pinia 的 API 更简洁,不需要 mutations 那层样板代码,配合组合式 API 写起来非常顺手。路由用 Vue Router 4,权限控制靠动态路由实现。重点说一下动态路由:用户登录后,后端根据用户角色返回可访问的菜单和按钮权限标识,前端拿到后动态注册路由,而不是把全部路由写死在前端代码里,这样更安全也更好维护。
前端工程为了对接多个微服务,引入了统一的 request 封装模块,基于 Axios。之前用拦截器统一在请求头加 token,响应拦截器统一处理 401、403、500 等错误码。基础配置只有一个 Nginx 反向代理,开发环境用 Vite 的 proxy 把/api前缀转发到网关,这样前端根本不用关心不同服务不同端口的问题。
2.3 数据存储与中间件:MySQL + Redis + MinIO
数据库选 MySQL 8.0,字符集统一 utf8mb4。每个微服务一个库,公共数据字典单独一个库。MySQL 8.0 相比 5.7 在窗口函数、JSON 类型、性能上都好不少,而且对微服务的多库管理更友好。
Redis 在这个系统里有三个核心用途:第一是缓存登录用户的 token 和权限信息,避免每次请求都查数据库;第二是缓存菜单和菜品数据,降低数据库压力;第三是实现分布式锁,后面讲订餐扣减库存的时候会详细展开。
文件存储选 MinIO。养老院管理系统涉及老人照片、体检报告 PDF、菜品图片等大量非结构化数据,如果把文件直接存 MySQL 会导致数据库膨胀。MinIO 是开源对象存储,兼容 S3 API,部署简单。我在 SpringBoot 里通过minio-javaSDK 接入,上传时自动生成 UUID 文件名,再和业务数据进行关联。要注意 MinIO 的桶(bucket)访问权限要按场景区分,老人隐私文件用私有读,菜品图片可以公开读。
2.4 微服务核心组件选型速查表
| 能力 | 选型 | 说明 |
|---|---|---|
| 注册中心 | Nacos | 同时承担服务发现和配置管理 |
| 网关 | SpringCloud Gateway | 统一认证、路由转发、限流 |
| 远程调用 | OpenFeign | 声明式 HTTP 客户端,带负载均衡 |
| 服务保护 | Sentinel | 熔断、限流、系统自适应保护 |
| 分布式事务 | Seata | AT 模式,对业务代码侵入小 |
| 认证方案 | Sa-Token / JWT | 选择符合业务场景的令牌方案 |
| 缓存 | Redis + Redisson | 缓存 + 分布式锁 |
| 对象存储 | MinIO | 图片、PDF、文件统一管理 |
| 网关 | SpringCloud Gateway | 统一入口,WebFlux 异步模型 |
| 链路追踪 | Micrometer Tracing + Zipkin | 排查跨服务调用问题 |
3. 系统整体架构设计
3.1 服务划分与调用链路设计
这套系统整体拓扑很清晰:浏览器访问 Vue 前端,前端所有请求打到 Nginx(开发环境是 Vite proxy),Nginx 转发到 SpringCloud Gateway 网关。网关根据 URL 前缀路由到对应服务,比如/api/elder/**到 elder-service,/api/diet/**到 diet-service。所有经过网关的请求都会过一遍全局过滤器,完成 token 校验和白名单放行。
服务之间的调用走 OpenFeign。例如老人入住的时候需要分配床位,elder-service 需要远程调用 bed-service 查询空闲床位列表;膳食专员给老人配餐时,diet-service 需要远程调用 elder-service 查询老人的过敏史和饮食禁忌。这些跨服务调用不能绕过网关直连,因为服务间调用走的是内部网络,直接用服务名即可,SpringCloud LoadBalancer 会根据服务名从 Nacos 拉取实例列表并负载均衡。
链路追踪这层我要单独提一下。微服务排查问题难度比单体大很多,因为一个请求可能跨了三四个服务。当时我引入了 Micrometer Tracing + Zipkin,在网关和每个服务里加上 traceId 传递。前端返回的响应头里带 traceId,出问题直接把 traceId 丢到 Zipkin 里就能看到整条调用链的性能消耗和失败节点。这个能力不属于"炫技",而是微服务排障的基础设施,强烈建议从一开始就接上。
3.2 统一认证与网关鉴权设计
认证方案我最终选的是 Sa-Token 结合 JWT 的混合模式。用户登录成功后,auth-service 生成 JWT,同时把会话信息写入 Redis,过期时间设 2 小时。JWT 的 payload 里只放 userId、角色编码等非敏感信息,避免 token 过大。
网关层面不做复杂的业务校验,只做三件事:第一,从请求头取出 token;第二,通过 Redis 检验 token 是否有效;第三,根据路由白名单判断这个 URL 是否需要登录。白名单包括登录接口、验证码接口、菜品图片访问等公开资源。剩余请求一律校验 token,校验失败直接返回 401,不再下发给下游服务。
这里存在一个细节:网关是 WebFlux 环境,写代码的时候不能用传统 SpringMVC 那套HttpServletRequest。踩过一次坑之后我总结的经验是:网关过滤器里操作 token 用ServerWebExchange,从exchange.getRequest().getHeaders()拿 token,再通过exchange.getRequest().mutate()把解析出来的 userId 等信息写回请求头往下游传。下游服务再通过统一拦截器从请求头获取当前用户信息,塞到 ThreadLocal 里供业务代码使用。
3.3 分布式事务:Seata AT 模式落地
分布式事务是这个系统里最难啃的一块。典型场景:老人订餐成功后,要同时扣减当日菜品的预订配额、记录订单、生成配送任务。这三个操作分别落在 diet-service 的两个表里。如果巡检服务挂了,或者写订单成功但写配送任务失败,数据就不一致了。
我选了 Seata AT 模式。AT 模式的核心思想是:业务 SQL 正常执行,Seata 自动在 undo_log 表里记录数据快照,事务提交时反向补偿。对业务代码几乎零侵入,只在需要全局事务的方法上打@GlobalTransactional注解就行。
Seata 落地时注意一个坑:数据库里必须建一个undo_log表,否则回滚日志写不进去,事务永远起不来。另一个点:AT 模式对数据库隔离级别有要求,如果用了 MySQL 默认的可重复读,要确保 Seata 的全局锁和本地事务锁不冲突。实际项目中我把 Seata 的全局事务范围控制到最小,很多非核心链路上的操作宁可最终一致也不强求强一致,因为分布式事务能不用就不用,性能损耗是实打实的。
4. 膳食管理模块详细设计
4.1 膳食模块业务需求拆解
膳食管理是这个系统的核心特色模块,我单独拿出来讲,因为它最能体现"业务要在微服务架构里落地"的完整过程。养老院的膳食工作不是简单的菜单增删改查,它的业务流程是:营养师制定周期菜谱(比如一周七天、每天三餐)→ 膳食专员根据菜谱生成每日采购清单 → 老人或护工在系统里完成订餐 → 厨房按订餐量备餐 → 配送人员按房间配送并在系统里标记完成 → 系统生成膳食报表供管理员查看成本和人效。
从角色角度去拆:营养师要看到老人的健康信息(糖尿病、高血压、过敏史)作为配餐依据;老人或家属端要能按周查看菜谱并选择菜品;护工和配送人员要在移动端接收送餐任务。因此膳食服务不是一个独立的"菜谱管理"功能,它必须同时和老人健康档案、任务分配、数据报表产生联动,这天然就是一个微服务域。
4.2 膳食模块数据库表设计
膳食服务我用了一个独立的db_diet数据库,核心表有六张:
diet_dish菜品表:维护菜品的基础信息、分类、热量、价格、图片、营养素含量。diet_menu菜谱表:定义周期菜谱头,绑定生效日期范围。diet_menu_item菜谱明细表:菜谱下每一天、每一餐包含哪些菜品。diet_order订餐记录表:记录哪位老人订了哪份餐、时间、金额、状态。diet_task配送任务表:按房间或楼层生成的送餐任务,含状态和完成时间。diet_nutrition_record营养记录表:按老人维度记录每日营养摄入汇总。
设计时特别注意了两个点。第一,菜品表和菜谱明细表是多对多关系,一定要加一张中间表来做关联,而不是傻乎乎在菜品表里加menu_id字段,否则一个菜品被多个菜谱引用时数据冗余就来了。第二,订餐表设计时加了一个order_snapshot_json字段,用来保存下单那一刻的菜品名称、价格快照。因为菜品的价格会调整,如果改价后老订单的数据跟着变,报表和财务对账就全乱套了。数据快照这个思想在业务系统开发里极其常用。
4.3 膳食服务关键接口实现
膳食服务的核心接口有两个:一个是"生成每周菜谱",一个是"老人订餐"。生成菜谱时,营养师传入开始日期,系统自动生成七天的菜谱框架,每天三餐每餐可选菜品数量由前端控制。这个功能看起来简单,但实现时要注意事务边界:菜谱头和七天明细需要在一个本地事务里提交,因为这是一个完整业务动作。
订餐接口就涉及到库存扣减了,我把"菜品的每日可订份数"存在 Redis 里,用 String 类型存储菜品加日期的 key,比如dish:count:20260415:1001。订餐时先用 Redisson 的RLock加锁,锁的 key 是lock:dish:1001:20260415,然后执行 Redis 的原子扣减。这里用 RedisLock 而不是数据库悲观锁,是因为订餐操作的频率远高于普通的 CRUD,而且 Redis 锁天然适合这种存在热点 key 的并发场景。
4.4 膳食管理前端页面联动
前端膳食管理有四个核心页面:菜品管理、菜谱制定、订餐中心、配送看板。
菜品管理页面用的是 Element Plus 的表格 + 弹窗表单,上传菜品图片走 MinIO。菜谱制定页面用的是日历组件方案,按周展示七天三餐的配置面板,每个餐次里有可选的菜品列表。订餐中心单独一条链路:老人端(或护工代点)看到一个精简的卡片列表,每张卡片是一个菜品,点击卡片完成选择,确认后调订餐接口。配送看板是实时更新的,用轮询加定时器的方式每 30 秒拉取一次今日配送任务列表,按"未配送/配送中/已完成"三个状态展示。
轮询虽然简单,但要注意不要轮询太频繁,30 秒一次完全够用。如果后面接 RabbitMQ 或者 WebSocket,能实现真正的服务端推送,但初期轮询是最稳定也最容易排错的方案,别一上来就上重型方案。
5. 实操过程与核心代码落地
5.1 环境准备:从零搭一套微服务开发环境
微服务开发环境比单体复杂不少,我先列一下我实际使用的开发环境清单:
- JDK 17 + Maven 3.9
- MySQL 8.0(本地装一个即可,多库逻辑分离)
- Redis 7.x
- Nacos 2.3.x(单机模式,使用内置 Derby 存储)
- MinIO 最新稳定版
- IDEA 2024.x + Vue 前端工程的 Vite
- Docker Desktop(用于快速启动中间件,Windows/macOS 都方便)
中间件最简单的启动方式是用 Docker Compose 一键拉起:
version: '3.8' services: mysql: image: mysql:8.0 ports: ["3306:3306"] environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_CHARACTER_SET_SERVER: utf8mb4 redis: image: redis:7-alpine ports: ["6379:6379"] nacos: image: nacos/nacos-server:v2.3.0 ports: ["8848:8848", "9848:9848"] environment: MODE: standalone minio: image: minio/minio:latest ports: ["9000:9000", "9001:9001"] command: server /data --console-address ":9001" environment: MINIO_ROOT_USER: minioadmin MINIO_ROOT_PASSWORD: minioadmin注意 Nacos 有两个端口都要映射,8848 是 HTTP 端口,9848 是 gRPC 端口。Nacos 从 2.x 开始客户端和服务端的长连接走 gRPC,如果只开 8848,服务注册时会报错"Client not connected, current status: STARTING"。这个小细节卡了我整整一下午。
5.2 公共服务与统一返回值设计
微服务工程不能每个服务里各自写一套返回体,必须抽公共依赖。我在项目里建了common-core模块,包含统一返回体Result<T>、统一异常处理器、BaseEntity(含 id、createTime、updateTime 等通用字段)、分页参数封装、JWT 上下文工具。
Result<T>的设计要点:
public class Result<T> implements Serializable { private Integer code; private String message; private T data; private Long timestamp; // 静态工厂方法 success/fail }code 统一用 200 表示成功,401 表示未认证,403 表示无权限,500 表示业务异常,超过 600 的 code 留给自定义错误码。每个服务在启动类上扫描com.xxx.common包,这样统一的异常处理器和上下文过滤器才能生效。
工程结构上我用多模块 Maven 管理,父 pom 统一管理依赖版本。子模块包括common-core、auth-service、elder-service、diet-service、bed-service、care-service、system-service。每个 service 是独立的 SpringBoot 应用,有自己独立的application.yml和启动类。
5.3 Feign 远程调用与配置
一个典型场景:老人入住登记时,前端传入房间 ID,elder-service 需要从 bed-service 查这个房间的状态和费用标准。在 elder-service 里定义 Feign 接口:
@FeignClient(name = "bed-service", contextId = "bedFeignClient") public interface BedFeignClient { @GetMapping("/api/bed/room/{roomId}") Result<RoomVO> getRoomInfo(@PathVariable("roomId") Long roomId); }注意contextId这个属性,如果不配置,一个服务里有两个 FeignClient 同时指向同一目标服务时,Spring 会报 Bean 名称冲突。实际项目里 elder-service 要调 bed-service 查房间,又要调 auth-service 查操作人信息,所以 contextId 必不可少。
Feign 调用超时设置也很关键,早期我用默认配置,遇到一个慢 SQL 导致 Feign 直接抛超时异常。后来在配置文件里统一设置了超时时间:
feign: client: config: default: connect-timeout: 3000 read-timeout: 5000另外,Feign 默认不开启日志打印,排查问题时会很痛苦。生产环境可以只打印错误日志,开发环境我开启了 FULL 级别日志,这样能看到完整的请求头、请求体、响应体。
5.4 网关过滤器与登录鉴权
网关的核心过滤器我实现了一个AuthGlobalFilter,实现GlobalFilter和Ordered接口。逻辑很简单:从 exchange 取 token,查 Redis 校验,校验通过后把用户信息写入请求头往下游传。路由白名单放在 Nacos 配置中心,可以动态调整不必重启网关。
白名单配置示例:
auth: white-list: - /api/auth/login - /api/auth/captcha - /api/diet/dish/page - /api/files/**膳食菜品分页接口之所以放进白名单,是因为访客模式(家属未登录)需要浏览菜品展示页,但下单必须登录。这种"部分接口匿名可访问"的诉求在实际项目中非常常见,设计网关白名单时要有这个意识。
5.5 前端动态路由与权限控制
前端路由设计用的方案是"动态注册"。登录成功后,后端返回当前用户可访问的菜单树,前端把菜单树转成 Vue Router 的路由配置,然后通过router.addRoute()动态注册。这样用户没权限的页面根本不会出现在路由表里,即使手动输入 URL 也会因为路由不存在而进入 404 页面。
菜单到路由的转换逻辑里有一个坑:组件路径不能简单用字符串存,比如"elder/list",因为import()动态加载时需要一个相对views目录的路径。我的做法是后端返回菜单时,约定component字段存的是相对路径,前端转换函数里做拼接:
const module = () => import(`../views/${component}.vue`)但注意 Vite 对动态 import 的限制:必须是相对路径且不能完全用变量拼接,否则构建时无法解析。我最终用一个路由映射表来解决:
const viewMap = { 'elder/list': () => import('@/views/elder/list.vue'), 'diet/menu': () => import('@/views/diet/menu.vue') }这样既绕过了 Vite 的构建限制,又保证路由懒加载生效。这个问题当时翻了不少文档才搞清楚,属于典型的一踩一个准。
6. 常见问题与排查技巧实录
6.1 服务注册不上 Nacos 的问题
现象:服务启动日志里能看到 Nacos 客户端启动成功,但控制台看不到服务列表。排查思路:第一步看 Nacos 端口映射,9848 的 gRPC 端口是否开放;第二步看服务配置文件里的spring.cloud.nacos.discovery.server-addr是否配置正确;第三步看命名空间和分组,服务端默认public命名空间、DEFAULT_GROUP,客户端如果配了别的 namespace,控制台不注意就是看不到。
最典型的错误是把server-addr配成了localhost:8848/nacos,Nacos 的地址根本不需要/nacos后缀,它是给浏览器访问控制台用的 URL。服务注册填localhost:8848就够,加后缀反而解析失败。
6.2 跨服务调用出现 500 却看不到错误日志
这是微服务排障最常见的问题。Feign 默认会吞掉下游服务返回的异常详情,只给调用方返回一个通用错误。排查技巧:先看被调服务(provider)的日志,把日志级别调到 DEBUG 或直接查 Zipkin 链路;再看调用方(consumer)的 Feign 日志,确认是不是没配置日志等级。
我后期做了一个统一处理:在公共模块里定义一个 Feign 的错误解码器,解析下游返回的 Result 对象,把里面的 message 信息直接抛给上层业务。这样调用方就能看到完整的错误原因,不再是一头雾水。
6.3 前端跨域问题
微服务架构下跨域是家常便饭。我处理跨域是在网关层统一解决,而不是让每个微服务各加一遍 CORS 配置。网关里加一个 CORS 过滤器:
@Bean public CorsWebFilter corsWebFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOriginPattern("*"); config.addAllowedMethod("*"); config.addAllowedHeader("*"); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsWebFilter(source); }注意:setAllowCredentials(true)之后addAllowedOrigin不能用*,必须用addAllowedOriginPattern("*"),这是框架的硬性约束。另外一个容易忽略的点:网关 CORS 配置好之后,下游服务就不要再加 CORS 配置,否则可能出现 CORS 头重复或者 Origin 为空的问题。
6.4 Seata 分布式事务回滚失效
现象:全局事务方法里,第一个服务调用成功了,第二个服务调用抛异常,回滚后发现第一个服务的数据没有还原。排查方向:第一看undo_log表有没有数据,AT 模式靠它回滚;第二看全局事务拦截器生效没有,@GlobalTransactional是不是打在了被同类调用(同类内部this调用)的方法上,这样代理不会拦截;第三看 Seata 的 TC(事务协调器)是否注册正常,io.seata 的日志会显示Resource Manager是否连接成功。
还有一个容易中招的点:Seata AT 模式要求所有事务分支持undo_log表,如果某个分服务漏建了,事务执行时不会报错,但回滚时这个服务的数据就恢复不了。建议在所有数据库的初始化脚本里统一加上建表语句,别每个库手动建。
6.5 Redis 分布式锁失效的场景
订餐时我用 Redisson 的RLock时,遇到过一个时钟跳跃导致锁提前过期的问题。Redisson 的看门狗机制默认每 10 秒续期一次锁,但如果操作系统时钟跳变,续期可能失败。解决方式:锁的过期时间设一个合理的上限(比如 30 秒),业务逻辑尽量控制在 2 秒内执行完,同时锁粒度按菜品加日期拆分,避免全局锁造成吞吐量瓶颈。
另外记得释放锁的代码一定放在finally块里,否则中间抛异常,锁永远不释放,后续订餐全部阻塞。这个坑我项目里真实发生过,餐都没订出去,膳食专员在后台一脸懵。
7. 避坑心得与扩展建议
整套系统从技术选型到上线维护整体跑下来,我的体会是:微服务架构本身不难,难的是"要不要拆、拆到哪一层"的决策以及排障思路。养老院管理系统本身业务不复杂,但如果你把微服务的整套规范走完——服务拆分、网关统一认证、配置中心、链路追踪、分布式事务、容器化部署——这个项目的含金量会明显不一样。
最后再分享两个值得继续扩展的方向。第一是消息异步化:现在的订餐-配送链路是同步调用的,高峰期后可以引入 RabbitMQ 或者 RocketMQ,订餐成功后就发消息,配送服务监听消息生成配送任务,顺便处理流量削峰。第二是数据同步与物化视图:膳食报表如果频繁跨服务聚合数据,可以直接在业务库建一张冗余汇总表,用定时任务或者消息通知刷新,把多表关联查询变成单表查询,性能会提升很多。
从我在实战里的观察来看,代码能力只是一部分,能否把业务需求转化成合理的服务边界和数据模型,才是这个项目磨练到的核心能力。踩坑记录上面都列出来了,照着这套思路去设计和实现,你的养老院管理系统也能稳稳落地。