初始提交:OCI 面板后端(含 GenAI 网关一期)
This commit is contained in:
@@ -0,0 +1,346 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"oci-portal/internal/crypto"
|
||||
"oci-portal/internal/model"
|
||||
)
|
||||
|
||||
// Telegram 通知相关的配置键。
|
||||
const (
|
||||
settingTelegramEnabled = "telegram_enabled" // "1" / "0"
|
||||
settingTelegramBotToken = "telegram_bot_token" // AES-GCM 密文
|
||||
settingTelegramChatID = "telegram_chat_id" // 明文
|
||||
)
|
||||
|
||||
// 通知事件开关的配置键(notify_event_{kind}),值 "1"/"0";
|
||||
// 键缺省(存量部署从未保存过)视为开启,只有显式 "0" 视为关闭。
|
||||
// 云端事件按类别细分(旧总开关键 notify_event_log_event 已废弃,库中遗留无害)。
|
||||
const (
|
||||
settingNotifyEventTaskFail = "notify_event_task_fail"
|
||||
settingNotifyEventTaskRecover = "notify_event_task_recover"
|
||||
settingNotifyEventSnatchSuccess = "notify_event_snatch_success"
|
||||
settingNotifyEventTenantDead = "notify_event_tenant_dead"
|
||||
settingNotifyEventTaskStop = "notify_event_task_stop"
|
||||
settingNotifyEventLoginLock = "notify_event_login_lock"
|
||||
settingNotifyEventModelDeprecated = "notify_event_model_deprecated"
|
||||
settingNotifyEventLogEventInstance = "notify_event_log_event_instance"
|
||||
settingNotifyEventLogEventIdentity = "notify_event_log_event_identity"
|
||||
settingNotifyEventLogEventPolicy = "notify_event_log_event_policy"
|
||||
settingNotifyEventLogEventRegion = "notify_event_log_event_region"
|
||||
settingNotifyEventLogEventLogin = "notify_event_log_event_login"
|
||||
)
|
||||
|
||||
// 任务行为相关的配置键与取值约束(抢机连续 NotAuthenticated 熔断阈值)。
|
||||
const (
|
||||
settingSnatchAuthFailLimit = "snatch_auth_fail_limit"
|
||||
defaultSnatchAuthFailLimit = 3
|
||||
minSnatchAuthFailLimit = 1
|
||||
maxSnatchAuthFailLimit = 10
|
||||
)
|
||||
|
||||
// ErrInvalidAuthFailLimit 标记抢机熔断阈值越界,api 层据此返回 400。
|
||||
var ErrInvalidAuthFailLimit = fmt.Errorf("熔断阈值须在 %d-%d 之间", minSnatchAuthFailLimit, maxSnatchAuthFailLimit)
|
||||
|
||||
// SettingService 读写系统级键值配置,敏感值经 Cipher 加密落库;
|
||||
// 安全设置(WAF/真实IP头/app_url)另持内存快照供每请求热路径零 IO 读取。
|
||||
type SettingService struct {
|
||||
db *gorm.DB
|
||||
cipher *crypto.Cipher
|
||||
security atomic.Value // SecuritySettings 快照,ReloadSecurity/UpdateSecurity 刷新
|
||||
envPublicURL string // PUBLIC_URL 环境变量,app_url 未配置时的回退
|
||||
}
|
||||
|
||||
// NewSettingService 组装依赖。
|
||||
func NewSettingService(db *gorm.DB, cipher *crypto.Cipher) *SettingService {
|
||||
return &SettingService{db: db, cipher: cipher}
|
||||
}
|
||||
|
||||
// get 读取键值;键不存在视为未配置,返回空串。
|
||||
func (s *SettingService) get(ctx context.Context, key string) (string, error) {
|
||||
var st model.Setting
|
||||
err := s.db.WithContext(ctx).First(&st, "key = ?", key).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return "", nil
|
||||
}
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("get setting %s: %w", key, err)
|
||||
}
|
||||
return st.Value, nil
|
||||
}
|
||||
|
||||
// set 写入键值(key 为主键,Save 即 upsert)。
|
||||
func (s *SettingService) set(ctx context.Context, key, value string) error {
|
||||
st := model.Setting{Key: key, Value: value, UpdatedAt: time.Now()}
|
||||
if err := s.db.WithContext(ctx).Save(&st).Error; err != nil {
|
||||
return fmt.Errorf("set setting %s: %w", key, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getMany 批量读取多个键;任一失败即返回错误。
|
||||
func (s *SettingService) getMany(ctx context.Context, keys ...string) (map[string]string, error) {
|
||||
out := make(map[string]string, len(keys))
|
||||
for _, key := range keys {
|
||||
value, err := s.get(ctx, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out[key] = value
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// TelegramConfig 是通知发送所需的完整配置,token 已解密,仅供服务内部消费。
|
||||
type TelegramConfig struct {
|
||||
Enabled bool
|
||||
BotToken string
|
||||
ChatID string
|
||||
}
|
||||
|
||||
// TelegramConfig 返回解密后的 Telegram 配置。
|
||||
func (s *SettingService) TelegramConfig(ctx context.Context) (TelegramConfig, error) {
|
||||
enabled, err := s.get(ctx, settingTelegramEnabled)
|
||||
if err != nil {
|
||||
return TelegramConfig{}, err
|
||||
}
|
||||
chatID, err := s.get(ctx, settingTelegramChatID)
|
||||
if err != nil {
|
||||
return TelegramConfig{}, err
|
||||
}
|
||||
token, err := s.telegramToken(ctx)
|
||||
if err != nil {
|
||||
return TelegramConfig{}, err
|
||||
}
|
||||
return TelegramConfig{Enabled: enabled == "1", BotToken: token, ChatID: chatID}, nil
|
||||
}
|
||||
|
||||
// telegramToken 读取并解密 bot token;未配置时返回空串。
|
||||
func (s *SettingService) telegramToken(ctx context.Context) (string, error) {
|
||||
enc, err := s.get(ctx, settingTelegramBotToken)
|
||||
if err != nil || enc == "" {
|
||||
return "", err
|
||||
}
|
||||
token, err := s.cipher.DecryptString(enc)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("decrypt telegram token: %w", err)
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// TelegramView 是设置接口的响应视图,绝不包含 token 明文。
|
||||
type TelegramView struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
ChatID string `json:"chatId"`
|
||||
TokenSet bool `json:"tokenSet"`
|
||||
TokenTail string `json:"tokenTail"`
|
||||
}
|
||||
|
||||
// TelegramView 返回脱敏后的 Telegram 配置视图。
|
||||
func (s *SettingService) TelegramView(ctx context.Context) (TelegramView, error) {
|
||||
cfg, err := s.TelegramConfig(ctx)
|
||||
if err != nil {
|
||||
return TelegramView{}, err
|
||||
}
|
||||
return TelegramView{
|
||||
Enabled: cfg.Enabled,
|
||||
ChatID: cfg.ChatID,
|
||||
TokenSet: cfg.BotToken != "",
|
||||
TokenTail: tokenTail(cfg.BotToken),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// tokenTail 取 token 尾 4 位用于展示;过短时不展示,避免变相泄露。
|
||||
func tokenTail(token string) string {
|
||||
if len(token) < 8 {
|
||||
return ""
|
||||
}
|
||||
return token[len(token)-4:]
|
||||
}
|
||||
|
||||
// UpdateTelegramInput 是更新 Telegram 配置的输入;
|
||||
// BotToken 为 nil 沿用已存 token,非 nil 时覆盖(空串表示清除)。
|
||||
type UpdateTelegramInput struct {
|
||||
Enabled bool
|
||||
BotToken *string
|
||||
ChatID string
|
||||
}
|
||||
|
||||
// UpdateTelegram 保存 Telegram 配置,token 加密落库。
|
||||
func (s *SettingService) UpdateTelegram(ctx context.Context, in UpdateTelegramInput) error {
|
||||
enabled := "0"
|
||||
if in.Enabled {
|
||||
enabled = "1"
|
||||
}
|
||||
if err := s.set(ctx, settingTelegramEnabled, enabled); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.set(ctx, settingTelegramChatID, in.ChatID); err != nil {
|
||||
return err
|
||||
}
|
||||
if in.BotToken == nil {
|
||||
return nil
|
||||
}
|
||||
return s.saveTelegramToken(ctx, *in.BotToken)
|
||||
}
|
||||
|
||||
// saveTelegramToken 加密保存 token;空串直接落空值表示清除。
|
||||
func (s *SettingService) saveTelegramToken(ctx context.Context, token string) error {
|
||||
if token == "" {
|
||||
return s.set(ctx, settingTelegramBotToken, "")
|
||||
}
|
||||
enc, err := s.cipher.EncryptString(token)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encrypt telegram token: %w", err)
|
||||
}
|
||||
return s.set(ctx, settingTelegramBotToken, enc)
|
||||
}
|
||||
|
||||
// NotifyEventsView 是通知事件开关视图;零值无意义,一律由 NotifyEvents 构造。
|
||||
// 云端事件按类别细分:实例生命周期 / 用户与凭据 / 策略 / 区域订阅 / 控制台登录。
|
||||
type NotifyEventsView struct {
|
||||
TaskFail bool `json:"taskFail"`
|
||||
TaskRecover bool `json:"taskRecover"`
|
||||
SnatchSuccess bool `json:"snatchSuccess"`
|
||||
TenantDead bool `json:"tenantDead"`
|
||||
TaskStop bool `json:"taskStop"`
|
||||
LoginLock bool `json:"loginLock"`
|
||||
ModelDeprecated bool `json:"modelDeprecated"`
|
||||
LogEventInstance bool `json:"logEventInstance"`
|
||||
LogEventIdentity bool `json:"logEventIdentity"`
|
||||
LogEventPolicy bool `json:"logEventPolicy"`
|
||||
LogEventRegion bool `json:"logEventRegion"`
|
||||
LogEventLogin bool `json:"logEventLogin"`
|
||||
}
|
||||
|
||||
// notifyEventFields 把视图字段与配置键一一对应,供读写复用。
|
||||
func notifyEventFields(v *NotifyEventsView) []struct {
|
||||
key string
|
||||
on *bool
|
||||
} {
|
||||
return []struct {
|
||||
key string
|
||||
on *bool
|
||||
}{
|
||||
{settingNotifyEventTaskFail, &v.TaskFail},
|
||||
{settingNotifyEventTaskRecover, &v.TaskRecover},
|
||||
{settingNotifyEventSnatchSuccess, &v.SnatchSuccess},
|
||||
{settingNotifyEventTenantDead, &v.TenantDead},
|
||||
{settingNotifyEventTaskStop, &v.TaskStop},
|
||||
{settingNotifyEventLoginLock, &v.LoginLock},
|
||||
{settingNotifyEventModelDeprecated, &v.ModelDeprecated},
|
||||
{settingNotifyEventLogEventInstance, &v.LogEventInstance},
|
||||
{settingNotifyEventLogEventIdentity, &v.LogEventIdentity},
|
||||
{settingNotifyEventLogEventPolicy, &v.LogEventPolicy},
|
||||
{settingNotifyEventLogEventRegion, &v.LogEventRegion},
|
||||
{settingNotifyEventLogEventLogin, &v.LogEventLogin},
|
||||
}
|
||||
}
|
||||
|
||||
// NotifyEvents 返回全部通知事件开关;键缺省视为开启,兼容存量部署。
|
||||
func (s *SettingService) NotifyEvents(ctx context.Context) (NotifyEventsView, error) {
|
||||
var view NotifyEventsView
|
||||
for _, f := range notifyEventFields(&view) {
|
||||
val, err := s.get(ctx, f.key)
|
||||
if err != nil {
|
||||
return NotifyEventsView{}, err
|
||||
}
|
||||
*f.on = val != "0"
|
||||
}
|
||||
return view, nil
|
||||
}
|
||||
|
||||
// UpdateNotifyEvents 全量保存五个通知事件开关。
|
||||
func (s *SettingService) UpdateNotifyEvents(ctx context.Context, in NotifyEventsView) error {
|
||||
for _, f := range notifyEventFields(&in) {
|
||||
value := "0"
|
||||
if *f.on {
|
||||
value = "1"
|
||||
}
|
||||
if err := s.set(ctx, f.key, value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NotifyEventEnabled 查询单个通知事件开关,供发送前过滤;
|
||||
// 只有显式 "0" 视为关闭,键缺省或读取失败一律按开启返回(降级不漏发),错误仅记日志。
|
||||
func (s *SettingService) NotifyEventEnabled(ctx context.Context, kind string) bool {
|
||||
val, err := s.get(ctx, "notify_event_"+kind)
|
||||
if err != nil {
|
||||
log.Printf("notify event %s switch: %v", kind, err)
|
||||
return true
|
||||
}
|
||||
return val != "0"
|
||||
}
|
||||
|
||||
// NotifyTemplate 读取 kind 的自定义模板;空串表示未自定义(按默认模板发送)。
|
||||
func (s *SettingService) NotifyTemplate(ctx context.Context, kind string) (string, error) {
|
||||
return s.get(ctx, notifyTplKey(kind))
|
||||
}
|
||||
|
||||
// NotifyTemplates 返回全部通知模板视图(默认模板 + 自定义现值),顺序稳定。
|
||||
func (s *SettingService) NotifyTemplates(ctx context.Context) ([]NotifyTemplateView, error) {
|
||||
out := make([]NotifyTemplateView, 0, len(notifyTplOrder))
|
||||
for _, kind := range notifyTplOrder {
|
||||
def := notifyTplDefs[kind]
|
||||
custom, err := s.get(ctx, notifyTplKey(kind))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, NotifyTemplateView{
|
||||
Kind: kind, Label: def.Label, Vars: def.Vars,
|
||||
DefaultTemplate: def.Default, Template: custom,
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// UpdateNotifyTemplate 保存 kind 的自定义模板;空串清除自定义(恢复默认)。
|
||||
func (s *SettingService) UpdateNotifyTemplate(ctx context.Context, kind, tpl string) error {
|
||||
if !notifyTplValid(kind) {
|
||||
return fmt.Errorf("未知通知类型 %q", kind)
|
||||
}
|
||||
tpl = strings.TrimSpace(tpl)
|
||||
if len([]rune(tpl)) > 2000 {
|
||||
return fmt.Errorf("模板长度不能超过 2000 字符")
|
||||
}
|
||||
return s.set(ctx, notifyTplKey(kind), tpl)
|
||||
}
|
||||
|
||||
// TaskSettingsView 是任务行为设置的读写视图。
|
||||
type TaskSettingsView struct {
|
||||
SnatchAuthFailLimit int `json:"snatchAuthFailLimit"`
|
||||
}
|
||||
|
||||
// TaskSettings 返回任务行为设置;阈值缺省或非法(非整数 / 越界脏数据)时回退默认值。
|
||||
func (s *SettingService) TaskSettings(ctx context.Context) (TaskSettingsView, error) {
|
||||
val, err := s.get(ctx, settingSnatchAuthFailLimit)
|
||||
if err != nil {
|
||||
return TaskSettingsView{}, err
|
||||
}
|
||||
limit, err := strconv.Atoi(val)
|
||||
if err != nil || limit < minSnatchAuthFailLimit || limit > maxSnatchAuthFailLimit {
|
||||
limit = defaultSnatchAuthFailLimit
|
||||
}
|
||||
return TaskSettingsView{SnatchAuthFailLimit: limit}, nil
|
||||
}
|
||||
|
||||
// UpdateTaskSettings 保存任务行为设置;阈值越界返回 ErrInvalidAuthFailLimit。
|
||||
func (s *SettingService) UpdateTaskSettings(ctx context.Context, in TaskSettingsView) error {
|
||||
if in.SnatchAuthFailLimit < minSnatchAuthFailLimit || in.SnatchAuthFailLimit > maxSnatchAuthFailLimit {
|
||||
return fmt.Errorf("update task settings: %w", ErrInvalidAuthFailLimit)
|
||||
}
|
||||
return s.set(ctx, settingSnatchAuthFailLimit, strconv.Itoa(in.SnatchAuthFailLimit))
|
||||
}
|
||||
Reference in New Issue
Block a user