diff --git a/config/validator.go b/config/validator.go index b3e29db..8a54809 100644 --- a/config/validator.go +++ b/config/validator.go @@ -136,7 +136,7 @@ func setDefaults(config *AppConfig) { config.Database.MySQL.MaxOpenConns = 100 } if config.Database.SQLite.Path == "" { - config.Database.SQLite.Path = "./recharge.db" + config.Database.SQLite.Path = "./database.db" } // Redis默认值 diff --git a/controllers/admin/app.go b/controllers/admin/app.go index 6541493..6899749 100644 --- a/controllers/admin/app.go +++ b/controllers/admin/app.go @@ -801,16 +801,18 @@ func AppGetBindConfigHandler(w http.ResponseWriter, r *http.Request) { // 返回绑定配置信息 response := map[string]interface{}{ - "machine_code_verify": app.MachineCodeVerify, - "machine_code_option": app.MachineCodeOption, - "machine_code_free_count": app.MachineCodeFreeCount, - "machine_code_rebind_count": app.MachineCodeRebindCount, - "machine_code_rebind_deduct": app.MachineCodeRebindDeduct, - "ip_verify": app.IPVerify, - "ip_option": app.IPOption, - "ip_free_count": app.IPFreeCount, - "ip_rebind_count": app.IPRebindCount, - "ip_rebind_deduct": app.IPRebindDeduct, + "machine_code_verify": app.MachineCodeVerify, + "machine_code_rebind_enabled": app.MachineCodeRebindEnabled, + "machine_code_option": app.MachineCodeOption, + "machine_code_free_count": app.MachineCodeFreeCount, + "machine_code_rebind_count": app.MachineCodeRebindCount, + "machine_code_rebind_deduct": app.MachineCodeRebindDeduct, + "ip_verify": app.IPVerify, + "ip_rebind_enabled": app.IPRebindEnabled, + "ip_option": app.IPOption, + "ip_free_count": app.IPFreeCount, + "ip_rebind_count": app.IPRebindCount, + "ip_rebind_deduct": app.IPRebindDeduct, } w.Header().Set("Content-Type", "application/json") @@ -827,11 +829,13 @@ func AppUpdateBindConfigHandler(w http.ResponseWriter, r *http.Request) { var req struct { UUID string `json:"uuid"` MachineCodeVerify int `json:"machine_code_verify"` + MachineCodeRebindEnabled int `json:"machine_code_rebind_enabled"` MachineCodeOption int `json:"machine_code_option"` MachineCodeFreeCount int `json:"machine_code_free_count"` MachineCodeRebindCount int `json:"machine_code_rebind_count"` MachineCodeRebindDeduct int `json:"machine_code_rebind_deduct"` IPVerify int `json:"ip_verify"` + IPRebindEnabled int `json:"ip_rebind_enabled"` IPOption int `json:"ip_option"` IPFreeCount int `json:"ip_free_count"` IPRebindCount int `json:"ip_rebind_count"` @@ -911,6 +915,26 @@ func AppUpdateBindConfigHandler(w http.ResponseWriter, r *http.Request) { return } + if req.MachineCodeRebindEnabled < 0 || req.MachineCodeRebindEnabled > 1 { + response := map[string]interface{}{ + "code": 1, + "msg": "机器码重绑开关参数无效", + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + return + } + + if req.IPRebindEnabled < 0 || req.IPRebindEnabled > 1 { + response := map[string]interface{}{ + "code": 1, + "msg": "IP地址重绑开关参数无效", + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + return + } + if req.MachineCodeFreeCount < 0 || req.MachineCodeRebindCount < 0 || req.MachineCodeRebindDeduct < 0 || req.IPFreeCount < 0 || req.IPRebindCount < 0 || req.IPRebindDeduct < 0 { response := map[string]interface{}{ @@ -949,16 +973,18 @@ func AppUpdateBindConfigHandler(w http.ResponseWriter, r *http.Request) { // 更新绑定配置 updates := map[string]interface{}{ - "machine_code_verify": req.MachineCodeVerify, - "machine_code_option": req.MachineCodeOption, - "machine_code_free_count": req.MachineCodeFreeCount, - "machine_code_rebind_count": req.MachineCodeRebindCount, - "machine_code_rebind_deduct": req.MachineCodeRebindDeduct, - "ip_verify": req.IPVerify, - "ip_option": req.IPOption, - "ip_free_count": req.IPFreeCount, - "ip_rebind_count": req.IPRebindCount, - "ip_rebind_deduct": req.IPRebindDeduct, + "machine_code_verify": req.MachineCodeVerify, + "machine_code_rebind_enabled": req.MachineCodeRebindEnabled, + "machine_code_option": req.MachineCodeOption, + "machine_code_free_count": req.MachineCodeFreeCount, + "machine_code_rebind_count": req.MachineCodeRebindCount, + "machine_code_rebind_deduct": req.MachineCodeRebindDeduct, + "ip_verify": req.IPVerify, + "ip_rebind_enabled": req.IPRebindEnabled, + "ip_option": req.IPOption, + "ip_free_count": req.IPFreeCount, + "ip_rebind_count": req.IPRebindCount, + "ip_rebind_deduct": req.IPRebindDeduct, } if err := db.Model(&app).Updates(updates).Error; err != nil { @@ -980,3 +1006,248 @@ func AppUpdateBindConfigHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } + +// AppGetRegisterConfigHandler 获取应用注册配置 +func AppGetRegisterConfigHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + appUUID := r.URL.Query().Get("uuid") + if appUUID == "" { + response := map[string]interface{}{ + "code": 1, + "msg": "缺少应用UUID", + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + return + } + + // 验证UUID格式 + if _, err := uuid.Parse(appUUID); err != nil { + response := map[string]interface{}{ + "code": 1, + "msg": "无效的UUID格式", + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + return + } + + db, err := database.GetDB() + if err != nil { + logrus.WithError(err).Error("Failed to get database connection") + response := map[string]interface{}{ + "code": 1, + "msg": "数据库连接失败", + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + return + } + + var app models.App + if err := db.Where("uuid = ?", appUUID).First(&app).Error; err != nil { + logrus.WithError(err).Error("Failed to find app") + response := map[string]interface{}{ + "code": 1, + "msg": "应用不存在", + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + return + } + + // 返回注册配置信息 + response := map[string]interface{}{ + "register_enabled": app.RegisterEnabled, + "register_limit_enabled": app.RegisterLimitEnabled, + "register_limit_time": app.RegisterLimitTime, + "register_count": app.RegisterCount, + "trial_enabled": app.TrialEnabled, + "trial_limit_time": app.TrialLimitTime, + "trial_duration": app.TrialDuration, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +// AppUpdateRegisterConfigHandler 更新应用注册配置 +func AppUpdateRegisterConfigHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + var req struct { + UUID string `json:"uuid"` + RegisterEnabled int `json:"register_enabled"` + RegisterLimitEnabled int `json:"register_limit_enabled"` + RegisterLimitTime int `json:"register_limit_time"` + RegisterCount int `json:"register_count"` + TrialEnabled int `json:"trial_enabled"` + TrialLimitTime int `json:"trial_limit_time"` + TrialDuration int `json:"trial_duration"` + } + + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + logrus.WithError(err).Error("Failed to decode JSON request") + response := map[string]interface{}{ + "code": 1, + "msg": "无效的JSON格式", + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + return + } + + // 验证UUID + if req.UUID == "" { + response := map[string]interface{}{ + "code": 1, + "msg": "应用UUID不能为空", + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + return + } + + if _, err := uuid.Parse(req.UUID); err != nil { + response := map[string]interface{}{ + "code": 1, + "msg": "无效的UUID格式", + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + return + } + + // 验证参数范围 + if req.RegisterEnabled < 0 || req.RegisterEnabled > 1 { + response := map[string]interface{}{ + "code": 1, + "msg": "账号注册开关参数无效", + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + return + } + + if req.RegisterLimitEnabled < 0 || req.RegisterLimitEnabled > 1 { + response := map[string]interface{}{ + "code": 1, + "msg": "注册限制开关参数无效", + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + return + } + + if req.RegisterLimitTime < 0 || req.RegisterLimitTime > 1 { + response := map[string]interface{}{ + "code": 1, + "msg": "注册限制时间参数无效", + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + return + } + + if req.RegisterCount < 1 { + response := map[string]interface{}{ + "code": 1, + "msg": "注册次数必须大于0", + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + return + } + + if req.TrialEnabled < 0 || req.TrialEnabled > 1 { + response := map[string]interface{}{ + "code": 1, + "msg": "领取试用开关参数无效", + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + return + } + + if req.TrialLimitTime < 0 || req.TrialLimitTime > 1 { + response := map[string]interface{}{ + "code": 1, + "msg": "试用限制时间参数无效", + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + return + } + + if req.TrialDuration < 0 { + response := map[string]interface{}{ + "code": 1, + "msg": "试用时间不能为负数", + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + return + } + + db, err := database.GetDB() + if err != nil { + logrus.WithError(err).Error("Failed to get database connection") + response := map[string]interface{}{ + "code": 1, + "msg": "数据库连接失败", + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + return + } + + // 查找应用 + var app models.App + if err := db.Where("uuid = ?", req.UUID).First(&app).Error; err != nil { + logrus.WithError(err).Error("Failed to find app") + response := map[string]interface{}{ + "code": 1, + "msg": "应用不存在", + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + return + } + + // 更新注册配置 + updates := map[string]interface{}{ + "register_enabled": req.RegisterEnabled, + "register_limit_enabled": req.RegisterLimitEnabled, + "register_limit_time": req.RegisterLimitTime, + "register_count": req.RegisterCount, + "trial_enabled": req.TrialEnabled, + "trial_limit_time": req.TrialLimitTime, + "trial_duration": req.TrialDuration, + } + + if err := db.Model(&app).Updates(updates).Error; err != nil { + logrus.WithError(err).Error("Failed to update app register config") + response := map[string]interface{}{ + "code": 1, + "msg": "更新注册配置失败", + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + return + } + + response := map[string]interface{}{ + "code": 0, + "msg": "注册配置更新成功", + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} diff --git a/controllers/admin/captcha.go b/controllers/admin/captcha.go index 189554e..dc738b0 100644 --- a/controllers/admin/captcha.go +++ b/controllers/admin/captcha.go @@ -1,9 +1,10 @@ package admin import ( + "crypto/rand" "encoding/base64" "encoding/json" - "math/rand" + "math/big" "net/http" "strings" "time" @@ -14,6 +15,15 @@ import ( // 全局验证码存储器 var store = base64Captcha.DefaultMemStore +// secureRandomInt 生成安全的随机整数,范围 [0, max) +func secureRandomInt(max int) (int, error) { + n, err := rand.Int(rand.Reader, big.NewInt(int64(max))) + if err != nil { + return 0, err + } + return int(n.Int64()), nil +} + // CaptchaHandler 生成验证码图片 // GET /admin/captcha - 返回验证码图片 func CaptchaHandler(w http.ResponseWriter, r *http.Request) { @@ -23,8 +33,13 @@ func CaptchaHandler(w http.ResponseWriter, r *http.Request) { } // 随机生成4-6位长度 - // Go 1.20+ 无需手动设置随机种子,使用默认全局随机源即可 - captchaLength := 4 + rand.Intn(3) // 4-6位随机长度 + // 使用crypto/rand生成安全的随机数 + randomNum, err := secureRandomInt(3) + if err != nil { + http.Error(w, "生成随机数失败", http.StatusInternalServerError) + return + } + captchaLength := 4 + randomNum // 4-6位随机长度 // 配置验证码参数 - 使用字母数字混合 driver := base64Captcha.DriverString{ diff --git a/database/database.go b/database/database.go index c102713..abc00a7 100644 --- a/database/database.go +++ b/database/database.go @@ -78,7 +78,7 @@ func GetDB() (*gorm.DB, error) { func initSQLite() error { path := viper.GetString("database.sqlite.path") if path == "" { - path = "./recharge.db" + path = "./database.db" } dsn := fmt.Sprintf("file:%s?cache=shared&_busy_timeout=5000&_fk=1", path) db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{}) diff --git a/models/app.go b/models/app.go index 263dbf5..a9961ec 100644 --- a/models/app.go +++ b/models/app.go @@ -56,6 +56,8 @@ type App struct { // 机器码验证相关字段 // MachineCodeVerify:机器码验证(0=关闭,1=开启) MachineCodeVerify int `gorm:"default:0;not null;comment:机器码验证,0=关闭,1=开启" json:"machine_code_verify"` + // MachineCodeRebindEnabled:机器码重绑开关(0=关闭,1=开启) + MachineCodeRebindEnabled int `gorm:"default:0;not null;comment:机器码重绑开关,0=关闭,1=开启" json:"machine_code_rebind_enabled"` // MachineCodeOption:机器码选项(0=每天,1=永久) MachineCodeOption int `gorm:"default:0;not null;comment:机器码选项,0=每天,1=永久" json:"machine_code_option"` // MachineCodeFreeCount:机器码免费次数(默认0) @@ -68,6 +70,8 @@ type App struct { // IP地址验证相关字段 // IPVerify:IP地址验证(0=关闭,1=开启,2=开启(市),3=开启(省)) IPVerify int `gorm:"default:0;not null;comment:IP地址验证,0=关闭,1=开启,2=开启(市),3=开启(省)" json:"ip_verify"` + // IPRebindEnabled:IP地址重绑开关(0=关闭,1=开启) + IPRebindEnabled int `gorm:"default:0;not null;comment:IP地址重绑开关,0=关闭,1=开启" json:"ip_rebind_enabled"` // IPOption:IP地址选项(0=每天,1=永久) IPOption int `gorm:"default:0;not null;comment:IP地址选项,0=每天,1=永久" json:"ip_option"` // IPFreeCount:IP地址免费次数(默认0) @@ -77,6 +81,24 @@ type App struct { // IPRebindDeduct:IP地址重绑扣除(默认0,单位:分钟) IPRebindDeduct int `gorm:"default:0;not null;comment:IP地址重绑扣除,单位分钟" json:"ip_rebind_deduct"` + // 账号注册相关字段 + // RegisterEnabled:账号注册开关(0=关闭,1=开启) + RegisterEnabled int `gorm:"default:1;not null;comment:账号注册开关,0=关闭,1=开启" json:"register_enabled"` + // RegisterLimitEnabled:注册限制开关(0=关闭,1=开启) + RegisterLimitEnabled int `gorm:"default:0;not null;comment:注册限制开关,0=关闭,1=开启" json:"register_limit_enabled"` + // RegisterLimitTime:限制时间(0=每天,1=永久) + RegisterLimitTime int `gorm:"default:1;not null;comment:注册限制时间,0=每天,1=永久" json:"register_limit_time"` + // RegisterCount:注册次数 + RegisterCount int `gorm:"default:1;not null;comment:注册次数" json:"register_count"` + + // 领取试用相关字段 + // TrialEnabled:领取试用开关(0=关闭,1=开启) + TrialEnabled int `gorm:"default:0;not null;comment:领取试用开关,0=关闭,1=开启" json:"trial_enabled"` + // TrialLimitTime:试用限制时间(0=每天,1=永久) + TrialLimitTime int `gorm:"default:1;not null;comment:试用限制时间,0=每天,1=永久" json:"trial_limit_time"` + // TrialDuration:试用时间(单位:分钟) + TrialDuration int `gorm:"default:0;not null;comment:试用时间,单位分钟" json:"trial_duration"` + // CreatedAt/UpdatedAt:时间字段,返回为 created_at/updated_at,便于前端展示 CreatedAt time.Time `gorm:"comment:创建时间" json:"created_at"` UpdatedAt time.Time `gorm:"comment:更新时间" json:"updated_at"` diff --git a/server/admin.go b/server/admin.go index 818c12c..f050ce8 100644 --- a/server/admin.go +++ b/server/admin.go @@ -67,6 +67,8 @@ func RegisterAdminRoutes(mux *http.ServeMux) { mux.HandleFunc("/admin/api/apps/update_multi_config", adminctl.AdminAuthRequired(adminctl.AppUpdateMultiConfigHandler)) mux.HandleFunc("/admin/api/apps/get_bind_config", adminctl.AdminAuthRequired(adminctl.AppGetBindConfigHandler)) mux.HandleFunc("/admin/api/apps/update_bind_config", adminctl.AdminAuthRequired(adminctl.AppUpdateBindConfigHandler)) + mux.HandleFunc("/admin/api/apps/get_register_config", adminctl.AdminAuthRequired(adminctl.AppGetRegisterConfigHandler)) + mux.HandleFunc("/admin/api/apps/update_register_config", adminctl.AdminAuthRequired(adminctl.AppUpdateRegisterConfigHandler)) // 系统信息API(用于仪表盘定时刷新) diff --git a/web/template/admin/apps.html b/web/template/admin/apps.html index f19ba91..0da30c6 100644 --- a/web/template/admin/apps.html +++ b/web/template/admin/apps.html @@ -330,6 +330,10 @@ title: '绑定设置', id: 'bind_settings' }, + { + title: '注册设置', + id: 'register_settings' + }, { title: '重置密钥', id: 'reset_secret' @@ -578,6 +582,13 @@ '' + '' + '