You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

244 lines
9.0 KiB

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)
}