Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions api-service/internal/dto/request/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ type UserCreateRequest struct {
Avatar *string `json:"avatar,omitempty" binding:"omitempty,url" example:"https://example.com/avatar.jpg"` // Avatar URL (optional)
Gender *int `json:"gender,omitempty" binding:"omitempty,min=0,max=2" example:"1"` // Gender: 1-male, 2-female, 0-unknown (optional)
Signature *string `json:"signature,omitempty" binding:"omitempty,max=255" example:"This is my signature"` // Personal signature (optional)
Status *int `json:"status,omitempty" binding:"omitempty,min=0,max=1" example:"1"` // Status: 0-disabled, 1-enabled (optional)
Status *int `json:"status,omitempty" validate:"oneof=0 1" example:"1"` // Status: 0-disabled, 1-enabled (optional)
Timezone *string `json:"timezone,omitempty" binding:"omitempty,max=64" example:"Asia/Shanghai"` // Timezone, default UTC (optional)
Language *string `json:"language,omitempty" binding:"omitempty,max=10" example:"zh-CN"` // Language, default zh-CN (optional)
RoleIDs []uint `json:"role_ids,omitempty" binding:"omitempty,dive,gt=0" example:"1,2"` // Array of role IDs to assign (optional)
Expand All @@ -63,7 +63,7 @@ type UserUpdateRequest struct {

// UserUpdateStatusRequest
type UserUpdateStatusRequest struct {
Status int `json:"status" binding:"min=0,max=1" example:"1"`
Status int `json:"status" validate:"oneof=0 1" example:"1"`
}

// UserPasswordUpdateRequest
Expand Down
1 change: 1 addition & 0 deletions api-service/internal/interface/repository/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type UserRepository interface {
GetByEmail(ctx context.Context, email string) (*model.User, error)
GetByUsernameOrEmail(ctx context.Context, usernameOrEmail string) (*model.User, error)
Update(ctx context.Context, user *model.User) error
UpdateStatus(ctx context.Context, id uint, status int) error
Delete(ctx context.Context, id uint) error

// CreateUserRole creates a user-role association record.
Expand Down
2 changes: 1 addition & 1 deletion api-service/internal/model/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ type User struct {
Phone string `json:"phone" gorm:"size:20;comment:Phone number"`
Gender int `json:"gender" gorm:"type:tinyint(1);default:0;comment:Gender: 0-unknown, 1-male, 2-female"` // 0:unknown, 1:male, 2:female
Signature string `json:"signature" gorm:"size:255;comment:Personal signature"`
Status int `json:"status" gorm:"type:tinyint(1);default:1;index:idx_user_status;comment:Status: 0-disabled, 1-enabled"` // 1:active, 0:inactive
Status int `json:"status" gorm:"type:tinyint(1);default:1;index:idx_user_status;comment:Status: -1-deleted, 0-disabled, 1-enabled"` // -1:deleted, 0:disabled, 1:enabled
LastLoginAt *time.Time `json:"last_login_at" gorm:"type:datetime;serializer:datetime;comment:Last login time"`
LastLoginIP string `json:"last_login_ip" gorm:"size:45;comment:Last login IP"`
Timezone string `json:"timezone" gorm:"size:64;default:UTC;comment:Timezone"`
Expand Down
8 changes: 8 additions & 0 deletions api-service/internal/repository/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,14 @@ func (r *userRepository) Update(ctx context.Context, user *model.User) error {
return nil
}

// UpdateStatus updates a user's status explicitly, including zero values
func (r *userRepository) UpdateStatus(ctx context.Context, id uint, status int) error {
if err := r.db.WithContext(ctx).Model(&model.User{}).Where("id = ?", id).Update("status", status).Error; err != nil {
return errors.NewAppErrorWrapError(err, errors.CodeRecordUpdateFailed)
}
return nil
}

// Delete deletes a user (soft delete)
func (r *userRepository) Delete(ctx context.Context, id uint) error {
// soft delete: set status = -1 and update updated_at
Expand Down
20 changes: 13 additions & 7 deletions api-service/internal/service/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,13 +247,14 @@ func (s *userService) syncUserRoles(ctx context.Context, currentUserID, userID u
func (s *userService) UpdateUserStatus(ctx context.Context, userID uint, req *request.UserUpdateStatusRequest) error {
s.logger.InfoContext(ctx, "Updating user status", logger.Uint("user_id", userID), logger.Int("status", req.Status))

user, err := s.userRepo.GetByID(ctx, userID)
// Check if user exists
_, err := s.userRepo.GetByID(ctx, userID)
if err != nil {
return err
}

user.Status = req.Status
if err := s.userRepo.Update(ctx, user); err != nil {
// Use UpdateStatus to explicitly update status field, including zero values
if err := s.userRepo.UpdateStatus(ctx, userID, req.Status); err != nil {
s.logger.ErrorContext(ctx, "Failed to update user status", logger.ErrorField(err))
return err
}
Expand Down Expand Up @@ -343,15 +344,20 @@ func (s *userService) validateEmailUniqueness(ctx context.Context, email string,
// validateUserCreation validates user creation

// validatePhoneFormat validates phone number format (must start with + and contain only digits)

func validatePhoneFormat(phone string) bool {
if phone == "" {
return true // Phone is optional
}
if !strings.HasPrefix(phone, "+") {
return false

// Phone can optionally start with +
startIndex := 0
if strings.HasPrefix(phone, "+") {
startIndex = 1
}
// Check remaining characters are digits
for _, ch := range phone[1:] {

// Check remaining characters are all digits
for _, ch := range phone[startIndex:] {
if ch < '0' || ch > '9' {
return false
}
Expand Down
8 changes: 6 additions & 2 deletions api-service/internal/service/user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,15 @@ func (m *MockUserRepository) Update(ctx context.Context, user *model.User) error
return args.Error(0)
}

func (m *MockUserRepository) UpdateStatus(ctx context.Context, id uint, status int) error {
args := m.Called(ctx, id, status)
return args.Error(0)
}

func (m *MockUserRepository) Delete(ctx context.Context, id uint) error {
args := m.Called(ctx, id)
return args.Error(0)
}

func (m *MockUserRepository) List(ctx context.Context, offset, limit int, filters map[string]interface{}) ([]*model.User, int64, error) {
args := m.Called(ctx, offset, limit, filters)
return args.Get(0).([]*model.User), args.Get(1).(int64), args.Error(2)
Expand Down Expand Up @@ -536,7 +540,7 @@ func TestUserService_UpdateUserStatus_Success(t *testing.T) {
}

mockRepo.On("GetByID", ctx, userID).Return(testUser, nil)
mockRepo.On("Update", ctx, mock.AnythingOfType("*model.User")).Return(nil)
mockRepo.On("UpdateStatus", ctx, userID, UserStatusInactive).Return(nil)

err := service.UpdateUserStatus(ctx, userID, req)

Expand Down
Loading