Java 项目实战:交友项目 —— 项目导读与整体架构

概述

掌握了 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,说明分层破了。

官方文档

总结

交友项目的价值不在功能本身,而在于它把"微服务项目该怎么拆、每项技术该放在哪"完整演示了一遍:应用层做编排、域服务层做能力、接口契约单独成模块、注册配置交给 Nacos、非结构化数据交给 MongoDB、异步动作交给 RabbitMQ。

看工程结构时,先记 mf-dubbo 是聚合目录不能写代码;跑项目时,先只起 Nacos + MySQL + Redis 把登录跑通,再按上面的路径往上加中间件和第三方服务。零散的技术点只是"点",这个项目把这些点连成"面"。

下一步,从工程搭建开始:先建模块、配依赖,把短信验证码和用户登录跑通,再一路往上加功能。

相关推荐
今年下半年1 小时前
【短信发送】Spring Boot整合多种短信接口的实现(附源码)
java·spring boot·后端·sms·qcloudsms
Source.Liu1 小时前
【A11】拥有者模式平台架构笔记
windows·笔记·架构·rust
wespaul2 小时前
VeapAI 自然语言操作实战|第 04 篇:系统数据加工——把系统自身的元数据吃成知识
java·大数据·运维·数据库·人工智能
幼儿园蛋小黑2 小时前
SPI 子系统:从原理到实践
java·前端·数据库
yangmu32032 小时前
Unity 开发过程与底层原理
java·开发语言
lexiangqicheng2 小时前
自定义表单跨端共用机制-PC设计APP渲染
java·前端·javascript
zhikouai2 小时前
AI辅助逆向工程链路:从二进制接入到等价性校验
java·linux·前端
霍格沃兹测试学院-小舟畅学2 小时前
Playwright MCP:AI Agent 驱动 Web 自动化测试的架构与执行机制
前端·人工智能·架构
随性而行3602 小时前
企业微信API接口开发实战:如何实现消息回调接收与业务事件处理?
java·前端·python·微信·机器人·企业微信