概述
掌握了 Dubbo、SpringCloud 这些技术之后,下一步是把它们真正拼成一个能跑的服务端系统。这篇文章解决的就是"上车前先看清路线"的问题:项目到底做什么、由哪些模块组成、每项技术落在哪块业务里、工程该按什么顺序搭。
纲要
- 项目定位:陌生人社交 App,以及它作为练手项目的价值
- 功能模块清单:C 端(App)与 B 端(后台管理)
- 技术栈全景:每项技术的用途与业务落点
- 架构分层:接入层 / 应用服务层 / Dubbo 域服务层 / 数据层
- 为什么要拆成"应用层 + 域服务层"两层
- 工程模块结构(tree)与模块职责表
- 一次登录请求的完整调用链
- 学习路径:阶段依赖顺序与各项技术的落点
- 环境与工具清单(含第三方服务账号准备)
- 常见误解澄清
项目是什么
交友项目是一个定位于陌生人交友市场的移动端社交 App,服务端用 Java 实现。功能上对标探探、Soul 那一类产品:左滑右滑匹配、搜附近的人、圈子动态、在线聊天、小视频。
拿它当练手项目的理由很直接------微服务项目里主流的技术点,它几乎全覆盖了:服务拆分与 RPC、注册中心与配置中心、网关、关系型与非关系型数据库、缓存、消息队列、对象存储、即时通信、推荐算法、后台管理系统。换句话说,你之前单独学的每一块知识,在这里都能找到真实的落点,而不是各自躺在 demo 里。
跟单体 CRUD 项目比,它多出来的价值在"业务复杂度"和"技术选型的理由":为什么要用 MongoDB 而不是 MySQL 存动态,为什么要用 Redis 做二级缓存,为什么聊天要走第三方而不是自己写 socket。这些问题在做单体 CRUD 项目时很少遇到,在这个项目里绕不开。
功能模块清单
按客户端形态分成两边,C 端给普通用户用,B 端给运营和审核用。
| 端 | 模块 | 核心能力 |
|---|---|---|
| C 端 | 登录注册 | 手机号 + 短信验证码登录,新用户自动注册,JWT 下发 token |
| C 端 | 完善个人信息 | 昵称、性别、生日、头像、标签,人脸识别校验头像 |
| C 端 | 交友 | 推荐好友、搜附近的人(按地理位置)、左滑右滑匹配 |
| C 端 | 圈子 | 发布动态(图文/视频)、点赞、评论、好友动态时间线 |
| C 端 | 消息 | 与好友的实时在线聊天,会话列表、历史消息 |
| C 端 | 小视频 | 短视频上传与播放,按兴趣推荐 |
| C 端 | 我的 | 访客记录、谁看过我、谁喜欢我、通用设置 |
| B 端 | 用户管理 | 用户列表、详情、冻结与解冻 |
| B 端 | 动态与视频管理 | 内容列表、详情、下架 |
| B 端 | 内容审核 | 文本/图片自动审核 + 人工复核队列 |
| B 端 | 数据统计 | 新增用户、活跃度等指标的可视化看板 |
C 端是 App 形态,B 端是 Web 后台,两边共用同一套域服务,这是后面架构分层的关键前提。
技术栈全景
只列技术名词没有意义,这张表按"这项技术在哪块业务里真正被用到"来写。
| 技术 | 用途 | 落在哪个功能里 |
|---|---|---|
| SpringBoot | 系统主框架、自动装配 | 全部模块 |
| SpringCloud Alibaba(Nacos) | 服务注册发现 + 配置中心 + 配置热更新 | 各服务之间调用与配置下发 |
| Dubbo | RPC 远程调用(替代 Feign/Ribbon,走二进制协议) | 应用层调用域服务 |
| SpringCloud Gateway | 统一入口、路由转发、跨域 | App 与后台的所有请求入口 |
| MySQL + MyBatis-Plus | 业务主数据存储 | 用户、账号、后台管理数据 |
| MongoDB | 海量数据存储 + 地理位置索引 | 圈子动态、评论、好友时间线、搜附近 |
| Redis | 缓存、验证码、计数 | 短信验证码、热点数据缓存、点赞数 |
| FastDFS | 分布式文件存储 | 小视频文件的存取 |
| 阿里云 OSS | 对象存储 | 用户头像、动态配图 |
| RabbitMQ | 异步解耦、削峰、日志采集 | 内容审核、消息推送、统计数据采集 |
| 阿里云短信 | 短信通道 | 登录验证码下发 |
| 阿里云内容审核 / 百度人脸识别 | 文本图片审核、人脸检测 | 内容审核、头像合规校验 |
| 环信(Easemob) | 第三方即时通信 SDK | 在线聊天、会话与历史消息 |
| Spark + MLlib(独立推荐服务) | 推荐算法 | 好友推荐、小视频推荐 |
| Flutter + Android(前端) | 移动端 App | C 端全部界面 |
| Vue(前端) | 后台管理系统 | B 端全部界面 |
前端部分是专业前端团队做的,服务端只需要按接口文档对数据,不用碰界面代码。但接口文档就是契约,前后端分离下拿不到 session,所以服务端要靠 token 自己解析出用户身份,这一点在登录部分会体现得很清楚。
架构分层
项目最终形态是四层,从上到下:
#mermaid-svg-1pTadRP8ZDXhCyI5{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-1pTadRP8ZDXhCyI5 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1pTadRP8ZDXhCyI5 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1pTadRP8ZDXhCyI5 .error-icon{fill:#552222;}#mermaid-svg-1pTadRP8ZDXhCyI5 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1pTadRP8ZDXhCyI5 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1pTadRP8ZDXhCyI5 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1pTadRP8ZDXhCyI5 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1pTadRP8ZDXhCyI5 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1pTadRP8ZDXhCyI5 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1pTadRP8ZDXhCyI5 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1pTadRP8ZDXhCyI5 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1pTadRP8ZDXhCyI5 .marker.cross{stroke:#333333;}#mermaid-svg-1pTadRP8ZDXhCyI5 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1pTadRP8ZDXhCyI5 p{margin:0;}#mermaid-svg-1pTadRP8ZDXhCyI5 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-1pTadRP8ZDXhCyI5 .cluster-label text{fill:#333;}#mermaid-svg-1pTadRP8ZDXhCyI5 .cluster-label span{color:#333;}#mermaid-svg-1pTadRP8ZDXhCyI5 .cluster-label span p{background-color:transparent;}#mermaid-svg-1pTadRP8ZDXhCyI5 .label text,#mermaid-svg-1pTadRP8ZDXhCyI5 span{fill:#333;color:#333;}#mermaid-svg-1pTadRP8ZDXhCyI5 .node rect,#mermaid-svg-1pTadRP8ZDXhCyI5 .node circle,#mermaid-svg-1pTadRP8ZDXhCyI5 .node ellipse,#mermaid-svg-1pTadRP8ZDXhCyI5 .node polygon,#mermaid-svg-1pTadRP8ZDXhCyI5 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-1pTadRP8ZDXhCyI5 .rough-node .label text,#mermaid-svg-1pTadRP8ZDXhCyI5 .node .label text,#mermaid-svg-1pTadRP8ZDXhCyI5 .image-shape .label,#mermaid-svg-1pTadRP8ZDXhCyI5 .icon-shape .label{text-anchor:middle;}#mermaid-svg-1pTadRP8ZDXhCyI5 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-1pTadRP8ZDXhCyI5 .rough-node .label,#mermaid-svg-1pTadRP8ZDXhCyI5 .node .label,#mermaid-svg-1pTadRP8ZDXhCyI5 .image-shape .label,#mermaid-svg-1pTadRP8ZDXhCyI5 .icon-shape .label{text-align:center;}#mermaid-svg-1pTadRP8ZDXhCyI5 .node.clickable{cursor:pointer;}#mermaid-svg-1pTadRP8ZDXhCyI5 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-1pTadRP8ZDXhCyI5 .arrowheadPath{fill:#333333;}#mermaid-svg-1pTadRP8ZDXhCyI5 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-1pTadRP8ZDXhCyI5 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-1pTadRP8ZDXhCyI5 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1pTadRP8ZDXhCyI5 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-1pTadRP8ZDXhCyI5 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1pTadRP8ZDXhCyI5 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-1pTadRP8ZDXhCyI5 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-1pTadRP8ZDXhCyI5 .cluster text{fill:#333;}#mermaid-svg-1pTadRP8ZDXhCyI5 .cluster span{color:#333;}#mermaid-svg-1pTadRP8ZDXhCyI5 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-1pTadRP8ZDXhCyI5 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-1pTadRP8ZDXhCyI5 rect.text{fill:none;stroke-width:0;}#mermaid-svg-1pTadRP8ZDXhCyI5 .icon-shape,#mermaid-svg-1pTadRP8ZDXhCyI5 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1pTadRP8ZDXhCyI5 .icon-shape p,#mermaid-svg-1pTadRP8ZDXhCyI5 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-1pTadRP8ZDXhCyI5 .icon-shape .label rect,#mermaid-svg-1pTadRP8ZDXhCyI5 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1pTadRP8ZDXhCyI5 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-1pTadRP8ZDXhCyI5 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-1pTadRP8ZDXhCyI5 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 数据层
Dubbo 域服务层(提供业务能力)
应用服务层(面向客户端,做编排)
接入层
DubboReference
DubboReference
DubboReference
HTTP
移动端 App
(Flutter + Android)
后台管理系统
(Vue)
网关 mf-gateway
路由 / 跨域 / 统一鉴权
mf-app-server
面向 App
mf-admin
面向后台
mf-dubbo-interface
接口契约
mf-dubbo-db
MySQL 域服务
mf-dubbo-mongo
MongoDB 域服务
mf-recommend
推荐服务
MySQL
MongoDB
Redis
FastDFS
阿里云 OSS
为什么分成"应用层 + 域服务层"两层
这正是 Dubbo 推荐的结构:接口和实现分模块。放到项目里,两层的分工是这样的:
- **应用层(mf-app-server / mf-admin)**负责协议适配和业务编排。它拿到 HTTP 请求,把参数解析出来,然后按顺序调用几个域服务,最后拼装返回结果。它不直接写 SQL,也不直接连 MongoDB。
- **域服务层(mf-dubbo-db / mf-dubbo-mongo)**负责提供可复用的业务能力。它是
@DubboService暴露出来的,谁都能调。
这样拆的直接好处是:App 和后台能复用同一批域服务。用户管理这个能力,App 端要"查自己的资料",后台要"查任意用户并冻结",底层查库逻辑是同一套,只是应用层的编排不同。如果写成单体,这两条路径最后会变成两份几乎一样的 Service 代码。
接口契约单独放在 mf-dubbo-interface 里,消费者和提供者都依赖它,这与"抽取公共接口模块"的常见做法完全一致。
工程模块结构
项目初始版本的工程快照如下(根目录 mf 取 makefriends 之意)。功能推进过程中还会继续加模块,先认清这个骨架。
tree
mf
├── pom.xml # 聚合工程父 POM,统一管版本
├── mf-model # 实体类(domain / vo / dto)
├── mf-commons # 工具类(JWT 等)
├── mf-autoconfig # 自动装配组件(短信、OSS、人脸等 Template)
├── mf-dubbo # Dubbo 模块聚合目录
│ ├── mf-dubbo-interface # Dubbo 接口契约(无实现)
│ ├── mf-dubbo-db # Dubbo 服务实现:MySQL 部分
│ └── mf-dubbo-mongo # Dubbo 服务实现:MongoDB 部分
└── mf-app-server # 面向 App 的入口模块
搭建完后台系统之后,骨架会长成这样(多了网关、后台、推荐三块):
tree
mf
├── mf-model
├── mf-commons
├── mf-autoconfig
├── mf-dubbo
│ ├── mf-dubbo-interface
│ ├── mf-dubbo-db
│ └── mf-dubbo-mongo
├── mf-app-server # 面向 App 的应用层
├── mf-admin # 面向后台的应用层
├── mf-gateway # SpringCloud 网关
└── mf-recommend # Spark 推荐服务
各模块职责:
| 模块 | 层级 | 职责 | 关键依赖 |
|---|---|---|---|
mf |
父工程 | 聚合模块、dependencyManagement 统一版本(SpringBoot 2.3.9、SpringCloud Hoxton.SR10、SpringCloud Alibaba 2.2.5) |
--- |
mf-model |
公共 | 实体类:com.mf.model.domain 等,供所有模块复用 |
mybatis-plus(注解用) |
mf-commons |
公共 | 工具类,如 JwtUtils 生成/解析 token |
jjwt、commons-lang3 |
mf-autoconfig |
公共 | 把第三方能力封装成 SpringBoot 自动装配组件:SmsTemplate、SmsProperties,配 META-INF/spring.factories |
阿里云短信/OSS、百度人脸、环信 SDK |
mf-dubbo-interface |
域服务 | 只放接口(如 UserApi),消费者与提供者共同依赖 |
mf-model |
mf-dubbo-db |
域服务 | UserApiImpl 用 @DubboService 暴露服务,内部走 MyBatis-Plus 操作 MySQL |
mybatis-plus、dubbo |
mf-dubbo-mongo |
域服务 | MongoDB 相关的域服务实现(圈子、好友、位置数据),初期还是空壳 | mongodb-driver、dubbo |
mf-app-server |
应用层 | 面向 App 的 HTTP 入口,Controller + Service,通过 @DubboReference 调域服务 |
dubbo、redis、nacos |
mf-admin |
应用层 | 面向后台管理的 HTTP 入口,复用同一批域服务 | 同 app-server |
mf-gateway |
接入层 | 统一入口、路由到 app-server / admin,处理跨域 | spring-cloud-gateway |
mf-recommend |
应用层 | 独立的推荐算法服务,App 侧通过 HTTP 调用 | Spark、MLlib |
这里有个容易踩的点:mf-dubbo 本身是 pom 类型的聚合目录,不要把它当成能写代码的模块。真正写代码的是它下面三个子模块。
mf-autoconfig 的设计值得单独看一眼,它把"发短信"这件事抽象成了 SmsTemplate,配置项走 SmsProperties,通过 spring.factories 注册成自动装配组件。这样业务模块只需要注入 SmsTemplate 就能发短信,第三方 SDK 的细节被挡在组件内部------换短信服务商时不用改业务代码。
一次登录请求的调用链
拿最简单的登录验证码链路,把这几层串起来看。下面是真实的示例代码。
应用层的 UserService 负责编排,它同时用到了域服务、Redis 和自动装配组件:
java
package com.mf.server.service;
import com.mf.autoconfig.template.SmsTemplate;
import com.mf.commons.utils.JwtUtils;
import com.mf.dubbo.api.UserApi;
import com.mf.model.domain.User;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
@Service
public class UserService {
@Autowired
private SmsTemplate template;
@Autowired
private RedisTemplate<String, String> redisTemplate;
// 域服务:走 Dubbo 远程调用,不是本地 Bean
@DubboReference
private UserApi userApi;
public void sendMsg(String phone) {
String code = "123456";
// template.sendSms(phone, code); // 真实环境调用阿里云短信
redisTemplate.opsForValue().set("CHECK_CODE_" + phone, code, Duration.ofMinutes(5));
}
public Map loginVerification(String phone, String code) {
String redisCode = redisTemplate.opsForValue().get("CHECK_CODE_" + phone);
if (StringUtils.isEmpty(redisCode) || !redisCode.equals(code)) {
throw new RuntimeException("验证码错误");
}
redisTemplate.delete("CHECK_CODE_" + phone);
User user = userApi.findByMobile(phone);
boolean isNew = false;
if (user == null) {
user = new User();
user.setMobile(phone);
user.setPassword(DigestUtils.md5Hex("123456"));
Long userId = userApi.save(user);
user.setId(userId);
isNew = true;
}
Map<String, Object> tokenMap = new HashMap<>();
tokenMap.put("id", user.getId());
tokenMap.put("mobile", phone);
String token = JwtUtils.getToken(tokenMap);
Map<String, Object> retMap = new HashMap<>();
retMap.put("token", token);
retMap.put("isNew", isNew);
return retMap;
}
}
域服务这一侧只关心数据,接口和实现分在两个模块:
java
package com.mf.dubbo.api;
import com.mf.model.domain.User;
public interface UserApi {
// 根据手机号码查询用户
User findByMobile(String mobile);
// 保存用户,返回用户 id
Long save(User user);
}
java
package com.mf.dubbo.api;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.mf.dubbo.mappers.UserMapper;
import com.mf.model.domain.User;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.beans.factory.annotation.Autowired;
@DubboService
public class UserApiImpl implements UserApi {
@Autowired
private UserMapper userMapper;
public User findByMobile(String mobile) {
QueryWrapper<User> qw = new QueryWrapper<>();
qw.eq("mobile", mobile);
return userMapper.selectOne(qw);
}
@Override
public Long save(User user) {
userMapper.insert(user);
return user.getId();
}
}
mf-app-server 的配置也能印证"SpringCloud 整合 Dubbo"这套写法:
yaml
server:
port: 18080
spring:
application:
name: mf-app-server
redis:
port: 6379
host: 192.168.136.160
cloud:
nacos:
discovery:
server-addr: 192.168.136.160:8848
dubbo:
registry:
address: spring-cloud://localhost # 复用 SpringCloud 的注册中心
consumer:
check: false # 开发期允许没有提供者
注意 registry.address 写的是 spring-cloud://localhost,不是入门案例里常见的 nacos://127.0.0.1:8848。这就是 SpringCloud Alibaba 整合 Dubbo 的关键差异:注册中心由 SpringCloud 统一管,Dubbo 直接复用 。check: false 是开发期必备,否则域服务没启动时应用层会直接启动失败。
整个链路的时序:
MySQL mf-dubbo-db Redis mf-app-server mf-gateway 移动端 App MySQL mf-dubbo-db Redis mf-app-server mf-gateway 移动端 App #mermaid-svg-eBIG89MdegWBxEv4{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-eBIG89MdegWBxEv4 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-eBIG89MdegWBxEv4 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-eBIG89MdegWBxEv4 .error-icon{fill:#552222;}#mermaid-svg-eBIG89MdegWBxEv4 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-eBIG89MdegWBxEv4 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-eBIG89MdegWBxEv4 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-eBIG89MdegWBxEv4 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-eBIG89MdegWBxEv4 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-eBIG89MdegWBxEv4 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-eBIG89MdegWBxEv4 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-eBIG89MdegWBxEv4 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-eBIG89MdegWBxEv4 .marker.cross{stroke:#333333;}#mermaid-svg-eBIG89MdegWBxEv4 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-eBIG89MdegWBxEv4 p{margin:0;}#mermaid-svg-eBIG89MdegWBxEv4 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-eBIG89MdegWBxEv4 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-eBIG89MdegWBxEv4 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-eBIG89MdegWBxEv4 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-eBIG89MdegWBxEv4 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-eBIG89MdegWBxEv4 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-eBIG89MdegWBxEv4 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-eBIG89MdegWBxEv4 .sequenceNumber{fill:white;}#mermaid-svg-eBIG89MdegWBxEv4 #sequencenumber{fill:#333;}#mermaid-svg-eBIG89MdegWBxEv4 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-eBIG89MdegWBxEv4 .messageText{fill:#333;stroke:none;}#mermaid-svg-eBIG89MdegWBxEv4 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-eBIG89MdegWBxEv4 .labelText,#mermaid-svg-eBIG89MdegWBxEv4 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-eBIG89MdegWBxEv4 .loopText,#mermaid-svg-eBIG89MdegWBxEv4 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-eBIG89MdegWBxEv4 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-eBIG89MdegWBxEv4 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-eBIG89MdegWBxEv4 .noteText,#mermaid-svg-eBIG89MdegWBxEv4 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-eBIG89MdegWBxEv4 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-eBIG89MdegWBxEv4 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-eBIG89MdegWBxEv4 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-eBIG89MdegWBxEv4 .actorPopupMenu{position:absolute;}#mermaid-svg-eBIG89MdegWBxEv4 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-eBIG89MdegWBxEv4 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-eBIG89MdegWBxEv4 .actor-man circle,#mermaid-svg-eBIG89MdegWBxEv4 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-eBIG89MdegWBxEv4 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} alt 用户不存在 POST /user/loginVerification phone + verificationCode 路由转发 取出 CHECK_CODE_ + phone 验证码 校验验证码 UserApi.findByMobile(phone) select ... where mobile = ? 用户记录 / null User UserApi.save(user) insert into tb_user JWT 生成 token {token, isNew} 200 OK
学习路径
整个项目按"基础 → 功能实现 → 复杂业务 → 实战"四个阶段推进,各部分的依赖关系大致是这样:
| 阶段 | 内容 | 依赖关系 |
|---|---|---|
| 前置基础 | Dubbo、Nacos、SpringCloud 整合 Dubbo | 项目的技术地基,需先掌握 |
| 环境搭建与登录 | 工程搭建、前后端分离、短信验证码、用户登录 | 起点,先跑通登录 |
| 完善用户信息 | 文件上传、人脸识别、个人信息 | 依赖登录 |
| MongoDB 基础 | MongoDB 入门与地理索引 | 可提前并行理解 |
| 圈子功能 | 发布动态、好友时间线 | 依赖 MongoDB 基础 |
| 圈子互动 | 点赞、评论、消息通知 | 依赖圈子功能 |
| 即时通信 | 环信接入、聊天气泡与历史消息 | 可与圈子互动并行 |
| 附近的人 | MongoDB geo 附近查询 | 依赖 MongoDB 基础 |
| 小视频 | FastDFS 存储、视频流 | 依赖 MongoDB 基础 |
| 后台系统 | 搭建 mf-admin 与网关 | 依赖前面沉淀的域服务 |
| 数据统计与内容审核 | RabbitMQ 异步、审核流程 | 依赖后台系统 |
| 推荐系统与实战 | Spark 推荐、综合实战 | 最后进行 |
各项前置技术在这个项目里的落点:
- Dubbo :落在应用层调用域服务的每一次
@DubboReference,以及mf-dubbo-interface/db/mongo的接口与实现拆分。 - SpringCloud(Nacos) :落在所有服务模块的
application.yml,注册发现 + 配置中心 + 配置热更新。 - SpringCloud(Gateway) :落在
mf-gateway,后台系统阶段才加入,之前 App 直连 app-server。 - Docker:落在环境搭建,Nacos、MySQL、MongoDB、Redis、RabbitMQ 这些中间件用容器起最省事。
- MQ(RabbitMQ):落在内容审核、消息推送、统计数据采集这些"不该阻塞主流程"的动作上。
节奏上有一个明确的拐点:前面八个阶段是"把功能一个个做出来",从后台系统开始是"把服务端整理成一套架构"。前者考验业务理解和编码,后者考验架构意识。
环境与工具清单
| 工具 / 服务 | 版本或说明 | 用途 |
|---|---|---|
| IDEA | 2020+ | 开发 IDE |
| JDK | 1.8 | 编译目标,父 POM 里锁定 maven.compiler.source=8 |
| Maven | 3.6+ | 依赖管理与聚合构建 |
| MySQL | 5.7 左右 | 业务主库,驱动版本 5.1.47 |
| MongoDB | 4.x | 圈子动态、地理位置 |
| Redis | 5.x | 缓存与验证码 |
| Nacos | 1.x(单机 standalone) |
注册中心 + 配置中心,默认端口 8848 |
| RabbitMQ | 3.x | 消息中间件,内容审核阶段引入 |
| FastDFS | --- | 小视频文件存储 |
| Docker | 可选但强烈建议 | 一键起中间件,省去本机装一堆服务 |
| Android 模拟器 | 网易 MuMu 等 | 跑 C 端 App |
| Postman | --- | 调服务端接口,App 未就绪时先验证接口 |
第三方服务需要自己注册申请,这是最容易被卡住的地方:
- 阿里云短信 :开通短信服务、申请签名和模板,拿到
accessKey/secret。 - 阿里云 OSS:创建 Bucket,拿到访问密钥和 Endpoint。
- 阿里云内容审核:图片/文本审核,需要开通对应服务。
- 百度人脸识别 :申请应用,拿
AppID/API Key/Secret Key。 - 环信即时通信 :注册账号、创建应用,拿
AppKey和 IM 相关密钥。
示例配置里给的 key 基本都已失效,照抄只会得到鉴权失败的报错。要么自己申请一套,要么在本地开发时先用"写死验证码"的方式绕过短信通道------示例代码里就是这么做的(UserService.sendMsg 写死验证码、注释掉 template.sendSms(...)),先把接口链路跑通比真发短信更重要。
常见误解
以为所有技术都得先搭好才能跑。 不需要。起步阶段只依赖 Nacos、MySQL、Redis 就能跑通登录;MongoDB 到圈子功能才用上,RabbitMQ 到内容审核阶段才引入。分批搭建,跑通一步再加下一步,出问题也好定位。
以为第三方服务开箱即用。 短信、OSS、人脸、环信都要自己申请账号,示例里的密钥大多已失效。示例代码把验证码写死、把 template.sendSms(...) 注释掉,就是为了让你在没有账号时也能把流程跑完。
以为这是个单体项目。 不是。它是多模块的微服务架构:应用层与域服务层分离,服务之间走 Dubbo,注册配置走 Nacos,前端走网关。模块多、依赖多,这也是它代码量偏大、节奏偏快的原因,做好心理准备。
以为掌握技术点就能直接开跑。 零散的技术点只是"零件",这个项目要的是"环境 + 账号 + 业务理解"。环境没配好、账号没申请,代码抄对了也起不来。起步阶段的环境搭建看着琐碎,但它决定了后面顺不顺。
以为应用层可以直接连库。 应用层只做编排,数据访问一律下沉到域服务层。看到 mf-app-server 里出现 Mapper 或者直接写 SQL,说明分层破了。
官方文档
- Dubbo 官方文档
- Nacos 官方文档
- Spring Cloud Alibaba 文档
- Spring Cloud Gateway
- MyBatis-Plus 官方文档
- MongoDB 地理空间索引
- RabbitMQ 官方文档
- 阿里云短信服务
- 环信即时通信云
总结
交友项目的价值不在功能本身,而在于它把"微服务项目该怎么拆、每项技术该放在哪"完整演示了一遍:应用层做编排、域服务层做能力、接口契约单独成模块、注册配置交给 Nacos、非结构化数据交给 MongoDB、异步动作交给 RabbitMQ。
看工程结构时,先记 mf-dubbo 是聚合目录不能写代码;跑项目时,先只起 Nacos + MySQL + Redis 把登录跑通,再按上面的路径往上加中间件和第三方服务。零散的技术点只是"点",这个项目把这些点连成"面"。
下一步,从工程搭建开始:先建模块、配依赖,把短信验证码和用户登录跑通,再一路往上加功能。