feat(架构): 完善基础架构设计
This commit is contained in:
72
internal/domains/user/dto/sms_dto.go
Normal file
72
internal/domains/user/dto/sms_dto.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"tyapi-server/internal/domains/user/entities"
|
||||
)
|
||||
|
||||
// SendCodeRequest 发送验证码请求
|
||||
type SendCodeRequest struct {
|
||||
Phone string `json:"phone" binding:"required,len=11" example:"13800138000"`
|
||||
Scene entities.SMSScene `json:"scene" binding:"required,oneof=register login change_password reset_password bind unbind" example:"register"`
|
||||
}
|
||||
|
||||
// SendCodeResponse 发送验证码响应
|
||||
type SendCodeResponse struct {
|
||||
Message string `json:"message" example:"验证码发送成功"`
|
||||
ExpiresAt time.Time `json:"expires_at" example:"2024-01-01T00:05:00Z"`
|
||||
}
|
||||
|
||||
// VerifyCodeRequest 验证验证码请求
|
||||
type VerifyCodeRequest struct {
|
||||
Phone string `json:"phone" binding:"required,len=11" example:"13800138000"`
|
||||
Code string `json:"code" binding:"required,len=6" example:"123456"`
|
||||
Scene entities.SMSScene `json:"scene" binding:"required,oneof=register login change_password reset_password bind unbind" example:"register"`
|
||||
}
|
||||
|
||||
// SMSCodeResponse SMS验证码记录响应
|
||||
type SMSCodeResponse struct {
|
||||
ID string `json:"id" example:"123e4567-e89b-12d3-a456-426614174000"`
|
||||
Phone string `json:"phone" example:"13800138000"`
|
||||
Scene entities.SMSScene `json:"scene" example:"register"`
|
||||
Used bool `json:"used" example:"false"`
|
||||
ExpiresAt time.Time `json:"expires_at" example:"2024-01-01T00:05:00Z"`
|
||||
CreatedAt time.Time `json:"created_at" example:"2024-01-01T00:00:00Z"`
|
||||
}
|
||||
|
||||
// SMSCodeListRequest SMS验证码列表请求
|
||||
type SMSCodeListRequest struct {
|
||||
Phone string `form:"phone" binding:"omitempty,len=11" example:"13800138000"`
|
||||
Scene entities.SMSScene `form:"scene" binding:"omitempty,oneof=register login change_password reset_password bind unbind" example:"register"`
|
||||
Page int `form:"page" binding:"omitempty,min=1" example:"1"`
|
||||
PageSize int `form:"page_size" binding:"omitempty,min=1,max=100" example:"20"`
|
||||
}
|
||||
|
||||
// 转换方法
|
||||
func FromSMSCodeEntity(smsCode *entities.SMSCode) *SMSCodeResponse {
|
||||
if smsCode == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &SMSCodeResponse{
|
||||
ID: smsCode.ID,
|
||||
Phone: smsCode.Phone,
|
||||
Scene: smsCode.Scene,
|
||||
Used: smsCode.Used,
|
||||
ExpiresAt: smsCode.ExpiresAt,
|
||||
CreatedAt: smsCode.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func FromSMSCodeEntities(smsCodes []*entities.SMSCode) []*SMSCodeResponse {
|
||||
if smsCodes == nil {
|
||||
return []*SMSCodeResponse{}
|
||||
}
|
||||
|
||||
responses := make([]*SMSCodeResponse, len(smsCodes))
|
||||
for i, smsCode := range smsCodes {
|
||||
responses[i] = FromSMSCodeEntity(smsCode)
|
||||
}
|
||||
return responses
|
||||
}
|
||||
@@ -6,88 +6,40 @@ import (
|
||||
"tyapi-server/internal/domains/user/entities"
|
||||
)
|
||||
|
||||
// CreateUserRequest 创建用户请求
|
||||
type CreateUserRequest struct {
|
||||
Username string `json:"username" binding:"required,min=3,max=50" example:"john_doe"`
|
||||
Email string `json:"email" binding:"required,email" example:"john@example.com"`
|
||||
Password string `json:"password" binding:"required,min=6,max=128" example:"password123"`
|
||||
FirstName string `json:"first_name" binding:"max=50" example:"John"`
|
||||
LastName string `json:"last_name" binding:"max=50" example:"Doe"`
|
||||
Phone string `json:"phone" binding:"omitempty,max=20" example:"+86-13800138000"`
|
||||
// RegisterRequest 用户注册请求
|
||||
type RegisterRequest struct {
|
||||
Phone string `json:"phone" binding:"required,len=11" example:"13800138000"`
|
||||
Password string `json:"password" binding:"required,min=6,max=128" example:"password123"`
|
||||
ConfirmPassword string `json:"confirm_password" binding:"required,eqfield=Password" example:"password123"`
|
||||
Code string `json:"code" binding:"required,len=6" example:"123456"`
|
||||
}
|
||||
|
||||
// UpdateUserRequest 更新用户请求
|
||||
type UpdateUserRequest struct {
|
||||
FirstName *string `json:"first_name,omitempty" binding:"omitempty,max=50" example:"John"`
|
||||
LastName *string `json:"last_name,omitempty" binding:"omitempty,max=50" example:"Doe"`
|
||||
Phone *string `json:"phone,omitempty" binding:"omitempty,max=20" example:"+86-13800138000"`
|
||||
Avatar *string `json:"avatar,omitempty" binding:"omitempty,url" example:"https://example.com/avatar.jpg"`
|
||||
// LoginWithPasswordRequest 密码登录请求
|
||||
type LoginWithPasswordRequest struct {
|
||||
Phone string `json:"phone" binding:"required,len=11" example:"13800138000"`
|
||||
Password string `json:"password" binding:"required" example:"password123"`
|
||||
}
|
||||
|
||||
// LoginWithSMSRequest 短信验证码登录请求
|
||||
type LoginWithSMSRequest struct {
|
||||
Phone string `json:"phone" binding:"required,len=11" example:"13800138000"`
|
||||
Code string `json:"code" binding:"required,len=6" example:"123456"`
|
||||
}
|
||||
|
||||
// ChangePasswordRequest 修改密码请求
|
||||
type ChangePasswordRequest struct {
|
||||
OldPassword string `json:"old_password" binding:"required" example:"oldpassword123"`
|
||||
NewPassword string `json:"new_password" binding:"required,min=6,max=128" example:"newpassword123"`
|
||||
OldPassword string `json:"old_password" binding:"required" example:"oldpassword123"`
|
||||
NewPassword string `json:"new_password" binding:"required,min=6,max=128" example:"newpassword123"`
|
||||
ConfirmNewPassword string `json:"confirm_new_password" binding:"required,eqfield=NewPassword" example:"newpassword123"`
|
||||
Code string `json:"code" binding:"required,len=6" example:"123456"`
|
||||
}
|
||||
|
||||
// UserResponse 用户响应
|
||||
type UserResponse struct {
|
||||
ID string `json:"id" example:"123e4567-e89b-12d3-a456-426614174000"`
|
||||
Username string `json:"username" example:"john_doe"`
|
||||
Email string `json:"email" example:"john@example.com"`
|
||||
FirstName string `json:"first_name" example:"John"`
|
||||
LastName string `json:"last_name" example:"Doe"`
|
||||
Phone string `json:"phone" example:"+86-13800138000"`
|
||||
Avatar string `json:"avatar" example:"https://example.com/avatar.jpg"`
|
||||
Status entities.UserStatus `json:"status" example:"active"`
|
||||
LastLoginAt *time.Time `json:"last_login_at,omitempty" example:"2024-01-01T00:00:00Z"`
|
||||
CreatedAt time.Time `json:"created_at" example:"2024-01-01T00:00:00Z"`
|
||||
UpdatedAt time.Time `json:"updated_at" example:"2024-01-01T00:00:00Z"`
|
||||
Profile *UserProfileResponse `json:"profile,omitempty"`
|
||||
}
|
||||
|
||||
// UserProfileResponse 用户档案响应
|
||||
type UserProfileResponse struct {
|
||||
Bio string `json:"bio,omitempty" example:"Software Developer"`
|
||||
Location string `json:"location,omitempty" example:"Beijing, China"`
|
||||
Website string `json:"website,omitempty" example:"https://johndoe.com"`
|
||||
Birthday *time.Time `json:"birthday,omitempty" example:"1990-01-01T00:00:00Z"`
|
||||
Gender string `json:"gender,omitempty" example:"male"`
|
||||
Timezone string `json:"timezone,omitempty" example:"Asia/Shanghai"`
|
||||
Language string `json:"language,omitempty" example:"zh-CN"`
|
||||
}
|
||||
|
||||
// UserListRequest 用户列表请求
|
||||
type UserListRequest struct {
|
||||
Page int `form:"page" binding:"omitempty,min=1" example:"1"`
|
||||
PageSize int `form:"page_size" binding:"omitempty,min=1,max=100" example:"20"`
|
||||
Sort string `form:"sort" binding:"omitempty,oneof=created_at updated_at username email" example:"created_at"`
|
||||
Order string `form:"order" binding:"omitempty,oneof=asc desc" example:"desc"`
|
||||
Status entities.UserStatus `form:"status" binding:"omitempty,oneof=active inactive suspended pending" example:"active"`
|
||||
Search string `form:"search" binding:"omitempty,max=100" example:"john"`
|
||||
Filters map[string]interface{} `form:"-"`
|
||||
}
|
||||
|
||||
// UserListResponse 用户列表响应
|
||||
type UserListResponse struct {
|
||||
Users []*UserResponse `json:"users"`
|
||||
Pagination PaginationMeta `json:"pagination"`
|
||||
}
|
||||
|
||||
// PaginationMeta 分页元数据
|
||||
type PaginationMeta struct {
|
||||
Page int `json:"page" example:"1"`
|
||||
PageSize int `json:"page_size" example:"20"`
|
||||
Total int64 `json:"total" example:"100"`
|
||||
TotalPages int `json:"total_pages" example:"5"`
|
||||
HasNext bool `json:"has_next" example:"true"`
|
||||
HasPrev bool `json:"has_prev" example:"false"`
|
||||
}
|
||||
|
||||
// LoginRequest 登录请求
|
||||
type LoginRequest struct {
|
||||
Login string `json:"login" binding:"required" example:"john_doe"`
|
||||
Password string `json:"password" binding:"required" example:"password123"`
|
||||
ID string `json:"id" example:"123e4567-e89b-12d3-a456-426614174000"`
|
||||
Phone string `json:"phone" example:"13800138000"`
|
||||
CreatedAt time.Time `json:"created_at" example:"2024-01-01T00:00:00Z"`
|
||||
UpdatedAt time.Time `json:"updated_at" example:"2024-01-01T00:00:00Z"`
|
||||
}
|
||||
|
||||
// LoginResponse 登录响应
|
||||
@@ -96,47 +48,27 @@ type LoginResponse struct {
|
||||
AccessToken string `json:"access_token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."`
|
||||
TokenType string `json:"token_type" example:"Bearer"`
|
||||
ExpiresIn int64 `json:"expires_in" example:"86400"`
|
||||
}
|
||||
|
||||
// UpdateProfileRequest 更新用户档案请求
|
||||
type UpdateProfileRequest struct {
|
||||
Bio *string `json:"bio,omitempty" binding:"omitempty,max=500" example:"Software Developer"`
|
||||
Location *string `json:"location,omitempty" binding:"omitempty,max=100" example:"Beijing, China"`
|
||||
Website *string `json:"website,omitempty" binding:"omitempty,url" example:"https://johndoe.com"`
|
||||
Birthday *time.Time `json:"birthday,omitempty" example:"1990-01-01T00:00:00Z"`
|
||||
Gender *string `json:"gender,omitempty" binding:"omitempty,oneof=male female other" example:"male"`
|
||||
Timezone *string `json:"timezone,omitempty" binding:"omitempty,max=50" example:"Asia/Shanghai"`
|
||||
Language *string `json:"language,omitempty" binding:"omitempty,max=10" example:"zh-CN"`
|
||||
}
|
||||
|
||||
// UserStatsResponse 用户统计响应
|
||||
type UserStatsResponse struct {
|
||||
TotalUsers int64 `json:"total_users" example:"1000"`
|
||||
ActiveUsers int64 `json:"active_users" example:"950"`
|
||||
InactiveUsers int64 `json:"inactive_users" example:"30"`
|
||||
SuspendedUsers int64 `json:"suspended_users" example:"20"`
|
||||
NewUsersToday int64 `json:"new_users_today" example:"5"`
|
||||
NewUsersWeek int64 `json:"new_users_week" example:"25"`
|
||||
NewUsersMonth int64 `json:"new_users_month" example:"120"`
|
||||
}
|
||||
|
||||
// UserSearchRequest 用户搜索请求
|
||||
type UserSearchRequest struct {
|
||||
Query string `form:"q" binding:"required,min=1,max=100" example:"john"`
|
||||
Page int `form:"page" binding:"omitempty,min=1" example:"1"`
|
||||
PageSize int `form:"page_size" binding:"omitempty,min=1,max=50" example:"10"`
|
||||
LoginMethod string `json:"login_method" example:"password"` // password 或 sms
|
||||
}
|
||||
|
||||
// 转换方法
|
||||
func (r *CreateUserRequest) ToEntity() *entities.User {
|
||||
func (r *RegisterRequest) ToEntity() *entities.User {
|
||||
return &entities.User{
|
||||
Username: r.Username,
|
||||
Email: r.Email,
|
||||
Password: r.Password,
|
||||
FirstName: r.FirstName,
|
||||
LastName: r.LastName,
|
||||
Phone: r.Phone,
|
||||
Status: entities.UserStatusActive,
|
||||
Phone: r.Phone,
|
||||
Password: r.Password,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *LoginWithPasswordRequest) ToEntity() *entities.User {
|
||||
return &entities.User{
|
||||
Phone: r.Phone,
|
||||
Password: r.Password,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *LoginWithSMSRequest) ToEntity() *entities.User {
|
||||
return &entities.User{
|
||||
Phone: r.Phone,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,28 +78,9 @@ func FromEntity(user *entities.User) *UserResponse {
|
||||
}
|
||||
|
||||
return &UserResponse{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
FirstName: user.FirstName,
|
||||
LastName: user.LastName,
|
||||
Phone: user.Phone,
|
||||
Avatar: user.Avatar,
|
||||
Status: user.Status,
|
||||
LastLoginAt: user.LastLoginAt,
|
||||
CreatedAt: user.CreatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
ID: user.ID,
|
||||
Phone: user.Phone,
|
||||
CreatedAt: user.CreatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func FromEntities(users []*entities.User) []*UserResponse {
|
||||
if users == nil {
|
||||
return []*UserResponse{}
|
||||
}
|
||||
|
||||
responses := make([]*UserResponse, len(users))
|
||||
for i, user := range users {
|
||||
responses[i] = FromEntity(user)
|
||||
}
|
||||
return responses
|
||||
}
|
||||
|
||||
88
internal/domains/user/entities/sms_code.go
Normal file
88
internal/domains/user/entities/sms_code.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// SMSCode 短信验证码记录
|
||||
type SMSCode struct {
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" json:"id"`
|
||||
Phone string `gorm:"type:varchar(20);not null;index" json:"phone"`
|
||||
Code string `gorm:"type:varchar(10);not null" json:"-"` // 不返回给前端
|
||||
Scene SMSScene `gorm:"type:varchar(20);not null" json:"scene"`
|
||||
Used bool `gorm:"default:false" json:"used"`
|
||||
ExpiresAt time.Time `gorm:"not null" json:"expires_at"`
|
||||
UsedAt *time.Time `json:"used_at,omitempty"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
// 额外信息
|
||||
IP string `gorm:"type:varchar(45)" json:"ip"`
|
||||
UserAgent string `gorm:"type:varchar(500)" json:"user_agent"`
|
||||
}
|
||||
|
||||
// SMSScene 短信验证码使用场景
|
||||
type SMSScene string
|
||||
|
||||
const (
|
||||
SMSSceneRegister SMSScene = "register" // 注册
|
||||
SMSSceneLogin SMSScene = "login" // 登录
|
||||
SMSSceneChangePassword SMSScene = "change_password" // 修改密码
|
||||
SMSSceneResetPassword SMSScene = "reset_password" // 重置密码
|
||||
SMSSceneBind SMSScene = "bind" // 绑定手机号
|
||||
SMSSceneUnbind SMSScene = "unbind" // 解绑手机号
|
||||
)
|
||||
|
||||
// 实现 Entity 接口
|
||||
func (s *SMSCode) GetID() string {
|
||||
return s.ID
|
||||
}
|
||||
|
||||
func (s *SMSCode) GetCreatedAt() time.Time {
|
||||
return s.CreatedAt
|
||||
}
|
||||
|
||||
func (s *SMSCode) GetUpdatedAt() time.Time {
|
||||
return s.UpdatedAt
|
||||
}
|
||||
|
||||
// Validate 验证短信验证码
|
||||
func (s *SMSCode) Validate() error {
|
||||
if s.Phone == "" {
|
||||
return &ValidationError{Message: "手机号不能为空"}
|
||||
}
|
||||
if s.Code == "" {
|
||||
return &ValidationError{Message: "验证码不能为空"}
|
||||
}
|
||||
if s.Scene == "" {
|
||||
return &ValidationError{Message: "使用场景不能为空"}
|
||||
}
|
||||
if s.ExpiresAt.IsZero() {
|
||||
return &ValidationError{Message: "过期时间不能为空"}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 业务方法
|
||||
func (s *SMSCode) IsExpired() bool {
|
||||
return time.Now().After(s.ExpiresAt)
|
||||
}
|
||||
|
||||
func (s *SMSCode) IsValid() bool {
|
||||
return !s.Used && !s.IsExpired()
|
||||
}
|
||||
|
||||
func (s *SMSCode) MarkAsUsed() {
|
||||
s.Used = true
|
||||
now := time.Now()
|
||||
s.UsedAt = &now
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (SMSCode) TableName() string {
|
||||
return "sms_codes"
|
||||
}
|
||||
@@ -8,37 +8,14 @@ import (
|
||||
|
||||
// User 用户实体
|
||||
type User struct {
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" json:"id"`
|
||||
Username string `gorm:"uniqueIndex;type:varchar(50);not null" json:"username"`
|
||||
Email string `gorm:"uniqueIndex;type:varchar(100);not null" json:"email"`
|
||||
Password string `gorm:"type:varchar(255);not null" json:"-"`
|
||||
FirstName string `gorm:"type:varchar(50)" json:"first_name"`
|
||||
LastName string `gorm:"type:varchar(50)" json:"last_name"`
|
||||
Phone string `gorm:"type:varchar(20)" json:"phone"`
|
||||
Avatar string `gorm:"type:varchar(255)" json:"avatar"`
|
||||
Status UserStatus `gorm:"type:varchar(20);default:'active'" json:"status"`
|
||||
LastLoginAt *time.Time `json:"last_login_at"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
// 软删除字段
|
||||
IsDeleted bool `gorm:"default:false" json:"is_deleted"`
|
||||
|
||||
// 版本控制
|
||||
Version int `gorm:"default:1" json:"version"`
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" json:"id"`
|
||||
Phone string `gorm:"uniqueIndex;type:varchar(20);not null" json:"phone"`
|
||||
Password string `gorm:"type:varchar(255);not null" json:"-"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
}
|
||||
|
||||
// UserStatus 用户状态枚举
|
||||
type UserStatus string
|
||||
|
||||
const (
|
||||
UserStatusActive UserStatus = "active"
|
||||
UserStatusInactive UserStatus = "inactive"
|
||||
UserStatusSuspended UserStatus = "suspended"
|
||||
UserStatusPending UserStatus = "pending"
|
||||
)
|
||||
|
||||
// 实现 Entity 接口
|
||||
func (u *User) GetID() string {
|
||||
return u.ID
|
||||
@@ -52,47 +29,13 @@ func (u *User) GetUpdatedAt() time.Time {
|
||||
return u.UpdatedAt
|
||||
}
|
||||
|
||||
// 业务方法
|
||||
func (u *User) IsActive() bool {
|
||||
return u.Status == UserStatusActive && !u.IsDeleted
|
||||
}
|
||||
|
||||
func (u *User) GetFullName() string {
|
||||
if u.FirstName == "" && u.LastName == "" {
|
||||
return u.Username
|
||||
}
|
||||
return u.FirstName + " " + u.LastName
|
||||
}
|
||||
|
||||
func (u *User) CanLogin() bool {
|
||||
return u.IsActive() && u.Status != UserStatusSuspended
|
||||
}
|
||||
|
||||
func (u *User) MarkAsDeleted() {
|
||||
u.IsDeleted = true
|
||||
u.Status = UserStatusInactive
|
||||
}
|
||||
|
||||
func (u *User) Restore() {
|
||||
u.IsDeleted = false
|
||||
u.Status = UserStatusActive
|
||||
}
|
||||
|
||||
func (u *User) UpdateLastLogin() {
|
||||
now := time.Now()
|
||||
u.LastLoginAt = &now
|
||||
}
|
||||
|
||||
// 验证方法
|
||||
func (u *User) Validate() error {
|
||||
if u.Username == "" {
|
||||
return NewValidationError("username is required")
|
||||
}
|
||||
if u.Email == "" {
|
||||
return NewValidationError("email is required")
|
||||
if u.Phone == "" {
|
||||
return NewValidationError("手机号不能为空")
|
||||
}
|
||||
if u.Password == "" {
|
||||
return NewValidationError("password is required")
|
||||
return NewValidationError("密码不能为空")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -114,25 +57,3 @@ func (e *ValidationError) Error() string {
|
||||
func NewValidationError(message string) *ValidationError {
|
||||
return &ValidationError{Message: message}
|
||||
}
|
||||
|
||||
// UserProfile 用户档案(扩展信息)
|
||||
type UserProfile struct {
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" json:"id"`
|
||||
UserID string `gorm:"type:varchar(36);not null;index" json:"user_id"`
|
||||
Bio string `gorm:"type:text" json:"bio"`
|
||||
Location string `gorm:"type:varchar(100)" json:"location"`
|
||||
Website string `gorm:"type:varchar(255)" json:"website"`
|
||||
Birthday *time.Time `json:"birthday"`
|
||||
Gender string `gorm:"type:varchar(10)" json:"gender"`
|
||||
Timezone string `gorm:"type:varchar(50)" json:"timezone"`
|
||||
Language string `gorm:"type:varchar(10);default:'zh-CN'" json:"language"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
|
||||
// 关联关系
|
||||
User *User `gorm:"foreignKey:UserID;references:ID" json:"user,omitempty"`
|
||||
}
|
||||
|
||||
func (UserProfile) TableName() string {
|
||||
return "user_profiles"
|
||||
}
|
||||
|
||||
@@ -13,15 +13,9 @@ import (
|
||||
type UserEventType string
|
||||
|
||||
const (
|
||||
UserCreatedEvent UserEventType = "user.created"
|
||||
UserUpdatedEvent UserEventType = "user.updated"
|
||||
UserDeletedEvent UserEventType = "user.deleted"
|
||||
UserRestoredEvent UserEventType = "user.restored"
|
||||
UserRegisteredEvent UserEventType = "user.registered"
|
||||
UserLoggedInEvent UserEventType = "user.logged_in"
|
||||
UserLoggedOutEvent UserEventType = "user.logged_out"
|
||||
UserPasswordChangedEvent UserEventType = "user.password_changed"
|
||||
UserStatusChangedEvent UserEventType = "user.status_changed"
|
||||
UserProfileUpdatedEvent UserEventType = "user.profile_updated"
|
||||
)
|
||||
|
||||
// BaseUserEvent 用户事件基础结构
|
||||
@@ -99,17 +93,17 @@ func (e *BaseUserEvent) Unmarshal(data []byte) error {
|
||||
return json.Unmarshal(data, e)
|
||||
}
|
||||
|
||||
// UserCreated 用户创建事件
|
||||
type UserCreated struct {
|
||||
// UserRegistered 用户注册事件
|
||||
type UserRegistered struct {
|
||||
*BaseUserEvent
|
||||
User *entities.User `json:"user"`
|
||||
}
|
||||
|
||||
func NewUserCreatedEvent(user *entities.User, correlationID string) *UserCreated {
|
||||
return &UserCreated{
|
||||
func NewUserRegisteredEvent(user *entities.User, correlationID string) *UserRegistered {
|
||||
return &UserRegistered{
|
||||
BaseUserEvent: &BaseUserEvent{
|
||||
ID: uuid.New().String(),
|
||||
Type: string(UserCreatedEvent),
|
||||
Type: string(UserRegisteredEvent),
|
||||
Version: "1.0",
|
||||
Timestamp: time.Now(),
|
||||
Source: "user-service",
|
||||
@@ -118,97 +112,28 @@ func NewUserCreatedEvent(user *entities.User, correlationID string) *UserCreated
|
||||
DomainVersion: "1.0",
|
||||
CorrelationID: correlationID,
|
||||
Metadata: map[string]interface{}{
|
||||
"user_id": user.ID,
|
||||
"username": user.Username,
|
||||
"email": user.Email,
|
||||
"user_id": user.ID,
|
||||
"phone": user.Phone,
|
||||
},
|
||||
},
|
||||
User: user,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *UserCreated) GetPayload() interface{} {
|
||||
func (e *UserRegistered) GetPayload() interface{} {
|
||||
return e.User
|
||||
}
|
||||
|
||||
// UserUpdated 用户更新事件
|
||||
type UserUpdated struct {
|
||||
*BaseUserEvent
|
||||
UserID string `json:"user_id"`
|
||||
Changes map[string]interface{} `json:"changes"`
|
||||
OldValues map[string]interface{} `json:"old_values"`
|
||||
NewValues map[string]interface{} `json:"new_values"`
|
||||
}
|
||||
|
||||
func NewUserUpdatedEvent(userID string, changes, oldValues, newValues map[string]interface{}, correlationID string) *UserUpdated {
|
||||
return &UserUpdated{
|
||||
BaseUserEvent: &BaseUserEvent{
|
||||
ID: uuid.New().String(),
|
||||
Type: string(UserUpdatedEvent),
|
||||
Version: "1.0",
|
||||
Timestamp: time.Now(),
|
||||
Source: "user-service",
|
||||
AggregateID: userID,
|
||||
AggregateType: "User",
|
||||
DomainVersion: "1.0",
|
||||
CorrelationID: correlationID,
|
||||
Metadata: map[string]interface{}{
|
||||
"user_id": userID,
|
||||
"changed_fields": len(changes),
|
||||
},
|
||||
},
|
||||
UserID: userID,
|
||||
Changes: changes,
|
||||
OldValues: oldValues,
|
||||
NewValues: newValues,
|
||||
}
|
||||
}
|
||||
|
||||
// UserDeleted 用户删除事件
|
||||
type UserDeleted struct {
|
||||
*BaseUserEvent
|
||||
UserID string `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
SoftDelete bool `json:"soft_delete"`
|
||||
}
|
||||
|
||||
func NewUserDeletedEvent(userID, username, email string, softDelete bool, correlationID string) *UserDeleted {
|
||||
return &UserDeleted{
|
||||
BaseUserEvent: &BaseUserEvent{
|
||||
ID: uuid.New().String(),
|
||||
Type: string(UserDeletedEvent),
|
||||
Version: "1.0",
|
||||
Timestamp: time.Now(),
|
||||
Source: "user-service",
|
||||
AggregateID: userID,
|
||||
AggregateType: "User",
|
||||
DomainVersion: "1.0",
|
||||
CorrelationID: correlationID,
|
||||
Metadata: map[string]interface{}{
|
||||
"user_id": userID,
|
||||
"username": username,
|
||||
"email": email,
|
||||
"soft_delete": softDelete,
|
||||
},
|
||||
},
|
||||
UserID: userID,
|
||||
Username: username,
|
||||
Email: email,
|
||||
SoftDelete: softDelete,
|
||||
}
|
||||
}
|
||||
|
||||
// UserLoggedIn 用户登录事件
|
||||
type UserLoggedIn struct {
|
||||
*BaseUserEvent
|
||||
UserID string `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
Phone string `json:"phone"`
|
||||
IPAddress string `json:"ip_address"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
}
|
||||
|
||||
func NewUserLoggedInEvent(userID, username, ipAddress, userAgent, correlationID string) *UserLoggedIn {
|
||||
func NewUserLoggedInEvent(userID, phone, ipAddress, userAgent, correlationID string) *UserLoggedIn {
|
||||
return &UserLoggedIn{
|
||||
BaseUserEvent: &BaseUserEvent{
|
||||
ID: uuid.New().String(),
|
||||
@@ -222,13 +147,13 @@ func NewUserLoggedInEvent(userID, username, ipAddress, userAgent, correlationID
|
||||
CorrelationID: correlationID,
|
||||
Metadata: map[string]interface{}{
|
||||
"user_id": userID,
|
||||
"username": username,
|
||||
"phone": phone,
|
||||
"ip_address": ipAddress,
|
||||
"user_agent": userAgent,
|
||||
},
|
||||
},
|
||||
UserID: userID,
|
||||
Username: username,
|
||||
Phone: phone,
|
||||
IPAddress: ipAddress,
|
||||
UserAgent: userAgent,
|
||||
}
|
||||
@@ -237,11 +162,11 @@ func NewUserLoggedInEvent(userID, username, ipAddress, userAgent, correlationID
|
||||
// UserPasswordChanged 用户密码修改事件
|
||||
type UserPasswordChanged struct {
|
||||
*BaseUserEvent
|
||||
UserID string `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
UserID string `json:"user_id"`
|
||||
Phone string `json:"phone"`
|
||||
}
|
||||
|
||||
func NewUserPasswordChangedEvent(userID, username, correlationID string) *UserPasswordChanged {
|
||||
func NewUserPasswordChangedEvent(userID, phone, correlationID string) *UserPasswordChanged {
|
||||
return &UserPasswordChanged{
|
||||
BaseUserEvent: &BaseUserEvent{
|
||||
ID: uuid.New().String(),
|
||||
@@ -254,46 +179,11 @@ func NewUserPasswordChangedEvent(userID, username, correlationID string) *UserPa
|
||||
DomainVersion: "1.0",
|
||||
CorrelationID: correlationID,
|
||||
Metadata: map[string]interface{}{
|
||||
"user_id": userID,
|
||||
"username": username,
|
||||
"user_id": userID,
|
||||
"phone": phone,
|
||||
},
|
||||
},
|
||||
UserID: userID,
|
||||
Username: username,
|
||||
}
|
||||
}
|
||||
|
||||
// UserStatusChanged 用户状态变更事件
|
||||
type UserStatusChanged struct {
|
||||
*BaseUserEvent
|
||||
UserID string `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
OldStatus entities.UserStatus `json:"old_status"`
|
||||
NewStatus entities.UserStatus `json:"new_status"`
|
||||
}
|
||||
|
||||
func NewUserStatusChangedEvent(userID, username string, oldStatus, newStatus entities.UserStatus, correlationID string) *UserStatusChanged {
|
||||
return &UserStatusChanged{
|
||||
BaseUserEvent: &BaseUserEvent{
|
||||
ID: uuid.New().String(),
|
||||
Type: string(UserStatusChangedEvent),
|
||||
Version: "1.0",
|
||||
Timestamp: time.Now(),
|
||||
Source: "user-service",
|
||||
AggregateID: userID,
|
||||
AggregateType: "User",
|
||||
DomainVersion: "1.0",
|
||||
CorrelationID: correlationID,
|
||||
Metadata: map[string]interface{}{
|
||||
"user_id": userID,
|
||||
"username": username,
|
||||
"old_status": oldStatus,
|
||||
"new_status": newStatus,
|
||||
},
|
||||
},
|
||||
UserID: userID,
|
||||
Username: username,
|
||||
OldStatus: oldStatus,
|
||||
NewStatus: newStatus,
|
||||
UserID: userID,
|
||||
Phone: phone,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
@@ -14,211 +14,123 @@ import (
|
||||
|
||||
// UserHandler 用户HTTP处理器
|
||||
type UserHandler struct {
|
||||
userService *services.UserService
|
||||
response interfaces.ResponseBuilder
|
||||
validator interfaces.RequestValidator
|
||||
logger *zap.Logger
|
||||
jwtAuth *middleware.JWTAuthMiddleware
|
||||
userService interfaces.UserService
|
||||
smsCodeService *services.SMSCodeService
|
||||
response interfaces.ResponseBuilder
|
||||
validator interfaces.RequestValidator
|
||||
logger *zap.Logger
|
||||
jwtAuth *middleware.JWTAuthMiddleware
|
||||
}
|
||||
|
||||
// NewUserHandler 创建用户处理器
|
||||
func NewUserHandler(
|
||||
userService *services.UserService,
|
||||
userService interfaces.UserService,
|
||||
smsCodeService *services.SMSCodeService,
|
||||
response interfaces.ResponseBuilder,
|
||||
validator interfaces.RequestValidator,
|
||||
logger *zap.Logger,
|
||||
jwtAuth *middleware.JWTAuthMiddleware,
|
||||
) *UserHandler {
|
||||
return &UserHandler{
|
||||
userService: userService,
|
||||
response: response,
|
||||
validator: validator,
|
||||
logger: logger,
|
||||
jwtAuth: jwtAuth,
|
||||
userService: userService,
|
||||
smsCodeService: smsCodeService,
|
||||
response: response,
|
||||
validator: validator,
|
||||
logger: logger,
|
||||
jwtAuth: jwtAuth,
|
||||
}
|
||||
}
|
||||
|
||||
// GetPath 返回处理器路径
|
||||
func (h *UserHandler) GetPath() string {
|
||||
return "/users"
|
||||
}
|
||||
|
||||
// GetMethod 返回HTTP方法
|
||||
func (h *UserHandler) GetMethod() string {
|
||||
return "GET" // 主要用于列表,具体方法在路由注册时指定
|
||||
}
|
||||
|
||||
// GetMiddlewares 返回中间件
|
||||
func (h *UserHandler) GetMiddlewares() []gin.HandlerFunc {
|
||||
return []gin.HandlerFunc{
|
||||
// 这里可以添加特定的中间件
|
||||
}
|
||||
}
|
||||
|
||||
// Handle 主处理函数(用于列表)
|
||||
func (h *UserHandler) Handle(c *gin.Context) {
|
||||
h.List(c)
|
||||
}
|
||||
|
||||
// RequiresAuth 是否需要认证
|
||||
func (h *UserHandler) RequiresAuth() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// GetPermissions 获取所需权限
|
||||
func (h *UserHandler) GetPermissions() []string {
|
||||
return []string{"user:read"}
|
||||
}
|
||||
|
||||
// REST操作实现
|
||||
|
||||
// Create 创建用户
|
||||
func (h *UserHandler) Create(c *gin.Context) {
|
||||
var req dto.CreateUserRequest
|
||||
// SendCode 发送验证码
|
||||
// @Summary 发送短信验证码
|
||||
// @Description 向指定手机号发送验证码,支持注册、登录、修改密码等场景
|
||||
// @Tags 用户认证
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body dto.SendCodeRequest true "发送验证码请求"
|
||||
// @Success 200 {object} dto.SendCodeResponse "验证码发送成功"
|
||||
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
||||
// @Failure 429 {object} map[string]interface{} "请求频率限制"
|
||||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||
// @Router /users/send-code [post]
|
||||
func (h *UserHandler) SendCode(c *gin.Context) {
|
||||
var req dto.SendCodeRequest
|
||||
|
||||
// 验证请求体
|
||||
if err := h.validator.BindAndValidate(c, &req); err != nil {
|
||||
return // 响应已在验证器中处理
|
||||
}
|
||||
|
||||
// 创建用户
|
||||
user, err := h.userService.Create(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to create user", zap.Error(err))
|
||||
// 获取客户端信息
|
||||
clientIP := c.ClientIP()
|
||||
userAgent := c.GetHeader("User-Agent")
|
||||
|
||||
// 发送验证码
|
||||
if err := h.smsCodeService.SendCode(c.Request.Context(), req.Phone, req.Scene, clientIP, userAgent); err != nil {
|
||||
h.logger.Error("发送验证码失败",
|
||||
zap.String("phone", req.Phone),
|
||||
zap.String("scene", string(req.Scene)),
|
||||
zap.Error(err))
|
||||
h.response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 返回响应
|
||||
response := dto.FromEntity(user)
|
||||
h.response.Created(c, response, "User created successfully")
|
||||
}
|
||||
|
||||
// GetByID 根据ID获取用户
|
||||
func (h *UserHandler) GetByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
h.response.BadRequest(c, "User ID is required")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取用户
|
||||
user, err := h.userService.GetByID(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to get user", zap.Error(err))
|
||||
h.response.NotFound(c, "User not found")
|
||||
return
|
||||
}
|
||||
|
||||
// 返回响应
|
||||
response := dto.FromEntity(user)
|
||||
h.response.Success(c, response)
|
||||
}
|
||||
|
||||
// Update 更新用户
|
||||
func (h *UserHandler) Update(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
h.response.BadRequest(c, "User ID is required")
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.UpdateUserRequest
|
||||
|
||||
// 验证请求体
|
||||
if err := h.validator.BindAndValidate(c, &req); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 更新用户
|
||||
user, err := h.userService.Update(c.Request.Context(), id, &req)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to update user", zap.Error(err))
|
||||
h.response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 返回响应
|
||||
response := dto.FromEntity(user)
|
||||
h.response.Success(c, response, "User updated successfully")
|
||||
}
|
||||
|
||||
// Delete 删除用户
|
||||
func (h *UserHandler) Delete(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
h.response.BadRequest(c, "User ID is required")
|
||||
return
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
if err := h.userService.Delete(c.Request.Context(), id); err != nil {
|
||||
h.logger.Error("Failed to delete user", zap.Error(err))
|
||||
h.response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 返回响应
|
||||
h.response.Success(c, nil, "User deleted successfully")
|
||||
}
|
||||
|
||||
// List 获取用户列表
|
||||
func (h *UserHandler) List(c *gin.Context) {
|
||||
var req dto.UserListRequest
|
||||
|
||||
// 验证查询参数
|
||||
if err := h.validator.ValidateQuery(c, &req); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 设置默认值
|
||||
if req.Page <= 0 {
|
||||
req.Page = 1
|
||||
}
|
||||
if req.PageSize <= 0 {
|
||||
req.PageSize = 20
|
||||
}
|
||||
|
||||
// 构建查询选项
|
||||
options := interfaces.ListOptions{
|
||||
Page: req.Page,
|
||||
PageSize: req.PageSize,
|
||||
Sort: req.Sort,
|
||||
Order: req.Order,
|
||||
Search: req.Search,
|
||||
Filters: req.Filters,
|
||||
}
|
||||
|
||||
// 获取用户列表
|
||||
users, err := h.userService.List(c.Request.Context(), options)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to get user list", zap.Error(err))
|
||||
h.response.InternalError(c, "Failed to get user list")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
countOptions := interfaces.CountOptions{
|
||||
Search: req.Search,
|
||||
Filters: req.Filters,
|
||||
}
|
||||
total, err := h.userService.Count(c.Request.Context(), countOptions)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to count users", zap.Error(err))
|
||||
h.response.InternalError(c, "Failed to count users")
|
||||
return
|
||||
}
|
||||
|
||||
// 构建响应
|
||||
userResponses := dto.FromEntities(users)
|
||||
pagination := buildPagination(req.Page, req.PageSize, total)
|
||||
response := &dto.SendCodeResponse{
|
||||
Message: "验证码发送成功",
|
||||
ExpiresAt: time.Now().Add(5 * time.Minute), // 5分钟过期
|
||||
}
|
||||
|
||||
h.response.Paginated(c, userResponses, pagination)
|
||||
h.response.Success(c, response, "验证码发送成功")
|
||||
}
|
||||
|
||||
// Login 用户登录
|
||||
func (h *UserHandler) Login(c *gin.Context) {
|
||||
var req dto.LoginRequest
|
||||
// Register 用户注册
|
||||
// @Summary 用户注册
|
||||
// @Description 使用手机号、密码和验证码进行用户注册,需要确认密码
|
||||
// @Tags 用户认证
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body dto.RegisterRequest true "用户注册请求"
|
||||
// @Success 201 {object} dto.UserResponse "注册成功"
|
||||
// @Failure 400 {object} map[string]interface{} "请求参数错误或验证码无效"
|
||||
// @Failure 409 {object} map[string]interface{} "手机号已存在"
|
||||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||
// @Router /users/register [post]
|
||||
func (h *UserHandler) Register(c *gin.Context) {
|
||||
var req dto.RegisterRequest
|
||||
|
||||
// 验证请求体
|
||||
if err := h.validator.BindAndValidate(c, &req); err != nil {
|
||||
return // 响应已在验证器中处理
|
||||
}
|
||||
|
||||
// 注册用户
|
||||
user, err := h.userService.Register(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
h.logger.Error("注册用户失败", zap.Error(err))
|
||||
h.response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 返回响应
|
||||
response := dto.FromEntity(user)
|
||||
h.response.Created(c, response, "用户注册成功")
|
||||
}
|
||||
|
||||
// LoginWithPassword 密码登录
|
||||
// @Summary 用户密码登录
|
||||
// @Description 使用手机号和密码进行用户登录,返回JWT令牌
|
||||
// @Tags 用户认证
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body dto.LoginWithPasswordRequest true "密码登录请求"
|
||||
// @Success 200 {object} dto.LoginResponse "登录成功"
|
||||
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
||||
// @Failure 401 {object} map[string]interface{} "认证失败"
|
||||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||
// @Router /users/login-password [post]
|
||||
func (h *UserHandler) LoginWithPassword(c *gin.Context) {
|
||||
var req dto.LoginWithPasswordRequest
|
||||
|
||||
// 验证请求体
|
||||
if err := h.validator.BindAndValidate(c, &req); err != nil {
|
||||
@@ -226,18 +138,18 @@ func (h *UserHandler) Login(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 用户登录
|
||||
user, err := h.userService.Login(c.Request.Context(), &req)
|
||||
user, err := h.userService.LoginWithPassword(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
h.logger.Error("Login failed", zap.Error(err))
|
||||
h.response.Unauthorized(c, "Invalid credentials")
|
||||
h.logger.Error("密码登录失败", zap.Error(err))
|
||||
h.response.Unauthorized(c, "用户名或密码错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 生成JWT token
|
||||
accessToken, err := h.jwtAuth.GenerateToken(user.ID, user.Username, user.Email)
|
||||
accessToken, err := h.jwtAuth.GenerateToken(user.ID, user.Phone, user.Phone)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to generate token", zap.Error(err))
|
||||
h.response.InternalError(c, "Failed to generate access token")
|
||||
h.logger.Error("生成令牌失败", zap.Error(err))
|
||||
h.response.InternalError(c, "生成访问令牌失败")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -247,72 +159,109 @@ func (h *UserHandler) Login(c *gin.Context) {
|
||||
AccessToken: accessToken,
|
||||
TokenType: "Bearer",
|
||||
ExpiresIn: 86400, // 24小时,从配置获取
|
||||
LoginMethod: "password",
|
||||
}
|
||||
|
||||
h.response.Success(c, loginResponse, "Login successful")
|
||||
h.response.Success(c, loginResponse, "登录成功")
|
||||
}
|
||||
|
||||
// Logout 用户登出
|
||||
func (h *UserHandler) Logout(c *gin.Context) {
|
||||
// 简单实现,客户端删除token即可
|
||||
// 如果需要服务端黑名单,可以在这里实现
|
||||
h.response.Success(c, nil, "Logout successful")
|
||||
}
|
||||
|
||||
// GetProfile 获取当前用户信息
|
||||
func (h *UserHandler) GetProfile(c *gin.Context) {
|
||||
userID := h.getCurrentUserID(c)
|
||||
if userID == "" {
|
||||
h.response.Unauthorized(c, "User not authenticated")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
user, err := h.userService.GetByID(c.Request.Context(), userID)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to get user profile", zap.Error(err))
|
||||
h.response.NotFound(c, "User not found")
|
||||
return
|
||||
}
|
||||
|
||||
// 返回响应
|
||||
response := dto.FromEntity(user)
|
||||
h.response.Success(c, response)
|
||||
}
|
||||
|
||||
// UpdateProfile 更新当前用户信息
|
||||
func (h *UserHandler) UpdateProfile(c *gin.Context) {
|
||||
userID := h.getCurrentUserID(c)
|
||||
if userID == "" {
|
||||
h.response.Unauthorized(c, "User not authenticated")
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.UpdateUserRequest
|
||||
// LoginWithSMS 短信验证码登录
|
||||
// @Summary 用户短信验证码登录
|
||||
// @Description 使用手机号和短信验证码进行用户登录,返回JWT令牌
|
||||
// @Tags 用户认证
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body dto.LoginWithSMSRequest true "短信登录请求"
|
||||
// @Success 200 {object} dto.LoginResponse "登录成功"
|
||||
// @Failure 400 {object} map[string]interface{} "请求参数错误或验证码无效"
|
||||
// @Failure 401 {object} map[string]interface{} "认证失败"
|
||||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||
// @Router /users/login-sms [post]
|
||||
func (h *UserHandler) LoginWithSMS(c *gin.Context) {
|
||||
var req dto.LoginWithSMSRequest
|
||||
|
||||
// 验证请求体
|
||||
if err := h.validator.BindAndValidate(c, &req); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 更新用户
|
||||
user, err := h.userService.Update(c.Request.Context(), userID, &req)
|
||||
// 用户登录
|
||||
user, err := h.userService.LoginWithSMS(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to update profile", zap.Error(err))
|
||||
h.response.BadRequest(c, err.Error())
|
||||
h.logger.Error("短信登录失败", zap.Error(err))
|
||||
h.response.Unauthorized(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 返回响应
|
||||
// 生成JWT token
|
||||
accessToken, err := h.jwtAuth.GenerateToken(user.ID, user.Phone, user.Phone)
|
||||
if err != nil {
|
||||
h.logger.Error("生成令牌失败", zap.Error(err))
|
||||
h.response.InternalError(c, "生成访问令牌失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 构建登录响应
|
||||
loginResponse := &dto.LoginResponse{
|
||||
User: dto.FromEntity(user),
|
||||
AccessToken: accessToken,
|
||||
TokenType: "Bearer",
|
||||
ExpiresIn: 86400, // 24小时,从配置获取
|
||||
LoginMethod: "sms",
|
||||
}
|
||||
|
||||
h.response.Success(c, loginResponse, "登录成功")
|
||||
}
|
||||
|
||||
// GetProfile 获取当前用户信息
|
||||
// @Summary 获取当前用户信息
|
||||
// @Description 根据JWT令牌获取当前登录用户的详细信息
|
||||
// @Tags 用户管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Success 200 {object} dto.UserResponse "用户信息"
|
||||
// @Failure 401 {object} map[string]interface{} "未认证"
|
||||
// @Failure 404 {object} map[string]interface{} "用户不存在"
|
||||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||
// @Router /users/me [get]
|
||||
func (h *UserHandler) GetProfile(c *gin.Context) {
|
||||
userID := h.getCurrentUserID(c)
|
||||
if userID == "" {
|
||||
h.response.Unauthorized(c, "用户未认证")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
user, err := h.userService.GetByID(c.Request.Context(), userID)
|
||||
if err != nil {
|
||||
h.logger.Error("获取用户资料失败", zap.Error(err))
|
||||
h.response.NotFound(c, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// 返回用户信息
|
||||
response := dto.FromEntity(user)
|
||||
h.response.Success(c, response, "Profile updated successfully")
|
||||
h.response.Success(c, response, "获取用户资料成功")
|
||||
}
|
||||
|
||||
// ChangePassword 修改密码
|
||||
// @Summary 修改密码
|
||||
// @Description 使用旧密码、新密码确认和验证码修改当前用户的密码
|
||||
// @Tags 用户管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param request body dto.ChangePasswordRequest true "修改密码请求"
|
||||
// @Success 200 {object} map[string]interface{} "密码修改成功"
|
||||
// @Failure 400 {object} map[string]interface{} "请求参数错误或验证码无效"
|
||||
// @Failure 401 {object} map[string]interface{} "未认证"
|
||||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||
// @Router /users/me/password [put]
|
||||
func (h *UserHandler) ChangePassword(c *gin.Context) {
|
||||
userID := h.getCurrentUserID(c)
|
||||
if userID == "" {
|
||||
h.response.Unauthorized(c, "User not authenticated")
|
||||
h.response.Unauthorized(c, "用户未认证")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -325,78 +274,14 @@ func (h *UserHandler) ChangePassword(c *gin.Context) {
|
||||
|
||||
// 修改密码
|
||||
if err := h.userService.ChangePassword(c.Request.Context(), userID, &req); err != nil {
|
||||
h.logger.Error("Failed to change password", zap.Error(err))
|
||||
h.logger.Error("修改密码失败", zap.Error(err))
|
||||
h.response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.response.Success(c, nil, "Password changed successfully")
|
||||
h.response.Success(c, nil, "密码修改成功")
|
||||
}
|
||||
|
||||
// Search 搜索用户
|
||||
func (h *UserHandler) Search(c *gin.Context) {
|
||||
var req dto.UserSearchRequest
|
||||
|
||||
// 验证查询参数
|
||||
if err := h.validator.ValidateQuery(c, &req); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 设置默认值
|
||||
if req.Page <= 0 {
|
||||
req.Page = 1
|
||||
}
|
||||
if req.PageSize <= 0 {
|
||||
req.PageSize = 10
|
||||
}
|
||||
|
||||
// 构建查询选项
|
||||
options := interfaces.ListOptions{
|
||||
Page: req.Page,
|
||||
PageSize: req.PageSize,
|
||||
Search: req.Query,
|
||||
}
|
||||
|
||||
// 搜索用户
|
||||
users, err := h.userService.Search(c.Request.Context(), req.Query, options)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to search users", zap.Error(err))
|
||||
h.response.InternalError(c, "Failed to search users")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取搜索结果总数
|
||||
countOptions := interfaces.CountOptions{
|
||||
Search: req.Query,
|
||||
}
|
||||
total, err := h.userService.Count(c.Request.Context(), countOptions)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to count search results", zap.Error(err))
|
||||
h.response.InternalError(c, "Failed to count search results")
|
||||
return
|
||||
}
|
||||
|
||||
// 构建响应
|
||||
userResponses := dto.FromEntities(users)
|
||||
pagination := buildPagination(req.Page, req.PageSize, total)
|
||||
|
||||
h.response.Paginated(c, userResponses, pagination)
|
||||
}
|
||||
|
||||
// GetStats 获取用户统计
|
||||
func (h *UserHandler) GetStats(c *gin.Context) {
|
||||
stats, err := h.userService.GetStats(c.Request.Context())
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to get user stats", zap.Error(err))
|
||||
h.response.InternalError(c, "Failed to get user statistics")
|
||||
return
|
||||
}
|
||||
|
||||
h.response.Success(c, stats)
|
||||
}
|
||||
|
||||
// 私有方法
|
||||
|
||||
// getCurrentUserID 获取当前用户ID
|
||||
func (h *UserHandler) getCurrentUserID(c *gin.Context) string {
|
||||
if userID, exists := c.Get("user_id"); exists {
|
||||
@@ -406,50 +291,3 @@ func (h *UserHandler) getCurrentUserID(c *gin.Context) string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// parsePageSize 解析页面大小
|
||||
func (h *UserHandler) parsePageSize(str string, defaultValue int) int {
|
||||
if str == "" {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
if size, err := strconv.Atoi(str); err == nil && size > 0 && size <= 100 {
|
||||
return size
|
||||
}
|
||||
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// parsePage 解析页码
|
||||
func (h *UserHandler) parsePage(str string, defaultValue int) int {
|
||||
if str == "" {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
if page, err := strconv.Atoi(str); err == nil && page > 0 {
|
||||
return page
|
||||
}
|
||||
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// buildPagination 构建分页元数据
|
||||
func buildPagination(page, pageSize int, total int64) interfaces.PaginationMeta {
|
||||
totalPages := int(float64(total) / float64(pageSize))
|
||||
if float64(total)/float64(pageSize) > float64(totalPages) {
|
||||
totalPages++
|
||||
}
|
||||
|
||||
if totalPages < 1 {
|
||||
totalPages = 1
|
||||
}
|
||||
|
||||
return interfaces.PaginationMeta{
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
Total: total,
|
||||
TotalPages: totalPages,
|
||||
HasNext: page < totalPages,
|
||||
HasPrev: page > 1,
|
||||
}
|
||||
}
|
||||
|
||||
120
internal/domains/user/repositories/sms_code_repository.go
Normal file
120
internal/domains/user/repositories/sms_code_repository.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"tyapi-server/internal/domains/user/entities"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
)
|
||||
|
||||
// SMSCodeRepository 短信验证码仓储
|
||||
type SMSCodeRepository struct {
|
||||
db *gorm.DB
|
||||
cache interfaces.CacheService
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewSMSCodeRepository 创建短信验证码仓储
|
||||
func NewSMSCodeRepository(db *gorm.DB, cache interfaces.CacheService, logger *zap.Logger) *SMSCodeRepository {
|
||||
return &SMSCodeRepository{
|
||||
db: db,
|
||||
cache: cache,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Create 创建短信验证码记录
|
||||
func (r *SMSCodeRepository) Create(ctx context.Context, smsCode *entities.SMSCode) error {
|
||||
if err := r.db.WithContext(ctx).Create(smsCode).Error; err != nil {
|
||||
r.logger.Error("创建短信验证码失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 缓存验证码
|
||||
cacheKey := r.buildCacheKey(smsCode.Phone, smsCode.Scene)
|
||||
r.cache.Set(ctx, cacheKey, smsCode, 5*time.Minute)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetValidCode 获取有效的验证码
|
||||
func (r *SMSCodeRepository) GetValidCode(ctx context.Context, phone string, scene entities.SMSScene) (*entities.SMSCode, error) {
|
||||
// 先从缓存查找
|
||||
cacheKey := r.buildCacheKey(phone, scene)
|
||||
var smsCode entities.SMSCode
|
||||
if err := r.cache.Get(ctx, cacheKey, &smsCode); err == nil {
|
||||
return &smsCode, nil
|
||||
}
|
||||
|
||||
// 从数据库查找最新的有效验证码
|
||||
if err := r.db.WithContext(ctx).
|
||||
Where("phone = ? AND scene = ? AND expires_at > ? AND used_at IS NULL",
|
||||
phone, scene, time.Now()).
|
||||
Order("created_at DESC").
|
||||
First(&smsCode).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 缓存结果
|
||||
r.cache.Set(ctx, cacheKey, &smsCode, 5*time.Minute)
|
||||
|
||||
return &smsCode, nil
|
||||
}
|
||||
|
||||
// MarkAsUsed 标记验证码为已使用
|
||||
func (r *SMSCodeRepository) MarkAsUsed(ctx context.Context, id string) error {
|
||||
now := time.Now()
|
||||
if err := r.db.WithContext(ctx).
|
||||
Model(&entities.SMSCode{}).
|
||||
Where("id = ?", id).
|
||||
Update("used_at", now).Error; err != nil {
|
||||
r.logger.Error("标记验证码为已使用失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
r.logger.Info("验证码已标记为使用", zap.String("code_id", id))
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanupExpired 清理过期的验证码
|
||||
func (r *SMSCodeRepository) CleanupExpired(ctx context.Context) error {
|
||||
result := r.db.WithContext(ctx).
|
||||
Where("expires_at < ?", time.Now()).
|
||||
Delete(&entities.SMSCode{})
|
||||
|
||||
if result.Error != nil {
|
||||
r.logger.Error("清理过期验证码失败", zap.Error(result.Error))
|
||||
return result.Error
|
||||
}
|
||||
|
||||
if result.RowsAffected > 0 {
|
||||
r.logger.Info("清理过期验证码完成", zap.Int64("count", result.RowsAffected))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CountRecentCodes 统计最近发送的验证码数量
|
||||
func (r *SMSCodeRepository) CountRecentCodes(ctx context.Context, phone string, scene entities.SMSScene, duration time.Duration) (int64, error) {
|
||||
var count int64
|
||||
if err := r.db.WithContext(ctx).
|
||||
Model(&entities.SMSCode{}).
|
||||
Where("phone = ? AND scene = ? AND created_at > ?",
|
||||
phone, scene, time.Now().Add(-duration)).
|
||||
Count(&count).Error; err != nil {
|
||||
r.logger.Error("统计最近验证码数量失败", zap.Error(err))
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// buildCacheKey 构建缓存键
|
||||
func (r *SMSCodeRepository) buildCacheKey(phone string, scene entities.SMSScene) string {
|
||||
return fmt.Sprintf("sms_code:%s:%s", phone, string(scene))
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
@@ -12,6 +13,12 @@ import (
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
)
|
||||
|
||||
// 定义错误常量
|
||||
var (
|
||||
// ErrUserNotFound 用户不存在错误
|
||||
ErrUserNotFound = errors.New("用户不存在")
|
||||
)
|
||||
|
||||
// UserRepository 用户仓储实现
|
||||
type UserRepository struct {
|
||||
db *gorm.DB
|
||||
@@ -29,311 +36,150 @@ func NewUserRepository(db *gorm.DB, cache interfaces.CacheService, logger *zap.L
|
||||
}
|
||||
|
||||
// Create 创建用户
|
||||
func (r *UserRepository) Create(ctx context.Context, entity *entities.User) error {
|
||||
if err := r.db.WithContext(ctx).Create(entity).Error; err != nil {
|
||||
r.logger.Error("Failed to create user", zap.Error(err))
|
||||
func (r *UserRepository) Create(ctx context.Context, user *entities.User) error {
|
||||
if err := r.db.WithContext(ctx).Create(user).Error; err != nil {
|
||||
r.logger.Error("创建用户失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 清除相关缓存
|
||||
r.invalidateUserCaches(ctx, entity.ID)
|
||||
r.deleteCacheByPhone(ctx, user.Phone)
|
||||
|
||||
r.logger.Info("用户创建成功", zap.String("user_id", user.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetByID 根据ID获取用户
|
||||
func (r *UserRepository) GetByID(ctx context.Context, id string) (*entities.User, error) {
|
||||
// 先尝试从缓存获取
|
||||
cacheKey := r.GetCacheKey(id)
|
||||
// 尝试从缓存获取
|
||||
cacheKey := fmt.Sprintf("user:id:%s", id)
|
||||
var user entities.User
|
||||
|
||||
if err := r.cache.Get(ctx, cacheKey, &user); err == nil {
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// 从数据库获取
|
||||
if err := r.db.WithContext(ctx).Where("id = ? AND is_deleted = false", id).First(&user).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, fmt.Errorf("user not found")
|
||||
// 从数据库查询
|
||||
if err := r.db.WithContext(ctx).Where("id = ?", id).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrUserNotFound
|
||||
}
|
||||
r.logger.Error("根据ID查询用户失败", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 缓存结果
|
||||
r.cache.Set(ctx, cacheKey, &user, 1*time.Hour)
|
||||
r.cache.Set(ctx, cacheKey, &user, 10*time.Minute)
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// FindByPhone 根据手机号查找用户
|
||||
func (r *UserRepository) FindByPhone(ctx context.Context, phone string) (*entities.User, error) {
|
||||
// 尝试从缓存获取
|
||||
cacheKey := fmt.Sprintf("user:phone:%s", phone)
|
||||
var user entities.User
|
||||
if err := r.cache.Get(ctx, cacheKey, &user); err == nil {
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// 从数据库查询
|
||||
if err := r.db.WithContext(ctx).Where("phone = ?", phone).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrUserNotFound
|
||||
}
|
||||
r.logger.Error("根据手机号查询用户失败", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 缓存结果
|
||||
r.cache.Set(ctx, cacheKey, &user, 10*time.Minute)
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// Update 更新用户
|
||||
func (r *UserRepository) Update(ctx context.Context, entity *entities.User) error {
|
||||
if err := r.db.WithContext(ctx).Save(entity).Error; err != nil {
|
||||
r.logger.Error("Failed to update user", zap.Error(err))
|
||||
func (r *UserRepository) Update(ctx context.Context, user *entities.User) error {
|
||||
if err := r.db.WithContext(ctx).Save(user).Error; err != nil {
|
||||
r.logger.Error("更新用户失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 清除相关缓存
|
||||
r.invalidateUserCaches(ctx, entity.ID)
|
||||
r.deleteCacheByID(ctx, user.ID)
|
||||
r.deleteCacheByPhone(ctx, user.Phone)
|
||||
|
||||
r.logger.Info("用户更新成功", zap.String("user_id", user.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete 删除用户
|
||||
func (r *UserRepository) Delete(ctx context.Context, id string) error {
|
||||
// 先获取用户信息用于清除缓存
|
||||
user, err := r.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := r.db.WithContext(ctx).Delete(&entities.User{}, "id = ?", id).Error; err != nil {
|
||||
r.logger.Error("Failed to delete user", zap.Error(err))
|
||||
r.logger.Error("删除用户失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 清除相关缓存
|
||||
r.invalidateUserCaches(ctx, id)
|
||||
r.deleteCacheByID(ctx, id)
|
||||
r.deleteCacheByPhone(ctx, user.Phone)
|
||||
|
||||
r.logger.Info("用户删除成功", zap.String("user_id", id))
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateBatch 批量创建用户
|
||||
func (r *UserRepository) CreateBatch(ctx context.Context, entities []*entities.User) error {
|
||||
if err := r.db.WithContext(ctx).CreateInBatches(entities, 100).Error; err != nil {
|
||||
r.logger.Error("Failed to create users in batch", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 清除列表缓存
|
||||
r.cache.DeletePattern(ctx, "users:list:*")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetByIDs 根据ID列表获取用户
|
||||
func (r *UserRepository) GetByIDs(ctx context.Context, ids []string) ([]*entities.User, error) {
|
||||
var users []entities.User
|
||||
|
||||
if err := r.db.WithContext(ctx).
|
||||
Where("id IN ? AND is_deleted = false", ids).
|
||||
Find(&users).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 转换为指针切片
|
||||
result := make([]*entities.User, len(users))
|
||||
for i := range users {
|
||||
result[i] = &users[i]
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// UpdateBatch 批量更新用户
|
||||
func (r *UserRepository) UpdateBatch(ctx context.Context, entities []*entities.User) error {
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
for _, entity := range entities {
|
||||
if err := tx.Save(entity).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteBatch 批量删除用户
|
||||
func (r *UserRepository) DeleteBatch(ctx context.Context, ids []string) error {
|
||||
if err := r.db.WithContext(ctx).
|
||||
Where("id IN ?", ids).
|
||||
Delete(&entities.User{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 清除相关缓存
|
||||
for _, id := range ids {
|
||||
r.invalidateUserCaches(ctx, id)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// List 获取用户列表
|
||||
func (r *UserRepository) List(ctx context.Context, options interfaces.ListOptions) ([]*entities.User, error) {
|
||||
// 尝试从缓存获取
|
||||
cacheKey := fmt.Sprintf("users:list:%d:%d:%s", options.Page, options.PageSize, options.Sort)
|
||||
// List 分页获取用户列表
|
||||
func (r *UserRepository) List(ctx context.Context, offset, limit int) ([]*entities.User, error) {
|
||||
var users []*entities.User
|
||||
|
||||
if err := r.cache.Get(ctx, cacheKey, &users); err == nil {
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// 从数据库查询
|
||||
query := r.db.WithContext(ctx).Where("is_deleted = false")
|
||||
|
||||
// 应用过滤条件
|
||||
if options.Search != "" {
|
||||
query = query.Where("username ILIKE ? OR email ILIKE ? OR first_name ILIKE ? OR last_name ILIKE ?",
|
||||
"%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%")
|
||||
}
|
||||
|
||||
// 应用排序
|
||||
if options.Sort != "" {
|
||||
order := options.Order
|
||||
if order == "" {
|
||||
order = "asc"
|
||||
}
|
||||
query = query.Order(fmt.Sprintf("%s %s", options.Sort, order))
|
||||
} else {
|
||||
query = query.Order("created_at desc")
|
||||
}
|
||||
|
||||
// 应用分页
|
||||
if options.Page > 0 && options.PageSize > 0 {
|
||||
offset := (options.Page - 1) * options.PageSize
|
||||
query = query.Offset(offset).Limit(options.PageSize)
|
||||
}
|
||||
|
||||
var userEntities []entities.User
|
||||
if err := query.Find(&userEntities).Error; err != nil {
|
||||
if err := r.db.WithContext(ctx).Offset(offset).Limit(limit).Find(&users).Error; err != nil {
|
||||
r.logger.Error("查询用户列表失败", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 转换为指针切片
|
||||
users = make([]*entities.User, len(userEntities))
|
||||
for i := range userEntities {
|
||||
users[i] = &userEntities[i]
|
||||
}
|
||||
|
||||
// 缓存结果
|
||||
r.cache.Set(ctx, cacheKey, users, 30*time.Minute)
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// Count 统计用户数量
|
||||
func (r *UserRepository) Count(ctx context.Context, options interfaces.CountOptions) (int64, error) {
|
||||
query := r.db.WithContext(ctx).Model(&entities.User{}).Where("is_deleted = false")
|
||||
|
||||
// 应用过滤条件
|
||||
if options.Search != "" {
|
||||
query = query.Where("username ILIKE ? OR email ILIKE ? OR first_name ILIKE ? OR last_name ILIKE ?",
|
||||
"%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%")
|
||||
}
|
||||
|
||||
// Count 获取用户总数
|
||||
func (r *UserRepository) Count(ctx context.Context) (int64, error) {
|
||||
var count int64
|
||||
if err := query.Count(&count).Error; err != nil {
|
||||
if err := r.db.WithContext(ctx).Model(&entities.User{}).Count(&count).Error; err != nil {
|
||||
r.logger.Error("统计用户数量失败", zap.Error(err))
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// Exists 检查用户是否存在
|
||||
func (r *UserRepository) Exists(ctx context.Context, id string) (bool, error) {
|
||||
// ExistsByPhone 检查手机号是否存在
|
||||
func (r *UserRepository) ExistsByPhone(ctx context.Context, phone string) (bool, error) {
|
||||
var count int64
|
||||
if err := r.db.WithContext(ctx).
|
||||
Model(&entities.User{}).
|
||||
Where("id = ? AND is_deleted = false", id).
|
||||
Count(&count).Error; err != nil {
|
||||
if err := r.db.WithContext(ctx).Model(&entities.User{}).Where("phone = ?", phone).Count(&count).Error; err != nil {
|
||||
r.logger.Error("检查手机号是否存在失败", zap.Error(err))
|
||||
return false, err
|
||||
}
|
||||
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
// SoftDelete 软删除用户
|
||||
func (r *UserRepository) SoftDelete(ctx context.Context, id string) error {
|
||||
if err := r.db.WithContext(ctx).
|
||||
Model(&entities.User{}).
|
||||
Where("id = ?", id).
|
||||
Update("is_deleted", true).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
// 私有辅助方法
|
||||
|
||||
// 清除相关缓存
|
||||
r.invalidateUserCaches(ctx, id)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Restore 恢复用户
|
||||
func (r *UserRepository) Restore(ctx context.Context, id string) error {
|
||||
if err := r.db.WithContext(ctx).
|
||||
Model(&entities.User{}).
|
||||
Where("id = ?", id).
|
||||
Update("is_deleted", false).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 清除相关缓存
|
||||
r.invalidateUserCaches(ctx, id)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithTx 使用事务
|
||||
func (r *UserRepository) WithTx(tx interface{}) interfaces.Repository[*entities.User] {
|
||||
gormTx, ok := tx.(*gorm.DB)
|
||||
if !ok {
|
||||
return r
|
||||
}
|
||||
|
||||
return &UserRepository{
|
||||
db: gormTx,
|
||||
cache: r.cache,
|
||||
logger: r.logger,
|
||||
// deleteCacheByID 根据ID删除缓存
|
||||
func (r *UserRepository) deleteCacheByID(ctx context.Context, id string) {
|
||||
cacheKey := fmt.Sprintf("user:id:%s", id)
|
||||
if err := r.cache.Delete(ctx, cacheKey); err != nil {
|
||||
r.logger.Warn("删除用户ID缓存失败", zap.String("cache_key", cacheKey), zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
// InvalidateCache 清除缓存
|
||||
func (r *UserRepository) InvalidateCache(ctx context.Context, keys ...string) error {
|
||||
return r.cache.Delete(ctx, keys...)
|
||||
}
|
||||
|
||||
// WarmupCache 预热缓存
|
||||
func (r *UserRepository) WarmupCache(ctx context.Context) error {
|
||||
// 预热热门用户数据
|
||||
// 这里可以实现具体的预热逻辑
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCacheKey 获取缓存键
|
||||
func (r *UserRepository) GetCacheKey(id string) string {
|
||||
return fmt.Sprintf("user:%s", id)
|
||||
}
|
||||
|
||||
// FindByUsername 根据用户名查找用户
|
||||
func (r *UserRepository) FindByUsername(ctx context.Context, username string) (*entities.User, error) {
|
||||
var user entities.User
|
||||
|
||||
if err := r.db.WithContext(ctx).
|
||||
Where("username = ? AND is_deleted = false", username).
|
||||
First(&user).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, fmt.Errorf("user not found")
|
||||
}
|
||||
return nil, err
|
||||
// deleteCacheByPhone 根据手机号删除缓存
|
||||
func (r *UserRepository) deleteCacheByPhone(ctx context.Context, phone string) {
|
||||
cacheKey := fmt.Sprintf("user:phone:%s", phone)
|
||||
if err := r.cache.Delete(ctx, cacheKey); err != nil {
|
||||
r.logger.Warn("删除用户手机号缓存失败", zap.String("cache_key", cacheKey), zap.Error(err))
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// FindByEmail 根据邮箱查找用户
|
||||
func (r *UserRepository) FindByEmail(ctx context.Context, email string) (*entities.User, error) {
|
||||
var user entities.User
|
||||
|
||||
if err := r.db.WithContext(ctx).
|
||||
Where("email = ? AND is_deleted = false", email).
|
||||
First(&user).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, fmt.Errorf("user not found")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// invalidateUserCaches 清除用户相关缓存
|
||||
func (r *UserRepository) invalidateUserCaches(ctx context.Context, userID string) {
|
||||
keys := []string{
|
||||
r.GetCacheKey(userID),
|
||||
}
|
||||
|
||||
r.cache.Delete(ctx, keys...)
|
||||
r.cache.DeletePattern(ctx, "users:list:*")
|
||||
}
|
||||
|
||||
@@ -7,127 +7,23 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// UserRoutes 用户路由注册器
|
||||
type UserRoutes struct {
|
||||
handler *handlers.UserHandler
|
||||
jwtAuth *middleware.JWTAuthMiddleware
|
||||
optionalAuth *middleware.OptionalAuthMiddleware
|
||||
}
|
||||
|
||||
// NewUserRoutes 创建用户路由注册器
|
||||
func NewUserRoutes(
|
||||
handler *handlers.UserHandler,
|
||||
jwtAuth *middleware.JWTAuthMiddleware,
|
||||
optionalAuth *middleware.OptionalAuthMiddleware,
|
||||
) *UserRoutes {
|
||||
return &UserRoutes{
|
||||
handler: handler,
|
||||
jwtAuth: jwtAuth,
|
||||
optionalAuth: optionalAuth,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册用户路由
|
||||
func (r *UserRoutes) RegisterRoutes(router *gin.Engine) {
|
||||
// API版本组
|
||||
v1 := router.Group("/api/v1")
|
||||
|
||||
// 公开路由(不需要认证)
|
||||
public := v1.Group("/auth")
|
||||
// UserRoutes 注册用户相关路由
|
||||
func UserRoutes(router *gin.Engine, handler *handlers.UserHandler, authMiddleware *middleware.JWTAuthMiddleware) {
|
||||
// 用户域路由组
|
||||
usersGroup := router.Group("/api/v1/users")
|
||||
{
|
||||
public.POST("/login", r.handler.Login)
|
||||
public.POST("/register", r.handler.Create)
|
||||
}
|
||||
// 公开路由(不需要认证)
|
||||
usersGroup.POST("/send-code", handler.SendCode) // 发送验证码
|
||||
usersGroup.POST("/register", handler.Register) // 用户注册
|
||||
usersGroup.POST("/login-password", handler.LoginWithPassword) // 密码登录
|
||||
usersGroup.POST("/login-sms", handler.LoginWithSMS) // 短信验证码登录
|
||||
|
||||
// 需要认证的路由
|
||||
protected := v1.Group("/users")
|
||||
protected.Use(r.jwtAuth.Handle())
|
||||
{
|
||||
// 用户管理(管理员)
|
||||
protected.GET("", r.handler.List)
|
||||
protected.POST("", r.handler.Create)
|
||||
protected.GET("/:id", r.handler.GetByID)
|
||||
protected.PUT("/:id", r.handler.Update)
|
||||
protected.DELETE("/:id", r.handler.Delete)
|
||||
|
||||
// 用户搜索
|
||||
protected.GET("/search", r.handler.Search)
|
||||
|
||||
// 用户统计
|
||||
protected.GET("/stats", r.handler.GetStats)
|
||||
}
|
||||
|
||||
// 用户个人操作路由
|
||||
profile := v1.Group("/profile")
|
||||
profile.Use(r.jwtAuth.Handle())
|
||||
{
|
||||
profile.GET("", r.handler.GetProfile)
|
||||
profile.PUT("", r.handler.UpdateProfile)
|
||||
profile.POST("/change-password", r.handler.ChangePassword)
|
||||
profile.POST("/logout", r.handler.Logout)
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterPublicRoutes 注册公开路由
|
||||
func (r *UserRoutes) RegisterPublicRoutes(router *gin.Engine) {
|
||||
v1 := router.Group("/api/v1")
|
||||
|
||||
// 公开的用户相关路由
|
||||
public := v1.Group("/public")
|
||||
{
|
||||
// 可选认证的路由(用户可能登录也可能未登录)
|
||||
public.Use(r.optionalAuth.Handle())
|
||||
|
||||
// 这里可以添加一些公开的用户信息查询接口
|
||||
// 比如根据用户名查看公开信息(如果用户设置为公开)
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterAdminRoutes 注册管理员路由
|
||||
func (r *UserRoutes) RegisterAdminRoutes(router *gin.Engine) {
|
||||
admin := router.Group("/admin/v1")
|
||||
admin.Use(r.jwtAuth.Handle())
|
||||
// 这里可以添加管理员权限检查中间件
|
||||
|
||||
// 管理员用户管理
|
||||
users := admin.Group("/users")
|
||||
{
|
||||
users.GET("", r.handler.List)
|
||||
users.GET("/:id", r.handler.GetByID)
|
||||
users.PUT("/:id", r.handler.Update)
|
||||
users.DELETE("/:id", r.handler.Delete)
|
||||
users.GET("/stats", r.handler.GetStats)
|
||||
users.GET("/search", r.handler.Search)
|
||||
|
||||
// 批量操作
|
||||
users.POST("/batch-delete", r.handleBatchDelete)
|
||||
users.POST("/batch-update", r.handleBatchUpdate)
|
||||
}
|
||||
}
|
||||
|
||||
// 批量删除处理器
|
||||
func (r *UserRoutes) handleBatchDelete(c *gin.Context) {
|
||||
// 实现批量删除逻辑
|
||||
// 这里可以接收用户ID列表并调用服务进行批量删除
|
||||
c.JSON(200, gin.H{"message": "Batch delete not implemented yet"})
|
||||
}
|
||||
|
||||
// 批量更新处理器
|
||||
func (r *UserRoutes) handleBatchUpdate(c *gin.Context) {
|
||||
// 实现批量更新逻辑
|
||||
c.JSON(200, gin.H{"message": "Batch update not implemented yet"})
|
||||
}
|
||||
|
||||
// RegisterHealthRoutes 注册健康检查路由
|
||||
func (r *UserRoutes) RegisterHealthRoutes(router *gin.Engine) {
|
||||
health := router.Group("/health")
|
||||
{
|
||||
health.GET("/users", func(c *gin.Context) {
|
||||
// 用户服务健康检查
|
||||
c.JSON(200, gin.H{
|
||||
"service": "users",
|
||||
"status": "healthy",
|
||||
})
|
||||
})
|
||||
// 需要认证的路由
|
||||
authenticated := usersGroup.Group("")
|
||||
authenticated.Use(authMiddleware.Handle())
|
||||
{
|
||||
authenticated.GET("/me", handler.GetProfile) // 获取当前用户信息
|
||||
authenticated.PUT("/me/password", handler.ChangePassword) // 修改密码
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
187
internal/domains/user/services/sms_code_service.go
Normal file
187
internal/domains/user/services/sms_code_service.go
Normal file
@@ -0,0 +1,187 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/config"
|
||||
"tyapi-server/internal/domains/user/entities"
|
||||
"tyapi-server/internal/domains/user/repositories"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
"tyapi-server/internal/shared/sms"
|
||||
)
|
||||
|
||||
// SMSCodeService 短信验证码服务
|
||||
type SMSCodeService struct {
|
||||
repo *repositories.SMSCodeRepository
|
||||
smsClient sms.Service
|
||||
cache interfaces.CacheService
|
||||
config config.SMSConfig
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewSMSCodeService 创建短信验证码服务
|
||||
func NewSMSCodeService(
|
||||
repo *repositories.SMSCodeRepository,
|
||||
smsClient sms.Service,
|
||||
cache interfaces.CacheService,
|
||||
config config.SMSConfig,
|
||||
logger *zap.Logger,
|
||||
) *SMSCodeService {
|
||||
return &SMSCodeService{
|
||||
repo: repo,
|
||||
smsClient: smsClient,
|
||||
cache: cache,
|
||||
config: config,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// SendCode 发送验证码
|
||||
func (s *SMSCodeService) SendCode(ctx context.Context, phone string, scene entities.SMSScene, clientIP, userAgent string) error {
|
||||
// 检查频率限制
|
||||
if err := s.checkRateLimit(ctx, phone); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 生成验证码
|
||||
code := s.smsClient.GenerateCode(s.config.CodeLength)
|
||||
|
||||
// 创建SMS验证码记录
|
||||
smsCode := &entities.SMSCode{
|
||||
ID: uuid.New().String(),
|
||||
Phone: phone,
|
||||
Code: code,
|
||||
Scene: scene,
|
||||
IP: clientIP,
|
||||
UserAgent: userAgent,
|
||||
Used: false,
|
||||
ExpiresAt: time.Now().Add(s.config.ExpireTime),
|
||||
}
|
||||
|
||||
// 保存验证码
|
||||
if err := s.repo.Create(ctx, smsCode); err != nil {
|
||||
s.logger.Error("保存短信验证码失败",
|
||||
zap.String("phone", phone),
|
||||
zap.String("scene", string(scene)),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("保存验证码失败: %w", err)
|
||||
}
|
||||
|
||||
// 发送短信
|
||||
if err := s.smsClient.SendVerificationCode(ctx, phone, code); err != nil {
|
||||
// 记录发送失败但不删除验证码记录,让其自然过期
|
||||
s.logger.Error("发送短信验证码失败",
|
||||
zap.String("phone", phone),
|
||||
zap.String("code", code),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("短信发送失败: %w", err)
|
||||
}
|
||||
|
||||
// 更新发送记录缓存
|
||||
s.updateSendRecord(ctx, phone)
|
||||
|
||||
s.logger.Info("短信验证码发送成功",
|
||||
zap.String("phone", phone),
|
||||
zap.String("scene", string(scene)))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyCode 验证验证码
|
||||
func (s *SMSCodeService) VerifyCode(ctx context.Context, phone, code string, scene entities.SMSScene) error {
|
||||
// 根据手机号和场景获取有效的验证码记录
|
||||
smsCode, err := s.repo.GetValidCode(ctx, phone, scene)
|
||||
if err != nil {
|
||||
return fmt.Errorf("验证码无效或已过期")
|
||||
}
|
||||
|
||||
// 验证验证码是否匹配
|
||||
if smsCode.Code != code {
|
||||
return fmt.Errorf("验证码无效或已过期")
|
||||
}
|
||||
|
||||
// 标记验证码为已使用
|
||||
if err := s.repo.MarkAsUsed(ctx, smsCode.ID); err != nil {
|
||||
s.logger.Error("标记验证码为已使用失败",
|
||||
zap.String("code_id", smsCode.ID),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("验证码状态更新失败")
|
||||
}
|
||||
|
||||
s.logger.Info("短信验证码验证成功",
|
||||
zap.String("phone", phone),
|
||||
zap.String("scene", string(scene)))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkRateLimit 检查发送频率限制
|
||||
func (s *SMSCodeService) checkRateLimit(ctx context.Context, phone string) error {
|
||||
now := time.Now()
|
||||
|
||||
// 检查最小发送间隔
|
||||
lastSentKey := fmt.Sprintf("sms:last_sent:%s", phone)
|
||||
var lastSent time.Time
|
||||
if err := s.cache.Get(ctx, lastSentKey, &lastSent); err == nil {
|
||||
if now.Sub(lastSent) < s.config.RateLimit.MinInterval {
|
||||
return fmt.Errorf("请等待 %v 后再试", s.config.RateLimit.MinInterval)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查每小时发送限制
|
||||
hourlyKey := fmt.Sprintf("sms:hourly:%s:%s", phone, now.Format("2006010215"))
|
||||
var hourlyCount int
|
||||
if err := s.cache.Get(ctx, hourlyKey, &hourlyCount); err == nil {
|
||||
if hourlyCount >= s.config.RateLimit.HourlyLimit {
|
||||
return fmt.Errorf("每小时最多发送 %d 条短信", s.config.RateLimit.HourlyLimit)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查每日发送限制
|
||||
dailyKey := fmt.Sprintf("sms:daily:%s:%s", phone, now.Format("20060102"))
|
||||
var dailyCount int
|
||||
if err := s.cache.Get(ctx, dailyKey, &dailyCount); err == nil {
|
||||
if dailyCount >= s.config.RateLimit.DailyLimit {
|
||||
return fmt.Errorf("每日最多发送 %d 条短信", s.config.RateLimit.DailyLimit)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateSendRecord 更新发送记录
|
||||
func (s *SMSCodeService) updateSendRecord(ctx context.Context, phone string) {
|
||||
now := time.Now()
|
||||
|
||||
// 更新最后发送时间
|
||||
lastSentKey := fmt.Sprintf("sms:last_sent:%s", phone)
|
||||
s.cache.Set(ctx, lastSentKey, now, s.config.RateLimit.MinInterval)
|
||||
|
||||
// 更新每小时计数
|
||||
hourlyKey := fmt.Sprintf("sms:hourly:%s:%s", phone, now.Format("2006010215"))
|
||||
var hourlyCount int
|
||||
if err := s.cache.Get(ctx, hourlyKey, &hourlyCount); err == nil {
|
||||
s.cache.Set(ctx, hourlyKey, hourlyCount+1, time.Hour)
|
||||
} else {
|
||||
s.cache.Set(ctx, hourlyKey, 1, time.Hour)
|
||||
}
|
||||
|
||||
// 更新每日计数
|
||||
dailyKey := fmt.Sprintf("sms:daily:%s:%s", phone, now.Format("20060102"))
|
||||
var dailyCount int
|
||||
if err := s.cache.Get(ctx, dailyKey, &dailyCount); err == nil {
|
||||
s.cache.Set(ctx, dailyKey, dailyCount+1, 24*time.Hour)
|
||||
} else {
|
||||
s.cache.Set(ctx, dailyKey, 1, 24*time.Hour)
|
||||
}
|
||||
}
|
||||
|
||||
// CleanExpiredCodes 清理过期验证码
|
||||
func (s *SMSCodeService) CleanExpiredCodes(ctx context.Context) error {
|
||||
return s.repo.CleanupExpired(ctx)
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package services
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
"regexp"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
@@ -18,21 +18,24 @@ import (
|
||||
|
||||
// UserService 用户服务实现
|
||||
type UserService struct {
|
||||
repo *repositories.UserRepository
|
||||
eventBus interfaces.EventBus
|
||||
logger *zap.Logger
|
||||
repo *repositories.UserRepository
|
||||
smsCodeService *SMSCodeService
|
||||
eventBus interfaces.EventBus
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewUserService 创建用户服务
|
||||
func NewUserService(
|
||||
repo *repositories.UserRepository,
|
||||
smsCodeService *SMSCodeService,
|
||||
eventBus interfaces.EventBus,
|
||||
logger *zap.Logger,
|
||||
) *UserService {
|
||||
return &UserService{
|
||||
repo: repo,
|
||||
eventBus: eventBus,
|
||||
logger: logger,
|
||||
repo: repo,
|
||||
smsCodeService: smsCodeService,
|
||||
eventBus: eventBus,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,341 +46,209 @@ func (s *UserService) Name() string {
|
||||
|
||||
// Initialize 初始化服务
|
||||
func (s *UserService) Initialize(ctx context.Context) error {
|
||||
s.logger.Info("User service initialized")
|
||||
s.logger.Info("用户服务已初始化")
|
||||
return nil
|
||||
}
|
||||
|
||||
// HealthCheck 健康检查
|
||||
func (s *UserService) HealthCheck(ctx context.Context) error {
|
||||
// 简单检查:尝试查询用户数量
|
||||
_, err := s.repo.Count(ctx, interfaces.CountOptions{})
|
||||
return err
|
||||
// 简单的健康检查
|
||||
return nil
|
||||
}
|
||||
|
||||
// Shutdown 关闭服务
|
||||
func (s *UserService) Shutdown(ctx context.Context) error {
|
||||
s.logger.Info("User service shutdown")
|
||||
s.logger.Info("用户服务已关闭")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create 创建用户
|
||||
func (s *UserService) Create(ctx context.Context, createDTO interface{}) (*entities.User, error) {
|
||||
req, ok := createDTO.(*dto.CreateUserRequest)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid DTO type for user creation")
|
||||
// Register 用户注册
|
||||
func (s *UserService) Register(ctx context.Context, registerReq *dto.RegisterRequest) (*entities.User, error) {
|
||||
// 验证手机号格式
|
||||
if !s.isValidPhone(registerReq.Phone) {
|
||||
return nil, fmt.Errorf("手机号格式无效")
|
||||
}
|
||||
|
||||
// 验证业务规则
|
||||
if err := s.ValidateCreate(ctx, req); err != nil {
|
||||
return nil, err
|
||||
// 验证密码确认
|
||||
if registerReq.Password != registerReq.ConfirmPassword {
|
||||
return nil, fmt.Errorf("密码和确认密码不匹配")
|
||||
}
|
||||
|
||||
// 检查用户名和邮箱是否已存在
|
||||
if err := s.checkDuplicates(ctx, req.Username, req.Email); err != nil {
|
||||
// 验证短信验证码
|
||||
if err := s.smsCodeService.VerifyCode(ctx, registerReq.Phone, registerReq.Code, entities.SMSSceneRegister); err != nil {
|
||||
return nil, fmt.Errorf("验证码验证失败: %w", err)
|
||||
}
|
||||
|
||||
// 检查手机号是否已存在
|
||||
if err := s.checkPhoneDuplicate(ctx, registerReq.Phone); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 创建用户实体
|
||||
user := req.ToEntity()
|
||||
user := registerReq.ToEntity()
|
||||
user.ID = uuid.New().String()
|
||||
|
||||
// 加密密码
|
||||
hashedPassword, err := s.hashPassword(req.Password)
|
||||
// 哈希密码
|
||||
hashedPassword, err := s.hashPassword(registerReq.Password)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to hash password: %w", err)
|
||||
return nil, fmt.Errorf("密码加密失败: %w", err)
|
||||
}
|
||||
user.Password = hashedPassword
|
||||
|
||||
// 保存用户
|
||||
if err := s.repo.Create(ctx, user); err != nil {
|
||||
s.logger.Error("Failed to create user", zap.Error(err))
|
||||
return nil, fmt.Errorf("failed to create user: %w", err)
|
||||
s.logger.Error("创建用户失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("创建用户失败: %w", err)
|
||||
}
|
||||
|
||||
// 发布用户创建事件
|
||||
event := events.NewUserCreatedEvent(user, s.getCorrelationID(ctx))
|
||||
// 发布用户注册事件
|
||||
event := events.NewUserRegisteredEvent(user, s.getCorrelationID(ctx))
|
||||
if err := s.eventBus.Publish(ctx, event); err != nil {
|
||||
s.logger.Warn("Failed to publish user created event", zap.Error(err))
|
||||
s.logger.Warn("发布用户注册事件失败", zap.Error(err))
|
||||
}
|
||||
|
||||
s.logger.Info("User created successfully",
|
||||
s.logger.Info("用户注册成功",
|
||||
zap.String("user_id", user.ID),
|
||||
zap.String("username", user.Username))
|
||||
zap.String("phone", user.Phone))
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// GetByID 根据ID获取用户
|
||||
func (s *UserService) GetByID(ctx context.Context, id string) (*entities.User, error) {
|
||||
if id == "" {
|
||||
return nil, fmt.Errorf("user ID is required")
|
||||
}
|
||||
|
||||
user, err := s.repo.GetByID(ctx, id)
|
||||
// LoginWithPassword 密码登录
|
||||
func (s *UserService) LoginWithPassword(ctx context.Context, loginReq *dto.LoginWithPasswordRequest) (*entities.User, error) {
|
||||
// 根据手机号查找用户
|
||||
user, err := s.repo.FindByPhone(ctx, loginReq.Phone)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("user not found: %w", err)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// Update 更新用户
|
||||
func (s *UserService) Update(ctx context.Context, id string, updateDTO interface{}) (*entities.User, error) {
|
||||
req, ok := updateDTO.(*dto.UpdateUserRequest)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid DTO type for user update")
|
||||
}
|
||||
|
||||
// 验证业务规则
|
||||
if err := s.ValidateUpdate(ctx, id, req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取现有用户
|
||||
user, err := s.repo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("user not found: %w", err)
|
||||
}
|
||||
|
||||
// 记录变更前的值
|
||||
oldValues := s.captureUserValues(user)
|
||||
|
||||
// 应用更新
|
||||
s.applyUserUpdates(user, req)
|
||||
|
||||
// 保存更新
|
||||
if err := s.repo.Update(ctx, user); err != nil {
|
||||
s.logger.Error("Failed to update user", zap.Error(err))
|
||||
return nil, fmt.Errorf("failed to update user: %w", err)
|
||||
}
|
||||
|
||||
// 发布用户更新事件
|
||||
newValues := s.captureUserValues(user)
|
||||
changes := s.findChanges(oldValues, newValues)
|
||||
if len(changes) > 0 {
|
||||
event := events.NewUserUpdatedEvent(user.ID, changes, oldValues, newValues, s.getCorrelationID(ctx))
|
||||
if err := s.eventBus.Publish(ctx, event); err != nil {
|
||||
s.logger.Warn("Failed to publish user updated event", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Info("User updated successfully",
|
||||
zap.String("user_id", user.ID),
|
||||
zap.Int("changes", len(changes)))
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// Delete 删除用户
|
||||
func (s *UserService) Delete(ctx context.Context, id string) error {
|
||||
if id == "" {
|
||||
return fmt.Errorf("user ID is required")
|
||||
}
|
||||
|
||||
// 获取用户信息用于事件
|
||||
user, err := s.repo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("user not found: %w", err)
|
||||
}
|
||||
|
||||
// 软删除用户
|
||||
if err := s.repo.SoftDelete(ctx, id); err != nil {
|
||||
s.logger.Error("Failed to delete user", zap.Error(err))
|
||||
return fmt.Errorf("failed to delete user: %w", err)
|
||||
}
|
||||
|
||||
// 发布用户删除事件
|
||||
event := events.NewUserDeletedEvent(user.ID, user.Username, user.Email, true, s.getCorrelationID(ctx))
|
||||
if err := s.eventBus.Publish(ctx, event); err != nil {
|
||||
s.logger.Warn("Failed to publish user deleted event", zap.Error(err))
|
||||
}
|
||||
|
||||
s.logger.Info("User deleted successfully", zap.String("user_id", id))
|
||||
return nil
|
||||
}
|
||||
|
||||
// List 获取用户列表
|
||||
func (s *UserService) List(ctx context.Context, options interfaces.ListOptions) ([]*entities.User, error) {
|
||||
return s.repo.List(ctx, options)
|
||||
}
|
||||
|
||||
// Search 搜索用户
|
||||
func (s *UserService) Search(ctx context.Context, query string, options interfaces.ListOptions) ([]*entities.User, error) {
|
||||
// 设置搜索关键字
|
||||
searchOptions := options
|
||||
searchOptions.Search = query
|
||||
|
||||
return s.repo.List(ctx, searchOptions)
|
||||
}
|
||||
|
||||
// Count 统计用户数量
|
||||
func (s *UserService) Count(ctx context.Context, options interfaces.CountOptions) (int64, error) {
|
||||
return s.repo.Count(ctx, options)
|
||||
}
|
||||
|
||||
// Validate 验证用户实体
|
||||
func (s *UserService) Validate(ctx context.Context, entity *entities.User) error {
|
||||
return entity.Validate()
|
||||
}
|
||||
|
||||
// ValidateCreate 验证创建请求
|
||||
func (s *UserService) ValidateCreate(ctx context.Context, createDTO interface{}) error {
|
||||
req, ok := createDTO.(*dto.CreateUserRequest)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid DTO type")
|
||||
}
|
||||
|
||||
// 基础验证已经由binding标签处理,这里添加业务规则验证
|
||||
if req.Username == "admin" || req.Username == "root" {
|
||||
return fmt.Errorf("username '%s' is reserved", req.Username)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateUpdate 验证更新请求
|
||||
func (s *UserService) ValidateUpdate(ctx context.Context, id string, updateDTO interface{}) error {
|
||||
_, ok := updateDTO.(*dto.UpdateUserRequest)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid DTO type")
|
||||
}
|
||||
|
||||
if id == "" {
|
||||
return fmt.Errorf("user ID is required")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 业务方法
|
||||
|
||||
// Login 用户登录
|
||||
func (s *UserService) Login(ctx context.Context, loginReq *dto.LoginRequest) (*entities.User, error) {
|
||||
// 根据用户名或邮箱查找用户
|
||||
var user *entities.User
|
||||
var err error
|
||||
|
||||
if s.isEmail(loginReq.Login) {
|
||||
user, err = s.repo.FindByEmail(ctx, loginReq.Login)
|
||||
} else {
|
||||
user, err = s.repo.FindByUsername(ctx, loginReq.Login)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid credentials")
|
||||
return nil, fmt.Errorf("用户名或密码错误")
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
if !s.checkPassword(loginReq.Password, user.Password) {
|
||||
return nil, fmt.Errorf("invalid credentials")
|
||||
return nil, fmt.Errorf("用户名或密码错误")
|
||||
}
|
||||
|
||||
// 检查用户状态
|
||||
if !user.CanLogin() {
|
||||
return nil, fmt.Errorf("account is disabled or suspended")
|
||||
}
|
||||
|
||||
// 更新最后登录时间
|
||||
user.UpdateLastLogin()
|
||||
if err := s.repo.Update(ctx, user); err != nil {
|
||||
s.logger.Warn("Failed to update last login time", zap.Error(err))
|
||||
}
|
||||
|
||||
// 发布登录事件
|
||||
// 发布用户登录事件
|
||||
event := events.NewUserLoggedInEvent(
|
||||
user.ID, user.Username,
|
||||
user.ID, user.Phone,
|
||||
s.getClientIP(ctx), s.getUserAgent(ctx),
|
||||
s.getCorrelationID(ctx))
|
||||
if err := s.eventBus.Publish(ctx, event); err != nil {
|
||||
s.logger.Warn("Failed to publish user logged in event", zap.Error(err))
|
||||
s.logger.Warn("发布用户登录事件失败", zap.Error(err))
|
||||
}
|
||||
|
||||
s.logger.Info("User logged in successfully",
|
||||
s.logger.Info("用户密码登录成功",
|
||||
zap.String("user_id", user.ID),
|
||||
zap.String("username", user.Username))
|
||||
zap.String("phone", user.Phone))
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// LoginWithSMS 短信验证码登录
|
||||
func (s *UserService) LoginWithSMS(ctx context.Context, loginReq *dto.LoginWithSMSRequest) (*entities.User, error) {
|
||||
// 验证短信验证码
|
||||
if err := s.smsCodeService.VerifyCode(ctx, loginReq.Phone, loginReq.Code, entities.SMSSceneLogin); err != nil {
|
||||
return nil, fmt.Errorf("验证码验证失败: %w", err)
|
||||
}
|
||||
|
||||
// 根据手机号查找用户
|
||||
user, err := s.repo.FindByPhone(ctx, loginReq.Phone)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("用户不存在")
|
||||
}
|
||||
|
||||
// 发布用户登录事件
|
||||
event := events.NewUserLoggedInEvent(
|
||||
user.ID, user.Phone,
|
||||
s.getClientIP(ctx), s.getUserAgent(ctx),
|
||||
s.getCorrelationID(ctx))
|
||||
if err := s.eventBus.Publish(ctx, event); err != nil {
|
||||
s.logger.Warn("发布用户登录事件失败", zap.Error(err))
|
||||
}
|
||||
|
||||
s.logger.Info("用户短信登录成功",
|
||||
zap.String("user_id", user.ID),
|
||||
zap.String("phone", user.Phone))
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// ChangePassword 修改密码
|
||||
func (s *UserService) ChangePassword(ctx context.Context, userID string, req *dto.ChangePasswordRequest) error {
|
||||
// 获取用户
|
||||
// 验证新密码确认
|
||||
if req.NewPassword != req.ConfirmNewPassword {
|
||||
return fmt.Errorf("新密码和确认新密码不匹配")
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
user, err := s.repo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("user not found: %w", err)
|
||||
return fmt.Errorf("用户不存在: %w", err)
|
||||
}
|
||||
|
||||
// 验证旧密码
|
||||
// 验证短信验证码
|
||||
if err := s.smsCodeService.VerifyCode(ctx, user.Phone, req.Code, entities.SMSSceneChangePassword); err != nil {
|
||||
return fmt.Errorf("验证码验证失败: %w", err)
|
||||
}
|
||||
|
||||
// 验证当前密码
|
||||
if !s.checkPassword(req.OldPassword, user.Password) {
|
||||
return fmt.Errorf("current password is incorrect")
|
||||
return fmt.Errorf("当前密码错误")
|
||||
}
|
||||
|
||||
// 加密新密码
|
||||
// 哈希新密码
|
||||
hashedPassword, err := s.hashPassword(req.NewPassword)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to hash new password: %w", err)
|
||||
return fmt.Errorf("新密码加密失败: %w", err)
|
||||
}
|
||||
|
||||
// 更新密码
|
||||
user.Password = hashedPassword
|
||||
if err := s.repo.Update(ctx, user); err != nil {
|
||||
return fmt.Errorf("failed to update password: %w", err)
|
||||
return fmt.Errorf("密码更新失败: %w", err)
|
||||
}
|
||||
|
||||
// 发布密码修改事件
|
||||
event := events.NewUserPasswordChangedEvent(user.ID, user.Username, s.getCorrelationID(ctx))
|
||||
event := events.NewUserPasswordChangedEvent(user.ID, user.Phone, s.getCorrelationID(ctx))
|
||||
if err := s.eventBus.Publish(ctx, event); err != nil {
|
||||
s.logger.Warn("Failed to publish password changed event", zap.Error(err))
|
||||
s.logger.Warn("发布密码修改事件失败", zap.Error(err))
|
||||
}
|
||||
|
||||
s.logger.Info("Password changed successfully", zap.String("user_id", userID))
|
||||
s.logger.Info("密码修改成功", zap.String("user_id", userID))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetStats 获取用户统计
|
||||
func (s *UserService) GetStats(ctx context.Context) (*dto.UserStatsResponse, error) {
|
||||
total, err := s.repo.Count(ctx, interfaces.CountOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// GetByID 根据ID获取用户
|
||||
func (s *UserService) GetByID(ctx context.Context, id string) (*entities.User, error) {
|
||||
if id == "" {
|
||||
return nil, fmt.Errorf("用户ID不能为空")
|
||||
}
|
||||
|
||||
// 这里可以并行查询不同状态的用户数量
|
||||
// 简化实现,返回基础统计
|
||||
return &dto.UserStatsResponse{
|
||||
TotalUsers: total,
|
||||
ActiveUsers: total, // 简化
|
||||
InactiveUsers: 0,
|
||||
SuspendedUsers: 0,
|
||||
NewUsersToday: 0,
|
||||
NewUsersWeek: 0,
|
||||
NewUsersMonth: 0,
|
||||
}, nil
|
||||
user, err := s.repo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("用户不存在: %w", err)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// 私有方法
|
||||
// 工具方法
|
||||
|
||||
// checkDuplicates 检查重复的用户名和邮箱
|
||||
func (s *UserService) checkDuplicates(ctx context.Context, username, email string) error {
|
||||
// 检查用户名
|
||||
if existingUser, err := s.repo.FindByUsername(ctx, username); err == nil && existingUser != nil {
|
||||
return fmt.Errorf("username already exists")
|
||||
// checkPhoneDuplicate 检查手机号重复
|
||||
func (s *UserService) checkPhoneDuplicate(ctx context.Context, phone string) error {
|
||||
if _, err := s.repo.FindByPhone(ctx, phone); err == nil {
|
||||
return fmt.Errorf("手机号已存在")
|
||||
}
|
||||
|
||||
// 检查邮箱
|
||||
if existingUser, err := s.repo.FindByEmail(ctx, email); err == nil && existingUser != nil {
|
||||
return fmt.Errorf("email already exists")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// hashPassword 加密密码
|
||||
func (s *UserService) hashPassword(password string) (string, error) {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
hashedBytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(hash), nil
|
||||
return string(hashedBytes), nil
|
||||
}
|
||||
|
||||
// checkPassword 验证密码
|
||||
@@ -386,63 +257,24 @@ func (s *UserService) checkPassword(password, hash string) bool {
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// isEmail 检查是否为邮箱格式
|
||||
func (s *UserService) isEmail(str string) bool {
|
||||
return len(str) > 0 && len(str) < 255 &&
|
||||
len(str) > 5 &&
|
||||
str[len(str)-4:] != ".." &&
|
||||
(len(str) > 6 && str[len(str)-4:] == ".com") ||
|
||||
(len(str) > 5 && str[len(str)-3:] == ".cn") ||
|
||||
(len(str) > 6 && str[len(str)-4:] == ".org") ||
|
||||
(len(str) > 6 && str[len(str)-4:] == ".net")
|
||||
// 简化的邮箱检查,实际应该使用正则表达式
|
||||
// isValidPhone 验证手机号格式
|
||||
func (s *UserService) isValidPhone(phone string) bool {
|
||||
// 简单的中国手机号验证(11位数字,以1开头)
|
||||
pattern := `^1[3-9]\d{9}$`
|
||||
matched, _ := regexp.MatchString(pattern, phone)
|
||||
return matched
|
||||
}
|
||||
|
||||
// applyUserUpdates 应用用户更新
|
||||
func (s *UserService) applyUserUpdates(user *entities.User, req *dto.UpdateUserRequest) {
|
||||
if req.FirstName != nil {
|
||||
user.FirstName = *req.FirstName
|
||||
}
|
||||
if req.LastName != nil {
|
||||
user.LastName = *req.LastName
|
||||
}
|
||||
if req.Phone != nil {
|
||||
user.Phone = *req.Phone
|
||||
}
|
||||
if req.Avatar != nil {
|
||||
user.Avatar = *req.Avatar
|
||||
}
|
||||
user.UpdatedAt = time.Now()
|
||||
}
|
||||
|
||||
// captureUserValues 捕获用户值用于变更比较
|
||||
func (s *UserService) captureUserValues(user *entities.User) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"first_name": user.FirstName,
|
||||
"last_name": user.LastName,
|
||||
"phone": user.Phone,
|
||||
"avatar": user.Avatar,
|
||||
}
|
||||
}
|
||||
|
||||
// findChanges 找出变更的字段
|
||||
func (s *UserService) findChanges(oldValues, newValues map[string]interface{}) map[string]interface{} {
|
||||
changes := make(map[string]interface{})
|
||||
|
||||
for key, newValue := range newValues {
|
||||
if oldValue, exists := oldValues[key]; !exists || oldValue != newValue {
|
||||
changes[key] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
return changes
|
||||
// generateUserID 生成用户ID
|
||||
func (s *UserService) generateUserID() string {
|
||||
return uuid.New().String()
|
||||
}
|
||||
|
||||
// getCorrelationID 获取关联ID
|
||||
func (s *UserService) getCorrelationID(ctx context.Context) string {
|
||||
if id := ctx.Value("correlation_id"); id != nil {
|
||||
if correlationID, ok := id.(string); ok {
|
||||
return correlationID
|
||||
if strID, ok := id.(string); ok {
|
||||
return strID
|
||||
}
|
||||
}
|
||||
return uuid.New().String()
|
||||
@@ -451,19 +283,19 @@ func (s *UserService) getCorrelationID(ctx context.Context) string {
|
||||
// getClientIP 获取客户端IP
|
||||
func (s *UserService) getClientIP(ctx context.Context) string {
|
||||
if ip := ctx.Value("client_ip"); ip != nil {
|
||||
if clientIP, ok := ip.(string); ok {
|
||||
return clientIP
|
||||
if strIP, ok := ip.(string); ok {
|
||||
return strIP
|
||||
}
|
||||
}
|
||||
return "unknown"
|
||||
return ""
|
||||
}
|
||||
|
||||
// getUserAgent 获取用户代理
|
||||
func (s *UserService) getUserAgent(ctx context.Context) string {
|
||||
if ua := ctx.Value("user_agent"); ua != nil {
|
||||
if userAgent, ok := ua.(string); ok {
|
||||
return userAgent
|
||||
if strUA, ok := ua.(string); ok {
|
||||
return strUA
|
||||
}
|
||||
}
|
||||
return "unknown"
|
||||
return ""
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user