☰
SpringCloud微服务实战:校园社交心理辅导平台架构与踩坑复盘
2026/10/2 10:17:36 网站建设 项目流程

去年接到校园大学生社交在线交友心理辅导平台这个项目时,我第一反应不是“交友匹配”有多难写,而是在校园网这种复杂环境下,怎么把社交互动、心理咨询预约、聊天视频这些模块稳定地塞进一套分布式微服务里。前端 Vue、后端 SpringBoot,中间再用 SpringCloud 串联,听起来是标准套路,可真把服务拆开之后,跨服务调用、数据一致性、分布式锁这些问题全都浮出来了。

这套平台的服务对象是高校学生,业务主要分两块:一块是社交在线交友,包含注册资料、兴趣标签、匹配推荐、好友申请、站内私聊;另一块是心理辅导,包含咨询师排班、学生预约、音视频咨询、回放记录。两者混在一个系统里,如果只做一个单体应用,开发倒是快,但后续想接移动端、接校内统一认证、按模块扩容,就会非常别扭。所以我最终选择了 SpringBoot + Vue + SpringCloud 的微服务分布式方案,把用户、匹配、即时通讯、咨询、文件存储拆成独立服务。

下面这篇文章是我整个项目从搭骨架到踩坑填坑的完整复盘,适合正在做类似校园社交、心理咨询类系统的人参考,尤其是想用 SpringCloud 微服务落地却不知道怎么拆服务、怎么处理分布式事务的同学。

1. 项目需求分析与架构选型

1.1 校园社交与心理辅导场景的特殊之处

校园场景跟普通社区网站差别不小。大学生社交的产品逻辑需要克制,平台不能做成陌生人社交里常见的“附近的人”那种模式,更多是基于兴趣标签、院系年级、共同活动来做推荐。心理辅导模块更特殊,学生对隐私极度敏感,咨询记录、聊天内容、预约日程这些数据必须做访问控制,不能谁登录了都能查。

我做需求调研时收集到几个核心痛点:一是学生找不到合适的聊天入口,匹配靠人工拉群效率低;二是心理咨询师排班主要靠线下表格,学生不知道哪个时段有空;三是学生和咨询师之间缺少一个安全的音视频沟通渠道。所以系统最终要解决的核心问题不是“交友”本身,而是“让合适的人在合适的时间建立连接,同时保证数据合规可控”。

这种场景还有个特点:并发量不高,但业务复杂度集中在校内网络和隐私边界上。真实并发峰值可能就是选课开放那几天,但会有大量预约操作集中在同一时间点,比如心理咨询师放出一周排班后,多个学生同时抢同一个时间段,这就直接引出分布式锁的命题。

1.2 单体架构能做,为什么还要上 SpringCloud 微服务

不少人问我,一个校园项目,用户量撑死几千,单体架构完全够用,为什么要引入 SpringCloud 微服务?我的回答是:微服务不只是为了扛并发,更是为了把复杂度拆开。

先看模块边界。这个系统有用户认证、资料标签、匹配推荐、好友关系、实时聊天、咨询预约、排班管理、文件上传、视频回放,塞进一个 SpringBoot 工程里虽然能跑,但每次改版都得担心别的模块崩掉。比如咨询模块要加“心理测评量表”,如果跟社交匹配模块放在一个服务里,改一个表结构都得全员冻结上线。拆成独立服务后,心理咨询师排班服务、匹配服务都可以单独部署升级。

第二点考虑是异构接入。前端不止是 Vue Web 端,学生端的移动端 H5、未来可能的微信小程序都要接功能。把认证、文件、聊天统一收敛到网关后面的服务里,不同端只对接网关,业务端不用重复开发。

第三点是技术团队自身的训练价值。这个项目原本就是高校信息化建设项目,同时也是一个教学实践样本。用 SpringCloud 能把服务注册发现、配置中心、网关路由、分布式锁这种“工业级”的东西真正跑一遍,比纸上谈兵强太多。

