package console import ( "fmt" "strings" "time" "yunyan/comm" "yunyan/lego/sys/postgres" "yunyan/pb" "yunyan/sys/email" "github.com/gin-gonic/gin" ) // ============================ 平台系统服务配置域 (sysconfig) ============================ // // 存 console 主库(postgres.GetSys())的 sys_service_config 表,平台自用(对象存储/邮件/翻译), // 与面向客户端下发的 svc_config 彻底分离。加密字段值 AES-256-GCM 落库、返回前端脱敏为 "[encrypted]"。 // // 类型别名,包内沿用简名。 type PlatformService = comm.PlatformService // getSysConfigs 列出全部平台服务配置(加密字段脱敏)。 func (this *serverComp) getSysConfigs(c *gin.Context) { items := make([]*PlatformService, 0) if err := postgres.Find(comm.TableSysServiceConfig, &items, ""); err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } for _, it := range items { maskSvcFields(it.Fields) } writeOK(c, gin.H{"items": items}) } // saveSysConfig 新增/更新一条平台服务配置(按 id upsert)。加密字段留空/占位=保留原密文。 func (this *serverComp) saveSysConfig(c *gin.Context) { var m PlatformService if err := c.ShouldBindJSON(&m); err != nil { writeErr(c, pb.ErrorCode_ReqParameterError, err.Error()) return } if strings.TrimSpace(m.Id) == "" || strings.TrimSpace(m.Category) == "" { writeErr(c, pb.ErrorCode_ReqParameterError, "id 和 category 必填") return } now := time.Now().Unix() exist := &PlatformService{} isUpdate := postgres.FindOne(comm.TableSysServiceConfig, exist, "id=?", m.Id) == nil // 加密字段:更新时空值/占位保留旧密文;新增时直接加密非空值。 var oldMap map[string]string if isUpdate { oldMap = map[string]string{} for _, f := range exist.Fields { if f.Encrypted { oldMap[f.Key] = f.DefValue } } } if err := encryptFieldsSlice(this.options.EncryptKey, m.Fields, oldMap); err != nil { writeErr(c, pb.ErrorCode_SystemError, "字段加密失败: "+err.Error()) return } m.Updatetime = now if isUpdate { m.Createtime = exist.Createtime if err := postgres.Save(comm.TableSysServiceConfig, &m); err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } } else { m.Createtime = now if err := postgres.Insert(comm.TableSysServiceConfig, &m); err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } } // 若本条被设为默认,则清掉同类目其它行的默认标记(默认唯一)。 if m.IsDefault { if res := postgres.Exec( "UPDATE "+comm.TableSysServiceConfig+" SET is_default=false WHERE category=? AND id<>?", m.Category, m.Id); res.Error != nil { writeErr(c, pb.ErrorCode_DBError, res.Error.Error()) return } } writeOK(c, gin.H{"id": m.Id}) } // setSysDefault 把某类目下的指定服务设为默认(其余清默认),并顺带启用它。 func (this *serverComp) setSysDefault(c *gin.Context) { var req struct { Id string `json:"id"` Category string `json:"category"` } _ = c.ShouldBindJSON(&req) if req.Id == "" || req.Category == "" { writeErr(c, pb.ErrorCode_ReqParameterError, "id 和 category 必填") return } if res := postgres.Exec( "UPDATE "+comm.TableSysServiceConfig+" SET is_default=false WHERE category=?", req.Category); res.Error != nil { writeErr(c, pb.ErrorCode_DBError, res.Error.Error()) return } if res := postgres.Exec( "UPDATE "+comm.TableSysServiceConfig+" SET is_default=true, enable=true, updatetime=? WHERE id=?", time.Now().Unix(), req.Id); res.Error != nil { writeErr(c, pb.ErrorCode_DBError, res.Error.Error()) return } writeOK(c, gin.H{"id": req.Id}) } // delSysConfig 删除一条平台服务配置。 func (this *serverComp) delSysConfig(c *gin.Context) { var req struct { Id string `json:"id"` } _ = c.ShouldBindJSON(&req) if req.Id == "" { writeErr(c, pb.ErrorCode_ReqParameterError, "id 必填") return } if err := postgres.Delete(comm.TableSysServiceConfig, "id=?", req.Id); err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } writeOK(c, gin.H{"id": req.Id}) } // loadSysStorageAliyun 读平台对象存储里启用的阿里云 OSS 配置,解密为可用凭据; // 无有效配置返回 nil(调用方据此回退旧的 svc_config 存储配置)。 func (this *serverComp) loadSysStorageAliyun() *aliyunOSSConf { rows := make([]*PlatformService, 0) if err := postgres.Find(comm.TableSysServiceConfig, &rows, "category=? AND provider=? AND enable=?", comm.SysCatStorage, "aliyun_oss", true); err != nil || len(rows) == 0 { return nil } // 有 is_default 优先取默认,否则取第一条启用的。 cfg := rows[0] for _, r := range rows { if r.IsDefault { cfg = r break } } key := this.options.EncryptKey oc := &aliyunOSSConf{ Endpoint: strings.TrimSpace(cfg.SysFieldPlain("endpoint", key)), AccessKeyId: strings.TrimSpace(cfg.SysFieldPlain("access_key_id", key)), AccessSecret: strings.TrimSpace(cfg.SysFieldPlain("access_key_secret", key)), Bucket: strings.TrimSpace(cfg.SysFieldPlain("bucket", key)), Domain: strings.TrimSpace(cfg.SysFieldPlain("domain", key)), } if oc.Endpoint == "" || oc.AccessKeyId == "" || oc.AccessSecret == "" || oc.Bucket == "" { return nil } if !strings.HasPrefix(oc.Endpoint, "http://") && !strings.HasPrefix(oc.Endpoint, "https://") { oc.Endpoint = "https://" + oc.Endpoint } return oc } // loadSysEmail 取平台邮件配置(启用的那条;无则 nil)。 func (this *serverComp) loadSysEmail() *PlatformService { rows := make([]*PlatformService, 0) if err := postgres.Find(comm.TableSysServiceConfig, &rows, "category=? AND enable=?", comm.SysCatEmail, true); err != nil || len(rows) == 0 { return nil } return rows[0] } // applySysEmail 用平台邮件配置热重载 email 系统(供发信前调用)。未配置/不合法返回 error。 func (this *serverComp) applySysEmail() error { cfg := this.loadSysEmail() if cfg == nil { return fmt.Errorf("未配置邮件服务:请在【系统配置 → 邮件服务】填写并启用") } key := this.options.EncryptKey conf := map[string]interface{}{ "EmailType": cfg.SysFieldInt("EmailType", 1), "FromEmail": cfg.SysFieldPlain("FromEmail", key), "FromName": cfg.SysFieldPlain("FromName", key), "Password": cfg.SysFieldPlain("Password", key), } return email.Reload(conf) } // testSysEmail 用当前邮件配置向指定邮箱发一封测试邮件。 func (this *serverComp) testSysEmail(c *gin.Context) { var req struct { To string `json:"to"` } _ = c.ShouldBindJSON(&req) if strings.TrimSpace(req.To) == "" { writeErr(c, pb.ErrorCode_ReqParameterError, "测试收件邮箱必填") return } if err := this.applySysEmail(); err != nil { writeErr(c, pb.ErrorCode_SystemError, err.Error()) return } if err := email.SendMail("【系统配置】邮件服务测试", "这是一封来自后台系统配置的测试邮件,收到即表示邮件服务配置正确。", req.To); err != nil { writeErr(c, pb.ErrorCode_SystemError, "发送失败: "+err.Error()+mailErrorHint(this.loadSysEmail(), err)) return } writeOK(c, gin.H{"ok": true}) } // mailErrorHint 把服务商返回的原始 SMTP 错误翻译成「接下来该去改什么」。 // // 存在的理由:SMTP 的 5xx 文案是服务商写给邮件管理员看的,运营在后台只看到一串 // `535 Error: authentication failed, system busy`,既不知道连的是哪台服务器, // 也不知道该改密码还是去开服务。配置本身能读能解密(读不到会在 applySysEmail 就报 // 「Password 为空」),所以走到这里的失败一律是服务商侧的凭据/权限问题。 func mailErrorHint(cfg *PlatformService, err error) string { if cfg == nil || err == nil { return "" } msg := strings.ToLower(err.Error()) host := map[int]string{ 0: "smtp.qq.com:25", 1: "smtp.exmail.qq.com:465", 2: "smtp.gmail.com:587", 3: "Resend HTTP API", }[cfg.SysFieldInt("EmailType", 1)] tip := "" switch { case strings.Contains(msg, "535"), strings.Contains(msg, "authentication failed"), strings.Contains(msg, "auth"): // 这几家都要求用「授权码 / 客户端专用密码」而不是登录密码,且要先在邮箱后台开 SMTP。 tip = "(认证被拒:①「SMTP 授权码」要填邮箱后台生成的**客户端专用密码/授权码**,不是登录密码;" + "②该邮箱要先在服务商后台开启 SMTP/IMAP 服务;③确认发件人邮箱与这份授权码属于同一个账号)" case strings.Contains(msg, "timeout"), strings.Contains(msg, "i/o timeout"), strings.Contains(msg, "connection refused"): tip = "(连不上发信服务器:多半是服务器出网被限制,或该端口被封)" case strings.Contains(msg, "550"), strings.Contains(msg, "553"), strings.Contains(msg, "not allowed"): tip = "(服务商拒收:发件人地址与账号不符,或域名未通过 SPF/发信域名验证)" } return fmt.Sprintf(" [发信服务器 %s,发件人 %s]%s", host, cfg.SysFieldPlain("FromEmail", ""), tip) }