v3
veypi 2 months ago
parent 69b4b7c504
commit 95eccc2cde

@ -1,16 +1,3 @@
# OneAuth
统一验证及应用管理服务
[Demo](https://oa.veypi.com)
## 使用方法
## 引入
/init.go
```go
```
# VBase
对标 Firebase 的后端服务,基于 vyes/vigo 框架实现,提供用户认证、数据库存储、文件存储等功能。

@ -9,6 +9,7 @@ package api
import (
"github.com/veypi/OneAuth/api/app"
"github.com/veypi/OneAuth/api/sms"
"github.com/veypi/OneAuth/api/token"
"github.com/veypi/OneAuth/api/user"
"github.com/veypi/OneAuth/libs/auth"
@ -22,8 +23,9 @@ var (
_ = Router.Extend("user", user.Router)
_ = Router.Extend("token", token.Router)
_ = Router.Extend("app", app.Router)
_ = Router.Extend("sms", sms.Router)
)
var _ = Router.Any("*", func(x *vigo.X) error {
var _ = Router.Any("*", vigo.SkipBefore, func(x *vigo.X) error {
return vigo.ErrNotFound
})

@ -0,0 +1,14 @@
//
// init.go
// Copyright (C) 2025 veypi <i@veypi.com>
//
// Distributed under terms of the MIT license.
//
package sms
import "github.com/vyes-ai/vigo"
var Router = vigo.NewRouter()

@ -0,0 +1,111 @@
package sms
import (
"fmt"
"time"
"github.com/veypi/OneAuth/cfg"
"github.com/veypi/OneAuth/libs/sms_providers"
"github.com/veypi/OneAuth/libs/utils"
"github.com/veypi/OneAuth/models"
"github.com/vyes-ai/vigo"
"github.com/vyes-ai/vigo/contrib/limiter"
"gorm.io/gorm"
)
// SendCodeRequest 发送验证码请求
type SendCodeRequest struct {
Phone string `json:"phone"`
Region string `json:"region"`
Purpose string `json:"purpose"`
TemplateID string `json:"template_id"`
}
// SendCodeResponse 发送验证码响应
type SendCodeResponse struct {
ID uint `json:"id"`
Phone string `json:"phone"`
ExpiresAt time.Time `json:"expires_at"`
Interval int `json:"interval"` // 下次可发送间隔(秒)
}
var _ = Router.Post("/", vigo.SkipBefore, limiter.NewAdvancedRequestLimiter(time.Minute*3, 5, time.Second*30), sendCode)
// SendCode 发送验证码
func sendCode(x *vigo.X) (any, error) {
req := &SendCodeRequest{}
err := x.Parse(req)
if err != nil {
return nil, err
}
// 1. 验证手机号
normalizedPhone := utils.NormalizePhoneNumber(req.Phone)
if !utils.ValidatePhoneNumber(normalizedPhone) {
return nil, fmt.Errorf("invalid phone number: %s", req.Phone)
}
// 2. 检查区域配置
provider, err := sms_providers.GetSMSProvider(req.Region)
if err != nil {
return nil, err
}
// 3. 检查发送频率限制
if err := CheckSendInterval(normalizedPhone, req.Region, req.Purpose); err != nil {
return nil, err
}
// 4. 检查每日发送次数限制
if err := CheckDailyLimit(normalizedPhone, req.Region); err != nil {
return nil, err
}
// 5. 生成验证码
code, err := utils.GenerateCode(cfg.Config.SMS.Global.CodeLength)
if err != nil {
return nil, fmt.Errorf("failed to generate code: %w", err)
}
// 6. 创建验证码记录
smsCode := &models.SMSCode{
Phone: normalizedPhone,
Code: code,
Region: req.Region,
Purpose: req.Purpose,
Status: models.CodeStatusPending,
ExpiresAt: time.Now().Add(cfg.Config.SMS.Global.CodeExpiry),
}
// 7. 发送短信
sendReq := &sms_providers.SendSMSRequest{
Phone: normalizedPhone,
TemplateID: req.TemplateID,
Region: req.Region,
Purpose: req.Purpose,
Params: map[string]string{
"code": code,
},
}
err = cfg.DB().Transaction(func(tx *gorm.DB) error {
err := tx.Create(smsCode).Error
if err != nil {
return err
}
smsID, err := provider.SendSMS(x.Context(), sendReq)
LogSMS(normalizedPhone, req.Region, provider.GetName(), smsID, err)
if err != nil || smsID == "" {
return fmt.Errorf("failed to send sms: %v", err)
}
return nil
})
if err != nil {
return nil, err
}
return &SendCodeResponse{
ID: smsCode.ID,
Phone: normalizedPhone,
ExpiresAt: smsCode.ExpiresAt,
Interval: int(cfg.Config.SMS.Global.SendInterval.Seconds()),
}, nil
}

@ -0,0 +1,90 @@
//
// utils.go
// Copyright (C) 2025 veypi <i@veypi.com>
//
// Distributed under terms of the MIT license.
//
package sms
import (
"fmt"
"time"
"github.com/veypi/OneAuth/cfg"
"github.com/veypi/OneAuth/models"
"gorm.io/gorm"
)
// checkDailyLimit 检查每日发送次数限制
func CheckDailyLimit(phone, region string) error {
today := time.Now().Truncate(24 * time.Hour)
tomorrow := today.Add(24 * time.Hour)
var count int64
err := cfg.DB().Model(&models.SMSCode{}).
Where("phone = ? AND region = ? AND created_at >= ? AND created_at < ?",
phone, region, today, tomorrow).
Count(&count).Error
if err != nil {
return fmt.Errorf("failed to check daily limit: %w", err)
}
if count >= int64(cfg.Config.SMS.Global.MaxDailyCount) {
return fmt.Errorf("daily sms limit exceeded")
}
return nil
}
// checkSendInterval 检查发送间隔
func CheckSendInterval(phone, region, purpose string) error {
var lastCode models.SMSCode
err := cfg.DB().Where("phone = ? AND region = ? AND purpose = ?", phone, region, purpose).
Order("created_at DESC").
First(&lastCode).Error
if err != nil && err != gorm.ErrRecordNotFound {
return fmt.Errorf("failed to check send interval: %w", err)
}
if err == nil {
timeSinceLastSend := time.Since(lastCode.CreatedAt)
if timeSinceLastSend < cfg.Config.SMS.Global.SendInterval {
remaining := cfg.Config.SMS.Global.SendInterval - timeSinceLastSend
return fmt.Errorf("please wait %d seconds before sending again", int(remaining.Seconds()))
}
}
return nil
}
// logSMS 记录短信发送日志
func LogSMS(phone, region, provider, messageID string, err error) {
log := &models.SMSLog{
Phone: phone,
Region: region,
Provider: provider,
MessageID: messageID,
Status: "ok",
}
if err != nil {
log.Status = "failed"
log.Error = err.Error()
}
cfg.DB().Create(log)
}
// CleanupExpiredCodes 清理过期的验证码
func CleanupExpiredCodes() error {
result := cfg.DB().Model(&models.SMSCode{}).
Where("status = ? AND expires_at < ?", models.CodeStatusPending, time.Now()).
Update("status", models.CodeStatusExpired)
if result.Error != nil {
return fmt.Errorf("failed to cleanup expired codes: %w", result.Error)
}
return nil
}

@ -0,0 +1,72 @@
//
// validate.go
// Copyright (C) 2025 veypi <i@veypi.com>
//
// Distributed under terms of the MIT license.
//
package sms
import (
"fmt"
"time"
"github.com/veypi/OneAuth/cfg"
"github.com/veypi/OneAuth/libs/utils"
"github.com/veypi/OneAuth/models"
"gorm.io/gorm"
)
// VerifyCode 验证验证码
func VerifyCode(Phone, Code, Region, Purpose string) error {
normalizedPhone := utils.NormalizePhoneNumber(Phone)
// 1. 查找最新的待验证码
var smsCode models.SMSCode
err := cfg.DB().Where("phone = ? AND region = ? AND purpose = ? AND status = ?",
normalizedPhone, Region, Purpose, models.CodeStatusPending).
Order("created_at DESC").
First(&smsCode).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return fmt.Errorf("verification code not found or already used")
}
return fmt.Errorf("failed to query sms code: %w", err)
}
// 2. 检查验证码是否过期
if smsCode.IsExpired() {
cfg.DB().Model(&smsCode).Updates(map[string]any{
"status": models.CodeStatusExpired,
})
return fmt.Errorf("verification code has expired")
}
// 3. 检查是否已达到最大尝试次数
if !smsCode.CanRetry(cfg.Config.SMS.Global.MaxAttempts) {
cfg.DB().Model(&smsCode).Updates(map[string]any{
"status": models.CodeStatusFailed,
})
return fmt.Errorf("verification failed too many times")
}
// 4. 验证码不匹配
if smsCode.Code != Code {
cfg.DB().Model(&smsCode).Updates(map[string]any{
"attempts": smsCode.Attempts + 1,
})
remaining := cfg.Config.SMS.Global.MaxAttempts - smsCode.Attempts - 1
return fmt.Errorf("verification code incorrect, %d attempts remaining", remaining)
}
// 5. 验证成功
now := time.Now()
cfg.DB().Model(&smsCode).Updates(map[string]any{
"status": models.CodeStatusUsed,
"used_at": &now,
})
return nil
}

@ -99,7 +99,7 @@ func tokenPost(x *vigo.X) (any, error) {
if opts.Device != nil {
newToken.Device = *opts.Device
}
newToken.Ip = x.GetRemoteIp()
newToken.Ip = x.GetRemoteIP()
logv.AssertError(cfg.DB().Create(newToken).Error)
// gen other app token
claim.ID = newToken.ID

@ -16,6 +16,7 @@ import (
"math/rand"
"github.com/google/uuid"
"github.com/veypi/OneAuth/api/sms"
"github.com/veypi/OneAuth/cfg"
"github.com/veypi/OneAuth/libs/utils"
"github.com/veypi/OneAuth/models"
@ -26,12 +27,15 @@ import (
var _ = Router.Post("/", vigo.SkipBefore, publicLimits, userPost)
type postOpts struct {
Username string `json:"username" gorm:"varchar(100);unique;default:not null" parse:"json"`
Code string `json:"code" gorm:"varchar(128)" parse:"json"`
Username string `json:"username" gorm:"varchar(100);unique;default:not null" parse:"json"`
Code string `json:"code" gorm:"varchar(128)" parse:"json"`
Phone string `json:"phone" gorm:"varchar(50);unique;default:null" parse:"json"`
VerifyCode string `json:"verify_code" parse:"json"`
Region string `json:"region" parse:"json"`
Nickname *string `json:"nickname" parse:"json"`
Icon *string `json:"icon" parse:"json"`
Email *string `json:"email" gorm:"varchar(20);unique;default:null" parse:"json"`
Phone *string `json:"phone" gorm:"varchar(50);unique;default:null" parse:"json"`
}
func userPost(x *vigo.X) (any, error) {
@ -43,6 +47,12 @@ func userPost(x *vigo.X) (any, error) {
data := &models.User{}
data.ID = strings.ReplaceAll(uuid.New().String(), "-", "")
data.Phone = opts.Region + opts.Phone
data.Region = opts.Region
err = sms.VerifyCode(opts.Phone, opts.VerifyCode, opts.Region, "signup")
if err != nil {
return nil, vigo.ErrArgInvalid.WithArgs("verify code").WithString(err.Error())
}
data.Username = opts.Username
data.Code = opts.Code
data.Salt = utils.RandSeq(16)
@ -73,9 +83,6 @@ func userPost(x *vigo.X) (any, error) {
if opts.Email != nil {
data.Email = *opts.Email
}
if opts.Phone != nil {
data.Phone = *opts.Phone
}
data.Status = 1
err = cfg.DB().Transaction(func(tx *gorm.DB) error {
err := tx.Create(data).Error

@ -12,6 +12,7 @@ import (
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/veypi/OneAuth/api/sms"
"github.com/veypi/OneAuth/cfg"
"github.com/veypi/OneAuth/libs/auth"
"github.com/veypi/OneAuth/libs/utils"
@ -21,24 +22,24 @@ import (
"github.com/vyes-ai/vigo/logv"
)
var publicLimits = limiter.NewAdvancedRequestLimiter(limiter.LimiterConfig{
Window: time.Minute * 5,
MaxRequests: 20,
MinInterval: time.Second * 3,
}).Limit
var publicLimits = limiter.NewAdvancedRequestLimiter(time.Minute*5, 20, time.Second*3, nil).Limit
var _ = Router.Post("/login",
vigo.SkipBefore, publicLimits,
userLogin)
type loginOpts struct {
UserName string `json:"username" parse:"json"`
Code string `json:"code" parse:"json"`
Phone *string `json:"phone" parse:"json"`
Email *string `json:"email" parse:"json"`
Type *string `json:"type" parse:"json"`
AppID *string `json:"app_id" parse:"json"`
Device *string `json:"device" parse:"json"`
UserName string `json:"username" parse:"json"`
Code string `json:"code" parse:"json"`
VerifyCode string `json:"verify_code" parse:"json"`
Region string `json:"region" parse:"json"`
Phone string `json:"phone" parse:"json"`
Email string `json:"email" parse:"json"`
Type *string `json:"type" parse:"json"`
AppID *string `json:"app_id" parse:"json"`
Device *string `json:"device" parse:"json"`
}
func userLogin(x *vigo.X) (any, error) {
@ -57,7 +58,7 @@ func userLogin(x *vigo.X) (any, error) {
}
switch typ {
case "phone":
query = query.Where("phone = ?", opts.Phone)
query = query.Where("phone = ?", opts.Region+opts.Phone)
case "email":
query = query.Where("email = ?", opts.Email)
default:
@ -68,16 +69,22 @@ func userLogin(x *vigo.X) (any, error) {
return nil, vigo.ErrNotFound
}
logv.Info().Str("user", user.ID).Msg("login")
code, err := base64.URLEncoding.DecodeString(opts.Code)
if err != nil {
return nil, vigo.ErrArgInvalid.WithArgs("code")
}
logv.Warn().Msgf("code: %s", code)
if opts.VerifyCode != "" {
err = sms.VerifyCode(opts.Phone, opts.VerifyCode, opts.Region, "signin")
if err != nil {
logv.Warn().Msgf("verify code: %v", err)
return nil, vigo.ErrNotAuthorized.WithError(err)
}
} else {
code, err := base64.URLEncoding.DecodeString(opts.Code)
if err != nil {
return nil, vigo.ErrArgInvalid.WithArgs("code")
}
ncode, err := utils.AesDecrypt([]byte(user.Code), utils.PKCS7Padding(code, 32), []byte(user.Salt))
logv.Warn().Msgf("id: %s\n%s", ncode, user.ID)
if err != nil || string(ncode) != user.ID {
return nil, vigo.ErrNotAuthorized
ncode, err := utils.AesDecrypt([]byte(user.Code), utils.PKCS7Padding(code, 32), []byte(user.Salt))
if err != nil || string(ncode) != user.ID {
return nil, vigo.ErrNotAuthorized
}
}
aid := cfg.Config.ID
if opts.AppID != nil && *opts.AppID != "" {
@ -92,7 +99,7 @@ func userLogin(x *vigo.X) (any, error) {
if opts.Device != nil {
data.Device = *opts.Device
}
data.Ip = x.GetRemoteIp()
data.Ip = x.GetRemoteIP()
logv.AssertError(cfg.DB().Create(data).Error)
claim := &auth.Claims{}
claim.IssuedAt = jwt.NewNumericDate(time.Now())

@ -7,20 +7,42 @@
package cfg
import "time"
import (
"os"
"time"
)
type Options struct {
DSN string `json:"dsn"` // Data Source Name
DB string `json:"db"` // DB type: mysql, postgres, sqlite
DSN string `json:"dsn"` // Data Source Name
DB string `json:"db"` // DB type: mysql, postgres, sqlite
RedisAddr string `json:"redis_addr"`
ID string `json:"id"`
Key string `json:"key"`
TokenExpire time.Duration `json:"token_expire"` // Token expiration time in seconds
SMS *SMSConfig `json:"sms"`
}
var Config = &Options{
TokenExpire: time.Second * 100,
ID: "test",
Key: "asdfghjklqwertyuiopzxcvbnm1234567890",
DSN: "root:123456@tcp(127.0.0.1:3306)/test?charset=utf8&parseTime=True&loc=Local",
DB: "mysql",
TokenExpire: time.Minute * 120,
ID: getEnv("APPID", "test"),
Key: getEnv("APPKEY", "asdfghjklqwertyuiopzxcvbnm1234567890"),
DSN: getEnv("DSN", "root:123456@tcp(127.0.0.1:3306)/test?charset=utf8&parseTime=True&loc=Local"),
DB: getEnv("DB", "mysql"),
SMS: defaultSMS(),
}
func getEnv(key, defaultValue string) string {
v := os.Getenv(key)
if v != "" {
return v
}
return defaultValue
}
type PublicSettings struct {
SMS string `json:"sms"`
SMSKey string `json:"sms_key"`
SMSSecret string `json:"sms_secret"`
SMSRegion string `json:"sms_region"`
}

@ -0,0 +1,51 @@
package cfg
import (
"time"
)
// Config 短信验证码配置
type SMSConfig struct {
// 区域配置 +86/.../global
Regions map[string]RegionConfig `json:"regions"`
Default RegionConfig `json:"default"` // 默认区域配置
// 全局配置
Global GlobalConfig `json:"global"`
}
// RegionConfig 区域配置
type RegionConfig struct {
Provider string `json:"provider"` // aliyun, tencent
TemplateID string `json:"template_id"` // 模板ID
Key string `json:"key"`
Secret string `json:"secret"`
SignName string `json:"sign_name"`
Endpoint string `json:"endpoint"`
}
// GlobalConfig 全局配置
type GlobalConfig struct {
CodeLength int `json:"code_length" usage:"验证码长度默认6位"`
CodeExpiry time.Duration `json:"code_expiry" usage:"验证码有效期默认5分钟"`
SendInterval time.Duration `json:"send_interval" usage:"发送间隔默认60秒"`
MaxAttempts int `json:"max_attempts" usage:"最大重试次数默认3次"`
MaxDailyCount int `json:"max_daily_count" usage:"每日最大发送次数默认100次,0禁用-1不限制"`
}
// DefaultConfig 默认配置
func defaultSMS() *SMSConfig {
return &SMSConfig{
Global: GlobalConfig{
CodeLength: 6,
CodeExpiry: 5 * time.Minute,
SendInterval: 60 * time.Second,
MaxAttempts: 3,
MaxDailyCount: 100,
},
Default: RegionConfig{
Provider: "aliyun",
},
}
}

@ -36,6 +36,7 @@ func init() {
cmdMain.StringVar(&cliOpts.Host, "host", "0.0.0.0", "host")
cmdMain.IntVar(&cliOpts.Port, "p", 4000, "port")
cmdMain.StringVar(&cliOpts.LoggerLevel, "l", "info", "log level")
cmdMain.AutoRegister(cliOpts)
cmdMain.Before = func() error {
flags.LoadCfg(*configFile, cfg.Config)

@ -7,6 +7,11 @@ replace github.com/veypi/vyes-ui => ../vyes-ui/
replace github.com/vyes-ai/vigo => ../vigo/
require (
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.9
github.com/alibabacloud-go/dysmsapi-20170525/v5 v5.1.2
github.com/alibabacloud-go/tea v1.3.10
github.com/alibabacloud-go/tea-utils/v2 v2.0.7
github.com/aliyun/credentials-go v1.4.7
github.com/glebarez/sqlite v1.11.0
github.com/golang-jwt/jwt/v5 v5.2.3
github.com/google/uuid v1.6.0
@ -20,6 +25,11 @@ require (
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
github.com/alibabacloud-go/debug v1.0.1 // indirect
github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect
github.com/alibabacloud-go/openapi-util v0.1.1 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/glebarez/go-sqlite v1.21.2 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
@ -29,16 +39,22 @@ require (
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/rs/zerolog v1.34.0 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/text v0.27.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.22.5 // indirect

244
go.sum

@ -1,5 +1,63 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6 h1:eIf+iGJxdU4U9ypaUfbtOWCsZSbTb8AUHvyPrxu6mAA=
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6/go.mod h1:4EUIoxs/do24zMOGGqYVWgw0s9NtiylnJglOeEB5UJo=
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc=
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 h1:zE8vH9C7JiZLNJJQ5OwjU9mSi4T9ef9u3BURT6LCLC8=
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5/go.mod h1:tWnyE9AjF8J8qqLk645oUmVUnFybApTQWklQmi5tY6g=
github.com/alibabacloud-go/darabonba-array v0.1.0 h1:vR8s7b1fWAQIjEjWnuF0JiKsCvclSRTfDzZHTYqfufY=
github.com/alibabacloud-go/darabonba-array v0.1.0/go.mod h1:BLKxr0brnggqOJPqT09DFJ8g3fsDshapUD3C3aOEFaI=
github.com/alibabacloud-go/darabonba-encode-util v0.0.2 h1:1uJGrbsGEVqWcWxrS9MyC2NG0Ax+GpOM5gtupki31XE=
github.com/alibabacloud-go/darabonba-encode-util v0.0.2/go.mod h1:JiW9higWHYXm7F4PKuMgEUETNZasrDM6vqVr/Can7H8=
github.com/alibabacloud-go/darabonba-map v0.0.2 h1:qvPnGB4+dJbJIxOOfawxzF3hzMnIpjmafa0qOTp6udc=
github.com/alibabacloud-go/darabonba-map v0.0.2/go.mod h1:28AJaX8FOE/ym8OUFWga+MtEzBunJwQGceGQlvaPGPc=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.11/go.mod h1:wHxkgZT1ClZdcwEVP/pDgYK/9HucsnCfMipmJgCz4xY=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.9 h1:7P0KWfed/YMtpeuW3E2iwokzoz9L7H9rB+VZzg5DeBs=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.9/go.mod h1:kgnXaV74AVjM3ZWJu1GhyXGuCtxljJ677oUfz6MyJOE=
github.com/alibabacloud-go/darabonba-signature-util v0.0.7 h1:UzCnKvsjPFzApvODDNEYqBHMFt1w98wC7FOo0InLyxg=
github.com/alibabacloud-go/darabonba-signature-util v0.0.7/go.mod h1:oUzCYV2fcCH797xKdL6BDH8ADIHlzrtKVjeRtunBNTQ=
github.com/alibabacloud-go/darabonba-string v1.0.2 h1:E714wms5ibdzCqGeYJ9JCFywE5nDyvIXIIQbZVFkkqo=
github.com/alibabacloud-go/darabonba-string v1.0.2/go.mod h1:93cTfV3vuPhhEwGGpKKqhVW4jLe7tDpo3LUM0i0g6mA=
github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68/go.mod h1:6pb/Qy8c+lqua8cFpEy7g39NRRqOWc3rOwAy8m5Y2BY=
github.com/alibabacloud-go/debug v1.0.0/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=
github.com/alibabacloud-go/debug v1.0.1 h1:MsW9SmUtbb1Fnt3ieC6NNZi6aEwrXfDksD4QA6GSbPg=
github.com/alibabacloud-go/debug v1.0.1/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=
github.com/alibabacloud-go/dysmsapi-20170525/v5 v5.1.2 h1:mNSlLE7QQiZLmC55BJog3PFJFtQp10lbnnEWWIxeCvM=
github.com/alibabacloud-go/dysmsapi-20170525/v5 v5.1.2/go.mod h1:mYOaEwXaib4RLB2NY8cXFjKbxPQHUqt6lhPEOvqR8aw=
github.com/alibabacloud-go/endpoint-util v1.1.0 h1:r/4D3VSw888XGaeNpP994zDUaxdgTSHBbVfZlzf6b5Q=
github.com/alibabacloud-go/endpoint-util v1.1.0/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE=
github.com/alibabacloud-go/openapi-util v0.1.0/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws=
github.com/alibabacloud-go/openapi-util v0.1.1 h1:ujGErJjG8ncRW6XtBBMphzHTvCxn4DjrVw4m04HsS28=
github.com/alibabacloud-go/openapi-util v0.1.1/go.mod h1:/UehBSE2cf1gYT43GV4E+RxTdLRzURImCYY0aRmlXpw=
github.com/alibabacloud-go/tea v1.1.0/go.mod h1:IkGyUSX4Ba1V+k4pCtJUc6jDpZLFph9QMy2VUPTwukg=
github.com/alibabacloud-go/tea v1.1.7/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
github.com/alibabacloud-go/tea v1.1.8/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
github.com/alibabacloud-go/tea v1.1.11/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
github.com/alibabacloud-go/tea v1.1.20/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk=
github.com/alibabacloud-go/tea v1.3.10 h1:J0Ke8iMyoxX2daj90hdPr1QgfxJnhR8SOflB910o/Dk=
github.com/alibabacloud-go/tea v1.3.10/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg=
github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE=
github.com/alibabacloud-go/tea-utils/v2 v2.0.5/go.mod h1:dL6vbUT35E4F4bFTHL845eUloqaerYBYPsdWR2/jhe4=
github.com/alibabacloud-go/tea-utils/v2 v2.0.6/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 h1:WDx5qW3Xa5ZgJ1c8NfqJkF6w+AU5wB8835UdhPr6Ax0=
github.com/alibabacloud-go/tea-utils/v2 v2.0.7/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
github.com/alibabacloud-go/tea-xml v1.1.3/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8=
github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw=
github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0=
github.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmPrib8NVePL3fxM=
github.com/aliyun/credentials-go v1.4.5/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=
github.com/aliyun/credentials-go v1.4.7 h1:T17dLqEtPUFvjDRRb5giVvLh6dFT8IcNFJJb7MeyCxw=
github.com/aliyun/credentials-go v1.4.7/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/clbanning/mxj/v2 v2.5.5/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -7,6 +65,9 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
@ -16,10 +77,30 @@ github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqw
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0=
github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
@ -32,8 +113,16 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
@ -41,9 +130,18 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
@ -52,31 +150,171 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/vyes-ai/vigo v0.5.0 h1:Cdbq93VL9Eecp09yk6fCJeI18PZijj7M0irLH2Hx26s=
github.com/vyes-ai/vigo v0.5.0/go.mod h1:FTltuMDGidB5YNvklD/QayIPPLQLza7nRkp2IErHGcU=
github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@ -86,6 +324,8 @@ gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/gorm v1.30.1 h1:lSHg33jJTBxs2mgJRfRZeLDG+WZaHYCk3Wtfl6Ngzo4=
gorm.io/gorm v1.30.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=

@ -0,0 +1,110 @@
package sms_providers
import (
"context"
"encoding/json"
"fmt"
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
dysmsapi20170525 "github.com/alibabacloud-go/dysmsapi-20170525/v5/client"
util "github.com/alibabacloud-go/tea-utils/v2/service"
"github.com/alibabacloud-go/tea/tea"
credential "github.com/aliyun/credentials-go/credentials"
"github.com/veypi/OneAuth/cfg"
"github.com/vyes-ai/vigo"
"github.com/vyes-ai/vigo/logv"
)
// AliyunProvider 阿里云短信服务
type AliyunProvider struct {
accessKeyID string
accessKeySecret string
signName string
endpoint string
templateID string
}
// NewAliyunProvider 创建阿里云短信服务
func NewAliyunProvider(config cfg.RegionConfig) (SMSProvider, error) {
if config.Endpoint == "" {
config.Endpoint = "dysmsapi.aliyuncs.com"
}
return &AliyunProvider{
accessKeyID: config.Key,
accessKeySecret: config.Secret,
signName: config.SignName,
endpoint: config.Endpoint,
templateID: config.TemplateID,
}, nil
}
// SendSMS 发送短信
func (p *AliyunProvider) SendSMS(ctx context.Context, req *SendSMSRequest) (string, error) {
if req.Phone == "" {
return "", fmt.Errorf("phone is required")
}
// 工程代码建议使用更安全的无AK方式凭据配置方式请参见https://help.aliyun.com/document_detail/378661.html。
credential, err := credential.NewCredential(new(credential.Config).SetType("access_key").SetAccessKeyId(p.accessKeyID).SetAccessKeySecret(p.accessKeySecret))
// credential, err := credential.NewCredential(nil)
if err != nil {
return "", err
}
config := &openapi.Config{
Credential: credential,
}
// Endpoint 请参考 https://api.aliyun.com/product/Dysmsapi
config.Endpoint = tea.String(p.endpoint)
client, err := dysmsapi20170525.NewClient(config)
if err != nil {
return "", err
}
templateID := req.TemplateID
if templateID == "" {
templateID = p.templateID
}
paramsBytes, _ := json.Marshal(req.Params)
logv.Warn().Msgf("Send SMS to %s, %s, templateID: %s, params: %s", req.Phone, p.signName, templateID, string(paramsBytes))
sendSmsRequest := &dysmsapi20170525.SendSmsRequest{
SignName: tea.String(p.signName),
TemplateCode: tea.String(templateID),
PhoneNumbers: tea.String(req.Phone),
TemplateParam: tea.String(string(paramsBytes)),
}
runtime := &util.RuntimeOptions{}
msgID := ""
tryErr := func() (_e error) {
defer func() {
if r := tea.Recover(recover()); r != nil {
_e = r
}
}()
// 复制代码运行请自行打印 API 的返回值
res, _err := client.SendSmsWithOptions(sendSmsRequest, runtime)
if _err != nil {
return _err
}
if res != nil && res.Body != nil && res.Body.BizId != nil {
msgID = *res.Body.BizId
}
if msgID == "" && res != nil && res.Body != nil && res.Body.Message != nil {
logv.Warn().Msgf("%+v", res)
return vigo.NewError(*res.Body.Message)
}
return nil
}()
if tryErr != nil {
return "", tryErr
}
return msgID, nil
}
// GetName 获取服务商名称
func (p *AliyunProvider) GetName() string {
return "aliyun"
}

@ -0,0 +1,114 @@
package sms_providers
import (
"context"
"fmt"
"github.com/veypi/OneAuth/cfg"
)
// SMSProvider 短信服务商接口
type SMSProvider interface {
// SendSMS 发送短信
SendSMS(ctx context.Context, req *SendSMSRequest) (id string, err error)
// GetName 获取服务商名称
GetName() string
}
var factory *ProviderFactory
var providers = make(map[string]SMSProvider)
func GetSMSProvider(region string) (SMSProvider, error) {
if factory == nil {
factory = NewProviderFactory()
// 初始化各区域的短信服务商
for region, regionConfig := range cfg.Config.SMS.Regions {
provider, err := factory.Create(regionConfig)
if err != nil {
return nil, fmt.Errorf("failed to create provider for region %s: %w", region, err)
}
providers[region] = provider
}
provider, err := factory.Create(cfg.Config.SMS.Default)
if err != nil {
return nil, fmt.Errorf("failed to create default provider: %w", err)
}
providers["default"] = provider
}
p, ok := providers[region]
if !ok {
p, ok = providers["default"]
if !ok {
return nil, fmt.Errorf("no SMS provider found for region %s", region)
}
}
return p, nil
}
// SendSMSRequest 发送短信请求
type SendSMSRequest struct {
Phone string `json:"phone"` // 手机号
TemplateID string `json:"template_id"` // 模板ID
Params map[string]string `json:"params"` // 模板参数
Region string `json:"region"` // 区域
Purpose string `json:"purpose"` // 用途
}
// SendSMSResponse 发送短信响应
type SendSMSResponse struct {
MessageID string `json:"message_id"` // 消息ID
Status string `json:"status"` // 发送状态
Cost float64 `json:"cost"` // 费用
Error string `json:"error"` // 错误信息
}
// ProviderFactory 服务商工厂
type ProviderFactory struct {
providers map[string]func(cfg.RegionConfig) (SMSProvider, error)
}
// NewProviderFactory 创建服务商工厂
func NewProviderFactory() *ProviderFactory {
factory := &ProviderFactory{
providers: make(map[string]func(cfg.RegionConfig) (SMSProvider, error)),
}
// 注册服务商
factory.Register("aliyun", NewAliyunProvider)
factory.Register("tencent", NewTencentProvider)
return factory
}
// Register 注册服务商
func (f *ProviderFactory) Register(name string, constructor func(cfg.RegionConfig) (SMSProvider, error)) {
f.providers[name] = constructor
}
// Create 创建服务商实例
func (f *ProviderFactory) Create(config cfg.RegionConfig) (SMSProvider, error) {
constructor, exists := f.providers[config.Provider]
if !exists {
return nil, &ProviderError{
Provider: config.Provider,
Message: "unsupported provider",
}
}
return constructor(config)
}
// ProviderError 服务商错误
type ProviderError struct {
Provider string
Message string
Err error
}
func (e *ProviderError) Error() string {
if e.Err != nil {
return e.Provider + ": " + e.Message + ": " + e.Err.Error()
}
return e.Provider + ": " + e.Message
}

@ -0,0 +1,46 @@
package sms_providers
import (
"context"
"fmt"
"github.com/veypi/OneAuth/cfg"
)
// TencentProvider 腾讯云短信服务
type TencentProvider struct {
secretID string
secretKey string
sdkAppID string
signName string
region string
}
// NewTencentProvider 创建腾讯云短信服务
func NewTencentProvider(config cfg.RegionConfig) (SMSProvider, error) {
return &TencentProvider{
secretID: config.Key,
secretKey: config.Secret,
sdkAppID: config.Endpoint,
signName: config.SignName,
region: config.Provider,
}, nil
}
// SendSMS 发送短信
func (p *TencentProvider) SendSMS(ctx context.Context, req *SendSMSRequest) (string, error) {
// 这里应该调用腾讯云SDK为了示例简化处理
// 实际实现需要集成腾讯云SMS SDK
// 模拟发送逻辑
if req.Phone == "" {
return "", fmt.Errorf("phone is empty")
}
return "tencent-message-id-12345", nil
}
// GetName 获取服务商名称
func (p *TencentProvider) GetName() string {
return "tencent"
}

@ -0,0 +1,81 @@
package utils
import (
"crypto/rand"
"fmt"
"math/big"
"strings"
)
// GenerateCode 生成指定长度的数字验证码
func GenerateCode(length int) (string, error) {
if length <= 0 {
return "", fmt.Errorf("code length must be positive")
}
var code strings.Builder
for range length {
digit, err := rand.Int(rand.Reader, big.NewInt(10))
if err != nil {
return "", fmt.Errorf("failed to generate random digit: %w", err)
}
code.WriteString(digit.String())
}
return code.String(), nil
}
// GenerateAlphaNumericCode 生成字母数字混合验证码
func GenerateAlphaNumericCode(length int) (string, error) {
if length <= 0 {
return "", fmt.Errorf("code length must be positive")
}
const charset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
var code strings.Builder
for range length {
index, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
if err != nil {
return "", fmt.Errorf("failed to generate random character: %w", err)
}
code.WriteByte(charset[index.Int64()])
}
return code.String(), nil
}
// ValidatePhoneNumber 简单的手机号验证
func ValidatePhoneNumber(phone string) bool {
if len(phone) < 10 || len(phone) > 15 {
return false
}
// 检查是否都是数字或以+开头
if phone[0] == '+' {
phone = phone[1:]
}
for _, char := range phone {
if char < '0' || char > '9' {
return false
}
}
return true
}
// NormalizePhoneNumber 标准化手机号
func NormalizePhoneNumber(phone string) string {
// 移除所有非数字字符,除了+号
var normalized strings.Builder
for i, char := range phone {
if char == '+' && i == 0 {
normalized.WriteRune(char)
} else if char >= '0' && char <= '9' {
normalized.WriteRune(char)
}
}
return normalized.String()
}

@ -34,7 +34,7 @@ func (m *BaseModel) BeforeCreate(tx *gorm.DB) error {
var AllModels = &dbmodels.ModelList{}
func init() {
AllModels.Append(User{}, AppUser{}, Resource{}, Access{}, Role{}, UserRole{}, Token{}, App{})
AllModels.Append(User{}, AppUser{}, Resource{}, Access{}, Role{}, UserRole{}, Token{}, App{}, SMSCode{}, SMSLog{})
}
func Migrate() error {

@ -0,0 +1,65 @@
package models
import (
"gorm.io/gorm"
"time"
)
// SMSCode 短信验证码记录
type SMSCode struct {
ID uint `gorm:"primarykey" json:"id"`
Phone string `gorm:"index;not null" json:"phone"` // 手机号
Code string `gorm:"not null" json:"code"` // 验证码
Region string `gorm:"index;not null" json:"region"` // 区域
Purpose string `gorm:"index;not null" json:"purpose"` // 用途(注册、登录、重置密码等)
Status CodeStatus `gorm:"default:0" json:"status"` // 状态
ExpiresAt time.Time `gorm:"index;not null" json:"expires_at"` // 过期时间
UsedAt *time.Time `gorm:"index" json:"used_at"` // 使用时间
Attempts int `gorm:"default:0" json:"attempts"` // 验证尝试次数
SendCount int `gorm:"default:1" json:"send_count"` // 发送次数
RemoteIP string `gorm:"index" json:"remote_ip"` // 远程IP地址
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"`
}
// CodeStatus 验证码状态
type CodeStatus int
const (
CodeStatusPending CodeStatus = iota // 待验证
CodeStatusUsed // 已使用
CodeStatusExpired // 已过期
CodeStatusFailed // 验证失败(超过最大尝试次数)
)
// SMSLog 短信发送日志
type SMSLog struct {
ID uint `gorm:"primarykey" json:"id"`
Phone string `gorm:"index;not null" json:"phone"`
Region string `gorm:"index;not null" json:"region"`
Provider string `gorm:"not null" json:"provider"`
MessageID string `gorm:"index" json:"message_id"` // 服务商返回的消息ID
Content string `json:"content"` // 短信内容
Status string `gorm:"not null" json:"status"` // 发送状态
Error string `json:"error"` // 错误信息
Cost float64 `json:"cost"` // 费用
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"`
}
// IsExpired 检查验证码是否过期
func (s *SMSCode) IsExpired() bool {
return time.Now().After(s.ExpiresAt)
}
// IsUsed 检查验证码是否已使用
func (s *SMSCode) IsUsed() bool {
return s.Status == CodeStatusUsed && s.UsedAt != nil
}
// CanRetry 检查是否可以重试验证
func (s *SMSCode) CanRetry(maxAttempts int) bool {
return s.Status == CodeStatusPending && s.Attempts < maxAttempts && !s.IsExpired()
}

@ -13,8 +13,9 @@ type User struct {
Nickname string `json:"nickname" gorm:"type:varchar(100)" parse:"json"`
Icon string `json:"icon" parse:"json"`
Email string `json:"email" gorm:"unique;type:varchar(64);null;default:null" parse:"json"`
Phone string `json:"phone" gorm:"type:varchar(64);unique;null;default:null" parse:"json"`
Email string `json:"email" gorm:"unique;type:varchar(64);null;default:null" parse:"json"`
Phone string `json:"phone" gorm:"type:varchar(64);unique;null;default:null" parse:"json"`
Region string `json:"region" gorm:"type:varchar(32);default:null" parse:"json"`
Status uint `json:"status" parse:"json"`

File diff suppressed because one or more lines are too long

@ -5,7 +5,6 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>登录与注册</title>
<meta name="description" content="用户登录与注册页面" />
<style>
* {
box-sizing: border-box;
@ -18,7 +17,6 @@
height: 100vh;
overflow: hidden;
background: #ffebee;
/* 红色主题背景 */
display: flex;
justify-content: center;
align-items: center;
@ -33,7 +31,6 @@
left: 0;
overflow: hidden;
background: linear-gradient(to right, #ef5350, #e53935);
/* 红色渐变 */
}
.bubble {
@ -77,7 +74,6 @@
a {
color: #c62828;
/* 红色链接 */
font-size: 14px;
text-decoration: none;
margin: 15px 0;
@ -86,9 +82,7 @@
button {
border-radius: 20px;
border: 1px solid #e53935;
/* 红色边框 */
background-color: #e53935;
/* 红色背景 */
color: #ffffff;
font-size: 12px;
font-weight: bold;
@ -97,6 +91,8 @@
text-transform: uppercase;
transition: transform 80ms ease-in;
cursor: pointer;
position: relative;
overflow: hidden;
}
button:active {
@ -112,6 +108,40 @@
border-color: #ffffff;
}
button:disabled {
background-color: #ccc;
border-color: #ccc;
cursor: not-allowed;
}
/* 加载动画样式 */
.loading-spinner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 16px;
height: 16px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top: 2px solid #ffffff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: translate(-50%, -50%) rotate(0deg);
}
100% {
transform: translate(-50%, -50%) rotate(360deg);
}
}
.button-loading {
color: transparent !important;
}
form {
background-color: #ffffff;
display: flex;
@ -126,8 +156,6 @@
input {
background-color: #eee;
border: none;
padding: 12px 15px;
margin: 8px 0;
width: 100%;
border-radius: 5px;
}
@ -190,9 +218,7 @@
.overlay {
background: #e53935;
/* 红色背景 */
background: linear-gradient(to right, #ef5350, #e53935);
/* 红色渐变 */
background-repeat: no-repeat;
background-size: cover;
background-position: 0 0;
@ -247,6 +273,7 @@
.social-container a {
border: 1px solid #dddddd;
font-size: 1.2rem;
border-radius: 50%;
display: inline-flex;
justify-content: center;
@ -261,11 +288,99 @@
background-color: #f2f2f2;
}
.social-container .fa-github {
color: #000;
}
.social-container .fa-weixin {
color: #07c160;
}
.social-container .fa-google {
color: #db4437;
}
.error-message {
color: red;
font-size: 12px;
margin-top: 8px;
}
.login-tab {
display: flex;
margin-bottom: 20px;
border-bottom: 1px solid #eee;
}
.tab-item {
padding: 10px 15px;
cursor: pointer;
flex: 1;
text-align: center;
color: #666;
font-size: 14px;
border-bottom: 2px solid transparent;
transition: all 0.3s;
}
.tab-item.active {
color: #e53935;
border-bottom-color: #e53935;
}
.input-group {
position: relative;
margin: 8px 0;
width: 100%;
}
.phone-input-group {
display: flex;
width: 100%;
margin: 8px 0;
}
.region-select {
background-color: #eee;
border: none;
border-radius: 5px 0 0 5px;
width: 80px;
font-size: 12px;
}
.phone-input {
background-color: #eee;
border: none;
border-radius: 0 5px 5px 0;
flex: 1;
}
.verify-code-input {
padding-right: 100px;
}
.verify-code-btn {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: #e53935;
font-size: 12px;
padding: 5px 10px;
border-radius: 3px;
cursor: pointer;
transition: all 0.3s;
}
.verify-code-btn:hover {
opacity: 0.8;
}
.verify-code-btn:disabled {
cursor: not-allowed;
}
</style>
</head>
@ -275,36 +390,101 @@
</div>
<div class="container" :class="{ 'right-panel-active': isSignUp }" id="container">
<!-- 注册表单 -->
<div class="form-container sign-up-container">
<form @submit="handleSignUp">
<h1>创建账户</h1>
<div class="social-container">
<a href="#" @click="handleSocialLogin('github')"><i class="fa-brands fa-github"></i></a>
<a href="#" @click="handleSocialLogin('weixin')"><i class="fa-brands fa-weixin"></i></a>
<a href="#" @click="handleSocialLogin('qq')"><i class="fa-brands fa-qq"></i></a>
<a href="#" @click="handleSocialLogin('google')"><i class="fa-brands fa-google"></i></a>
</div>
<span>或使用您的信息进行注册</span>
<input type="text" placeholder="用户名" v:value="signUpForm.username" class="input-group" required />
<!-- 手机号输入框带区域选择 -->
<div class="phone-input-group">
<select class="region-select" v:value="signUpForm.region">
<option v-for="region in regions" :value="region.code" :disabled='!region.enabled'>
{{ region.code }} {{region.name}}</option>
</select>
<input type="text" placeholder="手机号" v:value="signUpForm.phone" class="phone-input" required />
</div>
<div class="input-group">
<input type="text" placeholder="验证码" v:value="signUpForm.verifyCode" class="verify-code-input" required />
<button type="button" class="verify-code-btn" @click="sendVerifyCode('signup')"
:disabled="smsCountdown > 0 || smsLoading">
<span v-if="!smsLoading">{{ smsCountdown > 0 ? `${smsCountdown}s` : '获取验证码' }}</span>
<div v-if="smsLoading" class="loading-spinner"></div>
</button>
</div>
<span>或使用您的用户名进行注册</span>
<input type="text" placeholder="用户名" v:value="signUpForm.username" />
<input type="password" placeholder="密码" v:value="signUpForm.password" />
<input type="password" placeholder="密码" class="input-group" v:value="signUpForm.password" required />
<div class="error-message">{{ signUpError }}</div>
<button>注册</button>
<button type="submit" :class="{ 'button-loading': signUpLoading }" :disabled="signUpLoading">
<span v-if="!signUpLoading">注册</span>
<div v-if="signUpLoading" class="loading-spinner"></div>
</button>
</form>
</div>
<!-- 登录表单 -->
<div class="form-container sign-in-container">
<form @submit="handleSignIn">
<h1>登录</h1>
<div class="social-container">
<a href="#" @click="handleSocialLogin('github')"><i class="fa-brands fa-github"></i></a>
<a href="#" @click="handleSocialLogin('weixin')"><i class="fa-brands fa-weixin"></i></a>
<a href="#" @click="handleSocialLogin('qq')"><i class="fa-brands fa-qq"></i></a>
<a href="#" @click="handleSocialLogin('google')"><i class="fa-brands fa-google"></i></a>
</div>
<span>或使用您的账户</span>
<input type="text" placeholder="用户名" v:value="signInForm.username" />
<input type="password" placeholder="密码" v:value="signInForm.password" />
<!-- 登录方式选择 -->
<div class="login-tab">
<div class="tab-item" :class="{ active: loginType === 'username' }" @click="switchLoginType('username')">
用户名登录
</div>
<div class="tab-item" :class="{ active: loginType === 'phone' }" @click="switchLoginType('phone')">
手机号登录
</div>
</div>
<!-- 用户名登录 -->
<div v-if="loginType === 'username'">
<input type="text" placeholder="用户名" class="input-group" v:value="signInForm.username" required />
<input type="password" placeholder="密码" class="input-group" v:value="signInForm.password" required />
</div>
<!-- 手机号登录 -->
<div v-if="loginType === 'phone'">
<!-- 手机号输入框带区域选择 -->
<div class="phone-input-group">
<select class="region-select" v:value="signInForm.region">
<option v-for="region in regions" :value="region.code" :disabled='!region.enabled'>
{{ region.code }} {{region.name}}</option>
</select>
<input type="text" placeholder="手机号" v:value="signInForm.phone" class="phone-input" required />
</div>
<div class="input-group">
<input type="text" placeholder="验证码" v:value="signInForm.verifyCode" class="verify-code-input" required />
<button type="button" class="verify-code-btn" @click="sendVerifyCode('signin')"
:disabled="smsCountdown > 0 || smsLoading">
<span v-if="!smsLoading">{{ smsCountdown > 0 ? `${smsCountdown}s` : '获取验证码' }}</span>
<div v-if="smsLoading" class="loading-spinner"></div>
</button>
</div>
</div>
<a href="#">忘记密码?</a>
<button>登录</button>
<div class="error-message">{{ signInError }}</div>
<button type="submit" :class="{ 'button-loading': signInLoading }" :disabled="signInLoading">
<span v-if="!signInLoading">登录</span>
<div v-if="signInLoading" class="loading-spinner"></div>
</button>
</form>
</div>
<div class="overlay-container">
<div class="overlay">
<div class="overlay-panel overlay-left">
@ -323,89 +503,293 @@
<script setup>
// 响应式数据
logout = $router.query.logout
redirect = $router.query.redirect || '/'
logout = $router.query.logout;
redirect = $router.query.redirect || '/';
isSignUp = false;
signUpForm = {username: '', password: ''};
signInForm = {username: '', password: ''};
loginType = 'username'; // 'username' 或 'phone'
signUpForm = {username: '', phone: '', verifyCode: '', password: '', region: '+86'};
signInForm = {username: '', password: '', phone: '', verifyCode: '', region: '+86'};
bubbles = [];
signUpError = '';
signInError = '';
smsCountdown = 0; // 验证码倒计时
smsLoading = false; // 验证码发送加载状态
signUpLoading = false; // 注册按钮加载状态
signInLoading = false; // 登录按钮加载状态
// 常用国家/地区代码
regions = [
{code: '+86', name: '中国', enabled: true},
{code: '+1', name: '美国'},
{code: '+44', name: '英国'},
{code: '+81', name: '日本'},
{code: '+82', name: '韩国'},
{code: '+65', name: '新加坡'},
{code: '+852', name: '香港'},
{code: '+853', name: '澳门'},
{code: '+886', name: '台湾'},
{code: '+91', name: '印度'},
{code: '+33', name: '法国'},
{code: '+49', name: '德国'},
{code: '+7', name: '俄国'},
{code: '+61', name: '澳大利亚'},
{code: '+55', name: '巴西'},
{code: '+39', name: '意大利'},
{code: '+34', name: '西班牙'},
{code: '+31', name: '荷兰'},
{code: '+46', name: '瑞典'},
{code: '+47', name: '挪威'}
];
// 验证手机号格式(根据不同地区调整)
validatePhone = (phone, region) => {
if (region === '+86') {
// 中国手机号
const regex = /^1[3-9]\d{9}$/;
return regex.test(phone);
} else if (region === '+1') {
// 美国手机号
const regex = /^\d{10}$/;
return regex.test(phone);
} else {
// 其他地区,简单验证是否为数字且长度合理
const regex = /^\d{7,15}$/;
return regex.test(phone);
}
};
// 验证密码是否符合要求
const validatePassword = (password) => {
validatePassword = (password) => {
const regex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[_]).{9,}$/;
return regex.test(password);
};
// 切换登录方式
switchLoginType = (type) => {
if (signInLoading) return; // 如果正在登录,禁止切换
loginType = type;
signInError = '';
// 清空表单
signInForm.username = '';
signInForm.password = '';
signInForm.phone = '';
signInForm.verifyCode = '';
};
// 切换到注册页面
switchToSignUp = () => {
if (signInLoading || signUpLoading) return; // 如果正在加载,禁止切换
isSignUp = true;
signUpError = '';
signInError = '';
};
// 切换到登录页面
switchToSignIn = () => {
if (signInLoading || signUpLoading) return; // 如果正在加载,禁止切换
isSignUp = false;
signUpError = '';
signInError = '';
};
// 发送验证码
sendVerifyCode = async (type) => {
if (smsLoading) return; // 防止重复点击
const phone = type === 'signup' ? signUpForm.phone : signInForm.phone;
const region = type === 'signup' ? signUpForm.region : signInForm.region;
if (!phone) {
const errorMsg = '请输入手机号';
if (type === 'signup') {
signUpError = errorMsg;
} else {
signInError = errorMsg;
}
return;
}
if (!validatePhone(phone, region)) {
const errorMsg = '请输入正确的手机号格式';
if (type === 'signup') {
signUpError = errorMsg;
} else {
signInError = errorMsg;
}
return;
}
smsLoading = true; // 开始加载
try {
// 清空错误信息
if (type === 'signup') {
signUpError = '';
} else {
signInError = '';
}
await $axios.post('/api/sms', {
phone: phone,
region: region,
purpose: type // 'signup' 或 'signin'
});
$message.success('验证码已发送');
// 开始倒计时
smsCountdown = 60;
const timer = setInterval(() => {
smsCountdown--;
if (smsCountdown <= 0) {
clearInterval(timer);
}
}, 1000);
} catch (error) {
const errorMsg = error.message || '发送验证码失败,请重试';
if (type === 'signup') {
signUpError = errorMsg;
} else {
signInError = errorMsg;
}
$message.warning(errorMsg);
} finally {
smsLoading = false; // 结束加载
}
};
// 处理第三方登录
handleSocialLogin = (provider) => {
console.log(`尝试使用 ${provider} 登录`);
$message.warning(`未开放 ${provider} 登录`);
};
function deriveKey(password, salt) {
return CryptoJS.PBKDF2(password, salt, {
keySize: 256 / 32, iterations:
100, hasher: CryptoJS.algo.SHA256
})
keySize: 256 / 32,
iterations: 100,
hasher: CryptoJS.algo.SHA256
});
}
// 处理注册表单提交
handleSignUp = async (e) => {
e.preventDefault();
if (signUpLoading) return; // 防止重复提交
signUpError = '';
if (!validatePassword(signUpForm.password)) {
signUpError = '密码必须大于8位且包含大小写字母、下划线和数字。';
// 验证用户名
if (signUpForm.username.length < 6) {
signUpError = '用户名必须大于5位。';
return;
}
if (signUpForm.username.length < 2) {
signUpError = '用户名必须大于2位。';
// 验证手机号
if (!validatePhone(signUpForm.phone, signUpForm.region)) {
signUpError = '请输入正确的手机号格式。';
return;
}
signUpError = '';
// 验证验证码
if (!signUpForm.verifyCode) {
signUpError = '请输入验证码。';
return;
}
// 验证密码
if (!validatePassword(signUpForm.password)) {
signUpError = '密码必须大于8位且包含大小写字母、下划线和数字。';
return;
}
signUpLoading = true; // 开始加载
try {
const response = await $axios.post('/api/user', {
username: signUpForm.username,
phone: signUpForm.phone,
region: signUpForm.region,
verify_code: signUpForm.verifyCode,
code: btoa(signUpForm.password),
}, {noretry: true});
if (response) {
$message.success('注册成功!');
switchToSignIn();
}
signUpLoading = false; // 结束加载
$message.success('注册成功!');
// 清空表单
signUpForm = {username: '', phone: '', verifyCode: '', password: '', region: '+86'};
switchToSignIn();
} catch (error) {
signUpError = error.message || '注册失败,请重试。';
$message.warning('注册失败,请重试。');
$message.warning(signUpError);
console.error(signUpError);
} finally {
signUpLoading = false; // 结束加载
}
};
// 处理登录表单提交
handleSignIn = async (e) => {
e.preventDefault();
if (signInLoading) return; // 防止重复提交
signInError = '';
try {
const loginResponse = await $axios.post('/api/user/login', {
username: signInForm.username,
code: btoa(signInForm.password),
}, {noretry: true});
let loginData = {};
if (loginType === 'username') {
// 用户名密码登录
if (!signInForm.username) {
signInError = '请输入用户名';
return;
}
if (!signInForm.password) {
signInError = '请输入密码';
return;
}
loginData = {
username: signInForm.username,
code: btoa(signInForm.password),
type: 'username'
};
} else {
// 手机号验证码登录
if (!validatePhone(signInForm.phone, signInForm.region)) {
signInError = '请输入正确的手机号格式';
return;
}
if (!signInForm.verifyCode) {
signInError = '请输入验证码';
return;
}
loginData = {
phone: signInForm.phone,
region: signInForm.region,
verify_code: signInForm.verifyCode,
type: 'phone'
};
}
signInLoading = true; // 开始加载
const loginResponse = await $axios.post('/api/user/login', loginData, {noretry: true});
if (loginResponse && typeof loginResponse === 'string') {
localStorage.setItem('refresh', loginResponse)
window.location.href = redirect
localStorage.setItem('refresh', loginResponse);
window.location.href = redirect;
} else {
console.warn('登录失败,服务器返回异常数据', loginResponse);
$message.warning('服务器异常');
}
} catch (error) {
console.warn(error.message || error)
$message.warning('登录失败,请检查您的凭据。');
signInError = error.message || '登录失败,请检查您的凭据。';
console.warn(signInError);
$message.warning(signInError);
} finally {
signInLoading = false; // 结束加载
}
};

@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>oa</title>
<script type="module" key='vyes' src="http://test2.vyesai.com/vyes/v.js"></script>
<script type="module" key='vyes' src="/vyes/v.js"></script>
<link rel="stylesheet" href="/assets/common.css">
<link href="/assets/libs/tailwind/tailwind.min.css" rel="stylesheet">
<link href="/assets/libs/animate/animate.min.css" rel="stylesheet">

@ -106,7 +106,12 @@ class TokenService {
method: 'post',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh: refreshToken })
}).then(res => res.text())
}).then(res => {
if (res.status !== 200) {
throw new Error(`Token刷新失败状态码: ${res.status}`);
}
return res.text()
})
this.__cache = null; // 清除缓存
this.setToken(data);
} catch (e) {
@ -173,6 +178,7 @@ class TokenService {
// 检查错误响应状态码是否为 401 (未授权)
// 并且确保这不是一个已经重试过的请求 (通过 originalRequest._retry 标记)
if (error.response && error.response.status === 401 && !originalRequest.noretry) {
// 统计该请求的重试次数

Loading…
Cancel
Save