但我必须说清楚:如果这只是一个几十个人用的内网 Demo,单体绝对更省事。微服务解决的是维护成本和扩展性问题,不是为了炫技。我最终坚持拆分,是因为这个系统后续确实要常年维护,而且“心理辅导”模块未来一定会有独立的合规审计需求,单体里混着社交数据会非常难受。

1.3 整体架构设计模型

整个系统的架构我按“前端访问层—网关层—业务服务层—基础设施层”来组织:

  • 前端:Vue 3 + Element Plus + Pinia,部署在 Nginx,反向代理到网关。
  • 网关层:Spring Cloud Gateway,负责统一入口、路由转发、跨域处理、JWT 校验。
  • 业务服务层:拆出五个服务,分别是用户资料服务(user-service)、匹配交友服务(match-service)、即时通讯服务(im-service)、心理辅导服务(counseling-service)、文件存储服务(file-service)。
  • 基础设施层:MySQL 分库存储业务数据,Redis 缓存会话和热点数据,MinIO 存图片和视频文件,Nacos 做注册中心和配置中心。

各服务之间用 OpenFeign 进行声明式调用。比如匹配服务要查询用户标签,不直接连数据库,而是通过 Feign 调用用户服务接口;咨询服务要保存录音录像文件信息,也通过 Feign 调用文件服务生成上传地址。

网关不写业务逻辑,只做路由和认证。真正判断用户是否登录、有没有权限的代码,我在网关层写了一个全局过滤器,把 token 解析后放到请求头 X-User-Id 传给下游。这么做的好处是业务服务完全不用关心 token 怎么解析,只要信任网关传过来的用户 ID 就行。

2. 技术栈选型与基础设施落地

2.1 SpringBoot 与 SpringCloud 版本怎么搭配

这个项目刚开始踩的第一个坑就是版本匹配。SpringBoot、SpringCloud、SpringCloud Alibaba 三者的版本必须严格对应,否则依赖冲突能让人调一整天。

我最终使用的组合是 JDK 17 + Spring Boot 3.2.10 + Spring Cloud 2023.0.3 + Spring Cloud Alibaba 2023.0.1.0,注册中心和配置中心用 Nacos 2.3.2。这套组合在 2025 年来看依然稳定,网上资料也多,踩坑好查。

Gradle 或 Maven 选型上,我建议直接用 Maven,因为 SpringCloud 相关的 BOM 管理在 Maven 里最直观。核心依赖如下:

<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>2023.0.3</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2023.0.1.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>

这里有个经验:不要自己去各个子模块里写 spring-cloud 版本号,全部交给 BOM 统一管理。我之前试过在某个服务里单独指定版本,直接导致注册中心序列化不一致,Nacos 上一会儿能注册上一会儿就掉线。

2.2 Nacos 注册中心与配置中心

Nacos 我放在了校园服务器上,端口 8848,控制台 9848 是 gRPC 通信端口要一并放通。启动参数里我特意指定了运行模式为单机,因为校园场景不需要集群:

startup.sh -m standalone

每个服务注册时,我都在 bootstrap.yml 里配置了 namespace,用命名空间把“开发环境”和“生产环境”隔离。很多初学者忽略 namespace,一旦开发环境和生产环境连同一个 Nacos,服务列表会互相污染,网关路由都能绕乱。

spring: application: name: user-service cloud: nacos: discovery: server-addr: 192.168.1.100:8848 namespace: campus-prod config: server-addr: 192.168.1.100:8848 namespace: campus-prod file-extension: yml

配置中心我用 Nacos 管理的是那些“改起来不想重启”的参数,比如匹配算法的相似度权重、咨询预约的并发阈值、文件上传大小限制。这样运营想调匹配强度时,直接在 Nacos 控制台改配置,服务不需要发版。

2.3 Spring Cloud Gateway 网关设计

