package admin import ( "crypto/tls" "fmt" "net/smtp" "github.com/goravel/framework/contracts/http" "github.com/goravel/framework/facades" "github.com/goravel/framework/support/carbon" "github.com/spf13/cast" apperrors "goravel/app/errors" "goravel/app/http/response" "goravel/app/models" ) type ConfigController struct { } func NewConfigController() *ConfigController { return &ConfigController{} } // GetByGroup 根据分组获取配置 func (r *ConfigController) GetByGroup(ctx http.Context) http.Response { group := ctx.Request().Route("group") if group == "" { return response.Error(ctx, http.StatusBadRequest, apperrors.ErrConfigGroupRequired.Code) } var configs []models.Config // 查询配置,即使没有数据也返回空数组,不返回错误 _ = facades.Orm().Query().Where("group", group).Order("sort asc, id asc").Get(&configs) // 如果是邮箱配置分组,将密码字段的值设为空,不让前端看到 if group == "email" { for i := range configs { if configs[i].Key == "email_password" { configs[i].Value = "" } } } return response.Success(ctx, http.Json{ "configs": configs, }) } // Save 保存配置(按分组批量保存) func (r *ConfigController) Save(ctx http.Context) http.Response { group := ctx.Request().Input("group") if group == "" { return response.Error(ctx, http.StatusBadRequest, apperrors.ErrConfigGroupRequired.Code) } configsMap := ctx.Request().InputMap("configs") if len(configsMap) == 0 { return response.Error(ctx, http.StatusBadRequest, apperrors.ErrConfigsRequired.Code) } // 获取该分组下的所有配置(即使查询失败也继续,使用空数组) var existingConfigs []models.Config _ = facades.Orm().Query().Where("group", group).Get(&existingConfigs) // 创建key到config的映射 configMap := make(map[string]*models.Config) for i := range existingConfigs { configMap[existingConfigs[i].Key] = &existingConfigs[i] } now := carbon.Now() // 对于 storage 分组,只允许保存驱动选择字段(白名单) if group == "storage" { allowedKeys := map[string]bool{ "file_disk": true, "storage_disk": true, // 向后兼容,保留但不推荐使用 "export_disk": true, // 向后兼容 } filteredConfigs := make(map[string]any) for key, value := range configsMap { if allowedKeys[key] { filteredConfigs[key] = value } } configsMap = filteredConfigs } // 批量处理配置更新和创建 for key, value := range configsMap { // 转换值为字符串,处理布尔值 var valueStr string switch v := value.(type) { case bool: if v { valueStr = "1" } else { valueStr = "0" } case nil: valueStr = "" default: valueStr = cast.ToString(value) } // 如果是邮箱配置的密码字段,且值为空,且配置已存在,则跳过更新(保持原有值) if group == "email" && key == "email_password" && valueStr == "" { if _, exists := configMap[key]; exists { continue } // 如果配置不存在,则创建空值配置(允许首次创建时为空) } if config, exists := configMap[key]; exists { // 更新现有配置 config.Value = valueStr if err := facades.Orm().Query().Save(config); err != nil { return response.ErrorWithLog(ctx, "config", err, map[string]any{ "group": group, "key": key, }) } } else { // 创建新配置 configData := map[string]any{ "group": group, "key": key, "value": valueStr, "type": "input", "sort": 0, "created_at": now, "updated_at": now, } if err := facades.Orm().Query().Table("configs").Create(configData); err != nil { return response.ErrorWithLog(ctx, "config", err, map[string]any{ "group": group, "key": key, }) } } } return response.Success(ctx) } // TestEmail 测试邮件发送 func (r *ConfigController) TestEmail(ctx http.Context) http.Response { emailHost := ctx.Request().Input("email_host") emailPort := cast.ToInt(ctx.Request().Input("email_port", "587")) emailUsername := ctx.Request().Input("email_username") emailPassword := ctx.Request().Input("email_password") emailFrom := ctx.Request().Input("email_from") emailFromName := ctx.Request().Input("email_from_name") emailEncryption := ctx.Request().Input("email_encryption", "tls") // 验证必填字段 if emailHost == "" || emailPort == 0 || emailUsername == "" || emailFrom == "" { return response.Error(ctx, http.StatusBadRequest, apperrors.ErrEmailConfigRequired.Code) } // 获取当前登录的管理员邮箱作为测试收件人 adminValue := ctx.Value("admin") if adminValue == nil { return response.Error(ctx, http.StatusUnauthorized, apperrors.ErrNotLoggedIn.Code) } admin, ok := adminValue.(models.Admin) if !ok { return response.Error(ctx, http.StatusUnauthorized, apperrors.ErrNotLoggedIn.Code) } // 如果没有邮箱,使用发件人邮箱作为测试收件人 testEmail := emailFrom if admin.Email != "" { testEmail = admin.Email } // 构建邮件内容 fromName := emailFromName if fromName == "" { fromName = emailFrom } subject := "测试邮件" body := fmt.Sprintf(`

