336 lines
14 KiB
Markdown
336 lines
14 KiB
Markdown
# 邮箱变更
|
||
|
||
<cite>
|
||
**本文引用的文件**
|
||
- [user_handler.go](file://internal/handler/user_handler.go)
|
||
- [routes.go](file://internal/handler/routes.go)
|
||
- [auth.go](file://internal/middleware/auth.go)
|
||
- [verification_service.go](file://internal/service/verification_service.go)
|
||
- [user_service.go](file://internal/service/user_service.go)
|
||
- [user_repository.go](file://internal/repository/user_repository.go)
|
||
- [common.go](file://internal/types/common.go)
|
||
- [response.go](file://internal/model/response.go)
|
||
- [redis.go](file://pkg/redis/redis.go)
|
||
- [email.go](file://pkg/email/email.go)
|
||
</cite>
|
||
|
||
## 目录
|
||
1. [简介](#简介)
|
||
2. [项目结构](#项目结构)
|
||
3. [核心组件](#核心组件)
|
||
4. [架构总览](#架构总览)
|
||
5. [详细组件分析](#详细组件分析)
|
||
6. [依赖关系分析](#依赖关系分析)
|
||
7. [性能与可靠性](#性能与可靠性)
|
||
8. [故障排查指南](#故障排查指南)
|
||
9. [结论](#结论)
|
||
|
||
## 简介
|
||
本文档围绕“POST /api/v1/user/change-email”端点,系统化阐述邮箱变更的完整流程与技术实现细节。该接口要求用户提供新邮箱地址与验证码,后端通过验证服务核验Redis中存储的验证码,随后由用户服务层执行数据库更新,确保邮箱唯一性与安全性。文档还解释了请求体结构、验证码生成与存储机制、错误处理策略,并给出成功与失败场景的响应示例,强调该操作需要有效的JWT认证。
|
||
|
||
## 项目结构
|
||
- 路由与鉴权
|
||
- 路由在 v1 组下,用户相关接口均受JWT中间件保护。
|
||
- 邮箱变更接口位于 /api/v1/user/change-email,采用POST方法。
|
||
- 控制器层
|
||
- 用户控制器负责接收请求、参数校验、调用服务层、组装响应。
|
||
- 服务层
|
||
- 验证服务:生成、发送、校验验证码;验证码以特定键规则存入Redis。
|
||
- 用户服务:执行邮箱唯一性检查与数据库更新。
|
||
- 数据访问层
|
||
- 用户仓库封装GORM操作,提供按邮箱查询与字段更新能力。
|
||
- 基础设施
|
||
- Redis客户端:提供键值存取、过期控制、删除等操作。
|
||
- 邮件服务:根据类型发送不同主题的验证码邮件。
|
||
|
||
```mermaid
|
||
graph TB
|
||
Client["客户端"] --> Routes["路由: /api/v1/user/change-email"]
|
||
Routes --> AuthMW["JWT中间件"]
|
||
AuthMW --> Handler["用户处理器: ChangeEmail"]
|
||
Handler --> VerifySvc["验证服务: VerifyCode"]
|
||
Handler --> UserSvc["用户服务: ChangeUserEmail"]
|
||
VerifySvc --> Redis["Redis: 验证码存储"]
|
||
UserSvc --> Repo["用户仓库: UpdateUserFields"]
|
||
Repo --> DB["数据库: user 表"]
|
||
VerifySvc --> Email["邮件服务: SendChangeEmail"]
|
||
```
|
||
|
||
图表来源
|
||
- [routes.go](file://internal/handler/routes.go#L1-L120)
|
||
- [auth.go](file://internal/middleware/auth.go#L1-L79)
|
||
- [user_handler.go](file://internal/handler/user_handler.go#L328-L416)
|
||
- [verification_service.go](file://internal/service/verification_service.go#L1-L119)
|
||
- [user_service.go](file://internal/service/user_service.go#L186-L201)
|
||
- [user_repository.go](file://internal/repository/user_repository.go#L65-L69)
|
||
- [redis.go](file://pkg/redis/redis.go#L60-L83)
|
||
- [email.go](file://pkg/email/email.go#L47-L55)
|
||
|
||
章节来源
|
||
- [routes.go](file://internal/handler/routes.go#L1-L120)
|
||
- [auth.go](file://internal/middleware/auth.go#L1-L79)
|
||
|
||
## 核心组件
|
||
- 请求体结构
|
||
- ChangeEmailRequest.new_email:新邮箱地址,必填且符合邮箱格式。
|
||
- ChangeEmailRequest.verification_code:验证码,必填且长度为6。
|
||
- 鉴权要求
|
||
- 所有用户相关接口均需携带有效的Bearer JWT。
|
||
- 验证码机制
|
||
- 验证码类型常量包含“change_email”,用于区分不同用途。
|
||
- 验证码长度为6,有效期10分钟,发送频率限制1分钟。
|
||
- 验证通过后,Redis中的验证码键会被删除。
|
||
- 邮箱唯一性校验
|
||
- 在更新邮箱前,查询数据库确认新邮箱未被其他用户占用;若被占用则拒绝。
|
||
- 错误处理
|
||
- 参数错误、未授权、验证码错误、邮箱已被使用、服务器内部错误等均有明确响应码与消息。
|
||
|
||
章节来源
|
||
- [common.go](file://internal/types/common.go#L62-L66)
|
||
- [verification_service.go](file://internal/service/verification_service.go#L14-L24)
|
||
- [verification_service.go](file://internal/service/verification_service.go#L79-L98)
|
||
- [user_service.go](file://internal/service/user_service.go#L186-L201)
|
||
- [response.go](file://internal/model/response.go#L27-L53)
|
||
|
||
## 架构总览
|
||
POST /api/v1/user/change-email 的端到端流程如下:
|
||
1. 客户端携带JWT向 /api/v1/user/change-email 发起POST请求,请求体包含 new_email 与 verification_code。
|
||
2. 路由匹配到用户组,JWT中间件校验通过后进入用户处理器。
|
||
3. 处理器解析请求体并调用验证服务,验证Redis中对应类型的验证码是否匹配。
|
||
4. 验证通过后,处理器调用用户服务更新数据库中的邮箱字段。
|
||
5. 更新完成后,重新查询用户信息并返回成功响应。
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant C as "客户端"
|
||
participant R as "路由"
|
||
participant M as "JWT中间件"
|
||
participant H as "用户处理器"
|
||
participant VS as "验证服务"
|
||
participant RS as "Redis"
|
||
participant US as "用户服务"
|
||
participant UR as "用户仓库"
|
||
participant DB as "数据库"
|
||
C->>R : "POST /api/v1/user/change-email"
|
||
R->>M : "鉴权"
|
||
M-->>R : "通过"
|
||
R->>H : "进入ChangeEmail"
|
||
H->>VS : "VerifyCode(email, code, type=change_email)"
|
||
VS->>RS : "Get(verification : code : change_email : new_email)"
|
||
RS-->>VS : "验证码"
|
||
VS-->>H : "验证通过"
|
||
H->>US : "ChangeUserEmail(userID, new_email)"
|
||
US->>UR : "FindUserByEmail(new_email)"
|
||
UR-->>US : "查询结果"
|
||
US->>UR : "UpdateUserFields(userID, {email : new_email})"
|
||
UR->>DB : "更新"
|
||
DB-->>UR : "成功"
|
||
UR-->>US : "成功"
|
||
US-->>H : "成功"
|
||
H-->>C : "返回用户信息"
|
||
```
|
||
|
||
图表来源
|
||
- [routes.go](file://internal/handler/routes.go#L27-L41)
|
||
- [auth.go](file://internal/middleware/auth.go#L12-L56)
|
||
- [user_handler.go](file://internal/handler/user_handler.go#L328-L416)
|
||
- [verification_service.go](file://internal/service/verification_service.go#L79-L98)
|
||
- [redis.go](file://pkg/redis/redis.go#L60-L83)
|
||
- [user_service.go](file://internal/service/user_service.go#L186-L201)
|
||
- [user_repository.go](file://internal/repository/user_repository.go#L65-L69)
|
||
|
||
## 详细组件分析
|
||
|
||
### 控制器:ChangeEmail 端点
|
||
- 功能职责
|
||
- 从上下文提取用户ID(JWT中间件注入)。
|
||
- 解析请求体为 ChangeEmailRequest。
|
||
- 调用验证服务核验验证码(类型为 change_email)。
|
||
- 调用用户服务更新邮箱。
|
||
- 重新查询用户信息并返回成功响应。
|
||
- 错误处理
|
||
- 缺少Authorization头或无效token:返回401。
|
||
- 请求体绑定失败:返回400。
|
||
- 验证码错误或过期:返回400。
|
||
- 邮箱已被占用:返回400。
|
||
- 用户不存在:返回404。
|
||
- 其他异常:返回500。
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
Start(["进入ChangeEmail"]) --> GetCtx["获取user_id"]
|
||
GetCtx --> CheckAuth{"user_id存在?"}
|
||
CheckAuth -- 否 --> Resp401["返回401未授权"]
|
||
CheckAuth -- 是 --> BindReq["绑定ChangeEmailRequest"]
|
||
BindReq --> BindOK{"绑定成功?"}
|
||
BindOK -- 否 --> Resp400["返回400参数错误"]
|
||
BindOK -- 是 --> Verify["调用VerifyCode(type=change_email)"]
|
||
Verify --> VerifyOK{"验证通过?"}
|
||
VerifyOK -- 否 --> Resp400V["返回400验证码错误/过期"]
|
||
VerifyOK -- 是 --> Update["调用ChangeUserEmail"]
|
||
Update --> UpdateOK{"更新成功?"}
|
||
UpdateOK -- 否 --> Resp400E["返回400邮箱已被使用/其他错误"]
|
||
UpdateOK -- 是 --> Reload["重新查询用户信息"]
|
||
Reload --> Resp200["返回200成功"]
|
||
```
|
||
|
||
图表来源
|
||
- [user_handler.go](file://internal/handler/user_handler.go#L328-L416)
|
||
- [auth.go](file://internal/middleware/auth.go#L12-L56)
|
||
- [response.go](file://internal/model/response.go#L27-L53)
|
||
|
||
章节来源
|
||
- [user_handler.go](file://internal/handler/user_handler.go#L328-L416)
|
||
- [auth.go](file://internal/middleware/auth.go#L12-L56)
|
||
- [response.go](file://internal/model/response.go#L27-L53)
|
||
|
||
### 验证服务:验证码生成、发送与校验
|
||
- 生成与存储
|
||
- 生成6位数字验证码。
|
||
- 以键模式 verification:code:{type}:{email} 写入Redis,有效期10分钟。
|
||
- 发送频率限制键 verification:rate_limit:{type}:{email} 设置1分钟。
|
||
- 校验逻辑
|
||
- 读取Redis中的验证码并与请求一致进行比对。
|
||
- 校验通过后删除该验证码键。
|
||
- 邮件发送
|
||
- 根据类型选择不同主题与正文,发送更换邮箱验证码邮件。
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
Gen["生成6位验证码"] --> Store["写入Redis: code键(10分钟)"]
|
||
Store --> Rate["设置rate_limit键(1分钟)"]
|
||
Rate --> Mail["发送邮件(ChangeEmail)"]
|
||
Mail --> Verify["VerifyCode: 读取code键"]
|
||
Verify --> Match{"是否匹配?"}
|
||
Match -- 否 --> Err["返回错误(验证码错误/过期)"]
|
||
Match -- 是 --> Del["删除code键"]
|
||
Del --> Ok["返回成功"]
|
||
```
|
||
|
||
图表来源
|
||
- [verification_service.go](file://internal/service/verification_service.go#L26-L77)
|
||
- [verification_service.go](file://internal/service/verification_service.go#L79-L98)
|
||
- [email.go](file://pkg/email/email.go#L47-L55)
|
||
- [redis.go](file://pkg/redis/redis.go#L60-L83)
|
||
|
||
章节来源
|
||
- [verification_service.go](file://internal/service/verification_service.go#L14-L24)
|
||
- [verification_service.go](file://internal/service/verification_service.go#L26-L77)
|
||
- [verification_service.go](file://internal/service/verification_service.go#L79-L98)
|
||
- [email.go](file://pkg/email/email.go#L47-L55)
|
||
- [redis.go](file://pkg/redis/redis.go#L60-L83)
|
||
|
||
### 用户服务:邮箱唯一性与更新
|
||
- 唯一性检查
|
||
- 查询新邮箱是否已被其他用户占用;若存在且ID不同则报错。
|
||
- 更新逻辑
|
||
- 使用UpdateUserFields更新邮箱字段。
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
Check["FindUserByEmail(new_email)"] --> Found{"是否找到用户?"}
|
||
Found -- 是且ID!=userID --> Err["返回错误: 邮箱已被使用"]
|
||
Found -- 否或ID==userID --> Update["UpdateUserFields(userID, {email: new_email})"]
|
||
Update --> Done["返回成功"]
|
||
```
|
||
|
||
图表来源
|
||
- [user_service.go](file://internal/service/user_service.go#L186-L201)
|
||
- [user_repository.go](file://internal/repository/user_repository.go#L45-L57)
|
||
- [user_repository.go](file://internal/repository/user_repository.go#L65-L69)
|
||
|
||
章节来源
|
||
- [user_service.go](file://internal/service/user_service.go#L186-L201)
|
||
- [user_repository.go](file://internal/repository/user_repository.go#L45-L57)
|
||
- [user_repository.go](file://internal/repository/user_repository.go#L65-L69)
|
||
|
||
### 数据模型与响应
|
||
- 请求体
|
||
- ChangeEmailRequest.new_email:新邮箱地址,必填且符合邮箱格式。
|
||
- ChangeEmailRequest.verification_code:验证码,必填且长度为6。
|
||
- 成功响应
|
||
- 返回通用响应结构,包含用户信息(含更新后的邮箱)。
|
||
- 错误响应
|
||
- 400:请求参数错误、验证码错误/过期、邮箱已被使用。
|
||
- 401:未授权。
|
||
- 404:用户不存在。
|
||
- 500:服务器内部错误。
|
||
|
||
章节来源
|
||
- [common.go](file://internal/types/common.go#L62-L66)
|
||
- [response.go](file://internal/model/response.go#L27-L53)
|
||
- [user_handler.go](file://internal/handler/user_handler.go#L328-L416)
|
||
|
||
## 依赖关系分析
|
||
- 控制器依赖
|
||
- 路由与JWT中间件:保证接口受保护。
|
||
- 验证服务:负责验证码核验。
|
||
- 用户服务:负责邮箱更新与唯一性检查。
|
||
- 服务层依赖
|
||
- Redis客户端:验证码存取与删除。
|
||
- 邮件服务:发送验证码邮件。
|
||
- 用户仓库:数据库读写。
|
||
- 数据库约束
|
||
- 用户表的邮箱字段具备唯一索引,配合服务层检查可避免并发冲突。
|
||
|
||
```mermaid
|
||
graph LR
|
||
Handler["用户处理器"] --> VerifySvc["验证服务"]
|
||
Handler --> UserSvc["用户服务"]
|
||
VerifySvc --> Redis["Redis客户端"]
|
||
VerifySvc --> Email["邮件服务"]
|
||
UserSvc --> Repo["用户仓库"]
|
||
Repo --> DB["数据库"]
|
||
```
|
||
|
||
图表来源
|
||
- [user_handler.go](file://internal/handler/user_handler.go#L328-L416)
|
||
- [verification_service.go](file://internal/service/verification_service.go#L79-L98)
|
||
- [user_service.go](file://internal/service/user_service.go#L186-L201)
|
||
- [user_repository.go](file://internal/repository/user_repository.go#L65-L69)
|
||
- [redis.go](file://pkg/redis/redis.go#L60-L83)
|
||
- [email.go](file://pkg/email/email.go#L47-L55)
|
||
|
||
章节来源
|
||
- [user_handler.go](file://internal/handler/user_handler.go#L328-L416)
|
||
- [verification_service.go](file://internal/service/verification_service.go#L79-L98)
|
||
- [user_service.go](file://internal/service/user_service.go#L186-L201)
|
||
- [user_repository.go](file://internal/repository/user_repository.go#L65-L69)
|
||
- [redis.go](file://pkg/redis/redis.go#L60-L83)
|
||
- [email.go](file://pkg/email/email.go#L47-L55)
|
||
|
||
## 性能与可靠性
|
||
- 验证码缓存
|
||
- Redis键过期时间10分钟,避免长期占用内存;rate_limit键1分钟,限制发送频率,降低滥用风险。
|
||
- 并发一致性
|
||
- 服务层在更新前进行邮箱唯一性检查,结合数据库唯一索引,有效防止并发写入导致的重复。
|
||
- 日志与可观测性
|
||
- 处理器与服务层在关键路径记录日志,便于定位问题。
|
||
- 错误传播
|
||
- 明确的错误码与消息,便于前端统一处理。
|
||
|
||
[本节为通用指导,不直接分析具体文件]
|
||
|
||
## 故障排查指南
|
||
- 401 未授权
|
||
- 检查请求头 Authorization 是否为 Bearer 令牌,且令牌有效。
|
||
- 400 请求参数错误
|
||
- 确认请求体包含 new_email 与 verification_code,且格式正确。
|
||
- 400 验证码错误/过期
|
||
- 检查Redis中 verification:code:change_email:{email} 是否存在且未过期;确认邮件是否送达。
|
||
- 400 邮箱已被使用
|
||
- 确认新邮箱未被其他用户占用;若被占用请更换邮箱。
|
||
- 404 用户不存在
|
||
- 检查用户是否被软删除或账户状态异常。
|
||
- 500 服务器内部错误
|
||
- 查看服务日志,关注数据库更新与Redis存取异常。
|
||
|
||
章节来源
|
||
- [user_handler.go](file://internal/handler/user_handler.go#L328-L416)
|
||
- [verification_service.go](file://internal/service/verification_service.go#L79-L98)
|
||
- [user_service.go](file://internal/service/user_service.go#L186-L201)
|
||
- [redis.go](file://pkg/redis/redis.go#L60-L83)
|
||
- [response.go](file://internal/model/response.go#L27-L53)
|
||
|
||
## 结论
|
||
POST /api/v1/user/change-email 通过严格的JWT鉴权、Redis验证码校验与数据库唯一性检查,实现了安全可靠的邮箱变更流程。请求体简洁明确,错误处理清晰,适合在生产环境中稳定运行。建议在部署时确保Redis与邮件服务可用,并合理配置验证码有效期与发送频率限制,以兼顾用户体验与安全。 |