Java 项目实战: 交友项目:登录链路回顾与今日任务地图

概述

之前搭好了工程骨架并跑通了"验证码登录"这条完整链路;今天要做的是在这条链路之上把用户资料补齐------文件存储、人脸识别、资料保存与头像上传,外加两项横切优化(统一 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"补齐成型。带着"昨天怎么做的"去读今天每个板块,代码会好懂很多;实现层面的任务拆解与验收标准,直接翻《完善用户信息-业务概述》。

相关推荐
信誓旦旦的程序猿2 小时前
【Python量化实战宝典 #08】可转债与债券接口手册:转股价·溢价率·纯债价值(Python 可跑)
java·python·股票数据api·股票数据api接口·股票api数据接口·股票量化数据api·股票量化数据接口
砚底藏山河2 小时前
python量化:行情数据回溯
java·开发语言·python·金融·maven
今年下半年2 小时前
AI智能体项目实战记录(02):初始化 Python 项目
开发语言·人工智能·python·fastapi
Jlzn88882 小时前
华东电芯分选通道数怎么配才不堵线
java·人工智能·架构·自动化·制造
Williamtym2 小时前
PickGo Lab实践:用浏览器、Python和MCP串起机器人搬运仿真
开发语言·python·机器人
香吧香2 小时前
GRPC连接机制---长连接分析
java·网络与传输协议
DongQiShanRen2 小时前
六根硬件映射(四):归属澄清、两处不一致与卷十一总表
开发语言·人工智能·算法·rust·哈希算法·集成学习
CS创新实验室2 小时前
Python 对象“死“了之后去了哪?——引用计数与分代垃圾回收
开发语言·jvm·python
我是人✓2 小时前
SpringBoot项目的五种创建方式
java·spring boot·后端