这是一封测试邮件

如果您收到这封邮件,说明邮件配置正确。

发送时间:%s

SMTP服务器:%s:%d

加密方式:%s

`, carbon.Now().ToDateTimeString(), emailHost, emailPort, emailEncryption) // 构建邮件消息 message := fmt.Sprintf("From: %s <%s>\r\n", fromName, emailFrom) message += fmt.Sprintf("To: %s\r\n", testEmail) message += fmt.Sprintf("Subject: %s\r\n", subject) message += "MIME-Version: 1.0\r\n" message += "Content-Type: text/html; charset=UTF-8\r\n" message += "\r\n" + body // 构建SMTP地址 addr := fmt.Sprintf("%s:%d", emailHost, emailPort) // 创建SMTP认证 auth := smtp.PlainAuth("", emailUsername, emailPassword, emailHost) // 发送邮件 var err error if emailEncryption == "ssl" { // SSL连接 tlsConfig := &tls.Config{ InsecureSkipVerify: false, ServerName: emailHost, } conn, connErr := tls.Dial("tcp", addr, tlsConfig) if connErr != nil { return response.ErrorWithLog(ctx, "config", connErr, map[string]any{ "host": emailHost, "port": emailPort, }) } defer conn.Close() client, clientErr := smtp.NewClient(conn, emailHost) if clientErr != nil { return response.ErrorWithLog(ctx, "config", clientErr) } defer client.Close() if err = client.Auth(auth); err != nil { return response.ErrorWithLog(ctx, "config", err) } if err = client.Mail(emailFrom); err != nil { return response.ErrorWithLog(ctx, "config", err) } if err = client.Rcpt(testEmail); err != nil { return response.ErrorWithLog(ctx, "config", err) } writer, writerErr := client.Data() if writerErr != nil { return response.ErrorWithLog(ctx, "config", writerErr) } _, err = writer.Write([]byte(message)) if err != nil { writer.Close() return response.ErrorWithLog(ctx, "config", err) } err = writer.Close() if err != nil { return response.ErrorWithLog(ctx, "config", err) } } else { // TLS或普通连接 if emailEncryption == "tls" { // TLS连接 tlsConfig := &tls.Config{ InsecureSkipVerify: false, ServerName: emailHost, } err = smtp.SendMail(addr, auth, emailFrom, []string{testEmail}, []byte(message)) if err != nil { // 如果直接SendMail失败,尝试手动TLS conn, connErr := smtp.Dial(addr) if connErr != nil { return response.ErrorWithLog(ctx, "config", connErr, map[string]any{ "host": emailHost, "port": emailPort, }) } defer conn.Close() if err = conn.StartTLS(tlsConfig); err != nil { return response.ErrorWithLog(ctx, "config", err) } if err = conn.Auth(auth); err != nil { return response.ErrorWithLog(ctx, "config", err) } if err = conn.Mail(emailFrom); err != nil { return response.ErrorWithLog(ctx, "config", err) } if err = conn.Rcpt(testEmail); err != nil { return response.ErrorWithLog(ctx, "config", err) } writer, writerErr := conn.Data() if writerErr != nil { return response.ErrorWithLog(ctx, "config", writerErr) } _, err = writer.Write([]byte(message)) if err != nil { writer.Close() return response.ErrorWithLog(ctx, "config", err) } err = writer.Close() if err != nil { return response.ErrorWithLog(ctx, "config", err) } } } else { // 普通连接(无加密) err = smtp.SendMail(addr, auth, emailFrom, []string{testEmail}, []byte(message)) } } if err != nil { return response.ErrorWithLog(ctx, "config", err, map[string]any{ "host": emailHost, "port": emailPort, "from": emailFrom, "to": testEmail, }) } return response.Success(ctx, "test_email_success", http.Json{ "message": "测试邮件已发送到 " + testEmail, }) }