网关层我用的是 Spring Cloud Gateway,基于 WebFlux 响应式编程,性能比 Zuul 1.x 好很多。它底层走 Netty,所以网关注入的依赖要避开 javax.servlet 那套,否则端口起不来。

我配置了五条核心路由,每条路由对应一个服务:

spring: cloud: gateway: routes: - id: user-service uri: lb://user-service predicates: - Path=/api/user/** filters: - StripPrefix=1 - id: match-service uri: lb://match-service predicates: - Path=/api/match/** filters: - StripPrefix=1 - id: im-service uri: lb:ws://im-service predicates: - Path=/ws/** - id: counseling-service uri: lb://counseling-service predicates: - Path=/api/counseling/** filters: - StripPrefix=1 - id: file-service uri: lb://file-service predicates: - Path=/api/file/** filters: - StripPrefix=1

注意看 im 服务那条,我特意用了 lb:ws 前缀,这是因为 WebSocket 的握手协议要走 ws:// 而不是 http://,Spring Cloud Gateway 对 WebSocket 路由默认支持这种写法。

网关层还做了全局跨域配置,不然 Vue 开发环境 8080 端口调网关 8080 端口必然被浏览器拦截。我直接写了一个 CorsWebFilter 的 Bean,allowedOrigins 允许校园网的域名,allowedMethods 允许全部常用方法。

2.4 MySQL、Redis、MinIO 的分工组合

存储层的分工很简单:MySQL 管结构化业务数据,Redis 管缓存、在线状态和分布式锁,MinIO 管图片、视频、音频等文件资源。

MySQL 我是按服务分库的,不是把所有表塞进一个库。user-service 用 user_db,match-service 用 match_db,im-service 用 im_db,counseling-service 用 counseling_db。这么分的直接好处是后续某一库的压力大,可以单独做读写分离,不影响其他库。

Redis 我主要干了三件事:缓存登录会话和验证码、缓存匹配推荐结果、实现分布式锁。

比如登录验证码,我把验证码存在 Redis 里,key 为 login:captcha:{uuid},过期时间五分钟。推荐结果缓存用的是 ZSET,按匹配分排序,分页时直接走 ZRANGE,减少数据库压力。

MinIO 在校园内网部署了一台,用来存储用户头像、咨询视频回放、心理课件等。它的优势是兼容 S3 API,而且部署极简,单机只需要一个 bucket 和 access key 就能跑起来。我之前想过直接存 Nginx 静态目录,但面对视频分片和防盗链太难管理,MinIO 自带预设策略和签名 URL,更适合这种场景。

3. 微服务边界划分与数据模型设计

3.1 按哪些维度拆分服务

服务拆分是整个项目最关键也最容易过度的环节。拆太细,比如把排班单独拆一个服务,服务间调用链变得又长又绕;拆太粗,微服务就变成了“单体分模块”。

我采用的拆分标准是“业务域 + 数据生命周期”。用户资料、社交匹配、即时通讯、心理辅导、文件存储这五个域的数据变化频率和访问控制级别明显不同,适合拆开。其中文件存储服务虽然是基础设施性质的,但因为要管 MinIO 的临时凭证和安全策略,值得独立。

另外我特意把“心理辅导服务”拆成一个独立服务,哪怕它的代码量不大。因为心理辅导涉及预约订单、咨询师排班、咨询记录,这是一条完整的业务链,而且未来如果学校需要导出心理健康数据分析,可以直接从这个服务取数,不会打扰社交模块。

3.2 各服务的数据模型设计

数据模型上我没有追求极致的分布式设计,而是尽量保持“单一服务主导一张表”。

user-service 核心表包括:

  • user_account:账号表,字段有 id、student_no、password_hash、status。
  • user_profile:资料表,关联 account_id,存昵称、院系、年级、头像地址、一句话介绍。
  • user_tag:标签表,一个用户最多十个标签,标签名提前维护,避免用户随便填造成匹配噪音。

