概述
交友项目第一章从零把一个散装的 Maven 空工程,搭成了一个能注册到 Nacos、能发短信、能用手机号 + 验证码登录的微服务骨架。本篇不复述每一步怎么点,而是把这一章的五个板块串成一条链,抽走那些"下次还会再写一遍"的套路,并给出一份能自己勾选的自检清单。
纲要
- 本章一句话产出:可运行的微服务骨架 + 手机号验证码登录(含首次登录自动注册)
- 本章知识地图:五个板块的依赖顺序(前一板块是后一板块的前提)
- 工程终态:第一章结束时的真实模块清单与关键类
- 可复用套路清单:第三方能力封装四件套 / Dubbo 三件套 / 验证码 + Redis / JWT / BasePojo
- 关键坑回顾:
spring.factories路径、Dubbo 注册协议、序列化、123456硬编码 - 自检清单:八项可勾选验证
- 练习建议:唯一索引 + 频控 + 密钥外置
- 下一章预告:完善用户信息(头像、人脸识别、统一鉴权)
本章一句话产出
一句话概括:用 Dubbo + Nacos 搭起可注册的微服务骨架,跑通"手机号 + 验证码"登录,新用户在登录时自动注册,并把时间字段的填充收敛到公共基类。
这句话里有三个动作,缺一个链路就断:骨架要先注册得上去,短信才发得出去;验证码要能落进 Redis,登录时才有东西校验;用户表要能查、能写,自动注册才成立。
本章知识地图
五个板块不是并列的,是有严格先后依赖的。前一板块不成立,后一板块就没有落脚点。
#mermaid-svg-bTGzKH5Khg8bRuRq{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-bTGzKH5Khg8bRuRq .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-bTGzKH5Khg8bRuRq .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-bTGzKH5Khg8bRuRq .error-icon{fill:#552222;}#mermaid-svg-bTGzKH5Khg8bRuRq .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-bTGzKH5Khg8bRuRq .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-bTGzKH5Khg8bRuRq .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-bTGzKH5Khg8bRuRq .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-bTGzKH5Khg8bRuRq .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-bTGzKH5Khg8bRuRq .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-bTGzKH5Khg8bRuRq .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-bTGzKH5Khg8bRuRq .marker{fill:#333333;stroke:#333333;}#mermaid-svg-bTGzKH5Khg8bRuRq .marker.cross{stroke:#333333;}#mermaid-svg-bTGzKH5Khg8bRuRq svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-bTGzKH5Khg8bRuRq p{margin:0;}#mermaid-svg-bTGzKH5Khg8bRuRq .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-bTGzKH5Khg8bRuRq .cluster-label text{fill:#333;}#mermaid-svg-bTGzKH5Khg8bRuRq .cluster-label span{color:#333;}#mermaid-svg-bTGzKH5Khg8bRuRq .cluster-label span p{background-color:transparent;}#mermaid-svg-bTGzKH5Khg8bRuRq .label text,#mermaid-svg-bTGzKH5Khg8bRuRq span{fill:#333;color:#333;}#mermaid-svg-bTGzKH5Khg8bRuRq .node rect,#mermaid-svg-bTGzKH5Khg8bRuRq .node circle,#mermaid-svg-bTGzKH5Khg8bRuRq .node ellipse,#mermaid-svg-bTGzKH5Khg8bRuRq .node polygon,#mermaid-svg-bTGzKH5Khg8bRuRq .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-bTGzKH5Khg8bRuRq .rough-node .label text,#mermaid-svg-bTGzKH5Khg8bRuRq .node .label text,#mermaid-svg-bTGzKH5Khg8bRuRq .image-shape .label,#mermaid-svg-bTGzKH5Khg8bRuRq .icon-shape .label{text-anchor:middle;}#mermaid-svg-bTGzKH5Khg8bRuRq .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-bTGzKH5Khg8bRuRq .rough-node .label,#mermaid-svg-bTGzKH5Khg8bRuRq .node .label,#mermaid-svg-bTGzKH5Khg8bRuRq .image-shape .label,#mermaid-svg-bTGzKH5Khg8bRuRq .icon-shape .label{text-align:center;}#mermaid-svg-bTGzKH5Khg8bRuRq .node.clickable{cursor:pointer;}#mermaid-svg-bTGzKH5Khg8bRuRq .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-bTGzKH5Khg8bRuRq .arrowheadPath{fill:#333333;}#mermaid-svg-bTGzKH5Khg8bRuRq .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-bTGzKH5Khg8bRuRq .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-bTGzKH5Khg8bRuRq .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-bTGzKH5Khg8bRuRq .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-bTGzKH5Khg8bRuRq .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-bTGzKH5Khg8bRuRq .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-bTGzKH5Khg8bRuRq .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-bTGzKH5Khg8bRuRq .cluster text{fill:#333;}#mermaid-svg-bTGzKH5Khg8bRuRq .cluster span{color:#333;}#mermaid-svg-bTGzKH5Khg8bRuRq 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-bTGzKH5Khg8bRuRq .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-bTGzKH5Khg8bRuRq rect.text{fill:none;stroke-width:0;}#mermaid-svg-bTGzKH5Khg8bRuRq .icon-shape,#mermaid-svg-bTGzKH5Khg8bRuRq .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-bTGzKH5Khg8bRuRq .icon-shape p,#mermaid-svg-bTGzKH5Khg8bRuRq .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-bTGzKH5Khg8bRuRq .icon-shape .label rect,#mermaid-svg-bTGzKH5Khg8bRuRq .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-bTGzKH5Khg8bRuRq .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-bTGzKH5Khg8bRuRq .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-bTGzKH5Khg8bRuRq :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 决定模块怎么切
模块不定好,组件没地方放
依赖没治理好,组件引不进来
发不出验证码,登录无从谈起
created/updated 反复手写才想抽基类
项目认知
是什么 / 什么架构 / 有哪些模块
工程准备
工具环境 + 模块划分 + 依赖治理
第三方能力接入
阿里云短信 + 自动装配组件
用户登录
验证码 -> JWT -> 自动注册
代码优化
BasePojo + 自动填充
依赖关系说穿了就三句话:
- 模块划分不定好,
mf-autoconfig这个组件就没地方放,mf-dubbo-interface也不能被别的模块依赖。 - 依赖没在父工程
dependencyManagement里统一版本,组件引进来就是版本漂移,A 模块 3.4.1 的 MyBatis-Plus、B 模块 3.5.x,跑起来才报错。 - 短信和 JWT 都是登录的前置条件:前者负责把验证码送到用户手里,后者负责把"登录成功"这个状态封成 stateless 的凭证。
工程终态:第一章结束时长什么样
实读第一章代码快照,模块清单与讲义 3.2.2 的模块表一致,八个模块全部就位(mf-dubbo-mongo 在这一章还是空壳,只建了目录)。
tree
mf/ # 父工程(聚合 + 统一版本管理)
├── pom.xml
├── mf-autoconfig/ # 自动装配组件(第三方能力封装)
│ └── src/main/
│ ├── java/com/mf/autoconfig/
│ │ ├── mfAutoConfiguration.java
│ │ ├── properties/SmsProperties.java
│ │ └── template/SmsTemplate.java
│ └── resources/META-INF/spring.factories
├── mf-model/ # 实体类模块
│ └── src/main/java/com/mf/model/domain/
│ ├── BasePojo.java # created / updated 自动填充基类
│ └── User.java # 继承 BasePojo
├── mf-commons/ # 工具类模块
│ └── src/main/java/com/mf/commons/utils/JwtUtils.java
├── mf-dubbo/ # Dubbo 模块的父级(仅作管理)
│ ├── mf-dubbo-interface/ # Dubbo 接口(契约)
│ │ └── com/mf/dubbo/api/UserApi.java
│ ├── mf-dubbo-db/ # Dubbo 服务(数据库部分)
│ │ └── com/mf/dubbo/
│ │ ├── DubboDBApplication.java
│ │ ├── api/UserApiImpl.java
│ │ ├── handler/MyMetaObjectHandler.java
│ │ └── mappers/UserMapper.java
│ └── mf-dubbo-mongo/ # Dubbo 服务(MongoDB 部分,暂空)
└── mf-app-server/ # 与手机端交互的入口
└── com/mf/server/
├── AppServerApplication.java
├── controller/LoginController.java
└── service/UserService.java
两个关键产物在这一章定型:mf-autoconfig 里的短信组件被 mf-app-server 直接 @Autowired 就能用,证明自动装配真的生效;mf-model 的 User 继承 BasePojo,把 created/updated 从业务代码里彻底摘掉。
可复用套路清单
这一章的价值不在"写了哪些类",而在于几个会反复出现的模式。后面每个业务域基本都是在套这几张模板。
| 套路 | 本章用在哪 | 后续哪里还会再用 |
|---|---|---|
第三方能力封装四件套:Properties + Template + AutoConfiguration + spring.factories |
阿里云短信 | OSS 上传、人脸识别、内容审核,凡是"接一个云厂商 SDK"都是同一套 |
Dubbo 服务三件套:interface 契约 + Impl 实现 + 消费者 @DubboReference |
UserApi / UserApiImpl |
后续每个新业务域(动态、推荐、IM)都要新建一组 |
| 验证码 + Redis 组合 | 登录验证码,5 分钟 TTL | 凡是短时效数据(临时凭证、一次性令牌、防重复提交标记)都能照此办理 |
| JWT 签发与校验 | 登录后签发 token,携带 id + mobile |
后续所有需要身份识别的接口都靠它,配合统一鉴权拦截器 |
BasePojo + 自动填充 |
created / updated |
后续所有持久化实体都继承它,不再手写时间字段 |
四件套的骨架长这样,换个第三方 SDK 只需替换 SmsTemplate 里的实现。
text
# mf-autoconfig/src/main/resources/META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.mf.autoconfig.mfAutoConfiguration
java
package com.mf.autoconfig;
import com.mf.autoconfig.properties.SmsProperties;
import com.mf.autoconfig.template.SmsTemplate;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
// 自动装配入口:被 spring.factories 声明后,其他工程引入本模块即自动生效
@EnableConfigurationProperties({SmsProperties.class})
public class mfAutoConfiguration {
// 把配置类里的参数注入 Template,再以 Bean 的形式暴露给使用者
@Bean
public SmsTemplate smsTemplate(SmsProperties properties) {
return new SmsTemplate(properties);
}
}
Dubbo 三件套的分工值得记牢:mf-dubbo-interface 只放 UserApi 接口,被提供者和消费者共同依赖;mf-dubbo-db 里 UserApiImpl 用 @DubboService 暴露;mf-app-server 里用 @DubboReference(新版本注解,等价于旧的 @Reference)引用。接口模块不依赖实现模块,这是解耦的关键。
消费者端的配置也是固定模板,注意 registry.address 用 spring-cloud://localhost(走 Nacos 注册中心),不是 nacos://。
yml
# mf-app-server/src/main/resources/application.yml
server:
port: 18080 # Web 端口
spring:
application:
name: mf-app-server
redis: # 验证码存这里
host: 192.168.136.160
port: 6379
cloud:
nacos:
discovery:
server-addr: 192.168.136.160:8848
dubbo:
registry:
address: spring-cloud://localhost # 由 Nacos 托管注册发现
consumer:
check: false # 启动时不强校验提供者是否存在
本章的关键坑回顾
坑都集中在"配置写对但装不上/发现不到",而且很多是静默失败------不报错,就是没效果。
| 坑 | 症状 | 解法 |
|---|---|---|
spring.factories 路径写错 |
短信组件 @Autowired 注入失败或注入为 null,启动无异常 |
必须放在 resources/META-INF/spring.factories,文件全名不能差一个字母;先确认模块已 install |
dubbo.scan.base-packages 写错 |
提供者启动正常,但注册中心里看不到服务 | 路径要精确到 com.mf.dubbo.api,与 @DubboService 所在包一致 |
spring-cloud:// 与 nacos:// 混淆 |
服务注册不上,或消费者发现不到提供者 | 走 Nacos 注册中心时统一用 spring-cloud://localhost |
| Web 端口与 Dubbo 协议端口混用 | 两个服务抢占端口,或调用超时 | Web 用 server.port(18080/18081),Dubbo 协议用 dubbo.protocol.port(20881),两条线分开 |
实体没实现 Serializable |
Dubbo 调用时报序列化异常 | 传输对象(如 User)必须实现 Serializable,BasePojo 已实现,继承即可 |
| JWT 密钥硬编码、payload 放敏感信息 | 密钥泄露即全站伪造;claims 可被 base64 解码 | 密钥外置到配置,payload 只放 id、mobile 这类非敏感标识 |
验证码开发态写死 123456 忘记改回 |
上线后任意手机号都能用 123456 登录 |
联调期临时写死可以,提交前必须换回随机 6 位并走短信通道 |
tb_user.mobile 缺唯一索引 |
并发首次登录时同一手机号插入两条 | 给 mobile 加唯一索引,并捕获 DuplicateKeyException 兜底 |
多模块工程未先 install 公共模块 |
依赖 mf-model 报找不到坐标 |
先 mvn install 父工程与公共模块,再做子模块开发 |
| Maven 依赖未统一版本管理 | 运行期 NoSuchMethodError、版本漂移 |
版本统一写在父工程 <dependencyManagement>,子模块只写坐标不写版本 |
自检清单
跑通本章的标准不是"代码能编译",而是下面八项逐条成立。每一项都给了验证方式。
| 检查项 | 怎么验 |
|---|---|
| 环境已就绪 | Linux 里 docker ps 能看到 MySQL、Redis、Nacos;Maven 能拉到依赖 |
| 服务已注册到 Nacos | 打开 Nacos 控制台 http://<ip>:8848,服务列表里有 mf-app-server 与 mf-dubbo-db |
| 短信能真正发出 | 调用 smsTemplate.sendSms("真实号码","1234"),手机收到短信(不是只打印日志) |
| 验证码能落进 Redis | 调 POST /user/login 后 redis-cli 执行 keys CHECK_CODE_*,能看到键且 TTL 约 300s |
| 登录能拿到 token | 调 POST /user/loginVerification,返回体里有 token 字段且是一串 A.B.C |
新用户自动注册且 isNew=true |
用未注册手机号登录,DB tb_user 新增一行,返回值 isNew=true |
老用户 isNew=false |
用已注册手机号再登录一次,tb_user 不新增,返回值 isNew=false |
created 字段自动填充 |
新注册用户那一行 created 不为空,且值约等于插入时刻 |
练习建议
不看讲义、把登录链路独立再实现一遍,是最有效的检验------建议改用不同的包名/类名,验证自己记住的是结构而不是代码位置。
补一道并发防线。 tb_user.mobile 加唯一索引,并处理 DuplicateKeyException。
sql
-- 改哪里:数据库执行
ALTER TABLE tb_user ADD UNIQUE INDEX uk_mobile (mobile);
java
// 怎么验证:并发调用登录,同一手机号插两次只成功一次,另一次被捕获而非抛 500
try {
Long userId = userApi.save(user);
user.setId(userId);
isNew = true;
} catch (DuplicateKeyException e) {
// 并发下已被别人抢先注册,回查即可
user = userApi.findByMobile(phone);
isNew = false;
}
补一个频控。 给发验证码接口加"同一手机号 60 秒一次 / 一天 10 条",用 Redis 计数 + TTL。
java
// 改哪里:UserService#sendMsg 开头
String limitKey = "SMS_LIMIT_" + phone;
Long count = redisTemplate.opsForValue().increment(limitKey);
if (count != null && count == 1) {
redisTemplate.expire(limitKey, Duration.ofDays(1)); // 首次设置日上限 TTL
}
if (count != null && count > 10) {
throw new RuntimeException("今日验证码次数已达上限");
}
// 怎么验证:连续点 11 次发短信,第 11 次返回受限;把键删掉后恢复
把硬编码清掉。 JWT 密钥 mfxs123、默认密码 123456、Redis key 前缀 CHECK_CODE_ 都提到配置或常量类。
yml
# 改哪里:application.yml 增加
mf:
jwt:
secret: ${MF_JWT_SECRET:change-me}
expire-seconds: 3600
java
// 怎么验证:全局搜索代码里不再出现 "mfxs123" 与 "123456" 字面量
// 只保留常量类或配置读取
给验证码加一次性语义。 校验成功后立即删除 Redis 里的验证码(现有代码已经做了 delete),并确认删的是同一个 key,避免大小写或前缀不一致导致删了个寂寞。
常见误解澄清
- 以为登录做完项目就过半了。其实只是把骨架打通了,业务功能(动态、推荐、IM)一个都还没开始。
- 以为学到的是一次性代码。短信组件、Dubbo 三件套、JWT 工具类都是可迁移的套路,后面每个模块都在复用它。
- 以为
123456可以留在代码里。联调临时写死可以,交付前必须换回随机验证码并接通短信。 - 以为流程跑通就算完成。还要补防线:唯一索引挡并发、频控挡刷短信、密钥外置挡泄露。
下一篇预告
后续将进入"完善用户信息":上传头像、人脸识别、个人资料管理,并引入统一鉴权与统一异常处理。它会直接吃掉本章的产物------短信组件那套自动装配套路会被复用来接 OSS 与人脸识别 SDK,JWT 会在统一鉴权拦截器里被解析出 id,BasePojo 会被新的用户资料实体继续继承。本章把骨架和凭证体系立住了,下一章才有地方往上长业务。
官方文档
总结
- 本章的产出是一条可运行的登录链路:注册中心能发现服务,短信能发出,验证码能落 Redis,登录能拿 token,新用户能自动注册。
- 真正沉淀下来的不是八个模块的目录,而是五套可复用套路:第三方封装四件套、Dubbo 三件套、验证码 + Redis、JWT、
BasePojo。 - 容易翻车的地方几乎都在配置静默失效(
spring.factories路径、Dubbo 扫描包、注册协议),排查时先看"服务有没有注册上去",再往下查。 - 流程跑通只是及格线,唯一索引、发短信频控、密钥外置这三条防线补上,才算把这一章的隐患真正修掉。