feat: 添加注册发送验证码功能,支持手机号验证及频率限制,更新相关文档和国际化文本

This commit is contained in:
wood911010 committed 2026-01-10 15:44:49 +08:00
1 parent af76eaf1b1
commit 9c2b503d2b
13 files changed
+530 -28

No files matched your search

+7 -5
View File
@@ -18,11 +18,13 @@ errors:
customer:
"200001": "Customer not found"
"200002": "Customer already exists"
"200003": "Customer name is required"
"200004": "Invalid customer information format"
"200005": "Failed to query customer list"
"200006": "Customer has authorization codes, cannot delete. Please delete authorizations first"
"200007": "Customer is disabled, cannot create authorization"
"200003": "Request too frequent, please try again later"
"200004": "SMS sending failed"
"200005": "Customer name is required"
"200006": "Invalid customer information format"
"200007": "Failed to query customer list"
"200008": "Customer has authorization codes, cannot delete. Please delete authorizations first"
"200009": "Customer is disabled, cannot create authorization"
# Authorization module (30xxxx)
authorization:
+7 -5
View File
@@ -18,11 +18,13 @@ errors:
customer:
"200001": "客户不存在"
"200002": "客户已存在"
"200003": "客户名称不能为空"
"200004": "客户信息格式无效"
"200005": "客户列表查询失败"
"200006": "客户仍有授权,无法删除,请先删除授权"
"200007": "客户已停用,无法创建授权"
"200003": "请求过于频繁,请稍后再试"
"200004": "短信发送失败"
"200005": "客户名称不能为空"
"200006": "客户信息格式无效"
"200007": "客户列表查询失败"
"200008": "客户仍有授权,无法删除,请先删除授权"
"200009": "客户已停用,无法创建授权"
# 授权模块 (30xxxx)
authorization:
+48 -5
View File
@@ -6,9 +6,11 @@
ORM: GORM,开发环境使用Auto Migration
配置: Viper
日志: logrus
认证: JWT
认证: JWT (支持管理员和C端用户双JWT体系)
缓存: Redis (可选)
数据库: PostgreSQL/MySQL
支付集成: 支付宝
加密算法: RSA
## 目录结构
@@ -20,16 +22,32 @@
backend/
├── cmd/
│ ├── main.go # 程序入口
│ └── license_manager.sh # 启动脚本
│ ├── license_manager.sh # 启动脚本
│ ├── client-demo/ # C端客户端演示程序
│ │ ├── main.go
│ │ ├── rsa.go
│ │ ├── client_config.json
│ │ ├── README.md
│ │ └── license_code/
│ └── gen-rsa-keys/ # RSA密钥生成工具
│ └── main.go
├── internal/
│ ├── api/
│ │ ├── handlers/ # HTTP处理器
│ │ │ ├── auth_handler.go
│ │ │ ├── authorization_code_handler.go
│ │ │ ├── customer_handler.go
│ │ │ ├── dashboard_handler.go
│ │ │ ├── enum_handler.go
│ │ │ ├── license_handler.go
│ │ │ └── system_handler.go
│ │ │ ├── payment_handler.go
│ │ │ ├── system_handler.go
│ │ │ ├── cu_auth_handler.go
│ │ │ ├── cu_authorization_handler.go
│ │ │ ├── cu_device_handler.go
│ │ │ ├── cu_order_handler.go
│ │ │ ├── cu_order_handler_test.go
│ │ │ └── cu_profile_handler.go
│ │ ├── middleware/ # 中间件
│ │ │ ├── auth.go
│ │ │ ├── cors.go
@@ -41,22 +59,36 @@ backend/
│ │ ├── auth_service.go
│ │ ├── authorization_code_service.go
│ │ ├── customer_service.go
│ │ ├── dashboard_service.go
│ │ ├── enum_service.go
│ │ ├── license_service.go
│ │ ├── payment_service.go
│ │ ├── system_service.go
│ │ ├── cu_device_service.go
│ │ ├── cu_order_service.go
│ │ ├── cu_user_service.go
│ │ └── interfaces.go # 接口定义
│ ├── repository/ # 数据访问层
│ │ ├── authorization_code_repository.go
│ │ ├── customer_repository.go
│ │ ├── dashboard_repository.go
│ │ ├── license_repository.go
│ │ ├── payment_repository.go
│ │ ├── user_repository.go
│ │ ├── cu_order_repository.go
│ │ ├── cu_user_repository.go
│ │ ├── gorm/ # GORM相关扩展
│ │ ├── errors.go
│ │ └── interfaces.go
│ ├── models/ # 数据模型
│ │ ├── auth.go
│ │ ├── common.go
│ │ ├── customer.go
│ │ ├── cu_order.go
│ │ ├── cu_user.go
│ │ ├── dashboard.go
│ │ ├── license.go
│ │ ├── payment.go
│ │ ├── system.go
│ │ └── user.go
│ ├── config/ # 配置管理
@@ -71,8 +103,11 @@ backend/
│ │ ├── errors.go
│ │ └── manager.go
│ ├── utils/ # 工具函数
│ │ ├── crypto.go
│ │ └── jwt.go
│ │ ├── alipay.go # 支付宝支付集成
│ │ ├── crypto.go # 加密工具
│ │ ├── cu_jwt.go # C端用户JWT工具
│ │ ├── jwt.go # JWT工具
│ │ └── license_codec.go # 授权码编解码
│ └── logger/ # 日志封装
│ └── logger.go
├── configs/
@@ -113,6 +148,11 @@ backend/
│ ├── 004_create_licenses_table.sql
│ ├── 005_create_authorization_changes_table.sql
│ ├── 006_create_users_table.sql
│ ├── 007_create_cu_users_table.sql
│ ├── 008_create_cu_orders_table.sql
│ ├── 009_update_authorization_codes_code_length.sql
│ ├── 010_update_cu_orders_authorization_code_length.sql
│ ├── 011_create_payments_table.sql
│ └── README.md
├── go.mod
└── go.sum
@@ -123,4 +163,7 @@ backend/
- **依赖注入**:通过接口解耦各层
- **配置驱动**:Viper管理所有配置
- **缓存抽象**:支持内存/Redis切换
- **双JWT体系**:支持管理员和C端用户独立认证体系
- **模块化设计**:支持客户管理、订单管理、支付集成等独立模块
- **国际化支持**:完整的多语言错误信息系统
+5 -1
View File
@@ -59,10 +59,12 @@ errors:
"100004": "缺少认证信息"
"100005": "权限不足"
# 客户模块 (20xxxx) - 预留
# 客户模块 (20xxxx)
customer:
"200001": "客户不存在"
"200002": "客户已存在"
"200003": "请求过于频繁,请稍后再试"
"200004": "短信发送失败"
# 授权模块 (30xxxx) - 预留
license:
@@ -99,6 +101,8 @@ errors:
customer:
"200001": "Customer not found"
"200002": "Customer already exists"
"200003": "Request too frequent, please try again later"
"200004": "SMS sending failed"
license:
"300001": "Invalid license key"
@@ -0,0 +1,135 @@
# 注册发送验证码接口
## 概述
为注册流程提供手机号验证功能,用户在注册前需要先获取短信验证码进行手机号验证。
## 接口信息
- **接口路径**: `POST /api/cu/send-register-sms`
- **认证方式**: 无需认证(公开接口)
- **请求格式**: `application/json`
- **响应格式**: `application/json`
## 请求参数
```json
{
"phone": "13800138000", // 必填,手机号
"phone_country_code": "+86" // 可选,国家代码,默认+86
}
```
### 参数说明
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| phone | string | 是 | 手机号,长度7-20位 |
| phone_country_code | string | 否 | 国家代码,默认"+86" |
## 响应格式
### 成功响应 (200)
```json
{
"code": 200,
"message": "验证码发送成功",
"data": null
}
```
### 错误响应
#### 手机号已存在 (200002)
```json
{
"code": "200002",
"message": "客户已存在",
"data": null
}
```
#### 请求过于频繁 (200003)
```json
{
"code": "200003",
"message": "请求过于频繁,请稍后再试",
"data": null
}
```
#### 参数无效 (900001)
```json
{
"code": "900001",
"message": "请求参数无效",
"data": null
}
```
#### 短信发送失败 (200004)
```json
{
"code": "200004",
"message": "短信发送失败",
"data": null
}
```
## 业务逻辑
1. **参数验证**
- 验证手机号格式
- 验证国家代码格式(如提供)
- 参数不符合要求时,返回错误码 `900001`
2. **用户检查**
- 检查手机号是否已被注册
- 如已注册,返回错误码 `200002`
3. **频率限制**
- 限制同一手机号发送频率(建议:1分钟内最多1次,1小时内最多5次)
- 使用Redis缓存记录发送时间和次数
- 超过频率限制时,返回错误码 `200003`
4. **验证码生成**
- 生成6位数字验证码
- 将验证码存储到Redis(设置过期时间,如5分钟)
5. **短信发送**
- 调用短信服务发送验证码
- 发送失败时,返回错误码 `200004`
- 记录发送日志
## 与忘记密码接口对比
| 对比项 | 注册验证码接口 | 忘记密码验证码接口 |
|--------|----------------|-------------------|
| 接口路径 | `/api/cu/send-register-sms` | `/api/cu/forgot-password` |
| 用户存在检查 | 检查用户**不存在** | 检查用户**存在** |
| 验证码用途 | 手机号验证 | 密码重置 |
| 频率限制 | 较宽松 | 较严格 |
| 缓存键前缀 | `register_sms:` | `forgot_sms:` |
## 缓存设计
- **验证码存储**: `register_sms:{phone_country_code}:{phone}` (过期时间: 5分钟)
- **频率限制**: `register_sms_limit:{phone_country_code}:{phone}` (过期时间: 1小时)
## 安全考虑
1. 验证码只能使用一次,验证后立即删除
2. 限制发送频率,防止短信轰炸
3. 记录发送日志,便于审计
4. 支持图形验证码作为二次验证(可选)
## 后续流程
1. 用户收到验证码后,填写注册表单
2. 调用注册接口 `/api/cu/register`,提交手机号、验证码、密码等信息
3. 后端验证验证码正确性,完成注册流程
+82 -4
View File
@@ -1180,6 +1180,64 @@ const docTemplate = `{
}
}
},
"/api/cu/send-register-sms": {
"post": {
"description": "注册前发送短信验证码到手机",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"客户用户管理"
],
"summary": "注册发送验证码",
"parameters": [
{
"description": "注册发送验证码请求",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.CuUserSendRegisterSmsRequest"
}
}
],
"responses": {
"200": {
"description": "验证码发送成功",
"schema": {
"$ref": "#/definitions/models.APIResponse"
}
},
"400": {
"description": "请求参数无效",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"409": {
"description": "手机号已被注册",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"429": {
"description": "请求过于频繁",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"500": {
"description": "短信发送失败",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
}
}
}
},
"/api/customers": {
"get": {
"security": [
@@ -4325,7 +4383,7 @@ const docTemplate = `{
"type": "string"
},
"payment_method": {
"description": "可选:支付方式,不传则为免费订单",
"description": "可选:支付方式,不传则为免费订单 支持:alipay,wechat",
"type": "string"
}
}
@@ -4642,6 +4700,23 @@ const docTemplate = `{
}
}
},
"models.CuUserSendRegisterSmsRequest": {
"type": "object",
"required": [
"phone"
],
"properties": {
"phone": {
"type": "string",
"maxLength": 20,
"minLength": 7
},
"phone_country_code": {
"description": "可选,默认+86",
"type": "string"
}
}
},
"models.Customer": {
"type": "object",
"properties": {
@@ -5587,9 +5662,15 @@ const docTemplate = `{
"created_at": {
"type": "string"
},
"cu_user_id": {
"type": "string"
},
"currency": {
"type": "string"
},
"customer_id": {
"type": "string"
},
"expire_time": {
"type": "string"
},
@@ -5619,9 +5700,6 @@ const docTemplate = `{
},
"updated_at": {
"type": "string"
},
"user_id": {
"type": "integer"
}
}
},
+82 -4
View File
@@ -1176,6 +1176,64 @@
}
}
},
"/api/cu/send-register-sms": {
"post": {
"description": "注册前发送短信验证码到手机",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"客户用户管理"
],
"summary": "注册发送验证码",
"parameters": [
{
"description": "注册发送验证码请求",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.CuUserSendRegisterSmsRequest"
}
}
],
"responses": {
"200": {
"description": "验证码发送成功",
"schema": {
"$ref": "#/definitions/models.APIResponse"
}
},
"400": {
"description": "请求参数无效",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"409": {
"description": "手机号已被注册",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"429": {
"description": "请求过于频繁",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"500": {
"description": "短信发送失败",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
}
}
}
},
"/api/customers": {
"get": {
"security": [
@@ -4321,7 +4379,7 @@
"type": "string"
},
"payment_method": {
"description": "可选:支付方式,不传则为免费订单",
"description": "可选:支付方式,不传则为免费订单 支持:alipay,wechat",
"type": "string"
}
}
@@ -4638,6 +4696,23 @@
}
}
},
"models.CuUserSendRegisterSmsRequest": {
"type": "object",
"required": [
"phone"
],
"properties": {
"phone": {
"type": "string",
"maxLength": 20,
"minLength": 7
},
"phone_country_code": {
"description": "可选,默认+86",
"type": "string"
}
}
},
"models.Customer": {
"type": "object",
"properties": {
@@ -5583,9 +5658,15 @@
"created_at": {
"type": "string"
},
"cu_user_id": {
"type": "string"
},
"currency": {
"type": "string"
},
"customer_id": {
"type": "string"
},
"expire_time": {
"type": "string"
},
@@ -5615,9 +5696,6 @@
},
"updated_at": {
"type": "string"
},
"user_id": {
"type": "integer"
}
}
},
+55 -3
View File
@@ -470,7 +470,7 @@ definitions:
package_id:
type: string
payment_method:
description: 可选:支付方式,不传则为免费订单
description: 可选:支付方式,不传则为免费订单 支持:alipay,wechat
type: string
required:
- license_count
@@ -693,6 +693,18 @@ definitions:
user_role:
type: string
type: object
models.CuUserSendRegisterSmsRequest:
properties:
phone:
maxLength: 20
minLength: 7
type: string
phone_country_code:
description: 可选,默认+86
type: string
required:
- phone
type: object
models.Customer:
properties:
address:
@@ -1354,8 +1366,12 @@ definitions:
type: string
created_at:
type: string
cu_user_id:
type: string
currency:
type: string
customer_id:
type: string
expire_time:
type: string
id:
@@ -1376,8 +1392,6 @@ definitions:
type: string
updated_at:
type: string
user_id:
type: integer
type: object
models.PaymentStatusResponse:
properties:
@@ -2241,6 +2255,44 @@ paths:
summary: 重置密码
tags:
- 客户用户管理
/api/cu/send-register-sms:
post:
consumes:
- application/json
description: 注册前发送短信验证码到手机
parameters:
- description: 注册发送验证码请求
in: body
name: request
required: true
schema:
$ref: '#/definitions/models.CuUserSendRegisterSmsRequest'
produces:
- application/json
responses:
"200":
description: 验证码发送成功
schema:
$ref: '#/definitions/models.APIResponse'
"400":
description: 请求参数无效
schema:
$ref: '#/definitions/models.ErrorResponse'
"409":
description: 手机号已被注册
schema:
$ref: '#/definitions/models.ErrorResponse'
"429":
description: 请求过于频繁
schema:
$ref: '#/definitions/models.ErrorResponse'
"500":
description: 短信发送失败
schema:
$ref: '#/definitions/models.ErrorResponse'
summary: 注册发送验证码
tags:
- 客户用户管理
/api/customers:
get:
consumes:
@@ -235,6 +235,64 @@ func (h *CuAuthHandler) CuUserForgotPassword(c *gin.Context) {
})
}
// CuUserSendRegisterSms 注册发送验证码
// @Summary 注册发送验证码
// @Description 注册前发送短信验证码到手机
// @Tags 客户用户管理
// @Accept json
// @Produce json
// @Param request body models.CuUserSendRegisterSmsRequest true "注册发送验证码请求"
// @Success 200 {object} models.APIResponse "验证码发送成功"
// @Failure 400 {object} models.ErrorResponse "请求参数无效"
// @Failure 409 {object} models.ErrorResponse "手机号已被注册"
// @Failure 429 {object} models.ErrorResponse "请求过于频繁"
// @Failure 500 {object} models.ErrorResponse "短信发送失败"
// @Router /api/cu/send-register-sms [post]
func (h *CuAuthHandler) CuUserSendRegisterSms(c *gin.Context) {
var req models.CuUserSendRegisterSmsRequest
if err := c.ShouldBindJSON(&req); err != nil {
lang := middleware.GetLanguage(c)
status, errCode, message := i18n.NewI18nErrorResponse("900001", lang)
c.JSON(status, models.ErrorResponse{
Code: errCode,
Message: message + ": " + err.Error(),
Timestamp: getCurrentTimestamp(),
})
return
}
err := h.cuUserService.SendRegisterSms(c.Request.Context(), &req)
if err != nil {
// err已经是i18n.I18nError,直接使用
i18nErr, ok := err.(*i18n.I18nError)
if ok {
c.JSON(i18nErr.HttpCode, models.ErrorResponse{
Code: i18nErr.Code,
Message: i18nErr.Message,
Timestamp: getCurrentTimestamp(),
})
return
}
// 如果不是i18n错误,使用通用错误
lang := middleware.GetLanguage(c)
status, errCode, message := i18n.NewI18nErrorResponse("900004", lang, err.Error())
c.JSON(status, models.ErrorResponse{
Code: errCode,
Message: message,
Timestamp: getCurrentTimestamp(),
})
return
}
lang := middleware.GetLanguage(c)
successMessage := i18n.GetErrorMessage("000000", lang)
c.JSON(http.StatusOK, models.APIResponse{
Code: "000000",
Message: successMessage,
Data: nil,
})
}
// CuUserResetPassword 重置密码
// @Summary 重置密码
// @Description 通过验证码重置密码
+1
View File
@@ -160,6 +160,7 @@ func SetupRouter() *gin.Engine {
{
cuPublic.POST("/register", cuAuthHandler.CuUserRegister)
cuPublic.POST("/login", cuAuthHandler.CuUserLogin)
cuPublic.POST("/send-register-sms", cuAuthHandler.CuUserSendRegisterSms)
cuPublic.POST("/forgot-password", cuAuthHandler.CuUserForgotPassword)
cuPublic.POST("/reset-password", cuAuthHandler.CuUserResetPassword)
cuPublic.GET("/packages", cuOrderHandler.GetPackages)
+1 -1
View File
@@ -61,7 +61,7 @@ func (o *CuOrder) BeforeUpdate(tx *gorm.DB) error {
type CuOrderCreateRequest struct {
PackageID string `json:"package_id" binding:"required"`
LicenseCount int `json:"license_count" binding:"required,min=1,max=1000"`
PaymentMethod string `json:"payment_method,omitempty"` // 可选:支付方式,不传则为免费订单
PaymentMethod string `json:"payment_method,omitempty"` // 可选:支付方式,不传则为免费订单 支持:alipay,wechat
}
// CuOrderResponse 订单响应结构
+6
View File
@@ -112,6 +112,12 @@ type CuUserPhoneUpdateRequest struct {
NewSmsCode string `json:"new_sms_code" binding:"required,len=6"` // 新手机号验证码
}
// CuUserSendRegisterSmsRequest 注册发送验证码请求结构
type CuUserSendRegisterSmsRequest struct {
Phone string `json:"phone" binding:"required,min=7,max=20"`
PhoneCountryCode string `json:"phone_country_code" binding:"omitempty"` // 可选,默认+86
}
// CuUserForgotPasswordRequest 忘记密码请求结构(发送重置验证码)
type CuUserForgotPasswordRequest struct {
Phone string `json:"phone" binding:"required,min=7,max=20"`
@@ -22,6 +22,7 @@ type CuUserService interface {
UpdateProfile(ctx context.Context, userID string, req *models.CuUserProfileUpdateRequest) error
UpdatePhone(ctx context.Context, userID string, req *models.CuUserPhoneUpdateRequest) error
ChangePassword(ctx context.Context, userID, oldPassword, newPassword string) error
SendRegisterSms(ctx context.Context, req *models.CuUserSendRegisterSmsRequest) error
ForgotPassword(ctx context.Context, req *models.CuUserForgotPasswordRequest) error
ResetPassword(ctx context.Context, req *models.CuUserResetPasswordRequest) error
GetUsersByCustomer(ctx context.Context, customerID string, offset, limit int) ([]*models.CuUser, int64, error)
@@ -353,6 +354,48 @@ func (s *cuUserService) ChangePassword(ctx context.Context, userID, oldPassword,
return nil
}
func (s *cuUserService) SendRegisterSms(ctx context.Context, req *models.CuUserSendRegisterSmsRequest) error {
lang := pkgcontext.GetLanguageFromContext(ctx)
// 业务逻辑:参数验证
if req == nil {
return i18n.NewI18nError("900001", lang)
}
// 设置默认国家代码
phoneCountryCode := req.PhoneCountryCode
if phoneCountryCode == "" {
phoneCountryCode = "+86" // 默认中国
}
// 检查手机号是否已被注册
exists, err := s.repo.CheckPhoneExists(req.Phone, phoneCountryCode, "")
if err != nil {
return i18n.NewI18nError("900004", lang, err.Error())
}
if exists {
return i18n.NewI18nError("200002", lang) // 客户已存在
}
// TODO: 实现频率限制检查
// 这里应该检查Redis中该手机号的发送频率
// 例如:1分钟内最多发送1次,1小时内最多发送5次
// 如果超出限制,返回错误码 200003
// TODO: 生成6位随机验证码并发送短信
// 1. 生成随机验证码
// 2. 调用短信服务发送验证码
// 3. 将验证码存储到Redis中,设置过期时间(5分钟)
// 例如:
// verificationCode := generateRandomCode(6)
// smsService.SendSms(phoneCountryCode + req.Phone, fmt.Sprintf("您的注册验证码是:%s", verificationCode))
// redis.Set(fmt.Sprintf("register_sms:%s:%s", phoneCountryCode, req.Phone), verificationCode, 5*time.Minute)
// 如果发送失败,返回错误码 200004
// 暂时返回成功,等待短信服务实现
return nil
}
func (s *cuUserService) ForgotPassword(ctx context.Context, req *models.CuUserForgotPasswordRequest) error {
lang := pkgcontext.GetLanguageFromContext(ctx)