match-service 主要表:

  • match_like:用户对用户的右滑喜欢记录,包含 initiator_id、target_id、status。
  • match_similarity:定期计算好的用户相似度结果,给推荐列表排序用。
  • friend_request:好友申请表,记录发起方、接收方、验证消息和状态。

im-service 的表:

  • conversation:会话表,保存双人会话 id 和最近一条消息摘要。
  • message_record:消息记录表,字段 sender_id、receiver_id、content_type、content、create_time。

counseling-service 的表最多:

  • counselor:咨询师表,复用 user_account 里的人员 id,再补充专业方向、证书编号、简介。
  • counseling_schedule:排班表,咨询师可用的时间段。
  • counseling_order:预约订单表,记录学生 id、咨询师 id、时间槽 id、状态(待确认/已完成/已取消)。
  • counseling_record:咨询记录表,关联订单 id,存咨询开始时间、结束时间、视频文件地址。

每个库的 id 我全部使用雪花 ID,因为微服务环境下如果用数据库自增主键,多个服务的 id 全局不唯一,后面做数据汇总会非常痛苦。MyBatis-Plus 自带 IdType.ASSIGN_ID,可以直接生成雪花 ID。

3.3 跨服务数据一致性怎么做

微服务里最头疼的就是数据一致性。我这里有两个典型场景。

第一个场景是心理咨询预约。学生点击预约时,counseling-service 要扣减这个时间槽的预约名额,同时生成一条预约订单。这个操作必须保证不超卖。我用的是“Redis 分布式锁 + MySQL 唯一索引”的双保险。

第二个场景是用户匹配成功后,match-service 要往 im-service 里建一个会话。如果匹配服务写完好友关系后,直接调用 im-service 建会话,一旦 im-service 宕机,会话就丢了,用户看到“已匹配”但没有聊天入口。

这种场景我采取“最终一致性”思路:不用 Seata 全局事务,而是让 match-service 落一条好友关系数据后,发一个本地事件,由异步线程去调用 im-service 建会话,如果调用失败则重试三次,还是失败就记录到一张本地消息表,由定时任务补扫。反正创建会话这件事可以延迟十秒,不影响用户体验。

有一个原则必须强调:跨服务调用的写操作,一定要设计成幂等的。im-service 创建会话接口以 conversation_key 做了唯一索引,即使 match-service 重试多次,会话也不会重复创建。

4. 核心功能实现与踩坑记录

4.1 基于 JWT 的登录认证体系

认证链路我设计成“网关校验 + 服务间透传”。用户输入学号和密码登录后,user-service 校验账号密码,生成 JWT 令牌返回给前端。JWT 里只存用户 id、角色、过期时间,不存敏感信息。后续请求携带 Authorization: Bearer 头,网关过滤器解析 token,把 userId 写进 X-User-Id 头再转发给下游服务。

这里有一个关键点:JWT 是无状态的,如果要让用户修改密码后旧 token 立即失效,光靠 JWT 做不到。我把“token 黑名单”放进了 Redis。用户修改密码或退出登录时,把旧 token 的 jti(JWT ID)存入黑名单,设置过期时间等于 token 剩余有效时间。网关过滤器每次先查 Redis 黑名单,存在就拒绝访问。

前端 Vue 端我用 axios 拦截器统一管理 token:

