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