初始提交:OCI 面板后端(含 GenAI 网关一期)

This commit is contained in:
Wang Defa
2026-07-09 15:31:04 +08:00
commit b9a3e97e84
168 changed files with 31794 additions and 0 deletions
+346
View File
@@ -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))
}