service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) config.headers.Authorization = 'Bearer ' + token return config }) service.interceptors.response.use( response => response, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') router.push('/login') } return Promise.reject(error) } )

注意 401 处理不能简单跳转登录页,因为有些请求是用户无感进行的,突然跳转会打断操作。我后来改成弹一个二次确认提示框,点击“重新登录”才跳转。

4.2 交友匹配:基于标签向量的相似度打分

匹配功能我最初想用 Redis 的 Geo 做基于位置的匹配,后来想想大学校园里位置其实没意义,重点是兴趣和院系。所以我改成标签相似度算法。

每个用户注册时从平台维护的标签库里选最多十个,比如“跑步”“动漫”“代码”“吉他”“读书”。匹配服务把所有标签做成标签字典,每个用户就是一个由 0/1 组成的向量,其中 1 代表拥有该标签。两个用户之间的相似度,我用余弦相似度加额外加权因子来计算。

核心计算逻辑类似这样:

public double score(UserProfile me, UserProfile other) { double base = cosineSimilarity(me.getTagVector(), other.getTagVector()); double bonus = 0; if (me.getCollege().equals(other.getCollege())) { bonus += 0.15; } if (me.getGrade().equals(other.getGrade())) { bonus += 0.1; } return base + bonus; }

相似度结果不是每次请求都现场算的。我写了一个定时任务,每天凌晨跑全量用户之间的相似度计算,结果存到 match_similarity 表,同时把 TopN 结果推入 Redis 缓存。用户刷新推荐页时,直接从 Redis 读,速度非常快。

踩坑提醒:这个地方千万不要设计成“用户点查询时现场计算”,一旦用户量到几千,全量两两比较是千万级计算量,接口延迟能飙到几秒。定时预热是必须的。

4.3 心理咨询预约的分布式锁

心理咨询预约是这个项目里最需要保护的业务。举个例子:咨询师周一放出一个时间段,比如周三下午 14:00-14:50,五个学生同时点预约,如果业务代码先查有没有被约、再插入订单,中间没有任何加锁,就会出现五个人都显示预约成功,可这个时间段的订单字段还是空的。

我用 Redis 分布式锁锁住这个时间槽的预约操作。获取锁的代码核心如下:

String lockKey = "counsel:slot:" + scheduleId; String requestId = UUID.randomUUID().toString(); Boolean locked = redisTemplate.opsForValue() .setIfAbsent(lockKey, requestId, 30, TimeUnit.SECONDS); if (Boolean.TRUE.equals(locked)) { try { // 真正执行预约下单逻辑 counselingOrderService.createOrder(studentId, scheduleId); } finally { // 用 Lua 脚本释放锁,防止误删 String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Collections.singletonList(lockKey), requestId); } }

这里有两个细节特别重要。第一,setIfAbsent 后面跟的 30 秒是过期时间,防止线程执行到一半挂了导致锁永远不释放。第二,释放锁不能直接 del key,必须先判断 value 是不是自己放的,否则可能把别人刚获取的锁删掉。我用 Lua 脚本保证判断和删除是原子操作。

分布式锁保证的是同一时刻只有一个请求在操作,但为了保险,我在 counseling_order 表的 student_id 和 schedule_id 上还加了唯一索引。即使极端情况下分布式锁失效,数据库也能兜底。

4.4 即时聊天与 WebSocket 接入

即时通讯模块我用了 Spring Boot 自带 WebSocket 功能,配合原生 WebSocket API,没有引入额外的消息队列中间件。由于 IM 服务是独立部署的,前端连接地址不能写死某个实例,必须走网关的 ws 路由。

网关配置里的 lb:ws://im-service 已经支持了 WebSocket 转发,前端连接地址写:

ws://gateway地址/ws?token=xxxx

WebSocket 握手时在 query 参数带 token,服务端握手拦截器先校验 token,校验通过才能建立连接。

在线状态我存在 Redis 里,key 为 online:user:{userId},value 是当前连接的设备类型和服务器地址。服务端收到消息后先判断接收方是否在线,在线就直接转发给对应 WebSocket 连接;不在线就把消息落库,等对方上线后推送离线消息提醒。

前端 Vue 页面在用户登录后用 JS 原生创建 WebSocket:

const ws = new WebSocket('ws://' + baseUrl + '/ws?token=' + token) ws.onmessage = function (event) { const data = JSON.parse(event.data) // 更新聊天列表或弹出新消息提醒 }

聊天消息内容我统一用 JSON 传输,包含 type、fromId、toId、content、timestamp 五个字段。图片消息不传输 base64,而是先调文件服务上传拿到 URL,再把 URL 放进 content 字段,否则消息体太大。

4.5 Vue 3 前端接入的几个细节

前端部分我用的是 Vue 3.4 + Element Plus 2.x + Pinia + Vue Router 4,整体是当前 Vue3 生态里最通用的组合。

路由设计上我做了两级角色区分:普通学生端和管理员端、咨询师端。Vue Router 的 beforeEach 守卫里读取本地存储的角色信息,结合 meta.roles 判断当前用户能不能访问某个页面。比如心理咨询师的排班管理页面,学生角色访问时直接重定向到首页。

音频视频回放部分,客户端只播放 m3u8 的直播或回放流。这里有个常见的坑:浏览器原生 video 标签并不直接支持 m3u8 格式,Safari 因为内核支持 HLS 能放,Chrome 和 Edge 放不了。我引入了 hls.js 这个播放器库,在 Chrome 上通过 MSE 播放 m3u8,代码量很小:

import Hls from 'hls.js' if (Hls.isSupported()) { const hls = new Hls() hls.loadSource(videoUrl) hls.attachMedia(videoElement) }

hls.js 的好处是不需要用户安装任何播放器插件,纯 Web 播放,正好符合“Vue 播放 m3u8 免安装”的需求。初始化前记得判断 Hls.isSupported(),Safari 走的是原生逻辑,不要强制用 hls.js 再包一层。

5. 常见问题与实战排查技巧

5.1 Feign 调用超时与连接不够

上线后收到的第一个投诉就是“打开推荐页很慢,有时候报 load balancer does not have available server for client”的错误。我查了半天,发现不是负载均衡器的问题,而是 match-service 服务启动时,user-service 还没上线,Feign 第一次调用走到空路由。

这个问题通过两件事解决:第一,服务启动顺序上,先启动注册中心,再启动用户服务和文件服务,最后启动依赖方比较多的匹配服务;第二,Feign 连接超时和读取超时单独设置,避免默认 60 秒等太久:

feign: client: config: default: connectTimeout: 3000 readTimeout: 5000 loggerLevel: basic

同时我给所有 Feign 接口的 fallback 降级都写了兜底逻辑。比如匹配列表调用用户服务失败时,直接返回一个空列表,前端显示“加载失败”,而不是整个接口 500。降级逻辑一定要写在独立类里,不能在接口方法上硬编码 try-catch,否则 Feign 的重试机制会失效。

5.2 Nacos 配置修改后不生效

这个问题我在开发阶段反复碰见。明明在 Nacos 控制台改了配置,保存成功,服务却没有任何反应。后来发现原因是我漏了刷新配置的注解和依赖。

单纯引入 Nacos Config 不够,必须保证配置类支持动态刷新。我给需要动态读取配置的类加了 @RefreshScope,同时在配置中心里填写了 dataId(服务名 + profile + 文件后缀)。如果 dataId 写错,配置根本加载不到,启动时静默失败,非常迷惑人。

排查这类问题有个技巧:在服务启动日志里搜索 “Located property source” 这个关键字,看 Nacos 是否真的加载到了配置。如果没加载到,九成是 dataId 拼写或 namespace 写错。

5.3 分布式锁误删与过期时间设置

分布式锁我实际踩过两个坑,都是自己代码写得不严谨导致的。

第一个坑是释放锁时直接 del key。某次咨询预约接口压力测试时,线程 A 获取锁后处理业务超过了预设过期时间,锁自动过期了;线程 B 立即拿到新锁;此时线程 A 处理完,finally 里删锁,把线程 B 的锁删掉了。后面的线程 C 也能拿到锁,预约流程就被打乱。修复方式就是我上文写的 Lua 脚本,删除前校验 value 是否属于自己的请求 ID。

第二个坑是过期时间设置太短。我最初设了 10 秒,但咨询预约接口里要调学生服务查资料、调文件服务生成视频上传地址,整体耗时超过了 10 秒,导致业务没跑完锁先过期。后来我把过期时间调到 30 秒,同时在锁没释放时前端会提示“请勿重复提交”。

这里也说明一个经验:分布式锁不能只靠过期时间,业务设计上尽量把同步操作降到最低,尽量把耗时的外部调用挪到拿到锁的 try 块之外。

5.4 MinIO 文件访问与 m3u8 播放的坑

MinIO 本身部署不难,难的是访问控制和浏览器兼容。

视频回放我最初直接把 m3u8 文件的公开访问地址给前端,省事是省事,但存在两个问题:一是数据泄露,拿到链接的人不登录也能看咨询回放;二是访问地址里如果不带签名,外部可以直接遍历。后来我改用 MinIO 的预签名 URL 机制:文件服务生成一个临时访问地址,默认 15 分钟有效,前端只拿这个临时地址去拉流。

还有一个容易忽略的配置:MinIO 控制台的 bucket 设置里,如果涉及 Web 端直接播放,必须正确配置 CORS 规则。否则浏览器发起跨域请求拉 m3u8 切片时会被拦截,播放器黑屏但后端日志没有异常。

我以前也遇过一种情况,m3u8 文件在 MinIO 里能下载,但前端播放非常卡,视频每隔几秒缓冲一次。排查后发现问题出在 MinIO 的 bucket 名里包含了下划线。HLS 切片寻址在某些播放器场景下对下划线兼容不好,建议 bucket 命名统一用小写字母和连字符,比如 counseling-video 而不是 counseling_video。

5.5 网关跨域与前端打包部署

最后再补一个前端部署的细节。Vue 项目开发时我们用 vite 代理解决跨域,生产环境则把前端构建产物放到 Nginx。此时跨域问题通常要落到网关处理,因为 Nginx 到网关的请求是服务端到服务端,不存在浏览器跨域,网关的 CORS 配置针对的是浏览器直连网关的场景。

但如果你是“前端打包放进 SpringBoot 里”这种部署方式,也就是直接把 dist 目录丢到一个 web 服务的静态目录,那要特别注意接口地址不能写死。我更推荐把 dist 目录用 Nginx 单独提供服务,然后 Nginx 配置里把 /api 前缀反向代理到网关地址,这样前后端配合最灵活,后续网关增加路由也不需要重新打包前端。

网关层的全局 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); }

需要注意的是 allowCredentials 为 true 时,allowedOrigin 不能用 *,要用 allowedOriginPattern,不然 Spring 启动时会直接报错。

写在最后的小体会

做完这套平台,我最大的感触是:微服务架构真正难的不是 SpringCloud 组件怎么用,而是怎么在动手写代码之前把服务边界想清楚。我前前后后重构过两次服务拆分,第一次把聊天的会话功能放在 match-service 里,结果每次优化聊天都要牵连匹配服务,后来狠下心单独拆了 im-service,代码结构清爽了很多。

还有一点想提醒所有做校园系统的人:心理辅导模块的数据比社交数据敏感得多,哪怕是练手项目,也要从架构层面把访问控制设计好,咨询记录不能被普通用户接口顺路查到,文件访问要用临时签名,这些都做不到的话,功能做得再多也不敢上线。

如果未来要在这个项目上扩展,我强烈建议优先补上监控告警。微服务节点一多,每个请求要经过网关、用户、匹配、文件多个跳转,没有链路追踪很难定位慢接口。可以用 SkyWalking 或者 Micrometer + Prometheus 把调用链路和指标拉起来,在线排查问题的效率会翻倍。

最后一个小建议:如果是第一次做 SpringCloud 微服务项目,不要一上来就拆五个服务,先把用户服务和文件服务拆出来,把网关、注册中心、Feign 调用跑通,然后一个一个往里加服务。这样每次只引入一个新的复杂度点,出问题了能一眼定位。分布式这潭水,深浅得自己淌一遍才知道。

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

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

立即咨询