Files
ops-assistant/models/models.go
2026-03-19 21:23:28 +08:00

337 lines
17 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package models
import (
"time"
"gorm.io/gorm"
)
type Transaction struct {
ID uint `gorm:"primaryKey" json:"id"`
UserID int64 `json:"user_id"`
Amount int64 `json:"amount"` // 金额,单位:分
Category string `gorm:"size:50" json:"category"`
Note string `json:"note"`
Date string `gorm:"size:20;index" json:"date"`
IsDeleted bool `gorm:"default:false" json:"is_deleted"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type CategoryKeyword struct {
ID uint `gorm:"primaryKey"`
Keyword string `gorm:"uniqueIndex;size:50"`
Category string `gorm:"size:50"`
}
// FeatureFlag 高风险能力开关(默认关闭)
type FeatureFlag struct {
ID uint `gorm:"primaryKey" json:"id"`
Key string `gorm:"uniqueIndex;size:100" json:"key"`
Enabled bool `gorm:"default:false" json:"enabled"`
RiskLevel string `gorm:"size:20" json:"risk_level"` // low|medium|high
Description string `gorm:"size:255" json:"description"`
RequireReason bool `gorm:"default:false" json:"require_reason"`
UpdatedBy int64 `json:"updated_by"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// OpsTarget 运维目标主机配置
type OpsTarget struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"uniqueIndex;size:64" json:"name"`
Host string `gorm:"size:128" json:"host"`
Port int `gorm:"default:22" json:"port"`
User string `gorm:"size:64" json:"user"`
Enabled bool `gorm:"default:true" json:"enabled"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// FeatureFlagHistory 开关变更历史
type AppSetting struct {
ID uint `gorm:"primaryKey" json:"id"`
Key string `gorm:"uniqueIndex;size:100" json:"key"`
Value string `gorm:"type:text" json:"value"`
UpdatedBy int64 `json:"updated_by"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// FeatureFlagHistory 开关变更历史
type FeatureFlagHistory struct {
ID uint `gorm:"primaryKey" json:"id"`
FlagKey string `gorm:"index;size:100" json:"flag_key"`
OldValue bool `json:"old_value"`
NewValue bool `json:"new_value"`
ChangedBy int64 `json:"changed_by"`
Reason string `gorm:"size:255" json:"reason"`
RequestID string `gorm:"size:100" json:"request_id"`
CreatedAt time.Time `json:"created_at"`
}
// ChannelConfig 渠道接入配置(平台适配层参数)
type ChannelConfig struct {
ID uint `gorm:"primaryKey" json:"id"`
Platform string `gorm:"uniqueIndex;size:32" json:"platform"` // qqbot_official|telegram|feishu
Name string `gorm:"size:64" json:"name"`
Enabled bool `gorm:"default:false" json:"enabled"`
Status string `gorm:"size:20;default:'disabled'" json:"status"` // ok|error|disabled
ConfigJSON string `gorm:"type:text" json:"config_json"` // 生效配置 JSON
SecretJSON string `gorm:"type:text" json:"-"` // 生效密钥 JSON建议加密
DraftConfigJSON string `gorm:"type:text" json:"draft_config_json"` // 草稿配置 JSON
DraftSecretJSON string `gorm:"type:text" json:"-"` // 草稿密钥 JSON建议加密
LastCheck *time.Time `json:"last_check_at"`
PublishedAt *time.Time `json:"published_at"`
UpdatedBy int64 `json:"updated_by"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// AuditLog 通用审计日志
type AuditLog struct {
ID uint `gorm:"primaryKey" json:"id"`
ActorID int64 `gorm:"index" json:"actor_id"`
Action string `gorm:"size:64;index" json:"action"`
TargetType string `gorm:"size:64;index" json:"target_type"`
TargetID string `gorm:"size:128;index" json:"target_id"`
BeforeJSON string `gorm:"type:text" json:"before_json"`
AfterJSON string `gorm:"type:text" json:"after_json"`
Note string `gorm:"size:255" json:"note"`
CreatedAt time.Time `json:"created_at"`
}
// MessageDedup 入站事件幂等去重
type MessageDedup struct {
ID uint `gorm:"primaryKey" json:"id"`
Platform string `gorm:"size:32;index:idx_platform_event,unique" json:"platform"`
EventID string `gorm:"size:128;index:idx_platform_event,unique" json:"event_id"`
ProcessedAt time.Time `json:"processed_at"`
}
// OpsJob 运维命令执行任务
type OpsJob struct {
ID uint `gorm:"primaryKey" json:"id"`
Command string `gorm:"size:255;index" json:"command"`
Runbook string `gorm:"size:128;index" json:"runbook"`
Operator int64 `gorm:"index" json:"operator"`
Target string `gorm:"size:128;index" json:"target"`
RiskLevel string `gorm:"size:16" json:"risk_level"`
RequestID string `gorm:"size:100;index" json:"request_id"`
ConfirmHash string `gorm:"size:80" json:"confirm_hash"`
InputJSON string `gorm:"type:text" json:"input_json"`
Status string `gorm:"size:20;index" json:"status"` // pending|running|success|failed|cancelled
CancelNote string `gorm:"size:255" json:"cancel_note"`
Summary string `gorm:"size:500" json:"summary"`
StartedAt time.Time `json:"started_at"`
EndedAt time.Time `json:"ended_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// OpsJobStep 任务步骤日志
type OpsJobStep struct {
ID uint `gorm:"primaryKey" json:"id"`
JobID uint `gorm:"index" json:"job_id"`
StepID string `gorm:"size:80" json:"step_id"`
Action string `gorm:"size:80" json:"action"`
Status string `gorm:"size:20;index" json:"status"` // running|success|failed|skipped
RC int `json:"rc"`
StdoutTail string `gorm:"type:text" json:"stdout_tail"`
StderrTail string `gorm:"type:text" json:"stderr_tail"`
StartedAt time.Time `json:"started_at"`
EndedAt time.Time `json:"ended_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// AmountYuan 返回元为单位的金额(显示用)
func (t *Transaction) AmountYuan() float64 {
return float64(t.Amount) / 100.0
}
func seedDefaultFeatureFlags(db *gorm.DB) error {
defaults := []FeatureFlag{
{Key: "allow_cross_user_read", Enabled: false, RiskLevel: "high", Description: "允许读取非本人账本数据", RequireReason: true},
{Key: "allow_cross_user_delete", Enabled: false, RiskLevel: "high", Description: "允许删除非本人账本记录", RequireReason: true},
{Key: "allow_export_all_users", Enabled: false, RiskLevel: "high", Description: "允许导出全量用户账本数据", RequireReason: true},
{Key: "allow_manual_role_grant", Enabled: false, RiskLevel: "medium", Description: "允许人工授予角色", RequireReason: true},
{Key: "allow_bot_admin_commands", Enabled: false, RiskLevel: "medium", Description: "允许 Bot 侧执行管理命令", RequireReason: true},
{Key: "allow_ops_restore", Enabled: false, RiskLevel: "high", Description: "允许执行 usage restore 高风险动作", RequireReason: true},
{Key: "enable_module_cpa", Enabled: true, RiskLevel: "low", Description: "启用 CPA 模块命令入口", RequireReason: false},
{Key: "enable_module_cf", Enabled: false, RiskLevel: "medium", Description: "启用 CF 模块命令入口", RequireReason: true},
{Key: "enable_module_mail", Enabled: false, RiskLevel: "medium", Description: "启用 Mail 模块命令入口", RequireReason: true},
}
for _, ff := range defaults {
if err := db.Where("key = ?", ff.Key).FirstOrCreate(&ff).Error; err != nil {
return err
}
}
return nil
}
func seedDefaultAppSettings(db *gorm.DB) error {
defaults := []AppSetting{
{Key: "cpa_management_token", Value: ""},
{Key: "cf_account_id", Value: ""},
{Key: "cf_api_token", Value: ""},
{Key: "ai_enabled", Value: "false"},
{Key: "ai_base_url", Value: ""},
{Key: "ai_api_key", Value: ""},
{Key: "ai_model", Value: ""},
{Key: "ai_timeout_seconds", Value: "15"},
}
for _, s := range defaults {
if err := db.Where("key = ?", s.Key).FirstOrCreate(&s).Error; err != nil {
return err
}
}
return nil
}
func seedDefaultChannels(db *gorm.DB) error {
defaults := []ChannelConfig{
{Platform: "qqbot_official", Name: "QQ 官方 Bot", Enabled: false, Status: "disabled", ConfigJSON: "{}", SecretJSON: "{}", DraftConfigJSON: "{}", DraftSecretJSON: "{}"},
{Platform: "telegram", Name: "Telegram Bot", Enabled: false, Status: "disabled", ConfigJSON: "{}", SecretJSON: "{}", DraftConfigJSON: "{}", DraftSecretJSON: "{}"},
{Platform: "feishu", Name: "飞书 Bot", Enabled: false, Status: "disabled", ConfigJSON: "{}", SecretJSON: "{}", DraftConfigJSON: "{}", DraftSecretJSON: "{}"},
}
for _, ch := range defaults {
if err := db.Where("platform = ?", ch.Platform).FirstOrCreate(&ch).Error; err != nil {
return err
}
}
return nil
}
// Migrate 自动迁移数据库表结构并初始化分类关键词
func Migrate(db *gorm.DB) error {
if err := db.AutoMigrate(
&Transaction{},
&CategoryKeyword{},
&FeatureFlag{},
&FeatureFlagHistory{},
&ChannelConfig{},
&AuditLog{},
&MessageDedup{},
&OpsTarget{},
&OpsJob{},
&OpsJobStep{},
&AppSetting{},
); err != nil {
return err
}
if err := seedDefaultFeatureFlags(db); err != nil {
return err
}
if err := seedDefaultChannels(db); err != nil {
return err
}
if err := seedDefaultAppSettings(db); err != nil {
return err
}
// 检查是否已有关键词数据
var count int64
db.Model(&CategoryKeyword{}).Count(&count)
if count > 0 {
return nil
}
// 预设分类关键词
keywords := []CategoryKeyword{
// 餐饮
{Keyword: "早餐", Category: "餐饮"}, {Keyword: "午餐", Category: "餐饮"}, {Keyword: "晚餐", Category: "餐饮"},
{Keyword: "早饭", Category: "餐饮"}, {Keyword: "午饭", Category: "餐饮"}, {Keyword: "晚饭", Category: "餐饮"},
{Keyword: "吃饭", Category: "餐饮"}, {Keyword: "吃", Category: "餐饮"}, {Keyword: "饭", Category: "餐饮"},
{Keyword: "面", Category: "餐饮"}, {Keyword: "粉", Category: "餐饮"}, {Keyword: "粥", Category: "餐饮"},
{Keyword: "火锅", Category: "餐饮"}, {Keyword: "烧烤", Category: "餐饮"}, {Keyword: "烤肉", Category: "餐饮"},
{Keyword: "外卖", Category: "餐饮"}, {Keyword: "点餐", Category: "餐饮"}, {Keyword: "宵夜", Category: "餐饮"},
{Keyword: "夜宵", Category: "餐饮"}, {Keyword: "小吃", Category: "餐饮"}, {Keyword: "快餐", Category: "餐饮"},
{Keyword: "饺子", Category: "餐饮"}, {Keyword: "面条", Category: "餐饮"}, {Keyword: "米饭", Category: "餐饮"},
{Keyword: "菜", Category: "餐饮"}, {Keyword: "肉", Category: "餐饮"}, {Keyword: "鱼", Category: "餐饮"},
{Keyword: "鸡", Category: "餐饮"}, {Keyword: "蛋", Category: "餐饮"}, {Keyword: "汤", Category: "餐饮"},
{Keyword: "麻辣烫", Category: "餐饮"}, {Keyword: "炒饭", Category: "餐饮"}, {Keyword: "盖饭", Category: "餐饮"},
{Keyword: "包子", Category: "餐饮"}, {Keyword: "馒头", Category: "餐饮"}, {Keyword: "饼", Category: "餐饮"},
{Keyword: "食堂", Category: "餐饮"}, {Keyword: "餐厅", Category: "餐饮"}, {Keyword: "饭店", Category: "餐饮"},
{Keyword: "美团", Category: "餐饮"}, {Keyword: "饿了么", Category: "餐饮"},
// 交通
{Keyword: "打车", Category: "交通"}, {Keyword: "车费", Category: "交通"}, {Keyword: "出租车", Category: "交通"},
{Keyword: "滴滴", Category: "交通"}, {Keyword: "公交", Category: "交通"}, {Keyword: "地铁", Category: "交通"},
{Keyword: "高铁", Category: "交通"}, {Keyword: "火车", Category: "交通"}, {Keyword: "飞机", Category: "交通"},
{Keyword: "机票", Category: "交通"}, {Keyword: "车票", Category: "交通"}, {Keyword: "船票", Category: "交通"},
{Keyword: "加油", Category: "交通"}, {Keyword: "油费", Category: "交通"}, {Keyword: "停车", Category: "交通"},
{Keyword: "停车费", Category: "交通"}, {Keyword: "过路费", Category: "交通"}, {Keyword: "高速", Category: "交通"},
{Keyword: "骑车", Category: "交通"}, {Keyword: "单车", Category: "交通"}, {Keyword: "共享", Category: "交通"},
{Keyword: "顺风车", Category: "交通"}, {Keyword: "快车", Category: "交通"}, {Keyword: "专车", Category: "交通"},
{Keyword: "拼车", Category: "交通"}, {Keyword: "出行", Category: "交通"}, {Keyword: "通勤", Category: "交通"},
// 购物
{Keyword: "买", Category: "购物"}, {Keyword: "购物", Category: "购物"}, {Keyword: "淘宝", Category: "购物"},
{Keyword: "京东", Category: "购物"}, {Keyword: "拼多多", Category: "购物"}, {Keyword: "网购", Category: "购物"},
{Keyword: "超市", Category: "购物"}, {Keyword: "商场", Category: "购物"}, {Keyword: "衣服", Category: "购物"},
{Keyword: "鞋", Category: "购物"}, {Keyword: "裤子", Category: "购物"}, {Keyword: "裙子", Category: "购物"},
{Keyword: "包", Category: "购物"}, {Keyword: "手机", Category: "购物"}, {Keyword: "电脑", Category: "购物"},
{Keyword: "日用品", Category: "购物"}, {Keyword: "生活用品", Category: "购物"},
// 饮品
{Keyword: "咖啡", Category: "饮品"}, {Keyword: "奶茶", Category: "饮品"}, {Keyword: "茶", Category: "饮品"},
{Keyword: "饮料", Category: "饮品"}, {Keyword: "水", Category: "饮品"}, {Keyword: "果汁", Category: "饮品"},
{Keyword: "星巴克", Category: "饮品"}, {Keyword: "瑞幸", Category: "饮品"}, {Keyword: "喜茶", Category: "饮品"},
{Keyword: "蜜雪", Category: "饮品"}, {Keyword: "可乐", Category: "饮品"}, {Keyword: "啤酒", Category: "饮品"},
{Keyword: "酒", Category: "饮品"}, {Keyword: "牛奶", Category: "饮品"},
// 水果
{Keyword: "水果", Category: "水果"}, {Keyword: "苹果", Category: "水果"}, {Keyword: "香蕉", Category: "水果"},
{Keyword: "橘子", Category: "水果"}, {Keyword: "橙子", Category: "水果"}, {Keyword: "葡萄", Category: "水果"},
{Keyword: "西瓜", Category: "水果"}, {Keyword: "草莓", Category: "水果"}, {Keyword: "芒果", Category: "水果"},
// 零食
{Keyword: "零食", Category: "零食"}, {Keyword: "薯片", Category: "零食"}, {Keyword: "糖", Category: "零食"},
{Keyword: "巧克力", Category: "零食"}, {Keyword: "饼干", Category: "零食"}, {Keyword: "面包", Category: "零食"},
{Keyword: "蛋糕", Category: "零食"}, {Keyword: "甜品", Category: "零食"}, {Keyword: "甜点", Category: "零食"},
// 住房
{Keyword: "房租", Category: "住房"}, {Keyword: "租房", Category: "住房"}, {Keyword: "水电", Category: "住房"},
{Keyword: "电费", Category: "住房"}, {Keyword: "水费", Category: "住房"}, {Keyword: "燃气", Category: "住房"},
{Keyword: "物业", Category: "住房"}, {Keyword: "宽带", Category: "住房"}, {Keyword: "网费", Category: "住房"},
// 通讯
{Keyword: "话费", Category: "通讯"}, {Keyword: "流量", Category: "通讯"}, {Keyword: "充值", Category: "通讯"},
{Keyword: "手机费", Category: "通讯"},
// 医疗
{Keyword: "看病", Category: "医疗"}, {Keyword: "药", Category: "医疗"}, {Keyword: "医院", Category: "医疗"},
{Keyword: "挂号", Category: "医疗"}, {Keyword: "体检", Category: "医疗"}, {Keyword: "医疗", Category: "医疗"},
{Keyword: "门诊", Category: "医疗"}, {Keyword: "牙", Category: "医疗"},
// 娱乐
{Keyword: "电影", Category: "娱乐"}, {Keyword: "游戏", Category: "娱乐"}, {Keyword: "KTV", Category: "娱乐"},
{Keyword: "唱歌", Category: "娱乐"}, {Keyword: "旅游", Category: "娱乐"}, {Keyword: "景点", Category: "娱乐"},
{Keyword: "门票", Category: "娱乐"}, {Keyword: "健身", Category: "娱乐"}, {Keyword: "运动", Category: "娱乐"},
{Keyword: "会员", Category: "娱乐"}, {Keyword: "VIP", Category: "娱乐"},
// 教育
{Keyword: "书", Category: "教育"}, {Keyword: "课", Category: "教育"}, {Keyword: "培训", Category: "教育"},
{Keyword: "学费", Category: "教育"}, {Keyword: "考试", Category: "教育"}, {Keyword: "学习", Category: "教育"},
// 烟酒
{Keyword: "烟", Category: "烟酒"}, {Keyword: "香烟", Category: "烟酒"}, {Keyword: "白酒", Category: "烟酒"},
{Keyword: "红酒", Category: "烟酒"},
// 红包/转账
{Keyword: "红包", Category: "红包"}, {Keyword: "转账", Category: "转账"}, {Keyword: "借", Category: "转账"},
{Keyword: "还钱", Category: "转账"},
// 宠物
{Keyword: "猫粮", Category: "宠物"}, {Keyword: "狗粮", Category: "宠物"}, {Keyword: "宠物", Category: "宠物"},
}
return db.CreateInBatches(keywords, 50).Error
}