diff --git a/backend/internal/database/database.go b/backend/internal/database/database.go index 21f294d..5461b81 100644 --- a/backend/internal/database/database.go +++ b/backend/internal/database/database.go @@ -165,6 +165,8 @@ func initMySQL() { &model.ExtensionAPIKey{}, &model.DynamicCode{}, &model.PaymentChannel{}, + &model.EmailConfig{}, + &model.StorageConfig{}, &model.Captcha{}, &model.ApiUsage{}, &model.StorageUsage{}, @@ -366,7 +368,22 @@ func initData() { DB.Where("app_id IS NULL").Delete(&model.CloudVariable{}) } - // 初始化文档分类和文档 + var localStorageCount int64 + DB.Model(&model.StorageConfig{}).Where("type = ?", "local").Count(&localStorageCount) + if localStorageCount == 0 { + log.Println("Creating default local storage config...") + defaultStorage := model.StorageConfig{ + Name: "本地存储", + Type: "local", + IsDefault: true, + Status: "active", + Remark: "系统默认本地存储", + } + DB.Create(&defaultStorage) + } else { + DB.Model(&model.StorageConfig{}).Where("type = ? AND (status = '' OR status IS NULL)", "local").Update("status", "active") + } + initDocData() } diff --git a/backend/internal/model/models.go b/backend/internal/model/models.go index a22b901..8fe1595 100644 --- a/backend/internal/model/models.go +++ b/backend/internal/model/models.go @@ -80,6 +80,41 @@ type PaymentChannel struct { DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` } +type EmailConfig struct { + ID uint `gorm:"primaryKey" json:"id"` + Name string `gorm:"size:100;not null" json:"name"` + SMTPHost string `gorm:"size:100;not null" json:"smtp_host"` + SMTPPort int `gorm:"not null" json:"smtp_port"` + SMTPUser string `gorm:"size:100;not null" json:"smtp_user"` + SMTPPassword string `gorm:"size:255" json:"smtp_password"` + SMTPFrom string `gorm:"size:100;not null" json:"smtp_from"` + SMTPFromName string `gorm:"size:100" json:"smtp_from_name"` + Encryption string `gorm:"size:20;default:tls" json:"encryption"` + Status string `gorm:"size:20;default:active" json:"status"` + Remark string `gorm:"size:500" json:"remark"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` +} + +type StorageConfig struct { + ID uint `gorm:"primaryKey" json:"id"` + Name string `gorm:"size:100;not null" json:"name"` + Type string `gorm:"size:20;not null" json:"type"` + Endpoint string `gorm:"size:255" json:"endpoint"` + Bucket string `gorm:"size:100" json:"bucket"` + AccessKey string `gorm:"size:255" json:"access_key"` + SecretKey string `gorm:"size:255" json:"secret_key"` + Region string `gorm:"size:50" json:"region"` + PathPrefix string `gorm:"size:255" json:"path_prefix"` + IsDefault bool `gorm:"default:false" json:"is_default"` + Status string `gorm:"size:20;default:active" json:"status"` + Remark string `gorm:"size:500" json:"remark"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` +} + // DynamicCode 动态代码模型 type DynamicCode struct { ID uint `gorm:"primaryKey" json:"id"` diff --git a/backend/internal/router/admin/developer.go b/backend/internal/router/admin/developer.go index 5ea8f2a..76a06ab 100644 --- a/backend/internal/router/admin/developer.go +++ b/backend/internal/router/admin/developer.go @@ -1,4 +1,4 @@ -package admin +package admin import ( "github.com/gin-gonic/gin" @@ -24,6 +24,9 @@ func SetupRoutes(r *gin.RouterGroup) { SetupVersionRoutes(r) SetupProfileRoutes(r) SetupEmailRoutes(r) + SetupSystemSettingsRoutes(r) + SetupEmailConfigRoutes(r) + SetupStorageConfigRoutes(r) } func SetupRoutesWithoutPackage(r *gin.RouterGroup) { diff --git a/backend/internal/router/admin/email_config.go b/backend/internal/router/admin/email_config.go new file mode 100644 index 0000000..764a02e --- /dev/null +++ b/backend/internal/router/admin/email_config.go @@ -0,0 +1,303 @@ +package admin + +import ( + "fmt" + "net/http" + "verification-platform-backend/internal/database" + "verification-platform-backend/internal/model" + "verification-platform-backend/internal/service" + + "github.com/gin-gonic/gin" +) + +func SetupEmailConfigRoutes(r *gin.RouterGroup) { + emailConfigs := r.Group("/email-configs") + { + emailConfigs.GET("", handleGetSystemEmailConfigs) + emailConfigs.POST("", handleCreateSystemEmailConfig) + emailConfigs.GET("/:id", handleGetSystemEmailConfig) + emailConfigs.PUT("/:id", handleUpdateSystemEmailConfig) + emailConfigs.DELETE("/:id", handleDeleteSystemEmailConfig) + emailConfigs.PUT("/:id/status", handleUpdateSystemEmailConfigStatus) + emailConfigs.POST("/:id/test", handleTestSystemEmailConfig) + } +} + +func handleGetSystemEmailConfigs(c *gin.Context) { + var configs []model.EmailConfig + database.DB.Order("id desc").Find(&configs) + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "data": gin.H{ + "email_configs": configs, + }, + }) +} + +type CreateSystemEmailConfigRequest struct { + Name string `json:"name" binding:"required"` + SMTPHost string `json:"smtp_host" binding:"required"` + SMTPPort int `json:"smtp_port" binding:"required"` + SMTPUser string `json:"smtp_user" binding:"required"` + SMTPPassword string `json:"smtp_password"` + SMTPFrom string `json:"smtp_from" binding:"required"` + SMTPFromName string `json:"smtp_from_name"` + Encryption string `json:"encryption"` + Status string `json:"status"` + Remark string `json:"remark"` +} + +func handleCreateSystemEmailConfig(c *gin.Context) { + var req CreateSystemEmailConfigRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "无效的请求数据", + }) + return + } + + status := "active" + if req.Status != "" { + status = req.Status + } + + encryption := "tls" + if req.Encryption != "" { + encryption = req.Encryption + } + + config := model.EmailConfig{ + Name: req.Name, + SMTPHost: req.SMTPHost, + SMTPPort: req.SMTPPort, + SMTPUser: req.SMTPUser, + SMTPPassword: req.SMTPPassword, + SMTPFrom: req.SMTPFrom, + SMTPFromName: req.SMTPFromName, + Encryption: encryption, + Status: status, + Remark: req.Remark, + } + + if err := database.DB.Create(&config).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "message": "创建失败", + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "message": "创建成功", + "data": config, + }) +} + +func handleGetSystemEmailConfig(c *gin.Context) { + id := c.Param("id") + + var config model.EmailConfig + if err := database.DB.First(&config, id).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{ + "code": 404, + "message": "邮箱配置不存在", + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "data": config, + }) +} + +type UpdateSystemEmailConfigRequest struct { + Name string `json:"name"` + SMTPHost string `json:"smtp_host"` + SMTPPort int `json:"smtp_port"` + SMTPUser string `json:"smtp_user"` + SMTPPassword string `json:"smtp_password"` + SMTPFrom string `json:"smtp_from"` + SMTPFromName string `json:"smtp_from_name"` + Encryption string `json:"encryption"` + Remark string `json:"remark"` +} + +func handleUpdateSystemEmailConfig(c *gin.Context) { + id := c.Param("id") + + var config model.EmailConfig + if err := database.DB.First(&config, id).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{ + "code": 404, + "message": "邮箱配置不存在", + }) + return + } + + var req UpdateSystemEmailConfigRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "无效的请求数据", + }) + return + } + + if req.Name != "" { + config.Name = req.Name + } + if req.SMTPHost != "" { + config.SMTPHost = req.SMTPHost + } + if req.SMTPPort != 0 { + config.SMTPPort = req.SMTPPort + } + if req.SMTPUser != "" { + config.SMTPUser = req.SMTPUser + } + if req.SMTPPassword != "" { + config.SMTPPassword = req.SMTPPassword + } + if req.SMTPFrom != "" { + config.SMTPFrom = req.SMTPFrom + } + config.SMTPFromName = req.SMTPFromName + if req.Encryption != "" { + config.Encryption = req.Encryption + } + config.Remark = req.Remark + + if err := database.DB.Save(&config).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "message": "更新失败", + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "message": "更新成功", + "data": config, + }) +} + +func handleDeleteSystemEmailConfig(c *gin.Context) { + id := c.Param("id") + + var config model.EmailConfig + if err := database.DB.First(&config, id).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{ + "code": 404, + "message": "邮箱配置不存在", + }) + return + } + + if err := database.DB.Delete(&config).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "message": "删除失败", + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "message": "删除成功", + }) +} + +type UpdateSystemEmailConfigStatusRequest struct { + Status string `json:"status" binding:"required"` +} + +func handleUpdateSystemEmailConfigStatus(c *gin.Context) { + id := c.Param("id") + + var config model.EmailConfig + if err := database.DB.First(&config, id).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{ + "code": 404, + "message": "邮箱配置不存在", + }) + return + } + + var req UpdateSystemEmailConfigStatusRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "无效的请求数据", + }) + return + } + + config.Status = req.Status + if err := database.DB.Save(&config).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "message": "更新失败", + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "message": "更新成功", + }) +} + +type TestSystemEmailConfigRequest struct { + Email string `json:"email" binding:"required,email"` +} + +func handleTestSystemEmailConfig(c *gin.Context) { + id := c.Param("id") + + var config model.EmailConfig + if err := database.DB.First(&config, id).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{ + "code": 404, + "message": "邮箱配置不存在", + }) + return + } + + var req TestSystemEmailConfigRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "请输入有效的邮箱地址", + }) + return + } + + emailService := service.NewEmailService() + emailConfig := service.EmailConfig{ + Host: config.SMTPHost, + Port: config.SMTPPort, + User: config.SMTPUser, + Password: config.SMTPPassword, + FromEmail: config.SMTPFrom, + FromName: config.SMTPFromName, + UseSSL: config.Encryption == "ssl", + } + + if err := emailService.SendTestEmail(emailConfig, req.Email, config.Name); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "message": fmt.Sprintf("发送失败: %v", err), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "message": "测试邮件已发送", + }) +} diff --git a/backend/internal/router/admin/storage_config.go b/backend/internal/router/admin/storage_config.go new file mode 100644 index 0000000..e74b02b --- /dev/null +++ b/backend/internal/router/admin/storage_config.go @@ -0,0 +1,319 @@ +package admin + +import ( + "net/http" + "verification-platform-backend/internal/database" + "verification-platform-backend/internal/model" + + "github.com/gin-gonic/gin" +) + +func SetupStorageConfigRoutes(r *gin.RouterGroup) { + storageConfigs := r.Group("/storage-configs") + { + storageConfigs.GET("", handleGetStorageConfigs) + storageConfigs.POST("", handleCreateStorageConfig) + storageConfigs.POST("/test", handleTestStorageConfigNew) + storageConfigs.GET("/:id", handleGetStorageConfig) + storageConfigs.PUT("/:id", handleUpdateStorageConfig) + storageConfigs.DELETE("/:id", handleDeleteStorageConfig) + storageConfigs.PUT("/:id/status", handleUpdateStorageConfigStatus) + storageConfigs.PUT("/:id/default", handleSetDefaultStorageConfig) + storageConfigs.POST("/:id/test", handleTestStorageConfig) + } +} + +func handleGetStorageConfigs(c *gin.Context) { + var configs []model.StorageConfig + database.DB.Order("id desc").Find(&configs) + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "data": gin.H{ + "storage_configs": configs, + }, + }) +} + +type CreateStorageConfigRequest struct { + Name string `json:"name" binding:"required"` + Type string `json:"type" binding:"required"` + Endpoint string `json:"endpoint"` + Bucket string `json:"bucket"` + AccessKey string `json:"access_key"` + SecretKey string `json:"secret_key"` + Region string `json:"region"` + PathPrefix string `json:"path_prefix"` + IsDefault bool `json:"is_default"` + Remark string `json:"remark"` +} + +func handleCreateStorageConfig(c *gin.Context) { + var req CreateStorageConfigRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "无效的请求数据", + }) + return + } + + if req.IsDefault { + database.DB.Model(&model.StorageConfig{}).Where("is_default = ?", true).Update("is_default", false) + } + + config := model.StorageConfig{ + Name: req.Name, + Type: req.Type, + Endpoint: req.Endpoint, + Bucket: req.Bucket, + AccessKey: req.AccessKey, + SecretKey: req.SecretKey, + Region: req.Region, + PathPrefix: req.PathPrefix, + IsDefault: req.IsDefault, + Status: "active", + Remark: req.Remark, + } + + if err := database.DB.Create(&config).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "message": "创建失败", + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "message": "创建成功", + "data": config, + }) +} + +func handleGetStorageConfig(c *gin.Context) { + id := c.Param("id") + + var config model.StorageConfig + if err := database.DB.First(&config, id).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{ + "code": 404, + "message": "存储配置不存在", + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "data": config, + }) +} + +type UpdateStorageConfigRequest struct { + Name string `json:"name"` + Type string `json:"type"` + Endpoint string `json:"endpoint"` + Bucket string `json:"bucket"` + AccessKey string `json:"access_key"` + SecretKey string `json:"secret_key"` + Region string `json:"region"` + PathPrefix string `json:"path_prefix"` + IsDefault bool `json:"is_default"` + Status string `json:"status"` + Remark string `json:"remark"` +} + +func handleUpdateStorageConfig(c *gin.Context) { + id := c.Param("id") + + var config model.StorageConfig + if err := database.DB.First(&config, id).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{ + "code": 404, + "message": "存储配置不存在", + }) + return + } + + var req UpdateStorageConfigRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "无效的请求数据", + }) + return + } + + if req.IsDefault && !config.IsDefault { + database.DB.Model(&model.StorageConfig{}).Where("is_default = ?", true).Update("is_default", false) + } + + if req.Name != "" { + config.Name = req.Name + } + if req.Type != "" { + config.Type = req.Type + } + config.Endpoint = req.Endpoint + config.Bucket = req.Bucket + config.AccessKey = req.AccessKey + config.SecretKey = req.SecretKey + config.Region = req.Region + config.PathPrefix = req.PathPrefix + config.IsDefault = req.IsDefault + if req.Status != "" { + config.Status = req.Status + } + config.Remark = req.Remark + + if err := database.DB.Save(&config).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "message": "更新失败", + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "message": "更新成功", + "data": config, + }) +} + +func handleDeleteStorageConfig(c *gin.Context) { + id := c.Param("id") + + var config model.StorageConfig + if err := database.DB.First(&config, id).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{ + "code": 404, + "message": "存储配置不存在", + }) + return + } + + if config.Type == "local" { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "本地存储不能删除", + }) + return + } + + if err := database.DB.Delete(&config).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "message": "删除失败", + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "message": "删除成功", + }) +} + +type UpdateStorageConfigStatusRequest struct { + Status string `json:"status" binding:"required"` +} + +func handleUpdateStorageConfigStatus(c *gin.Context) { + id := c.Param("id") + + var config model.StorageConfig + if err := database.DB.First(&config, id).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{ + "code": 404, + "message": "存储配置不存在", + }) + return + } + + var req UpdateStorageConfigStatusRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "无效的请求数据", + }) + return + } + + config.Status = req.Status + if err := database.DB.Save(&config).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "message": "更新失败", + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "message": "更新成功", + }) +} + +func handleSetDefaultStorageConfig(c *gin.Context) { + id := c.Param("id") + + var config model.StorageConfig + if err := database.DB.First(&config, id).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{ + "code": 404, + "message": "存储配置不存在", + }) + return + } + + database.DB.Model(&model.StorageConfig{}).Where("is_default = ?", true).Update("is_default", false) + + config.IsDefault = true + if err := database.DB.Save(&config).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "message": "设置失败", + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "message": "设置成功", + }) +} + +func handleTestStorageConfigNew(c *gin.Context) { + var req CreateStorageConfigRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "无效的请求数据", + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "message": "连接成功", + }) +} + +func handleTestStorageConfig(c *gin.Context) { + id := c.Param("id") + + var config model.StorageConfig + if err := database.DB.First(&config, id).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{ + "code": 404, + "message": "存储配置不存在", + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "message": "连接成功", + }) +} diff --git a/backend/internal/router/admin/system_settings.go b/backend/internal/router/admin/system_settings.go new file mode 100644 index 0000000..21b396c --- /dev/null +++ b/backend/internal/router/admin/system_settings.go @@ -0,0 +1,684 @@ +package admin + +import ( + "fmt" + "io" + "log" + "net/http" + "os" + "path/filepath" + "strings" + "time" + "verification-platform-backend/internal/database" + "verification-platform-backend/internal/model" + + "github.com/gin-gonic/gin" +) + +func SetupSystemSettingsRoutes(r *gin.RouterGroup) { + settings := r.Group("/system-settings") + { + settings.GET("", handleGetSystemSettings) + settings.PUT("", handleUpdateSystemSettings) + settings.POST("/upload", handleUploadSystemImage) + settings.GET("/payment", handleGetPaymentSettings) + settings.PUT("/payment", handleUpdatePaymentSettings) + settings.GET("/email", handleGetEmailSettings) + settings.PUT("/email", handleUpdateEmailSettings) + } + + paymentChannels := r.Group("/payment-channels") + { + paymentChannels.GET("", handleGetPaymentChannels) + paymentChannels.POST("", handleCreatePaymentChannel) + paymentChannels.GET("/:id", handleGetPaymentChannel) + paymentChannels.PUT("/:id", handleUpdatePaymentChannel) + paymentChannels.DELETE("/:id", handleDeletePaymentChannel) + paymentChannels.PUT("/:id/status", handleUpdatePaymentChannelStatus) + } +} + +type SystemSettingsResponse struct { + SiteName string `json:"site_name"` + SiteLogo string `json:"site_logo"` + SiteFavicon string `json:"site_favicon"` + SiteFooter string `json:"site_footer"` + + EnableCaptcha bool `json:"enable_captcha"` + LoginFailLockCount int `json:"login_fail_lock_count"` + LoginFailLockMinutes int `json:"login_fail_lock_minutes"` + PasswordMinLength int `json:"password_min_length"` + SessionTimeout int `json:"session_timeout"` + + EnableBackup bool `json:"enable_backup"` + BackupInterval int `json:"backup_interval"` + BackupRetention int `json:"backup_retention"` + BackupStorageType string `json:"backup_storage_type"` + + EnableTicketSystem bool `json:"enable_ticket_system"` + DefaultTheme string `json:"default_theme"` + EnableMultiLang bool `json:"enable_multi_lang"` + + EnableNotification bool `json:"enable_notification"` + AdminNotifyEmail string `json:"admin_notify_email"` + NotifyOnLogin bool `json:"notify_on_login"` + NotifyOnRecharge bool `json:"notify_on_recharge"` + NotifyOnTicket bool `json:"notify_on_ticket"` +} + +type PaymentSettingsResponse struct { + EnabledPaymentTypes []string `json:"enabled_payment_types"` + PaymentChannels []PaymentChannel `json:"payment_channels"` +} + +type PaymentChannel struct { + ID uint `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Icon string `json:"icon"` + Status string `json:"status"` + Sort int `json:"sort"` +} + +type EmailSettingsResponse struct { + SMTPHost string `json:"smtp_host"` + SMTPPort int `json:"smtp_port"` + SMTPUser string `json:"smtp_user"` + SMTPPassword string `json:"smtp_password"` + SMTPFrom string `json:"smtp_from"` + Enabled bool `json:"enabled"` +} + +func handleGetSystemSettings(c *gin.Context) { + var settings []model.Setting + database.DB.Where("category = ?", "system").Find(&settings) + + response := SystemSettingsResponse{ + SiteName: "验证平台", + SiteLogo: "", + SiteFavicon: "", + SiteFooter: "", + EnableCaptcha: true, + LoginFailLockCount: 5, + LoginFailLockMinutes: 30, + PasswordMinLength: 6, + SessionTimeout: 24, + EnableBackup: false, + BackupInterval: 24, + BackupRetention: 7, + BackupStorageType: "local", + EnableTicketSystem: true, + DefaultTheme: "system", + EnableMultiLang: false, + EnableNotification: false, + AdminNotifyEmail: "", + NotifyOnLogin: false, + NotifyOnRecharge: true, + NotifyOnTicket: true, + } + + for _, s := range settings { + switch s.Key { + case "site_name": + response.SiteName = s.Value + case "site_logo": + response.SiteLogo = s.Value + case "site_favicon": + response.SiteFavicon = s.Value + case "site_footer": + response.SiteFooter = s.Value + case "enable_captcha": + response.EnableCaptcha = s.Value == "true" + case "login_fail_lock_count": + response.LoginFailLockCount = parseSettingInt(s.Value, 5) + case "login_fail_lock_minutes": + response.LoginFailLockMinutes = parseSettingInt(s.Value, 30) + case "password_min_length": + response.PasswordMinLength = parseSettingInt(s.Value, 6) + case "session_timeout": + response.SessionTimeout = parseSettingInt(s.Value, 24) + case "enable_backup": + response.EnableBackup = s.Value == "true" + case "backup_interval": + response.BackupInterval = parseSettingInt(s.Value, 24) + case "backup_retention": + response.BackupRetention = parseSettingInt(s.Value, 7) + case "backup_storage_type": + response.BackupStorageType = s.Value + case "enable_ticket_system": + response.EnableTicketSystem = s.Value == "true" + case "default_theme": + response.DefaultTheme = s.Value + case "enable_multi_lang": + response.EnableMultiLang = s.Value == "true" + case "enable_notification": + response.EnableNotification = s.Value == "true" + case "admin_notify_email": + response.AdminNotifyEmail = s.Value + case "notify_on_login": + response.NotifyOnLogin = s.Value == "true" + case "notify_on_recharge": + response.NotifyOnRecharge = s.Value == "true" + case "notify_on_ticket": + response.NotifyOnTicket = s.Value == "true" + } + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "data": response, + }) +} + +func parseSettingInt(value string, defaultValue int) int { + if value == "" { + return defaultValue + } + var result int + if _, err := fmt.Sscanf(value, "%d", &result); err != nil { + return defaultValue + } + return result +} + +func handleUpdateSystemSettings(c *gin.Context) { + var req SystemSettingsResponse + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "无效的请求数据", + }) + return + } + + settings := []struct { + Key string + Value string + }{ + {"site_name", req.SiteName}, + {"site_logo", req.SiteLogo}, + {"site_favicon", req.SiteFavicon}, + {"site_footer", req.SiteFooter}, + {"enable_captcha", fmt.Sprintf("%v", req.EnableCaptcha)}, + {"login_fail_lock_count", fmt.Sprintf("%d", req.LoginFailLockCount)}, + {"login_fail_lock_minutes", fmt.Sprintf("%d", req.LoginFailLockMinutes)}, + {"password_min_length", fmt.Sprintf("%d", req.PasswordMinLength)}, + {"session_timeout", fmt.Sprintf("%d", req.SessionTimeout)}, + {"enable_backup", fmt.Sprintf("%v", req.EnableBackup)}, + {"backup_interval", fmt.Sprintf("%d", req.BackupInterval)}, + {"backup_retention", fmt.Sprintf("%d", req.BackupRetention)}, + {"backup_storage_type", req.BackupStorageType}, + {"enable_ticket_system", fmt.Sprintf("%v", req.EnableTicketSystem)}, + {"default_theme", req.DefaultTheme}, + {"enable_multi_lang", fmt.Sprintf("%v", req.EnableMultiLang)}, + {"enable_notification", fmt.Sprintf("%v", req.EnableNotification)}, + {"admin_notify_email", req.AdminNotifyEmail}, + {"notify_on_login", fmt.Sprintf("%v", req.NotifyOnLogin)}, + {"notify_on_recharge", fmt.Sprintf("%v", req.NotifyOnRecharge)}, + {"notify_on_ticket", fmt.Sprintf("%v", req.NotifyOnTicket)}, + } + + for _, s := range settings { + var setting model.Setting + result := database.DB.Where("category = ? AND key = ?", "system", s.Key).First(&setting) + if result.Error == nil { + setting.Value = s.Value + database.DB.Save(&setting) + } else { + setting = model.Setting{ + Category: "system", + Key: s.Key, + Value: s.Value, + } + database.DB.Create(&setting) + } + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "message": "保存成功", + }) +} + +func handleUploadSystemImage(c *gin.Context) { + file, header, err := c.Request.FormFile("file") + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "请选择要上传的文件", + }) + return + } + defer file.Close() + + uploadType := c.PostForm("type") + if uploadType != "logo" && uploadType != "favicon" { + uploadType = "logo" + } + + ext := strings.ToLower(filepath.Ext(header.Filename)) + allowedExts := map[string]bool{ + ".jpg": true, + ".jpeg": true, + ".png": true, + ".gif": true, + ".webp": true, + ".svg": true, + ".ico": true, + } + + if !allowedExts[ext] { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "不支持的文件格式", + }) + return + } + + const maxSize = 2 * 1024 * 1024 + if header.Size > maxSize { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "文件大小不能超过2MB", + }) + return + } + + uploadDir := "uploads/system" + if err := os.MkdirAll(uploadDir, 0755); err != nil { + log.Printf("创建上传目录失败: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "message": "创建上传目录失败", + }) + return + } + + filename := fmt.Sprintf("%s_%d%s", uploadType, time.Now().UnixNano(), ext) + filePath := filepath.Join(uploadDir, filename) + + dst, err := os.Create(filePath) + if err != nil { + log.Printf("创建文件失败: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "message": "创建文件失败", + }) + return + } + defer dst.Close() + + if _, err := io.Copy(dst, file); err != nil { + log.Printf("保存文件失败: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "message": "保存文件失败", + }) + return + } + + imageURL := "/uploads/system/" + filename + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "data": gin.H{ + "url": imageURL, + }, + }) +} + +func handleGetPaymentSettings(c *gin.Context) { + var channels []model.PaymentChannel + database.DB.Order("sort asc").Find(&channels) + + var paymentChannels []PaymentChannel + for _, ch := range channels { + paymentChannels = append(paymentChannels, PaymentChannel{ + ID: ch.ID, + Name: ch.Name, + Type: ch.Type, + Icon: ch.Icon, + Status: ch.Status, + Sort: ch.Sort, + }) + } + + var enabledTypes []string + var setting model.Setting + if err := database.DB.Where("category = ? AND key = ?", "payment", "enabled_types").First(&setting).Error; err == nil { + if setting.Value != "" { + enabledTypes = []string{setting.Value} + } + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "data": gin.H{ + "enabled_payment_types": enabledTypes, + "payment_channels": paymentChannels, + }, + }) +} + +func handleUpdatePaymentSettings(c *gin.Context) { + var req struct { + EnabledPaymentTypes []string `json:"enabled_payment_types"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "无效的请求数据", + }) + return + } + + enabledTypes := "" + if len(req.EnabledPaymentTypes) > 0 { + enabledTypes = req.EnabledPaymentTypes[0] + } + + var setting model.Setting + result := database.DB.Where("category = ? AND key = ?", "payment", "enabled_types").First(&setting) + if result.Error == nil { + setting.Value = enabledTypes + database.DB.Save(&setting) + } else { + setting = model.Setting{ + Category: "payment", + Key: "enabled_types", + Value: enabledTypes, + } + database.DB.Create(&setting) + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "message": "保存成功", + }) +} + +func handleGetEmailSettings(c *gin.Context) { + var settings []model.Setting + database.DB.Where("category = ?", "email").Find(&settings) + + response := EmailSettingsResponse{ + SMTPHost: "", + SMTPPort: 587, + SMTPUser: "", + SMTPPassword: "", + SMTPFrom: "", + Enabled: false, + } + + for _, s := range settings { + switch s.Key { + case "smtp_host": + response.SMTPHost = s.Value + case "smtp_port": + var port int + if _, err := fmt.Sscanf(s.Value, "%d", &port); err == nil { + response.SMTPPort = port + } + case "smtp_user": + response.SMTPUser = s.Value + case "smtp_password": + response.SMTPPassword = s.Value + case "smtp_from": + response.SMTPFrom = s.Value + case "enabled": + response.Enabled = s.Value == "true" + } + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "data": response, + }) +} + +func handleUpdateEmailSettings(c *gin.Context) { + var req EmailSettingsRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "无效的请求数据", + }) + return + } + + settings := []struct { + Key string + Value string + }{ + {"smtp_host", req.SMTPHost}, + {"smtp_port", fmt.Sprintf("%d", req.SMTPPort)}, + {"smtp_user", req.SMTPUser}, + {"smtp_password", req.SMTPPassword}, + {"smtp_from", req.SMTPFrom}, + {"enabled", fmt.Sprintf("%v", req.Enabled)}, + } + + for _, s := range settings { + var setting model.Setting + result := database.DB.Where("category = ? AND key = ?", "email", s.Key).First(&setting) + if result.Error == nil { + setting.Value = s.Value + database.DB.Save(&setting) + } else { + setting = model.Setting{ + Category: "email", + Key: s.Key, + Value: s.Value, + } + database.DB.Create(&setting) + } + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "message": "保存成功", + }) +} + +type EmailSettingsRequest struct { + SMTPHost string `json:"smtp_host"` + SMTPPort int `json:"smtp_port"` + SMTPUser string `json:"smtp_user"` + SMTPPassword string `json:"smtp_password"` + SMTPFrom string `json:"smtp_from"` + Enabled bool `json:"enabled"` +} + +func handleGetPaymentChannels(c *gin.Context) { + var channels []model.PaymentChannel + database.DB.Order("sort asc").Find(&channels) + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "data": channels, + }) +} + +type CreatePaymentChannelRequest struct { + Name string `json:"name" binding:"required"` + Type string `json:"type" binding:"required"` + Icon string `json:"icon"` + Config string `json:"config"` + Sort int `json:"sort"` + Remark string `json:"remark"` +} + +func handleCreatePaymentChannel(c *gin.Context) { + var req CreatePaymentChannelRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "无效的请求数据", + }) + return + } + + channel := model.PaymentChannel{ + Name: req.Name, + Type: req.Type, + Icon: req.Icon, + Config: req.Config, + Sort: req.Sort, + Status: "active", + Remark: req.Remark, + } + + if err := database.DB.Create(&channel).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "message": "创建失败", + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "message": "创建成功", + "data": channel, + }) +} + +func handleGetPaymentChannel(c *gin.Context) { + id := c.Param("id") + + var channel model.PaymentChannel + if err := database.DB.First(&channel, id).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{ + "code": 404, + "message": "支付渠道不存在", + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "data": channel, + }) +} + +type UpdatePaymentChannelRequest struct { + Name string `json:"name"` + Type string `json:"type"` + Icon string `json:"icon"` + Config string `json:"config"` + Sort int `json:"sort"` + Remark string `json:"remark"` +} + +func handleUpdatePaymentChannel(c *gin.Context) { + id := c.Param("id") + + var channel model.PaymentChannel + if err := database.DB.First(&channel, id).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{ + "code": 404, + "message": "支付渠道不存在", + }) + return + } + + var req UpdatePaymentChannelRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "无效的请求数据", + }) + return + } + + if req.Name != "" { + channel.Name = req.Name + } + if req.Type != "" { + channel.Type = req.Type + } + channel.Icon = req.Icon + channel.Config = req.Config + channel.Sort = req.Sort + channel.Remark = req.Remark + + if err := database.DB.Save(&channel).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "message": "更新失败", + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "message": "更新成功", + "data": channel, + }) +} + +func handleDeletePaymentChannel(c *gin.Context) { + id := c.Param("id") + + var channel model.PaymentChannel + if err := database.DB.First(&channel, id).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{ + "code": 404, + "message": "支付渠道不存在", + }) + return + } + + if err := database.DB.Delete(&channel).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "message": "删除失败", + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "message": "删除成功", + }) +} + +type UpdatePaymentChannelStatusRequest struct { + Status string `json:"status" binding:"required"` +} + +func handleUpdatePaymentChannelStatus(c *gin.Context) { + id := c.Param("id") + + var channel model.PaymentChannel + if err := database.DB.First(&channel, id).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{ + "code": 404, + "message": "支付渠道不存在", + }) + return + } + + var req UpdatePaymentChannelStatusRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "无效的请求数据", + }) + return + } + + channel.Status = req.Status + if err := database.DB.Save(&channel).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "message": "更新失败", + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "message": "更新成功", + }) +} diff --git a/backend/internal/service/email.go b/backend/internal/service/email.go index 9097524..d3c1aa0 100644 --- a/backend/internal/service/email.go +++ b/backend/internal/service/email.go @@ -41,14 +41,14 @@ func (s *EmailService) SendEmail(config EmailConfig, to, subject, body string) e auth := smtp.PlainAuth("", config.User, config.Password, config.Host) - if config.UseSSL || config.Port == 465 || config.Port == 587 { - return s.sendWithTLS(addr, auth, from, []string{to}, []byte(msg)) + if config.Port == 465 || config.UseSSL { + return s.sendWithImplicitTLS(addr, auth, from, []string{to}, []byte(msg)) } - return smtp.SendMail(addr, auth, from, []string{to}, []byte(msg)) + return s.sendWithSTARTTLS(addr, auth, from, []string{to}, []byte(msg)) } -func (s *EmailService) sendWithTLS(addr string, auth smtp.Auth, from string, to []string, msg []byte) error { +func (s *EmailService) sendWithImplicitTLS(addr string, auth smtp.Auth, from string, to []string, msg []byte) error { host := strings.Split(addr, ":")[0] tlsConfig := &tls.Config{ @@ -99,6 +99,62 @@ func (s *EmailService) sendWithTLS(addr string, auth smtp.Auth, from string, to return client.Quit() } +func (s *EmailService) sendWithSTARTTLS(addr string, auth smtp.Auth, from string, to []string, msg []byte) error { + host := strings.Split(addr, ":")[0] + + client, err := smtp.Dial(addr) + if err != nil { + return fmt.Errorf("连接SMTP服务器失败: %v", err) + } + defer client.Close() + + if err = client.Hello("localhost"); err != nil { + return fmt.Errorf("HELO命令失败: %v", err) + } + + tlsConfig := &tls.Config{ + InsecureSkipVerify: true, + ServerName: host, + } + + if ok, _ := client.Extension("STARTTLS"); ok { + if err = client.StartTLS(tlsConfig); err != nil { + return fmt.Errorf("STARTTLS失败: %v", err) + } + } + + if err = client.Auth(auth); err != nil { + return fmt.Errorf("SMTP认证失败: %v", err) + } + + if err = client.Mail(from); err != nil { + return fmt.Errorf("设置发件人失败: %v", err) + } + + for _, addr := range to { + if err = client.Rcpt(addr); err != nil { + return fmt.Errorf("设置收件人失败: %v", err) + } + } + + w, err := client.Data() + if err != nil { + return fmt.Errorf("准备邮件数据失败: %v", err) + } + + _, err = w.Write(msg) + if err != nil { + return fmt.Errorf("写入邮件内容失败: %v", err) + } + + err = w.Close() + if err != nil { + return fmt.Errorf("关闭邮件写入失败: %v", err) + } + + return client.Quit() +} + func (s *EmailService) buildMessage(fromName, from, to, subject, body string) string { msg := "" diff --git a/backend/scripts/check_code.go b/backend/scripts/check_code.go index 41dba00..aa8d76d 100644 --- a/backend/scripts/check_code.go +++ b/backend/scripts/check_code.go @@ -1,4 +1,4 @@ -package main +package main import ( "log" diff --git a/backend/scripts/check_code_raw.go b/backend/scripts/check_code_raw.go index 986fec9..f8865ee 100644 --- a/backend/scripts/check_code_raw.go +++ b/backend/scripts/check_code_raw.go @@ -1,4 +1,4 @@ -package main +package main import ( "fmt" diff --git a/backend/scripts/init_order_functions.go b/backend/scripts/init_order_functions.go index d582d71..dc82e1a 100644 --- a/backend/scripts/init_order_functions.go +++ b/backend/scripts/init_order_functions.go @@ -1,4 +1,4 @@ -package main +package main import ( "log" diff --git a/backend/scripts/translate_docs_i18n.go b/backend/scripts/translate_docs_i18n.go index 0717e69..52756e2 100644 --- a/backend/scripts/translate_docs_i18n.go +++ b/backend/scripts/translate_docs_i18n.go @@ -1,4 +1,4 @@ -package main +package main import ( "fmt" diff --git a/backend/scripts/update_doc_translations.go b/backend/scripts/update_doc_translations.go index 40bb375..28cec1f 100644 --- a/backend/scripts/update_doc_translations.go +++ b/backend/scripts/update_doc_translations.go @@ -1,4 +1,4 @@ -package main +package main import ( "fmt" diff --git a/frontend/src/components/admin-sidebar/index.vue b/frontend/src/components/admin-sidebar/index.vue index 00aac83..7d5275b 100644 --- a/frontend/src/components/admin-sidebar/index.vue +++ b/frontend/src/components/admin-sidebar/index.vue @@ -1,6 +1,6 @@ diff --git a/frontend/src/layouts/admin.vue b/frontend/src/layouts/admin.vue index 74baa2c..4766104 100644 --- a/frontend/src/layouts/admin.vue +++ b/frontend/src/layouts/admin.vue @@ -1,11 +1,12 @@