概述
之前搭好了工程骨架并跑通了"验证码登录"这条完整链路;今天要做的是在这条链路之上把用户资料补齐------文件存储、人脸识别、资料保存与头像上传,外加两项横切优化(统一 token 处理、统一异常处理)。本文以回顾之前为主线,把第一天的登录链路完整串一遍,说清每个环节沉淀了什么可复用的东西,再看它们如何被今天的任务逐一复用。
纲要
- 之前做了什么
- 五个必须掌握的知识点回顾
- 登录链路时序:短信验证码 → Redis 校验 → Dubbo 查库/自动注册 → JWT 颁发 token
- 每个环节沉淀的可复用产出:自动装配套路、Dubbo 服务三件套、JWT 工具、BasePojo 自动填充
- 这些地基今天怎么被复用(对照表)
- 现在要做什么:五个板块一句话预告
- 将来结束时工程新增了哪些类(实际工程结构)
- 学习建议与常见误解澄清
之前做了什么
之前的知识点不算多,但每一个都直接决定了今天怎么写代码。先把五个点摆出来:
| 知识点 | 关键内容 | 落地的代码 |
|---|---|---|
| 前后端分离开发思想 | 前后端只通过 API 接口约定交互,接口传 JSON 数据 | 接口文档(YApi)驱动开发 |
| 工程结构与调用过程 | 客户端请求进 mf-app-server,内部以 Dubbo 形式构建微服务 |
7 个 Maven 模块的聚合工程 |
| 公共组件抽取 | 借助 SpringBoot 自动装配把发短信抽成公共组件,跨模块复用 | mf-autoconfig 模块 + SmsTemplate |
| JWT 鉴权思想 | 登录成功颁发 token,其余接口校验 token | JwtUtils 工具类 |
| 用户登录两步走 | 获取登录验证码、用验证码完成登录校验 | LoginController + UserService |
光看表格还不够,这些点在登录链路里是连成一条线的。
登录链路时序
MySQL UserApi (Dubbo) Redis SmsTemplate UserService LoginController 客户端(APP) MySQL UserApi (Dubbo) Redis SmsTemplate UserService LoginController 客户端(APP) #mermaid-svg-Se6fV5qcGahix3Bo{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-Se6fV5qcGahix3Bo .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Se6fV5qcGahix3Bo .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Se6fV5qcGahix3Bo .error-icon{fill:#552222;}#mermaid-svg-Se6fV5qcGahix3Bo .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Se6fV5qcGahix3Bo .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Se6fV5qcGahix3Bo .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Se6fV5qcGahix3Bo .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Se6fV5qcGahix3Bo .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Se6fV5qcGahix3Bo .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Se6fV5qcGahix3Bo .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Se6fV5qcGahix3Bo .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Se6fV5qcGahix3Bo .marker.cross{stroke:#333333;}#mermaid-svg-Se6fV5qcGahix3Bo svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Se6fV5qcGahix3Bo p{margin:0;}#mermaid-svg-Se6fV5qcGahix3Bo .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Se6fV5qcGahix3Bo text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-Se6fV5qcGahix3Bo .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Se6fV5qcGahix3Bo .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-Se6fV5qcGahix3Bo .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-Se6fV5qcGahix3Bo .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-Se6fV5qcGahix3Bo #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-Se6fV5qcGahix3Bo .sequenceNumber{fill:white;}#mermaid-svg-Se6fV5qcGahix3Bo #sequencenumber{fill:#333;}#mermaid-svg-Se6fV5qcGahix3Bo #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-Se6fV5qcGahix3Bo .messageText{fill:#333;stroke:none;}#mermaid-svg-Se6fV5qcGahix3Bo .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Se6fV5qcGahix3Bo .labelText,#mermaid-svg-Se6fV5qcGahix3Bo .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-Se6fV5qcGahix3Bo .loopText,#mermaid-svg-Se6fV5qcGahix3Bo .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-Se6fV5qcGahix3Bo .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-Se6fV5qcGahix3Bo .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-Se6fV5qcGahix3Bo .noteText,#mermaid-svg-Se6fV5qcGahix3Bo .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-Se6fV5qcGahix3Bo .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Se6fV5qcGahix3Bo .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Se6fV5qcGahix3Bo .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Se6fV5qcGahix3Bo .actorPopupMenu{position:absolute;}#mermaid-svg-Se6fV5qcGahix3Bo .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-Se6fV5qcGahix3Bo .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Se6fV5qcGahix3Bo .actor-man circle,#mermaid-svg-Se6fV5qcGahix3Bo line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-Se6fV5qcGahix3Bo :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} alt用户不存在(首次登录) POST /user/login (mobile)sendCode(mobile)生成随机验证码sendSms(mobile, code)下发短信SET CHECK_CODE_{mobile} = codePOST /user/loginVerification (mobile, code)login(mobile, code)GET CHECK_CODE_{mobile} 并比对DEL 验证码(一次性使用)findByMobile(mobile)select * from tb_usersave(user) 自动注册,isNew = trueJwtUtils.getToken(id, mobile)token + isNew
两个分支值得留意:验证码校验失败直接抛异常结束;校验通过后 findByMobile 返回 null 时走自动注册分支,isNew=true 会传给前端,客户端据此跳转"完善信息"页------这正是今天 day02 的入口场景。
每个环节产出了什么可复用的东西
回顾的目的不是背流程,而是认出昨天留下的"套路"。之前真正值钱的是四套可复用的模式。
自动装配套路(SmsTemplate 四件套) 。发短信这种第三方能力被抽成公共组件,靠的是 SpringBoot 自动装配的标准四件套:spring.factories 声明、自动装配类、属性配置类、模板对象。业务模块只注入 SmsTemplate 就能用,不感知 AccessKey、签名、模板编号这些细节。
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;
@EnableConfigurationProperties({
SmsProperties.class
})
public class mfAutoConfiguration {
@Bean
public SmsTemplate smsTemplate(SmsProperties smsProperties) {
return new SmsTemplate(smsProperties);
}
}
java
package com.mf.autoconfig.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
@Data
@ConfigurationProperties(prefix = "mf.sms")
public class SmsProperties {
private String signName;
private String templateCode;
private String accessKey;
private String secret;
}
配套的 /META-INF/spring.factories 只有两行:
markdown
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.mf.autoconfig.mfAutoConfiguration
Dubbo 服务三件套 。任何一类数据要跨服务访问,都是同一套动作:mf-model 里定义实体,mf-dubbo-interface 里定义 Api 接口,mf-dubbo-db 里用 @DubboService 给出实现并注入 Mapper。昨天为 User 建了 UserApi / UserApiImpl / UserMapper 这三件,UserApiImpl 的实现短到只有几行------因为 MyBatis-Plus 的 BaseMapper 把 SQL 都包了。
JWT 工具与 ErrorResult 。JwtUtils 提供三个静态方法:getToken(签发,写入 id/mobile,1 小时有效期)、getClaims(解析)、verifyToken(校验有效性,异常一律返回 false)。错误响应则统一走 ErrorResult,用静态工厂方法枚举各类错误(loginError、faceError......),错误码字符串约定成俗。
BasePojo 与自动填充 。所有实体的 created/updated 字段抽进 BasePojo 基类,配合 MyBatis-Plus 的 MetaObjectHandler 在插入和更新时自动填充时间,业务代码从此不用手动 set:
java
package com.mf.dubbo.server.handler;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.util.Date;
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
Object created = getFieldValByName("created", metaObject);
if (null == created) {
//字段为空,可以进行填充
setFieldValByName("created", new Date(), metaObject);
}
Object updated = getFieldValByName("updated", metaObject);
if (null == updated) {
//字段为空,可以进行填充
setFieldValByName("updated", new Date(), metaObject);
}
}
@Override
public void updateFill(MetaObject metaObject) {
setFieldValByName("updated", new Date(), metaObject);
}
}
这些地基今天怎么被复用
这是本文最想让你带走的一张表。现在的每个板块都不是从零开始,而是把昨天的套路原样再用一遍:
| 之前的产出 | 现在怎么用 |
|---|---|
| 自动装配套路(spring.factories + Properties + Template 四件套) | 照此封装 OssTemplate(阿里云 OSS 文件存储)和 AipFaceTemplate(百度人脸识别),mfAutoConfiguration 里各加一个 @Bean |
| Dubbo 服务三件套(Api 接口 / @DubboService 实现 / Mapper) | 为用户资料域新增 UserInfoApi / UserInfoApiImpl / UserInfoMapper,动作和昨天的 UserApi 一模一样 |
| 拦截器 + ThreadLocal 雏形(JwtUtils.verifyToken 已能校验 token) | 今天把它正式成型:TokenInterceptor 统一鉴权 + UserHolder 存取当前用户,各控制器不再重复解析 token |
| JWT 工具(token 里存了 id、mobile) | 查询/更新资料时直接从 token 取当前用户 id,作为 tb_user_info 的主键来源 |
| BasePojo 自动填充 | UserInfo 实体继承 BasePojo,创建/更新时间零代码维护 |
| ErrorResult 错误码约定 | 今天升级为 BusinessException + @RestControllerAdvice 的统一异常处理 |
同一套自动装配模式一天之内会写两遍(OSS、人脸),这不是重复劳动,而是在验证"套路"的通用性------这也是导引篇想强调的:今天每一块都站在昨天的地基上。
现在要做什么
内容比昨天多,五个板块各一句话:
| 板块 | 一句话概括 |
|---|---|
| 完善用户信息 | 新用户首次登录后强制完善详细资料并上传头像,涉及阿里云 OSS、百度人脸识别两个第三方组件的接入与模板抽取 |
| 用户信息管理 | 用户资料的查询与更新(修改手机号、所在地等),本质是对 tb_user_info 的增删改查 |
| 统一 token 处理 | 用拦截器 + ThreadLocal 消灭每个接口里重复的 token 解析与校验代码 |
| 统一异常处理 | 用 @RestControllerAdvice 把散落各处的 try-catch 收拢到一处 |
这里只做导引,不展开实现细节------每个板块的接口文档、执行流程、代码步骤与验收标准,见同目录的《完善用户信息-业务概述》,那里是 现在的开工任务书。
将来结束时工程长什么样
以实际工程快照为准,相对 之前 新增的类如下(标注 新增):
tree
mf
├── mf-app-server
│ └── src/main/java/com/mf/server
│ ├── AppServerApplication.java
│ ├── controller
│ │ ├── LoginController.java
│ │ ├── UserController.java # 新增:保存资料/上传头像
│ │ └── UsersControler.java # 新增:资料查询/更新
│ ├── exception
│ │ ├── BusinessException.java # 新增:业务异常对象
│ │ └── ExceptionAdvice.java # 新增:统一异常处理
│ ├── interceptor
│ │ ├── TokenInterceptor.java # 新增:统一鉴权拦截器
│ │ ├── UserHolder.java # 新增:ThreadLocal 工具类
│ │ └── WebConfig.java # 新增:注册拦截器
│ └── service
│ ├── UserService.java
│ └── UserInfoService.java # 新增
├── mf-autoconfig
│ └── src/main/java/com/mf/autoconfig
│ ├── mfAutoConfiguration.java # 修改:注册 Oss/AipFace 两个 Bean
│ ├── properties
│ │ ├── SmsProperties.java
│ │ ├── OssProperties.java # 新增
│ │ └── AipFaceProperties.java # 新增
│ └── template
│ ├── SmsTemplate.java
│ ├── OssTemplate.java # 新增
│ └── AipFaceTemplate.java # 新增
├── mf-commons
│ └── .../utils/JwtUtils.java
├── mf-model
│ └── src/main/java/com/mf/model
│ ├── domain
│ │ ├── BasePojo.java
│ │ ├── User.java
│ │ └── UserInfo.java # 新增:用户资料实体
│ └── vo
│ ├── ErrorResult.java
│ └── UserInfoVo.java # 新增:资料返回值对象
└── mf-dubbo
├── mf-dubbo-interface
│ └── .../api
│ ├── UserApi.java
│ └── UserInfoApi.java # 新增
└── mf-dubbo-db
└── src/main/java/com/mf/dubbo
├── api
│ ├── UserApiImpl.java
│ └── UserInfoApiImpl.java # 新增
├── handler/MyMetaObjectHandler.java
└── mappers
├── UserMapper.java
└── UserInfoMapper.java # 新增
一天下来新增约 15 个类,但拆开看几乎没有新模式:OssTemplate/AipFaceTemplate 是 SmsTemplate 的翻版,UserInfoApi 三件套是 UserApi 的翻版,真正的新东西只有拦截器 + ThreadLocal 和 @RestControllerAdvice 这两处。
学习建议
- 昨天的自动装配套路今天会原样复用两次(OSS、人脸),建议边写边对比"同一套路在不同能力上的差异":属性项不同(OSS 是 endpoint/bucket/accessKey,人脸是 appId/apiKey/secretKey)、模板方法不同(上传 vs 检测),骨架却完全一致。能在差异中认出共性,这套模式才算真正学会。
- 今天有两个安全主题------统一鉴权与统一异常处理,别急着抄代码,先理解动机:鉴权统一是为了消灭"N 个接口各写一遍 token 解析"的重复,异常统一是为了让
ErrorResult的约定不再依赖每个人自觉 try-catch。动机清楚了,TokenInterceptor和ExceptionAdvice的写法都是自然推导。 UserHolder用完要remove()。线程池里的线程是复用的,ThreadLocal 不清理,上一个请求的用户信息会串到下一个请求------这是这套方案最经典的坑。
常见误解澄清
| 误解 | 实际情况 |
|---|---|
| JWT 是加密,payload 看不懂 | JWT 只做签名防篡改,payload 部分(A.B.C 中的 B)Base64 解码即可读。所以 token 里只放 id、mobile 这类非敏感字段,绝不能放密码 |
| 首次登录自动注册 = 用户点了注册按钮 | 没有注册页。验证码校验通过后 findByMobile 查不到用户就落库一条 password=md5("123456") 的记录,isNew=true 由前端引导去完善资料 |
| 自动装配只是课上为了发短信做的特殊处理 | spring.factories + @ConfigurationProperties + 模板对象是编写 starter 的标准做法,今天接 OSS 和人脸识别时原样复用,属于全项目通用的基础设施模式 |
| 拦截器和过滤器是一回事 | 两者都能前置/后置处理请求,但拦截器是 SpringMVC 的机制,能拿到 handler 信息、依赖 Spring 容器,配合 ThreadLocal 做登录校验更顺手;过滤器工作在 Servlet 层,粒度更粗 |
总结
之前 表面只交付了一个登录功能,实际沉淀了四套可复用模式:自动装配四件套、Dubbo 服务三件套、JWT 工具、BasePojo 自动填充。现在 的五个板块全部建立在这些地基上------OSS 与人脸识别是装配套路的第二次、第三次落地,UserInfoApi 是 Dubbo 三件套的复制,统一 token 处理则把昨天"登录成功颁发 token"的下半句"其余接口校验 token"补齐成型。带着"昨天怎么做的"去读今天每个板块,代码会好懂很多;实现层面的任务拆解与验收标准,直接翻《完善用户信息-业务概述》。