feat: add SMS verification registration with UniSMS provider
- Add phone field to user model with index and helper methods - Implement SMS provider interface with UniSMS (合一短信) implementation - Add SMS verification code sending endpoint with rate limiting (1/60s) - Support SMS registration in Register() (mutually exclusive with email) - Add SMS configuration to admin settings (provider, keys, signature, template) - Display phone number in admin user list contact column - Add i18n translations for all SMS-related messages (zh-CN, en, zh-TW) - Add Claude Code skills: sync-upstream, migrate-server - Update CLAUDE.md with git conventions and deployment guide Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,8 @@ type User struct {
|
||||
Role int `json:"role" gorm:"type:int;default:1"` // admin, common
|
||||
Status int `json:"status" gorm:"type:int;default:1"` // enabled, disabled
|
||||
Email string `json:"email" gorm:"index" validate:"max=50"`
|
||||
Phone string `json:"phone" gorm:"index" validate:"max=20"`
|
||||
SmsVerificationCode string `json:"sms_verification_code" gorm:"-:all"`
|
||||
GitHubId string `json:"github_id" gorm:"column:github_id;index"`
|
||||
DiscordId string `json:"discord_id" gorm:"column:discord_id;index"`
|
||||
OidcId string `json:"oidc_id" gorm:"column:oidc_id;index"`
|
||||
@@ -159,26 +161,28 @@ func generateDefaultSidebarConfigForRole(userRole int) string {
|
||||
}
|
||||
|
||||
// CheckUserExistOrDeleted check if user exist or deleted, if not exist, return false, nil, if deleted or exist, return true, nil
|
||||
func CheckUserExistOrDeleted(username string, email string) (bool, error) {
|
||||
func CheckUserExistOrDeleted(username string, email string, phone string) (bool, error) {
|
||||
var user User
|
||||
|
||||
// err := DB.Unscoped().First(&user, "username = ? or email = ?", username, email).Error
|
||||
// check email if empty
|
||||
var err error
|
||||
if email == "" {
|
||||
err = DB.Unscoped().First(&user, "username = ?", username).Error
|
||||
} else {
|
||||
err = DB.Unscoped().First(&user, "username = ? or email = ?", username, email).Error
|
||||
conditions := []string{"username = ?"}
|
||||
args := []interface{}{username}
|
||||
if email != "" {
|
||||
conditions = append(conditions, "email = ?")
|
||||
args = append(args, email)
|
||||
}
|
||||
if phone != "" {
|
||||
conditions = append(conditions, "phone = ?")
|
||||
args = append(args, phone)
|
||||
}
|
||||
query := strings.Join(conditions, " or ")
|
||||
err = DB.Unscoped().Where(query, args...).First(&user).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// not exist, return false, nil
|
||||
return false, nil
|
||||
}
|
||||
// other error, return false, err
|
||||
return false, err
|
||||
}
|
||||
// exist, return true, nil
|
||||
return true, nil
|
||||
}
|
||||
|
||||
@@ -242,7 +246,7 @@ func SearchUsers(keyword string, group string, startIdx int, num int) ([]*User,
|
||||
query := tx.Unscoped().Model(&User{})
|
||||
|
||||
// 构建搜索条件
|
||||
likeCondition := "username LIKE ? OR email LIKE ? OR display_name LIKE ?"
|
||||
likeCondition := "username LIKE ? OR email LIKE ? OR display_name LIKE ? OR phone LIKE ?"
|
||||
|
||||
// 尝试将关键字转换为整数ID
|
||||
keywordInt, err := strconv.Atoi(keyword)
|
||||
@@ -251,19 +255,19 @@ func SearchUsers(keyword string, group string, startIdx int, num int) ([]*User,
|
||||
likeCondition = "id = ? OR " + likeCondition
|
||||
if group != "" {
|
||||
query = query.Where("("+likeCondition+") AND "+commonGroupCol+" = ?",
|
||||
keywordInt, "%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%", group)
|
||||
keywordInt, "%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%", group)
|
||||
} else {
|
||||
query = query.Where(likeCondition,
|
||||
keywordInt, "%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%")
|
||||
keywordInt, "%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%")
|
||||
}
|
||||
} else {
|
||||
// 非数字关键字,只搜索字符串字段
|
||||
if group != "" {
|
||||
query = query.Where("("+likeCondition+") AND "+commonGroupCol+" = ?",
|
||||
"%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%", group)
|
||||
"%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%", group)
|
||||
} else {
|
||||
query = query.Where(likeCondition,
|
||||
"%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%")
|
||||
"%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -546,6 +550,7 @@ func (user *User) ClearBinding(bindingType string) error {
|
||||
|
||||
bindingColumnMap := map[string]string{
|
||||
"email": "email",
|
||||
"phone": "phone",
|
||||
"github": "github_id",
|
||||
"discord": "discord_id",
|
||||
"oidc": "oidc_id",
|
||||
@@ -680,6 +685,18 @@ func IsEmailAlreadyTaken(email string) bool {
|
||||
return DB.Unscoped().Where("email = ?", email).Find(&User{}).RowsAffected == 1
|
||||
}
|
||||
|
||||
func IsPhoneAlreadyTaken(phone string) bool {
|
||||
return DB.Unscoped().Where("phone = ?", phone).Find(&User{}).RowsAffected == 1
|
||||
}
|
||||
|
||||
func (user *User) FillUserByPhone() error {
|
||||
if user.Phone == "" {
|
||||
return errors.New("phone 为空!")
|
||||
}
|
||||
DB.Where(User{Phone: user.Phone}).First(user)
|
||||
return nil
|
||||
}
|
||||
|
||||
func IsWeChatIdAlreadyTaken(wechatId string) bool {
|
||||
return DB.Unscoped().Where("wechat_id = ?", wechatId).Find(&User{}).RowsAffected == 1
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user