diff --git a/common/config/config.go b/common/config/config.go index 11da0b967d..59db1086d3 100644 --- a/common/config/config.go +++ b/common/config/config.go @@ -99,6 +99,7 @@ var requestInterval, _ = strconv.Atoi(os.Getenv("POLLING_INTERVAL")) var RequestInterval = time.Duration(requestInterval) * time.Second var SyncFrequency = env.Int("SYNC_FREQUENCY", 10*60) // unit is second +var TimerFrequency = env.Int("TIMER_FREQUENCY", 24) // unit is hour var BatchUpdateEnabled = false var BatchUpdateInterval = env.Int("BATCH_UPDATE_INTERVAL", 5) diff --git a/go.mod b/go.mod index ada53bc33c..d2d31381b1 100644 --- a/go.mod +++ b/go.mod @@ -55,6 +55,7 @@ require ( github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-co-op/gocron v1.37.0 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-playground/locales v0.14.1 // indirect @@ -87,6 +88,7 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect github.com/smarty/assertions v1.15.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect @@ -96,6 +98,7 @@ require ( go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect + go.uber.org/atomic v1.9.0 // indirect golang.org/x/arch v0.8.0 // indirect golang.org/x/net v0.26.0 // indirect golang.org/x/oauth2 v0.21.0 // indirect diff --git a/go.sum b/go.sum index 53db8df289..e414425f1f 100644 --- a/go.sum +++ b/go.sum @@ -31,6 +31,7 @@ github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= @@ -67,6 +68,8 @@ github.com/gin-contrib/static v1.1.2 h1:c3kT4bFkUJn2aoRU3s6XnMjJT8J6nNWJkR0Nglqm github.com/gin-contrib/static v1.1.2/go.mod h1:Fw90ozjHCmZBWbgrsqrDvO28YbhKEKzKp8GixhR4yLw= github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0= +github.com/go-co-op/gocron v1.37.0/go.mod h1:3L/n6BkO7ABj+TrfSVXLRzsP26zmikL4ISkLQ0O8iNY= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -116,6 +119,7 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= @@ -156,7 +160,12 @@ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02 github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 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/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= @@ -177,6 +186,7 @@ github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaR github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkoukk/tiktoken-go v0.1.7 h1:qOBHXX4PHtvIvmOtyg1EeKlwFRiMKAcoMp4Q+bLQDmw= @@ -184,7 +194,12 @@ github.com/pkoukk/tiktoken-go v0.1.7/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYde 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/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY= github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec= github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY= @@ -198,6 +213,7 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= @@ -217,6 +233,8 @@ go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGX go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= @@ -266,6 +284,7 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.187.0 h1:Mxs7VATVC2v7CY+7Xwm4ndkX71hpElcvx0D1Ji/p1eo= google.golang.org/api v0.187.0/go.mod h1:KIHlTc4x7N7gKKuVsdmfBXN13yEEWXWFURWY6SBp2gk= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= @@ -296,7 +315,10 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index 67a3cd95a5..41e7fdbf77 100644 --- a/main.go +++ b/main.go @@ -73,6 +73,8 @@ func main() { go model.SyncOptions(config.SyncFrequency) go model.SyncChannelCache(config.SyncFrequency) } + go model.ScheduleCheckAndDowngrade() + if os.Getenv("CHANNEL_TEST_FREQUENCY") != "" { frequency, err := strconv.Atoi(os.Getenv("CHANNEL_TEST_FREQUENCY")) if err != nil { @@ -112,4 +114,5 @@ func main() { if err != nil { logger.FatalLog("failed to start HTTP server: " + err.Error()) } + } diff --git a/model/option.go b/model/option.go index bed8d4c37d..5310725f57 100644 --- a/model/option.go +++ b/model/option.go @@ -1,9 +1,11 @@ package model import ( + "github.com/go-co-op/gocron" "github.com/songquanpeng/one-api/common/config" "github.com/songquanpeng/one-api/common/logger" billingratio "github.com/songquanpeng/one-api/relay/billing/ratio" + "log" "strconv" "strings" "time" @@ -78,6 +80,21 @@ func InitOptionMap() { loadOptionsFromDatabase() } +func ScheduleCheckAndDowngrade() { + s := gocron.NewScheduler(time.UTC) + + // 设置每天0点执行 + _, err := s.Every(1).Day().At("00:00").Do(checkAndDowngradeUsers) + + if err != nil { + log.Fatalf("创建调度任务失败: %v", err) + } + + // 开始调度 + s.StartBlocking() + log.Printf("开始调度") +} + func loadOptionsFromDatabase() { options, _ := AllOption() for _, option := range options { diff --git a/model/user.go b/model/user.go index 924d72f940..3c9a636462 100644 --- a/model/user.go +++ b/model/user.go @@ -10,7 +10,9 @@ import ( "github.com/songquanpeng/one-api/common/logger" "github.com/songquanpeng/one-api/common/random" "gorm.io/gorm" + "log" "strings" + "time" ) const ( @@ -47,6 +49,7 @@ type User struct { Group string `json:"group" gorm:"type:varchar(32);default:'default'"` AffCode string `json:"aff_code" gorm:"type:varchar(32);column:aff_code;uniqueIndex"` InviterId int `json:"inviter_id" gorm:"type:int;column:inviter_id;index"` + ExpirationDate int64 `json:"expiration_date" gorm:"bigint;default:0;column:expiration_date"` // Expiration date of the user's subscription or account. } func GetMaxUserId() int { @@ -210,6 +213,25 @@ func (user *User) ValidateAndFill() (err error) { if !okay || user.Status != UserStatusEnabled { return errors.New("用户名或密码错误,或用户已被封禁") } + // 校验用户是不是非default,如果是非default,判断到期时间如果过期了降级为default + if !(user.ExpirationDate > 0 && user.Username == "root") { + // 将时间戳转换为 time.Time 类型 + expirationTime := time.Unix(user.ExpirationDate, 0) + // 获取当前时间 + currentTime := time.Now() + + // 比较当前时间和到期时间 + if expirationTime.Before(currentTime) { + // 降级为 default + user.Group = "default" + err := DB.Model(user).Updates(user).Error + if err != nil { + fmt.Printf("用户: %s, 降级为 default 时发生错误: %v\n", user.Username, err) + return err + } + fmt.Printf("用户: %s, 特权组过期降为 default\n", user.Username) + } + } return nil } @@ -435,3 +457,48 @@ func GetUsernameById(id int) (username string) { DB.Model(&User{}).Where("id = ?", id).Select("username").Find(&username) return username } + +func checkAndDowngradeUsers() { + // 获取昨天的时间戳 + yesterdayTimestamp := time.Now().AddDate(0, 0, -1).Unix() + + // 获取需要降级的用户ID列表 + var userList []int + query := DB.Model(&User{}). + Where("`Group` != ?", "default"). + Where("`username` != ?", "root"). + Where("`expiration_date` > 0"). + Where("`expiration_date` <= ?", yesterdayTimestamp). + Select("id"). + Find(&userList) + + // 处理查询错误 + if query.Error != nil { + log.Printf("查询用户列表失败: %v", query.Error) + return + } + + // 如果没有用户需要降级,直接返回 + if len(userList) == 0 { + return + } + + // 批量降级用户 + updateQuery := DB.Model(&User{}).Where("id IN ?", userList).Update("Group", "default") + + // 处理更新错误 + if updateQuery.Error != nil { + log.Printf("批量更新用户分组失败: %v", updateQuery.Error) + return + } + + // 删除已过期用户的Redis缓存 + if common.RedisEnabled { + for _, userId := range userList { + err := common.RedisSet(fmt.Sprintf("user_group:%d", userId), "default", time.Duration(UserId2GroupCacheSeconds)*time.Second) + if err != nil { + log.Printf("更新用户: %d, 权益缓存失败, Error: %v", userId, err) + } + } + } +} diff --git a/web/berry/package.json b/web/berry/package.json index f8265ef7bb..f82aa0897c 100644 --- a/web/berry/package.json +++ b/web/berry/package.json @@ -17,6 +17,7 @@ "@tabler/icons-react": "^2.44.0", "apexcharts": "3.35.3", "axios": "^0.27.2", + "date-fns": "^3.6.0", "dayjs": "^1.11.10", "formik": "^2.2.9", "framer-motion": "^6.3.16", @@ -27,6 +28,7 @@ "prop-types": "^15.8.1", "react": "^18.2.0", "react-apexcharts": "1.4.0", + "react-datepicker": "^7.3.0", "react-device-detect": "^2.2.2", "react-dom": "^18.2.0", "react-perfect-scrollbar": "^1.5.8", diff --git a/web/berry/src/views/User/component/EditModal.js b/web/berry/src/views/User/component/EditModal.js index f6b533e2d1..16dcc2f0cf 100644 --- a/web/berry/src/views/User/component/EditModal.js +++ b/web/berry/src/views/User/component/EditModal.js @@ -2,7 +2,9 @@ import PropTypes from 'prop-types'; import * as Yup from 'yup'; import { Formik } from 'formik'; import { useTheme } from '@mui/material/styles'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; +import { format } from 'date-fns'; + import { Dialog, DialogTitle, @@ -17,7 +19,11 @@ import { Select, MenuItem, IconButton, - FormHelperText + FormHelperText, + TextField, + Typography, + Switch, + FormControlLabel } from '@mui/material'; import Visibility from '@mui/icons-material/Visibility'; @@ -44,6 +50,17 @@ const validationSchema = Yup.object().shape({ is: false, then: Yup.number().min(0, '额度 不能小于 0'), otherwise: Yup.number() + }), + expiration_date: Yup.mixed().when('group', { + is: (group) => group !== 'default', + then: Yup.mixed().test( + 'expiration_date-required', + '到期时间 不能为空', + function (value) { + const { expiration_date } = this.parent; + return expiration_date === -1 || !!expiration_date; + } + ), }) }); @@ -53,7 +70,8 @@ const originInputs = { display_name: '', password: '', group: 'default', - quota: 0 + quota: 0, + expiration_date: null }; const EditModal = ({ open, userId, onCancel, onOk }) => { @@ -65,6 +83,12 @@ const EditModal = ({ open, userId, onCancel, onOk }) => { const submit = async (values, { setErrors, setStatus, setSubmitting }) => { setSubmitting(true); + // 将到期时间转换为 Unix 时间戳 + if (values.expiration_date && values.expiration_date !== -1) { + const date = new Date(values.expiration_date); + values.expiration_date = Math.floor(date.getTime() / 1000); // 转换为秒级的 Unix 时间戳 + } + let res; if (values.is_edit) { res = await API.put(`/api/user/`, { ...values, id: parseInt(userId) }); @@ -95,16 +119,23 @@ const EditModal = ({ open, userId, onCancel, onOk }) => { event.preventDefault(); }; - const loadUser = async () => { + const loadUser = useCallback(async () => { let res = await API.get(`/api/user/${userId}`); const { success, message, data } = res.data; if (success) { data.is_edit = true; + + // 将 Unix 时间戳转换为日期字符串 + if (data.expiration_date && data.expiration_date !== -1) { + const date = new Date(data.expiration_date * 1000); // 转换为毫秒级的时间戳 + data.expiration_date = format(date, 'yyyy-MM-dd'); // 格式化为 date 格式 + } + setInputs(data); } else { showError(message); } - }; + }, [userId]); const fetchGroups = async () => { try { @@ -122,159 +153,203 @@ const EditModal = ({ open, userId, onCancel, onOk }) => { } else { setInputs(originInputs); } - }, [userId]); + }, [userId, loadUser]); return ( - - - {userId ? '编辑用户' : '新建用户'} - - - - - {({ errors, handleBlur, handleChange, handleSubmit, touched, values, isSubmitting }) => ( -
- - 用户名 - - {touched.username && errors.username && ( - - {errors.username} - - )} - - - - 显示名称 - - {touched.display_name && errors.display_name && ( - - {errors.display_name} - - )} - - - - 密码 - - - {showPassword ? : } - - - } - aria-describedby="helper-text-channel-password-label" - /> - {touched.password && errors.password && ( - - {errors.password} - - )} - - - {values.is_edit && ( - <> - - 额度 + + + {userId ? '编辑用户' : '新建用户'} + + + + + {({ errors, handleBlur, handleChange, handleSubmit, setFieldValue, touched, values, isSubmitting }) => ( + + + 用户名 {renderQuotaWithPrompt(values.quota)}} - onBlur={handleBlur} - onChange={handleChange} - aria-describedby="helper-text-channel-quota-label" - disabled={values.unlimited_quota} + id="channel-username-label" + label="用户名" + type="text" + value={values.username} + name="username" + onBlur={handleBlur} + onChange={handleChange} + inputProps={{ autoComplete: 'username' }} + aria-describedby="helper-text-channel-username-label" /> + {touched.username && errors.username && ( + + {errors.username} + + )} + - {touched.quota && errors.quota && ( - - {errors.quota} - + + 显示名称 + + {touched.display_name && errors.display_name && ( + + {errors.display_name} + )} - - 分组 - - {touched.group && errors.group && ( - - {errors.group} - + aria-describedby="helper-text-channel-password-label" + /> + {touched.password && errors.password && ( + + {errors.password} + )} - - )} - - - - - - )} - - - + + {values.is_edit && ( + <> + + 额度 + {renderQuotaWithPrompt(values.quota)}} + onBlur={handleBlur} + onChange={handleChange} + aria-describedby="helper-text-channel-quota-label" + disabled={values.unlimited_quota} + /> + + {touched.quota && errors.quota && ( + + {errors.quota} + + )} + + + + 分组 + + {touched.group && errors.group && ( + + {errors.group} + + )} + + + )} + + {values.group !== 'default' && ( + + + {touched.expiration_date && errors.expiration_date && ( + + {errors.expiration_date} + + )} + setFieldValue('expiration_date', e.target.checked ? -1 : '')} + name="permanent" + color="primary" + /> + } + label="永不过期" + /> + + )} + + + + + + + )} +
+
+
); }; @@ -285,4 +360,4 @@ EditModal.propTypes = { userId: PropTypes.number, onCancel: PropTypes.func, onOk: PropTypes.func -}; +}; \ No newline at end of file