{"repo": "canghai908/zbxtable", "files": {"internal/model/config.go": {"bcdc3997c4da6f884243c76a79541b3930b0018d": {"t": 1785745616.5485866, "n": 3886, "h": "b8bfd9591b590404", "c": "package model\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\t\"zbxtable/pkg/logger\"\n\t\"zbxtable/pkg/utils\"\n)\n\n// TableName alarm\nfunc (t *Config) TableName() string {\n\treturn TableName(\"config\")\n}\n\ntype Config struct {\n\tID int64 `gorm:\"column:id;primaryKey;autoIncrement\" json:\"id\"`\n\tName string `gorm:\"column:name;size:255\" json:\"name\"`\n\tConfigKey string `gorm:\"column:config_key;size:255\" json:\"config_key\"`\n\tConfigValue string `gorm:\"column:config_value;type:text\" json:\"config_value\"`\n\tComment string `gorm:\"column:comment;size:255\" json:\"comment\"`\n\tCreatedAt time.Time `gorm:\"column:created_at;autoCreateTime\" json:\"created_at\"`\n\tUpdatedAt time.Time `gorm:\"column:updated_at;autoUpdateTime\" json:\"updated_at\"`\n}\n\nfunc isSensitiveConfigKey(key string) bool {\n\tswitch key {\n\tcase \"email_secret\", \"wechat_secret\", \"deepseek_api_key\", \"custom_api_key\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc encryptConfigValueIfSensitive(key, value, encryptionKey string) (string, error) {\n\tif !isSensitiveConfigKey(key) || value == \"\" {\n\t\treturn value, nil\n\t}\n\n\tencryptedValue, err := utils.EncryptString(value, encryptionKey)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn encryptedValue, nil\n}\n\nfunc decryptConfigValueIfSensitive(key, value, encryptionKey string) (string, error) {\n\tif !isSensitiveConfigKey(key) || value == \"\" {\n\t\treturn value, nil\n\t}\n\n\tdecryptedValue, err := utils.DecryptString(value, encryptionKey)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn decryptedValue, nil\n}\n\n// GetConfigList 获取系统配置\nfunc GetConfigList() ([]Config, error) {\n\tvar v []Config\n\terr := DB.Order(\"id\").Find(&v).Error\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn v, nil\n}\n\n// GetConfigOne 获取某一个配置\nfunc GetConfigOne(id int64) (v *Config, err error) {\n\tv = &Config{}\n\terr = DB.Where(\"id = ?\", id).First(v).Error\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn v, nil\n}\n\n// UpdateConfig 更新系统配置\nfunc UpdateConfig(m *Config) (err error) {\n\tvar v Config\n\t//数据面板独立配置\n\terr = DB.Where(\"id = ?\", m.ID).First(&v).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// 禁止修改加密密钥\n\tif v.ConfigKey == \"encryption_key\" {\n\t\treturn errors.New(\"加密密钥不允许修改,如需更换请联系系统管理员\")\n\t}\n\n\tif IsTaskConfigKey(v.ConfigKey) {\n\t\tif err := ValidateTaskConfigValue(v.ConfigKey, m.ConfigValue); err != nil {\n\t\t\treturn fmt.Errorf(\"计划任务配置无效: %w\", err)\n\t\t}\n\t}\n\n\t// 对敏感字段进行加密\n\tvalueToSave, err := encryptConfigValueIfSensitive(v.ConfigKey, m.ConfigValue, GetEncryptionKey())\n\tif err != nil {\n\t\treturn errors.New(\"加密失败: \" + err.Error())\n\t}\n\n\terr = DB.Model(&Config{}).Where(\"id = ?\", m.ID).Update(\"config_value\", valueToSave).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif IsTaskConfigKey(v.ConfigKey) {\n\t\tReloadTaskScheduler()\n\t}\n\n\t//dash处理\n\tif m.ID == 1 {\n\t\terr = updateZbxDash(m)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t}\n\treturn nil\n}\n\n// 数据面板控制\nfunc updateZbxDash(m *Config) (err error) {\n\tvar menu Menu\n\terr = DB.Where(\"router = ?\", \"dash\").First(&menu).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\tswitch m.ConfigValue {\n\tcase \"0\":\n\t\tmenu.IsAvailable = true\n\tcase \"1\":\n\t\tmenu.IsAvailable = false\n\t}\n\terr = DB.Model(&Menu{}).Where(\"router = ?\", \"dash\").Update(\"is_available\", menu.IsAvailable).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// GetConfigValueByKey 根据 key 获取配置值,如果不存在或出错则返回默认值\n// 对于敏感字段会自动解密\nfunc GetConfigValueByKey(key string, defaultVal string) string {\n\t// 如果数据库未初始化,直接返回默认值\n\tif DB == nil {\n\t\treturn defaultVal\n\t}\n\n\tvar c Config\n\terr := DB.Where(\"config_key = ?\", key).First(&c).Error\n\tif err != nil || c.ConfigValue == \"\" {\n\t\treturn defaultVal\n\t}\n\n\t// 如果是敏感字段,尝试解密\n\tif isSensitiveConfigKey(key) && c.ConfigValue != \"\" {\n\t\tdecryptedValue, err := decryptConfigValueIfSensitive(key, c.ConfigValue, GetEncryptionKey())\n\t\tif err != nil {\n\t\t\t// 解密失败,可能是旧数据未加密,直接返回原值\n\t\t\tlogger.Log.Warnf(\"解密配置 %s 失败,返回原值: %v\", key, err)\n\t\t\treturn c.ConfigValue\n\t\t}\n\t\treturn decryptedValue\n\t}\n\n\treturn c.ConfigValue\n}\n"}, "b49dcd882eb1a3ea6101355bcbcb0c34733cbc2c": {"t": 1785754886.239426, "n": 3890, "h": "2a2805f919a0710c", "c": "package model\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\t\"zbxtable/pkg/logger\"\n\t\"zbxtable/pkg/utils\"\n)\n\n// TableName alarm\nfunc (t *Config) TableName() string {\n\treturn TableName(\"config\")\n}\n\ntype Config struct {\n\tID int64 `gorm:\"column:id;primaryKey;autoIncrement\" json:\"id\"`\n\tName string `gorm:\"column:name;size:255\" json:\"name\"`\n\tConfigKey string `gorm:\"column:config_key;size:255\" json:\"config_key\"`\n\tConfigValue string `gorm:\"column:config_value;type:longtext\" json:\"config_value\"`\n\tComment string `gorm:\"column:comment;size:255\" json:\"comment\"`\n\tCreatedAt time.Time `gorm:\"column:created_at;autoCreateTime\" json:\"created_at\"`\n\tUpdatedAt time.Time `gorm:\"column:updated_at;autoUpdateTime\" json:\"updated_at\"`\n}\n\nfunc isSensitiveConfigKey(key string) bool {\n\tswitch key {\n\tcase \"email_secret\", \"wechat_secret\", \"deepseek_api_key\", \"custom_api_key\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc encryptConfigValueIfSensitive(key, value, encryptionKey string) (string, error) {\n\tif !isSensitiveConfigKey(key) || value == \"\" {\n\t\treturn value, nil\n\t}\n\n\tencryptedValue, err := utils.EncryptString(value, encryptionKey)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn encryptedValue, nil\n}\n\nfunc decryptConfigValueIfSensitive(key, value, encryptionKey string) (string, error) {\n\tif !isSensitiveConfigKey(key) || value == \"\" {\n\t\treturn value, nil\n\t}\n\n\tdecryptedValue, err := utils.DecryptString(value, encryptionKey)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn decryptedValue, nil\n}\n\n// GetConfigList 获取系统配置\nfunc GetConfigList() ([]Config, error) {\n\tvar v []Config\n\terr := DB.Order(\"id\").Find(&v).Error\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn v, nil\n}\n\n// GetConfigOne 获取某一个配置\nfunc GetConfigOne(id int64) (v *Config, err error) {\n\tv = &Config{}\n\terr = DB.Where(\"id = ?\", id).First(v).Error\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn v, nil\n}\n\n// UpdateConfig 更新系统配置\nfunc UpdateConfig(m *Config) (err error) {\n\tvar v Config\n\t//数据面板独立配置\n\terr = DB.Where(\"id = ?\", m.ID).First(&v).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// 禁止修改加密密钥\n\tif v.ConfigKey == \"encryption_key\" {\n\t\treturn errors.New(\"加密密钥不允许修改,如需更换请联系系统管理员\")\n\t}\n\n\tif IsTaskConfigKey(v.ConfigKey) {\n\t\tif err := ValidateTaskConfigValue(v.ConfigKey, m.ConfigValue); err != nil {\n\t\t\treturn fmt.Errorf(\"计划任务配置无效: %w\", err)\n\t\t}\n\t}\n\n\t// 对敏感字段进行加密\n\tvalueToSave, err := encryptConfigValueIfSensitive(v.ConfigKey, m.ConfigValue, GetEncryptionKey())\n\tif err != nil {\n\t\treturn errors.New(\"加密失败: \" + err.Error())\n\t}\n\n\terr = DB.Model(&Config{}).Where(\"id = ?\", m.ID).Update(\"config_value\", valueToSave).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif IsTaskConfigKey(v.ConfigKey) {\n\t\tReloadTaskScheduler()\n\t}\n\n\t//dash处理\n\tif m.ID == 1 {\n\t\terr = updateZbxDash(m)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t}\n\treturn nil\n}\n\n// 数据面板控制\nfunc updateZbxDash(m *Config) (err error) {\n\tvar menu Menu\n\terr = DB.Where(\"router = ?\", \"dash\").First(&menu).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\tswitch m.ConfigValue {\n\tcase \"0\":\n\t\tmenu.IsAvailable = true\n\tcase \"1\":\n\t\tmenu.IsAvailable = false\n\t}\n\terr = DB.Model(&Menu{}).Where(\"router = ?\", \"dash\").Update(\"is_available\", menu.IsAvailable).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// GetConfigValueByKey 根据 key 获取配置值,如果不存在或出错则返回默认值\n// 对于敏感字段会自动解密\nfunc GetConfigValueByKey(key string, defaultVal string) string {\n\t// 如果数据库未初始化,直接返回默认值\n\tif DB == nil {\n\t\treturn defaultVal\n\t}\n\n\tvar c Config\n\terr := DB.Where(\"config_key = ?\", key).First(&c).Error\n\tif err != nil || c.ConfigValue == \"\" {\n\t\treturn defaultVal\n\t}\n\n\t// 如果是敏感字段,尝试解密\n\tif isSensitiveConfigKey(key) && c.ConfigValue != \"\" {\n\t\tdecryptedValue, err := decryptConfigValueIfSensitive(key, c.ConfigValue, GetEncryptionKey())\n\t\tif err != nil {\n\t\t\t// 解密失败,可能是旧数据未加密,直接返回原值\n\t\t\tlogger.Log.Warnf(\"解密配置 %s 失败,返回原值: %v\", key, err)\n\t\t\treturn c.ConfigValue\n\t\t}\n\t\treturn decryptedValue\n\t}\n\n\treturn c.ConfigValue\n}\n"}, "6a555a99b0280f3cd63b1281b32175f5525ca6b2": {"t": 1785754888.0065823, "n": 3886, "h": "b8bfd9591b590404", "c": "package model\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\t\"zbxtable/pkg/logger\"\n\t\"zbxtable/pkg/utils\"\n)\n\n// TableName alarm\nfunc (t *Config) TableName() string {\n\treturn TableName(\"config\")\n}\n\ntype Config struct {\n\tID int64 `gorm:\"column:id;primaryKey;autoIncrement\" json:\"id\"`\n\tName string `gorm:\"column:name;size:255\" json:\"name\"`\n\tConfigKey string `gorm:\"column:config_key;size:255\" json:\"config_key\"`\n\tConfigValue string `gorm:\"column:config_value;type:text\" json:\"config_value\"`\n\tComment string `gorm:\"column:comment;size:255\" json:\"comment\"`\n\tCreatedAt time.Time `gorm:\"column:created_at;autoCreateTime\" json:\"created_at\"`\n\tUpdatedAt time.Time `gorm:\"column:updated_at;autoUpdateTime\" json:\"updated_at\"`\n}\n\nfunc isSensitiveConfigKey(key string) bool {\n\tswitch key {\n\tcase \"email_secret\", \"wechat_secret\", \"deepseek_api_key\", \"custom_api_key\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc encryptConfigValueIfSensitive(key, value, encryptionKey string) (string, error) {\n\tif !isSensitiveConfigKey(key) || value == \"\" {\n\t\treturn value, nil\n\t}\n\n\tencryptedValue, err := utils.EncryptString(value, encryptionKey)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn encryptedValue, nil\n}\n\nfunc decryptConfigValueIfSensitive(key, value, encryptionKey string) (string, error) {\n\tif !isSensitiveConfigKey(key) || value == \"\" {\n\t\treturn value, nil\n\t}\n\n\tdecryptedValue, err := utils.DecryptString(value, encryptionKey)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn decryptedValue, nil\n}\n\n// GetConfigList 获取系统配置\nfunc GetConfigList() ([]Config, error) {\n\tvar v []Config\n\terr := DB.Order(\"id\").Find(&v).Error\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn v, nil\n}\n\n// GetConfigOne 获取某一个配置\nfunc GetConfigOne(id int64) (v *Config, err error) {\n\tv = &Config{}\n\terr = DB.Where(\"id = ?\", id).First(v).Error\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn v, nil\n}\n\n// UpdateConfig 更新系统配置\nfunc UpdateConfig(m *Config) (err error) {\n\tvar v Config\n\t//数据面板独立配置\n\terr = DB.Where(\"id = ?\", m.ID).First(&v).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// 禁止修改加密密钥\n\tif v.ConfigKey == \"encryption_key\" {\n\t\treturn errors.New(\"加密密钥不允许修改,如需更换请联系系统管理员\")\n\t}\n\n\tif IsTaskConfigKey(v.ConfigKey) {\n\t\tif err := ValidateTaskConfigValue(v.ConfigKey, m.ConfigValue); err != nil {\n\t\t\treturn fmt.Errorf(\"计划任务配置无效: %w\", err)\n\t\t}\n\t}\n\n\t// 对敏感字段进行加密\n\tvalueToSave, err := encryptConfigValueIfSensitive(v.ConfigKey, m.ConfigValue, GetEncryptionKey())\n\tif err != nil {\n\t\treturn errors.New(\"加密失败: \" + err.Error())\n\t}\n\n\terr = DB.Model(&Config{}).Where(\"id = ?\", m.ID).Update(\"config_value\", valueToSave).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif IsTaskConfigKey(v.ConfigKey) {\n\t\tReloadTaskScheduler()\n\t}\n\n\t//dash处理\n\tif m.ID == 1 {\n\t\terr = updateZbxDash(m)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t}\n\treturn nil\n}\n\n// 数据面板控制\nfunc updateZbxDash(m *Config) (err error) {\n\tvar menu Menu\n\terr = DB.Where(\"router = ?\", \"dash\").First(&menu).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\tswitch m.ConfigValue {\n\tcase \"0\":\n\t\tmenu.IsAvailable = true\n\tcase \"1\":\n\t\tmenu.IsAvailable = false\n\t}\n\terr = DB.Model(&Menu{}).Where(\"router = ?\", \"dash\").Update(\"is_available\", menu.IsAvailable).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// GetConfigValueByKey 根据 key 获取配置值,如果不存在或出错则返回默认值\n// 对于敏感字段会自动解密\nfunc GetConfigValueByKey(key string, defaultVal string) string {\n\t// 如果数据库未初始化,直接返回默认值\n\tif DB == nil {\n\t\treturn defaultVal\n\t}\n\n\tvar c Config\n\terr := DB.Where(\"config_key = ?\", key).First(&c).Error\n\tif err != nil || c.ConfigValue == \"\" {\n\t\treturn defaultVal\n\t}\n\n\t// 如果是敏感字段,尝试解密\n\tif isSensitiveConfigKey(key) && c.ConfigValue != \"\" {\n\t\tdecryptedValue, err := decryptConfigValueIfSensitive(key, c.ConfigValue, GetEncryptionKey())\n\t\tif err != nil {\n\t\t\t// 解密失败,可能是旧数据未加密,直接返回原值\n\t\t\tlogger.Log.Warnf(\"解密配置 %s 失败,返回原值: %v\", key, err)\n\t\t\treturn c.ConfigValue\n\t\t}\n\t\treturn decryptedValue\n\t}\n\n\treturn c.ConfigValue\n}\n"}, "da91f1a55b83b5d0bfe3e4cb33dec689c3a35d5f": {"t": 1785754889.6544616, "n": 3845, "h": "32760e5148c28549", "c": "package model\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\t\"zbxtable/pkg/logger\"\n\t\"zbxtable/pkg/utils\"\n)\n\n// TableName alarm\nfunc (t *Config) TableName() string {\n\treturn TableName(\"config\")\n}\n\ntype Config struct {\n\tID int64 `gorm:\"column:id;primaryKey;autoIncrement\" json:\"id\"`\n\tName string `gorm:\"column:name;size:255\" json:\"name\"`\n\tConfigKey string `gorm:\"column:config_key;size:255\" json:\"config_key\"`\n\tConfigValue string `gorm:\"column:config_value;type:text\" json:\"config_value\"`\n\tComment string `gorm:\"column:comment;size:255\" json:\"comment\"`\n\tCreatedAt time.Time `gorm:\"column:created_at;autoCreateTime\" json:\"created_at\"`\n\tUpdatedAt time.Time `gorm:\"column:updated_at;autoUpdateTime\" json:\"updated_at\"`\n}\n\n// GetConfigList 获取系统配置\nfunc GetConfigList() ([]Config, error) {\n\tvar v []Config\n\terr := DB.Order(\"id\").Find(&v).Error\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn v, nil\n}\n\n// GetConfigOne 获取某一个配置\nfunc GetConfigOne(id int64) (v *Config, err error) {\n\tv = &Config{}\n\terr = DB.Where(\"id = ?\", id).First(v).Error\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn v, nil\n}\n\n// UpdateConfig 更新系统配置\nfunc UpdateConfig(m *Config) (err error) {\n\tvar v Config\n\t//数据面板独立配置\n\terr = DB.Where(\"id = ?\", m.ID).First(&v).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// 禁止修改加密密钥\n\tif v.ConfigKey == \"encryption_key\" {\n\t\treturn errors.New(\"加密密钥不允许修改,如需更换请联系系统管理员\")\n\t}\n\n\tif IsTaskConfigKey(v.ConfigKey) {\n\t\tif err := ValidateTaskConfigValue(v.ConfigKey, m.ConfigValue); err != nil {\n\t\t\treturn fmt.Errorf(\"计划任务配置无效: %w\", err)\n\t\t}\n\t}\n\n\t// 对敏感字段进行加密\n\tvalueToSave := m.ConfigValue\n\tsensitiveKeys := []string{\n\t\t\"email_secret\", // SMTP 密码/授权码\n\t\t\"wechat_secret\", // 企业微信 Secret\n\t\t\"deepseek_api_key\", // Deepseek API Key\n\t}\n\n\t// 检查是否是敏感字段\n\tisSensitive := false\n\tfor _, key := range sensitiveKeys {\n\t\tif v.ConfigKey == key {\n\t\t\tisSensitive = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// 如果是敏感字段且值不为空,进行加密\n\tif isSensitive && valueToSave != \"\" {\n\t\tencryptionKey := GetEncryptionKey()\n\t\tencryptedValue, err := utils.EncryptString(valueToSave, encryptionKey)\n\t\tif err != nil {\n\t\t\treturn errors.New(\"加密失败: \" + err.Error())\n\t\t}\n\t\tvalueToSave = encryptedValue\n\t}\n\n\terr = DB.Model(&Config{}).Where(\"id = ?\", m.ID).Update(\"config_value\", valueToSave).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif IsTaskConfigKey(v.ConfigKey) {\n\t\tReloadTaskScheduler()\n\t}\n\n\t//dash处理\n\tif m.ID == 1 {\n\t\terr = updateZbxDash(m)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t}\n\treturn nil\n}\n\n// 数据面板控制\nfunc updateZbxDash(m *Config) (err error) {\n\tvar menu Menu\n\terr = DB.Where(\"router = ?\", \"dash\").First(&menu).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\tswitch m.ConfigValue {\n\tcase \"0\":\n\t\tmenu.IsAvailable = true\n\tcase \"1\":\n\t\tmenu.IsAvailable = false\n\t}\n\terr = DB.Model(&Menu{}).Where(\"router = ?\", \"dash\").Update(\"is_available\", menu.IsAvailable).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// GetConfigValueByKey 根据 key 获取配置值,如果不存在或出错则返回默认值\n// 对于敏感字段会自动解密\nfunc GetConfigValueByKey(key string, defaultVal string) string {\n\t// 如果数据库未初始化,直接返回默认值\n\tif DB == nil {\n\t\treturn defaultVal\n\t}\n\n\tvar c Config\n\terr := DB.Where(\"config_key = ?\", key).First(&c).Error\n\tif err != nil || c.ConfigValue == \"\" {\n\t\treturn defaultVal\n\t}\n\n\t// 定义需要解密的敏感字段\n\tsensitiveKeys := []string{\n\t\t\"email_secret\", // SMTP 密码/授权码\n\t\t\"wechat_secret\", // 企业微信 Secret\n\t\t\"deepseek_api_key\", // Deepseek API Key\n\t}\n\n\t// 检查是否是敏感字段\n\tisSensitive := false\n\tfor _, skey := range sensitiveKeys {\n\t\tif key == skey {\n\t\t\tisSensitive = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// 如果是敏感字段,尝试解密\n\tif isSensitive && c.ConfigValue != \"\" {\n\t\tencryptionKey := GetEncryptionKey()\n\t\tdecryptedValue, err := utils.DecryptString(c.ConfigValue, encryptionKey)\n\t\tif err != nil {\n\t\t\t// 解密失败,可能是旧数据未加密,直接返回原值\n\t\t\tlogger.Log.Warnf(\"解密配置 %s 失败,返回原值: %v\", key, err)\n\t\t\treturn c.ConfigValue\n\t\t}\n\t\treturn decryptedValue\n\t}\n\n\treturn c.ConfigValue\n}\n"}, "e95145171b92fcb09a4d1e3fba862a0525a92d18": {"t": 1785754891.5727715, "n": 3611, "h": "3910281fa23d402c", "c": "package model\n\nimport (\n\t\"errors\"\n\t\"time\"\n\t\"zbxtable/pkg/logger\"\n\t\"zbxtable/pkg/utils\"\n)\n\n// TableName alarm\nfunc (t *Config) TableName() string {\n\treturn TableName(\"config\")\n}\n\ntype Config struct {\n\tID int64 `gorm:\"column:id;primaryKey;autoIncrement\" json:\"id\"`\n\tName string `gorm:\"column:name;size:255\" json:\"name\"`\n\tConfigKey string `gorm:\"column:config_key;size:255\" json:\"config_key\"`\n\tConfigValue string `gorm:\"column:config_value;type:text\" json:\"config_value\"`\n\tComment string `gorm:\"column:comment;size:255\" json:\"comment\"`\n\tCreatedAt time.Time `gorm:\"column:created_at;autoCreateTime\" json:\"created_at\"`\n\tUpdatedAt time.Time `gorm:\"column:updated_at;autoUpdateTime\" json:\"updated_at\"`\n}\n\n// GetConfigList 获取系统配置\nfunc GetConfigList() ([]Config, error) {\n\tvar v []Config\n\terr := DB.Order(\"id\").Find(&v).Error\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn v, nil\n}\n\n// GetConfigOne 获取某一个配置\nfunc GetConfigOne(id int64) (v *Config, err error) {\n\tv = &Config{}\n\terr = DB.Where(\"id = ?\", id).First(v).Error\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn v, nil\n}\n\n// UpdateConfig 更新系统配置\nfunc UpdateConfig(m *Config) (err error) {\n\tvar v Config\n\t//数据面板独立配置\n\terr = DB.Where(\"id = ?\", m.ID).First(&v).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// 禁止修改加密密钥\n\tif v.ConfigKey == \"encryption_key\" {\n\t\treturn errors.New(\"加密密钥不允许修改,如需更换请联系系统管理员\")\n\t}\n\n\t// 对敏感字段进行加密\n\tvalueToSave := m.ConfigValue\n\tsensitiveKeys := []string{\n\t\t\"email_secret\", // SMTP 密码/授权码\n\t\t\"wechat_secret\", // 企业微信 Secret\n\t\t\"deepseek_api_key\", // Deepseek API Key\n\t}\n\n\t// 检查是否是敏感字段\n\tisSensitive := false\n\tfor _, key := range sensitiveKeys {\n\t\tif v.ConfigKey == key {\n\t\t\tisSensitive = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// 如果是敏感字段且值不为空,进行加密\n\tif isSensitive && valueToSave != \"\" {\n\t\tencryptionKey := GetEncryptionKey()\n\t\tencryptedValue, err := utils.EncryptString(valueToSave, encryptionKey)\n\t\tif err != nil {\n\t\t\treturn errors.New(\"加密失败: \" + err.Error())\n\t\t}\n\t\tvalueToSave = encryptedValue\n\t}\n\n\terr = DB.Model(&Config{}).Where(\"id = ?\", m.ID).Update(\"config_value\", valueToSave).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\t//dash处理\n\tif m.ID == 1 {\n\t\terr = updateZbxDash(m)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t}\n\treturn nil\n}\n\n// 数据面板控制\nfunc updateZbxDash(m *Config) (err error) {\n\tvar menu Menu\n\terr = DB.Where(\"router = ?\", \"dash\").First(&menu).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\tswitch m.ConfigValue {\n\tcase \"0\":\n\t\tmenu.IsAvailable = true\n\tcase \"1\":\n\t\tmenu.IsAvailable = false\n\t}\n\terr = DB.Model(&Menu{}).Where(\"router = ?\", \"dash\").Update(\"is_available\", menu.IsAvailable).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// GetConfigValueByKey 根据 key 获取配置值,如果不存在或出错则返回默认值\n// 对于敏感字段会自动解密\nfunc GetConfigValueByKey(key string, defaultVal string) string {\n\t// 如果数据库未初始化,直接返回默认值\n\tif DB == nil {\n\t\treturn defaultVal\n\t}\n\n\tvar c Config\n\terr := DB.Where(\"config_key = ?\", key).First(&c).Error\n\tif err != nil || c.ConfigValue == \"\" {\n\t\treturn defaultVal\n\t}\n\n\t// 定义需要解密的敏感字段\n\tsensitiveKeys := []string{\n\t\t\"email_secret\", // SMTP 密码/授权码\n\t\t\"wechat_secret\", // 企业微信 Secret\n\t\t\"deepseek_api_key\", // Deepseek API Key\n\t}\n\n\t// 检查是否是敏感字段\n\tisSensitive := false\n\tfor _, skey := range sensitiveKeys {\n\t\tif key == skey {\n\t\t\tisSensitive = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// 如果是敏感字段,尝试解密\n\tif isSensitive && c.ConfigValue != \"\" {\n\t\tencryptionKey := GetEncryptionKey()\n\t\tdecryptedValue, err := utils.DecryptString(c.ConfigValue, encryptionKey)\n\t\tif err != nil {\n\t\t\t// 解密失败,可能是旧数据未加密,直接返回原值\n\t\t\tlogger.Log.Warnf(\"解密配置 %s 失败,返回原值: %v\", key, err)\n\t\t\treturn c.ConfigValue\n\t\t}\n\t\treturn decryptedValue\n\t}\n\n\treturn c.ConfigValue\n}\n"}, "8bfb5b8ac688fa9cae258e25cc7b5155ac6dc950": {"t": 1785754893.8404887, "n": 3503, "h": "d8ce3358856d73be", "c": "package model\n\nimport (\n\t\"errors\"\n\t\"time\"\n\t\"zbxtable/pkg/logger\"\n\t\"zbxtable/pkg/utils\"\n)\n\n// TableName alarm\nfunc (t *Config) TableName() string {\n\treturn TableName(\"config\")\n}\n\ntype Config struct {\n\tID int64 `gorm:\"column:id;primaryKey;autoIncrement\" json:\"id\"`\n\tName string `gorm:\"column:name;size:255\" json:\"name\"`\n\tKey string `gorm:\"column:key;size:255\" json:\"key\"`\n\tValue string `gorm:\"column:value;type:text\" json:\"value\"`\n\tComment string `gorm:\"column:comment;size:255\" json:\"comment\"`\n\tCreatedAt time.Time `gorm:\"column:created_at;autoCreateTime\" json:\"created_at\"`\n\tUpdatedAt time.Time `gorm:\"column:updated_at;autoUpdateTime\" json:\"updated_at\"`\n}\n\n// GetConfigList 获取系统配置\nfunc GetConfigList() ([]Config, error) {\n\tvar v []Config\n\terr := DB.Order(\"id\").Find(&v).Error\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn v, nil\n}\n\n// GetConfigOne 获取某一个配置\nfunc GetConfigOne(id int64) (v *Config, err error) {\n\tv = &Config{}\n\terr = DB.Where(\"id = ?\", id).First(v).Error\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn v, nil\n}\n\n// UpdateConfig 更新系统配置\nfunc UpdateConfig(m *Config) (err error) {\n\tvar v Config\n\t//数据面板独立配置\n\terr = DB.Where(\"id = ?\", m.ID).First(&v).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// 禁止修改加密密钥\n\tif v.Key == \"encryption_key\" {\n\t\treturn errors.New(\"加密密钥不允许修改,如需更换请联系系统管理员\")\n\t}\n\n\t// 对敏感字段进行加密\n\tvalueToSave := m.Value\n\tsensitiveKeys := []string{\n\t\t\"email_secret\", // SMTP 密码/授权码\n\t\t\"wechat_secret\", // 企业微信 Secret\n\t\t\"deepseek_api_key\", // Deepseek API Key\n\t}\n\n\t// 检查是否是敏感字段\n\tisSensitive := false\n\tfor _, key := range sensitiveKeys {\n\t\tif v.Key == key {\n\t\t\tisSensitive = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// 如果是敏感字段且值不为空,进行加密\n\tif isSensitive && valueToSave != \"\" {\n\t\tencryptionKey := GetEncryptionKey()\n\t\tencryptedValue, err := utils.EncryptString(valueToSave, encryptionKey)\n\t\tif err != nil {\n\t\t\treturn errors.New(\"加密失败: \" + err.Error())\n\t\t}\n\t\tvalueToSave = encryptedValue\n\t}\n\n\terr = DB.Model(&Config{}).Where(\"id = ?\", m.ID).Update(\"value\", valueToSave).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\t//dash处理\n\tif m.ID == 1 {\n\t\terr = updateZbxDash(m)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t}\n\treturn nil\n}\n\n// 数据面板控制\nfunc updateZbxDash(m *Config) (err error) {\n\tvar menu Menu\n\terr = DB.Where(\"router = ?\", \"dash\").First(&menu).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\tswitch m.Value {\n\tcase \"0\":\n\t\tmenu.IsAvailable = true\n\tcase \"1\":\n\t\tmenu.IsAvailable = false\n\t}\n\terr = DB.Model(&Menu{}).Where(\"router = ?\", \"dash\").Update(\"is_available\", menu.IsAvailable).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// GetConfigValueByKey 根据 key 获取配置值,如果不存在或出错则返回默认值\n// 对于敏感字段会自动解密\nfunc GetConfigValueByKey(key string, defaultVal string) string {\n\t// 如果数据库未初始化,直接返回默认值\n\tif DB == nil {\n\t\treturn defaultVal\n\t}\n\n\tvar c Config\n\terr := DB.Where(\"`key` = ?\", key).First(&c).Error\n\tif err != nil || c.Value == \"\" {\n\t\treturn defaultVal\n\t}\n\n\t// 定义需要解密的敏感字段\n\tsensitiveKeys := []string{\n\t\t\"email_secret\", // SMTP 密码/授权码\n\t\t\"wechat_secret\", // 企业微信 Secret\n\t\t\"deepseek_api_key\", // Deepseek API Key\n\t}\n\n\t// 检查是否是敏感字段\n\tisSensitive := false\n\tfor _, skey := range sensitiveKeys {\n\t\tif key == skey {\n\t\t\tisSensitive = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// 如果是敏感字段,尝试解密\n\tif isSensitive && c.Value != \"\" {\n\t\tencryptionKey := GetEncryptionKey()\n\t\tdecryptedValue, err := utils.DecryptString(c.Value, encryptionKey)\n\t\tif err != nil {\n\t\t\t// 解密失败,可能是旧数据未加密,直接返回原值\n\t\t\tlogger.Log.Warnf(\"解密配置 %s 失败,返回原值: %v\", key, err)\n\t\t\treturn c.Value\n\t\t}\n\t\treturn decryptedValue\n\t}\n\n\treturn c.Value\n}\n"}, "e996ccfc882ad5a0cf03b6b98ef5ca1990d7a5be": {"t": 1785754895.4237347, "n": 3441, "h": "18700b206e476295", "c": "package model\n\nimport (\n\t\"errors\"\n\t\"time\"\n\t\"zbxtable/pkg/logger\"\n\t\"zbxtable/pkg/utils\"\n)\n\n// TableName alarm\nfunc (t *Config) TableName() string {\n\treturn TableName(\"config\")\n}\n\ntype Config struct {\n\tID int64 `gorm:\"column:id;primaryKey;autoIncrement\" json:\"id\"`\n\tName string `gorm:\"column:name;size:255\" json:\"name\"`\n\tKey string `gorm:\"column:key;size:255\" json:\"key\"`\n\tValue string `gorm:\"column:value;type:text\" json:\"value\"`\n\tComment string `gorm:\"column:comment;size:255\" json:\"comment\"`\n\tCreatedAt time.Time `gorm:\"column:created_at;autoCreateTime\" json:\"created_at\"`\n\tUpdatedAt time.Time `gorm:\"column:updated_at;autoUpdateTime\" json:\"updated_at\"`\n}\n\n// GetConfigList 获取系统配置\nfunc GetConfigList() ([]Config, error) {\n\tvar v []Config\n\terr := DB.Order(\"id\").Find(&v).Error\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn v, nil\n}\n\n// GetConfigOne 获取某一个配置\nfunc GetConfigOne(id int64) (v *Config, err error) {\n\tv = &Config{}\n\terr = DB.Where(\"id = ?\", id).First(v).Error\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn v, nil\n}\n\n// UpdateConfig 更新系统配置\nfunc UpdateConfig(m *Config) (err error) {\n\tvar v Config\n\t//数据面板独立配置\n\terr = DB.Where(\"id = ?\", m.ID).First(&v).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// 禁止修改加密密钥\n\tif v.Key == \"encryption_key\" {\n\t\treturn errors.New(\"加密密钥不允许修改,如需更换请联系系统管理员\")\n\t}\n\n\t// 对敏感字段进行加密\n\tvalueToSave := m.Value\n\tsensitiveKeys := []string{\n\t\t\"email_secret\", // SMTP 密码/授权码\n\t\t\"wechat_secret\", // 企业微信 Secret\n\t\t\"deepseek_api_key\", // Deepseek API Key\n\t}\n\n\t// 检查是否是敏感字段\n\tisSensitive := false\n\tfor _, key := range sensitiveKeys {\n\t\tif v.Key == key {\n\t\t\tisSensitive = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// 如果是敏感字段且值不为空,进行加密\n\tif isSensitive && valueToSave != \"\" {\n\t\tencryptionKey := GetEncryptionKey()\n\t\tencryptedValue, err := utils.EncryptString(valueToSave, encryptionKey)\n\t\tif err != nil {\n\t\t\treturn errors.New(\"加密失败: \" + err.Error())\n\t\t}\n\t\tvalueToSave = encryptedValue\n\t}\n\n\terr = DB.Model(&Config{}).Where(\"id = ?\", m.ID).Update(\"value\", valueToSave).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\t//dash处理\n\tif m.ID == 1 {\n\t\terr = updateZbxDash(m)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t}\n\treturn nil\n}\n\n// 数据面板控制\nfunc updateZbxDash(m *Config) (err error) {\n\tvar menu Menu\n\terr = DB.Where(\"router = ?\", \"dash\").First(&menu).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\tswitch m.Value {\n\tcase \"0\":\n\t\tmenu.IsAvailable = true\n\tcase \"1\":\n\t\tmenu.IsAvailable = false\n\t}\n\terr = DB.Model(&Menu{}).Where(\"router = ?\", \"dash\").Update(\"is_available\", menu.IsAvailable).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// GetConfigValueByKey 根据 key 获取配置值,如果不存在或出错则返回默认值\n// 对于敏感字段会自动解密\nfunc GetConfigValueByKey(key string, defaultVal string) string {\n\tvar c Config\n\terr := DB.Where(\"`key` = ?\", key).First(&c).Error\n\tif err != nil || c.Value == \"\" {\n\t\treturn defaultVal\n\t}\n\n\t// 定义需要解密的敏感字段\n\tsensitiveKeys := []string{\n\t\t\"email_secret\", // SMTP 密码/授权码\n\t\t\"wechat_secret\", // 企业微信 Secret\n\t\t\"deepseek_api_key\", // Deepseek API Key\n\t}\n\n\t// 检查是否是敏感字段\n\tisSensitive := false\n\tfor _, skey := range sensitiveKeys {\n\t\tif key == skey {\n\t\t\tisSensitive = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// 如果是敏感字段,尝试解密\n\tif isSensitive && c.Value != \"\" {\n\t\tencryptionKey := GetEncryptionKey()\n\t\tdecryptedValue, err := utils.DecryptString(c.Value, encryptionKey)\n\t\tif err != nil {\n\t\t\t// 解密失败,可能是旧数据未加密,直接返回原值\n\t\t\tlogger.Log.Warnf(\"解密配置 %s 失败,返回原值: %v\", key, err)\n\t\t\treturn c.Value\n\t\t}\n\t\treturn decryptedValue\n\t}\n\n\treturn c.Value\n}\n"}, "4471dd019ef1c3da2ce87879b8c842a858fdbae5": {"t": 1785754897.8812025, "n": 2180, "h": "87ae79b8132e0fae", "c": "package model\n\nimport (\n\t\"errors\"\n\t\"time\"\n)\n\n// TableName alarm\nfunc (t *Config) TableName() string {\n\treturn TableName(\"config\")\n}\n\ntype Config struct {\n\tID int64 `gorm:\"column:id;primaryKey;autoIncrement\" json:\"id\"`\n\tName string `gorm:\"column:name;size:255\" json:\"name\"`\n\tKey string `gorm:\"column:key;size:255\" json:\"key\"`\n\tValue string `gorm:\"column:value;type:text\" json:\"value\"`\n\tComment string `gorm:\"column:comment;size:255\" json:\"comment\"`\n\tCreatedAt time.Time `gorm:\"column:created_at;autoCreateTime\" json:\"created_at\"`\n\tUpdatedAt time.Time `gorm:\"column:updated_at;autoUpdateTime\" json:\"updated_at\"`\n}\n\n// GetConfigList 获取系统配置\nfunc GetConfigList() ([]Config, error) {\n\tvar v []Config\n\terr := DB.Order(\"id\").Find(&v).Error\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn v, nil\n}\n\n// GetConfigOne 获取某一个配置\nfunc GetConfigOne(id int64) (v *Config, err error) {\n\tv = &Config{}\n\terr = DB.Where(\"id = ?\", id).First(v).Error\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn v, nil\n}\n\n// UpdateConfig 更新系统配置\nfunc UpdateConfig(m *Config) (err error) {\n\tvar v Config\n\t//数据面板独立配置\n\terr = DB.Where(\"id = ?\", m.ID).First(&v).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// 禁止修改加密密钥\n\tif v.Key == \"encryption_key\" {\n\t\treturn errors.New(\"加密密钥不允许修改,如需更换请联系系统管理员\")\n\t}\n\n\terr = DB.Model(&Config{}).Where(\"id = ?\", m.ID).Update(\"value\", m.Value).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\t//dash处理\n\tif m.ID == 1 {\n\t\terr = updateZbxDash(m)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t}\n\treturn nil\n}\n\n// 数据面板控制\nfunc updateZbxDash(m *Config) (err error) {\n\tvar menu Menu\n\terr = DB.Where(\"router = ?\", \"dash\").First(&menu).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\tswitch m.Value {\n\tcase \"0\":\n\t\tmenu.IsAvailable = true\n\tcase \"1\":\n\t\tmenu.IsAvailable = false\n\t}\n\terr = DB.Model(&Menu{}).Where(\"router = ?\", \"dash\").Update(\"is_available\", menu.IsAvailable).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// GetConfigValueByKey 根据 key 获取配置值,如果不存在或出错则返回默认值\nfunc GetConfigValueByKey(key string, defaultVal string) string {\n\tvar c Config\n\terr := DB.Where(\"`key` = ?\", key).First(&c).Error\n\tif err == nil && c.Value != \"\" {\n\t\treturn c.Value\n\t}\n\treturn defaultVal\n}\n"}, "932d2f7e6967888f9611259b01c450a475aa1301": {"t": 1785754899.122189, "n": 2182, "h": "39f95ebd37febeca", "c": "package model\n\nimport (\n\t\"errors\"\n\t\"time\"\n)\n\n// TableName alarm\nfunc (t *Config) TableName() string {\n\treturn TableName(\"config\")\n}\n\ntype Config struct {\n\tID int64 `gorm:\"column:id;primaryKey;autoIncrement\" json:\"id\"`\n\tName string `gorm:\"column:name;size:255\" json:\"name\"`\n\tKey string `gorm:\"column:key;size:255\" json:\"key\"`\n\tValue string `gorm:\"column:value;type:text\" json:\"value\"`\n\tComment string `gorm:\"column:comment;size:255\" json:\"comment\"`\n\tCreatedAt time.Time `gorm:\"column:created_at;autoCreateTime\" json:\"created_at\"`\n\tUpdatedAt time.Time `gorm:\"column:updated_at;autoUpdateTime\" json:\"updated_at\"`\n}\n\n// GetConfigList 获取系统配置\nfunc GetConfigList() ([]Config, error) {\n\tvar v []Config\n\terr := DB.Order(\"id\").Find(&v).Error\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn v, nil\n}\n\n// GetConfigOne 获取某一个配置\nfunc GetConfigOne(id int64) (v *Config, err error) {\n\tv = &Config{}\n\terr = DB.Where(\"id = ?\", id).First(v).Error\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn v, nil\n}\n\n// UpdateConfig 更新系统配置\nfunc UpdateConfig(m *Config) (err error) {\n\tvar v Config\n\t//数据面板独立配置\n\terr = DB.Where(\"id = ?\", m.ID).First(&v).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\t\n\t// 禁止修改加密密钥\n\tif v.Key == \"encryption_key\" {\n\t\treturn errors.New(\"加密密钥不允许修改,如需更换请联系系统管理员\")\n\t}\n\t\n\terr = DB.Model(&Config{}).Where(\"id = ?\", m.ID).Update(\"value\", m.Value).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\t//dash处理\n\tif m.ID == 1 {\n\t\terr = updateZbxDash(m)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t}\n\treturn nil\n}\n\n// 数据面板控制\nfunc updateZbxDash(m *Config) (err error) {\n\tvar menu Menu\n\terr = DB.Where(\"router = ?\", \"dash\").First(&menu).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\tswitch m.Value {\n\tcase \"0\":\n\t\tmenu.IsAvailable = true\n\tcase \"1\":\n\t\tmenu.IsAvailable = false\n\t}\n\terr = DB.Model(&Menu{}).Where(\"router = ?\", \"dash\").Update(\"is_available\", menu.IsAvailable).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// GetConfigValueByKey 根据 key 获取配置值,如果不存在或出错则返回默认值\nfunc GetConfigValueByKey(key string, defaultVal string) string {\n\tvar c Config\n\terr := DB.Where(\"`key` = ?\", key).First(&c).Error\n\tif err == nil && c.Value != \"\" {\n\t\treturn c.Value\n\t}\n\treturn defaultVal\n}\n"}, "ad66cc1ac3ee3337e7b97fce4801b678a4d830e6": {"t": 1785754900.8105505, "n": 2181, "h": "5c33c057560e7392", "c": "package model\n\nimport (\n\t\"errors\"\n\t\"time\"\n)\n\n// TableName alarm\nfunc (t *Config) TableName() string {\n\treturn TableName(\"config\")\n}\n\ntype Config struct {\n\tID int64 `gorm:\"column:id;primaryKey;autoIncrement\" json:\"id\"`\n\tName string `gorm:\"column:name;size:255\" json:\"name\"`\n\tKey string `gorm:\"column:key;size:255\" json:\"key\"`\n\tValue string `gorm:\"column:value;size:255\" json:\"value\"`\n\tComment string `gorm:\"column:comment;size:255\" json:\"comment\"`\n\tCreatedAt time.Time `gorm:\"column:created_at;autoCreateTime\" json:\"created_at\"`\n\tUpdatedAt time.Time `gorm:\"column:updated_at;autoUpdateTime\" json:\"updated_at\"`\n}\n\n// GetConfigList 获取系统配置\nfunc GetConfigList() ([]Config, error) {\n\tvar v []Config\n\terr := DB.Order(\"id\").Find(&v).Error\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn v, nil\n}\n\n// GetConfigOne 获取某一个配置\nfunc GetConfigOne(id int64) (v *Config, err error) {\n\tv = &Config{}\n\terr = DB.Where(\"id = ?\", id).First(v).Error\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn v, nil\n}\n\n// UpdateConfig 更新系统配置\nfunc UpdateConfig(m *Config) (err error) {\n\tvar v Config\n\t//数据面板独立配置\n\terr = DB.Where(\"id = ?\", m.ID).First(&v).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\t\n\t// 禁止修改加密密钥\n\tif v.Key == \"encryption_key\" {\n\t\treturn errors.New(\"加密密钥不允许修改,如需更换请联系系统管理员\")\n\t}\n\t\n\terr = DB.Model(&Config{}).Where(\"id = ?\", m.ID).Update(\"value\", m.Value).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\t//dash处理\n\tif m.ID == 1 {\n\t\terr = updateZbxDash(m)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t}\n\treturn nil\n}\n\n// 数据面板控制\nfunc updateZbxDash(m *Config) (err error) {\n\tvar menu Menu\n\terr = DB.Where(\"router = ?\", \"dash\").First(&menu).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\tswitch m.Value {\n\tcase \"0\":\n\t\tmenu.IsAvailable = true\n\tcase \"1\":\n\t\tmenu.IsAvailable = false\n\t}\n\terr = DB.Model(&Menu{}).Where(\"router = ?\", \"dash\").Update(\"is_available\", menu.IsAvailable).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// GetConfigValueByKey 根据 key 获取配置值,如果不存在或出错则返回默认值\nfunc GetConfigValueByKey(key string, defaultVal string) string {\n\tvar c Config\n\terr := DB.Where(\"`key` = ?\", key).First(&c).Error\n\tif err == nil && c.Value != \"\" {\n\t\treturn c.Value\n\t}\n\treturn defaultVal\n}\n"}, "f507b9d7748c5aaff10657b4336f9efe45493357": {"t": 1785754902.3170369, "n": 2073, "h": "9de54e28774c3c8a", "c": "package model\n\nimport (\n\t\"time\"\n)\n\n// TableName alarm\nfunc (t *Config) TableName() string {\n\treturn TableName(\"config\")\n}\n\ntype Config struct {\n\tID int64 `gorm:\"column:id;primaryKey;autoIncrement\" json:\"id\"`\n\tName string `gorm:\"column:name;size:255\" json:\"name\"`\n\tKey string `gorm:\"column:key;size:255\" json:\"key\"`\n\tValue string `gorm:\"column:value;size:255\" json:\"value\"`\n\tComment string `gorm:\"column:comment;size:255\" json:\"comment\"`\n\tCreatedAt time.Time `gorm:\"column:created_at;autoCreateTime\" json:\"created_at\"`\n\tUpdatedAt time.Time `gorm:\"column:updated_at;autoUpdateTime\" json:\"updated_at\"`\n}\n\n// GetConfigList 获取系统配置\nfunc GetConfigList() ([]Config, error) {\n\tvar v []Config\n\terr := DB.Order(\"id\").Find(&v).Error\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn v, nil\n}\n\n// GetConfigOne 获取某一个配置\nfunc GetConfigOne(id int64) (v *Config, err error) {\n\tv = &Config{}\n\terr = DB.Where(\"id = ?\", id).First(v).Error\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn v, nil\n}\n\n// UpdateConfig 更新系统配置\nfunc UpdateConfig(m *Config) (err error) {\n\tvar v Config\n\t//数据面板独立配置\n\terr = DB.Where(\"id = ?\", m.ID).First(&v).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = DB.Model(&Config{}).Where(\"id = ?\", m.ID).Update(\"value\", m.Value).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\t//dash处理\n\tif m.ID == 1 {\n\t\terr = updateZbxDash(m)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t}\n\treturn nil\n}\n\n// 数据面板控制\nfunc updateZbxDash(m *Config) (err error) {\n\tvar menu Menu\n\terr = DB.Where(\"router = ?\", \"dash\").First(&menu).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\tswitch m.Value {\n\tcase \"0\":\n\t\tmenu.IsAvailable = true\n\tcase \"1\":\n\t\tmenu.IsAvailable = false\n\t}\n\terr = DB.Model(&Menu{}).Where(\"router = ?\", \"dash\").Update(\"is_available\", menu.IsAvailable).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// GetConfigValueByKey 根据 key 获取配置值,如果不存在或出错则返回默认值\nfunc GetConfigValueByKey(key string, defaultVal string) string {\n\tvar c Config\n\terr := DB.Where(\"`key` = ?\", key).First(&c).Error\n\tif err == nil && c.Value != \"\" {\n\t\treturn c.Value\n\t}\n\treturn defaultVal\n}\n"}, "54928571d31b4cc10676191ae02c19c271ab8824": {"t": 1785754903.821043, "n": 2074, "h": "ebf496859767d857", "c": "package models\n\nimport (\n\t\"time\"\n)\n\n// TableName alarm\nfunc (t *Config) TableName() string {\n\treturn TableName(\"config\")\n}\n\ntype Config struct {\n\tID int64 `gorm:\"column:id;primaryKey;autoIncrement\" json:\"id\"`\n\tName string `gorm:\"column:name;size:255\" json:\"name\"`\n\tKey string `gorm:\"column:key;size:255\" json:\"key\"`\n\tValue string `gorm:\"column:value;size:255\" json:\"value\"`\n\tComment string `gorm:\"column:comment;size:255\" json:\"comment\"`\n\tCreatedAt time.Time `gorm:\"column:created_at;autoCreateTime\" json:\"created_at\"`\n\tUpdatedAt time.Time `gorm:\"column:updated_at;autoUpdateTime\" json:\"updated_at\"`\n}\n\n// GetConfigList 获取系统配置\nfunc GetConfigList() ([]Config, error) {\n\tvar v []Config\n\terr := DB.Order(\"id\").Find(&v).Error\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn v, nil\n}\n\n// GetConfigOne 获取某一个配置\nfunc GetConfigOne(id int64) (v *Config, err error) {\n\tv = &Config{}\n\terr = DB.Where(\"id = ?\", id).First(v).Error\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn v, nil\n}\n\n// UpdateConfig 更新系统配置\nfunc UpdateConfig(m *Config) (err error) {\n\tvar v Config\n\t//数据面板独立配置\n\terr = DB.Where(\"id = ?\", m.ID).First(&v).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = DB.Model(&Config{}).Where(\"id = ?\", m.ID).Update(\"value\", m.Value).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\t//dash处理\n\tif m.ID == 1 {\n\t\terr = updateZbxDash(m)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t}\n\treturn nil\n}\n\n// 数据面板控制\nfunc updateZbxDash(m *Config) (err error) {\n\tvar menu Menu\n\terr = DB.Where(\"router = ?\", \"dash\").First(&menu).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\tswitch m.Value {\n\tcase \"0\":\n\t\tmenu.IsAvailable = true\n\tcase \"1\":\n\t\tmenu.IsAvailable = false\n\t}\n\terr = DB.Model(&Menu{}).Where(\"router = ?\", \"dash\").Update(\"is_available\", menu.IsAvailable).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// GetConfigValueByKey 根据 key 获取配置值,如果不存在或出错则返回默认值\nfunc GetConfigValueByKey(key string, defaultVal string) string {\n\tvar c Config\n\terr := DB.Where(\"`key` = ?\", key).First(&c).Error\n\tif err == nil && c.Value != \"\" {\n\t\treturn c.Value\n\t}\n\treturn defaultVal\n}\n"}}, "internal/model/base.go": {"bcdc3997c4da6f884243c76a79541b3930b0018d": {"t": 1785745617.953983, "n": 22210, "h": "9eb6a661ac5eb21f", "c": "package model\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"zbxtable/pkg/logger\"\n\t\"zbxtable/pkg/utils\"\n\n\tzabbix \"github.com/canghai908/zabbix-go\"\n\t_ \"github.com/go-sql-driver/mysql\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t_ \"github.com/lib/pq\"\n\tworkwx \"github.com/xen0n/go-workwx\"\n\tini \"gopkg.in/ini.v1\"\n\t\"gorm.io/gorm\"\n)\n\nvar (\n\tAPI = &zabbix.API{}\n\tjson = jsoniter.Config{\n\t\tEscapeHTML: false,\n\t\tSortMapKeys: true,\n\t\tValidateJsonRawMessage: true,\n\t}.Froze()\n\tZBX_VER string\n\tZBX_V bool\n\tVersion string\n\tGitHash string\n\tBuildTime string\n\tWeApp *workwx.WorkwxApp // 不初始化,默认为 nil\n\n\tenvConfig map[string]string\n\tenvConfigOnce sync.Once\n\tenvFileExists bool\n)\n\nconst (\n\t//ms-agent 常量定义\n\tMSName = \"ms-agent\"\n\tMSUser = \"ms-agent\"\n\tMSGroup = \"MS-Agent\"\n\tMSMedia = \"MS-Agent\"\n\tMSAction = \"MS-Agent\"\n\t//webhook 常量定义\n\tWebhookName = \"ZbxTable\"\n\tWebhookAction = \"ZbxTable Webhook\"\n\tWebhookGroup = \"ZbxTable Webhook\"\n\tWebhookUser = \"zbxtable-webhook\"\n\t//down\n\tDownloadPath = \"./download/\"\n\tTplPath = \"./assets/templates/\"\n\t// 默认用户头像 (空字符串,由前端处理默认头像)\n\tDefaultAvatar = \"\"\n\t//默认更新服务器\n\tUpdateURL = \"http://dl.cactifans.com/stable/\"\n\n\t// 上传目录\n\tUploadDir = \"./upload\"\n\t// Logo 上传目录\n\tLogoUploadDir = \"./upload/logo\"\n\t// 背景图片上传目录\n\tBackgroundImageDir = \"./upload/background\"\n\t// 允许的图片格式\n\tAllowedImageExts = \".jpg,.jpeg,.png,.gif,.bmp,.webp,.svg\"\n\t// 最大文件大小 (10MB)\n\tMaxFileSize = 10 * 1024 * 1024\n)\n\n// TableName 表名前缀\nfunc TableName(str string) string {\n\treturn fmt.Sprintf(\"%s%s\", \"zbxtable_\", str)\n}\n\n// 接收数据库信息初始化\nfunc ModelInit(dbtype, dbhost, dbuser, dbpass, dbname, dbport string) error {\n\t// 直接使用 GORM\n\trunmode := GetConfKey(\"runmode\")\n\terr := InitGormDB(dbtype, dbhost, dbuser, dbpass, dbname, dbport, runmode)\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to connect database: \", err)\n\t\treturn err\n\t}\n\tlogger.Log.Info(\"Database connected!\")\n\t// 自动迁移表\n\terr = AutoMigrate()\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to auto migrate: \", err)\n\t\treturn err\n\t}\n\t// 基础数据初始化\n\tif err := DatabaseInit(); err != nil {\n\t\tlogger.Log.Error(\"Database init failed: \", err)\n\t\treturn err\n\t}\n\t// 使用go-cache替代Redis(不再需要Redis连接)\n\tif err := InitCache(); err != nil {\n\t\tlogger.Log.Error(\"Cache init failed: \", err)\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc InitWechat() {\n\t// 首先检查企业微信开关是否启用\n\twechatEnabled := GetConfigValueByKey(\"wechat_enabled\", \"0\")\n\tif wechatEnabled != \"1\" {\n\t\tlogger.Log.Info(\"WeChat is disabled in system config, skipping WeChat initialization\")\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\t// 开关已启用,从数据库读取企业微信配置\n\tagentIDStr := GetConfigValueByKey(\"wechat_agentid\", \"\")\n\tcorpid := GetConfigValueByKey(\"wechat_corpid\", \"\")\n\tsecret := GetConfigValueByKey(\"wechat_secret\", \"\") // 这里会自动解密\n\n\t// 验证必填配置项\n\tif agentIDStr == \"\" || corpid == \"\" || secret == \"\" {\n\t\tlogger.Log.Info(\"WeChat is enabled but configuration is incomplete (agentid/corpid/secret), WeChat app will not be initialized\")\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\t// 解析 AgentID\n\tAgentId, err := strconv.ParseInt(agentIDStr, 10, 64)\n\tif err != nil {\n\t\tlogger.Log.Error(\"wechat_agentid parse error:\", err)\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\n\t// 初始化企业微信客户端\n\tclient := workwx.New(corpid)\n\tWeApp = client.WithApp(secret, AgentId)\n\tWeApp.SpawnAccessTokenRefresher()\n\tlogger.Log.Info(\"WeChat inited successfully!\")\n}\n\n// DatabaseInit 数据初始化\nfunc DatabaseInit() error {\n\t//数据初始化操作 - 使用 GORM\n\tvar v User\n\terr := DB.Where(\"username = ?\", \"admin\").First(&v).Error\n\t//检查权限\n\tif err == nil && v.Operation == \"\" {\n\t\terr := DB.Model(&User{}).Where(\"id = ?\", v.ID).Update(\"operation\", \"['add', 'edit', 'delete','update']\").Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn fmt.Errorf(\"update admin operation failed: %w\", err)\n\t\t}\n\t\tlogger.Log.Info(\"update admin operation successfully\")\n\t}\n\t//添加管理员账号\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\tlogger.Log.Info(\"the admin user does not exist, create a new admin account later!\")\n\n\t\t// 构建默认主题配置\n\t\tdefaultThemeConfig := map[string]interface{}{\n\t\t\t\"theme\": map[string]string{\n\t\t\t\t\"color\": \"#722ed1\",\n\t\t\t\t\"mode\": \"dark\",\n\t\t\t\t\"success\": \"#52c41a\",\n\t\t\t\t\"warning\": \"#faad14\",\n\t\t\t\t\"error\": \"#f5222f\",\n\t\t\t},\n\t\t\t\"animate\": map[string]interface{}{\n\t\t\t\t\"disabled\": true,\n\t\t\t\t\"name\": \"lightSpeed\",\n\t\t\t\t\"direction\": \"left\",\n\t\t\t},\n\t\t}\n\n\t\t// 将配置转换为 JSON 字符串\n\t\tthemeJSON, err := json.Marshal(defaultThemeConfig)\n\t\tif err != nil {\n\t\t\tlogger.Log.Error(\"Failed to marshal default theme config:\", err)\n\t\t\tthemeJSON = []byte(`{\"theme\":{\"color\":\"#722ed1\",\"mode\":\"dark\",\"success\":\"#52c41a\",\"warning\":\"#faad14\",\"error\":\"#f5222f\"},\"animate\":{\"disabled\":true,\"name\":\"lightSpeed\",\"direction\":\"left\"}}`)\n\t\t}\n\n\t\t// 使用结构体插入管理员账号\n\t\tpassword, _ := utils.PasswordHash(\"Zbxtable\")\n\t\tuser := User{\n\t\t\tUsername: \"admin\",\n\t\t\tPassword: password,\n\t\t\tAvatar: DefaultAvatar,\n\t\t\tRole: \"admin\",\n\t\t\tOperation: \"['add', 'edit', 'delete','update']\",\n\t\t\tTheme: string(themeJSON),\n\t\t\tStatus: 0,\n\t\t}\n\t\terr = DB.Create(&user).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn fmt.Errorf(\"create admin account failed: %w\", err)\n\t\t}\n\t\tlogger.Log.Info(\"create an administrator account successfully, the admin ID is:\", user.ID)\n\t}\n\t//初始化系统数据\n\tif err = EnsureSystemBindingsForAssetTypes(); err != nil {\n\t\tlogger.Log.Info(\"Init system info error!\", err)\n\t\treturn fmt.Errorf(\"init system data failed: %w\", err)\n\t}\n\t//告警默认规则初始化 default rule\n\tvar rules []Rule\n\terr = DB.Where(\"m_type = ?\", \"2\").Find(&rules).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn fmt.Errorf(\"query default rules failed: %w\", err)\n\t}\n\tif len(rules) == 0 {\n\t\t// 使用 \"*\" 作为全局默认规则,匹配所有租户\n\t\tdefaultRule := []Rule{\n\t\t\t{\n\t\t\t\tName: \"全局默认规则\",\n\t\t\t\tZIDs: \"*\",\n\t\t\t\tMType: \"2\",\n\t\t\t\tChannel: \"wechat_robot\",\n\t\t\t\tUserIds: \"1\",\n\t\t\t\tSweek: \"0,1,2,3,4,5,6\",\n\t\t\t\tStime: \"00:00\",\n\t\t\t\tEtime: \"23.59\",\n\t\t\t\tStatus: \"0\"},\n\t\t}\n\t\terr := DB.Create(&defaultRule).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init default rule error!\", err)\n\t\t\treturn fmt.Errorf(\"init default rule failed: %w\", err)\n\t\t}\n\t\tlogger.Log.Info(\"Init default rule successfully!\")\n\t}\n\t// 初始化加密密钥(如果不存在)\n\tvar encryptionKeyConfig Config\n\terr = DB.Where(\"config_key = ?\", \"encryption_key\").First(&encryptionKeyConfig).Error\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t// 生成随机加密密钥\n\t\trandomKey := utils.GenerateRandomKey()\n\t\tencryptionKeyConfig = Config{\n\t\t\tName: \"加密密钥\",\n\t\t\tConfigKey: \"encryption_key\",\n\t\t\tConfigValue: randomKey,\n\t\t\tComment: \"用于加密存储敏感信息(如Zabbix密码和Token)的密钥,系统自动生成,不可修改\",\n\t\t}\n\t\tif insertErr := DB.Create(&encryptionKeyConfig).Error; insertErr != nil {\n\t\t\tlogger.Log.Error(\"Init encryption_key error:\", insertErr)\n\t\t\treturn fmt.Errorf(\"init encryption_key failed: %w\", insertErr)\n\t\t} else {\n\t\t\tlogger.Log.Info(\"Init encryption_key successfully! Key length:\", len(randomKey))\n\t\t}\n\t}\n\n\t// 初始化内置映射规则\n\t//InitDefaultMappingRules()\n\n\t// 获取结构化的默认配置\n\tdefaultConfigs := getDefaultConfigs()\n\n\tfor _, cfgItem := range defaultConfigs {\n\t\tvar existing Config\n\t\terr = DB.Where(\"config_key = ?\", cfgItem.ConfigKey).First(&existing).Error\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\tif insertErr := DB.Create(&cfgItem).Error; insertErr != nil {\n\t\t\t\tlogger.Log.Infof(\"Init config key %s error: %v\", cfgItem.ConfigKey, insertErr)\n\t\t\t\treturn fmt.Errorf(\"init config key %s failed: %w\", cfgItem.ConfigKey, insertErr)\n\t\t\t}\n\t\t\tlogger.Log.Infof(\"Init config key %s successfully!\", cfgItem.ConfigKey)\n\t\t} else if err != nil {\n\t\t\tlogger.Log.Infof(\"Init config key %s error: %v\", cfgItem.ConfigKey, err)\n\t\t\treturn fmt.Errorf(\"check config key %s failed: %w\", cfgItem.ConfigKey, err)\n\t\t}\n\t}\n\t//默认菜单初始化\n\tif err := InitMenuData(); err != nil {\n\t\treturn fmt.Errorf(\"init menu data failed: %w\", err)\n\t}\n\t//检查并添加缺失的菜单项(用于版本升级)\n\tif err := CheckAndAddMenus(); err != nil {\n\t\treturn fmt.Errorf(\"check and add menus failed: %w\", err)\n\t}\n\n\t// 存量 asset_type 记录回填 monitor_type(幂等,需在 InitDefaultAssetTypes 之前)\n\t_ = MigrateAssetTypeMonitorType()\n\t// 初始化内置设备分组(幂等)\n\tif err := InitDefaultAssetGroups(); err != nil {\n\t\treturn fmt.Errorf(\"init default asset groups failed: %w\", err)\n\t}\n\t// 初始化默认资产类型(幂等)\n\tif err := InitDefaultAssetTypes(); err != nil {\n\t\treturn fmt.Errorf(\"init default asset types failed: %w\", err)\n\t}\n\t// 统一存量默认资产类型名称(幂等)\n\tif err := MigrateAssetTypeNames(); err != nil {\n\t\treturn fmt.Errorf(\"migrate asset type names failed: %w\", err)\n\t}\n\t// 回填存量 System 记录的 type_code(幂等)\n\tif err := MigrateSystemTypeCodes(); err != nil {\n\t\treturn fmt.Errorf(\"migrate system type codes failed: %w\", err)\n\t}\n\n\t// 启动异步预热:主机统计缓存(资产树页面首次打开即命中缓存,无需等待 Zabbix)\n\tWarmHostCountCache()\n\n\treturn nil\n}\n\nfunc GetConfKey(v string) string {\n\t// 优先从 .env 中读取(便于开发环境配置敏感信息且不提交到 git)\n\tenvConfigOnce.Do(func() {\n\t\tfile, err := os.Open(\".env\")\n\t\tif err != nil {\n\t\t\t// 没有 .env 文件时直接跳过,后面回退到 app.conf\n\t\t\tenvFileExists = false\n\t\t\treturn\n\t\t}\n\t\tdefer file.Close()\n\n\t\tenvFileExists = true\n\t\tenvConfig = make(map[string]string)\n\n\t\tscanner := bufio.NewScanner(file)\n\t\tfor scanner.Scan() {\n\t\t\tline := strings.TrimSpace(scanner.Text())\n\t\t\tif line == \"\" || strings.HasPrefix(line, \"#\") || strings.HasPrefix(line, \";\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tparts := strings.SplitN(line, \"=\", 2)\n\t\t\tif len(parts) != 2 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tkey := strings.TrimSpace(parts[0])\n\t\t\tval := strings.TrimSpace(parts[1])\n\t\t\t// 去掉可能的引号\n\t\t\tval = strings.Trim(val, `\"'`)\n\t\t\tif key != \"\" {\n\t\t\t\tenvConfig[key] = val // 即使 val 为空也认为是有效配置\n\t\t\t}\n\t\t}\n\t})\n\n\t// 如果存在 .env 文件,则所有配置完全由 .env 决定:\n\t// - key 存在:返回对应值(可以是空字符串)\n\t// - key 不存在:返回空字符串\n\tif envFileExists {\n\t\tif envConfig == nil {\n\t\t\treturn \"\"\n\t\t}\n\t\tif val, ok := envConfig[v]; ok {\n\t\t\treturn val\n\t\t}\n\t\treturn \"\"\n\t}\n\t// 其次从 config/app.conf 读取\n\tcfg, err := ini.Load(\"./config/app.conf\")\n\tif err != nil {\n\t\tlogger.Log.Error(err)\n\t\treturn \"\"\n\t}\n\tp, err := cfg.Section(\"\").GetKey(v)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn p.String()\n}\n\n// IsPasswordConfigured 检查是否配置了密码(用于图形查看)\n// 优先检查数据库中是否有激活的 Zabbix 实例配置了用户名和密码\n// 注意:查看图形需要用户名和密码,Token 方式无法用于 Web 登录\nfunc IsPasswordConfigured() bool {\n\t// 1. 优先检查数据库中是否有激活的 Zabbix 实例\n\tvar activeInstance ZabbixInstance\n\terr := DB.Where(\"is_active = ?\", true).First(&activeInstance).Error\n\tif err == nil {\n\t\t// 找到激活的实例,检查是否配置了用户名和密码(查看图形必须用密码,Token 无法用于 Web 登录)\n\t\tif strings.TrimSpace(activeInstance.User) != \"\" && strings.TrimSpace(activeInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 2. 如果数据库中没有激活实例,检查是否有任何可用的实例配置了密码\n\tvar anyInstance ZabbixInstance\n\terr = DB.Where(\"enabled = ?\", true).First(&anyInstance).Error\n\tif err == nil {\n\t\t// 找到可用的实例,检查是否配置了用户名和密码\n\t\tif strings.TrimSpace(anyInstance.User) != \"\" && strings.TrimSpace(anyInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 3. 回退到配置文件检查(兼容旧版本)\n\tpass := GetConfKey(\"zabbix_pass\")\n\tuser := GetConfKey(\"zabbix_user\")\n\treturn pass != \"\" && user != \"\"\n}\n\n// GetEncryptionKey 获取加密密钥\n// 优先从数据库读取,如果数据库中没有则从环境变量读取,最后使用默认密钥\nfunc GetEncryptionKey() string {\n\t// 1. 优先从数据库读取\n\tkey := GetConfigValueByKey(\"encryption_key\", \"\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 2. 从环境变量或配置文件读取\n\tkey = GetConfKey(\"encryption_key\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 3. 使用默认密钥(不应该到这里,因为 DatabaseInit 会生成)\n\tlogger.Log.Warn(\"使用默认加密密钥,建议在数据库中配置 encryption_key\")\n\treturn \"zbxtable-default-encryption-key-2024\"\n}\n\n// getDefaultConfigs 返回系统默认配置列表\n// 配置按功能模块分组,便于维护和管理\nfunc getDefaultConfigs() []Config {\n\tconfigs := make([]Config, 0)\n\n\t// 系统外观配置\n\tconfigs = append(configs, getSystemAppearanceConfigs()...)\n\n\t// 初始配置状态\n\tconfigs = append(configs, getInitialSetupConfigs()...)\n\n\t// Dashboard 相关配置\n\tconfigs = append(configs, getDashboardConfigs()...)\n\n\t// 计划任务配置\n\tconfigs = append(configs, getTaskConfigs()...)\n\n\t// 邮件配置\n\tconfigs = append(configs, getEmailConfigs()...)\n\n\t// 企业微信配置\n\tconfigs = append(configs, getWeChatConfigs()...)\n\n\t// AI 配置\n\tconfigs = append(configs, getAIConfigs()...)\n\n\t// 演示模式配置\n\tconfigs = append(configs, getDemoConfigs()...)\n\n\treturn configs\n}\n\n// getDemoConfigs 演示模式配置\nfunc getDemoConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"演示模式\",\n\t\t\tConfigKey: \"demo_mode\",\n\t\t\tConfigValue: \"false\",\n\t\t\tComment: \"是否开启演示模式(只读):true 开启, false 关闭\",\n\t\t},\n\t}\n}\n\n// getSystemAppearanceConfigs 系统外观配置\nfunc getSystemAppearanceConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"系统名称\",\n\t\t\tConfigKey: \"system_name\",\n\t\t\tConfigValue: \"ZbxTable\",\n\t\t\tComment: \"系统显示的名称\",\n\t\t},\n\t\t{\n\t\t\tName: \"系统副标题\",\n\t\t\tConfigKey: \"system_subtitle\",\n\t\t\tConfigValue: \"Zabbix 监控数据可视化平台\",\n\t\t\tComment: \"登录页显示的系统副标题\",\n\t\t},\n\t\t{\n\t\t\tName: \"系统Logo\",\n\t\t\tConfigKey: \"system_logo\",\n\t\t\tConfigValue: \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAYAAACtWK6eAAANoUlEQVR4nOzdS2xcVZ7H8f+513bZTmxfWzNgQAO2Bs0IFsRWZhazAEwizQ7wgBwYNJCY1axIyKJbvcHlXhCkXpD0upVHI9St0E0ctdS9SjBk0d1SQ1XSCFoCYdMLuluK4uvgR9lV9/xbt+wTV5v4uKpc595z7/19djx0z/Hj63NO+ZavQwCwIwQCoIFAADQQCIAGAgHQQCAAGggEQAOBAGggEAANBAKggUAANBAIgAYCAdBAIAAaCARAA4EAaCAQAA0EAqCBQAA0EAiABgIB0EAgABoIBEADgQBoIBAADQQCoIFAADQQCIAGAgHQQCAAGggEQAOBAGggEAANBAKggUAANBAIgAYCAdBAIAAaCARAA4EAaCAQAA0EAqCBQAA0EAiABgIB0EAgABptcU8ga0ZOLHiUo5Fb7fKAczs3/O1qe1+7K4dKZbf639scojY3oHaX5oWQX3fmqNjtrM8Xf9RfjHvuWSTinkAWjHx/YewvFRpfK7U/WVnOjayuORTI+j/1XTlJHW2SujrkbHeXnOnNrV0uvtU/b3TSUIVADAlXir+Wuo+XK+KEv+x6jQSxmzCWnu6g0NMlz8z/uOtCyy4M34FAWmzw/1eHyhUnv8Ty6NpSzvh4vd2V+e4cnx/0Vi5gVWk9BNIi4YrxZeBOlZY6TlRWOiIfP9fOc/cNrE9jRWktBNICPd+7Ob5+q/vc2redXtxz6e2uzN1ebT9EPxdYTVoAgexBuGp8sbB/arnknoh7LrU62iT3dlVO3vxJ5+m455J0CKRZL/JQm8OXKgGNxD2VnbS38dvld92Tcc8jyRBIM17kIWL+gIiG4p7Kbvq6g8Jw39Kh4ul+P+65JBECadRL6yMUtIVxxH7eqBtTgdbFIZoRiKRBuNWkEUmMg6o/Bkepg6/SOCdr3hbAClKvjW1VIXFx1MJK0jCsIPXYOnMkNw7CStIMBLKbBB3I6yJodF9f5Wr1pknYFQLRSVscm5ZL7uifbu5/P+55JAEC2UlK41BKZfcpeiE4G/c8bIdA7iblcWwRk4hED4Fsl5k4FESig0BqZS4ORUy2v4RI7gaBKJmNY0M5EJPdRyuIZBsEQohDWSk5x+gIT8U9D5sgEMRRS5DgKUSyJdu3moyzR7nq7SOI4x8xsZimi2I67onELbsrSBhHB1aOHWAl2ZTNQFQcwt43O1kAkWQyEMTRiMxHkq1AEEczMh1JtgJBHM3KbCTZCeQFPoc49iSTkWQjkDAO4mNxTyMFqpHsf6WcmUjS/3sQxGEC59oqJ9fe7Uj9391KdyCIwyQmFpN0UaT6T52md4uFOEwLt1vn6AgfjXsiJqUykN5rv5ly+pYQh3licGDt3MgPFlMbSeq2WAN//OUUMeflYjfdfudxkov74p5Sag32r9PgwDptbLd4sniqL3XbrVQFouJQ/4xIzKmJQ2FimlzP9Vz4LJ+eb6vUfCTb41AQSevdJQ5lgcrB4fV9XiEtkaTiDLJTHCGnb4V6X75GTt9y9BNLoX/uK+8UR6if2t0rHcv+6KN5jnZihiQ+EF0cCiJpjYGeMj3wT2u7/W+piiTRgdQTh4JI9iaM48F7do1DSU0kiQ2kkTgURNKcBuNQ7kQyluBIEnmSaiaOWji416/JOGotkAwOep3e3GwCD+6JW0G86+8f3UschJWkbvs7g73GQdWVxHGv+CV/OIkrSaICCeNwhDzfimshEr2unKTh+0qtutxwUiNJTCCtjEMJI+mZ+B2JXLmVl028MI6H718l12npN3MiI0lEICbiUNx7F6n35Y8QySZDcSiJi8T6QEzGoSCSDYbjUBIVidUvK3iFiyOO6xSiGi/4Wx/dfucJ4rX2qIa0RkRx1JojGRy2/dUta1eQzTg+iHLMrK4kHW2ShgcjjYOSspJYGUhNHJE/Ry+MZP/Eb6MeNjZhHA8/sEodbbF8k1ofiXWBxBmH0v7QTdr39B/iGj4yMceh3Ikkb2EkVm3+bIij1tqNB2n5V/8R9zSMsCSOWnO0Ehwc97yFvEVnEmtWEK9wacimOEK5x/6cypXEwjioupJ0O1dmfL/fppXEikA24pBWxaGkLRJL49gkRm2LJPZAtuJgax9DkJZI7I5DsSuSWANJQhxKGEnX45/HPY2mJSMOxZ5IYglkjD8I4xhOShxK1xOfJzKSZMWh3Ikk1m135IHkmalY9MM4riYpDiVpkSQzDiWMxH37v/KLsb2sFfnAXuFSv+PKT5IYR63Vjx6h1WuPxD0NLddh+vd/WUloHDWYTuc6el7//XT0nUS6ggzNXSLhBmeTHgclYCUJ43j4/qSuHNsIOr62fvtEHL9tjyyQ8Nxxe0nmBdF4VGOaZmskKo6unIx7Kq0iSIg3/JIf+Q/WyNasjUN58FVU40XJpu1WCuPYwjSb6+h5KsqtViQryMAXv6aNQ3k62bKSpDoOqv44fzLcakX50q/xQMKtlVxZOZ6Gc4eODZGkOo4N1a1WlC/9Gg+kWPSHHCFOmB7HBnFG8uA9pbTHofRTlxPZKmI0kDwzOW3yaNpXj1phJJ3/+WWkY4ZxDPRUIh0zVkK8FtUqYjSQ09dnPGLK3INsuv/7BnU89nUkY2Uujg3VVSSKgYwG4jrl8SytHrX2P/2x8UgyGscGIV4bmzK/zTIWSHg4Z+EcN3X9JDAZSabj2OD5a/6Y6UGMBRIezonx4H4TkQz2r2c9Dqq+ouW4xp+NaHCLVUnNb8z3qhrJv33Tkmtpnu6URc+a3mYZC0S44llT106ifU9/TO69/p6ugTi+w/NLZrdZRgJ59NNPSRAZ3x8miegsU+//XWs6EsSxA8cxuo03Esg35S8yf/a4m2YjQRw7EiTEAZMDGAnEdYNMvrRbj0YjQRy7etLkxY0EwsxYQTTqjQRx1MUz+T4RQ4f0bP5ysBEqkp0e4IM46uaZfJ+ImRVEOA+ZuG7aVCO5y1OuEEeDDL7SG/vfxcq67Y+CQxxNEJSsFQQaoyIZHFxeQBx2QSB2YKd3+fxgV+UgMc/FPRnYgkDix8Tywq0DE5NepzdHLA8jEnuYCmRv91Rkx2YcRybDf5jNC0IkTZk3dWEjgQiWiyaumzbMXFBxKIikCSVzP5CNBCKlKJq4bpow8ycslw/f7b8hkob4xdP9yQrEdYWxJS8NVBz+6LEdv7CIpE5MRn8YGwkkCMpYQXZQTxwKIqkD83WTlzcSiD/6v/PMOKhv10gcCiLRYnLFrMkBzL1hiuRlU9dOImb+qtE4lDCS9TASIZ8nogUzM0yoIEjeCkLVPzpBRstOEmaeY1lpKg7lszCSDq9AFBxGJJuYi8W3+o2ed40FEgTtM9hm3YnjULjt3Ou1EMk2TB+aHsJYIP7o//gkzO4PbdfKOBREcgcTyTOmBzF6qwkH5j8AW5mIQ0EkG49CML29ItOB+KMTs0zZO4uYjEMJI/HavQLJ4Dmz74iwUvjxno9iIOM3K3LA06bHsEkUcSiz04K8nDdLMng1U5EwzxdP9f40iqGMB5KlVSTKOJTNSM5nKBImEvmoBovkdvcsrCJxxKFkKhLmuahWD4oqkHAVIabTUYwVhzjjUDISCVfvKIhQZG+YktKd5hTeKmFDHMq2SNKGiXg6ileuakX6ZHav8N6Y44qrUY9rCjPdYlk+aEMctUbyC0RrTp6EmIp7Li3D/FXxVN+/Rj1spG+5DbdakvlklGOashGHPGxbHKFivp8oJ/PEKTn7hTuPiLdWSuTvSfcPTJxO+hduK44j1t7Wn6JIwnPHZNRbKyWWP9pw68BEnlhG8oueVktCHEoKImFifr34Vr/xe652EttfNam+FzthkSQpDiXBkWwcyk/1xXq7UuyH5YHr7yXiMJnEOGrVHNzfsOHrvgsmosnim70X4p6IFZ+ozUis/cJtvNmpYuWBvBEHp5iCdX+chHu2+ihlK/EtkvK5OLdVtaz5hvQK740Jh84KIYbjnkuN8CfZjAyWXt3Lm51sMpZn8kv+MAnnCln3ueYCSfl8XAfyu7EmEKpG8rMhx2mfIkFHY54bM5MviKdvHZhI5S37dm25eIGIzhTf7LPunGRVIIp34+IxweINIcRQDHNkYjoj5dJ0WlaNnWyuJkMk3HMkqk9qiuNzPUscvGrTqlHLykCUCEPZvHdJnJfB+g+TftZo1NgUk7/mHyPhHo0oFBXGtC1njZ1YHYjiFX4xLlx6RRCpZ6+3at5qK3VGyuUzaV8xdrMZyhgJ5xgJeoZIeK38XBOxT5IuEMkZ28NQEhGIEp5RyHXHBDnPEPPI5spCdX4cd+5wZaaiIPpQSp7xRycS8YWK2p1XvMgJfyg9QVufa6rj8711NzHzPDFdJpKXqURFk38m1IREBbKdV7g4QuQMOQ6PEIk+FvRQ+K9r/hdfkPCJeVEIeT0IqEi0Mp/1laJR+TzTjO97lKMRcp0DJGmIHNFHvO3JToLmSfIiOTRHUt5IYhAA0AA8QAdAA4EAaCAQAA0EAqCBQAA0EAiABgIB0EAgABoIBEADgQBoIBAADQQCoIFAADQQCIAGAgHQQCAAGggEQAOBAGggEAANBAKggUAANBAIgAYCAdBAIAAaCARAA4EAaCAQAA0EAqCBQAA0EAiABgIB0EAgABoIBEADgQBoIBAADQQCoIFAADQQCIAGAgHQQCAAGn8PAAD//9S/ZEj2odDoAAAAAElFTkSuQmCC\",\n\t\t\tComment: \"系统Logo的base64编码数据\",\n\t\t},\n\t}\n}\n\n// getInitialSetupConfigs 初始配置状态\nfunc getInitialSetupConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"初始配置完成\",\n\t\t\tConfigKey: \"initial_setup_completed\",\n\t\t\tConfigValue: \"0\",\n\t\t\tComment: \"标记系统是否完成初始配置:1 已完成,0 未完成\",\n\t\t},\n\t}\n}\n\n// getDashboardConfigs Dashboard 相关配置\nfunc getDashboardConfigs() []Config {\n\treturn []Config{\n\t\t// {\n\t\t// \tName: \"数据面板\",\n\t\t// \tConfigKey: \"zbx_dash\",\n\t\t// \tConfigValue: \"0\",\n\t\t// \tComment: \"是否开启Zabbix看板:1 开启,0 关闭\",\n\t\t// },\n\t\t// {\n\t\t// \tName: \"面板配置\",\n\t\t// \tConfigKey: \"dash_id\",\n\t\t// \tConfigValue: \"1\",\n\t\t// \tComment: \"需要引入的Zabbix面板的ID,默认为1\",\n\t\t// },\n\t\t{\n\t\t\tName: \"Linux Top数量\",\n\t\t\tConfigKey: \"dash_top_lin_num\",\n\t\t\tConfigValue: \"10\",\n\t\t\tComment: \"首页 Linux Top 列表展示数量\",\n\t\t},\n\t\t{\n\t\t\tName: \"Windows Top数量\",\n\t\t\tConfigKey: \"dash_top_win_num\",\n\t\t\tConfigValue: \"10\",\n\t\t\tComment: \"首页 Windows Top 列表展示数量\",\n\t\t},\n\t\t{\n\t\t\tName: \"Webhook回调地址\",\n\t\t\tConfigKey: \"webhook_url\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"webhook通知地址\",\n\t\t},\n\t}\n}\n\n// getTaskConfigs 计划任务配置\nfunc getTaskConfigs() []Config {\n\tconfigs := make([]Config, 0, len(getScheduledTaskDefinitions())*2)\n\n\tfor _, task := range getScheduledTaskDefinitions() {\n\t\tconfigs = append(configs,\n\t\t\tConfig{\n\t\t\t\tName: task.Name + \"开关\",\n\t\t\t\tConfigKey: task.EnabledKey,\n\t\t\t\tConfigValue: task.DefaultEnabled,\n\t\t\t\tComment: task.Name + \"计划任务是否启用:1 启用,0 停用\",\n\t\t\t},\n\t\t\tConfig{\n\t\t\t\tName: task.Name + \"Cron\",\n\t\t\t\tConfigKey: task.CronKey,\n\t\t\t\tConfigValue: task.DefaultCron,\n\t\t\t\tComment: task.Name + \"执行周期,格式:秒 分 时 日 月 周\",\n\t\t\t},\n\t\t)\n\t}\n\n\treturn configs\n}\n\n// getEmailConfigs 邮件配置\nfunc getEmailConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"邮件发件人\",\n\t\t\tConfigKey: \"email_from\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"告警邮件发件人邮箱地址\",\n\t\t},\n\t\t{\n\t\t\tName: \"邮件昵称\",\n\t\t\tConfigKey: \"email_nickname\",\n\t\t\tConfigValue: \"ZbxTable\",\n\t\t\tComment: \"告警邮件显示的发件人昵称\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 密码/授权码\",\n\t\t\tConfigKey: \"email_secret\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"SMTP 登录密码或授权码\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 服务器\",\n\t\t\tConfigKey: \"email_host\",\n\t\t\tConfigValue: \"smtp.qq.com\",\n\t\t\tComment: \"SMTP 服务器地址\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 端口\",\n\t\t\tConfigKey: \"email_port\",\n\t\t\tConfigValue: \"465\",\n\t\t\tComment: \"SMTP 端口号\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 使用 SSL\",\n\t\t\tConfigKey: \"email_isSSl\",\n\t\t\tConfigValue: \"true\",\n\t\t\tComment: \"是否启用 SSL:true/false\",\n\t\t},\n\t}\n}\n\n// getWeChatConfigs 企业微信配置\nfunc getWeChatConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"企业微信开关\",\n\t\t\tConfigKey: \"wechat_enabled\",\n\t\t\tConfigValue: \"0\",\n\t\t\tComment: \"是否启用企业微信:1 启用,0 禁用\",\n\t\t},\n\t\t{\n\t\t\tName: \"企业微信 AgentID\",\n\t\t\tConfigKey: \"wechat_agentid\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"企业微信应用的 AgentID\",\n\t\t},\n\t\t{\n\t\t\tName: \"企业微信 CorpID\",\n\t\t\tConfigKey: \"wechat_corpid\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"企业微信企业ID\",\n\t\t},\n\t\t{\n\t\t\tName: \"企业微信 Secret\",\n\t\t\tConfigKey: \"wechat_secret\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"企业微信应用的 Secret\",\n\t\t},\n\t}\n}\n\n// getAIConfigs AI 配置\nfunc getAIConfigs() []Config {\n\treturn []Config{\n\t\t// AI 类型选择\n\t\t{\n\t\t\tName: \"AI 类型\",\n\t\t\tConfigKey: \"ai_type\",\n\t\t\tConfigValue: \"ollama\",\n\t\t\tComment: \"选择使用的 AI 服务类型:ollama、deepseek 或 custom\",\n\t\t},\n\t\t// Ollama 配置\n\t\t{\n\t\t\tName: \"Ollama Host\",\n\t\t\tConfigKey: \"ollama_host\",\n\t\t\tConfigValue: \"http://localhost:11434\",\n\t\t\tComment: \"Ollama 服务地址,如 http://127.0.0.1:11434\",\n\t\t},\n\t\t{\n\t\t\tName: \"Ollama Model\",\n\t\t\tConfigKey: \"ollama_model\",\n\t\t\tConfigValue: \"deepseek-r1:32b\",\n\t\t\tComment: \"默认使用的大模型名称\",\n\t\t},\n\t\t// Deepseek 配置\n\t\t{\n\t\t\tName: \"Deepseek API Key\",\n\t\t\tConfigKey: \"deepseek_api_key\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"Deepseek API 密钥\",\n\t\t},\n\t\t{\n\t\t\tName: \"Deepseek Model\",\n\t\t\tConfigKey: \"deepseek_model\",\n\t\t\tConfigValue: \"deepseek-chat\",\n\t\t\tComment: \"Deepseek 模型名称,如 deepseek-chat\",\n\t\t},\n\t\t{\n\t\t\tName: \"Deepseek Base URL\",\n\t\t\tConfigKey: \"deepseek_base_url\",\n\t\t\tConfigValue: \"https://api.deepseek.com\",\n\t\t\tComment: \"Deepseek API 地址,默认为 https://api.deepseek.com\",\n\t\t},\n\t\t// Custom(OpenAI-compatible) 配置\n\t\t{\n\t\t\tName: \"Custom API Key\",\n\t\t\tConfigKey: \"custom_api_key\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"OpenAI 兼容接口的 API Key\",\n\t\t},\n\t\t{\n\t\t\tName: \"Custom Model\",\n\t\t\tConfigKey: \"custom_model\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"OpenAI 兼容接口的模型名称\",\n\t\t},\n\t\t{\n\t\t\tName: \"Custom Base URL\",\n\t\t\tConfigKey: \"custom_base_url\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"OpenAI 兼容接口地址,如 https://api.example.com/v1\",\n\t\t},\n\t\t{\n\t\t\tName: \"告警分析提示词\",\n\t\t\tConfigKey: \"alarm_analysis_prompt\",\n\t\t\tConfigValue: \"你是专业运维分析师。请基于以下告警信息进行分析:\\n\\n设备名称:{{hostname}}\\nIP:{{host_ip}}\\n告警描述:{{message}}\\n告警详情:{{detail}}\\n告警级别:{{level}}\\n告警状态:{{status}}\\n\\n请输出:\\n1. 可能根因(按概率排序)\\n2. 排查步骤(关键命令/检查项)\\n3. 修复方案与风险\\n4. 防复发建议\",\n\t\t\tComment: \"用于AI助手告警分析的提示词模板,支持占位符:{{hostname}} {{host_ip}} {{message}} {{detail}} {{level}} {{status}} {{alarm_context}}\",\n\t\t},\n\t}\n}\n"}, "7d796f32870cd59162ccfcc4cddefbd8111b1c0a": {"t": 1785754886.313077, "n": 22210, "h": "9eb6a661ac5eb21f", "c": "package model\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"zbxtable/pkg/logger\"\n\t\"zbxtable/pkg/utils\"\n\n\tzabbix \"github.com/canghai908/zabbix-go\"\n\t_ \"github.com/go-sql-driver/mysql\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t_ \"github.com/lib/pq\"\n\tworkwx \"github.com/xen0n/go-workwx\"\n\tini \"gopkg.in/ini.v1\"\n\t\"gorm.io/gorm\"\n)\n\nvar (\n\tAPI = &zabbix.API{}\n\tjson = jsoniter.Config{\n\t\tEscapeHTML: false,\n\t\tSortMapKeys: true,\n\t\tValidateJsonRawMessage: true,\n\t}.Froze()\n\tZBX_VER string\n\tZBX_V bool\n\tVersion string\n\tGitHash string\n\tBuildTime string\n\tWeApp *workwx.WorkwxApp // 不初始化,默认为 nil\n\n\tenvConfig map[string]string\n\tenvConfigOnce sync.Once\n\tenvFileExists bool\n)\n\nconst (\n\t//ms-agent 常量定义\n\tMSName = \"ms-agent\"\n\tMSUser = \"ms-agent\"\n\tMSGroup = \"MS-Agent\"\n\tMSMedia = \"MS-Agent\"\n\tMSAction = \"MS-Agent\"\n\t//webhook 常量定义\n\tWebhookName = \"ZbxTable\"\n\tWebhookAction = \"ZbxTable Webhook\"\n\tWebhookGroup = \"ZbxTable Webhook\"\n\tWebhookUser = \"zbxtable-webhook\"\n\t//down\n\tDownloadPath = \"./download/\"\n\tTplPath = \"./assets/templates/\"\n\t// 默认用户头像 (空字符串,由前端处理默认头像)\n\tDefaultAvatar = \"\"\n\t//默认更新服务器\n\tUpdateURL = \"http://dl.cactifans.com/stable/\"\n\n\t// 上传目录\n\tUploadDir = \"./upload\"\n\t// Logo 上传目录\n\tLogoUploadDir = \"./upload/logo\"\n\t// 背景图片上传目录\n\tBackgroundImageDir = \"./upload/background\"\n\t// 允许的图片格式\n\tAllowedImageExts = \".jpg,.jpeg,.png,.gif,.bmp,.webp,.svg\"\n\t// 最大文件大小 (10MB)\n\tMaxFileSize = 10 * 1024 * 1024\n)\n\n// TableName 表名前缀\nfunc TableName(str string) string {\n\treturn fmt.Sprintf(\"%s%s\", \"zbxtable_\", str)\n}\n\n// 接收数据库信息初始化\nfunc ModelInit(dbtype, dbhost, dbuser, dbpass, dbname, dbport string) error {\n\t// 直接使用 GORM\n\trunmode := GetConfKey(\"runmode\")\n\terr := InitGormDB(dbtype, dbhost, dbuser, dbpass, dbname, dbport, runmode)\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to connect database: \", err)\n\t\treturn err\n\t}\n\tlogger.Log.Info(\"Database connected!\")\n\t// 自动迁移表\n\terr = AutoMigrate()\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to auto migrate: \", err)\n\t\treturn err\n\t}\n\t// 基础数据初始化\n\tif err := DatabaseInit(); err != nil {\n\t\tlogger.Log.Error(\"Database init failed: \", err)\n\t\treturn err\n\t}\n\t// 使用go-cache替代Redis(不再需要Redis连接)\n\tif err := InitCache(); err != nil {\n\t\tlogger.Log.Error(\"Cache init failed: \", err)\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc InitWechat() {\n\t// 首先检查企业微信开关是否启用\n\twechatEnabled := GetConfigValueByKey(\"wechat_enabled\", \"0\")\n\tif wechatEnabled != \"1\" {\n\t\tlogger.Log.Info(\"WeChat is disabled in system config, skipping WeChat initialization\")\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\t// 开关已启用,从数据库读取企业微信配置\n\tagentIDStr := GetConfigValueByKey(\"wechat_agentid\", \"\")\n\tcorpid := GetConfigValueByKey(\"wechat_corpid\", \"\")\n\tsecret := GetConfigValueByKey(\"wechat_secret\", \"\") // 这里会自动解密\n\n\t// 验证必填配置项\n\tif agentIDStr == \"\" || corpid == \"\" || secret == \"\" {\n\t\tlogger.Log.Info(\"WeChat is enabled but configuration is incomplete (agentid/corpid/secret), WeChat app will not be initialized\")\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\t// 解析 AgentID\n\tAgentId, err := strconv.ParseInt(agentIDStr, 10, 64)\n\tif err != nil {\n\t\tlogger.Log.Error(\"wechat_agentid parse error:\", err)\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\n\t// 初始化企业微信客户端\n\tclient := workwx.New(corpid)\n\tWeApp = client.WithApp(secret, AgentId)\n\tWeApp.SpawnAccessTokenRefresher()\n\tlogger.Log.Info(\"WeChat inited successfully!\")\n}\n\n// DatabaseInit 数据初始化\nfunc DatabaseInit() error {\n\t//数据初始化操作 - 使用 GORM\n\tvar v User\n\terr := DB.Where(\"username = ?\", \"admin\").First(&v).Error\n\t//检查权限\n\tif err == nil && v.Operation == \"\" {\n\t\terr := DB.Model(&User{}).Where(\"id = ?\", v.ID).Update(\"operation\", \"['add', 'edit', 'delete','update']\").Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn fmt.Errorf(\"update admin operation failed: %w\", err)\n\t\t}\n\t\tlogger.Log.Info(\"update admin operation successfully\")\n\t}\n\t//添加管理员账号\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\tlogger.Log.Info(\"the admin user does not exist, create a new admin account later!\")\n\n\t\t// 构建默认主题配置\n\t\tdefaultThemeConfig := map[string]interface{}{\n\t\t\t\"theme\": map[string]string{\n\t\t\t\t\"color\": \"#722ed1\",\n\t\t\t\t\"mode\": \"dark\",\n\t\t\t\t\"success\": \"#52c41a\",\n\t\t\t\t\"warning\": \"#faad14\",\n\t\t\t\t\"error\": \"#f5222f\",\n\t\t\t},\n\t\t\t\"animate\": map[string]interface{}{\n\t\t\t\t\"disabled\": true,\n\t\t\t\t\"name\": \"lightSpeed\",\n\t\t\t\t\"direction\": \"left\",\n\t\t\t},\n\t\t}\n\n\t\t// 将配置转换为 JSON 字符串\n\t\tthemeJSON, err := json.Marshal(defaultThemeConfig)\n\t\tif err != nil {\n\t\t\tlogger.Log.Error(\"Failed to marshal default theme config:\", err)\n\t\t\tthemeJSON = []byte(`{\"theme\":{\"color\":\"#722ed1\",\"mode\":\"dark\",\"success\":\"#52c41a\",\"warning\":\"#faad14\",\"error\":\"#f5222f\"},\"animate\":{\"disabled\":true,\"name\":\"lightSpeed\",\"direction\":\"left\"}}`)\n\t\t}\n\n\t\t// 使用结构体插入管理员账号\n\t\tpassword, _ := utils.PasswordHash(\"Zbxtable\")\n\t\tuser := User{\n\t\t\tUsername: \"admin\",\n\t\t\tPassword: password,\n\t\t\tAvatar: DefaultAvatar,\n\t\t\tRole: \"admin\",\n\t\t\tOperation: \"['add', 'edit', 'delete','update']\",\n\t\t\tTheme: string(themeJSON),\n\t\t\tStatus: 0,\n\t\t}\n\t\terr = DB.Create(&user).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn fmt.Errorf(\"create admin account failed: %w\", err)\n\t\t}\n\t\tlogger.Log.Info(\"create an administrator account successfully, the admin ID is:\", user.ID)\n\t}\n\t//初始化系统数据\n\tif err = EnsureSystemBindingsForAssetTypes(); err != nil {\n\t\tlogger.Log.Info(\"Init system info error!\", err)\n\t\treturn fmt.Errorf(\"init system data failed: %w\", err)\n\t}\n\t//告警默认规则初始化 default rule\n\tvar rules []Rule\n\terr = DB.Where(\"m_type = ?\", \"2\").Find(&rules).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn fmt.Errorf(\"query default rules failed: %w\", err)\n\t}\n\tif len(rules) == 0 {\n\t\t// 使用 \"*\" 作为全局默认规则,匹配所有租户\n\t\tdefaultRule := []Rule{\n\t\t\t{\n\t\t\t\tName: \"全局默认规则\",\n\t\t\t\tZIDs: \"*\",\n\t\t\t\tMType: \"2\",\n\t\t\t\tChannel: \"wechat_robot\",\n\t\t\t\tUserIds: \"1\",\n\t\t\t\tSweek: \"0,1,2,3,4,5,6\",\n\t\t\t\tStime: \"00:00\",\n\t\t\t\tEtime: \"23.59\",\n\t\t\t\tStatus: \"0\"},\n\t\t}\n\t\terr := DB.Create(&defaultRule).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init default rule error!\", err)\n\t\t\treturn fmt.Errorf(\"init default rule failed: %w\", err)\n\t\t}\n\t\tlogger.Log.Info(\"Init default rule successfully!\")\n\t}\n\t// 初始化加密密钥(如果不存在)\n\tvar encryptionKeyConfig Config\n\terr = DB.Where(\"config_key = ?\", \"encryption_key\").First(&encryptionKeyConfig).Error\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t// 生成随机加密密钥\n\t\trandomKey := utils.GenerateRandomKey()\n\t\tencryptionKeyConfig = Config{\n\t\t\tName: \"加密密钥\",\n\t\t\tConfigKey: \"encryption_key\",\n\t\t\tConfigValue: randomKey,\n\t\t\tComment: \"用于加密存储敏感信息(如Zabbix密码和Token)的密钥,系统自动生成,不可修改\",\n\t\t}\n\t\tif insertErr := DB.Create(&encryptionKeyConfig).Error; insertErr != nil {\n\t\t\tlogger.Log.Error(\"Init encryption_key error:\", insertErr)\n\t\t\treturn fmt.Errorf(\"init encryption_key failed: %w\", insertErr)\n\t\t} else {\n\t\t\tlogger.Log.Info(\"Init encryption_key successfully! Key length:\", len(randomKey))\n\t\t}\n\t}\n\n\t// 初始化内置映射规则\n\t//InitDefaultMappingRules()\n\n\t// 获取结构化的默认配置\n\tdefaultConfigs := getDefaultConfigs()\n\n\tfor _, cfgItem := range defaultConfigs {\n\t\tvar existing Config\n\t\terr = DB.Where(\"config_key = ?\", cfgItem.ConfigKey).First(&existing).Error\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\tif insertErr := DB.Create(&cfgItem).Error; insertErr != nil {\n\t\t\t\tlogger.Log.Infof(\"Init config key %s error: %v\", cfgItem.ConfigKey, insertErr)\n\t\t\t\treturn fmt.Errorf(\"init config key %s failed: %w\", cfgItem.ConfigKey, insertErr)\n\t\t\t}\n\t\t\tlogger.Log.Infof(\"Init config key %s successfully!\", cfgItem.ConfigKey)\n\t\t} else if err != nil {\n\t\t\tlogger.Log.Infof(\"Init config key %s error: %v\", cfgItem.ConfigKey, err)\n\t\t\treturn fmt.Errorf(\"check config key %s failed: %w\", cfgItem.ConfigKey, err)\n\t\t}\n\t}\n\t//默认菜单初始化\n\tif err := InitMenuData(); err != nil {\n\t\treturn fmt.Errorf(\"init menu data failed: %w\", err)\n\t}\n\t//检查并添加缺失的菜单项(用于版本升级)\n\tif err := CheckAndAddMenus(); err != nil {\n\t\treturn fmt.Errorf(\"check and add menus failed: %w\", err)\n\t}\n\n\t// 存量 asset_type 记录回填 monitor_type(幂等,需在 InitDefaultAssetTypes 之前)\n\t_ = MigrateAssetTypeMonitorType()\n\t// 初始化内置设备分组(幂等)\n\tif err := InitDefaultAssetGroups(); err != nil {\n\t\treturn fmt.Errorf(\"init default asset groups failed: %w\", err)\n\t}\n\t// 初始化默认资产类型(幂等)\n\tif err := InitDefaultAssetTypes(); err != nil {\n\t\treturn fmt.Errorf(\"init default asset types failed: %w\", err)\n\t}\n\t// 统一存量默认资产类型名称(幂等)\n\tif err := MigrateAssetTypeNames(); err != nil {\n\t\treturn fmt.Errorf(\"migrate asset type names failed: %w\", err)\n\t}\n\t// 回填存量 System 记录的 type_code(幂等)\n\tif err := MigrateSystemTypeCodes(); err != nil {\n\t\treturn fmt.Errorf(\"migrate system type codes failed: %w\", err)\n\t}\n\n\t// 启动异步预热:主机统计缓存(资产树页面首次打开即命中缓存,无需等待 Zabbix)\n\tWarmHostCountCache()\n\n\treturn nil\n}\n\nfunc GetConfKey(v string) string {\n\t// 优先从 .env 中读取(便于开发环境配置敏感信息且不提交到 git)\n\tenvConfigOnce.Do(func() {\n\t\tfile, err := os.Open(\".env\")\n\t\tif err != nil {\n\t\t\t// 没有 .env 文件时直接跳过,后面回退到 app.conf\n\t\t\tenvFileExists = false\n\t\t\treturn\n\t\t}\n\t\tdefer file.Close()\n\n\t\tenvFileExists = true\n\t\tenvConfig = make(map[string]string)\n\n\t\tscanner := bufio.NewScanner(file)\n\t\tfor scanner.Scan() {\n\t\t\tline := strings.TrimSpace(scanner.Text())\n\t\t\tif line == \"\" || strings.HasPrefix(line, \"#\") || strings.HasPrefix(line, \";\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tparts := strings.SplitN(line, \"=\", 2)\n\t\t\tif len(parts) != 2 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tkey := strings.TrimSpace(parts[0])\n\t\t\tval := strings.TrimSpace(parts[1])\n\t\t\t// 去掉可能的引号\n\t\t\tval = strings.Trim(val, `\"'`)\n\t\t\tif key != \"\" {\n\t\t\t\tenvConfig[key] = val // 即使 val 为空也认为是有效配置\n\t\t\t}\n\t\t}\n\t})\n\n\t// 如果存在 .env 文件,则所有配置完全由 .env 决定:\n\t// - key 存在:返回对应值(可以是空字符串)\n\t// - key 不存在:返回空字符串\n\tif envFileExists {\n\t\tif envConfig == nil {\n\t\t\treturn \"\"\n\t\t}\n\t\tif val, ok := envConfig[v]; ok {\n\t\t\treturn val\n\t\t}\n\t\treturn \"\"\n\t}\n\t// 其次从 config/app.conf 读取\n\tcfg, err := ini.Load(\"./config/app.conf\")\n\tif err != nil {\n\t\tlogger.Log.Error(err)\n\t\treturn \"\"\n\t}\n\tp, err := cfg.Section(\"\").GetKey(v)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn p.String()\n}\n\n// IsPasswordConfigured 检查是否配置了密码(用于图形查看)\n// 优先检查数据库中是否有激活的 Zabbix 实例配置了用户名和密码\n// 注意:查看图形需要用户名和密码,Token 方式无法用于 Web 登录\nfunc IsPasswordConfigured() bool {\n\t// 1. 优先检查数据库中是否有激活的 Zabbix 实例\n\tvar activeInstance ZabbixInstance\n\terr := DB.Where(\"is_active = ?\", true).First(&activeInstance).Error\n\tif err == nil {\n\t\t// 找到激活的实例,检查是否配置了用户名和密码(查看图形必须用密码,Token 无法用于 Web 登录)\n\t\tif strings.TrimSpace(activeInstance.User) != \"\" && strings.TrimSpace(activeInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 2. 如果数据库中没有激活实例,检查是否有任何可用的实例配置了密码\n\tvar anyInstance ZabbixInstance\n\terr = DB.Where(\"enabled = ?\", true).First(&anyInstance).Error\n\tif err == nil {\n\t\t// 找到可用的实例,检查是否配置了用户名和密码\n\t\tif strings.TrimSpace(anyInstance.User) != \"\" && strings.TrimSpace(anyInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 3. 回退到配置文件检查(兼容旧版本)\n\tpass := GetConfKey(\"zabbix_pass\")\n\tuser := GetConfKey(\"zabbix_user\")\n\treturn pass != \"\" && user != \"\"\n}\n\n// GetEncryptionKey 获取加密密钥\n// 优先从数据库读取,如果数据库中没有则从环境变量读取,最后使用默认密钥\nfunc GetEncryptionKey() string {\n\t// 1. 优先从数据库读取\n\tkey := GetConfigValueByKey(\"encryption_key\", \"\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 2. 从环境变量或配置文件读取\n\tkey = GetConfKey(\"encryption_key\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 3. 使用默认密钥(不应该到这里,因为 DatabaseInit 会生成)\n\tlogger.Log.Warn(\"使用默认加密密钥,建议在数据库中配置 encryption_key\")\n\treturn \"zbxtable-default-encryption-key-2024\"\n}\n\n// getDefaultConfigs 返回系统默认配置列表\n// 配置按功能模块分组,便于维护和管理\nfunc getDefaultConfigs() []Config {\n\tconfigs := make([]Config, 0)\n\n\t// 系统外观配置\n\tconfigs = append(configs, getSystemAppearanceConfigs()...)\n\n\t// 初始配置状态\n\tconfigs = append(configs, getInitialSetupConfigs()...)\n\n\t// Dashboard 相关配置\n\tconfigs = append(configs, getDashboardConfigs()...)\n\n\t// 计划任务配置\n\tconfigs = append(configs, getTaskConfigs()...)\n\n\t// 邮件配置\n\tconfigs = append(configs, getEmailConfigs()...)\n\n\t// 企业微信配置\n\tconfigs = append(configs, getWeChatConfigs()...)\n\n\t// AI 配置\n\tconfigs = append(configs, getAIConfigs()...)\n\n\t// 演示模式配置\n\tconfigs = append(configs, getDemoConfigs()...)\n\n\treturn configs\n}\n\n// getDemoConfigs 演示模式配置\nfunc getDemoConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"演示模式\",\n\t\t\tConfigKey: \"demo_mode\",\n\t\t\tConfigValue: \"false\",\n\t\t\tComment: \"是否开启演示模式(只读):true 开启, false 关闭\",\n\t\t},\n\t}\n}\n\n// getSystemAppearanceConfigs 系统外观配置\nfunc getSystemAppearanceConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"系统名称\",\n\t\t\tConfigKey: \"system_name\",\n\t\t\tConfigValue: \"ZbxTable\",\n\t\t\tComment: \"系统显示的名称\",\n\t\t},\n\t\t{\n\t\t\tName: \"系统副标题\",\n\t\t\tConfigKey: \"system_subtitle\",\n\t\t\tConfigValue: \"Zabbix 监控数据可视化平台\",\n\t\t\tComment: \"登录页显示的系统副标题\",\n\t\t},\n\t\t{\n\t\t\tName: \"系统Logo\",\n\t\t\tConfigKey: \"system_logo\",\n\t\t\tConfigValue: \"data:image/png;base64,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\",\n\t\t\tComment: \"系统Logo的base64编码数据\",\n\t\t},\n\t}\n}\n\n// getInitialSetupConfigs 初始配置状态\nfunc getInitialSetupConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"初始配置完成\",\n\t\t\tConfigKey: \"initial_setup_completed\",\n\t\t\tConfigValue: \"0\",\n\t\t\tComment: \"标记系统是否完成初始配置:1 已完成,0 未完成\",\n\t\t},\n\t}\n}\n\n// getDashboardConfigs Dashboard 相关配置\nfunc getDashboardConfigs() []Config {\n\treturn []Config{\n\t\t// {\n\t\t// \tName: \"数据面板\",\n\t\t// \tConfigKey: \"zbx_dash\",\n\t\t// \tConfigValue: \"0\",\n\t\t// \tComment: \"是否开启Zabbix看板:1 开启,0 关闭\",\n\t\t// },\n\t\t// {\n\t\t// \tName: \"面板配置\",\n\t\t// \tConfigKey: \"dash_id\",\n\t\t// \tConfigValue: \"1\",\n\t\t// \tComment: \"需要引入的Zabbix面板的ID,默认为1\",\n\t\t// },\n\t\t{\n\t\t\tName: \"Linux Top数量\",\n\t\t\tConfigKey: \"dash_top_lin_num\",\n\t\t\tConfigValue: \"10\",\n\t\t\tComment: \"首页 Linux Top 列表展示数量\",\n\t\t},\n\t\t{\n\t\t\tName: \"Windows Top数量\",\n\t\t\tConfigKey: \"dash_top_win_num\",\n\t\t\tConfigValue: \"10\",\n\t\t\tComment: \"首页 Windows Top 列表展示数量\",\n\t\t},\n\t\t{\n\t\t\tName: \"Webhook回调地址\",\n\t\t\tConfigKey: \"webhook_url\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"webhook通知地址\",\n\t\t},\n\t}\n}\n\n// getTaskConfigs 计划任务配置\nfunc getTaskConfigs() []Config {\n\tconfigs := make([]Config, 0, len(getScheduledTaskDefinitions())*2)\n\n\tfor _, task := range getScheduledTaskDefinitions() {\n\t\tconfigs = append(configs,\n\t\t\tConfig{\n\t\t\t\tName: task.Name + \"开关\",\n\t\t\t\tConfigKey: task.EnabledKey,\n\t\t\t\tConfigValue: task.DefaultEnabled,\n\t\t\t\tComment: task.Name + \"计划任务是否启用:1 启用,0 停用\",\n\t\t\t},\n\t\t\tConfig{\n\t\t\t\tName: task.Name + \"Cron\",\n\t\t\t\tConfigKey: task.CronKey,\n\t\t\t\tConfigValue: task.DefaultCron,\n\t\t\t\tComment: task.Name + \"执行周期,格式:秒 分 时 日 月 周\",\n\t\t\t},\n\t\t)\n\t}\n\n\treturn configs\n}\n\n// getEmailConfigs 邮件配置\nfunc getEmailConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"邮件发件人\",\n\t\t\tConfigKey: \"email_from\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"告警邮件发件人邮箱地址\",\n\t\t},\n\t\t{\n\t\t\tName: \"邮件昵称\",\n\t\t\tConfigKey: \"email_nickname\",\n\t\t\tConfigValue: \"ZbxTable\",\n\t\t\tComment: \"告警邮件显示的发件人昵称\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 密码/授权码\",\n\t\t\tConfigKey: \"email_secret\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"SMTP 登录密码或授权码\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 服务器\",\n\t\t\tConfigKey: \"email_host\",\n\t\t\tConfigValue: \"smtp.qq.com\",\n\t\t\tComment: \"SMTP 服务器地址\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 端口\",\n\t\t\tConfigKey: \"email_port\",\n\t\t\tConfigValue: \"465\",\n\t\t\tComment: \"SMTP 端口号\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 使用 SSL\",\n\t\t\tConfigKey: \"email_isSSl\",\n\t\t\tConfigValue: \"true\",\n\t\t\tComment: \"是否启用 SSL:true/false\",\n\t\t},\n\t}\n}\n\n// getWeChatConfigs 企业微信配置\nfunc getWeChatConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"企业微信开关\",\n\t\t\tConfigKey: \"wechat_enabled\",\n\t\t\tConfigValue: \"0\",\n\t\t\tComment: \"是否启用企业微信:1 启用,0 禁用\",\n\t\t},\n\t\t{\n\t\t\tName: \"企业微信 AgentID\",\n\t\t\tConfigKey: \"wechat_agentid\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"企业微信应用的 AgentID\",\n\t\t},\n\t\t{\n\t\t\tName: \"企业微信 CorpID\",\n\t\t\tConfigKey: \"wechat_corpid\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"企业微信企业ID\",\n\t\t},\n\t\t{\n\t\t\tName: \"企业微信 Secret\",\n\t\t\tConfigKey: \"wechat_secret\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"企业微信应用的 Secret\",\n\t\t},\n\t}\n}\n\n// getAIConfigs AI 配置\nfunc getAIConfigs() []Config {\n\treturn []Config{\n\t\t// AI 类型选择\n\t\t{\n\t\t\tName: \"AI 类型\",\n\t\t\tConfigKey: \"ai_type\",\n\t\t\tConfigValue: \"ollama\",\n\t\t\tComment: \"选择使用的 AI 服务类型:ollama、deepseek 或 custom\",\n\t\t},\n\t\t// Ollama 配置\n\t\t{\n\t\t\tName: \"Ollama Host\",\n\t\t\tConfigKey: \"ollama_host\",\n\t\t\tConfigValue: \"http://localhost:11434\",\n\t\t\tComment: \"Ollama 服务地址,如 http://127.0.0.1:11434\",\n\t\t},\n\t\t{\n\t\t\tName: \"Ollama Model\",\n\t\t\tConfigKey: \"ollama_model\",\n\t\t\tConfigValue: \"deepseek-r1:32b\",\n\t\t\tComment: \"默认使用的大模型名称\",\n\t\t},\n\t\t// Deepseek 配置\n\t\t{\n\t\t\tName: \"Deepseek API Key\",\n\t\t\tConfigKey: \"deepseek_api_key\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"Deepseek API 密钥\",\n\t\t},\n\t\t{\n\t\t\tName: \"Deepseek Model\",\n\t\t\tConfigKey: \"deepseek_model\",\n\t\t\tConfigValue: \"deepseek-chat\",\n\t\t\tComment: \"Deepseek 模型名称,如 deepseek-chat\",\n\t\t},\n\t\t{\n\t\t\tName: \"Deepseek Base URL\",\n\t\t\tConfigKey: \"deepseek_base_url\",\n\t\t\tConfigValue: \"https://api.deepseek.com\",\n\t\t\tComment: \"Deepseek API 地址,默认为 https://api.deepseek.com\",\n\t\t},\n\t\t// Custom(OpenAI-compatible) 配置\n\t\t{\n\t\t\tName: \"Custom API Key\",\n\t\t\tConfigKey: \"custom_api_key\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"OpenAI 兼容接口的 API Key\",\n\t\t},\n\t\t{\n\t\t\tName: \"Custom Model\",\n\t\t\tConfigKey: \"custom_model\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"OpenAI 兼容接口的模型名称\",\n\t\t},\n\t\t{\n\t\t\tName: \"Custom Base URL\",\n\t\t\tConfigKey: \"custom_base_url\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"OpenAI 兼容接口地址,如 https://api.example.com/v1\",\n\t\t},\n\t\t{\n\t\t\tName: \"告警分析提示词\",\n\t\t\tConfigKey: \"alarm_analysis_prompt\",\n\t\t\tConfigValue: \"你是专业运维分析师。请基于以下告警信息进行分析:\\n\\n设备名称:{{hostname}}\\nIP:{{host_ip}}\\n告警描述:{{message}}\\n告警详情:{{detail}}\\n告警级别:{{level}}\\n告警状态:{{status}}\\n\\n请输出:\\n1. 可能根因(按概率排序)\\n2. 排查步骤(关键命令/检查项)\\n3. 修复方案与风险\\n4. 防复发建议\",\n\t\t\tComment: \"用于AI助手告警分析的提示词模板,支持占位符:{{hostname}} {{host_ip}} {{message}} {{detail}} {{level}} {{status}} {{alarm_context}}\",\n\t\t},\n\t}\n}\n"}, "b8180028a12561811542b690573791bee4619413": {"t": 1785754888.4147594, "n": 22027, "h": "1679a43cd20b693f", "c": "package model\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"zbxtable/pkg/logger\"\n\t\"zbxtable/pkg/utils\"\n\n\tzabbix \"github.com/canghai908/zabbix-go\"\n\t_ \"github.com/go-sql-driver/mysql\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t_ \"github.com/lib/pq\"\n\tworkwx \"github.com/xen0n/go-workwx\"\n\tini \"gopkg.in/ini.v1\"\n\t\"gorm.io/gorm\"\n)\n\nvar (\n\tAPI = &zabbix.API{}\n\tjson = jsoniter.Config{\n\t\tEscapeHTML: false,\n\t\tSortMapKeys: true,\n\t\tValidateJsonRawMessage: true,\n\t}.Froze()\n\tZBX_VER string\n\tZBX_V bool\n\tVersion string\n\tGitHash string\n\tBuildTime string\n\tWeApp *workwx.WorkwxApp // 不初始化,默认为 nil\n\n\tenvConfig map[string]string\n\tenvConfigOnce sync.Once\n\tenvFileExists bool\n)\n\nconst (\n\t//ms-agent 常量定义\n\tMSName = \"ms-agent\"\n\tMSUser = \"ms-agent\"\n\tMSGroup = \"MS-Agent\"\n\tMSMedia = \"MS-Agent\"\n\tMSAction = \"MS-Agent\"\n\t//webhook 常量定义\n\tWebhookName = \"ZbxTable\"\n\tWebhookAction = \"ZbxTable Webhook\"\n\tWebhookGroup = \"ZbxTable Webhook\"\n\tWebhookUser = \"zbxtable-webhook\"\n\t//down\n\tDownloadPath = \"./download/\"\n\tTplPath = \"./assets/templates/\"\n\t// 默认用户头像 (空字符串,由前端处理默认头像)\n\tDefaultAvatar = \"\"\n\t//默认更新服务器\n\tUpdateURL = \"http://dl.cactifans.com/stable/\"\n\n\t// 上传目录\n\tUploadDir = \"./upload\"\n\t// 背景图片上传目录\n\tBackgroundImageDir = \"./upload/background\"\n\t// 允许的图片格式\n\tAllowedImageExts = \".jpg,.jpeg,.png,.gif,.bmp,.webp,.svg\"\n\t// 最大文件大小 (10MB)\n\tMaxFileSize = 10 * 1024 * 1024\n)\n\n// TableName 表名前缀\nfunc TableName(str string) string {\n\treturn fmt.Sprintf(\"%s%s\", \"zbxtable_\", str)\n}\n\n// 接收数据库信息初始化\nfunc ModelInit(dbtype, dbhost, dbuser, dbpass, dbname, dbport string) error {\n\t// 直接使用 GORM\n\trunmode := GetConfKey(\"runmode\")\n\terr := InitGormDB(dbtype, dbhost, dbuser, dbpass, dbname, dbport, runmode)\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to connect database: \", err)\n\t\treturn err\n\t}\n\tlogger.Log.Info(\"Database connected!\")\n\t// 自动迁移表\n\terr = AutoMigrate()\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to auto migrate: \", err)\n\t\treturn err\n\t}\n\t// 基础数据初始化\n\tif err := DatabaseInit(); err != nil {\n\t\tlogger.Log.Error(\"Database init failed: \", err)\n\t\treturn err\n\t}\n\t// 使用go-cache替代Redis(不再需要Redis连接)\n\tif err := InitCache(); err != nil {\n\t\tlogger.Log.Error(\"Cache init failed: \", err)\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc InitWechat() {\n\t// 首先检查企业微信开关是否启用\n\twechatEnabled := GetConfigValueByKey(\"wechat_enabled\", \"0\")\n\tif wechatEnabled != \"1\" {\n\t\tlogger.Log.Info(\"WeChat is disabled in system config, skipping WeChat initialization\")\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\t// 开关已启用,从数据库读取企业微信配置\n\tagentIDStr := GetConfigValueByKey(\"wechat_agentid\", \"\")\n\tcorpid := GetConfigValueByKey(\"wechat_corpid\", \"\")\n\tsecret := GetConfigValueByKey(\"wechat_secret\", \"\") // 这里会自动解密\n\n\t// 验证必填配置项\n\tif agentIDStr == \"\" || corpid == \"\" || secret == \"\" {\n\t\tlogger.Log.Info(\"WeChat is enabled but configuration is incomplete (agentid/corpid/secret), WeChat app will not be initialized\")\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\t// 解析 AgentID\n\tAgentId, err := strconv.ParseInt(agentIDStr, 10, 64)\n\tif err != nil {\n\t\tlogger.Log.Error(\"wechat_agentid parse error:\", err)\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\n\t// 初始化企业微信客户端\n\tclient := workwx.New(corpid)\n\tWeApp = client.WithApp(secret, AgentId)\n\tWeApp.SpawnAccessTokenRefresher()\n\tlogger.Log.Info(\"WeChat inited successfully!\")\n}\n\n// DatabaseInit 数据初始化\nfunc DatabaseInit() error {\n\t//数据初始化操作 - 使用 GORM\n\tvar v User\n\terr := DB.Where(\"username = ?\", \"admin\").First(&v).Error\n\t//检查权限\n\tif err == nil && v.Operation == \"\" {\n\t\terr := DB.Model(&User{}).Where(\"id = ?\", v.ID).Update(\"operation\", \"['add', 'edit', 'delete','update']\").Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn fmt.Errorf(\"update admin operation failed: %w\", err)\n\t\t}\n\t\tlogger.Log.Info(\"update admin operation successfully\")\n\t}\n\t//添加管理员账号\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\tlogger.Log.Info(\"the admin user does not exist, create a new admin account later!\")\n\n\t\t// 构建默认主题配置\n\t\tdefaultThemeConfig := map[string]interface{}{\n\t\t\t\"theme\": map[string]string{\n\t\t\t\t\"color\": \"#722ed1\",\n\t\t\t\t\"mode\": \"dark\",\n\t\t\t\t\"success\": \"#52c41a\",\n\t\t\t\t\"warning\": \"#faad14\",\n\t\t\t\t\"error\": \"#f5222f\",\n\t\t\t},\n\t\t\t\"animate\": map[string]interface{}{\n\t\t\t\t\"disabled\": true,\n\t\t\t\t\"name\": \"lightSpeed\",\n\t\t\t\t\"direction\": \"left\",\n\t\t\t},\n\t\t}\n\n\t\t// 将配置转换为 JSON 字符串\n\t\tthemeJSON, err := json.Marshal(defaultThemeConfig)\n\t\tif err != nil {\n\t\t\tlogger.Log.Error(\"Failed to marshal default theme config:\", err)\n\t\t\tthemeJSON = []byte(`{\"theme\":{\"color\":\"#722ed1\",\"mode\":\"dark\",\"success\":\"#52c41a\",\"warning\":\"#faad14\",\"error\":\"#f5222f\"},\"animate\":{\"disabled\":true,\"name\":\"lightSpeed\",\"direction\":\"left\"}}`)\n\t\t}\n\n\t\t// 使用结构体插入管理员账号\n\t\tpassword, _ := utils.PasswordHash(\"Zbxtable\")\n\t\tuser := User{\n\t\t\tUsername: \"admin\",\n\t\t\tPassword: password,\n\t\t\tAvatar: DefaultAvatar,\n\t\t\tRole: \"admin\",\n\t\t\tOperation: \"['add', 'edit', 'delete','update']\",\n\t\t\tTheme: string(themeJSON),\n\t\t\tStatus: 0,\n\t\t}\n\t\terr = DB.Create(&user).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn fmt.Errorf(\"create admin account failed: %w\", err)\n\t\t}\n\t\tlogger.Log.Info(\"create an administrator account successfully, the admin ID is:\", user.ID)\n\t}\n\t//初始化系统数据\n\tif err = EnsureSystemBindingsForAssetTypes(); err != nil {\n\t\tlogger.Log.Info(\"Init system info error!\", err)\n\t\treturn fmt.Errorf(\"init system data failed: %w\", err)\n\t}\n\t//告警默认规则初始化 default rule\n\tvar rules []Rule\n\terr = DB.Where(\"m_type = ?\", \"2\").Find(&rules).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn fmt.Errorf(\"query default rules failed: %w\", err)\n\t}\n\tif len(rules) == 0 {\n\t\t// 使用 \"*\" 作为全局默认规则,匹配所有租户\n\t\tdefaultRule := []Rule{\n\t\t\t{\n\t\t\t\tName: \"全局默认规则\",\n\t\t\t\tZIDs: \"*\",\n\t\t\t\tMType: \"2\",\n\t\t\t\tChannel: \"wechat_robot\",\n\t\t\t\tUserIds: \"1\",\n\t\t\t\tSweek: \"0,1,2,3,4,5,6\",\n\t\t\t\tStime: \"00:00\",\n\t\t\t\tEtime: \"23.59\",\n\t\t\t\tStatus: \"0\"},\n\t\t}\n\t\terr := DB.Create(&defaultRule).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init default rule error!\", err)\n\t\t\treturn fmt.Errorf(\"init default rule failed: %w\", err)\n\t\t}\n\t\tlogger.Log.Info(\"Init default rule successfully!\")\n\t}\n\t// 初始化加密密钥(如果不存在)\n\tvar encryptionKeyConfig Config\n\terr = DB.Where(\"config_key = ?\", \"encryption_key\").First(&encryptionKeyConfig).Error\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t// 生成随机加密密钥\n\t\trandomKey := utils.GenerateRandomKey()\n\t\tencryptionKeyConfig = Config{\n\t\t\tName: \"加密密钥\",\n\t\t\tConfigKey: \"encryption_key\",\n\t\t\tConfigValue: randomKey,\n\t\t\tComment: \"用于加密存储敏感信息(如Zabbix密码和Token)的密钥,系统自动生成,不可修改\",\n\t\t}\n\t\tif insertErr := DB.Create(&encryptionKeyConfig).Error; insertErr != nil {\n\t\t\tlogger.Log.Error(\"Init encryption_key error:\", insertErr)\n\t\t\treturn fmt.Errorf(\"init encryption_key failed: %w\", insertErr)\n\t\t} else {\n\t\t\tlogger.Log.Info(\"Init encryption_key successfully! Key length:\", len(randomKey))\n\t\t}\n\t}\n\n\t// 初始化内置映射规则\n\t//InitDefaultMappingRules()\n\n\t// 获取结构化的默认配置\n\tdefaultConfigs := getDefaultConfigs()\n\n\tfor _, cfgItem := range defaultConfigs {\n\t\tvar existing Config\n\t\terr = DB.Where(\"config_key = ?\", cfgItem.ConfigKey).First(&existing).Error\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\tif insertErr := DB.Create(&cfgItem).Error; insertErr != nil {\n\t\t\t\tlogger.Log.Infof(\"Init config key %s error: %v\", cfgItem.ConfigKey, insertErr)\n\t\t\t\treturn fmt.Errorf(\"init config key %s failed: %w\", cfgItem.ConfigKey, insertErr)\n\t\t\t}\n\t\t\tlogger.Log.Infof(\"Init config key %s successfully!\", cfgItem.ConfigKey)\n\t\t} else if err != nil {\n\t\t\tlogger.Log.Infof(\"Init config key %s error: %v\", cfgItem.ConfigKey, err)\n\t\t\treturn fmt.Errorf(\"check config key %s failed: %w\", cfgItem.ConfigKey, err)\n\t\t}\n\t}\n\t//默认菜单初始化\n\tif err := InitMenuData(); err != nil {\n\t\treturn fmt.Errorf(\"init menu data failed: %w\", err)\n\t}\n\t//检查并添加缺失的菜单项(用于版本升级)\n\tif err := CheckAndAddMenus(); err != nil {\n\t\treturn fmt.Errorf(\"check and add menus failed: %w\", err)\n\t}\n\n\t// 存量 asset_type 记录回填 monitor_type(幂等,需在 InitDefaultAssetTypes 之前)\n\t_ = MigrateAssetTypeMonitorType()\n\t// 初始化内置设备分组(幂等)\n\tif err := InitDefaultAssetGroups(); err != nil {\n\t\treturn fmt.Errorf(\"init default asset groups failed: %w\", err)\n\t}\n\t// 初始化默认资产类型(幂等)\n\tif err := InitDefaultAssetTypes(); err != nil {\n\t\treturn fmt.Errorf(\"init default asset types failed: %w\", err)\n\t}\n\t// 统一存量默认资产类型名称(幂等)\n\tif err := MigrateAssetTypeNames(); err != nil {\n\t\treturn fmt.Errorf(\"migrate asset type names failed: %w\", err)\n\t}\n\t// 回填存量 System 记录的 type_code(幂等)\n\tif err := MigrateSystemTypeCodes(); err != nil {\n\t\treturn fmt.Errorf(\"migrate system type codes failed: %w\", err)\n\t}\n\n\t// 启动异步预热:主机统计缓存(资产树页面首次打开即命中缓存,无需等待 Zabbix)\n\tWarmHostCountCache()\n\n\treturn nil\n}\n\nfunc GetConfKey(v string) string {\n\t// 优先从 .env 中读取(便于开发环境配置敏感信息且不提交到 git)\n\tenvConfigOnce.Do(func() {\n\t\tfile, err := os.Open(\".env\")\n\t\tif err != nil {\n\t\t\t// 没有 .env 文件时直接跳过,后面回退到 app.conf\n\t\t\tenvFileExists = false\n\t\t\treturn\n\t\t}\n\t\tdefer file.Close()\n\n\t\tenvFileExists = true\n\t\tenvConfig = make(map[string]string)\n\n\t\tscanner := bufio.NewScanner(file)\n\t\tfor scanner.Scan() {\n\t\t\tline := strings.TrimSpace(scanner.Text())\n\t\t\tif line == \"\" || strings.HasPrefix(line, \"#\") || strings.HasPrefix(line, \";\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tparts := strings.SplitN(line, \"=\", 2)\n\t\t\tif len(parts) != 2 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tkey := strings.TrimSpace(parts[0])\n\t\t\tval := strings.TrimSpace(parts[1])\n\t\t\t// 去掉可能的引号\n\t\t\tval = strings.Trim(val, `\"'`)\n\t\t\tif key != \"\" {\n\t\t\t\tenvConfig[key] = val // 即使 val 为空也认为是有效配置\n\t\t\t}\n\t\t}\n\t})\n\n\t// 如果存在 .env 文件,则所有配置完全由 .env 决定:\n\t// - key 存在:返回对应值(可以是空字符串)\n\t// - key 不存在:返回空字符串\n\tif envFileExists {\n\t\tif envConfig == nil {\n\t\t\treturn \"\"\n\t\t}\n\t\tif val, ok := envConfig[v]; ok {\n\t\t\treturn val\n\t\t}\n\t\treturn \"\"\n\t}\n\t// 其次从 config/app.conf 读取\n\tcfg, err := ini.Load(\"./config/app.conf\")\n\tif err != nil {\n\t\tlogger.Log.Error(err)\n\t\treturn \"\"\n\t}\n\tp, err := cfg.Section(\"\").GetKey(v)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn p.String()\n}\n\n// IsPasswordConfigured 检查是否配置了密码(用于图形查看)\n// 优先检查数据库中是否有激活的 Zabbix 实例配置了用户名和密码\n// 注意:查看图形需要用户名和密码,Token 方式无法用于 Web 登录\nfunc IsPasswordConfigured() bool {\n\t// 1. 优先检查数据库中是否有激活的 Zabbix 实例\n\tvar activeInstance ZabbixInstance\n\terr := DB.Where(\"is_active = ?\", true).First(&activeInstance).Error\n\tif err == nil {\n\t\t// 找到激活的实例,检查是否配置了用户名和密码(查看图形必须用密码,Token 无法用于 Web 登录)\n\t\tif strings.TrimSpace(activeInstance.User) != \"\" && strings.TrimSpace(activeInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 2. 如果数据库中没有激活实例,检查是否有任何可用的实例配置了密码\n\tvar anyInstance ZabbixInstance\n\terr = DB.Where(\"enabled = ?\", true).First(&anyInstance).Error\n\tif err == nil {\n\t\t// 找到可用的实例,检查是否配置了用户名和密码\n\t\tif strings.TrimSpace(anyInstance.User) != \"\" && strings.TrimSpace(anyInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 3. 回退到配置文件检查(兼容旧版本)\n\tpass := GetConfKey(\"zabbix_pass\")\n\tuser := GetConfKey(\"zabbix_user\")\n\treturn pass != \"\" && user != \"\"\n}\n\n// GetEncryptionKey 获取加密密钥\n// 优先从数据库读取,如果数据库中没有则从环境变量读取,最后使用默认密钥\nfunc GetEncryptionKey() string {\n\t// 1. 优先从数据库读取\n\tkey := GetConfigValueByKey(\"encryption_key\", \"\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 2. 从环境变量或配置文件读取\n\tkey = GetConfKey(\"encryption_key\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 3. 使用默认密钥(不应该到这里,因为 DatabaseInit 会生成)\n\tlogger.Log.Warn(\"使用默认加密密钥,建议在数据库中配置 encryption_key\")\n\treturn \"zbxtable-default-encryption-key-2024\"\n}\n\n// getDefaultConfigs 返回系统默认配置列表\n// 配置按功能模块分组,便于维护和管理\nfunc getDefaultConfigs() []Config {\n\tconfigs := make([]Config, 0)\n\n\t// 系统外观配置\n\tconfigs = append(configs, getSystemAppearanceConfigs()...)\n\n\t// 初始配置状态\n\tconfigs = append(configs, getInitialSetupConfigs()...)\n\n\t// Dashboard 相关配置\n\tconfigs = append(configs, getDashboardConfigs()...)\n\n\t// 计划任务配置\n\tconfigs = append(configs, getTaskConfigs()...)\n\n\t// 邮件配置\n\tconfigs = append(configs, getEmailConfigs()...)\n\n\t// 企业微信配置\n\tconfigs = append(configs, getWeChatConfigs()...)\n\n\t// AI 配置\n\tconfigs = append(configs, getAIConfigs()...)\n\n\t// 演示模式配置\n\tconfigs = append(configs, getDemoConfigs()...)\n\n\treturn configs\n}\n\n// getDemoConfigs 演示模式配置\nfunc getDemoConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"演示模式\",\n\t\t\tConfigKey: \"demo_mode\",\n\t\t\tConfigValue: \"false\",\n\t\t\tComment: \"是否开启演示模式(只读):true 开启, false 关闭\",\n\t\t},\n\t}\n}\n\n// getSystemAppearanceConfigs 系统外观配置\nfunc getSystemAppearanceConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"系统名称\",\n\t\t\tConfigKey: \"system_name\",\n\t\t\tConfigValue: \"ZbxTable\",\n\t\t\tComment: \"系统显示的名称\",\n\t\t},\n\t\t{\n\t\t\tName: \"系统Logo\",\n\t\t\tConfigKey: \"system_logo\",\n\t\t\tConfigValue: \"data:image/png;base64,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\",\n\t\t\tComment: \"系统Logo的base64编码数据\",\n\t\t},\n\t}\n}\n\n// getInitialSetupConfigs 初始配置状态\nfunc getInitialSetupConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"初始配置完成\",\n\t\t\tConfigKey: \"initial_setup_completed\",\n\t\t\tConfigValue: \"0\",\n\t\t\tComment: \"标记系统是否完成初始配置:1 已完成,0 未完成\",\n\t\t},\n\t}\n}\n\n// getDashboardConfigs Dashboard 相关配置\nfunc getDashboardConfigs() []Config {\n\treturn []Config{\n\t\t// {\n\t\t// \tName: \"数据面板\",\n\t\t// \tConfigKey: \"zbx_dash\",\n\t\t// \tConfigValue: \"0\",\n\t\t// \tComment: \"是否开启Zabbix看板:1 开启,0 关闭\",\n\t\t// },\n\t\t// {\n\t\t// \tName: \"面板配置\",\n\t\t// \tConfigKey: \"dash_id\",\n\t\t// \tConfigValue: \"1\",\n\t\t// \tComment: \"需要引入的Zabbix面板的ID,默认为1\",\n\t\t// },\n\t\t{\n\t\t\tName: \"Linux Top数量\",\n\t\t\tConfigKey: \"dash_top_lin_num\",\n\t\t\tConfigValue: \"10\",\n\t\t\tComment: \"首页 Linux Top 列表展示数量\",\n\t\t},\n\t\t{\n\t\t\tName: \"Windows Top数量\",\n\t\t\tConfigKey: \"dash_top_win_num\",\n\t\t\tConfigValue: \"10\",\n\t\t\tComment: \"首页 Windows Top 列表展示数量\",\n\t\t},\n\t\t{\n\t\t\tName: \"Webhook回调地址\",\n\t\t\tConfigKey: \"webhook_url\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"webhook通知地址\",\n\t\t},\n\t}\n}\n\n// getTaskConfigs 计划任务配置\nfunc getTaskConfigs() []Config {\n\tconfigs := make([]Config, 0, len(getScheduledTaskDefinitions())*2)\n\n\tfor _, task := range getScheduledTaskDefinitions() {\n\t\tconfigs = append(configs,\n\t\t\tConfig{\n\t\t\t\tName: task.Name + \"开关\",\n\t\t\t\tConfigKey: task.EnabledKey,\n\t\t\t\tConfigValue: task.DefaultEnabled,\n\t\t\t\tComment: task.Name + \"计划任务是否启用:1 启用,0 停用\",\n\t\t\t},\n\t\t\tConfig{\n\t\t\t\tName: task.Name + \"Cron\",\n\t\t\t\tConfigKey: task.CronKey,\n\t\t\t\tConfigValue: task.DefaultCron,\n\t\t\t\tComment: task.Name + \"执行周期,格式:秒 分 时 日 月 周\",\n\t\t\t},\n\t\t)\n\t}\n\n\treturn configs\n}\n\n// getEmailConfigs 邮件配置\nfunc getEmailConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"邮件发件人\",\n\t\t\tConfigKey: \"email_from\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"告警邮件发件人邮箱地址\",\n\t\t},\n\t\t{\n\t\t\tName: \"邮件昵称\",\n\t\t\tConfigKey: \"email_nickname\",\n\t\t\tConfigValue: \"ZbxTable\",\n\t\t\tComment: \"告警邮件显示的发件人昵称\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 密码/授权码\",\n\t\t\tConfigKey: \"email_secret\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"SMTP 登录密码或授权码\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 服务器\",\n\t\t\tConfigKey: \"email_host\",\n\t\t\tConfigValue: \"smtp.qq.com\",\n\t\t\tComment: \"SMTP 服务器地址\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 端口\",\n\t\t\tConfigKey: \"email_port\",\n\t\t\tConfigValue: \"465\",\n\t\t\tComment: \"SMTP 端口号\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 使用 SSL\",\n\t\t\tConfigKey: \"email_isSSl\",\n\t\t\tConfigValue: \"true\",\n\t\t\tComment: \"是否启用 SSL:true/false\",\n\t\t},\n\t}\n}\n\n// getWeChatConfigs 企业微信配置\nfunc getWeChatConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"企业微信开关\",\n\t\t\tConfigKey: \"wechat_enabled\",\n\t\t\tConfigValue: \"0\",\n\t\t\tComment: \"是否启用企业微信:1 启用,0 禁用\",\n\t\t},\n\t\t{\n\t\t\tName: \"企业微信 AgentID\",\n\t\t\tConfigKey: \"wechat_agentid\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"企业微信应用的 AgentID\",\n\t\t},\n\t\t{\n\t\t\tName: \"企业微信 CorpID\",\n\t\t\tConfigKey: \"wechat_corpid\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"企业微信企业ID\",\n\t\t},\n\t\t{\n\t\t\tName: \"企业微信 Secret\",\n\t\t\tConfigKey: \"wechat_secret\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"企业微信应用的 Secret\",\n\t\t},\n\t}\n}\n\n// getAIConfigs AI 配置\nfunc getAIConfigs() []Config {\n\treturn []Config{\n\t\t// AI 类型选择\n\t\t{\n\t\t\tName: \"AI 类型\",\n\t\t\tConfigKey: \"ai_type\",\n\t\t\tConfigValue: \"ollama\",\n\t\t\tComment: \"选择使用的 AI 服务类型:ollama、deepseek 或 custom\",\n\t\t},\n\t\t// Ollama 配置\n\t\t{\n\t\t\tName: \"Ollama Host\",\n\t\t\tConfigKey: \"ollama_host\",\n\t\t\tConfigValue: \"http://localhost:11434\",\n\t\t\tComment: \"Ollama 服务地址,如 http://127.0.0.1:11434\",\n\t\t},\n\t\t{\n\t\t\tName: \"Ollama Model\",\n\t\t\tConfigKey: \"ollama_model\",\n\t\t\tConfigValue: \"deepseek-r1:32b\",\n\t\t\tComment: \"默认使用的大模型名称\",\n\t\t},\n\t\t// Deepseek 配置\n\t\t{\n\t\t\tName: \"Deepseek API Key\",\n\t\t\tConfigKey: \"deepseek_api_key\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"Deepseek API 密钥\",\n\t\t},\n\t\t{\n\t\t\tName: \"Deepseek Model\",\n\t\t\tConfigKey: \"deepseek_model\",\n\t\t\tConfigValue: \"deepseek-chat\",\n\t\t\tComment: \"Deepseek 模型名称,如 deepseek-chat\",\n\t\t},\n\t\t{\n\t\t\tName: \"Deepseek Base URL\",\n\t\t\tConfigKey: \"deepseek_base_url\",\n\t\t\tConfigValue: \"https://api.deepseek.com\",\n\t\t\tComment: \"Deepseek API 地址,默认为 https://api.deepseek.com\",\n\t\t},\n\t\t// Custom(OpenAI-compatible) 配置\n\t\t{\n\t\t\tName: \"Custom API Key\",\n\t\t\tConfigKey: \"custom_api_key\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"OpenAI 兼容接口的 API Key\",\n\t\t},\n\t\t{\n\t\t\tName: \"Custom Model\",\n\t\t\tConfigKey: \"custom_model\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"OpenAI 兼容接口的模型名称\",\n\t\t},\n\t\t{\n\t\t\tName: \"Custom Base URL\",\n\t\t\tConfigKey: \"custom_base_url\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"OpenAI 兼容接口地址,如 https://api.example.com/v1\",\n\t\t},\n\t\t{\n\t\t\tName: \"告警分析提示词\",\n\t\t\tConfigKey: \"alarm_analysis_prompt\",\n\t\t\tConfigValue: \"你是专业运维分析师。请基于以下告警信息进行分析:\\n\\n设备名称:{{hostname}}\\nIP:{{host_ip}}\\n告警描述:{{message}}\\n告警详情:{{detail}}\\n告警级别:{{level}}\\n告警状态:{{status}}\\n\\n请输出:\\n1. 可能根因(按概率排序)\\n2. 排查步骤(关键命令/检查项)\\n3. 修复方案与风险\\n4. 防复发建议\",\n\t\t\tComment: \"用于AI助手告警分析的提示词模板,支持占位符:{{hostname}} {{host_ip}} {{message}} {{detail}} {{level}} {{status}} {{alarm_context}}\",\n\t\t},\n\t}\n}\n"}, "6a555a99b0280f3cd63b1281b32175f5525ca6b2": {"t": 1785754892.0397105, "n": 21822, "h": "3b76b2ce704e8a92", "c": "package model\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"zbxtable/pkg/logger\"\n\t\"zbxtable/pkg/utils\"\n\n\tzabbix \"github.com/canghai908/zabbix-go\"\n\t_ \"github.com/go-sql-driver/mysql\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t_ \"github.com/lib/pq\"\n\tworkwx \"github.com/xen0n/go-workwx\"\n\tini \"gopkg.in/ini.v1\"\n\t\"gorm.io/gorm\"\n)\n\nvar (\n\tAPI = &zabbix.API{}\n\tjson = jsoniter.Config{\n\t\tEscapeHTML: false,\n\t\tSortMapKeys: true,\n\t\tValidateJsonRawMessage: true,\n\t}.Froze()\n\tZBX_VER string\n\tZBX_V bool\n\tVersion string\n\tGitHash string\n\tBuildTime string\n\tWeApp *workwx.WorkwxApp // 不初始化,默认为 nil\n\n\tenvConfig map[string]string\n\tenvConfigOnce sync.Once\n\tenvFileExists bool\n)\n\nconst (\n\t//ms-agent 常量定义\n\tMSName = \"ms-agent\"\n\tMSUser = \"ms-agent\"\n\tMSGroup = \"MS-Agent\"\n\tMSMedia = \"MS-Agent\"\n\tMSAction = \"MS-Agent\"\n\t//webhook 常量定义\n\tWebhookName = \"ZbxTable\"\n\tWebhookAction = \"ZbxTable Webhook\"\n\tWebhookGroup = \"ZbxTable Webhook\"\n\tWebhookUser = \"zbxtable-webhook\"\n\t//down\n\tDownloadPath = \"./download/\"\n\tTplPath = \"./assets/templates/\"\n\t// 默认用户头像 (空字符串,由前端处理默认头像)\n\tDefaultAvatar = \"\"\n\t//默认更新服务器\n\tUpdateURL = \"http://dl.cactifans.com/stable/\"\n\n\t// 上传目录\n\tUploadDir = \"./upload\"\n\t// 背景图片上传目录\n\tBackgroundImageDir = \"./upload/background\"\n\t// 允许的图片格式\n\tAllowedImageExts = \".jpg,.jpeg,.png,.gif,.bmp,.webp,.svg\"\n\t// 最大文件大小 (10MB)\n\tMaxFileSize = 10 * 1024 * 1024\n)\n\n// TableName 表名前缀\nfunc TableName(str string) string {\n\treturn fmt.Sprintf(\"%s%s\", \"zbxtable_\", str)\n}\n\n// 接收数据库信息初始化\nfunc ModelInit(dbtype, dbhost, dbuser, dbpass, dbname, dbport string) error {\n\t// 直接使用 GORM\n\trunmode := GetConfKey(\"runmode\")\n\terr := InitGormDB(dbtype, dbhost, dbuser, dbpass, dbname, dbport, runmode)\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to connect database: \", err)\n\t\treturn err\n\t}\n\tlogger.Log.Info(\"Database connected!\")\n\t// 自动迁移表\n\terr = AutoMigrate()\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to auto migrate: \", err)\n\t\treturn err\n\t}\n\t// 基础数据初始化\n\tif err := DatabaseInit(); err != nil {\n\t\tlogger.Log.Error(\"Database init failed: \", err)\n\t\treturn err\n\t}\n\t// 使用go-cache替代Redis(不再需要Redis连接)\n\tif err := InitCache(); err != nil {\n\t\tlogger.Log.Error(\"Cache init failed: \", err)\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc InitWechat() {\n\t// 首先检查企业微信开关是否启用\n\twechatEnabled := GetConfigValueByKey(\"wechat_enabled\", \"0\")\n\tif wechatEnabled != \"1\" {\n\t\tlogger.Log.Info(\"WeChat is disabled in system config, skipping WeChat initialization\")\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\t// 开关已启用,从数据库读取企业微信配置\n\tagentIDStr := GetConfigValueByKey(\"wechat_agentid\", \"\")\n\tcorpid := GetConfigValueByKey(\"wechat_corpid\", \"\")\n\tsecret := GetConfigValueByKey(\"wechat_secret\", \"\") // 这里会自动解密\n\n\t// 验证必填配置项\n\tif agentIDStr == \"\" || corpid == \"\" || secret == \"\" {\n\t\tlogger.Log.Info(\"WeChat is enabled but configuration is incomplete (agentid/corpid/secret), WeChat app will not be initialized\")\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\t// 解析 AgentID\n\tAgentId, err := strconv.ParseInt(agentIDStr, 10, 64)\n\tif err != nil {\n\t\tlogger.Log.Error(\"wechat_agentid parse error:\", err)\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\n\t// 初始化企业微信客户端\n\tclient := workwx.New(corpid)\n\tWeApp = client.WithApp(secret, AgentId)\n\tWeApp.SpawnAccessTokenRefresher()\n\tlogger.Log.Info(\"WeChat inited successfully!\")\n}\n\n// DatabaseInit 数据初始化\nfunc DatabaseInit() error {\n\t//数据初始化操作 - 使用 GORM\n\tvar v User\n\terr := DB.Where(\"username = ?\", \"admin\").First(&v).Error\n\t//检查权限\n\tif err == nil && v.Operation == \"\" {\n\t\terr := DB.Model(&User{}).Where(\"id = ?\", v.ID).Update(\"operation\", \"['add', 'edit', 'delete','update']\").Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn fmt.Errorf(\"update admin operation failed: %w\", err)\n\t\t}\n\t\tlogger.Log.Info(\"update admin operation successfully\")\n\t}\n\t//添加管理员账号\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\tlogger.Log.Info(\"the admin user does not exist, create a new admin account later!\")\n\n\t\t// 构建默认主题配置\n\t\tdefaultThemeConfig := map[string]interface{}{\n\t\t\t\"theme\": map[string]string{\n\t\t\t\t\"color\": \"#722ed1\",\n\t\t\t\t\"mode\": \"dark\",\n\t\t\t\t\"success\": \"#52c41a\",\n\t\t\t\t\"warning\": \"#faad14\",\n\t\t\t\t\"error\": \"#f5222f\",\n\t\t\t},\n\t\t\t\"animate\": map[string]interface{}{\n\t\t\t\t\"disabled\": true,\n\t\t\t\t\"name\": \"lightSpeed\",\n\t\t\t\t\"direction\": \"left\",\n\t\t\t},\n\t\t}\n\n\t\t// 将配置转换为 JSON 字符串\n\t\tthemeJSON, err := json.Marshal(defaultThemeConfig)\n\t\tif err != nil {\n\t\t\tlogger.Log.Error(\"Failed to marshal default theme config:\", err)\n\t\t\tthemeJSON = []byte(`{\"theme\":{\"color\":\"#722ed1\",\"mode\":\"dark\",\"success\":\"#52c41a\",\"warning\":\"#faad14\",\"error\":\"#f5222f\"},\"animate\":{\"disabled\":true,\"name\":\"lightSpeed\",\"direction\":\"left\"}}`)\n\t\t}\n\n\t\t// 使用结构体插入管理员账号\n\t\tpassword, _ := utils.PasswordHash(\"Zbxtable\")\n\t\tuser := User{\n\t\t\tUsername: \"admin\",\n\t\t\tPassword: password,\n\t\t\tAvatar: DefaultAvatar,\n\t\t\tRole: \"admin\",\n\t\t\tOperation: \"['add', 'edit', 'delete','update']\",\n\t\t\tTheme: string(themeJSON),\n\t\t\tStatus: 0,\n\t\t}\n\t\terr = DB.Create(&user).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn fmt.Errorf(\"create admin account failed: %w\", err)\n\t\t}\n\t\tlogger.Log.Info(\"create an administrator account successfully, the admin ID is:\", user.ID)\n\t}\n\t//初始化系统数据\n\tif err = EnsureSystemBindingsForAssetTypes(); err != nil {\n\t\tlogger.Log.Info(\"Init system info error!\", err)\n\t\treturn fmt.Errorf(\"init system data failed: %w\", err)\n\t}\n\t//告警默认规则初始化 default rule\n\tvar rules []Rule\n\terr = DB.Where(\"m_type = ?\", \"2\").Find(&rules).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn fmt.Errorf(\"query default rules failed: %w\", err)\n\t}\n\tif len(rules) == 0 {\n\t\t// 使用 \"*\" 作为全局默认规则,匹配所有租户\n\t\tdefaultRule := []Rule{\n\t\t\t{\n\t\t\t\tName: \"全局默认规则\",\n\t\t\t\tZIDs: \"*\",\n\t\t\t\tMType: \"2\",\n\t\t\t\tChannel: \"wechat_robot\",\n\t\t\t\tUserIds: \"1\",\n\t\t\t\tSweek: \"0,1,2,3,4,5,6\",\n\t\t\t\tStime: \"00:00\",\n\t\t\t\tEtime: \"23.59\",\n\t\t\t\tStatus: \"0\"},\n\t\t}\n\t\terr := DB.Create(&defaultRule).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init default rule error!\", err)\n\t\t\treturn fmt.Errorf(\"init default rule failed: %w\", err)\n\t\t}\n\t\tlogger.Log.Info(\"Init default rule successfully!\")\n\t}\n\t// 初始化加密密钥(如果不存在)\n\tvar encryptionKeyConfig Config\n\terr = DB.Where(\"config_key = ?\", \"encryption_key\").First(&encryptionKeyConfig).Error\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t// 生成随机加密密钥\n\t\trandomKey := utils.GenerateRandomKey()\n\t\tencryptionKeyConfig = Config{\n\t\t\tName: \"加密密钥\",\n\t\t\tConfigKey: \"encryption_key\",\n\t\t\tConfigValue: randomKey,\n\t\t\tComment: \"用于加密存储敏感信息(如Zabbix密码和Token)的密钥,系统自动生成,不可修改\",\n\t\t}\n\t\tif insertErr := DB.Create(&encryptionKeyConfig).Error; insertErr != nil {\n\t\t\tlogger.Log.Error(\"Init encryption_key error:\", insertErr)\n\t\t\treturn fmt.Errorf(\"init encryption_key failed: %w\", insertErr)\n\t\t} else {\n\t\t\tlogger.Log.Info(\"Init encryption_key successfully! Key length:\", len(randomKey))\n\t\t}\n\t}\n\n\t// 初始化内置映射规则\n\t//InitDefaultMappingRules()\n\n\t// 获取结构化的默认配置\n\tdefaultConfigs := getDefaultConfigs()\n\n\tfor _, cfgItem := range defaultConfigs {\n\t\tvar existing Config\n\t\terr = DB.Where(\"config_key = ?\", cfgItem.ConfigKey).First(&existing).Error\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\tif insertErr := DB.Create(&cfgItem).Error; insertErr != nil {\n\t\t\t\tlogger.Log.Infof(\"Init config key %s error: %v\", cfgItem.ConfigKey, insertErr)\n\t\t\t\treturn fmt.Errorf(\"init config key %s failed: %w\", cfgItem.ConfigKey, insertErr)\n\t\t\t}\n\t\t\tlogger.Log.Infof(\"Init config key %s successfully!\", cfgItem.ConfigKey)\n\t\t} else if err != nil {\n\t\t\tlogger.Log.Infof(\"Init config key %s error: %v\", cfgItem.ConfigKey, err)\n\t\t\treturn fmt.Errorf(\"check config key %s failed: %w\", cfgItem.ConfigKey, err)\n\t\t}\n\t}\n\t//默认菜单初始化\n\tif err := InitMenuData(); err != nil {\n\t\treturn fmt.Errorf(\"init menu data failed: %w\", err)\n\t}\n\t//检查并添加缺失的菜单项(用于版本升级)\n\tif err := CheckAndAddMenus(); err != nil {\n\t\treturn fmt.Errorf(\"check and add menus failed: %w\", err)\n\t}\n\n\t// 存量 asset_type 记录回填 monitor_type(幂等,需在 InitDefaultAssetTypes 之前)\n\t_ = MigrateAssetTypeMonitorType()\n\t// 初始化默认资产类型(幂等)\n\tif err := InitDefaultAssetTypes(); err != nil {\n\t\treturn fmt.Errorf(\"init default asset types failed: %w\", err)\n\t}\n\t// 统一存量默认资产类型名称(幂等)\n\tif err := MigrateAssetTypeNames(); err != nil {\n\t\treturn fmt.Errorf(\"migrate asset type names failed: %w\", err)\n\t}\n\t// 回填存量 System 记录的 type_code(幂等)\n\tif err := MigrateSystemTypeCodes(); err != nil {\n\t\treturn fmt.Errorf(\"migrate system type codes failed: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc GetConfKey(v string) string {\n\t// 优先从 .env 中读取(便于开发环境配置敏感信息且不提交到 git)\n\tenvConfigOnce.Do(func() {\n\t\tfile, err := os.Open(\".env\")\n\t\tif err != nil {\n\t\t\t// 没有 .env 文件时直接跳过,后面回退到 app.conf\n\t\t\tenvFileExists = false\n\t\t\treturn\n\t\t}\n\t\tdefer file.Close()\n\n\t\tenvFileExists = true\n\t\tenvConfig = make(map[string]string)\n\n\t\tscanner := bufio.NewScanner(file)\n\t\tfor scanner.Scan() {\n\t\t\tline := strings.TrimSpace(scanner.Text())\n\t\t\tif line == \"\" || strings.HasPrefix(line, \"#\") || strings.HasPrefix(line, \";\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tparts := strings.SplitN(line, \"=\", 2)\n\t\t\tif len(parts) != 2 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tkey := strings.TrimSpace(parts[0])\n\t\t\tval := strings.TrimSpace(parts[1])\n\t\t\t// 去掉可能的引号\n\t\t\tval = strings.Trim(val, `\"'`)\n\t\t\tif key != \"\" {\n\t\t\t\tenvConfig[key] = val // 即使 val 为空也认为是有效配置\n\t\t\t}\n\t\t}\n\t})\n\n\t// 如果存在 .env 文件,则所有配置完全由 .env 决定:\n\t// - key 存在:返回对应值(可以是空字符串)\n\t// - key 不存在:返回空字符串\n\tif envFileExists {\n\t\tif envConfig == nil {\n\t\t\treturn \"\"\n\t\t}\n\t\tif val, ok := envConfig[v]; ok {\n\t\t\treturn val\n\t\t}\n\t\treturn \"\"\n\t}\n\t// 其次从 config/app.conf 读取\n\tcfg, err := ini.Load(\"./config/app.conf\")\n\tif err != nil {\n\t\tlogger.Log.Error(err)\n\t\treturn \"\"\n\t}\n\tp, err := cfg.Section(\"\").GetKey(v)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn p.String()\n}\n\n// IsPasswordConfigured 检查是否配置了密码(用于图形查看)\n// 优先检查数据库中是否有激活的 Zabbix 实例配置了用户名和密码\n// 注意:查看图形需要用户名和密码,Token 方式无法用于 Web 登录\nfunc IsPasswordConfigured() bool {\n\t// 1. 优先检查数据库中是否有激活的 Zabbix 实例\n\tvar activeInstance ZabbixInstance\n\terr := DB.Where(\"is_active = ?\", true).First(&activeInstance).Error\n\tif err == nil {\n\t\t// 找到激活的实例,检查是否配置了用户名和密码(查看图形必须用密码,Token 无法用于 Web 登录)\n\t\tif strings.TrimSpace(activeInstance.User) != \"\" && strings.TrimSpace(activeInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 2. 如果数据库中没有激活实例,检查是否有任何可用的实例配置了密码\n\tvar anyInstance ZabbixInstance\n\terr = DB.Where(\"enabled = ?\", true).First(&anyInstance).Error\n\tif err == nil {\n\t\t// 找到可用的实例,检查是否配置了用户名和密码\n\t\tif strings.TrimSpace(anyInstance.User) != \"\" && strings.TrimSpace(anyInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 3. 回退到配置文件检查(兼容旧版本)\n\tpass := GetConfKey(\"zabbix_pass\")\n\tuser := GetConfKey(\"zabbix_user\")\n\treturn pass != \"\" && user != \"\"\n}\n\n// GetEncryptionKey 获取加密密钥\n// 优先从数据库读取,如果数据库中没有则从环境变量读取,最后使用默认密钥\nfunc GetEncryptionKey() string {\n\t// 1. 优先从数据库读取\n\tkey := GetConfigValueByKey(\"encryption_key\", \"\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 2. 从环境变量或配置文件读取\n\tkey = GetConfKey(\"encryption_key\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 3. 使用默认密钥(不应该到这里,因为 DatabaseInit 会生成)\n\tlogger.Log.Warn(\"使用默认加密密钥,建议在数据库中配置 encryption_key\")\n\treturn \"zbxtable-default-encryption-key-2024\"\n}\n\n// getDefaultConfigs 返回系统默认配置列表\n// 配置按功能模块分组,便于维护和管理\nfunc getDefaultConfigs() []Config {\n\tconfigs := make([]Config, 0)\n\n\t// 系统外观配置\n\tconfigs = append(configs, getSystemAppearanceConfigs()...)\n\n\t// 初始配置状态\n\tconfigs = append(configs, getInitialSetupConfigs()...)\n\n\t// Dashboard 相关配置\n\tconfigs = append(configs, getDashboardConfigs()...)\n\n\t// 计划任务配置\n\tconfigs = append(configs, getTaskConfigs()...)\n\n\t// 邮件配置\n\tconfigs = append(configs, getEmailConfigs()...)\n\n\t// 企业微信配置\n\tconfigs = append(configs, getWeChatConfigs()...)\n\n\t// AI 配置\n\tconfigs = append(configs, getAIConfigs()...)\n\n\t// 演示模式配置\n\tconfigs = append(configs, getDemoConfigs()...)\n\n\treturn configs\n}\n\n// getDemoConfigs 演示模式配置\nfunc getDemoConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"演示模式\",\n\t\t\tConfigKey: \"demo_mode\",\n\t\t\tConfigValue: \"false\",\n\t\t\tComment: \"是否开启演示模式(只读):true 开启, false 关闭\",\n\t\t},\n\t}\n}\n\n// getSystemAppearanceConfigs 系统外观配置\nfunc getSystemAppearanceConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"系统名称\",\n\t\t\tConfigKey: \"system_name\",\n\t\t\tConfigValue: \"ZbxTable\",\n\t\t\tComment: \"系统显示的名称\",\n\t\t},\n\t\t{\n\t\t\tName: \"系统Logo\",\n\t\t\tConfigKey: \"system_logo\",\n\t\t\tConfigValue: \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAYAAACtWK6eAAANoUlEQVR4nOzdS2xcVZ7H8f+513bZTmxfWzNgQAO2Bs0IFsRWZhazAEwizQ7wgBwYNJCY1axIyKJbvcHlXhCkXpD0upVHI9St0E0ctdS9SjBk0d1SQ1XSCFoCYdMLuluK4uvgR9lV9/xbt+wTV5v4uKpc595z7/19djx0z/Hj63NO+ZavQwCwIwQCoIFAADQQCIAGAgHQQCAAGggEQAOBAGggEAANBAKggUAANBAIgAYCAdBAIAAaCARAA4EAaCAQAA0EAqCBQAA0EAiABgIB0EAgABoIBEADgQBoIBAADQQCoIFAADQQCIAGAgHQQCAAGggEQAOBAGggEAANBAKggUAANBAIgAYCAdBAIAAaCARAA4EAaCAQAA0EAqCBQAA0EAiABgIB0EAgABptcU8ga0ZOLHiUo5Fb7fKAczs3/O1qe1+7K4dKZbf639scojY3oHaX5oWQX3fmqNjtrM8Xf9RfjHvuWSTinkAWjHx/YewvFRpfK7U/WVnOjayuORTI+j/1XTlJHW2SujrkbHeXnOnNrV0uvtU/b3TSUIVADAlXir+Wuo+XK+KEv+x6jQSxmzCWnu6g0NMlz8z/uOtCyy4M34FAWmzw/1eHyhUnv8Ty6NpSzvh4vd2V+e4cnx/0Vi5gVWk9BNIi4YrxZeBOlZY6TlRWOiIfP9fOc/cNrE9jRWktBNICPd+7Ob5+q/vc2redXtxz6e2uzN1ebT9EPxdYTVoAgexBuGp8sbB/arnknoh7LrU62iT3dlVO3vxJ5+m455J0CKRZL/JQm8OXKgGNxD2VnbS38dvld92Tcc8jyRBIM17kIWL+gIiG4p7Kbvq6g8Jw39Kh4ul+P+65JBECadRL6yMUtIVxxH7eqBtTgdbFIZoRiKRBuNWkEUmMg6o/Bkepg6/SOCdr3hbAClKvjW1VIXFx1MJK0jCsIPXYOnMkNw7CStIMBLKbBB3I6yJodF9f5Wr1pknYFQLRSVscm5ZL7uifbu5/P+55JAEC2UlK41BKZfcpeiE4G/c8bIdA7iblcWwRk4hED4Fsl5k4FESig0BqZS4ORUy2v4RI7gaBKJmNY0M5EJPdRyuIZBsEQohDWSk5x+gIT8U9D5sgEMRRS5DgKUSyJdu3moyzR7nq7SOI4x8xsZimi2I67onELbsrSBhHB1aOHWAl2ZTNQFQcwt43O1kAkWQyEMTRiMxHkq1AEEczMh1JtgJBHM3KbCTZCeQFPoc49iSTkWQjkDAO4mNxTyMFqpHsf6WcmUjS/3sQxGEC59oqJ9fe7Uj9391KdyCIwyQmFpN0UaT6T52md4uFOEwLt1vn6AgfjXsiJqUykN5rv5ly+pYQh3licGDt3MgPFlMbSeq2WAN//OUUMeflYjfdfudxkov74p5Sag32r9PgwDptbLd4sniqL3XbrVQFouJQ/4xIzKmJQ2FimlzP9Vz4LJ+eb6vUfCTb41AQSevdJQ5lgcrB4fV9XiEtkaTiDLJTHCGnb4V6X75GTt9y9BNLoX/uK+8UR6if2t0rHcv+6KN5jnZihiQ+EF0cCiJpjYGeMj3wT2u7/W+piiTRgdQTh4JI9iaM48F7do1DSU0kiQ2kkTgURNKcBuNQ7kQyluBIEnmSaiaOWji416/JOGotkAwOep3e3GwCD+6JW0G86+8f3UschJWkbvs7g73GQdWVxHGv+CV/OIkrSaICCeNwhDzfimshEr2unKTh+0qtutxwUiNJTCCtjEMJI+mZ+B2JXLmVl028MI6H718l12npN3MiI0lEICbiUNx7F6n35Y8QySZDcSiJi8T6QEzGoSCSDYbjUBIVidUvK3iFiyOO6xSiGi/4Wx/dfucJ4rX2qIa0RkRx1JojGRy2/dUta1eQzTg+iHLMrK4kHW2ShgcjjYOSspJYGUhNHJE/Ry+MZP/Eb6MeNjZhHA8/sEodbbF8k1ofiXWBxBmH0v7QTdr39B/iGj4yMceh3Ikkb2EkVm3+bIij1tqNB2n5V/8R9zSMsCSOWnO0Ehwc97yFvEVnEmtWEK9wacimOEK5x/6cypXEwjioupJ0O1dmfL/fppXEikA24pBWxaGkLRJL49gkRm2LJPZAtuJgax9DkJZI7I5DsSuSWANJQhxKGEnX45/HPY2mJSMOxZ5IYglkjD8I4xhOShxK1xOfJzKSZMWh3Ikk1m135IHkmalY9MM4riYpDiVpkSQzDiWMxH37v/KLsb2sFfnAXuFSv+PKT5IYR63Vjx6h1WuPxD0NLddh+vd/WUloHDWYTuc6el7//XT0nUS6ggzNXSLhBmeTHgclYCUJ43j4/qSuHNsIOr62fvtEHL9tjyyQ8Nxxe0nmBdF4VGOaZmskKo6unIx7Kq0iSIg3/JIf+Q/WyNasjUN58FVU40XJpu1WCuPYwjSb6+h5KsqtViQryMAXv6aNQ3k62bKSpDoOqv44fzLcakX50q/xQMKtlVxZOZ6Gc4eODZGkOo4N1a1WlC/9Gg+kWPSHHCFOmB7HBnFG8uA9pbTHofRTlxPZKmI0kDwzOW3yaNpXj1phJJ3/+WWkY4ZxDPRUIh0zVkK8FtUqYjSQ09dnPGLK3INsuv/7BnU89nUkY2Uujg3VVSSKgYwG4jrl8SytHrX2P/2x8UgyGscGIV4bmzK/zTIWSHg4Z+EcN3X9JDAZSabj2OD5a/6Y6UGMBRIezonx4H4TkQz2r2c9Dqq+ouW4xp+NaHCLVUnNb8z3qhrJv33Tkmtpnu6URc+a3mYZC0S44llT106ifU9/TO69/p6ugTi+w/NLZrdZRgJ59NNPSRAZ3x8miegsU+//XWs6EsSxA8cxuo03Esg35S8yf/a4m2YjQRw7EiTEAZMDGAnEdYNMvrRbj0YjQRy7etLkxY0EwsxYQTTqjQRx1MUz+T4RQ4f0bP5ysBEqkp0e4IM46uaZfJ+ImRVEOA+ZuG7aVCO5y1OuEEeDDL7SG/vfxcq67Y+CQxxNEJSsFQQaoyIZHFxeQBx2QSB2YKd3+fxgV+UgMc/FPRnYgkDix8Tywq0DE5NepzdHLA8jEnuYCmRv91Rkx2YcRybDf5jNC0IkTZk3dWEjgQiWiyaumzbMXFBxKIikCSVzP5CNBCKlKJq4bpow8ycslw/f7b8hkob4xdP9yQrEdYWxJS8NVBz+6LEdv7CIpE5MRn8YGwkkCMpYQXZQTxwKIqkD83WTlzcSiD/6v/PMOKhv10gcCiLRYnLFrMkBzL1hiuRlU9dOImb+qtE4lDCS9TASIZ8nogUzM0yoIEjeCkLVPzpBRstOEmaeY1lpKg7lszCSDq9AFBxGJJuYi8W3+o2ed40FEgTtM9hm3YnjULjt3Ou1EMk2TB+aHsJYIP7o//gkzO4PbdfKOBREcgcTyTOmBzF6qwkH5j8AW5mIQ0EkG49CML29ItOB+KMTs0zZO4uYjEMJI/HavQLJ4Dmz74iwUvjxno9iIOM3K3LA06bHsEkUcSiz04K8nDdLMng1U5EwzxdP9f40iqGMB5KlVSTKOJTNSM5nKBImEvmoBovkdvcsrCJxxKFkKhLmuahWD4oqkHAVIabTUYwVhzjjUDISCVfvKIhQZG+YktKd5hTeKmFDHMq2SNKGiXg6ileuakX6ZHav8N6Y44qrUY9rCjPdYlk+aEMctUbyC0RrTp6EmIp7Li3D/FXxVN+/Rj1spG+5DbdakvlklGOashGHPGxbHKFivp8oJ/PEKTn7hTuPiLdWSuTvSfcPTJxO+hduK44j1t7Wn6JIwnPHZNRbKyWWP9pw68BEnlhG8oueVktCHEoKImFifr34Vr/xe652EttfNam+FzthkSQpDiXBkWwcyk/1xXq7UuyH5YHr7yXiMJnEOGrVHNzfsOHrvgsmosnim70X4p6IFZ+ozUis/cJtvNmpYuWBvBEHp5iCdX+chHu2+ihlK/EtkvK5OLdVtaz5hvQK740Jh84KIYbjnkuN8CfZjAyWXt3Lm51sMpZn8kv+MAnnCln3ueYCSfl8XAfyu7EmEKpG8rMhx2mfIkFHY54bM5MviKdvHZhI5S37dm25eIGIzhTf7LPunGRVIIp34+IxweINIcRQDHNkYjoj5dJ0WlaNnWyuJkMk3HMkqk9qiuNzPUscvGrTqlHLykCUCEPZvHdJnJfB+g+TftZo1NgUk7/mHyPhHo0oFBXGtC1njZ1YHYjiFX4xLlx6RRCpZ6+3at5qK3VGyuUzaV8xdrMZyhgJ5xgJeoZIeK38XBOxT5IuEMkZ28NQEhGIEp5RyHXHBDnPEPPI5spCdX4cd+5wZaaiIPpQSp7xRycS8YWK2p1XvMgJfyg9QVufa6rj8711NzHzPDFdJpKXqURFk38m1IREBbKdV7g4QuQMOQ6PEIk+FvRQ+K9r/hdfkPCJeVEIeT0IqEi0Mp/1laJR+TzTjO97lKMRcp0DJGmIHNFHvO3JToLmSfIiOTRHUt5IYhAA0AA8QAdAA4EAaCAQAA0EAqCBQAA0EAiABgIB0EAgABoIBEADgQBoIBAADQQCoIFAADQQCIAGAgHQQCAAGggEQAOBAGggEAANBAKggUAANBAIgAYCAdBAIAAaCARAA4EAaCAQAA0EAqCBQAA0EAiABgIB0EAgABoIBEADgQBoIBAADQQCoIFAADQQCIAGAgHQQCAAGn8PAAD//9S/ZEj2odDoAAAAAElFTkSuQmCC\",\n\t\t\tComment: \"系统Logo的base64编码数据\",\n\t\t},\n\t}\n}\n\n// getInitialSetupConfigs 初始配置状态\nfunc getInitialSetupConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"初始配置完成\",\n\t\t\tConfigKey: \"initial_setup_completed\",\n\t\t\tConfigValue: \"0\",\n\t\t\tComment: \"标记系统是否完成初始配置:1 已完成,0 未完成\",\n\t\t},\n\t}\n}\n\n// getDashboardConfigs Dashboard 相关配置\nfunc getDashboardConfigs() []Config {\n\treturn []Config{\n\t\t// {\n\t\t// \tName: \"数据面板\",\n\t\t// \tConfigKey: \"zbx_dash\",\n\t\t// \tConfigValue: \"0\",\n\t\t// \tComment: \"是否开启Zabbix看板:1 开启,0 关闭\",\n\t\t// },\n\t\t// {\n\t\t// \tName: \"面板配置\",\n\t\t// \tConfigKey: \"dash_id\",\n\t\t// \tConfigValue: \"1\",\n\t\t// \tComment: \"需要引入的Zabbix面板的ID,默认为1\",\n\t\t// },\n\t\t{\n\t\t\tName: \"Linux Top数量\",\n\t\t\tConfigKey: \"dash_top_lin_num\",\n\t\t\tConfigValue: \"10\",\n\t\t\tComment: \"首页 Linux Top 列表展示数量\",\n\t\t},\n\t\t{\n\t\t\tName: \"Windows Top数量\",\n\t\t\tConfigKey: \"dash_top_win_num\",\n\t\t\tConfigValue: \"10\",\n\t\t\tComment: \"首页 Windows Top 列表展示数量\",\n\t\t},\n\t\t{\n\t\t\tName: \"Webhook回调地址\",\n\t\t\tConfigKey: \"webhook_url\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"webhook通知地址\",\n\t\t},\n\t}\n}\n\n// getTaskConfigs 计划任务配置\nfunc getTaskConfigs() []Config {\n\tconfigs := make([]Config, 0, len(getScheduledTaskDefinitions())*2)\n\n\tfor _, task := range getScheduledTaskDefinitions() {\n\t\tconfigs = append(configs,\n\t\t\tConfig{\n\t\t\t\tName: task.Name + \"开关\",\n\t\t\t\tConfigKey: task.EnabledKey,\n\t\t\t\tConfigValue: task.DefaultEnabled,\n\t\t\t\tComment: task.Name + \"计划任务是否启用:1 启用,0 停用\",\n\t\t\t},\n\t\t\tConfig{\n\t\t\t\tName: task.Name + \"Cron\",\n\t\t\t\tConfigKey: task.CronKey,\n\t\t\t\tConfigValue: task.DefaultCron,\n\t\t\t\tComment: task.Name + \"执行周期,格式:秒 分 时 日 月 周\",\n\t\t\t},\n\t\t)\n\t}\n\n\treturn configs\n}\n\n// getEmailConfigs 邮件配置\nfunc getEmailConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"邮件发件人\",\n\t\t\tConfigKey: \"email_from\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"告警邮件发件人邮箱地址\",\n\t\t},\n\t\t{\n\t\t\tName: \"邮件昵称\",\n\t\t\tConfigKey: \"email_nickname\",\n\t\t\tConfigValue: \"ZbxTable\",\n\t\t\tComment: \"告警邮件显示的发件人昵称\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 密码/授权码\",\n\t\t\tConfigKey: \"email_secret\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"SMTP 登录密码或授权码\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 服务器\",\n\t\t\tConfigKey: \"email_host\",\n\t\t\tConfigValue: \"smtp.qq.com\",\n\t\t\tComment: \"SMTP 服务器地址\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 端口\",\n\t\t\tConfigKey: \"email_port\",\n\t\t\tConfigValue: \"465\",\n\t\t\tComment: \"SMTP 端口号\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 使用 SSL\",\n\t\t\tConfigKey: \"email_isSSl\",\n\t\t\tConfigValue: \"true\",\n\t\t\tComment: \"是否启用 SSL:true/false\",\n\t\t},\n\t}\n}\n\n// getWeChatConfigs 企业微信配置\nfunc getWeChatConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"企业微信开关\",\n\t\t\tConfigKey: \"wechat_enabled\",\n\t\t\tConfigValue: \"0\",\n\t\t\tComment: \"是否启用企业微信:1 启用,0 禁用\",\n\t\t},\n\t\t{\n\t\t\tName: \"企业微信 AgentID\",\n\t\t\tConfigKey: \"wechat_agentid\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"企业微信应用的 AgentID\",\n\t\t},\n\t\t{\n\t\t\tName: \"企业微信 CorpID\",\n\t\t\tConfigKey: \"wechat_corpid\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"企业微信企业ID\",\n\t\t},\n\t\t{\n\t\t\tName: \"企业微信 Secret\",\n\t\t\tConfigKey: \"wechat_secret\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"企业微信应用的 Secret\",\n\t\t},\n\t}\n}\n\n// getAIConfigs AI 配置\nfunc getAIConfigs() []Config {\n\treturn []Config{\n\t\t// AI 类型选择\n\t\t{\n\t\t\tName: \"AI 类型\",\n\t\t\tConfigKey: \"ai_type\",\n\t\t\tConfigValue: \"ollama\",\n\t\t\tComment: \"选择使用的 AI 服务类型:ollama、deepseek 或 custom\",\n\t\t},\n\t\t// Ollama 配置\n\t\t{\n\t\t\tName: \"Ollama Host\",\n\t\t\tConfigKey: \"ollama_host\",\n\t\t\tConfigValue: \"http://localhost:11434\",\n\t\t\tComment: \"Ollama 服务地址,如 http://127.0.0.1:11434\",\n\t\t},\n\t\t{\n\t\t\tName: \"Ollama Model\",\n\t\t\tConfigKey: \"ollama_model\",\n\t\t\tConfigValue: \"deepseek-r1:32b\",\n\t\t\tComment: \"默认使用的大模型名称\",\n\t\t},\n\t\t// Deepseek 配置\n\t\t{\n\t\t\tName: \"Deepseek API Key\",\n\t\t\tConfigKey: \"deepseek_api_key\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"Deepseek API 密钥\",\n\t\t},\n\t\t{\n\t\t\tName: \"Deepseek Model\",\n\t\t\tConfigKey: \"deepseek_model\",\n\t\t\tConfigValue: \"deepseek-chat\",\n\t\t\tComment: \"Deepseek 模型名称,如 deepseek-chat\",\n\t\t},\n\t\t{\n\t\t\tName: \"Deepseek Base URL\",\n\t\t\tConfigKey: \"deepseek_base_url\",\n\t\t\tConfigValue: \"https://api.deepseek.com\",\n\t\t\tComment: \"Deepseek API 地址,默认为 https://api.deepseek.com\",\n\t\t},\n\t\t// Custom(OpenAI-compatible) 配置\n\t\t{\n\t\t\tName: \"Custom API Key\",\n\t\t\tConfigKey: \"custom_api_key\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"OpenAI 兼容接口的 API Key\",\n\t\t},\n\t\t{\n\t\t\tName: \"Custom Model\",\n\t\t\tConfigKey: \"custom_model\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"OpenAI 兼容接口的模型名称\",\n\t\t},\n\t\t{\n\t\t\tName: \"Custom Base URL\",\n\t\t\tConfigKey: \"custom_base_url\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"OpenAI 兼容接口地址,如 https://api.example.com/v1\",\n\t\t},\n\t\t{\n\t\t\tName: \"告警分析提示词\",\n\t\t\tConfigKey: \"alarm_analysis_prompt\",\n\t\t\tConfigValue: \"你是专业运维分析师。请基于以下告警信息进行分析:\\n\\n设备名称:{{hostname}}\\nIP:{{host_ip}}\\n告警描述:{{message}}\\n告警详情:{{detail}}\\n告警级别:{{level}}\\n告警状态:{{status}}\\n\\n请输出:\\n1. 可能根因(按概率排序)\\n2. 排查步骤(关键命令/检查项)\\n3. 修复方案与风险\\n4. 防复发建议\",\n\t\t\tComment: \"用于AI助手告警分析的提示词模板,支持占位符:{{hostname}} {{host_ip}} {{message}} {{detail}} {{level}} {{status}} {{alarm_context}}\",\n\t\t},\n\t}\n}\n"}, "da91f1a55b83b5d0bfe3e4cb33dec689c3a35d5f": {"t": 1785754893.5209434, "n": 21354, "h": "1c253c39a4a12274", "c": "package model\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"zbxtable/pkg/logger\"\n\t\"zbxtable/pkg/utils\"\n\n\tzabbix \"github.com/canghai908/zabbix-go\"\n\t_ \"github.com/go-sql-driver/mysql\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t_ \"github.com/lib/pq\"\n\tworkwx \"github.com/xen0n/go-workwx\"\n\tini \"gopkg.in/ini.v1\"\n\t\"gorm.io/gorm\"\n)\n\nvar (\n\tAPI = &zabbix.API{}\n\tjson = jsoniter.Config{\n\t\tEscapeHTML: false,\n\t\tSortMapKeys: true,\n\t\tValidateJsonRawMessage: true,\n\t}.Froze()\n\tZBX_VER string\n\tZBX_V bool\n\tVersion string\n\tGitHash string\n\tBuildTime string\n\tWeApp *workwx.WorkwxApp // 不初始化,默认为 nil\n\n\tenvConfig map[string]string\n\tenvConfigOnce sync.Once\n\tenvFileExists bool\n)\n\nconst (\n\t//ms-agent 常量定义\n\tMSName = \"ms-agent\"\n\tMSUser = \"ms-agent\"\n\tMSGroup = \"MS-Agent\"\n\tMSMedia = \"MS-Agent\"\n\tMSAction = \"MS-Agent\"\n\t//webhook 常量定义\n\tWebhookName = \"ZbxTable\"\n\tWebhookAction = \"ZbxTable Webhook\"\n\tWebhookGroup = \"ZbxTable Webhook\"\n\tWebhookUser = \"zbxtable-webhook\"\n\t//down\n\tDownloadPath = \"./download/\"\n\tTplPath = \"./assets/templates/\"\n\t// 默认用户头像 (空字符串,由前端处理默认头像)\n\tDefaultAvatar = \"\"\n\t//默认更新服务器\n\tUpdateURL = \"http://dl.cactifans.com/stable/\"\n\n\t// 上传目录\n\tUploadDir = \"./upload\"\n\t// 背景图片上传目录\n\tBackgroundImageDir = \"./upload/background\"\n\t// 允许的图片格式\n\tAllowedImageExts = \".jpg,.jpeg,.png,.gif,.bmp,.webp,.svg\"\n\t// 最大文件大小 (10MB)\n\tMaxFileSize = 10 * 1024 * 1024\n)\n\n// TableName 表名前缀\nfunc TableName(str string) string {\n\treturn fmt.Sprintf(\"%s%s\", \"zbxtable_\", str)\n}\n\n// 接收数据库信息初始化\nfunc ModelInit(dbtype, dbhost, dbuser, dbpass, dbname, dbport string) error {\n\t// 直接使用 GORM\n\trunmode := GetConfKey(\"runmode\")\n\terr := InitGormDB(dbtype, dbhost, dbuser, dbpass, dbname, dbport, runmode)\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to connect database: \", err)\n\t\treturn err\n\t}\n\tlogger.Log.Info(\"Database connected!\")\n\t// 自动迁移表\n\terr = AutoMigrate()\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to auto migrate: \", err)\n\t\treturn err\n\t}\n\t// 基础数据初始化\n\tif err := DatabaseInit(); err != nil {\n\t\tlogger.Log.Error(\"Database init failed: \", err)\n\t\treturn err\n\t}\n\t// 使用go-cache替代Redis(不再需要Redis连接)\n\tif err := InitCache(); err != nil {\n\t\tlogger.Log.Error(\"Cache init failed: \", err)\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc InitWechat() {\n\t// 首先检查企业微信开关是否启用\n\twechatEnabled := GetConfigValueByKey(\"wechat_enabled\", \"0\")\n\tif wechatEnabled != \"1\" {\n\t\tlogger.Log.Info(\"WeChat is disabled in system config, skipping WeChat initialization\")\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\t// 开关已启用,从数据库读取企业微信配置\n\tagentIDStr := GetConfigValueByKey(\"wechat_agentid\", \"\")\n\tcorpid := GetConfigValueByKey(\"wechat_corpid\", \"\")\n\tsecret := GetConfigValueByKey(\"wechat_secret\", \"\") // 这里会自动解密\n\n\t// 验证必填配置项\n\tif agentIDStr == \"\" || corpid == \"\" || secret == \"\" {\n\t\tlogger.Log.Info(\"WeChat is enabled but configuration is incomplete (agentid/corpid/secret), WeChat app will not be initialized\")\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\t// 解析 AgentID\n\tAgentId, err := strconv.ParseInt(agentIDStr, 10, 64)\n\tif err != nil {\n\t\tlogger.Log.Error(\"wechat_agentid parse error:\", err)\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\n\t// 初始化企业微信客户端\n\tclient := workwx.New(corpid)\n\tWeApp = client.WithApp(secret, AgentId)\n\tWeApp.SpawnAccessTokenRefresher()\n\tlogger.Log.Info(\"WeChat inited successfully!\")\n}\n\n// DatabaseInit 数据初始化\nfunc DatabaseInit() error {\n\t//数据初始化操作 - 使用 GORM\n\tvar v User\n\terr := DB.Where(\"username = ?\", \"admin\").First(&v).Error\n\t//检查权限\n\tif err == nil && v.Operation == \"\" {\n\t\terr := DB.Model(&User{}).Where(\"id = ?\", v.ID).Update(\"operation\", \"['add', 'edit', 'delete','update']\").Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn fmt.Errorf(\"update admin operation failed: %w\", err)\n\t\t}\n\t\tlogger.Log.Info(\"update admin operation successfully\")\n\t}\n\t//添加管理员账号\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\tlogger.Log.Info(\"the admin user does not exist, create a new admin account later!\")\n\n\t\t// 构建默认主题配置\n\t\tdefaultThemeConfig := map[string]interface{}{\n\t\t\t\"theme\": map[string]string{\n\t\t\t\t\"color\": \"#722ed1\",\n\t\t\t\t\"mode\": \"dark\",\n\t\t\t\t\"success\": \"#52c41a\",\n\t\t\t\t\"warning\": \"#faad14\",\n\t\t\t\t\"error\": \"#f5222f\",\n\t\t\t},\n\t\t\t\"animate\": map[string]interface{}{\n\t\t\t\t\"disabled\": true,\n\t\t\t\t\"name\": \"lightSpeed\",\n\t\t\t\t\"direction\": \"left\",\n\t\t\t},\n\t\t}\n\n\t\t// 将配置转换为 JSON 字符串\n\t\tthemeJSON, err := json.Marshal(defaultThemeConfig)\n\t\tif err != nil {\n\t\t\tlogger.Log.Error(\"Failed to marshal default theme config:\", err)\n\t\t\tthemeJSON = []byte(`{\"theme\":{\"color\":\"#722ed1\",\"mode\":\"dark\",\"success\":\"#52c41a\",\"warning\":\"#faad14\",\"error\":\"#f5222f\"},\"animate\":{\"disabled\":true,\"name\":\"lightSpeed\",\"direction\":\"left\"}}`)\n\t\t}\n\n\t\t// 使用结构体插入管理员账号\n\t\tpassword, _ := utils.PasswordHash(\"Zbxtable\")\n\t\tuser := User{\n\t\t\tUsername: \"admin\",\n\t\t\tPassword: password,\n\t\t\tAvatar: DefaultAvatar,\n\t\t\tRole: \"admin\",\n\t\t\tOperation: \"['add', 'edit', 'delete','update']\",\n\t\t\tTheme: string(themeJSON),\n\t\t\tStatus: 0,\n\t\t}\n\t\terr = DB.Create(&user).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn fmt.Errorf(\"create admin account failed: %w\", err)\n\t\t}\n\t\tlogger.Log.Info(\"create an administrator account successfully, the admin ID is:\", user.ID)\n\t}\n\t//初始化系统数据\n\tif err = EnsureSystemBindingsForAssetTypes(); err != nil {\n\t\tlogger.Log.Info(\"Init system info error!\", err)\n\t\treturn fmt.Errorf(\"init system data failed: %w\", err)\n\t}\n\t//告警默认规则初始化 default rule\n\tvar rules []Rule\n\terr = DB.Where(\"m_type = ?\", \"2\").Find(&rules).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn fmt.Errorf(\"query default rules failed: %w\", err)\n\t}\n\tif len(rules) == 0 {\n\t\t// 使用 \"*\" 作为全局默认规则,匹配所有租户\n\t\tdefaultRule := []Rule{\n\t\t\t{\n\t\t\t\tName: \"全局默认规则\",\n\t\t\t\tZIDs: \"*\",\n\t\t\t\tMType: \"2\",\n\t\t\t\tChannel: \"wechat_robot\",\n\t\t\t\tUserIds: \"1\",\n\t\t\t\tSweek: \"0,1,2,3,4,5,6\",\n\t\t\t\tStime: \"00:00\",\n\t\t\t\tEtime: \"23.59\",\n\t\t\t\tStatus: \"0\"},\n\t\t}\n\t\terr := DB.Create(&defaultRule).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init default rule error!\", err)\n\t\t\treturn fmt.Errorf(\"init default rule failed: %w\", err)\n\t\t}\n\t\tlogger.Log.Info(\"Init default rule successfully!\")\n\t}\n\t// 初始化加密密钥(如果不存在)\n\tvar encryptionKeyConfig Config\n\terr = DB.Where(\"config_key = ?\", \"encryption_key\").First(&encryptionKeyConfig).Error\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t// 生成随机加密密钥\n\t\trandomKey := utils.GenerateRandomKey()\n\t\tencryptionKeyConfig = Config{\n\t\t\tName: \"加密密钥\",\n\t\t\tConfigKey: \"encryption_key\",\n\t\t\tConfigValue: randomKey,\n\t\t\tComment: \"用于加密存储敏感信息(如Zabbix密码和Token)的密钥,系统自动生成,不可修改\",\n\t\t}\n\t\tif insertErr := DB.Create(&encryptionKeyConfig).Error; insertErr != nil {\n\t\t\tlogger.Log.Error(\"Init encryption_key error:\", insertErr)\n\t\t\treturn fmt.Errorf(\"init encryption_key failed: %w\", insertErr)\n\t\t} else {\n\t\t\tlogger.Log.Info(\"Init encryption_key successfully! Key length:\", len(randomKey))\n\t\t}\n\t}\n\n\t// 初始化内置映射规则\n\t//InitDefaultMappingRules()\n\n\t// 获取结构化的默认配置\n\tdefaultConfigs := getDefaultConfigs()\n\n\tfor _, cfgItem := range defaultConfigs {\n\t\tvar existing Config\n\t\terr = DB.Where(\"config_key = ?\", cfgItem.ConfigKey).First(&existing).Error\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\tif insertErr := DB.Create(&cfgItem).Error; insertErr != nil {\n\t\t\t\tlogger.Log.Infof(\"Init config key %s error: %v\", cfgItem.ConfigKey, insertErr)\n\t\t\t\treturn fmt.Errorf(\"init config key %s failed: %w\", cfgItem.ConfigKey, insertErr)\n\t\t\t}\n\t\t\tlogger.Log.Infof(\"Init config key %s successfully!\", cfgItem.ConfigKey)\n\t\t} else if err != nil {\n\t\t\tlogger.Log.Infof(\"Init config key %s error: %v\", cfgItem.ConfigKey, err)\n\t\t\treturn fmt.Errorf(\"check config key %s failed: %w\", cfgItem.ConfigKey, err)\n\t\t}\n\t}\n\t//默认菜单初始化\n\tif err := InitMenuData(); err != nil {\n\t\treturn fmt.Errorf(\"init menu data failed: %w\", err)\n\t}\n\t//检查并添加缺失的菜单项(用于版本升级)\n\tif err := CheckAndAddMenus(); err != nil {\n\t\treturn fmt.Errorf(\"check and add menus failed: %w\", err)\n\t}\n\n\t// 存量 asset_type 记录回填 monitor_type(幂等,需在 InitDefaultAssetTypes 之前)\n\t_ = MigrateAssetTypeMonitorType()\n\t// 初始化默认资产类型(幂等)\n\tif err := InitDefaultAssetTypes(); err != nil {\n\t\treturn fmt.Errorf(\"init default asset types failed: %w\", err)\n\t}\n\t// 统一存量默认资产类型名称(幂等)\n\tif err := MigrateAssetTypeNames(); err != nil {\n\t\treturn fmt.Errorf(\"migrate asset type names failed: %w\", err)\n\t}\n\t// 回填存量 System 记录的 type_code(幂等)\n\tif err := MigrateSystemTypeCodes(); err != nil {\n\t\treturn fmt.Errorf(\"migrate system type codes failed: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc GetConfKey(v string) string {\n\t// 优先从 .env 中读取(便于开发环境配置敏感信息且不提交到 git)\n\tenvConfigOnce.Do(func() {\n\t\tfile, err := os.Open(\".env\")\n\t\tif err != nil {\n\t\t\t// 没有 .env 文件时直接跳过,后面回退到 app.conf\n\t\t\tenvFileExists = false\n\t\t\treturn\n\t\t}\n\t\tdefer file.Close()\n\n\t\tenvFileExists = true\n\t\tenvConfig = make(map[string]string)\n\n\t\tscanner := bufio.NewScanner(file)\n\t\tfor scanner.Scan() {\n\t\t\tline := strings.TrimSpace(scanner.Text())\n\t\t\tif line == \"\" || strings.HasPrefix(line, \"#\") || strings.HasPrefix(line, \";\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tparts := strings.SplitN(line, \"=\", 2)\n\t\t\tif len(parts) != 2 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tkey := strings.TrimSpace(parts[0])\n\t\t\tval := strings.TrimSpace(parts[1])\n\t\t\t// 去掉可能的引号\n\t\t\tval = strings.Trim(val, `\"'`)\n\t\t\tif key != \"\" {\n\t\t\t\tenvConfig[key] = val // 即使 val 为空也认为是有效配置\n\t\t\t}\n\t\t}\n\t})\n\n\t// 如果存在 .env 文件,则所有配置完全由 .env 决定:\n\t// - key 存在:返回对应值(可以是空字符串)\n\t// - key 不存在:返回空字符串\n\tif envFileExists {\n\t\tif envConfig == nil {\n\t\t\treturn \"\"\n\t\t}\n\t\tif val, ok := envConfig[v]; ok {\n\t\t\treturn val\n\t\t}\n\t\treturn \"\"\n\t}\n\t// 其次从 config/app.conf 读取\n\tcfg, err := ini.Load(\"./config/app.conf\")\n\tif err != nil {\n\t\tlogger.Log.Error(err)\n\t\treturn \"\"\n\t}\n\tp, err := cfg.Section(\"\").GetKey(v)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn p.String()\n}\n\n// IsPasswordConfigured 检查是否配置了密码(用于图形查看)\n// 优先检查数据库中是否有激活的 Zabbix 实例配置了用户名和密码\n// 注意:查看图形需要用户名和密码,Token 方式无法用于 Web 登录\nfunc IsPasswordConfigured() bool {\n\t// 1. 优先检查数据库中是否有激活的 Zabbix 实例\n\tvar activeInstance ZabbixInstance\n\terr := DB.Where(\"is_active = ?\", true).First(&activeInstance).Error\n\tif err == nil {\n\t\t// 找到激活的实例,检查是否配置了用户名和密码(查看图形必须用密码,Token 无法用于 Web 登录)\n\t\tif strings.TrimSpace(activeInstance.User) != \"\" && strings.TrimSpace(activeInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 2. 如果数据库中没有激活实例,检查是否有任何可用的实例配置了密码\n\tvar anyInstance ZabbixInstance\n\terr = DB.Where(\"enabled = ?\", true).First(&anyInstance).Error\n\tif err == nil {\n\t\t// 找到可用的实例,检查是否配置了用户名和密码\n\t\tif strings.TrimSpace(anyInstance.User) != \"\" && strings.TrimSpace(anyInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 3. 回退到配置文件检查(兼容旧版本)\n\tpass := GetConfKey(\"zabbix_pass\")\n\tuser := GetConfKey(\"zabbix_user\")\n\treturn pass != \"\" && user != \"\"\n}\n\n// GetEncryptionKey 获取加密密钥\n// 优先从数据库读取,如果数据库中没有则从环境变量读取,最后使用默认密钥\nfunc GetEncryptionKey() string {\n\t// 1. 优先从数据库读取\n\tkey := GetConfigValueByKey(\"encryption_key\", \"\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 2. 从环境变量或配置文件读取\n\tkey = GetConfKey(\"encryption_key\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 3. 使用默认密钥(不应该到这里,因为 DatabaseInit 会生成)\n\tlogger.Log.Warn(\"使用默认加密密钥,建议在数据库中配置 encryption_key\")\n\treturn \"zbxtable-default-encryption-key-2024\"\n}\n\n// getDefaultConfigs 返回系统默认配置列表\n// 配置按功能模块分组,便于维护和管理\nfunc getDefaultConfigs() []Config {\n\tconfigs := make([]Config, 0)\n\n\t// 系统外观配置\n\tconfigs = append(configs, getSystemAppearanceConfigs()...)\n\n\t// 初始配置状态\n\tconfigs = append(configs, getInitialSetupConfigs()...)\n\n\t// Dashboard 相关配置\n\tconfigs = append(configs, getDashboardConfigs()...)\n\n\t// 计划任务配置\n\tconfigs = append(configs, getTaskConfigs()...)\n\n\t// 邮件配置\n\tconfigs = append(configs, getEmailConfigs()...)\n\n\t// 企业微信配置\n\tconfigs = append(configs, getWeChatConfigs()...)\n\n\t// AI 配置\n\tconfigs = append(configs, getAIConfigs()...)\n\n\t// 演示模式配置\n\tconfigs = append(configs, getDemoConfigs()...)\n\n\treturn configs\n}\n\n// getDemoConfigs 演示模式配置\nfunc getDemoConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"演示模式\",\n\t\t\tConfigKey: \"demo_mode\",\n\t\t\tConfigValue: \"false\",\n\t\t\tComment: \"是否开启演示模式(只读):true 开启, false 关闭\",\n\t\t},\n\t}\n}\n\n// getSystemAppearanceConfigs 系统外观配置\nfunc getSystemAppearanceConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"系统名称\",\n\t\t\tConfigKey: \"system_name\",\n\t\t\tConfigValue: \"ZbxTable\",\n\t\t\tComment: \"系统显示的名称\",\n\t\t},\n\t\t{\n\t\t\tName: \"系统Logo\",\n\t\t\tConfigKey: \"system_logo\",\n\t\t\tConfigValue: \"data:image/png;base64,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\",\n\t\t\tComment: \"系统Logo的base64编码数据\",\n\t\t},\n\t}\n}\n\n// getInitialSetupConfigs 初始配置状态\nfunc getInitialSetupConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"初始配置完成\",\n\t\t\tConfigKey: \"initial_setup_completed\",\n\t\t\tConfigValue: \"0\",\n\t\t\tComment: \"标记系统是否完成初始配置:1 已完成,0 未完成\",\n\t\t},\n\t}\n}\n\n// getDashboardConfigs Dashboard 相关配置\nfunc getDashboardConfigs() []Config {\n\treturn []Config{\n\t\t// {\n\t\t// \tName: \"数据面板\",\n\t\t// \tConfigKey: \"zbx_dash\",\n\t\t// \tConfigValue: \"0\",\n\t\t// \tComment: \"是否开启Zabbix看板:1 开启,0 关闭\",\n\t\t// },\n\t\t// {\n\t\t// \tName: \"面板配置\",\n\t\t// \tConfigKey: \"dash_id\",\n\t\t// \tConfigValue: \"1\",\n\t\t// \tComment: \"需要引入的Zabbix面板的ID,默认为1\",\n\t\t// },\n\t\t{\n\t\t\tName: \"Linux Top数量\",\n\t\t\tConfigKey: \"dash_top_lin_num\",\n\t\t\tConfigValue: \"10\",\n\t\t\tComment: \"首页 Linux Top 列表展示数量\",\n\t\t},\n\t\t{\n\t\t\tName: \"Windows Top数量\",\n\t\t\tConfigKey: \"dash_top_win_num\",\n\t\t\tConfigValue: \"10\",\n\t\t\tComment: \"首页 Windows Top 列表展示数量\",\n\t\t},\n\t\t{\n\t\t\tName: \"Webhook回调地址\",\n\t\t\tConfigKey: \"webhook_url\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"webhook通知地址\",\n\t\t},\n\t}\n}\n\n// getTaskConfigs 计划任务配置\nfunc getTaskConfigs() []Config {\n\tconfigs := make([]Config, 0, len(getScheduledTaskDefinitions())*2)\n\n\tfor _, task := range getScheduledTaskDefinitions() {\n\t\tconfigs = append(configs,\n\t\t\tConfig{\n\t\t\t\tName: task.Name + \"开关\",\n\t\t\t\tConfigKey: task.EnabledKey,\n\t\t\t\tConfigValue: task.DefaultEnabled,\n\t\t\t\tComment: task.Name + \"计划任务是否启用:1 启用,0 停用\",\n\t\t\t},\n\t\t\tConfig{\n\t\t\t\tName: task.Name + \"Cron\",\n\t\t\t\tConfigKey: task.CronKey,\n\t\t\t\tConfigValue: task.DefaultCron,\n\t\t\t\tComment: task.Name + \"执行周期,格式:秒 分 时 日 月 周\",\n\t\t\t},\n\t\t)\n\t}\n\n\treturn configs\n}\n\n// getEmailConfigs 邮件配置\nfunc getEmailConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"邮件发件人\",\n\t\t\tConfigKey: \"email_from\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"告警邮件发件人邮箱地址\",\n\t\t},\n\t\t{\n\t\t\tName: \"邮件昵称\",\n\t\t\tConfigKey: \"email_nickname\",\n\t\t\tConfigValue: \"ZbxTable\",\n\t\t\tComment: \"告警邮件显示的发件人昵称\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 密码/授权码\",\n\t\t\tConfigKey: \"email_secret\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"SMTP 登录密码或授权码\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 服务器\",\n\t\t\tConfigKey: \"email_host\",\n\t\t\tConfigValue: \"smtp.qq.com\",\n\t\t\tComment: \"SMTP 服务器地址\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 端口\",\n\t\t\tConfigKey: \"email_port\",\n\t\t\tConfigValue: \"465\",\n\t\t\tComment: \"SMTP 端口号\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 使用 SSL\",\n\t\t\tConfigKey: \"email_isSSl\",\n\t\t\tConfigValue: \"true\",\n\t\t\tComment: \"是否启用 SSL:true/false\",\n\t\t},\n\t}\n}\n\n// getWeChatConfigs 企业微信配置\nfunc getWeChatConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"企业微信开关\",\n\t\t\tConfigKey: \"wechat_enabled\",\n\t\t\tConfigValue: \"0\",\n\t\t\tComment: \"是否启用企业微信:1 启用,0 禁用\",\n\t\t},\n\t\t{\n\t\t\tName: \"企业微信 AgentID\",\n\t\t\tConfigKey: \"wechat_agentid\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"企业微信应用的 AgentID\",\n\t\t},\n\t\t{\n\t\t\tName: \"企业微信 CorpID\",\n\t\t\tConfigKey: \"wechat_corpid\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"企业微信企业ID\",\n\t\t},\n\t\t{\n\t\t\tName: \"企业微信 Secret\",\n\t\t\tConfigKey: \"wechat_secret\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"企业微信应用的 Secret\",\n\t\t},\n\t}\n}\n\n// getAIConfigs AI 配置\nfunc getAIConfigs() []Config {\n\treturn []Config{\n\t\t// AI 类型选择\n\t\t{\n\t\t\tName: \"AI 类型\",\n\t\t\tConfigKey: \"ai_type\",\n\t\t\tConfigValue: \"ollama\",\n\t\t\tComment: \"选择使用的 AI 服务类型:ollama 或 deepseek\",\n\t\t},\n\t\t// Ollama 配置\n\t\t{\n\t\t\tName: \"Ollama Host\",\n\t\t\tConfigKey: \"ollama_host\",\n\t\t\tConfigValue: \"http://localhost:11434\",\n\t\t\tComment: \"Ollama 服务地址,如 http://127.0.0.1:11434\",\n\t\t},\n\t\t{\n\t\t\tName: \"Ollama Model\",\n\t\t\tConfigKey: \"ollama_model\",\n\t\t\tConfigValue: \"deepseek-r1:32b\",\n\t\t\tComment: \"默认使用的大模型名称\",\n\t\t},\n\t\t// Deepseek 配置\n\t\t{\n\t\t\tName: \"Deepseek API Key\",\n\t\t\tConfigKey: \"deepseek_api_key\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"Deepseek API 密钥\",\n\t\t},\n\t\t{\n\t\t\tName: \"Deepseek Model\",\n\t\t\tConfigKey: \"deepseek_model\",\n\t\t\tConfigValue: \"deepseek-chat\",\n\t\t\tComment: \"Deepseek 模型名称,如 deepseek-chat\",\n\t\t},\n\t\t{\n\t\t\tName: \"Deepseek Base URL\",\n\t\t\tConfigKey: \"deepseek_base_url\",\n\t\t\tConfigValue: \"https://api.deepseek.com\",\n\t\t\tComment: \"Deepseek API 地址,默认为 https://api.deepseek.com\",\n\t\t},\n\t\t{\n\t\t\tName: \"告警分析提示词\",\n\t\t\tConfigKey: \"alarm_analysis_prompt\",\n\t\t\tConfigValue: \"你是专业运维分析师。请基于以下告警信息进行分析:\\n\\n设备名称:{{hostname}}\\nIP:{{host_ip}}\\n告警描述:{{message}}\\n告警详情:{{detail}}\\n告警级别:{{level}}\\n告警状态:{{status}}\\n\\n请输出:\\n1. 可能根因(按概率排序)\\n2. 排查步骤(关键命令/检查项)\\n3. 修复方案与风险\\n4. 防复发建议\",\n\t\t\tComment: \"用于AI助手告警分析的提示词模板,支持占位符:{{hostname}} {{host_ip}} {{message}} {{detail}} {{level}} {{status}} {{alarm_context}}\",\n\t\t},\n\t}\n}\n"}, "4988a0d4c8988e6d4788dcc710968b8f3910bd8b": {"t": 1785754895.3082666, "n": 20773, "h": "686c73a4557929ce", "c": "package model\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\t\"zbxtable/pkg/logger\"\n\t\"zbxtable/pkg/utils\"\n\n\tzabbix \"github.com/canghai908/zabbix-go\"\n\t_ \"github.com/go-sql-driver/mysql\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t_ \"github.com/lib/pq\"\n\tworkwx \"github.com/xen0n/go-workwx\"\n\tini \"gopkg.in/ini.v1\"\n\t\"gorm.io/gorm\"\n)\n\nvar (\n\tAPI = &zabbix.API{}\n\tjson = jsoniter.Config{\n\t\tEscapeHTML: false,\n\t\tSortMapKeys: true,\n\t\tValidateJsonRawMessage: true,\n\t}.Froze()\n\tZBX_VER string\n\tZBX_V bool\n\tVersion string\n\tGitHash string\n\tBuildTime string\n\tWeApp *workwx.WorkwxApp // 不初始化,默认为 nil\n\n\tenvConfig map[string]string\n\tenvConfigOnce sync.Once\n\tenvFileExists bool\n)\n\nconst (\n\t//ms-agent 常量定义\n\tMSName = \"ms-agent\"\n\tMSUser = \"ms-agent\"\n\tMSGroup = \"MS-Agent\"\n\tMSMedia = \"MS-Agent\"\n\tMSAction = \"MS-Agent\"\n\t//webhook 常量定义\n\tWebhookName = \"ZbxTable\"\n\tWebhookAction = \"ZbxTable Webhook\"\n\tWebhookGroup = \"ZbxTable Webhook\"\n\tWebhookUser = \"zbxtable-webhook\"\n\t//down\n\tDownloadPath = \"./download/\"\n\tTplPath = \"./assets/templates/\"\n\t// 默认用户头像 (空字符串,由前端处理默认头像)\n\tDefaultAvatar = \"\"\n\t//默认更新服务器\n\tUpdateURL = \"http://dl.cactifans.com/stable/\"\n\n\t// 上传目录\n\tUploadDir = \"./upload\"\n\t// 背景图片上传目录\n\tBackgroundImageDir = \"./upload/background\"\n\t// 允许的图片格式\n\tAllowedImageExts = \".jpg,.jpeg,.png,.gif,.bmp,.webp,.svg\"\n\t// 最大文件大小 (10MB)\n\tMaxFileSize = 10 * 1024 * 1024\n)\n\n// TableName 表名前缀\nfunc TableName(str string) string {\n\treturn fmt.Sprintf(\"%s%s\", \"zbxtable_\", str)\n}\n\n// 接收数据库信息初始化\nfunc ModelInit(dbtype, dbhost, dbuser, dbpass, dbname, dbport string) error {\n\t// 直接使用 GORM\n\trunmode := GetConfKey(\"runmode\")\n\terr := InitGormDB(dbtype, dbhost, dbuser, dbpass, dbname, dbport, runmode)\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to connect database: \", err)\n\t\treturn err\n\t}\n\tlogger.Log.Info(\"Database connected!\")\n\t// 自动迁移表\n\terr = AutoMigrate()\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to auto migrate: \", err)\n\t\treturn err\n\t}\n\t// 基础数据初始化\n\tif err := DatabaseInit(); err != nil {\n\t\tlogger.Log.Error(\"Database init failed: \", err)\n\t\treturn err\n\t}\n\t// 使用go-cache替代Redis(不再需要Redis连接)\n\tif err := InitCache(); err != nil {\n\t\tlogger.Log.Error(\"Cache init failed: \", err)\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc InitWechat() {\n\t// 首先检查企业微信开关是否启用\n\twechatEnabled := GetConfigValueByKey(\"wechat_enabled\", \"0\")\n\tif wechatEnabled != \"1\" {\n\t\tlogger.Log.Info(\"WeChat is disabled in system config, skipping WeChat initialization\")\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\t// 开关已启用,从数据库读取企业微信配置\n\tagentIDStr := GetConfigValueByKey(\"wechat_agentid\", \"\")\n\tcorpid := GetConfigValueByKey(\"wechat_corpid\", \"\")\n\tsecret := GetConfigValueByKey(\"wechat_secret\", \"\") // 这里会自动解密\n\n\t// 验证必填配置项\n\tif agentIDStr == \"\" || corpid == \"\" || secret == \"\" {\n\t\tlogger.Log.Info(\"WeChat is enabled but configuration is incomplete (agentid/corpid/secret), WeChat app will not be initialized\")\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\t// 解析 AgentID\n\tAgentId, err := strconv.ParseInt(agentIDStr, 10, 64)\n\tif err != nil {\n\t\tlogger.Log.Error(\"wechat_agentid parse error:\", err)\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\n\t// 初始化企业微信客户端\n\tclient := workwx.New(corpid)\n\tWeApp = client.WithApp(secret, AgentId)\n\tWeApp.SpawnAccessTokenRefresher()\n\tlogger.Log.Info(\"WeChat inited successfully!\")\n}\n\n// DatabaseInit 数据初始化\nfunc DatabaseInit() error {\n\t//数据初始化操作 - 使用 GORM\n\tvar v User\n\terr := DB.Where(\"username = ?\", \"admin\").First(&v).Error\n\t//检查权限\n\tif err == nil && v.Operation == \"\" {\n\t\terr := DB.Model(&User{}).Where(\"id = ?\", v.ID).Update(\"operation\", \"['add', 'edit', 'delete','update']\").Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn fmt.Errorf(\"update admin operation failed: %w\", err)\n\t\t}\n\t\tlogger.Log.Info(\"update admin operation successfully\")\n\t}\n\t//添加管理员账号\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\tlogger.Log.Info(\"the admin user does not exist, create a new admin account later!\")\n\n\t\t// 构建默认主题配置\n\t\tdefaultThemeConfig := map[string]interface{}{\n\t\t\t\"theme\": map[string]string{\n\t\t\t\t\"color\": \"#722ed1\",\n\t\t\t\t\"mode\": \"dark\",\n\t\t\t\t\"success\": \"#52c41a\",\n\t\t\t\t\"warning\": \"#faad14\",\n\t\t\t\t\"error\": \"#f5222f\",\n\t\t\t},\n\t\t\t\"animate\": map[string]interface{}{\n\t\t\t\t\"disabled\": true,\n\t\t\t\t\"name\": \"lightSpeed\",\n\t\t\t\t\"direction\": \"left\",\n\t\t\t},\n\t\t}\n\n\t\t// 将配置转换为 JSON 字符串\n\t\tthemeJSON, err := json.Marshal(defaultThemeConfig)\n\t\tif err != nil {\n\t\t\tlogger.Log.Error(\"Failed to marshal default theme config:\", err)\n\t\t\tthemeJSON = []byte(`{\"theme\":{\"color\":\"#722ed1\",\"mode\":\"dark\",\"success\":\"#52c41a\",\"warning\":\"#faad14\",\"error\":\"#f5222f\"},\"animate\":{\"disabled\":true,\"name\":\"lightSpeed\",\"direction\":\"left\"}}`)\n\t\t}\n\n\t\t// 使用结构体插入管理员账号\n\t\tpassword, _ := utils.PasswordHash(\"Zbxtable\")\n\t\tuser := User{\n\t\t\tUsername: \"admin\",\n\t\t\tPassword: password,\n\t\t\tAvatar: DefaultAvatar,\n\t\t\tRole: \"admin\",\n\t\t\tOperation: \"['add', 'edit', 'delete','update']\",\n\t\t\tTheme: string(themeJSON),\n\t\t\tStatus: 0,\n\t\t}\n\t\terr = DB.Create(&user).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn fmt.Errorf(\"create admin account failed: %w\", err)\n\t\t}\n\t\tlogger.Log.Info(\"create an administrator account successfully, the admin ID is:\", user.ID)\n\t}\n\t//初始化系统数据\n\tvar cnt []System\n\terr = DB.Find(&cnt).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn fmt.Errorf(\"query system data failed: %w\", err)\n\t}\n\tif len(cnt) == 0 {\n\t\tnow := time.Now()\n\t\tsys := []System{\n\t\t\t{Name: \"Linux操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"Windows操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"网络设备\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"物理服务器\", Status: 0, InitedAt: &now},\n\t\t}\n\t\terr := DB.Create(&sys).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init system info error!\", err)\n\t\t\treturn fmt.Errorf(\"init system data failed: %w\", err)\n\t\t}\n\t\tlogger.Log.Info(\"Init system data successfully!\")\n\t}\n\t//告警默认规则初始化 default rule\n\tvar rules []Rule\n\terr = DB.Where(\"m_type = ?\", \"2\").Find(&rules).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn fmt.Errorf(\"query default rules failed: %w\", err)\n\t}\n\tif len(rules) == 0 {\n\t\t// 使用 \"*\" 作为全局默认规则,匹配所有租户\n\t\tdefaultRule := []Rule{\n\t\t\t{\n\t\t\t\tName: \"全局默认规则\",\n\t\t\t\tZIDs: \"*\",\n\t\t\t\tMType: \"2\",\n\t\t\t\tChannel: \"wechat_robot\",\n\t\t\t\tUserIds: \"1\",\n\t\t\t\tSweek: \"0,1,2,3,4,5,6\",\n\t\t\t\tStime: \"00:00\",\n\t\t\t\tEtime: \"23.59\",\n\t\t\t\tStatus: \"0\"},\n\t\t}\n\t\terr := DB.Create(&defaultRule).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init default rule error!\", err)\n\t\t\treturn fmt.Errorf(\"init default rule failed: %w\", err)\n\t\t}\n\t\tlogger.Log.Info(\"Init default rule successfully!\")\n\t}\n\t// 初始化加密密钥(如果不存在)\n\tvar encryptionKeyConfig Config\n\terr = DB.Where(\"config_key = ?\", \"encryption_key\").First(&encryptionKeyConfig).Error\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t// 生成随机加密密钥\n\t\trandomKey := utils.GenerateRandomKey()\n\t\tencryptionKeyConfig = Config{\n\t\t\tName: \"加密密钥\",\n\t\t\tConfigKey: \"encryption_key\",\n\t\t\tConfigValue: randomKey,\n\t\t\tComment: \"用于加密存储敏感信息(如Zabbix密码和Token)的密钥,系统自动生成,不可修改\",\n\t\t}\n\t\tif insertErr := DB.Create(&encryptionKeyConfig).Error; insertErr != nil {\n\t\t\tlogger.Log.Error(\"Init encryption_key error:\", insertErr)\n\t\t\treturn fmt.Errorf(\"init encryption_key failed: %w\", insertErr)\n\t\t} else {\n\t\t\tlogger.Log.Info(\"Init encryption_key successfully! Key length:\", len(randomKey))\n\t\t}\n\t}\n\n\t// 初始化内置映射规则\n\t//InitDefaultMappingRules()\n\n\t// 获取结构化的默认配置\n\tdefaultConfigs := getDefaultConfigs()\n\n\tfor _, cfgItem := range defaultConfigs {\n\t\tvar existing Config\n\t\terr = DB.Where(\"config_key = ?\", cfgItem.ConfigKey).First(&existing).Error\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\tif insertErr := DB.Create(&cfgItem).Error; insertErr != nil {\n\t\t\t\tlogger.Log.Infof(\"Init config key %s error: %v\", cfgItem.ConfigKey, insertErr)\n\t\t\t\treturn fmt.Errorf(\"init config key %s failed: %w\", cfgItem.ConfigKey, insertErr)\n\t\t\t}\n\t\t\tlogger.Log.Infof(\"Init config key %s successfully!\", cfgItem.ConfigKey)\n\t\t} else if err != nil {\n\t\t\tlogger.Log.Infof(\"Init config key %s error: %v\", cfgItem.ConfigKey, err)\n\t\t\treturn fmt.Errorf(\"check config key %s failed: %w\", cfgItem.ConfigKey, err)\n\t\t}\n\t}\n\t//默认菜单初始化\n\tif err := InitMenuData(); err != nil {\n\t\treturn fmt.Errorf(\"init menu data failed: %w\", err)\n\t}\n\t//检查并添加缺失的菜单项(用于版本升级)\n\tif err := CheckAndAddMenus(); err != nil {\n\t\treturn fmt.Errorf(\"check and add menus failed: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc GetConfKey(v string) string {\n\t// 优先从 .env 中读取(便于开发环境配置敏感信息且不提交到 git)\n\tenvConfigOnce.Do(func() {\n\t\tfile, err := os.Open(\".env\")\n\t\tif err != nil {\n\t\t\t// 没有 .env 文件时直接跳过,后面回退到 app.conf\n\t\t\tenvFileExists = false\n\t\t\treturn\n\t\t}\n\t\tdefer file.Close()\n\n\t\tenvFileExists = true\n\t\tenvConfig = make(map[string]string)\n\n\t\tscanner := bufio.NewScanner(file)\n\t\tfor scanner.Scan() {\n\t\t\tline := strings.TrimSpace(scanner.Text())\n\t\t\tif line == \"\" || strings.HasPrefix(line, \"#\") || strings.HasPrefix(line, \";\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tparts := strings.SplitN(line, \"=\", 2)\n\t\t\tif len(parts) != 2 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tkey := strings.TrimSpace(parts[0])\n\t\t\tval := strings.TrimSpace(parts[1])\n\t\t\t// 去掉可能的引号\n\t\t\tval = strings.Trim(val, `\"'`)\n\t\t\tif key != \"\" {\n\t\t\t\tenvConfig[key] = val // 即使 val 为空也认为是有效配置\n\t\t\t}\n\t\t}\n\t})\n\n\t// 如果存在 .env 文件,则所有配置完全由 .env 决定:\n\t// - key 存在:返回对应值(可以是空字符串)\n\t// - key 不存在:返回空字符串\n\tif envFileExists {\n\t\tif envConfig == nil {\n\t\t\treturn \"\"\n\t\t}\n\t\tif val, ok := envConfig[v]; ok {\n\t\t\treturn val\n\t\t}\n\t\treturn \"\"\n\t}\n\t// 其次从 config/app.conf 读取\n\tcfg, err := ini.Load(\"./config/app.conf\")\n\tif err != nil {\n\t\tlogger.Log.Error(err)\n\t\treturn \"\"\n\t}\n\tp, err := cfg.Section(\"\").GetKey(v)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn p.String()\n}\n\n// IsPasswordConfigured 检查是否配置了密码(用于图形查看)\n// 优先检查数据库中是否有激活的 Zabbix 实例配置了用户名和密码\n// 注意:查看图形需要用户名和密码,Token 方式无法用于 Web 登录\nfunc IsPasswordConfigured() bool {\n\t// 1. 优先检查数据库中是否有激活的 Zabbix 实例\n\tvar activeInstance ZabbixInstance\n\terr := DB.Where(\"is_active = ?\", true).First(&activeInstance).Error\n\tif err == nil {\n\t\t// 找到激活的实例,检查是否配置了用户名和密码(查看图形必须用密码,Token 无法用于 Web 登录)\n\t\tif strings.TrimSpace(activeInstance.User) != \"\" && strings.TrimSpace(activeInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 2. 如果数据库中没有激活实例,检查是否有任何可用的实例配置了密码\n\tvar anyInstance ZabbixInstance\n\terr = DB.Where(\"enabled = ?\", true).First(&anyInstance).Error\n\tif err == nil {\n\t\t// 找到可用的实例,检查是否配置了用户名和密码\n\t\tif strings.TrimSpace(anyInstance.User) != \"\" && strings.TrimSpace(anyInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 3. 回退到配置文件检查(兼容旧版本)\n\tpass := GetConfKey(\"zabbix_pass\")\n\tuser := GetConfKey(\"zabbix_user\")\n\treturn pass != \"\" && user != \"\"\n}\n\n// GetEncryptionKey 获取加密密钥\n// 优先从数据库读取,如果数据库中没有则从环境变量读取,最后使用默认密钥\nfunc GetEncryptionKey() string {\n\t// 1. 优先从数据库读取\n\tkey := GetConfigValueByKey(\"encryption_key\", \"\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 2. 从环境变量或配置文件读取\n\tkey = GetConfKey(\"encryption_key\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 3. 使用默认密钥(不应该到这里,因为 DatabaseInit 会生成)\n\tlogger.Log.Warn(\"使用默认加密密钥,建议在数据库中配置 encryption_key\")\n\treturn \"zbxtable-default-encryption-key-2024\"\n}\n\n// getDefaultConfigs 返回系统默认配置列表\n// 配置按功能模块分组,便于维护和管理\nfunc getDefaultConfigs() []Config {\n\tconfigs := make([]Config, 0)\n\n\t// 系统外观配置\n\tconfigs = append(configs, getSystemAppearanceConfigs()...)\n\n\t// 初始配置状态\n\tconfigs = append(configs, getInitialSetupConfigs()...)\n\n\t// Dashboard 相关配置\n\tconfigs = append(configs, getDashboardConfigs()...)\n\n\t// 邮件配置\n\tconfigs = append(configs, getEmailConfigs()...)\n\n\t// 企业微信配置\n\tconfigs = append(configs, getWeChatConfigs()...)\n\n\t// AI 配置\n\tconfigs = append(configs, getAIConfigs()...)\n\n\t// 演示模式配置\n\tconfigs = append(configs, getDemoConfigs()...)\n\n\treturn configs\n}\n\n// getDemoConfigs 演示模式配置\nfunc getDemoConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"演示模式\",\n\t\t\tConfigKey: \"demo_mode\",\n\t\t\tConfigValue: \"false\",\n\t\t\tComment: \"是否开启演示模式(只读):true 开启, false 关闭\",\n\t\t},\n\t}\n}\n\n// getSystemAppearanceConfigs 系统外观配置\nfunc getSystemAppearanceConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"系统名称\",\n\t\t\tConfigKey: \"system_name\",\n\t\t\tConfigValue: \"ZbxTable\",\n\t\t\tComment: \"系统显示的名称\",\n\t\t},\n\t\t{\n\t\t\tName: \"系统Logo\",\n\t\t\tConfigKey: \"system_logo\",\n\t\t\tConfigValue: \"data:image/png;base64,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\",\n\t\t\tComment: \"系统Logo的base64编码数据\",\n\t\t},\n\t}\n}\n\n// getInitialSetupConfigs 初始配置状态\nfunc getInitialSetupConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"初始配置完成\",\n\t\t\tConfigKey: \"initial_setup_completed\",\n\t\t\tConfigValue: \"0\",\n\t\t\tComment: \"标记系统是否完成初始配置:1 已完成,0 未完成\",\n\t\t},\n\t}\n}\n\n// getDashboardConfigs Dashboard 相关配置\nfunc getDashboardConfigs() []Config {\n\treturn []Config{\n\t\t// {\n\t\t// \tName: \"数据面板\",\n\t\t// \tConfigKey: \"zbx_dash\",\n\t\t// \tConfigValue: \"0\",\n\t\t// \tComment: \"是否开启Zabbix看板:1 开启,0 关闭\",\n\t\t// },\n\t\t// {\n\t\t// \tName: \"面板配置\",\n\t\t// \tConfigKey: \"dash_id\",\n\t\t// \tConfigValue: \"1\",\n\t\t// \tComment: \"需要引入的Zabbix面板的ID,默认为1\",\n\t\t// },\n\t\t{\n\t\t\tName: \"Linux Top数量\",\n\t\t\tConfigKey: \"dash_top_lin_num\",\n\t\t\tConfigValue: \"10\",\n\t\t\tComment: \"首页 Linux Top 列表展示数量\",\n\t\t},\n\t\t{\n\t\t\tName: \"Windows Top数量\",\n\t\t\tConfigKey: \"dash_top_win_num\",\n\t\t\tConfigValue: \"10\",\n\t\t\tComment: \"首页 Windows Top 列表展示数量\",\n\t\t},\n\t\t{\n\t\t\tName: \"主机分类同步\",\n\t\t\tConfigKey: \"sync_inventory\",\n\t\t\tConfigValue: \"1\",\n\t\t\tComment: \"主机分类同步计划任务是否启用:1 启用,0 不启用\",\n\t\t},\n\t\t{\n\t\t\tName: \"Webhook回调地址\",\n\t\t\tConfigKey: \"webhook_url\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"webhook通知地址\",\n\t\t},\n\t}\n}\n\n// getEmailConfigs 邮件配置\nfunc getEmailConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"邮件发件人\",\n\t\t\tConfigKey: \"email_from\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"告警邮件发件人邮箱地址\",\n\t\t},\n\t\t{\n\t\t\tName: \"邮件昵称\",\n\t\t\tConfigKey: \"email_nickname\",\n\t\t\tConfigValue: \"ZbxTable\",\n\t\t\tComment: \"告警邮件显示的发件人昵称\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 密码/授权码\",\n\t\t\tConfigKey: \"email_secret\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"SMTP 登录密码或授权码\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 服务器\",\n\t\t\tConfigKey: \"email_host\",\n\t\t\tConfigValue: \"smtp.qq.com\",\n\t\t\tComment: \"SMTP 服务器地址\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 端口\",\n\t\t\tConfigKey: \"email_port\",\n\t\t\tConfigValue: \"465\",\n\t\t\tComment: \"SMTP 端口号\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 使用 SSL\",\n\t\t\tConfigKey: \"email_isSSl\",\n\t\t\tConfigValue: \"true\",\n\t\t\tComment: \"是否启用 SSL:true/false\",\n\t\t},\n\t}\n}\n\n// getWeChatConfigs 企业微信配置\nfunc getWeChatConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"企业微信开关\",\n\t\t\tConfigKey: \"wechat_enabled\",\n\t\t\tConfigValue: \"0\",\n\t\t\tComment: \"是否启用企业微信:1 启用,0 禁用\",\n\t\t},\n\t\t{\n\t\t\tName: \"企业微信 AgentID\",\n\t\t\tConfigKey: \"wechat_agentid\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"企业微信应用的 AgentID\",\n\t\t},\n\t\t{\n\t\t\tName: \"企业微信 CorpID\",\n\t\t\tConfigKey: \"wechat_corpid\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"企业微信企业ID\",\n\t\t},\n\t\t{\n\t\t\tName: \"企业微信 Secret\",\n\t\t\tConfigKey: \"wechat_secret\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"企业微信应用的 Secret\",\n\t\t},\n\t}\n}\n\n// getAIConfigs AI 配置\nfunc getAIConfigs() []Config {\n\treturn []Config{\n\t\t// AI 类型选择\n\t\t{\n\t\t\tName: \"AI 类型\",\n\t\t\tConfigKey: \"ai_type\",\n\t\t\tConfigValue: \"ollama\",\n\t\t\tComment: \"选择使用的 AI 服务类型:ollama 或 deepseek\",\n\t\t},\n\t\t// Ollama 配置\n\t\t{\n\t\t\tName: \"Ollama Host\",\n\t\t\tConfigKey: \"ollama_host\",\n\t\t\tConfigValue: \"http://localhost:11434\",\n\t\t\tComment: \"Ollama 服务地址,如 http://127.0.0.1:11434\",\n\t\t},\n\t\t{\n\t\t\tName: \"Ollama Model\",\n\t\t\tConfigKey: \"ollama_model\",\n\t\t\tConfigValue: \"deepseek-r1:32b\",\n\t\t\tComment: \"默认使用的大模型名称\",\n\t\t},\n\t\t// Deepseek 配置\n\t\t{\n\t\t\tName: \"Deepseek API Key\",\n\t\t\tConfigKey: \"deepseek_api_key\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"Deepseek API 密钥\",\n\t\t},\n\t\t{\n\t\t\tName: \"Deepseek Model\",\n\t\t\tConfigKey: \"deepseek_model\",\n\t\t\tConfigValue: \"deepseek-chat\",\n\t\t\tComment: \"Deepseek 模型名称,如 deepseek-chat\",\n\t\t},\n\t\t{\n\t\t\tName: \"Deepseek Base URL\",\n\t\t\tConfigKey: \"deepseek_base_url\",\n\t\t\tConfigValue: \"https://api.deepseek.com\",\n\t\t\tComment: \"Deepseek API 地址,默认为 https://api.deepseek.com\",\n\t\t},\n\t\t{\n\t\t\tName: \"告警分析提示词\",\n\t\t\tConfigKey: \"alarm_analysis_prompt\",\n\t\t\tConfigValue: \"你是专业运维分析师。请基于以下告警信息进行分析:\\n\\n设备名称:{{hostname}}\\nIP:{{host_ip}}\\n告警描述:{{message}}\\n告警详情:{{detail}}\\n告警级别:{{level}}\\n告警状态:{{status}}\\n\\n请输出:\\n1. 可能根因(按概率排序)\\n2. 排查步骤(关键命令/检查项)\\n3. 修复方案与风险\\n4. 防复发建议\",\n\t\t\tComment: \"用于AI助手告警分析的提示词模板,支持占位符:{{hostname}} {{host_ip}} {{message}} {{detail}} {{level}} {{status}} {{alarm_context}}\",\n\t\t},\n\t}\n}\n"}, "3f1b74075fed0c95be23b2a7e624627a4b8b7df1": {"t": 1785754896.671288, "n": 20359, "h": "7e397e24617b00db", "c": "package model\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\t\"zbxtable/pkg/logger\"\n\t\"zbxtable/pkg/utils\"\n\n\tzabbix \"github.com/canghai908/zabbix-go\"\n\t_ \"github.com/go-sql-driver/mysql\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t_ \"github.com/lib/pq\"\n\tworkwx \"github.com/xen0n/go-workwx\"\n\tini \"gopkg.in/ini.v1\"\n\t\"gorm.io/gorm\"\n)\n\nvar (\n\tAPI = &zabbix.API{}\n\tjson = jsoniter.Config{\n\t\tEscapeHTML: false,\n\t\tSortMapKeys: true,\n\t\tValidateJsonRawMessage: true,\n\t}.Froze()\n\tZBX_VER string\n\tZBX_V bool\n\tVersion string\n\tGitHash string\n\tBuildTime string\n\tWeApp *workwx.WorkwxApp // 不初始化,默认为 nil\n\n\tenvConfig map[string]string\n\tenvConfigOnce sync.Once\n\tenvFileExists bool\n)\n\nconst (\n\t//ms-agent 常量定义\n\tMSName = \"ms-agent\"\n\tMSUser = \"ms-agent\"\n\tMSGroup = \"MS-Agent\"\n\tMSMedia = \"MS-Agent\"\n\tMSAction = \"MS-Agent\"\n\t//webhook 常量定义\n\tWebhookName = \"ZbxTable\"\n\tWebhookAction = \"ZbxTable Webhook\"\n\tWebhookGroup = \"ZbxTable Webhook\"\n\tWebhookUser = \"zbxtable-webhook\"\n\t//down\n\tDownloadPath = \"./download/\"\n\tTplPath = \"./assets/templates/\"\n\t// 默认用户头像 (空字符串,由前端处理默认头像)\n\tDefaultAvatar = \"\"\n\t//默认更新服务器\n\tUpdateURL = \"http://dl.cactifans.com/stable/\"\n\n\t// 上传目录\n\tUploadDir = \"./upload\"\n\t// 背景图片上传目录\n\tBackgroundImageDir = \"./upload/background\"\n\t// 允许的图片格式\n\tAllowedImageExts = \".jpg,.jpeg,.png,.gif,.bmp,.webp,.svg\"\n\t// 最大文件大小 (10MB)\n\tMaxFileSize = 10 * 1024 * 1024\n)\n\n// TableName 表名前缀\nfunc TableName(str string) string {\n\treturn fmt.Sprintf(\"%s%s\", \"zbxtable_\", str)\n}\n\n// 接收数据库信息初始化\nfunc ModelInit(dbtype, dbhost, dbuser, dbpass, dbname, dbport string) error {\n\t// 直接使用 GORM\n\trunmode := GetConfKey(\"runmode\")\n\terr := InitGormDB(dbtype, dbhost, dbuser, dbpass, dbname, dbport, runmode)\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to connect database: \", err)\n\t\treturn err\n\t}\n\tlogger.Log.Info(\"Database connected!\")\n\t// 自动迁移表\n\terr = AutoMigrate()\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to auto migrate: \", err)\n\t\treturn err\n\t}\n\t// 基础数据初始化\n\tif err := DatabaseInit(); err != nil {\n\t\tlogger.Log.Error(\"Database init failed: \", err)\n\t\treturn err\n\t}\n\t// 使用go-cache替代Redis(不再需要Redis连接)\n\tif err := InitCache(); err != nil {\n\t\tlogger.Log.Error(\"Cache init failed: \", err)\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc InitWechat() {\n\t// 首先检查企业微信开关是否启用\n\twechatEnabled := GetConfigValueByKey(\"wechat_enabled\", \"0\")\n\tif wechatEnabled != \"1\" {\n\t\tlogger.Log.Info(\"WeChat is disabled in system config, skipping WeChat initialization\")\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\t// 开关已启用,从数据库读取企业微信配置\n\tagentIDStr := GetConfigValueByKey(\"wechat_agentid\", \"\")\n\tcorpid := GetConfigValueByKey(\"wechat_corpid\", \"\")\n\tsecret := GetConfigValueByKey(\"wechat_secret\", \"\") // 这里会自动解密\n\n\t// 验证必填配置项\n\tif agentIDStr == \"\" || corpid == \"\" || secret == \"\" {\n\t\tlogger.Log.Info(\"WeChat is enabled but configuration is incomplete (agentid/corpid/secret), WeChat app will not be initialized\")\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\t// 解析 AgentID\n\tAgentId, err := strconv.ParseInt(agentIDStr, 10, 64)\n\tif err != nil {\n\t\tlogger.Log.Error(\"wechat_agentid parse error:\", err)\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\n\t// 初始化企业微信客户端\n\tclient := workwx.New(corpid)\n\tWeApp = client.WithApp(secret, AgentId)\n\tWeApp.SpawnAccessTokenRefresher()\n\tlogger.Log.Info(\"WeChat inited successfully!\")\n}\n\n// DatabaseInit 数据初始化\nfunc DatabaseInit() error {\n\t//数据初始化操作 - 使用 GORM\n\tvar v User\n\terr := DB.Where(\"username = ?\", \"admin\").First(&v).Error\n\t//检查权限\n\tif err == nil && v.Operation == \"\" {\n\t\terr := DB.Model(&User{}).Where(\"id = ?\", v.ID).Update(\"operation\", \"['add', 'edit', 'delete','update']\").Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn fmt.Errorf(\"update admin operation failed: %w\", err)\n\t\t}\n\t\tlogger.Log.Info(\"update admin operation successfully\")\n\t}\n\t//添加管理员账号\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\tlogger.Log.Info(\"the admin user does not exist, create a new admin account later!\")\n\n\t\t// 构建默认主题配置\n\t\tdefaultThemeConfig := map[string]interface{}{\n\t\t\t\"theme\": map[string]string{\n\t\t\t\t\"color\": \"#722ed1\",\n\t\t\t\t\"mode\": \"dark\",\n\t\t\t\t\"success\": \"#52c41a\",\n\t\t\t\t\"warning\": \"#faad14\",\n\t\t\t\t\"error\": \"#f5222f\",\n\t\t\t},\n\t\t\t\"animate\": map[string]interface{}{\n\t\t\t\t\"disabled\": true,\n\t\t\t\t\"name\": \"lightSpeed\",\n\t\t\t\t\"direction\": \"left\",\n\t\t\t},\n\t\t}\n\n\t\t// 将配置转换为 JSON 字符串\n\t\tthemeJSON, err := json.Marshal(defaultThemeConfig)\n\t\tif err != nil {\n\t\t\tlogger.Log.Error(\"Failed to marshal default theme config:\", err)\n\t\t\tthemeJSON = []byte(`{\"theme\":{\"color\":\"#722ed1\",\"mode\":\"dark\",\"success\":\"#52c41a\",\"warning\":\"#faad14\",\"error\":\"#f5222f\"},\"animate\":{\"disabled\":true,\"name\":\"lightSpeed\",\"direction\":\"left\"}}`)\n\t\t}\n\n\t\t// 使用结构体插入管理员账号\n\t\tpassword, _ := utils.PasswordHash(\"Zbxtable\")\n\t\tuser := User{\n\t\t\tUsername: \"admin\",\n\t\t\tPassword: password,\n\t\t\tAvatar: DefaultAvatar,\n\t\t\tRole: \"admin\",\n\t\t\tOperation: \"['add', 'edit', 'delete','update']\",\n\t\t\tTheme: string(themeJSON),\n\t\t\tStatus: 0,\n\t\t}\n\t\terr = DB.Create(&user).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn fmt.Errorf(\"create admin account failed: %w\", err)\n\t\t}\n\t\tlogger.Log.Info(\"create an administrator account successfully, the admin ID is:\", user.ID)\n\t}\n\t//初始化系统数据\n\tvar cnt []System\n\terr = DB.Find(&cnt).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn fmt.Errorf(\"query system data failed: %w\", err)\n\t}\n\tif len(cnt) == 0 {\n\t\tnow := time.Now()\n\t\tsys := []System{\n\t\t\t{Name: \"Linux操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"Windows操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"网络设备\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"物理服务器\", Status: 0, InitedAt: &now},\n\t\t}\n\t\terr := DB.Create(&sys).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init system info error!\", err)\n\t\t\treturn fmt.Errorf(\"init system data failed: %w\", err)\n\t\t}\n\t\tlogger.Log.Info(\"Init system data successfully!\")\n\t}\n\t//告警默认规则初始化 default rule\n\tvar rules []Rule\n\terr = DB.Where(\"m_type = ?\", \"2\").Find(&rules).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn fmt.Errorf(\"query default rules failed: %w\", err)\n\t}\n\tif len(rules) == 0 {\n\t\t// 使用 \"*\" 作为全局默认规则,匹配所有租户\n\t\tdefaultRule := []Rule{\n\t\t\t{\n\t\t\t\tName: \"全局默认规则\",\n\t\t\t\tZIDs: \"*\",\n\t\t\t\tMType: \"2\",\n\t\t\t\tChannel: \"wechat_robot\",\n\t\t\t\tUserIds: \"1\",\n\t\t\t\tSweek: \"0,1,2,3,4,5,6\",\n\t\t\t\tStime: \"00:00\",\n\t\t\t\tEtime: \"23.59\",\n\t\t\t\tStatus: \"0\"},\n\t\t}\n\t\terr := DB.Create(&defaultRule).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init default rule error!\", err)\n\t\t\treturn fmt.Errorf(\"init default rule failed: %w\", err)\n\t\t}\n\t\tlogger.Log.Info(\"Init default rule successfully!\")\n\t}\n\t// 初始化加密密钥(如果不存在)\n\tvar encryptionKeyConfig Config\n\terr = DB.Where(\"config_key = ?\", \"encryption_key\").First(&encryptionKeyConfig).Error\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t// 生成随机加密密钥\n\t\trandomKey := utils.GenerateRandomKey()\n\t\tencryptionKeyConfig = Config{\n\t\t\tName: \"加密密钥\",\n\t\t\tConfigKey: \"encryption_key\",\n\t\t\tConfigValue: randomKey,\n\t\t\tComment: \"用于加密存储敏感信息(如Zabbix密码和Token)的密钥,系统自动生成,不可修改\",\n\t\t}\n\t\tif insertErr := DB.Create(&encryptionKeyConfig).Error; insertErr != nil {\n\t\t\tlogger.Log.Error(\"Init encryption_key error:\", insertErr)\n\t\t\treturn fmt.Errorf(\"init encryption_key failed: %w\", insertErr)\n\t\t} else {\n\t\t\tlogger.Log.Info(\"Init encryption_key successfully! Key length:\", len(randomKey))\n\t\t}\n\t}\n\n\t// 初始化内置映射规则\n\t//InitDefaultMappingRules()\n\n\t// 获取结构化的默认配置\n\tdefaultConfigs := getDefaultConfigs()\n\n\tfor _, cfgItem := range defaultConfigs {\n\t\tvar existing Config\n\t\terr = DB.Where(\"config_key = ?\", cfgItem.ConfigKey).First(&existing).Error\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\tif insertErr := DB.Create(&cfgItem).Error; insertErr != nil {\n\t\t\t\tlogger.Log.Infof(\"Init config key %s error: %v\", cfgItem.ConfigKey, insertErr)\n\t\t\t\treturn fmt.Errorf(\"init config key %s failed: %w\", cfgItem.ConfigKey, insertErr)\n\t\t\t}\n\t\t\tlogger.Log.Infof(\"Init config key %s successfully!\", cfgItem.ConfigKey)\n\t\t} else if err != nil {\n\t\t\tlogger.Log.Infof(\"Init config key %s error: %v\", cfgItem.ConfigKey, err)\n\t\t\treturn fmt.Errorf(\"check config key %s failed: %w\", cfgItem.ConfigKey, err)\n\t\t}\n\t}\n\t//默认菜单初始化\n\tif err := InitMenuData(); err != nil {\n\t\treturn fmt.Errorf(\"init menu data failed: %w\", err)\n\t}\n\t//检查并添加缺失的菜单项(用于版本升级)\n\tif err := CheckAndAddMenus(); err != nil {\n\t\treturn fmt.Errorf(\"check and add menus failed: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc GetConfKey(v string) string {\n\t// 优先从 .env 中读取(便于开发环境配置敏感信息且不提交到 git)\n\tenvConfigOnce.Do(func() {\n\t\tfile, err := os.Open(\".env\")\n\t\tif err != nil {\n\t\t\t// 没有 .env 文件时直接跳过,后面回退到 app.conf\n\t\t\tenvFileExists = false\n\t\t\treturn\n\t\t}\n\t\tdefer file.Close()\n\n\t\tenvFileExists = true\n\t\tenvConfig = make(map[string]string)\n\n\t\tscanner := bufio.NewScanner(file)\n\t\tfor scanner.Scan() {\n\t\t\tline := strings.TrimSpace(scanner.Text())\n\t\t\tif line == \"\" || strings.HasPrefix(line, \"#\") || strings.HasPrefix(line, \";\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tparts := strings.SplitN(line, \"=\", 2)\n\t\t\tif len(parts) != 2 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tkey := strings.TrimSpace(parts[0])\n\t\t\tval := strings.TrimSpace(parts[1])\n\t\t\t// 去掉可能的引号\n\t\t\tval = strings.Trim(val, `\"'`)\n\t\t\tif key != \"\" {\n\t\t\t\tenvConfig[key] = val // 即使 val 为空也认为是有效配置\n\t\t\t}\n\t\t}\n\t})\n\n\t// 如果存在 .env 文件,则所有配置完全由 .env 决定:\n\t// - key 存在:返回对应值(可以是空字符串)\n\t// - key 不存在:返回空字符串\n\tif envFileExists {\n\t\tif envConfig == nil {\n\t\t\treturn \"\"\n\t\t}\n\t\tif val, ok := envConfig[v]; ok {\n\t\t\treturn val\n\t\t}\n\t\treturn \"\"\n\t}\n\t// 其次从 config/app.conf 读取\n\tcfg, err := ini.Load(\"./config/app.conf\")\n\tif err != nil {\n\t\tlogger.Log.Error(err)\n\t\treturn \"\"\n\t}\n\tp, err := cfg.Section(\"\").GetKey(v)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn p.String()\n}\n\n// IsPasswordConfigured 检查是否配置了密码(用于图形查看)\n// 优先检查数据库中是否有激活的 Zabbix 实例配置了用户名和密码\n// 注意:查看图形需要用户名和密码,Token 方式无法用于 Web 登录\nfunc IsPasswordConfigured() bool {\n\t// 1. 优先检查数据库中是否有激活的 Zabbix 实例\n\tvar activeInstance ZabbixInstance\n\terr := DB.Where(\"is_active = ?\", true).First(&activeInstance).Error\n\tif err == nil {\n\t\t// 找到激活的实例,检查是否配置了用户名和密码(查看图形必须用密码,Token 无法用于 Web 登录)\n\t\tif strings.TrimSpace(activeInstance.User) != \"\" && strings.TrimSpace(activeInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 2. 如果数据库中没有激活实例,检查是否有任何可用的实例配置了密码\n\tvar anyInstance ZabbixInstance\n\terr = DB.Where(\"enabled = ?\", true).First(&anyInstance).Error\n\tif err == nil {\n\t\t// 找到可用的实例,检查是否配置了用户名和密码\n\t\tif strings.TrimSpace(anyInstance.User) != \"\" && strings.TrimSpace(anyInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 3. 回退到配置文件检查(兼容旧版本)\n\tpass := GetConfKey(\"zabbix_pass\")\n\tuser := GetConfKey(\"zabbix_user\")\n\treturn pass != \"\" && user != \"\"\n}\n\n// GetEncryptionKey 获取加密密钥\n// 优先从数据库读取,如果数据库中没有则从环境变量读取,最后使用默认密钥\nfunc GetEncryptionKey() string {\n\t// 1. 优先从数据库读取\n\tkey := GetConfigValueByKey(\"encryption_key\", \"\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 2. 从环境变量或配置文件读取\n\tkey = GetConfKey(\"encryption_key\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 3. 使用默认密钥(不应该到这里,因为 DatabaseInit 会生成)\n\tlogger.Log.Warn(\"使用默认加密密钥,建议在数据库中配置 encryption_key\")\n\treturn \"zbxtable-default-encryption-key-2024\"\n}\n\n// getDefaultConfigs 返回系统默认配置列表\n// 配置按功能模块分组,便于维护和管理\nfunc getDefaultConfigs() []Config {\n\tconfigs := make([]Config, 0)\n\n\t// 系统外观配置\n\tconfigs = append(configs, getSystemAppearanceConfigs()...)\n\n\t// 初始配置状态\n\tconfigs = append(configs, getInitialSetupConfigs()...)\n\n\t// Dashboard 相关配置\n\tconfigs = append(configs, getDashboardConfigs()...)\n\n\t// 邮件配置\n\tconfigs = append(configs, getEmailConfigs()...)\n\n\t// 企业微信配置\n\tconfigs = append(configs, getWeChatConfigs()...)\n\n\t// AI 配置\n\tconfigs = append(configs, getAIConfigs()...)\n\n\t// 演示模式配置\n\tconfigs = append(configs, getDemoConfigs()...)\n\n\treturn configs\n}\n\n// getDemoConfigs 演示模式配置\nfunc getDemoConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"演示模式\",\n\t\t\tConfigKey: \"demo_mode\",\n\t\t\tConfigValue: \"false\",\n\t\t\tComment: \"是否开启演示模式(只读):true 开启, false 关闭\",\n\t\t},\n\t}\n}\n\n// getSystemAppearanceConfigs 系统外观配置\nfunc getSystemAppearanceConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"系统名称\",\n\t\t\tConfigKey: \"system_name\",\n\t\t\tConfigValue: \"ZbxTable\",\n\t\t\tComment: \"系统显示的名称\",\n\t\t},\n\t\t{\n\t\t\tName: \"系统Logo\",\n\t\t\tConfigKey: \"system_logo\",\n\t\t\tConfigValue: \"data:image/png;base64,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\",\n\t\t\tComment: \"系统Logo的base64编码数据\",\n\t\t},\n\t}\n}\n\n// getInitialSetupConfigs 初始配置状态\nfunc getInitialSetupConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"初始配置完成\",\n\t\t\tConfigKey: \"initial_setup_completed\",\n\t\t\tConfigValue: \"0\",\n\t\t\tComment: \"标记系统是否完成初始配置:1 已完成,0 未完成\",\n\t\t},\n\t}\n}\n\n// getDashboardConfigs Dashboard 相关配置\nfunc getDashboardConfigs() []Config {\n\treturn []Config{\n\t\t// {\n\t\t// \tName: \"数据面板\",\n\t\t// \tConfigKey: \"zbx_dash\",\n\t\t// \tConfigValue: \"0\",\n\t\t// \tComment: \"是否开启Zabbix看板:1 开启,0 关闭\",\n\t\t// },\n\t\t// {\n\t\t// \tName: \"面板配置\",\n\t\t// \tConfigKey: \"dash_id\",\n\t\t// \tConfigValue: \"1\",\n\t\t// \tComment: \"需要引入的Zabbix面板的ID,默认为1\",\n\t\t// },\n\t\t{\n\t\t\tName: \"Linux Top数量\",\n\t\t\tConfigKey: \"dash_top_lin_num\",\n\t\t\tConfigValue: \"10\",\n\t\t\tComment: \"首页 Linux Top 列表展示数量\",\n\t\t},\n\t\t{\n\t\t\tName: \"Windows Top数量\",\n\t\t\tConfigKey: \"dash_top_win_num\",\n\t\t\tConfigValue: \"10\",\n\t\t\tComment: \"首页 Windows Top 列表展示数量\",\n\t\t},\n\t\t{\n\t\t\tName: \"主机分类同步\",\n\t\t\tConfigKey: \"sync_inventory\",\n\t\t\tConfigValue: \"1\",\n\t\t\tComment: \"主机分类同步计划任务是否启用:1 启用,0 不启用\",\n\t\t},\n\t\t{\n\t\t\tName: \"Webhook回调地址\",\n\t\t\tConfigKey: \"webhook_url\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"webhook通知地址\",\n\t\t},\n\t}\n}\n\n// getEmailConfigs 邮件配置\nfunc getEmailConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"邮件发件人\",\n\t\t\tConfigKey: \"email_from\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"告警邮件发件人邮箱地址\",\n\t\t},\n\t\t{\n\t\t\tName: \"邮件昵称\",\n\t\t\tConfigKey: \"email_nickname\",\n\t\t\tConfigValue: \"ZbxTable\",\n\t\t\tComment: \"告警邮件显示的发件人昵称\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 密码/授权码\",\n\t\t\tConfigKey: \"email_secret\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"SMTP 登录密码或授权码\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 服务器\",\n\t\t\tConfigKey: \"email_host\",\n\t\t\tConfigValue: \"smtp.qq.com\",\n\t\t\tComment: \"SMTP 服务器地址\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 端口\",\n\t\t\tConfigKey: \"email_port\",\n\t\t\tConfigValue: \"465\",\n\t\t\tComment: \"SMTP 端口号\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 使用 SSL\",\n\t\t\tConfigKey: \"email_isSSl\",\n\t\t\tConfigValue: \"true\",\n\t\t\tComment: \"是否启用 SSL:true/false\",\n\t\t},\n\t}\n}\n\n// getWeChatConfigs 企业微信配置\nfunc getWeChatConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"企业微信开关\",\n\t\t\tConfigKey: \"wechat_enabled\",\n\t\t\tConfigValue: \"0\",\n\t\t\tComment: \"是否启用企业微信:1 启用,0 禁用\",\n\t\t},\n\t\t{\n\t\t\tName: \"企业微信 AgentID\",\n\t\t\tConfigKey: \"wechat_agentid\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"企业微信应用的 AgentID\",\n\t\t},\n\t\t{\n\t\t\tName: \"企业微信 CorpID\",\n\t\t\tConfigKey: \"wechat_corpid\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"企业微信企业ID\",\n\t\t},\n\t\t{\n\t\t\tName: \"企业微信 Secret\",\n\t\t\tConfigKey: \"wechat_secret\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"企业微信应用的 Secret\",\n\t\t},\n\t}\n}\n\n// getAIConfigs AI 配置\nfunc getAIConfigs() []Config {\n\treturn []Config{\n\t\t// AI 类型选择\n\t\t{\n\t\t\tName: \"AI 类型\",\n\t\t\tConfigKey: \"ai_type\",\n\t\t\tConfigValue: \"ollama\",\n\t\t\tComment: \"选择使用的 AI 服务类型:ollama 或 deepseek\",\n\t\t},\n\t\t// Ollama 配置\n\t\t{\n\t\t\tName: \"Ollama Host\",\n\t\t\tConfigKey: \"ollama_host\",\n\t\t\tConfigValue: \"http://localhost:11434\",\n\t\t\tComment: \"Ollama 服务地址,如 http://127.0.0.1:11434\",\n\t\t},\n\t\t{\n\t\t\tName: \"Ollama Model\",\n\t\t\tConfigKey: \"ollama_model\",\n\t\t\tConfigValue: \"deepseek-r1:32b\",\n\t\t\tComment: \"默认使用的大模型名称\",\n\t\t},\n\t\t// Deepseek 配置\n\t\t{\n\t\t\tName: \"Deepseek API Key\",\n\t\t\tConfigKey: \"deepseek_api_key\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"Deepseek API 密钥\",\n\t\t},\n\t\t{\n\t\t\tName: \"Deepseek Model\",\n\t\t\tConfigKey: \"deepseek_model\",\n\t\t\tConfigValue: \"deepseek-chat\",\n\t\t\tComment: \"Deepseek 模型名称,如 deepseek-chat\",\n\t\t},\n\t\t{\n\t\t\tName: \"Deepseek Base URL\",\n\t\t\tConfigKey: \"deepseek_base_url\",\n\t\t\tConfigValue: \"https://api.deepseek.com\",\n\t\t\tComment: \"Deepseek API 地址,默认为 https://api.deepseek.com\",\n\t\t},\n\t}\n}\n"}, "8a8c026f9a8e74e657902d9eeef825c80efb7558": {"t": 1785754899.1425803, "n": 20080, "h": "f754d01b32aea5ae", "c": "package model\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\t\"zbxtable/pkg/logger\"\n\t\"zbxtable/pkg/utils\"\n\n\tzabbix \"github.com/canghai908/zabbix-go\"\n\t_ \"github.com/go-sql-driver/mysql\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t_ \"github.com/lib/pq\"\n\tworkwx \"github.com/xen0n/go-workwx\"\n\tini \"gopkg.in/ini.v1\"\n\t\"gorm.io/gorm\"\n)\n\nvar (\n\tAPI = &zabbix.API{}\n\tjson = jsoniter.Config{\n\t\tEscapeHTML: false,\n\t\tSortMapKeys: true,\n\t\tValidateJsonRawMessage: true,\n\t}.Froze()\n\tZBX_VER string\n\tZBX_V bool\n\tVersion string\n\tGitHash string\n\tBuildTime string\n\tWeApp *workwx.WorkwxApp // 不初始化,默认为 nil\n\n\tenvConfig map[string]string\n\tenvConfigOnce sync.Once\n\tenvFileExists bool\n)\n\nconst (\n\t//ms-agent 常量定义\n\tMSName = \"ms-agent\"\n\tMSUser = \"ms-agent\"\n\tMSGroup = \"MS-Agent\"\n\tMSMedia = \"MS-Agent\"\n\tMSAction = \"MS-Agent\"\n\t//webhook 常量定义\n\tWebhookName = \"ZbxTable\"\n\tWebhookAction = \"ZbxTable Webhook\"\n\tWebhookGroup = \"ZbxTable Webhook\"\n\tWebhookUser = \"zbxtable-webhook\"\n\t//down\n\tDownloadPath = \"./download/\"\n\tTplPath = \"./assets/templates/\"\n\t// 默认用户头像 (空字符串,由前端处理默认头像)\n\tDefaultAvatar = \"\"\n\t//默认更新服务器\n\tUpdateURL = \"http://dl.cactifans.com/stable/\"\n\n\t// 上传目录\n\tUploadDir = \"./upload\"\n\t// 背景图片上传目录\n\tBackgroundImageDir = \"./upload/background\"\n\t// 允许的图片格式\n\tAllowedImageExts = \".jpg,.jpeg,.png,.gif,.bmp,.webp,.svg\"\n\t// 最大文件大小 (10MB)\n\tMaxFileSize = 10 * 1024 * 1024\n)\n\n// TableName 表名前缀\nfunc TableName(str string) string {\n\treturn fmt.Sprintf(\"%s%s\", \"zbxtable_\", str)\n}\n\n// 接收数据库信息初始化\nfunc ModelInit(dbtype, dbhost, dbuser, dbpass, dbname, dbport string) error {\n\t// 直接使用 GORM\n\trunmode := GetConfKey(\"runmode\")\n\terr := InitGormDB(dbtype, dbhost, dbuser, dbpass, dbname, dbport, runmode)\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to connect database: \", err)\n\t\treturn err\n\t}\n\tlogger.Log.Info(\"Database connected!\")\n\t// 自动迁移表\n\terr = AutoMigrate()\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to auto migrate: \", err)\n\t\treturn err\n\t}\n\t// 基础数据初始化\n\tif err := DatabaseInit(); err != nil {\n\t\tlogger.Log.Error(\"Database init failed: \", err)\n\t\treturn err\n\t}\n\t// 使用go-cache替代Redis(不再需要Redis连接)\n\tif err := InitCache(); err != nil {\n\t\tlogger.Log.Error(\"Cache init failed: \", err)\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc InitWechat() {\n\t// 首先检查企业微信开关是否启用\n\twechatEnabled := GetConfigValueByKey(\"wechat_enabled\", \"0\")\n\tif wechatEnabled != \"1\" {\n\t\tlogger.Log.Info(\"WeChat is disabled in system config, skipping WeChat initialization\")\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\t// 开关已启用,从数据库读取企业微信配置\n\tagentIDStr := GetConfigValueByKey(\"wechat_agentid\", \"\")\n\tcorpid := GetConfigValueByKey(\"wechat_corpid\", \"\")\n\tsecret := GetConfigValueByKey(\"wechat_secret\", \"\") // 这里会自动解密\n\n\t// 验证必填配置项\n\tif agentIDStr == \"\" || corpid == \"\" || secret == \"\" {\n\t\tlogger.Log.Info(\"WeChat is enabled but configuration is incomplete (agentid/corpid/secret), WeChat app will not be initialized\")\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\t// 解析 AgentID\n\tAgentId, err := strconv.ParseInt(agentIDStr, 10, 64)\n\tif err != nil {\n\t\tlogger.Log.Error(\"wechat_agentid parse error:\", err)\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\n\t// 初始化企业微信客户端\n\tclient := workwx.New(corpid)\n\tWeApp = client.WithApp(secret, AgentId)\n\tWeApp.SpawnAccessTokenRefresher()\n\tlogger.Log.Info(\"WeChat inited successfully!\")\n}\n\n// DatabaseInit 数据初始化\nfunc DatabaseInit() error {\n\t//数据初始化操作 - 使用 GORM\n\tvar v User\n\terr := DB.Where(\"username = ?\", \"admin\").First(&v).Error\n\t//检查权限\n\tif err == nil && v.Operation == \"\" {\n\t\terr := DB.Model(&User{}).Where(\"id = ?\", v.ID).Update(\"operation\", \"['add', 'edit', 'delete','update']\").Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn fmt.Errorf(\"update admin operation failed: %w\", err)\n\t\t}\n\t\tlogger.Log.Info(\"update admin operation successfully\")\n\t}\n\t//添加管理员账号\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\tlogger.Log.Info(\"the admin user does not exist, create a new admin account later!\")\n\n\t\t// 构建默认主题配置\n\t\tdefaultThemeConfig := map[string]interface{}{\n\t\t\t\"theme\": map[string]string{\n\t\t\t\t\"color\": \"#722ed1\",\n\t\t\t\t\"mode\": \"dark\",\n\t\t\t\t\"success\": \"#52c41a\",\n\t\t\t\t\"warning\": \"#faad14\",\n\t\t\t\t\"error\": \"#f5222f\",\n\t\t\t},\n\t\t\t\"animate\": map[string]interface{}{\n\t\t\t\t\"disabled\": true,\n\t\t\t\t\"name\": \"lightSpeed\",\n\t\t\t\t\"direction\": \"left\",\n\t\t\t},\n\t\t}\n\n\t\t// 将配置转换为 JSON 字符串\n\t\tthemeJSON, err := json.Marshal(defaultThemeConfig)\n\t\tif err != nil {\n\t\t\tlogger.Log.Error(\"Failed to marshal default theme config:\", err)\n\t\t\tthemeJSON = []byte(`{\"theme\":{\"color\":\"#722ed1\",\"mode\":\"dark\",\"success\":\"#52c41a\",\"warning\":\"#faad14\",\"error\":\"#f5222f\"},\"animate\":{\"disabled\":true,\"name\":\"lightSpeed\",\"direction\":\"left\"}}`)\n\t\t}\n\n\t\t// 使用结构体插入管理员账号\n\t\tpassword, _ := utils.PasswordHash(\"Zbxtable\")\n\t\tuser := User{\n\t\t\tUsername: \"admin\",\n\t\t\tPassword: password,\n\t\t\tAvatar: DefaultAvatar,\n\t\t\tRole: \"admin\",\n\t\t\tOperation: \"['add', 'edit', 'delete','update']\",\n\t\t\tTheme: string(themeJSON),\n\t\t\tStatus: 0,\n\t\t}\n\t\terr = DB.Create(&user).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn fmt.Errorf(\"create admin account failed: %w\", err)\n\t\t}\n\t\tlogger.Log.Info(\"create an administrator account successfully, the admin ID is:\", user.ID)\n\t}\n\t//初始化系统数据\n\tvar cnt []System\n\terr = DB.Find(&cnt).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn fmt.Errorf(\"query system data failed: %w\", err)\n\t}\n\tif len(cnt) == 0 {\n\t\tnow := time.Now()\n\t\tsys := []System{\n\t\t\t{Name: \"Linux操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"Windows操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"网络设备\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"物理服务器\", Status: 0, InitedAt: &now},\n\t\t}\n\t\terr := DB.Create(&sys).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init system info error!\", err)\n\t\t\treturn fmt.Errorf(\"init system data failed: %w\", err)\n\t\t}\n\t\tlogger.Log.Info(\"Init system data successfully!\")\n\t}\n\t//告警默认规则初始化 default rule\n\tvar rules []Rule\n\terr = DB.Where(\"m_type = ?\", \"2\").Find(&rules).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn fmt.Errorf(\"query default rules failed: %w\", err)\n\t}\n\tif len(rules) == 0 {\n\t\t// 使用 \"*\" 作为全局默认规则,匹配所有租户\n\t\tdefaultRule := []Rule{\n\t\t\t{\n\t\t\t\tName: \"全局默认规则\",\n\t\t\t\tZIDs: \"*\",\n\t\t\t\tMType: \"2\",\n\t\t\t\tChannel: \"wechat_robot\",\n\t\t\t\tUserIds: \"1\",\n\t\t\t\tSweek: \"0,1,2,3,4,5,6\",\n\t\t\t\tStime: \"00:00\",\n\t\t\t\tEtime: \"23.59\",\n\t\t\t\tStatus: \"0\"},\n\t\t}\n\t\terr := DB.Create(&defaultRule).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init default rule error!\", err)\n\t\t\treturn fmt.Errorf(\"init default rule failed: %w\", err)\n\t\t}\n\t\tlogger.Log.Info(\"Init default rule successfully!\")\n\t}\n\t// 初始化加密密钥(如果不存在)\n\tvar encryptionKeyConfig Config\n\terr = DB.Where(\"config_key = ?\", \"encryption_key\").First(&encryptionKeyConfig).Error\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t// 生成随机加密密钥\n\t\trandomKey := utils.GenerateRandomKey()\n\t\tencryptionKeyConfig = Config{\n\t\t\tName: \"加密密钥\",\n\t\t\tConfigKey: \"encryption_key\",\n\t\t\tConfigValue: randomKey,\n\t\t\tComment: \"用于加密存储敏感信息(如Zabbix密码和Token)的密钥,系统自动生成,不可修改\",\n\t\t}\n\t\tif insertErr := DB.Create(&encryptionKeyConfig).Error; insertErr != nil {\n\t\t\tlogger.Log.Error(\"Init encryption_key error:\", insertErr)\n\t\t\treturn fmt.Errorf(\"init encryption_key failed: %w\", insertErr)\n\t\t} else {\n\t\t\tlogger.Log.Info(\"Init encryption_key successfully! Key length:\", len(randomKey))\n\t\t}\n\t}\n\n\t// 初始化内置映射规则\n\t//InitDefaultMappingRules()\n\n\t// 获取结构化的默认配置\n\tdefaultConfigs := getDefaultConfigs()\n\n\tfor _, cfgItem := range defaultConfigs {\n\t\tvar existing Config\n\t\terr = DB.Where(\"config_key = ?\", cfgItem.ConfigKey).First(&existing).Error\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\tif insertErr := DB.Create(&cfgItem).Error; insertErr != nil {\n\t\t\t\tlogger.Log.Infof(\"Init config key %s error: %v\", cfgItem.ConfigKey, insertErr)\n\t\t\t\treturn fmt.Errorf(\"init config key %s failed: %w\", cfgItem.ConfigKey, insertErr)\n\t\t\t}\n\t\t\tlogger.Log.Infof(\"Init config key %s successfully!\", cfgItem.ConfigKey)\n\t\t} else if err != nil {\n\t\t\tlogger.Log.Infof(\"Init config key %s error: %v\", cfgItem.ConfigKey, err)\n\t\t\treturn fmt.Errorf(\"check config key %s failed: %w\", cfgItem.ConfigKey, err)\n\t\t}\n\t}\n\t//默认菜单初始化\n\tif err := InitMenuData(); err != nil {\n\t\treturn fmt.Errorf(\"init menu data failed: %w\", err)\n\t}\n\t//检查并添加缺失的菜单项(用于版本升级)\n\tif err := CheckAndAddMenus(); err != nil {\n\t\treturn fmt.Errorf(\"check and add menus failed: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc GetConfKey(v string) string {\n\t// 优先从 .env 中读取(便于开发环境配置敏感信息且不提交到 git)\n\tenvConfigOnce.Do(func() {\n\t\tfile, err := os.Open(\".env\")\n\t\tif err != nil {\n\t\t\t// 没有 .env 文件时直接跳过,后面回退到 app.conf\n\t\t\tenvFileExists = false\n\t\t\treturn\n\t\t}\n\t\tdefer file.Close()\n\n\t\tenvFileExists = true\n\t\tenvConfig = make(map[string]string)\n\n\t\tscanner := bufio.NewScanner(file)\n\t\tfor scanner.Scan() {\n\t\t\tline := strings.TrimSpace(scanner.Text())\n\t\t\tif line == \"\" || strings.HasPrefix(line, \"#\") || strings.HasPrefix(line, \";\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tparts := strings.SplitN(line, \"=\", 2)\n\t\t\tif len(parts) != 2 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tkey := strings.TrimSpace(parts[0])\n\t\t\tval := strings.TrimSpace(parts[1])\n\t\t\t// 去掉可能的引号\n\t\t\tval = strings.Trim(val, `\"'`)\n\t\t\tif key != \"\" {\n\t\t\t\tenvConfig[key] = val // 即使 val 为空也认为是有效配置\n\t\t\t}\n\t\t}\n\t})\n\n\t// 如果存在 .env 文件,则所有配置完全由 .env 决定:\n\t// - key 存在:返回对应值(可以是空字符串)\n\t// - key 不存在:返回空字符串\n\tif envFileExists {\n\t\tif envConfig == nil {\n\t\t\treturn \"\"\n\t\t}\n\t\tif val, ok := envConfig[v]; ok {\n\t\t\treturn val\n\t\t}\n\t\treturn \"\"\n\t}\n\t// 其次从 config/app.conf 读取\n\tcfg, err := ini.Load(\"./config/app.conf\")\n\tif err != nil {\n\t\tlogger.Log.Error(err)\n\t\treturn \"\"\n\t}\n\tp, err := cfg.Section(\"\").GetKey(v)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn p.String()\n}\n\n// IsPasswordConfigured 检查是否配置了密码(用于图形查看)\n// 优先检查数据库中是否有激活的 Zabbix 实例配置了用户名和密码\n// 注意:查看图形需要用户名和密码,Token 方式无法用于 Web 登录\nfunc IsPasswordConfigured() bool {\n\t// 1. 优先检查数据库中是否有激活的 Zabbix 实例\n\tvar activeInstance ZabbixInstance\n\terr := DB.Where(\"is_active = ?\", true).First(&activeInstance).Error\n\tif err == nil {\n\t\t// 找到激活的实例,检查是否配置了用户名和密码(查看图形必须用密码,Token 无法用于 Web 登录)\n\t\tif strings.TrimSpace(activeInstance.User) != \"\" && strings.TrimSpace(activeInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 2. 如果数据库中没有激活实例,检查是否有任何可用的实例配置了密码\n\tvar anyInstance ZabbixInstance\n\terr = DB.Where(\"enabled = ?\", true).First(&anyInstance).Error\n\tif err == nil {\n\t\t// 找到可用的实例,检查是否配置了用户名和密码\n\t\tif strings.TrimSpace(anyInstance.User) != \"\" && strings.TrimSpace(anyInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 3. 回退到配置文件检查(兼容旧版本)\n\tpass := GetConfKey(\"zabbix_pass\")\n\tuser := GetConfKey(\"zabbix_user\")\n\treturn pass != \"\" && user != \"\"\n}\n\n// GetEncryptionKey 获取加密密钥\n// 优先从数据库读取,如果数据库中没有则从环境变量读取,最后使用默认密钥\nfunc GetEncryptionKey() string {\n\t// 1. 优先从数据库读取\n\tkey := GetConfigValueByKey(\"encryption_key\", \"\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 2. 从环境变量或配置文件读取\n\tkey = GetConfKey(\"encryption_key\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 3. 使用默认密钥(不应该到这里,因为 DatabaseInit 会生成)\n\tlogger.Log.Warn(\"使用默认加密密钥,建议在数据库中配置 encryption_key\")\n\treturn \"zbxtable-default-encryption-key-2024\"\n}\n\n// getDefaultConfigs 返回系统默认配置列表\n// 配置按功能模块分组,便于维护和管理\nfunc getDefaultConfigs() []Config {\n\tconfigs := make([]Config, 0)\n\n\t// 系统外观配置\n\tconfigs = append(configs, getSystemAppearanceConfigs()...)\n\n\t// 初始配置状态\n\tconfigs = append(configs, getInitialSetupConfigs()...)\n\n\t// Dashboard 相关配置\n\tconfigs = append(configs, getDashboardConfigs()...)\n\n\t// 邮件配置\n\tconfigs = append(configs, getEmailConfigs()...)\n\n\t// 企业微信配置\n\tconfigs = append(configs, getWeChatConfigs()...)\n\n\t// AI 配置\n\tconfigs = append(configs, getAIConfigs()...)\n\n\treturn configs\n}\n\n// getSystemAppearanceConfigs 系统外观配置\nfunc getSystemAppearanceConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"系统名称\",\n\t\t\tConfigKey: \"system_name\",\n\t\t\tConfigValue: \"ZbxTable\",\n\t\t\tComment: \"系统显示的名称\",\n\t\t},\n\t\t{\n\t\t\tName: \"系统Logo\",\n\t\t\tConfigKey: \"system_logo\",\n\t\t\tConfigValue: \"data:image/png;base64,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\",\n\t\t\tComment: \"系统Logo的base64编码数据\",\n\t\t},\n\t}\n}\n\n// getInitialSetupConfigs 初始配置状态\nfunc getInitialSetupConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"初始配置完成\",\n\t\t\tConfigKey: \"initial_setup_completed\",\n\t\t\tConfigValue: \"0\",\n\t\t\tComment: \"标记系统是否完成初始配置:1 已完成,0 未完成\",\n\t\t},\n\t}\n}\n\n// getDashboardConfigs Dashboard 相关配置\nfunc getDashboardConfigs() []Config {\n\treturn []Config{\n\t\t// {\n\t\t// \tName: \"数据面板\",\n\t\t// \tConfigKey: \"zbx_dash\",\n\t\t// \tConfigValue: \"0\",\n\t\t// \tComment: \"是否开启Zabbix看板:1 开启,0 关闭\",\n\t\t// },\n\t\t// {\n\t\t// \tName: \"面板配置\",\n\t\t// \tConfigKey: \"dash_id\",\n\t\t// \tConfigValue: \"1\",\n\t\t// \tComment: \"需要引入的Zabbix面板的ID,默认为1\",\n\t\t// },\n\t\t{\n\t\t\tName: \"Linux Top数量\",\n\t\t\tConfigKey: \"dash_top_lin_num\",\n\t\t\tConfigValue: \"10\",\n\t\t\tComment: \"首页 Linux Top 列表展示数量\",\n\t\t},\n\t\t{\n\t\t\tName: \"Windows Top数量\",\n\t\t\tConfigKey: \"dash_top_win_num\",\n\t\t\tConfigValue: \"10\",\n\t\t\tComment: \"首页 Windows Top 列表展示数量\",\n\t\t},\n\t\t{\n\t\t\tName: \"主机分类同步\",\n\t\t\tConfigKey: \"sync_inventory\",\n\t\t\tConfigValue: \"1\",\n\t\t\tComment: \"主机分类同步计划任务是否启用:1 启用,0 不启用\",\n\t\t},\n\t\t{\n\t\t\tName: \"Webhook回调地址\",\n\t\t\tConfigKey: \"webhook_url\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"webhook通知地址\",\n\t\t},\n\t}\n}\n\n// getEmailConfigs 邮件配置\nfunc getEmailConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"邮件发件人\",\n\t\t\tConfigKey: \"email_from\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"告警邮件发件人邮箱地址\",\n\t\t},\n\t\t{\n\t\t\tName: \"邮件昵称\",\n\t\t\tConfigKey: \"email_nickname\",\n\t\t\tConfigValue: \"ZbxTable\",\n\t\t\tComment: \"告警邮件显示的发件人昵称\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 密码/授权码\",\n\t\t\tConfigKey: \"email_secret\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"SMTP 登录密码或授权码\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 服务器\",\n\t\t\tConfigKey: \"email_host\",\n\t\t\tConfigValue: \"smtp.qq.com\",\n\t\t\tComment: \"SMTP 服务器地址\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 端口\",\n\t\t\tConfigKey: \"email_port\",\n\t\t\tConfigValue: \"465\",\n\t\t\tComment: \"SMTP 端口号\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 使用 SSL\",\n\t\t\tConfigKey: \"email_isSSl\",\n\t\t\tConfigValue: \"true\",\n\t\t\tComment: \"是否启用 SSL:true/false\",\n\t\t},\n\t}\n}\n\n// getWeChatConfigs 企业微信配置\nfunc getWeChatConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"企业微信开关\",\n\t\t\tConfigKey: \"wechat_enabled\",\n\t\t\tConfigValue: \"0\",\n\t\t\tComment: \"是否启用企业微信:1 启用,0 禁用\",\n\t\t},\n\t\t{\n\t\t\tName: \"企业微信 AgentID\",\n\t\t\tConfigKey: \"wechat_agentid\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"企业微信应用的 AgentID\",\n\t\t},\n\t\t{\n\t\t\tName: \"企业微信 CorpID\",\n\t\t\tConfigKey: \"wechat_corpid\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"企业微信企业ID\",\n\t\t},\n\t\t{\n\t\t\tName: \"企业微信 Secret\",\n\t\t\tConfigKey: \"wechat_secret\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"企业微信应用的 Secret\",\n\t\t},\n\t}\n}\n\n// getAIConfigs AI 配置\nfunc getAIConfigs() []Config {\n\treturn []Config{\n\t\t// AI 类型选择\n\t\t{\n\t\t\tName: \"AI 类型\",\n\t\t\tConfigKey: \"ai_type\",\n\t\t\tConfigValue: \"ollama\",\n\t\t\tComment: \"选择使用的 AI 服务类型:ollama 或 deepseek\",\n\t\t},\n\t\t// Ollama 配置\n\t\t{\n\t\t\tName: \"Ollama Host\",\n\t\t\tConfigKey: \"ollama_host\",\n\t\t\tConfigValue: \"http://localhost:11434\",\n\t\t\tComment: \"Ollama 服务地址,如 http://127.0.0.1:11434\",\n\t\t},\n\t\t{\n\t\t\tName: \"Ollama Model\",\n\t\t\tConfigKey: \"ollama_model\",\n\t\t\tConfigValue: \"deepseek-r1:32b\",\n\t\t\tComment: \"默认使用的大模型名称\",\n\t\t},\n\t\t// Deepseek 配置\n\t\t{\n\t\t\tName: \"Deepseek API Key\",\n\t\t\tConfigKey: \"deepseek_api_key\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"Deepseek API 密钥\",\n\t\t},\n\t\t{\n\t\t\tName: \"Deepseek Model\",\n\t\t\tConfigKey: \"deepseek_model\",\n\t\t\tConfigValue: \"deepseek-chat\",\n\t\t\tComment: \"Deepseek 模型名称,如 deepseek-chat\",\n\t\t},\n\t\t{\n\t\t\tName: \"Deepseek Base URL\",\n\t\t\tConfigKey: \"deepseek_base_url\",\n\t\t\tConfigValue: \"https://api.deepseek.com\",\n\t\t\tComment: \"Deepseek API 地址,默认为 https://api.deepseek.com\",\n\t\t},\n\t}\n}\n"}, "202c377b8d78138fb1f154d01074e419848a19e1": {"t": 1785754901.224094, "n": 19921, "h": "88ed14e829bea80a", "c": "package model\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\t\"zbxtable/pkg/logger\"\n\t\"zbxtable/pkg/utils\"\n\n\tzabbix \"github.com/canghai908/zabbix-go\"\n\t_ \"github.com/go-sql-driver/mysql\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t_ \"github.com/lib/pq\"\n\tworkwx \"github.com/xen0n/go-workwx\"\n\tini \"gopkg.in/ini.v1\"\n\t\"gorm.io/gorm\"\n)\n\nvar (\n\tAPI = &zabbix.API{}\n\tjson = jsoniter.Config{\n\t\tEscapeHTML: false,\n\t\tSortMapKeys: true,\n\t\tValidateJsonRawMessage: true,\n\t}.Froze()\n\tZBX_VER string\n\tZBX_V bool\n\tVersion string\n\tGitHash string\n\tBuildTime string\n\tWeApp *workwx.WorkwxApp // 不初始化,默认为 nil\n\n\tenvConfig map[string]string\n\tenvConfigOnce sync.Once\n\tenvFileExists bool\n)\n\nconst (\n\t//ms-agent 常量定义\n\tMSName = \"ms-agent\"\n\tMSUser = \"ms-agent\"\n\tMSGroup = \"MS-Agent\"\n\tMSMedia = \"MS-Agent\"\n\tMSAction = \"MS-Agent\"\n\t//webhook 常量定义\n\tWebhookName = \"ZbxTable\"\n\tWebhookAction = \"ZbxTable Webhook\"\n\tWebhookGroup = \"ZbxTable Webhook\"\n\tWebhookUser = \"zbxtable-webhook\"\n\t//down\n\tDownloadPath = \"./download/\"\n\tTplPath = \"./assets/templates/\"\n\t// 默认用户头像 (空字符串,由前端处理默认头像)\n\tDefaultAvatar = \"\"\n\t//默认更新服务器\n\tUpdateURL = \"http://dl.cactifans.com/stable/\"\n\n\t// 上传目录\n\tUploadDir = \"./upload\"\n\t// 背景图片上传目录\n\tBackgroundImageDir = \"./upload/background\"\n\t// 允许的图片格式\n\tAllowedImageExts = \".jpg,.jpeg,.png,.gif,.bmp,.webp,.svg\"\n\t// 最大文件大小 (10MB)\n\tMaxFileSize = 10 * 1024 * 1024\n)\n\n// TableName 表名前缀\nfunc TableName(str string) string {\n\treturn fmt.Sprintf(\"%s%s\", \"zbxtable_\", str)\n}\n\n// 接收数据库信息初始化\nfunc ModelInit(dbtype, dbhost, dbuser, dbpass, dbname, dbport string) error {\n\t// 直接使用 GORM\n\trunmode := GetConfKey(\"runmode\")\n\terr := InitGormDB(dbtype, dbhost, dbuser, dbpass, dbname, dbport, runmode)\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to connect database: \", err)\n\t\treturn err\n\t}\n\tlogger.Log.Info(\"Database connected!\")\n\t// 自动迁移表\n\terr = AutoMigrate()\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to auto migrate: \", err)\n\t\treturn err\n\t}\n\t// 基础数据初始化\n\tif err := DatabaseInit(); err != nil {\n\t\tlogger.Log.Error(\"Database init failed: \", err)\n\t\treturn err\n\t}\n\t// 使用go-cache替代Redis(不再需要Redis连接)\n\tif err := InitCache(); err != nil {\n\t\tlogger.Log.Error(\"Cache init failed: \", err)\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc InitWechat() {\n\t// 首先检查企业微信开关是否启用\n\twechatEnabled := GetConfigValueByKey(\"wechat_enabled\", \"0\")\n\tif wechatEnabled != \"1\" {\n\t\tlogger.Log.Info(\"WeChat is disabled in system config, skipping WeChat initialization\")\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\t// 开关已启用,从数据库读取企业微信配置\n\tagentIDStr := GetConfigValueByKey(\"wechat_agentid\", \"\")\n\tcorpid := GetConfigValueByKey(\"wechat_corpid\", \"\")\n\tsecret := GetConfigValueByKey(\"wechat_secret\", \"\") // 这里会自动解密\n\n\t// 验证必填配置项\n\tif agentIDStr == \"\" || corpid == \"\" || secret == \"\" {\n\t\tlogger.Log.Info(\"WeChat is enabled but configuration is incomplete (agentid/corpid/secret), WeChat app will not be initialized\")\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\t// 解析 AgentID\n\tAgentId, err := strconv.ParseInt(agentIDStr, 10, 64)\n\tif err != nil {\n\t\tlogger.Log.Error(\"wechat_agentid parse error:\", err)\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\n\t// 初始化企业微信客户端\n\tclient := workwx.New(corpid)\n\tWeApp = client.WithApp(secret, AgentId)\n\tWeApp.SpawnAccessTokenRefresher()\n\tlogger.Log.Info(\"WeChat inited successfully!\")\n}\n\n// DatabaseInit 数据初始化\nfunc DatabaseInit() error {\n\t//数据初始化操作 - 使用 GORM\n\tvar v User\n\terr := DB.Where(\"username = ?\", \"admin\").First(&v).Error\n\t//检查权限\n\tif err == nil && v.Operation == \"\" {\n\t\terr := DB.Model(&User{}).Where(\"id = ?\", v.ID).Update(\"operation\", \"['add', 'edit', 'delete','update']\").Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn fmt.Errorf(\"update admin operation failed: %w\", err)\n\t\t}\n\t\tlogger.Log.Info(\"update admin operation successfully\")\n\t}\n\t//添加管理员账号\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\tlogger.Log.Info(\"the admin user does not exist, create a new admin account later!\")\n\n\t\t// 构建默认主题配置\n\t\tdefaultThemeConfig := map[string]interface{}{\n\t\t\t\"theme\": map[string]string{\n\t\t\t\t\"color\": \"#722ed1\",\n\t\t\t\t\"mode\": \"dark\",\n\t\t\t\t\"success\": \"#52c41a\",\n\t\t\t\t\"warning\": \"#faad14\",\n\t\t\t\t\"error\": \"#f5222f\",\n\t\t\t},\n\t\t\t\"animate\": map[string]interface{}{\n\t\t\t\t\"disabled\": true,\n\t\t\t\t\"name\": \"lightSpeed\",\n\t\t\t\t\"direction\": \"left\",\n\t\t\t},\n\t\t}\n\n\t\t// 将配置转换为 JSON 字符串\n\t\tthemeJSON, err := json.Marshal(defaultThemeConfig)\n\t\tif err != nil {\n\t\t\tlogger.Log.Error(\"Failed to marshal default theme config:\", err)\n\t\t\tthemeJSON = []byte(`{\"theme\":{\"color\":\"#722ed1\",\"mode\":\"dark\",\"success\":\"#52c41a\",\"warning\":\"#faad14\",\"error\":\"#f5222f\"},\"animate\":{\"disabled\":true,\"name\":\"lightSpeed\",\"direction\":\"left\"}}`)\n\t\t}\n\n\t\t// 使用结构体插入管理员账号\n\t\tpassword, _ := utils.PasswordHash(\"Zbxtable\")\n\t\tuser := User{\n\t\t\tUsername: \"admin\",\n\t\t\tPassword: password,\n\t\t\tAvatar: DefaultAvatar,\n\t\t\tRole: \"admin\",\n\t\t\tOperation: \"['add', 'edit', 'delete','update']\",\n\t\t\tTheme: string(themeJSON),\n\t\t\tStatus: 0,\n\t\t}\n\t\terr = DB.Create(&user).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn fmt.Errorf(\"create admin account failed: %w\", err)\n\t\t}\n\t\tlogger.Log.Info(\"create an administrator account successfully, the admin ID is:\", user.ID)\n\t}\n\t//初始化系统数据\n\tvar cnt []System\n\terr = DB.Find(&cnt).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn fmt.Errorf(\"query system data failed: %w\", err)\n\t}\n\tif len(cnt) == 0 {\n\t\tnow := time.Now()\n\t\tsys := []System{\n\t\t\t{Name: \"Linux操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"Windows操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"网络设备\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"物理服务器\", Status: 0, InitedAt: &now},\n\t\t}\n\t\terr := DB.Create(&sys).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init system info error!\", err)\n\t\t\treturn fmt.Errorf(\"init system data failed: %w\", err)\n\t\t}\n\t\tlogger.Log.Info(\"Init system data successfully!\")\n\t}\n\t//告警默认规则初始化 default rule\n\tvar rules []Rule\n\terr = DB.Where(\"m_type = ?\", \"2\").Find(&rules).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn fmt.Errorf(\"query default rules failed: %w\", err)\n\t}\n\tif len(rules) == 0 {\n\t\t// 使用 \"*\" 作为全局默认规则,匹配所有租户\n\t\tdefaultRule := []Rule{\n\t\t\t{\n\t\t\t\tName: \"全局默认规则\",\n\t\t\t\tZIDs: \"*\",\n\t\t\t\tMType: \"2\",\n\t\t\t\tChannel: \"wechat_robot\",\n\t\t\t\tUserIds: \"1\",\n\t\t\t\tSweek: \"0,1,2,3,4,5,6\",\n\t\t\t\tStime: \"00:00\",\n\t\t\t\tEtime: \"23.59\",\n\t\t\t\tStatus: \"0\"},\n\t\t}\n\t\terr := DB.Create(&defaultRule).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init default rule error!\", err)\n\t\t\treturn fmt.Errorf(\"init default rule failed: %w\", err)\n\t\t}\n\t\tlogger.Log.Info(\"Init default rule successfully!\")\n\t}\n\t// 初始化加密密钥(如果不存在)\n\tvar encryptionKeyConfig Config\n\terr = DB.Where(\"config_key = ?\", \"encryption_key\").First(&encryptionKeyConfig).Error\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t// 生成随机加密密钥\n\t\trandomKey := utils.GenerateRandomKey()\n\t\tencryptionKeyConfig = Config{\n\t\t\tName: \"加密密钥\",\n\t\t\tConfigKey: \"encryption_key\",\n\t\t\tConfigValue: randomKey,\n\t\t\tComment: \"用于加密存储敏感信息(如Zabbix密码和Token)的密钥,系统自动生成,不可修改\",\n\t\t}\n\t\tif insertErr := DB.Create(&encryptionKeyConfig).Error; insertErr != nil {\n\t\t\tlogger.Log.Error(\"Init encryption_key error:\", insertErr)\n\t\t\treturn fmt.Errorf(\"init encryption_key failed: %w\", insertErr)\n\t\t} else {\n\t\t\tlogger.Log.Info(\"Init encryption_key successfully! Key length:\", len(randomKey))\n\t\t}\n\t}\n\n\t// 初始化内置映射规则\n\t//InitDefaultMappingRules()\n\n\t// 获取结构化的默认配置\n\tdefaultConfigs := getDefaultConfigs()\n\n\tfor _, cfgItem := range defaultConfigs {\n\t\tvar existing Config\n\t\terr = DB.Where(\"config_key = ?\", cfgItem.ConfigKey).First(&existing).Error\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\tif insertErr := DB.Create(&cfgItem).Error; insertErr != nil {\n\t\t\t\tlogger.Log.Infof(\"Init config key %s error: %v\", cfgItem.ConfigKey, insertErr)\n\t\t\t\treturn fmt.Errorf(\"init config key %s failed: %w\", cfgItem.ConfigKey, insertErr)\n\t\t\t}\n\t\t\tlogger.Log.Infof(\"Init config key %s successfully!\", cfgItem.ConfigKey)\n\t\t} else if err != nil {\n\t\t\tlogger.Log.Infof(\"Init config key %s error: %v\", cfgItem.ConfigKey, err)\n\t\t\treturn fmt.Errorf(\"check config key %s failed: %w\", cfgItem.ConfigKey, err)\n\t\t}\n\t}\n\t//默认菜单初始化\n\tif err := InitMenuData(); err != nil {\n\t\treturn fmt.Errorf(\"init menu data failed: %w\", err)\n\t}\n\t//检查并添加缺失的菜单项(用于版本升级)\n\tif err := CheckAndAddMenus(); err != nil {\n\t\treturn fmt.Errorf(\"check and add menus failed: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc GetConfKey(v string) string {\n\t// 优先从 .env 中读取(便于开发环境配置敏感信息且不提交到 git)\n\tenvConfigOnce.Do(func() {\n\t\tfile, err := os.Open(\".env\")\n\t\tif err != nil {\n\t\t\t// 没有 .env 文件时直接跳过,后面回退到 app.conf\n\t\t\tenvFileExists = false\n\t\t\treturn\n\t\t}\n\t\tdefer file.Close()\n\n\t\tenvFileExists = true\n\t\tenvConfig = make(map[string]string)\n\n\t\tscanner := bufio.NewScanner(file)\n\t\tfor scanner.Scan() {\n\t\t\tline := strings.TrimSpace(scanner.Text())\n\t\t\tif line == \"\" || strings.HasPrefix(line, \"#\") || strings.HasPrefix(line, \";\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tparts := strings.SplitN(line, \"=\", 2)\n\t\t\tif len(parts) != 2 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tkey := strings.TrimSpace(parts[0])\n\t\t\tval := strings.TrimSpace(parts[1])\n\t\t\t// 去掉可能的引号\n\t\t\tval = strings.Trim(val, `\"'`)\n\t\t\tif key != \"\" {\n\t\t\t\tenvConfig[key] = val // 即使 val 为空也认为是有效配置\n\t\t\t}\n\t\t}\n\t})\n\n\t// 如果存在 .env 文件,则所有配置完全由 .env 决定:\n\t// - key 存在:返回对应值(可以是空字符串)\n\t// - key 不存在:返回空字符串\n\tif envFileExists {\n\t\tif envConfig == nil {\n\t\t\treturn \"\"\n\t\t}\n\t\tif val, ok := envConfig[v]; ok {\n\t\t\treturn val\n\t\t}\n\t\treturn \"\"\n\t}\n\t// 其次从 config/app.conf 读取\n\tcfg, err := ini.Load(\"./config/app.conf\")\n\tif err != nil {\n\t\tlogger.Log.Error(err)\n\t\treturn \"\"\n\t}\n\tp, err := cfg.Section(\"\").GetKey(v)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn p.String()\n}\n\n// IsPasswordConfigured 检查是否配置了密码(用于图形查看)\n// 优先检查数据库中是否有激活的 Zabbix 实例配置了用户名和密码\n// 注意:查看图形需要用户名和密码,Token 方式无法用于 Web 登录\nfunc IsPasswordConfigured() bool {\n\t// 1. 优先检查数据库中是否有激活的 Zabbix 实例\n\tvar activeInstance ZabbixInstance\n\terr := DB.Where(\"is_active = ?\", true).First(&activeInstance).Error\n\tif err == nil {\n\t\t// 找到激活的实例,检查是否配置了用户名和密码(查看图形必须用密码,Token 无法用于 Web 登录)\n\t\tif strings.TrimSpace(activeInstance.User) != \"\" && strings.TrimSpace(activeInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 2. 如果数据库中没有激活实例,检查是否有任何可用的实例配置了密码\n\tvar anyInstance ZabbixInstance\n\terr = DB.Where(\"enabled = ?\", true).First(&anyInstance).Error\n\tif err == nil {\n\t\t// 找到可用的实例,检查是否配置了用户名和密码\n\t\tif strings.TrimSpace(anyInstance.User) != \"\" && strings.TrimSpace(anyInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 3. 回退到配置文件检查(兼容旧版本)\n\tpass := GetConfKey(\"zabbix_pass\")\n\tuser := GetConfKey(\"zabbix_user\")\n\treturn pass != \"\" && user != \"\"\n}\n\n// GetEncryptionKey 获取加密密钥\n// 优先从数据库读取,如果数据库中没有则从环境变量读取,最后使用默认密钥\nfunc GetEncryptionKey() string {\n\t// 1. 优先从数据库读取\n\tkey := GetConfigValueByKey(\"encryption_key\", \"\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 2. 从环境变量或配置文件读取\n\tkey = GetConfKey(\"encryption_key\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 3. 使用默认密钥(不应该到这里,因为 DatabaseInit 会生成)\n\tlogger.Log.Warn(\"使用默认加密密钥,建议在数据库中配置 encryption_key\")\n\treturn \"zbxtable-default-encryption-key-2024\"\n}\n\n// getDefaultConfigs 返回系统默认配置列表\n// 配置按功能模块分组,便于维护和管理\nfunc getDefaultConfigs() []Config {\n\tconfigs := make([]Config, 0)\n\n\t// 系统外观配置\n\tconfigs = append(configs, getSystemAppearanceConfigs()...)\n\n\t// 初始配置状态\n\tconfigs = append(configs, getInitialSetupConfigs()...)\n\n\t// Dashboard 相关配置\n\tconfigs = append(configs, getDashboardConfigs()...)\n\n\t// 邮件配置\n\tconfigs = append(configs, getEmailConfigs()...)\n\n\t// 企业微信配置\n\tconfigs = append(configs, getWeChatConfigs()...)\n\n\t// AI 配置\n\tconfigs = append(configs, getAIConfigs()...)\n\n\treturn configs\n}\n\n// getSystemAppearanceConfigs 系统外观配置\nfunc getSystemAppearanceConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"系统名称\",\n\t\t\tConfigKey: \"system_name\",\n\t\t\tConfigValue: \"ZbxTable\",\n\t\t\tComment: \"系统显示的名称\",\n\t\t},\n\t\t{\n\t\t\tName: \"系统Logo\",\n\t\t\tConfigKey: \"system_logo\",\n\t\t\tConfigValue: \"data:image/png;base64,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\",\n\t\t\tComment: \"系统Logo的base64编码数据\",\n\t\t},\n\t}\n}\n\n// getInitialSetupConfigs 初始配置状态\nfunc getInitialSetupConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"初始配置完成\",\n\t\t\tConfigKey: \"initial_setup_completed\",\n\t\t\tConfigValue: \"0\",\n\t\t\tComment: \"标记系统是否完成初始配置:1 已完成,0 未完成\",\n\t\t},\n\t}\n}\n\n// getDashboardConfigs Dashboard 相关配置\nfunc getDashboardConfigs() []Config {\n\treturn []Config{\n\t\t// {\n\t\t// \tName: \"数据面板\",\n\t\t// \tConfigKey: \"zbx_dash\",\n\t\t// \tConfigValue: \"0\",\n\t\t// \tComment: \"是否开启Zabbix看板:1 开启,0 关闭\",\n\t\t// },\n\t\t// {\n\t\t// \tName: \"面板配置\",\n\t\t// \tConfigKey: \"dash_id\",\n\t\t// \tConfigValue: \"1\",\n\t\t// \tComment: \"需要引入的Zabbix面板的ID,默认为1\",\n\t\t// },\n\t\t{\n\t\t\tName: \"Top数量\",\n\t\t\tConfigKey: \"dash_top_num\",\n\t\t\tConfigValue: \"10\",\n\t\t\tComment: \"首页Top列表展示数量\",\n\t\t},\n\t\t{\n\t\t\tName: \"主机分类同步\",\n\t\t\tConfigKey: \"sync_inventory\",\n\t\t\tConfigValue: \"1\",\n\t\t\tComment: \"主机分类同步计划任务是否启用:1 启用,0 不启用\",\n\t\t},\n\t\t{\n\t\t\tName: \"Webhook回调地址\",\n\t\t\tConfigKey: \"webhook_url\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"webhook通知地址\",\n\t\t},\n\t}\n}\n\n// getEmailConfigs 邮件配置\nfunc getEmailConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"邮件发件人\",\n\t\t\tConfigKey: \"email_from\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"告警邮件发件人邮箱地址\",\n\t\t},\n\t\t{\n\t\t\tName: \"邮件昵称\",\n\t\t\tConfigKey: \"email_nickname\",\n\t\t\tConfigValue: \"ZbxTable\",\n\t\t\tComment: \"告警邮件显示的发件人昵称\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 密码/授权码\",\n\t\t\tConfigKey: \"email_secret\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"SMTP 登录密码或授权码\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 服务器\",\n\t\t\tConfigKey: \"email_host\",\n\t\t\tConfigValue: \"smtp.qq.com\",\n\t\t\tComment: \"SMTP 服务器地址\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 端口\",\n\t\t\tConfigKey: \"email_port\",\n\t\t\tConfigValue: \"465\",\n\t\t\tComment: \"SMTP 端口号\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 使用 SSL\",\n\t\t\tConfigKey: \"email_isSSl\",\n\t\t\tConfigValue: \"true\",\n\t\t\tComment: \"是否启用 SSL:true/false\",\n\t\t},\n\t}\n}\n\n// getWeChatConfigs 企业微信配置\nfunc getWeChatConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"企业微信开关\",\n\t\t\tConfigKey: \"wechat_enabled\",\n\t\t\tConfigValue: \"0\",\n\t\t\tComment: \"是否启用企业微信:1 启用,0 禁用\",\n\t\t},\n\t\t{\n\t\t\tName: \"企业微信 AgentID\",\n\t\t\tConfigKey: \"wechat_agentid\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"企业微信应用的 AgentID\",\n\t\t},\n\t\t{\n\t\t\tName: \"企业微信 CorpID\",\n\t\t\tConfigKey: \"wechat_corpid\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"企业微信企业ID\",\n\t\t},\n\t\t{\n\t\t\tName: \"企业微信 Secret\",\n\t\t\tConfigKey: \"wechat_secret\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"企业微信应用的 Secret\",\n\t\t},\n\t}\n}\n\n// getAIConfigs AI 配置\nfunc getAIConfigs() []Config {\n\treturn []Config{\n\t\t// AI 类型选择\n\t\t{\n\t\t\tName: \"AI 类型\",\n\t\t\tConfigKey: \"ai_type\",\n\t\t\tConfigValue: \"ollama\",\n\t\t\tComment: \"选择使用的 AI 服务类型:ollama 或 deepseek\",\n\t\t},\n\t\t// Ollama 配置\n\t\t{\n\t\t\tName: \"Ollama Host\",\n\t\t\tConfigKey: \"ollama_host\",\n\t\t\tConfigValue: \"http://localhost:11434\",\n\t\t\tComment: \"Ollama 服务地址,如 http://127.0.0.1:11434\",\n\t\t},\n\t\t{\n\t\t\tName: \"Ollama Model\",\n\t\t\tConfigKey: \"ollama_model\",\n\t\t\tConfigValue: \"deepseek-r1:32b\",\n\t\t\tComment: \"默认使用的大模型名称\",\n\t\t},\n\t\t// Deepseek 配置\n\t\t{\n\t\t\tName: \"Deepseek API Key\",\n\t\t\tConfigKey: \"deepseek_api_key\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"Deepseek API 密钥\",\n\t\t},\n\t\t{\n\t\t\tName: \"Deepseek Model\",\n\t\t\tConfigKey: \"deepseek_model\",\n\t\t\tConfigValue: \"deepseek-chat\",\n\t\t\tComment: \"Deepseek 模型名称,如 deepseek-chat\",\n\t\t},\n\t\t{\n\t\t\tName: \"Deepseek Base URL\",\n\t\t\tConfigKey: \"deepseek_base_url\",\n\t\t\tConfigValue: \"https://api.deepseek.com\",\n\t\t\tComment: \"Deepseek API 地址,默认为 https://api.deepseek.com\",\n\t\t},\n\t}\n}\n"}, "7c9ef6c7b6560a948b134a734de23c5e91a3ed59": {"t": 1785754902.7867923, "n": 19025, "h": "bc60303d6fea1d12", "c": "package model\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\t\"zbxtable/pkg/logger\"\n\t\"zbxtable/pkg/utils\"\n\n\tzabbix \"github.com/canghai908/zabbix-go\"\n\t_ \"github.com/go-sql-driver/mysql\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t_ \"github.com/lib/pq\"\n\tworkwx \"github.com/xen0n/go-workwx\"\n\tini \"gopkg.in/ini.v1\"\n\t\"gorm.io/gorm\"\n)\n\nvar (\n\tAPI = &zabbix.API{}\n\tjson = jsoniter.Config{\n\t\tEscapeHTML: false,\n\t\tSortMapKeys: true,\n\t\tValidateJsonRawMessage: true,\n\t}.Froze()\n\tZBX_VER string\n\tZBX_V bool\n\tVersion string\n\tGitHash string\n\tBuildTime string\n\tWeApp *workwx.WorkwxApp // 不初始化,默认为 nil\n\n\tenvConfig map[string]string\n\tenvConfigOnce sync.Once\n\tenvFileExists bool\n)\n\nconst (\n\t//ms-agent 常量定义\n\tMSName = \"ms-agent\"\n\tMSUser = \"ms-agent\"\n\tMSGroup = \"MS-Agent\"\n\tMSMedia = \"MS-Agent\"\n\tMSAction = \"MS-Agent\"\n\t//webhook 常量定义\n\tWebhookName = \"ZbxTable\"\n\tWebhookAction = \"ZbxTable Webhook\"\n\tWebhookGroup = \"ZbxTable Webhook\"\n\tWebhookUser = \"zbxtable-webhook\"\n\t//down\n\tDownloadPath = \"./download/\"\n\tTplPath = \"./assets/templates/\"\n\t// 默认用户头像 (空字符串,由前端处理默认头像)\n\tDefaultAvatar = \"\"\n\t//默认更新服务器\n\tUpdateURL = \"http://dl.cactifans.com/stable/\"\n\n\t// 上传目录\n\tUploadDir = \"./upload\"\n\t// 背景图片上传目录\n\tBackgroundImageDir = \"./upload/background\"\n\t// 允许的图片格式\n\tAllowedImageExts = \".jpg,.jpeg,.png,.gif,.bmp,.webp,.svg\"\n\t// 最大文件大小 (10MB)\n\tMaxFileSize = 10 * 1024 * 1024\n)\n\n// TableName 表名前缀\nfunc TableName(str string) string {\n\treturn fmt.Sprintf(\"%s%s\", \"zbxtable_\", str)\n}\n\n// 接收数据库信息初始化\nfunc ModelInit(dbtype, dbhost, dbuser, dbpass, dbname, dbport string) {\n\t// 直接使用 GORM\n\trunmode := GetConfKey(\"runmode\")\n\terr := InitGormDB(dbtype, dbhost, dbuser, dbpass, dbname, dbport, runmode)\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to connect database: \", err)\n\t\tos.Exit(1)\n\t}\n\tlogger.Log.Info(\"Database connected!\")\n\t// 自动迁移表\n\terr = AutoMigrate()\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to auto migrate: \", err)\n\t\tos.Exit(1)\n\t}\n\t// 基础数据初始化\n\tDatabaseInit()\n\t// 使用go-cache替代Redis(不再需要Redis连接)\n\tInitCache()\n}\n\nfunc InitWechat() {\n\t// 首先检查企业微信开关是否启用\n\twechatEnabled := GetConfigValueByKey(\"wechat_enabled\", \"0\")\n\tif wechatEnabled != \"1\" {\n\t\tlogger.Log.Info(\"WeChat is disabled in system config, skipping WeChat initialization\")\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\t// 开关已启用,从数据库读取企业微信配置\n\tagentIDStr := GetConfigValueByKey(\"wechat_agentid\", \"\")\n\tcorpid := GetConfigValueByKey(\"wechat_corpid\", \"\")\n\tsecret := GetConfigValueByKey(\"wechat_secret\", \"\") // 这里会自动解密\n\n\t// 验证必填配置项\n\tif agentIDStr == \"\" || corpid == \"\" || secret == \"\" {\n\t\tlogger.Log.Info(\"WeChat is enabled but configuration is incomplete (agentid/corpid/secret), WeChat app will not be initialized\")\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\t// 解析 AgentID\n\tAgentId, err := strconv.ParseInt(agentIDStr, 10, 64)\n\tif err != nil {\n\t\tlogger.Log.Error(\"wechat_agentid parse error:\", err)\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\n\t// 初始化企业微信客户端\n\tclient := workwx.New(corpid)\n\tWeApp = client.WithApp(secret, AgentId)\n\tWeApp.SpawnAccessTokenRefresher()\n\tlogger.Log.Info(\"WeChat inited successfully!\")\n}\n\n// DatabaseInit 数据初始化\nfunc DatabaseInit() {\n\t//数据初始化操作 - 使用 GORM\n\tvar v User\n\terr := DB.Where(\"username = ?\", \"admin\").First(&v).Error\n\t//检查权限\n\tif err == nil && v.Operation == \"\" {\n\t\terr := DB.Model(&User{}).Where(\"id = ?\", v.ID).Update(\"operation\", \"['add', 'edit', 'delete','update']\").Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"update admin operation successfully\")\n\t}\n\t//添加管理员账号\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\tlogger.Log.Info(\"the admin user does not exist, create a new admin account later!\")\n\n\t\t// 构建默认主题配置\n\t\tdefaultThemeConfig := map[string]interface{}{\n\t\t\t\"theme\": map[string]string{\n\t\t\t\t\"color\": \"#722ed1\",\n\t\t\t\t\"mode\": \"dark\",\n\t\t\t\t\"success\": \"#52c41a\",\n\t\t\t\t\"warning\": \"#faad14\",\n\t\t\t\t\"error\": \"#f5222f\",\n\t\t\t},\n\t\t\t\"animate\": map[string]interface{}{\n\t\t\t\t\"disabled\": true,\n\t\t\t\t\"name\": \"lightSpeed\",\n\t\t\t\t\"direction\": \"left\",\n\t\t\t},\n\t\t}\n\n\t\t// 将配置转换为 JSON 字符串\n\t\tthemeJSON, err := json.Marshal(defaultThemeConfig)\n\t\tif err != nil {\n\t\t\tlogger.Log.Error(\"Failed to marshal default theme config:\", err)\n\t\t\tthemeJSON = []byte(`{\"theme\":{\"color\":\"#722ed1\",\"mode\":\"dark\",\"success\":\"#52c41a\",\"warning\":\"#faad14\",\"error\":\"#f5222f\"},\"animate\":{\"disabled\":true,\"name\":\"lightSpeed\",\"direction\":\"left\"}}`)\n\t\t}\n\n\t\t// 使用结构体插入管理员账号\n\t\tpassword, _ := utils.PasswordHash(\"Zbxtable\")\n\t\tuser := User{\n\t\t\tUsername: \"admin\",\n\t\t\tPassword: password,\n\t\t\tAvatar: DefaultAvatar,\n\t\t\tRole: \"admin\",\n\t\t\tOperation: \"['add', 'edit', 'delete','update']\",\n\t\t\tTheme: string(themeJSON),\n\t\t\tStatus: 0,\n\t\t}\n\t\terr = DB.Create(&user).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"create an administrator account successfully, the admin ID is:\", user.ID)\n\t}\n\t//初始化系统数据\n\tvar cnt []System\n\terr = DB.Find(&cnt).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn\n\t}\n\tif len(cnt) == 0 {\n\t\tnow := time.Now()\n\t\tsys := []System{\n\t\t\t{Name: \"Linux操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"Windows操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"网络设备\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"物理服务器\", Status: 0, InitedAt: &now},\n\t\t}\n\t\terr := DB.Create(&sys).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init system info error!\", err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"Init system data successfully!\")\n\t}\n\t//告警默认规则初始化 default rule\n\tvar rules []Rule\n\terr = DB.Where(\"m_type = ?\", \"2\").Find(&rules).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn\n\t}\n\tif len(rules) == 0 {\n\t\t// 使用 \"*\" 作为全局默认规则,匹配所有租户\n\t\tdefaultRule := []Rule{\n\t\t\t{\n\t\t\t\tName: \"全局默认规则\",\n\t\t\t\tZIDs: \"*\",\n\t\t\t\tMType: \"2\",\n\t\t\t\tChannel: \"wechat_robot\",\n\t\t\t\tUserIds: \"1\",\n\t\t\t\tSweek: \"0,1,2,3,4,5,6\",\n\t\t\t\tStime: \"00:00\",\n\t\t\t\tEtime: \"23.59\",\n\t\t\t\tStatus: \"0\"},\n\t\t}\n\t\terr := DB.Create(&defaultRule).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init default rule error!\", err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"Init default rule successfully!\")\n\t}\n\t// 初始化加密密钥(如果不存在)\n\tvar encryptionKeyConfig Config\n\terr = DB.Where(\"config_key = ?\", \"encryption_key\").First(&encryptionKeyConfig).Error\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t// 生成随机加密密钥\n\t\trandomKey := utils.GenerateRandomKey()\n\t\tencryptionKeyConfig = Config{\n\t\t\tName: \"加密密钥\",\n\t\t\tConfigKey: \"encryption_key\",\n\t\t\tConfigValue: randomKey,\n\t\t\tComment: \"用于加密存储敏感信息(如Zabbix密码和Token)的密钥,系统自动生成,不可修改\",\n\t\t}\n\t\tif insertErr := DB.Create(&encryptionKeyConfig).Error; insertErr != nil {\n\t\t\tlogger.Log.Error(\"Init encryption_key error:\", insertErr)\n\t\t} else {\n\t\t\tlogger.Log.Info(\"Init encryption_key successfully! Key length:\", len(randomKey))\n\t\t}\n\t}\n\n\t// 初始化内置映射规则\n\t//InitDefaultMappingRules()\n\n\t// 获取结构化的默认配置\n\tdefaultConfigs := getDefaultConfigs()\n\n\tfor _, cfgItem := range defaultConfigs {\n\t\tvar existing Config\n\t\terr = DB.Where(\"config_key = ?\", cfgItem.ConfigKey).First(&existing).Error\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\tif insertErr := DB.Create(&cfgItem).Error; insertErr != nil {\n\t\t\t\tlogger.Log.Infof(\"Init config key %s error: %v\", cfgItem.ConfigKey, insertErr)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tlogger.Log.Infof(\"Init config key %s successfully!\", cfgItem.ConfigKey)\n\t\t} else {\n\t\t\tlogger.Log.Infof(\"Init config key %s error: %v\", cfgItem.ConfigKey, err)\n\t\t\tcontinue\n\t\t}\n\t}\n\t//默认菜单初始化\n\tInitMenuData()\n\t//检查并添加缺失的菜单项(用于版本升级)\n\tCheckAndAddMenus()\n\n}\n\nfunc GetConfKey(v string) string {\n\t// 优先从 .env 中读取(便于开发环境配置敏感信息且不提交到 git)\n\tenvConfigOnce.Do(func() {\n\t\tfile, err := os.Open(\".env\")\n\t\tif err != nil {\n\t\t\t// 没有 .env 文件时直接跳过,后面回退到 app.conf\n\t\t\tenvFileExists = false\n\t\t\treturn\n\t\t}\n\t\tdefer file.Close()\n\n\t\tenvFileExists = true\n\t\tenvConfig = make(map[string]string)\n\n\t\tscanner := bufio.NewScanner(file)\n\t\tfor scanner.Scan() {\n\t\t\tline := strings.TrimSpace(scanner.Text())\n\t\t\tif line == \"\" || strings.HasPrefix(line, \"#\") || strings.HasPrefix(line, \";\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tparts := strings.SplitN(line, \"=\", 2)\n\t\t\tif len(parts) != 2 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tkey := strings.TrimSpace(parts[0])\n\t\t\tval := strings.TrimSpace(parts[1])\n\t\t\t// 去掉可能的引号\n\t\t\tval = strings.Trim(val, `\"'`)\n\t\t\tif key != \"\" {\n\t\t\t\tenvConfig[key] = val // 即使 val 为空也认为是有效配置\n\t\t\t}\n\t\t}\n\t})\n\n\t// 如果存在 .env 文件,则所有配置完全由 .env 决定:\n\t// - key 存在:返回对应值(可以是空字符串)\n\t// - key 不存在:返回空字符串\n\tif envFileExists {\n\t\tif envConfig == nil {\n\t\t\treturn \"\"\n\t\t}\n\t\tif val, ok := envConfig[v]; ok {\n\t\t\treturn val\n\t\t}\n\t\treturn \"\"\n\t}\n\t// 其次从 config/app.conf 读取\n\tcfg, err := ini.Load(\"./config/app.conf\")\n\tif err != nil {\n\t\tlogger.Log.Error(err)\n\t\treturn \"\"\n\t}\n\tp, err := cfg.Section(\"\").GetKey(v)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn p.String()\n}\n\n// IsPasswordConfigured 检查是否配置了密码(用于图形查看)\n// 优先检查数据库中是否有激活的 Zabbix 实例配置了用户名和密码\n// 注意:查看图形需要用户名和密码,Token 方式无法用于 Web 登录\nfunc IsPasswordConfigured() bool {\n\t// 1. 优先检查数据库中是否有激活的 Zabbix 实例\n\tvar activeInstance ZabbixInstance\n\terr := DB.Where(\"is_active = ?\", true).First(&activeInstance).Error\n\tif err == nil {\n\t\t// 找到激活的实例,检查是否配置了用户名和密码(查看图形必须用密码,Token 无法用于 Web 登录)\n\t\tif strings.TrimSpace(activeInstance.User) != \"\" && strings.TrimSpace(activeInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 2. 如果数据库中没有激活实例,检查是否有任何可用的实例配置了密码\n\tvar anyInstance ZabbixInstance\n\terr = DB.Where(\"enabled = ?\", true).First(&anyInstance).Error\n\tif err == nil {\n\t\t// 找到可用的实例,检查是否配置了用户名和密码\n\t\tif strings.TrimSpace(anyInstance.User) != \"\" && strings.TrimSpace(anyInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 3. 回退到配置文件检查(兼容旧版本)\n\tpass := GetConfKey(\"zabbix_pass\")\n\tuser := GetConfKey(\"zabbix_user\")\n\treturn pass != \"\" && user != \"\"\n}\n\n// GetEncryptionKey 获取加密密钥\n// 优先从数据库读取,如果数据库中没有则从环境变量读取,最后使用默认密钥\nfunc GetEncryptionKey() string {\n\t// 1. 优先从数据库读取\n\tkey := GetConfigValueByKey(\"encryption_key\", \"\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 2. 从环境变量或配置文件读取\n\tkey = GetConfKey(\"encryption_key\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 3. 使用默认密钥(不应该到这里,因为 DatabaseInit 会生成)\n\tlogger.Log.Warn(\"使用默认加密密钥,建议在数据库中配置 encryption_key\")\n\treturn \"zbxtable-default-encryption-key-2024\"\n}\n\n// getDefaultConfigs 返回系统默认配置列表\n// 配置按功能模块分组,便于维护和管理\nfunc getDefaultConfigs() []Config {\n\tconfigs := make([]Config, 0)\n\n\t// 系统外观配置\n\tconfigs = append(configs, getSystemAppearanceConfigs()...)\n\n\t// 初始配置状态\n\tconfigs = append(configs, getInitialSetupConfigs()...)\n\n\t// Dashboard 相关配置\n\tconfigs = append(configs, getDashboardConfigs()...)\n\n\t// 邮件配置\n\tconfigs = append(configs, getEmailConfigs()...)\n\n\t// 企业微信配置\n\tconfigs = append(configs, getWeChatConfigs()...)\n\n\t// AI 配置\n\tconfigs = append(configs, getAIConfigs()...)\n\n\treturn configs\n}\n\n// getSystemAppearanceConfigs 系统外观配置\nfunc getSystemAppearanceConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"系统名称\",\n\t\t\tConfigKey: \"system_name\",\n\t\t\tConfigValue: \"ZbxTable\",\n\t\t\tComment: \"系统显示的名称\",\n\t\t},\n\t\t{\n\t\t\tName: \"系统Logo\",\n\t\t\tConfigKey: \"system_logo\",\n\t\t\tConfigValue: \"data:image/png;base64,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\",\n\t\t\tComment: \"系统Logo的base64编码数据\",\n\t\t},\n\t}\n}\n\n// getInitialSetupConfigs 初始配置状态\nfunc getInitialSetupConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"初始配置完成\",\n\t\t\tConfigKey: \"initial_setup_completed\",\n\t\t\tConfigValue: \"0\",\n\t\t\tComment: \"标记系统是否完成初始配置:1 已完成,0 未完成\",\n\t\t},\n\t}\n}\n\n// getDashboardConfigs Dashboard 相关配置\nfunc getDashboardConfigs() []Config {\n\treturn []Config{\n\t\t// {\n\t\t// \tName: \"数据面板\",\n\t\t// \tConfigKey: \"zbx_dash\",\n\t\t// \tConfigValue: \"0\",\n\t\t// \tComment: \"是否开启Zabbix看板:1 开启,0 关闭\",\n\t\t// },\n\t\t// {\n\t\t// \tName: \"面板配置\",\n\t\t// \tConfigKey: \"dash_id\",\n\t\t// \tConfigValue: \"1\",\n\t\t// \tComment: \"需要引入的Zabbix面板的ID,默认为1\",\n\t\t// },\n\t\t{\n\t\t\tName: \"Top数量\",\n\t\t\tConfigKey: \"dash_top_num\",\n\t\t\tConfigValue: \"10\",\n\t\t\tComment: \"首页Top列表展示数量\",\n\t\t},\n\t\t{\n\t\t\tName: \"主机分类同步\",\n\t\t\tConfigKey: \"sync_inventory\",\n\t\t\tConfigValue: \"1\",\n\t\t\tComment: \"主机分类同步计划任务是否启用:1 启用,0 不启用\",\n\t\t},\n\t\t{\n\t\t\tName: \"Webhook回调地址\",\n\t\t\tConfigKey: \"webhook_url\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"webhook通知地址\",\n\t\t},\n\t}\n}\n\n// getEmailConfigs 邮件配置\nfunc getEmailConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"邮件发件人\",\n\t\t\tConfigKey: \"email_from\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"告警邮件发件人邮箱地址\",\n\t\t},\n\t\t{\n\t\t\tName: \"邮件昵称\",\n\t\t\tConfigKey: \"email_nickname\",\n\t\t\tConfigValue: \"ZbxTable\",\n\t\t\tComment: \"告警邮件显示的发件人昵称\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 密码/授权码\",\n\t\t\tConfigKey: \"email_secret\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"SMTP 登录密码或授权码\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 服务器\",\n\t\t\tConfigKey: \"email_host\",\n\t\t\tConfigValue: \"smtp.qq.com\",\n\t\t\tComment: \"SMTP 服务器地址\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 端口\",\n\t\t\tConfigKey: \"email_port\",\n\t\t\tConfigValue: \"465\",\n\t\t\tComment: \"SMTP 端口号\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 使用 SSL\",\n\t\t\tConfigKey: \"email_isSSl\",\n\t\t\tConfigValue: \"true\",\n\t\t\tComment: \"是否启用 SSL:true/false\",\n\t\t},\n\t}\n}\n\n// getWeChatConfigs 企业微信配置\nfunc getWeChatConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"企业微信开关\",\n\t\t\tConfigKey: \"wechat_enabled\",\n\t\t\tConfigValue: \"0\",\n\t\t\tComment: \"是否启用企业微信:1 启用,0 禁用\",\n\t\t},\n\t\t{\n\t\t\tName: \"企业微信 AgentID\",\n\t\t\tConfigKey: \"wechat_agentid\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"企业微信应用的 AgentID\",\n\t\t},\n\t\t{\n\t\t\tName: \"企业微信 CorpID\",\n\t\t\tConfigKey: \"wechat_corpid\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"企业微信企业ID\",\n\t\t},\n\t\t{\n\t\t\tName: \"企业微信 Secret\",\n\t\t\tConfigKey: \"wechat_secret\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"企业微信应用的 Secret\",\n\t\t},\n\t}\n}\n\n// getAIConfigs AI 配置\nfunc getAIConfigs() []Config {\n\treturn []Config{\n\t\t// AI 类型选择\n\t\t{\n\t\t\tName: \"AI 类型\",\n\t\t\tConfigKey: \"ai_type\",\n\t\t\tConfigValue: \"ollama\",\n\t\t\tComment: \"选择使用的 AI 服务类型:ollama 或 deepseek\",\n\t\t},\n\t\t// Ollama 配置\n\t\t{\n\t\t\tName: \"Ollama Host\",\n\t\t\tConfigKey: \"ollama_host\",\n\t\t\tConfigValue: \"http://localhost:11434\",\n\t\t\tComment: \"Ollama 服务地址,如 http://127.0.0.1:11434\",\n\t\t},\n\t\t{\n\t\t\tName: \"Ollama Model\",\n\t\t\tConfigKey: \"ollama_model\",\n\t\t\tConfigValue: \"deepseek-r1:32b\",\n\t\t\tComment: \"默认使用的大模型名称\",\n\t\t},\n\t\t// Deepseek 配置\n\t\t{\n\t\t\tName: \"Deepseek API Key\",\n\t\t\tConfigKey: \"deepseek_api_key\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"Deepseek API 密钥\",\n\t\t},\n\t\t{\n\t\t\tName: \"Deepseek Model\",\n\t\t\tConfigKey: \"deepseek_model\",\n\t\t\tConfigValue: \"deepseek-chat\",\n\t\t\tComment: \"Deepseek 模型名称,如 deepseek-chat\",\n\t\t},\n\t\t{\n\t\t\tName: \"Deepseek Base URL\",\n\t\t\tConfigKey: \"deepseek_base_url\",\n\t\t\tConfigValue: \"https://api.deepseek.com\",\n\t\t\tComment: \"Deepseek API 地址,默认为 https://api.deepseek.com\",\n\t\t},\n\t}\n}\n"}, "8663584a752f44230e437c154c0dc9a7ca426a8e": {"t": 1785754904.5721693, "n": 18989, "h": "dc111a7bd0fc61eb", "c": "package model\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\t\"zbxtable/pkg/logger\"\n\t\"zbxtable/pkg/utils\"\n\n\tzabbix \"github.com/canghai908/zabbix-go\"\n\t_ \"github.com/go-sql-driver/mysql\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t_ \"github.com/lib/pq\"\n\tworkwx \"github.com/xen0n/go-workwx\"\n\tini \"gopkg.in/ini.v1\"\n\t\"gorm.io/gorm\"\n)\n\nvar (\n\tAPI = &zabbix.API{}\n\tjson = jsoniter.Config{\n\t\tEscapeHTML: false,\n\t\tSortMapKeys: true,\n\t\tValidateJsonRawMessage: true,\n\t}.Froze()\n\tZBX_VER string\n\tZBX_V bool\n\tVersion string\n\tGitHash string\n\tBuildTime string\n\tWeApp *workwx.WorkwxApp // 不初始化,默认为 nil\n\n\tenvConfig map[string]string\n\tenvConfigOnce sync.Once\n\tenvFileExists bool\n)\n\nconst (\n\t//ms-agent 常量定义\n\tMSName = \"ms-agent\"\n\tMSUser = \"ms-agent\"\n\tMSGroup = \"MS-Agent\"\n\tMSMedia = \"MS-Agent\"\n\tMSAction = \"MS-Agent\"\n\t//webhook 常量定义\n\tWebhookName = \"ZbxTable\"\n\tWebhookAction = \"ZbxTable Webhook\"\n\tWebhookGroup = \"ZbxTable Webhook\"\n\tWebhookUser = \"zbxtable-webhook\"\n\t//down\n\tDownloadPath = \"./download/\"\n\tTplPath = \"./assets/templates/\"\n\t// 默认用户头像 (空字符串,由前端处理默认头像)\n\tDefaultAvatar = \"\"\n\t//默认更新服务器\n\tUpdateURL = \"http://dl.cactifans.com/stable/\"\n\n\t// 上传目录\n\tUploadDir = \"./upload\"\n\t// 背景图片上传目录\n\tBackgroundImageDir = \"./upload/background\"\n\t// 允许的图片格式\n\tAllowedImageExts = \".jpg,.jpeg,.png,.gif,.bmp,.webp,.svg\"\n\t// 最大文件大小 (10MB)\n\tMaxFileSize = 10 * 1024 * 1024\n)\n\n// TableName 表名前缀\nfunc TableName(str string) string {\n\treturn fmt.Sprintf(\"%s%s\", \"zbxtable_\", str)\n}\n\n// 接收数据库信息初始化\nfunc ModelInit(dbtype, dbhost, dbuser, dbpass, dbname, dbport string) {\n\t// 直接使用 GORM\n\trunmode := GetConfKey(\"runmode\")\n\terr := InitGormDB(dbtype, dbhost, dbuser, dbpass, dbname, dbport, runmode)\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to connect database: \", err)\n\t\tos.Exit(1)\n\t}\n\tlogger.Log.Info(\"Database connected!\")\n\t// 自动迁移表\n\terr = AutoMigrate()\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to auto migrate: \", err)\n\t\tos.Exit(1)\n\t}\n\t// 基础数据初始化\n\tDatabaseInit()\n\t// 使用go-cache替代Redis(不再需要Redis连接)\n\tInitCache()\n}\n\nfunc InitWechat() {\n\t// 首先检查企业微信开关是否启用\n\twechatEnabled := GetConfigValueByKey(\"wechat_enabled\", \"0\")\n\tif wechatEnabled != \"1\" {\n\t\tlogger.Log.Info(\"WeChat is disabled in system config, skipping WeChat initialization\")\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\t// 开关已启用,从数据库读取企业微信配置\n\tagentIDStr := GetConfigValueByKey(\"wechat_agentid\", \"\")\n\tcorpid := GetConfigValueByKey(\"wechat_corpid\", \"\")\n\tsecret := GetConfigValueByKey(\"wechat_secret\", \"\") // 这里会自动解密\n\n\t// 验证必填配置项\n\tif agentIDStr == \"\" || corpid == \"\" || secret == \"\" {\n\t\tlogger.Log.Info(\"WeChat is enabled but configuration is incomplete (agentid/corpid/secret), WeChat app will not be initialized\")\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\t// 解析 AgentID\n\tAgentId, err := strconv.ParseInt(agentIDStr, 10, 64)\n\tif err != nil {\n\t\tlogger.Log.Error(\"wechat_agentid parse error:\", err)\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\n\t// 初始化企业微信客户端\n\tclient := workwx.New(corpid)\n\tWeApp = client.WithApp(secret, AgentId)\n\tWeApp.SpawnAccessTokenRefresher()\n\tlogger.Log.Info(\"WeChat inited successfully!\")\n}\n\n// DatabaseInit 数据初始化\nfunc DatabaseInit() {\n\t//数据初始化操作 - 使用 GORM\n\tvar v User\n\terr := DB.Where(\"username = ?\", \"admin\").First(&v).Error\n\t//检查权限\n\tif err == nil && v.Operation == \"\" {\n\t\terr := DB.Model(&User{}).Where(\"id = ?\", v.ID).Update(\"operation\", \"['add', 'edit', 'delete','update']\").Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"update admin operation successfully\")\n\t}\n\t//添加管理员账号\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\tlogger.Log.Info(\"the admin user does not exist, create a new admin account later!\")\n\n\t\t// 构建默认主题配置\n\t\tdefaultThemeConfig := map[string]interface{}{\n\t\t\t\"theme\": map[string]string{\n\t\t\t\t\"color\": \"#722ed1\",\n\t\t\t\t\"mode\": \"dark\",\n\t\t\t\t\"success\": \"#52c41a\",\n\t\t\t\t\"warning\": \"#faad14\",\n\t\t\t\t\"error\": \"#f5222f\",\n\t\t\t},\n\t\t\t\"animate\": map[string]interface{}{\n\t\t\t\t\"disabled\": true,\n\t\t\t\t\"name\": \"lightSpeed\",\n\t\t\t\t\"direction\": \"left\",\n\t\t\t},\n\t\t}\n\n\t\t// 将配置转换为 JSON 字符串\n\t\tthemeJSON, err := json.Marshal(defaultThemeConfig)\n\t\tif err != nil {\n\t\t\tlogger.Log.Error(\"Failed to marshal default theme config:\", err)\n\t\t\tthemeJSON = []byte(`{\"theme\":{\"color\":\"#722ed1\",\"mode\":\"dark\",\"success\":\"#52c41a\",\"warning\":\"#faad14\",\"error\":\"#f5222f\"},\"animate\":{\"disabled\":true,\"name\":\"lightSpeed\",\"direction\":\"left\"}}`)\n\t\t}\n\n\t\t// 使用结构体插入管理员账号\n\t\tpassword, _ := utils.PasswordHash(\"Zbxtable\")\n\t\tuser := User{\n\t\t\tUsername: \"admin\",\n\t\t\tPassword: password,\n\t\t\tAvatar: DefaultAvatar,\n\t\t\tRole: \"admin\",\n\t\t\tOperation: \"['add', 'edit', 'delete','update']\",\n\t\t\tTheme: string(themeJSON),\n\t\t\tStatus: 0,\n\t\t}\n\t\terr = DB.Create(&user).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"create an administrator account successfully, the admin ID is:\", user.ID)\n\t}\n\t//初始化系统数据\n\tvar cnt []System\n\terr = DB.Find(&cnt).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn\n\t}\n\tif len(cnt) == 0 {\n\t\tnow := time.Now()\n\t\tsys := []System{\n\t\t\t{Name: \"Linux操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"Windows操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"网络设备\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"物理服务器\", Status: 0, InitedAt: &now},\n\t\t}\n\t\terr := DB.Create(&sys).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init system info error!\", err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"Init system data successfully!\")\n\t}\n\t//告警默认规则初始化 default rule\n\tvar rules []Rule\n\terr = DB.Where(\"m_type = ?\", \"2\").Find(&rules).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn\n\t}\n\tif len(rules) == 0 {\n\t\t// 使用 \"*\" 作为全局默认规则,匹配所有租户\n\t\tdefaultRule := []Rule{\n\t\t\t{\n\t\t\t\tName: \"全局默认规则\",\n\t\t\t\tZIDs: \"*\",\n\t\t\t\tMType: \"2\",\n\t\t\t\tChannel: \"wechat_robot\",\n\t\t\t\tUserIds: \"1\",\n\t\t\t\tSweek: \"0,1,2,3,4,5,6\",\n\t\t\t\tStime: \"00:00\",\n\t\t\t\tEtime: \"23.59\",\n\t\t\t\tStatus: \"0\"},\n\t\t}\n\t\terr := DB.Create(&defaultRule).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init default rule error!\", err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"Init default rule successfully!\")\n\t}\n\t// 初始化加密密钥(如果不存在)\n\tvar encryptionKeyConfig Config\n\terr = DB.Where(\"config_key = ?\", \"encryption_key\").First(&encryptionKeyConfig).Error\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t// 生成随机加密密钥\n\t\trandomKey := utils.GenerateRandomKey()\n\t\tencryptionKeyConfig = Config{\n\t\t\tName: \"加密密钥\",\n\t\t\tConfigKey: \"encryption_key\",\n\t\t\tConfigValue: randomKey,\n\t\t\tComment: \"用于加密存储敏感信息(如Zabbix密码和Token)的密钥,系统自动生成,不可修改\",\n\t\t}\n\t\tif insertErr := DB.Create(&encryptionKeyConfig).Error; insertErr != nil {\n\t\t\tlogger.Log.Error(\"Init encryption_key error:\", insertErr)\n\t\t} else {\n\t\t\tlogger.Log.Info(\"Init encryption_key successfully! Key length:\", len(randomKey))\n\t\t}\n\t}\n\n\t// 初始化内置映射规则\n\t//InitDefaultMappingRules()\n\n\t// 获取结构化的默认配置\n\tdefaultConfigs := getDefaultConfigs()\n\n\tfor _, cfgItem := range defaultConfigs {\n\t\tvar existing Config\n\t\terr = DB.Where(\"config_key = ?\", cfgItem.ConfigKey).First(&existing).Error\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\tif insertErr := DB.Create(&cfgItem).Error; insertErr != nil {\n\t\t\t\tlogger.Log.Infof(\"Init config key %s error: %v\", cfgItem.ConfigKey, insertErr)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tlogger.Log.Infof(\"Init config key %s successfully!\", cfgItem.ConfigKey)\n\t\t} else {\n\t\t\tlogger.Log.Infof(\"Init config key %s error: %v\", cfgItem.ConfigKey, err)\n\t\t\tcontinue\n\t\t}\n\t}\n\t//默认菜单初始化\n\tInitMenuData()\n\t//检查并添加缺失的菜单项(用于版本升级)\n\tCheckAndAddMenus()\n\n}\n\nfunc GetConfKey(v string) string {\n\t// 优先从 .env 中读取(便于开发环境配置敏感信息且不提交到 git)\n\tenvConfigOnce.Do(func() {\n\t\tfile, err := os.Open(\".env\")\n\t\tif err != nil {\n\t\t\t// 没有 .env 文件时直接跳过,后面回退到 app.conf\n\t\t\tenvFileExists = false\n\t\t\treturn\n\t\t}\n\t\tdefer file.Close()\n\n\t\tenvFileExists = true\n\t\tenvConfig = make(map[string]string)\n\n\t\tscanner := bufio.NewScanner(file)\n\t\tfor scanner.Scan() {\n\t\t\tline := strings.TrimSpace(scanner.Text())\n\t\t\tif line == \"\" || strings.HasPrefix(line, \"#\") || strings.HasPrefix(line, \";\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tparts := strings.SplitN(line, \"=\", 2)\n\t\t\tif len(parts) != 2 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tkey := strings.TrimSpace(parts[0])\n\t\t\tval := strings.TrimSpace(parts[1])\n\t\t\t// 去掉可能的引号\n\t\t\tval = strings.Trim(val, `\"'`)\n\t\t\tif key != \"\" {\n\t\t\t\tenvConfig[key] = val // 即使 val 为空也认为是有效配置\n\t\t\t}\n\t\t}\n\t})\n\n\t// 如果存在 .env 文件,则所有配置完全由 .env 决定:\n\t// - key 存在:返回对应值(可以是空字符串)\n\t// - key 不存在:返回空字符串\n\tif envFileExists {\n\t\tif envConfig == nil {\n\t\t\treturn \"\"\n\t\t}\n\t\tif val, ok := envConfig[v]; ok {\n\t\t\treturn val\n\t\t}\n\t\treturn \"\"\n\t}\n\t// 其次从 config/app.conf 读取\n\tcfg, err := ini.Load(\"./config/app.conf\")\n\tif err != nil {\n\t\tlogger.Log.Error(err)\n\t\treturn \"\"\n\t}\n\tp, err := cfg.Section(\"\").GetKey(v)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn p.String()\n}\n\n// IsPasswordConfigured 检查是否配置了密码(用于图形查看)\n// 优先检查数据库中是否有激活的 Zabbix 实例配置了用户名和密码\n// 注意:查看图形需要用户名和密码,Token 方式无法用于 Web 登录\nfunc IsPasswordConfigured() bool {\n\t// 1. 优先检查数据库中是否有激活的 Zabbix 实例\n\tvar activeInstance ZabbixInstance\n\terr := DB.Where(\"is_active = ?\", true).First(&activeInstance).Error\n\tif err == nil {\n\t\t// 找到激活的实例,检查是否配置了用户名和密码(查看图形必须用密码,Token 无法用于 Web 登录)\n\t\tif strings.TrimSpace(activeInstance.User) != \"\" && strings.TrimSpace(activeInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 2. 如果数据库中没有激活实例,检查是否有任何可用的实例配置了密码\n\tvar anyInstance ZabbixInstance\n\terr = DB.Where(\"enabled = ?\", true).First(&anyInstance).Error\n\tif err == nil {\n\t\t// 找到可用的实例,检查是否配置了用户名和密码\n\t\tif strings.TrimSpace(anyInstance.User) != \"\" && strings.TrimSpace(anyInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 3. 回退到配置文件检查(兼容旧版本)\n\tpass := GetConfKey(\"zabbix_pass\")\n\tuser := GetConfKey(\"zabbix_user\")\n\treturn pass != \"\" && user != \"\"\n}\n\n// GetEncryptionKey 获取加密密钥\n// 优先从数据库读取,如果数据库中没有则从环境变量读取,最后使用默认密钥\nfunc GetEncryptionKey() string {\n\t// 1. 优先从数据库读取\n\tkey := GetConfigValueByKey(\"encryption_key\", \"\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 2. 从环境变量或配置文件读取\n\tkey = GetConfKey(\"encryption_key\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 3. 使用默认密钥(不应该到这里,因为 DatabaseInit 会生成)\n\tlogger.Log.Warn(\"使用默认加密密钥,建议在数据库中配置 encryption_key\")\n\treturn \"zbxtable-default-encryption-key-2024\"\n}\n\n// getDefaultConfigs 返回系统默认配置列表\n// 配置按功能模块分组,便于维护和管理\nfunc getDefaultConfigs() []Config {\n\tconfigs := make([]Config, 0)\n\n\t// 系统外观配置\n\tconfigs = append(configs, getSystemAppearanceConfigs()...)\n\n\t// 初始配置状态\n\tconfigs = append(configs, getInitialSetupConfigs()...)\n\n\t// Dashboard 相关配置\n\tconfigs = append(configs, getDashboardConfigs()...)\n\n\t// 邮件配置\n\tconfigs = append(configs, getEmailConfigs()...)\n\n\t// 企业微信配置\n\tconfigs = append(configs, getWeChatConfigs()...)\n\n\t// AI 配置\n\tconfigs = append(configs, getAIConfigs()...)\n\n\treturn configs\n}\n\n// getSystemAppearanceConfigs 系统外观配置\nfunc getSystemAppearanceConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"系统名称\",\n\t\t\tConfigKey: \"system_name\",\n\t\t\tConfigValue: \"ZbxTable\",\n\t\t\tComment: \"系统显示的名称\",\n\t\t},\n\t\t{\n\t\t\tName: \"系统Logo\",\n\t\t\tConfigKey: \"system_logo\",\n\t\t\tConfigValue: \"data:image/png;base64,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\",\n\t\t\tComment: \"系统Logo的base64编码数据\",\n\t\t},\n\t}\n}\n\n// getInitialSetupConfigs 初始配置状态\nfunc getInitialSetupConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"初始配置完成\",\n\t\t\tConfigKey: \"initial_setup_completed\",\n\t\t\tConfigValue: \"0\",\n\t\t\tComment: \"标记系统是否完成初始配置:1 已完成,0 未完成\",\n\t\t},\n\t}\n}\n\n// getDashboardConfigs Dashboard 相关配置\nfunc getDashboardConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"数据面板\",\n\t\t\tConfigKey: \"zbx_dash\",\n\t\t\tConfigValue: \"0\",\n\t\t\tComment: \"是否开启Zabbix看板:1 开启,0 关闭\",\n\t\t},\n\t\t{\n\t\t\tName: \"面板配置\",\n\t\t\tConfigKey: \"dash_id\",\n\t\t\tConfigValue: \"1\",\n\t\t\tComment: \"需要引入的Zabbix面板的ID,默认为1\",\n\t\t},\n\t\t{\n\t\t\tName: \"Top数量\",\n\t\t\tConfigKey: \"dash_top_num\",\n\t\t\tConfigValue: \"10\",\n\t\t\tComment: \"首页Top列表展示数量\",\n\t\t},\n\t\t{\n\t\t\tName: \"主机分类同步\",\n\t\t\tConfigKey: \"sync_inventory\",\n\t\t\tConfigValue: \"1\",\n\t\t\tComment: \"主机分类同步计划任务是否启用:1 启用,0 不启用\",\n\t\t},\n\t\t{\n\t\t\tName: \"Webhook回调地址\",\n\t\t\tConfigKey: \"webhook_url\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"webhook通知地址\",\n\t\t},\n\t}\n}\n\n// getEmailConfigs 邮件配置\nfunc getEmailConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"邮件发件人\",\n\t\t\tConfigKey: \"email_from\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"告警邮件发件人邮箱地址\",\n\t\t},\n\t\t{\n\t\t\tName: \"邮件昵称\",\n\t\t\tConfigKey: \"email_nickname\",\n\t\t\tConfigValue: \"ZbxTable\",\n\t\t\tComment: \"告警邮件显示的发件人昵称\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 密码/授权码\",\n\t\t\tConfigKey: \"email_secret\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"SMTP 登录密码或授权码\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 服务器\",\n\t\t\tConfigKey: \"email_host\",\n\t\t\tConfigValue: \"smtp.qq.com\",\n\t\t\tComment: \"SMTP 服务器地址\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 端口\",\n\t\t\tConfigKey: \"email_port\",\n\t\t\tConfigValue: \"465\",\n\t\t\tComment: \"SMTP 端口号\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 使用 SSL\",\n\t\t\tConfigKey: \"email_isSSl\",\n\t\t\tConfigValue: \"true\",\n\t\t\tComment: \"是否启用 SSL:true/false\",\n\t\t},\n\t}\n}\n\n// getWeChatConfigs 企业微信配置\nfunc getWeChatConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"企业微信开关\",\n\t\t\tConfigKey: \"wechat_enabled\",\n\t\t\tConfigValue: \"0\",\n\t\t\tComment: \"是否启用企业微信:1 启用,0 禁用\",\n\t\t},\n\t\t{\n\t\t\tName: \"企业微信 AgentID\",\n\t\t\tConfigKey: \"wechat_agentid\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"企业微信应用的 AgentID\",\n\t\t},\n\t\t{\n\t\t\tName: \"企业微信 CorpID\",\n\t\t\tConfigKey: \"wechat_corpid\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"企业微信企业ID\",\n\t\t},\n\t\t{\n\t\t\tName: \"企业微信 Secret\",\n\t\t\tConfigKey: \"wechat_secret\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"企业微信应用的 Secret\",\n\t\t},\n\t}\n}\n\n// getAIConfigs AI 配置\nfunc getAIConfigs() []Config {\n\treturn []Config{\n\t\t// AI 类型选择\n\t\t{\n\t\t\tName: \"AI 类型\",\n\t\t\tConfigKey: \"ai_type\",\n\t\t\tConfigValue: \"ollama\",\n\t\t\tComment: \"选择使用的 AI 服务类型:ollama 或 deepseek\",\n\t\t},\n\t\t// Ollama 配置\n\t\t{\n\t\t\tName: \"Ollama Host\",\n\t\t\tConfigKey: \"ollama_host\",\n\t\t\tConfigValue: \"http://localhost:11434\",\n\t\t\tComment: \"Ollama 服务地址,如 http://127.0.0.1:11434\",\n\t\t},\n\t\t{\n\t\t\tName: \"Ollama Model\",\n\t\t\tConfigKey: \"ollama_model\",\n\t\t\tConfigValue: \"deepseek-r1:32b\",\n\t\t\tComment: \"默认使用的大模型名称\",\n\t\t},\n\t\t// Deepseek 配置\n\t\t{\n\t\t\tName: \"Deepseek API Key\",\n\t\t\tConfigKey: \"deepseek_api_key\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"Deepseek API 密钥\",\n\t\t},\n\t\t{\n\t\t\tName: \"Deepseek Model\",\n\t\t\tConfigKey: \"deepseek_model\",\n\t\t\tConfigValue: \"deepseek-chat\",\n\t\t\tComment: \"Deepseek 模型名称,如 deepseek-chat\",\n\t\t},\n\t\t{\n\t\t\tName: \"Deepseek Base URL\",\n\t\t\tConfigKey: \"deepseek_base_url\",\n\t\t\tConfigValue: \"https://api.deepseek.com\",\n\t\t\tComment: \"Deepseek API 地址,默认为 https://api.deepseek.com\",\n\t\t},\n\t}\n}\n"}, "9dddaad1d4086fba3864efa37342014f00c54d44": {"t": 1785754905.977337, "n": 18870, "h": "2edd6f0151401705", "c": "package model\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\t\"zbxtable/pkg/logger\"\n\t\"zbxtable/pkg/utils\"\n\n\tzabbix \"github.com/canghai908/zabbix-go\"\n\t_ \"github.com/go-sql-driver/mysql\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t_ \"github.com/lib/pq\"\n\tworkwx \"github.com/xen0n/go-workwx\"\n\tini \"gopkg.in/ini.v1\"\n\t\"gorm.io/gorm\"\n)\n\nvar (\n\tAPI = &zabbix.API{}\n\tjson = jsoniter.Config{\n\t\tEscapeHTML: false,\n\t\tSortMapKeys: true,\n\t\tValidateJsonRawMessage: true,\n\t}.Froze()\n\tZBX_VER string\n\tZBX_V bool\n\tVersion string\n\tGitHash string\n\tBuildTime string\n\tWeApp *workwx.WorkwxApp // 不初始化,默认为 nil\n\n\tenvConfig map[string]string\n\tenvConfigOnce sync.Once\n\tenvFileExists bool\n)\n\nconst (\n\t//ms-agent 常量定义\n\tMSName = \"ms-agent\"\n\tMSUser = \"ms-agent\"\n\tMSGroup = \"MS-Agent\"\n\tMSMedia = \"MS-Agent\"\n\tMSAction = \"MS-Agent\"\n\t//webhook 常量定义\n\tWebhookName = \"ZbxTable\"\n\tWebhookAction = \"ZbxTable Webhook\"\n\tWebhookGroup = \"ZbxTable Webhook\"\n\tWebhookUser = \"zbxtable-webhook\"\n\t//down\n\tDownloadPath = \"./download/\"\n\tTplPath = \"./assets/templates/\"\n\t// 默认用户头像 (空字符串,由前端处理默认头像)\n\tDefaultAvatar = \"\"\n\t//默认更新服务器\n\tUpdateURL = \"http://dl.cactifans.com/stable/\"\n\n\t// 上传目录\n\tUploadDir = \"./upload\"\n\t// 背景图片上传目录\n\tBackgroundImageDir = \"./upload/background\"\n\t// 允许的图片格式\n\tAllowedImageExts = \".jpg,.jpeg,.png,.gif,.bmp,.webp,.svg\"\n\t// 最大文件大小 (10MB)\n\tMaxFileSize = 10 * 1024 * 1024\n)\n\n// TableName 表名前缀\nfunc TableName(str string) string {\n\treturn fmt.Sprintf(\"%s%s\", \"zbxtable_\", str)\n}\n\n// 接收数据库信息初始化\nfunc ModelInit(dbtype, dbhost, dbuser, dbpass, dbname, dbport string) {\n\t// 直接使用 GORM\n\trunmode := GetConfKey(\"runmode\")\n\terr := InitGormDB(dbtype, dbhost, dbuser, dbpass, dbname, dbport, runmode)\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to connect database: \", err)\n\t\tos.Exit(1)\n\t}\n\tlogger.Log.Info(\"Database connected!\")\n\t// 自动迁移表\n\terr = AutoMigrate()\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to auto migrate: \", err)\n\t\tos.Exit(1)\n\t}\n\t// 基础数据初始化\n\tDatabaseInit()\n\t// 使用go-cache替代Redis(不再需要Redis连接)\n\tInitCache()\n}\n\nfunc InitWechat() {\n\t// 首先检查企业微信开关是否启用\n\twechatEnabled := GetConfigValueByKey(\"wechat_enabled\", \"0\")\n\tif wechatEnabled != \"1\" {\n\t\tlogger.Log.Info(\"WeChat is disabled in system config, skipping WeChat initialization\")\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\t// 开关已启用,从数据库读取企业微信配置\n\tagentIDStr := GetConfigValueByKey(\"wechat_agentid\", \"\")\n\tcorpid := GetConfigValueByKey(\"wechat_corpid\", \"\")\n\tsecret := GetConfigValueByKey(\"wechat_secret\", \"\") // 这里会自动解密\n\n\t// 验证必填配置项\n\tif agentIDStr == \"\" || corpid == \"\" || secret == \"\" {\n\t\tlogger.Log.Info(\"WeChat is enabled but configuration is incomplete (agentid/corpid/secret), WeChat app will not be initialized\")\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\t// 解析 AgentID\n\tAgentId, err := strconv.ParseInt(agentIDStr, 10, 64)\n\tif err != nil {\n\t\tlogger.Log.Error(\"wechat_agentid parse error:\", err)\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\n\t// 初始化企业微信客户端\n\tclient := workwx.New(corpid)\n\tWeApp = client.WithApp(secret, AgentId)\n\tWeApp.SpawnAccessTokenRefresher()\n\tlogger.Log.Info(\"WeChat inited successfully!\")\n}\n\n// DatabaseInit 数据初始化\nfunc DatabaseInit() {\n\t//数据初始化操作 - 使用 GORM\n\tvar v User\n\terr := DB.Where(\"username = ?\", \"admin\").First(&v).Error\n\t//检查权限\n\tif err == nil && v.Operation == \"\" {\n\t\terr := DB.Model(&User{}).Where(\"id = ?\", v.ID).Update(\"operation\", \"['add', 'edit', 'delete','update']\").Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"update admin operation successfully\")\n\t}\n\t//添加管理员账号\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\tlogger.Log.Info(\"the admin user does not exist, create a new admin account later!\")\n\n\t\t// 构建默认主题配置\n\t\tdefaultThemeConfig := map[string]interface{}{\n\t\t\t\"theme\": map[string]string{\n\t\t\t\t\"color\": \"#722ed1\",\n\t\t\t\t\"mode\": \"dark\",\n\t\t\t\t\"success\": \"#52c41a\",\n\t\t\t\t\"warning\": \"#faad14\",\n\t\t\t\t\"error\": \"#f5222f\",\n\t\t\t},\n\t\t\t\"animate\": map[string]interface{}{\n\t\t\t\t\"disabled\": true,\n\t\t\t\t\"name\": \"lightSpeed\",\n\t\t\t\t\"direction\": \"left\",\n\t\t\t},\n\t\t}\n\n\t\t// 将配置转换为 JSON 字符串\n\t\tthemeJSON, err := json.Marshal(defaultThemeConfig)\n\t\tif err != nil {\n\t\t\tlogger.Log.Error(\"Failed to marshal default theme config:\", err)\n\t\t\tthemeJSON = []byte(`{\"theme\":{\"color\":\"#722ed1\",\"mode\":\"dark\",\"success\":\"#52c41a\",\"warning\":\"#faad14\",\"error\":\"#f5222f\"},\"animate\":{\"disabled\":true,\"name\":\"lightSpeed\",\"direction\":\"left\"}}`)\n\t\t}\n\n\t\t// 使用结构体插入管理员账号\n\t\tpassword, _ := utils.PasswordHash(\"Zbxtable\")\n\t\tuser := User{\n\t\t\tUsername: \"admin\",\n\t\t\tPassword: password,\n\t\t\tAvatar: DefaultAvatar,\n\t\t\tRole: \"admin\",\n\t\t\tOperation: \"['add', 'edit', 'delete','update']\",\n\t\t\tTheme: string(themeJSON),\n\t\t\tStatus: 0,\n\t\t}\n\t\terr = DB.Create(&user).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"create an administrator account successfully, the admin ID is:\", user.ID)\n\t}\n\t//初始化系统数据\n\tvar cnt []System\n\terr = DB.Find(&cnt).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn\n\t}\n\tif len(cnt) == 0 {\n\t\tnow := time.Now()\n\t\tsys := []System{\n\t\t\t{Name: \"Linux操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"Windows操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"网络设备\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"物理服务器\", Status: 0, InitedAt: &now},\n\t\t}\n\t\terr := DB.Create(&sys).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init system info error!\", err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"Init system data successfully!\")\n\t}\n\t//告警默认规则初始化 default rule\n\tvar rules []Rule\n\terr = DB.Where(\"m_type = ?\", \"2\").Find(&rules).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn\n\t}\n\tif len(rules) == 0 {\n\t\t// 使用 \"*\" 作为全局默认规则,匹配所有租户\n\t\tdefaultRule := []Rule{\n\t\t\t{\n\t\t\t\tName: \"全局默认规则\",\n\t\t\t\tZIDs: \"*\",\n\t\t\t\tMType: \"2\",\n\t\t\t\tChannel: \"wechat_robot\",\n\t\t\t\tUserIds: \"1\",\n\t\t\t\tSweek: \"0,1,2,3,4,5,6\",\n\t\t\t\tStime: \"00:00\",\n\t\t\t\tEtime: \"23.59\",\n\t\t\t\tStatus: \"0\"},\n\t\t}\n\t\terr := DB.Create(&defaultRule).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init default rule error!\", err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"Init default rule successfully!\")\n\t}\n\t// 初始化加密密钥(如果不存在)\n\tvar encryptionKeyConfig Config\n\terr = DB.Where(\"config_key = ?\", \"encryption_key\").First(&encryptionKeyConfig).Error\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t// 生成随机加密密钥\n\t\trandomKey := utils.GenerateRandomKey()\n\t\tencryptionKeyConfig = Config{\n\t\t\tName: \"加密密钥\",\n\t\t\tConfigKey: \"encryption_key\",\n\t\t\tConfigValue: randomKey,\n\t\t\tComment: \"用于加密存储敏感信息(如Zabbix密码和Token)的密钥,系统自动生成,不可修改\",\n\t\t}\n\t\tif insertErr := DB.Create(&encryptionKeyConfig).Error; insertErr != nil {\n\t\t\tlogger.Log.Error(\"Init encryption_key error:\", insertErr)\n\t\t} else {\n\t\t\tlogger.Log.Info(\"Init encryption_key successfully! Key length:\", len(randomKey))\n\t\t}\n\t}\n\n\t// 初始化内置映射规则\n\t//InitDefaultMappingRules()\n\n\t// 获取结构化的默认配置\n\tdefaultConfigs := getDefaultConfigs()\n\n\tfor _, cfgItem := range defaultConfigs {\n\t\tvar existing Config\n\t\terr = DB.Where(\"config_key = ?\", cfgItem.ConfigKey).First(&existing).Error\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\tif insertErr := DB.Create(&cfgItem).Error; insertErr != nil {\n\t\t\t\tlogger.Log.Infof(\"Init config key %s error: %v\", cfgItem.ConfigKey, insertErr)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tlogger.Log.Infof(\"Init config key %s successfully!\", cfgItem.ConfigKey)\n\t\t} else {\n\t\t\tlogger.Log.Infof(\"Init config key %s error: %v\", cfgItem.ConfigKey, err)\n\t\t\tcontinue\n\t\t}\n\t}\n\t//默认菜单初始化\n\tInitMenuData()\n\t//检查并添加缺失的菜单项(用于版本升级)\n\tCheckAndAddMenus()\n\n}\n\nfunc GetConfKey(v string) string {\n\t// 优先从 .env 中读取(便于开发环境配置敏感信息且不提交到 git)\n\tenvConfigOnce.Do(func() {\n\t\tfile, err := os.Open(\".env\")\n\t\tif err != nil {\n\t\t\t// 没有 .env 文件时直接跳过,后面回退到 app.conf\n\t\t\tenvFileExists = false\n\t\t\treturn\n\t\t}\n\t\tdefer file.Close()\n\n\t\tenvFileExists = true\n\t\tenvConfig = make(map[string]string)\n\n\t\tscanner := bufio.NewScanner(file)\n\t\tfor scanner.Scan() {\n\t\t\tline := strings.TrimSpace(scanner.Text())\n\t\t\tif line == \"\" || strings.HasPrefix(line, \"#\") || strings.HasPrefix(line, \";\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tparts := strings.SplitN(line, \"=\", 2)\n\t\t\tif len(parts) != 2 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tkey := strings.TrimSpace(parts[0])\n\t\t\tval := strings.TrimSpace(parts[1])\n\t\t\t// 去掉可能的引号\n\t\t\tval = strings.Trim(val, `\"'`)\n\t\t\tif key != \"\" {\n\t\t\t\tenvConfig[key] = val // 即使 val 为空也认为是有效配置\n\t\t\t}\n\t\t}\n\t})\n\n\t// 如果存在 .env 文件,则所有配置完全由 .env 决定:\n\t// - key 存在:返回对应值(可以是空字符串)\n\t// - key 不存在:返回空字符串\n\tif envFileExists {\n\t\tif envConfig == nil {\n\t\t\treturn \"\"\n\t\t}\n\t\tif val, ok := envConfig[v]; ok {\n\t\t\treturn val\n\t\t}\n\t\treturn \"\"\n\t}\n\t// 其次从 config/app.conf 读取\n\tcfg, err := ini.Load(\"./config/app.conf\")\n\tif err != nil {\n\t\tlogger.Log.Error(err)\n\t\treturn \"\"\n\t}\n\tp, err := cfg.Section(\"\").GetKey(v)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn p.String()\n}\n\n// IsPasswordConfigured 检查是否配置了密码(用于图形查看)\n// 优先检查数据库中是否有激活的 Zabbix 实例配置了用户名和密码\n// 注意:查看图形需要用户名和密码,Token 方式无法用于 Web 登录\nfunc IsPasswordConfigured() bool {\n\t// 1. 优先检查数据库中是否有激活的 Zabbix 实例\n\tvar activeInstance ZabbixInstance\n\terr := DB.Where(\"is_active = ?\", true).First(&activeInstance).Error\n\tif err == nil {\n\t\t// 找到激活的实例,检查是否配置了用户名和密码(查看图形必须用密码,Token 无法用于 Web 登录)\n\t\tif strings.TrimSpace(activeInstance.User) != \"\" && strings.TrimSpace(activeInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 2. 如果数据库中没有激活实例,检查是否有任何可用的实例配置了密码\n\tvar anyInstance ZabbixInstance\n\terr = DB.Where(\"enabled = ?\", true).First(&anyInstance).Error\n\tif err == nil {\n\t\t// 找到可用的实例,检查是否配置了用户名和密码\n\t\tif strings.TrimSpace(anyInstance.User) != \"\" && strings.TrimSpace(anyInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 3. 回退到配置文件检查(兼容旧版本)\n\tpass := GetConfKey(\"zabbix_pass\")\n\tuser := GetConfKey(\"zabbix_user\")\n\treturn pass != \"\" && user != \"\"\n}\n\n// GetEncryptionKey 获取加密密钥\n// 优先从数据库读取,如果数据库中没有则从环境变量读取,最后使用默认密钥\nfunc GetEncryptionKey() string {\n\t// 1. 优先从数据库读取\n\tkey := GetConfigValueByKey(\"encryption_key\", \"\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 2. 从环境变量或配置文件读取\n\tkey = GetConfKey(\"encryption_key\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 3. 使用默认密钥(不应该到这里,因为 DatabaseInit 会生成)\n\tlogger.Log.Warn(\"使用默认加密密钥,建议在数据库中配置 encryption_key\")\n\treturn \"zbxtable-default-encryption-key-2024\"\n}\n\n// getDefaultConfigs 返回系统默认配置列表\n// 配置按功能模块分组,便于维护和管理\nfunc getDefaultConfigs() []Config {\n\tconfigs := make([]Config, 0)\n\n\t// 系统外观配置\n\tconfigs = append(configs, getSystemAppearanceConfigs()...)\n\n\t// 初始配置状态\n\tconfigs = append(configs, getInitialSetupConfigs()...)\n\n\t// Dashboard 相关配置\n\tconfigs = append(configs, getDashboardConfigs()...)\n\n\t// 邮件配置\n\tconfigs = append(configs, getEmailConfigs()...)\n\n\t// 企业微信配置\n\tconfigs = append(configs, getWeChatConfigs()...)\n\n\t// AI 配置\n\tconfigs = append(configs, getAIConfigs()...)\n\n\treturn configs\n}\n\n// getSystemAppearanceConfigs 系统外观配置\nfunc getSystemAppearanceConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"系统名称\",\n\t\t\tConfigKey: \"system_name\",\n\t\t\tConfigValue: \"ZbxTable\",\n\t\t\tComment: \"系统显示的名称\",\n\t\t},\n\t\t{\n\t\t\tName: \"系统Logo\",\n\t\t\tConfigKey: \"system_logo\",\n\t\t\tConfigValue: \"data:image/png;base64,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\",\n\t\t\tComment: \"系统Logo的base64编码数据\",\n\t\t},\n\t}\n}\n\n// getInitialSetupConfigs 初始配置状态\nfunc getInitialSetupConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"初始配置完成\",\n\t\t\tConfigKey: \"initial_setup_completed\",\n\t\t\tConfigValue: \"0\",\n\t\t\tComment: \"标记系统是否完成初始配置:1 已完成,0 未完成\",\n\t\t},\n\t}\n}\n\n// getDashboardConfigs Dashboard 相关配置\nfunc getDashboardConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"数据面板\",\n\t\t\tConfigKey: \"zbx_dash\",\n\t\t\tConfigValue: \"0\",\n\t\t\tComment: \"是否开启Zabbix看板:1 开启,0 关闭\",\n\t\t},\n\t\t{\n\t\t\tName: \"面板配置\",\n\t\t\tConfigKey: \"dash_id\",\n\t\t\tConfigValue: \"1\",\n\t\t\tComment: \"需要引入的Zabbix面板的ID,默认为1\",\n\t\t},\n\t\t{\n\t\t\tName: \"主机分类同步\",\n\t\t\tConfigKey: \"sync_inventory\",\n\t\t\tConfigValue: \"1\",\n\t\t\tComment: \"主机分类同步计划任务是否启用:1 启用,0 不启用\",\n\t\t},\n\t\t{\n\t\t\tName: \"Webhook回调地址\",\n\t\t\tConfigKey: \"webhook_url\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"webhook通知地址\",\n\t\t},\n\t}\n}\n\n// getEmailConfigs 邮件配置\nfunc getEmailConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"邮件发件人\",\n\t\t\tConfigKey: \"email_from\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"告警邮件发件人邮箱地址\",\n\t\t},\n\t\t{\n\t\t\tName: \"邮件昵称\",\n\t\t\tConfigKey: \"email_nickname\",\n\t\t\tConfigValue: \"ZbxTable\",\n\t\t\tComment: \"告警邮件显示的发件人昵称\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 密码/授权码\",\n\t\t\tConfigKey: \"email_secret\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"SMTP 登录密码或授权码\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 服务器\",\n\t\t\tConfigKey: \"email_host\",\n\t\t\tConfigValue: \"smtp.qq.com\",\n\t\t\tComment: \"SMTP 服务器地址\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 端口\",\n\t\t\tConfigKey: \"email_port\",\n\t\t\tConfigValue: \"465\",\n\t\t\tComment: \"SMTP 端口号\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 使用 SSL\",\n\t\t\tConfigKey: \"email_isSSl\",\n\t\t\tConfigValue: \"true\",\n\t\t\tComment: \"是否启用 SSL:true/false\",\n\t\t},\n\t}\n}\n\n// getWeChatConfigs 企业微信配置\nfunc getWeChatConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"企业微信开关\",\n\t\t\tConfigKey: \"wechat_enabled\",\n\t\t\tConfigValue: \"0\",\n\t\t\tComment: \"是否启用企业微信:1 启用,0 禁用\",\n\t\t},\n\t\t{\n\t\t\tName: \"企业微信 AgentID\",\n\t\t\tConfigKey: \"wechat_agentid\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"企业微信应用的 AgentID\",\n\t\t},\n\t\t{\n\t\t\tName: \"企业微信 CorpID\",\n\t\t\tConfigKey: \"wechat_corpid\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"企业微信企业ID\",\n\t\t},\n\t\t{\n\t\t\tName: \"企业微信 Secret\",\n\t\t\tConfigKey: \"wechat_secret\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"企业微信应用的 Secret\",\n\t\t},\n\t}\n}\n\n// getAIConfigs AI 配置\nfunc getAIConfigs() []Config {\n\treturn []Config{\n\t\t// AI 类型选择\n\t\t{\n\t\t\tName: \"AI 类型\",\n\t\t\tConfigKey: \"ai_type\",\n\t\t\tConfigValue: \"ollama\",\n\t\t\tComment: \"选择使用的 AI 服务类型:ollama 或 deepseek\",\n\t\t},\n\t\t// Ollama 配置\n\t\t{\n\t\t\tName: \"Ollama Host\",\n\t\t\tConfigKey: \"ollama_host\",\n\t\t\tConfigValue: \"http://localhost:11434\",\n\t\t\tComment: \"Ollama 服务地址,如 http://127.0.0.1:11434\",\n\t\t},\n\t\t{\n\t\t\tName: \"Ollama Model\",\n\t\t\tConfigKey: \"ollama_model\",\n\t\t\tConfigValue: \"deepseek-r1:32b\",\n\t\t\tComment: \"默认使用的大模型名称\",\n\t\t},\n\t\t// Deepseek 配置\n\t\t{\n\t\t\tName: \"Deepseek API Key\",\n\t\t\tConfigKey: \"deepseek_api_key\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"Deepseek API 密钥\",\n\t\t},\n\t\t{\n\t\t\tName: \"Deepseek Model\",\n\t\t\tConfigKey: \"deepseek_model\",\n\t\t\tConfigValue: \"deepseek-chat\",\n\t\t\tComment: \"Deepseek 模型名称,如 deepseek-chat\",\n\t\t},\n\t\t{\n\t\t\tName: \"Deepseek Base URL\",\n\t\t\tConfigKey: \"deepseek_base_url\",\n\t\t\tConfigValue: \"https://api.deepseek.com\",\n\t\t\tComment: \"Deepseek API 地址,默认为 https://api.deepseek.com\",\n\t\t},\n\t}\n}\n"}, "7b780213a9711ac28b396db48195a14faa55386e": {"t": 1785754907.6081827, "n": 18826, "h": "374c57694dc0cb42", "c": "package model\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\t\"zbxtable/pkg/logger\"\n\t\"zbxtable/pkg/utils\"\n\n\tzabbix \"github.com/canghai908/zabbix-go\"\n\t_ \"github.com/go-sql-driver/mysql\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t_ \"github.com/lib/pq\"\n\tworkwx \"github.com/xen0n/go-workwx\"\n\tini \"gopkg.in/ini.v1\"\n\t\"gorm.io/gorm\"\n)\n\nvar (\n\tAPI = &zabbix.API{}\n\tjson = jsoniter.Config{\n\t\tEscapeHTML: false,\n\t\tSortMapKeys: true,\n\t\tValidateJsonRawMessage: true,\n\t}.Froze()\n\tZBX_VER string\n\tZBX_V bool\n\tVersion string\n\tGitHash string\n\tBuildTime string\n\tWeApp *workwx.WorkwxApp // 不初始化,默认为 nil\n\n\tenvConfig map[string]string\n\tenvConfigOnce sync.Once\n\tenvFileExists bool\n)\n\nconst (\n\t//ms-agent 常量定义\n\tMSName = \"ms-agent\"\n\tMSUser = \"ms-agent\"\n\tMSGroup = \"MS-Agent\"\n\tMSMedia = \"MS-Agent\"\n\tMSAction = \"MS-Agent\"\n\t//webhook 常量定义\n\tWebhookName = \"ZbxTable\"\n\tWebhookAction = \"ZbxTable Webhook\"\n\tWebhookGroup = \"ZbxTable Webhook\"\n\tWebhookUser = \"zbxtable-webhook\"\n\t//down\n\tDownloadPath = \"./download/\"\n\tTplPath = \"./assets/templates/\"\n\t// 默认用户头像 (空字符串,由前端处理默认头像)\n\tDefaultAvatar = \"\"\n\t//默认更新服务器\n\tUpdateURL = \"http://dl.cactifans.com/stable/\"\n\n\t// 上传目录\n\tUploadDir = \"./upload\"\n\t// 背景图片上传目录\n\tBackgroundImageDir = \"./upload/background\"\n\t// 允许的图片格式\n\tAllowedImageExts = \".jpg,.jpeg,.png,.gif,.bmp,.webp,.svg\"\n\t// 最大文件大小 (10MB)\n\tMaxFileSize = 10 * 1024 * 1024\n)\n\n// TableName 表名前缀\nfunc TableName(str string) string {\n\treturn fmt.Sprintf(\"%s%s\", \"zbxtable_\", str)\n}\n\n// 接收数据库信息初始化\nfunc ModelInit(dbtype, dbhost, dbuser, dbpass, dbname, dbport string) {\n\t// 直接使用 GORM\n\trunmode := GetConfKey(\"runmode\")\n\terr := InitGormDB(dbtype, dbhost, dbuser, dbpass, dbname, dbport, runmode)\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to connect database: \", err)\n\t\tos.Exit(1)\n\t}\n\tlogger.Log.Info(\"Database connected!\")\n\t// 自动迁移表\n\terr = AutoMigrate()\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to auto migrate: \", err)\n\t\tos.Exit(1)\n\t}\n\t// 基础数据初始化\n\tDatabaseInit()\n\t// 使用go-cache替代Redis(不再需要Redis连接)\n\tInitCache()\n}\n\nfunc InitWechat() {\n\t// 首先检查企业微信开关是否启用\n\twechatEnabled := GetConfigValueByKey(\"wechat_enabled\", \"0\")\n\tif wechatEnabled != \"1\" {\n\t\tlogger.Log.Info(\"WeChat is disabled in system config, skipping WeChat initialization\")\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\t// 开关已启用,从数据库读取企业微信配置\n\tagentIDStr := GetConfigValueByKey(\"wechat_agentid\", \"\")\n\tcorpid := GetConfigValueByKey(\"wechat_corpid\", \"\")\n\tsecret := GetConfigValueByKey(\"wechat_secret\", \"\") // 这里会自动解密\n\n\t// 验证必填配置项\n\tif agentIDStr == \"\" || corpid == \"\" || secret == \"\" {\n\t\tlogger.Log.Info(\"WeChat is enabled but configuration is incomplete (agentid/corpid/secret), WeChat app will not be initialized\")\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\t// 解析 AgentID\n\tAgentId, err := strconv.ParseInt(agentIDStr, 10, 64)\n\tif err != nil {\n\t\tlogger.Log.Error(\"wechat_agentid parse error:\", err)\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\n\t// 初始化企业微信客户端\n\tclient := workwx.New(corpid)\n\tWeApp = client.WithApp(secret, AgentId)\n\tWeApp.SpawnAccessTokenRefresher()\n\tlogger.Log.Info(\"WeChat inited successfully!\")\n}\n\n// DatabaseInit 数据初始化\nfunc DatabaseInit() {\n\t//数据初始化操作 - 使用 GORM\n\tvar v User\n\terr := DB.Where(\"username = ?\", \"admin\").First(&v).Error\n\t//检查权限\n\tif err == nil && v.Operation == \"\" {\n\t\terr := DB.Model(&User{}).Where(\"id = ?\", v.ID).Update(\"operation\", \"['add', 'edit', 'delete','update']\").Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"update admin operation successfully\")\n\t}\n\t//添加管理员账号\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\tlogger.Log.Info(\"the admin user does not exist, create a new admin account later!\")\n\n\t\t// 构建默认主题配置\n\t\tdefaultThemeConfig := map[string]interface{}{\n\t\t\t\"theme\": map[string]string{\n\t\t\t\t\"color\": \"#722ed1\",\n\t\t\t\t\"mode\": \"dark\",\n\t\t\t\t\"success\": \"#52c41a\",\n\t\t\t\t\"warning\": \"#faad14\",\n\t\t\t\t\"error\": \"#f5222f\",\n\t\t\t},\n\t\t\t\"animate\": map[string]interface{}{\n\t\t\t\t\"disabled\": true,\n\t\t\t\t\"name\": \"lightSpeed\",\n\t\t\t\t\"direction\": \"left\",\n\t\t\t},\n\t\t}\n\n\t\t// 将配置转换为 JSON 字符串\n\t\tthemeJSON, err := json.Marshal(defaultThemeConfig)\n\t\tif err != nil {\n\t\t\tlogger.Log.Error(\"Failed to marshal default theme config:\", err)\n\t\t\tthemeJSON = []byte(`{\"theme\":{\"color\":\"#722ed1\",\"mode\":\"dark\",\"success\":\"#52c41a\",\"warning\":\"#faad14\",\"error\":\"#f5222f\"},\"animate\":{\"disabled\":true,\"name\":\"lightSpeed\",\"direction\":\"left\"}}`)\n\t\t}\n\n\t\t// 使用结构体插入管理员账号\n\t\tpassword, _ := utils.PasswordHash(\"Zbxtable\")\n\t\tuser := User{\n\t\t\tUsername: \"admin\",\n\t\t\tPassword: password,\n\t\t\tAvatar: DefaultAvatar,\n\t\t\tRole: \"admin\",\n\t\t\tOperation: \"['add', 'edit', 'delete','update']\",\n\t\t\tTheme: string(themeJSON),\n\t\t\tStatus: 0,\n\t\t}\n\t\terr = DB.Create(&user).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"create an administrator account successfully, the admin ID is:\", user.ID)\n\t}\n\t//初始化系统数据\n\tvar cnt []System\n\terr = DB.Find(&cnt).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn\n\t}\n\tif len(cnt) == 0 {\n\t\tnow := time.Now()\n\t\tsys := []System{\n\t\t\t{Name: \"Linux操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"Windows操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"网络设备\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"物理服务器\", Status: 0, InitedAt: &now},\n\t\t}\n\t\terr := DB.Create(&sys).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init system info error!\", err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"Init system data successfully!\")\n\t}\n\t//告警默认规则初始化 default rule\n\tvar rules []Rule\n\terr = DB.Where(\"m_type = ?\", \"2\").Find(&rules).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn\n\t}\n\tif len(rules) == 0 {\n\t\t// 使用 \"*\" 作为全局默认规则,匹配所有租户\n\t\tdefaultRule := []Rule{\n\t\t\t{\n\t\t\t\tName: \"全局默认规则\",\n\t\t\t\tZIDs: \"*\",\n\t\t\t\tMType: \"2\",\n\t\t\t\tChannel: \"wechat_robot\",\n\t\t\t\tUserIds: \"1\",\n\t\t\t\tSweek: \"0,1,2,3,4,5,6\",\n\t\t\t\tStime: \"00:00\",\n\t\t\t\tEtime: \"23.59\",\n\t\t\t\tStatus: \"0\"},\n\t\t}\n\t\terr := DB.Create(&defaultRule).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init default rule error!\", err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"Init default rule successfully!\")\n\t}\n\t// 初始化加密密钥(如果不存在)\n\tvar encryptionKeyConfig Config\n\terr = DB.Where(\"config_key = ?\", \"encryption_key\").First(&encryptionKeyConfig).Error\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t// 生成随机加密密钥\n\t\trandomKey := utils.GenerateRandomKey()\n\t\tencryptionKeyConfig = Config{\n\t\t\tName: \"加密密钥\",\n\t\t\tConfigKey: \"encryption_key\",\n\t\t\tConfigValue: randomKey,\n\t\t\tComment: \"用于加密存储敏感信息(如Zabbix密码和Token)的密钥,系统自动生成,不可修改\",\n\t\t}\n\t\tif insertErr := DB.Create(&encryptionKeyConfig).Error; insertErr != nil {\n\t\t\tlogger.Log.Error(\"Init encryption_key error:\", insertErr)\n\t\t} else {\n\t\t\tlogger.Log.Info(\"Init encryption_key successfully! Key length:\", len(randomKey))\n\t\t}\n\t}\n\n\t// 获取结构化的默认配置\n\tdefaultConfigs := getDefaultConfigs()\n\n\tfor _, cfgItem := range defaultConfigs {\n\t\tvar existing Config\n\t\terr = DB.Where(\"config_key = ?\", cfgItem.ConfigKey).First(&existing).Error\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\tif insertErr := DB.Create(&cfgItem).Error; insertErr != nil {\n\t\t\t\tlogger.Log.Infof(\"Init config key %s error: %v\", cfgItem.ConfigKey, insertErr)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tlogger.Log.Infof(\"Init config key %s successfully!\", cfgItem.ConfigKey)\n\t\t} else {\n\t\t\tlogger.Log.Infof(\"Init config key %s error: %v\", cfgItem.ConfigKey, err)\n\t\t\tcontinue\n\t\t}\n\t}\n\t//默认菜单初始化\n\tInitMenuData()\n\t//检查并添加缺失的菜单项(用于版本升级)\n\tCheckAndAddMenus()\n\n}\n\nfunc GetConfKey(v string) string {\n\t// 优先从 .env 中读取(便于开发环境配置敏感信息且不提交到 git)\n\tenvConfigOnce.Do(func() {\n\t\tfile, err := os.Open(\".env\")\n\t\tif err != nil {\n\t\t\t// 没有 .env 文件时直接跳过,后面回退到 app.conf\n\t\t\tenvFileExists = false\n\t\t\treturn\n\t\t}\n\t\tdefer file.Close()\n\n\t\tenvFileExists = true\n\t\tenvConfig = make(map[string]string)\n\n\t\tscanner := bufio.NewScanner(file)\n\t\tfor scanner.Scan() {\n\t\t\tline := strings.TrimSpace(scanner.Text())\n\t\t\tif line == \"\" || strings.HasPrefix(line, \"#\") || strings.HasPrefix(line, \";\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tparts := strings.SplitN(line, \"=\", 2)\n\t\t\tif len(parts) != 2 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tkey := strings.TrimSpace(parts[0])\n\t\t\tval := strings.TrimSpace(parts[1])\n\t\t\t// 去掉可能的引号\n\t\t\tval = strings.Trim(val, `\"'`)\n\t\t\tif key != \"\" {\n\t\t\t\tenvConfig[key] = val // 即使 val 为空也认为是有效配置\n\t\t\t}\n\t\t}\n\t})\n\n\t// 如果存在 .env 文件,则所有配置完全由 .env 决定:\n\t// - key 存在:返回对应值(可以是空字符串)\n\t// - key 不存在:返回空字符串\n\tif envFileExists {\n\t\tif envConfig == nil {\n\t\t\treturn \"\"\n\t\t}\n\t\tif val, ok := envConfig[v]; ok {\n\t\t\treturn val\n\t\t}\n\t\treturn \"\"\n\t}\n\t// 其次从 config/app.conf 读取\n\tcfg, err := ini.Load(\"./config/app.conf\")\n\tif err != nil {\n\t\tlogger.Log.Error(err)\n\t\treturn \"\"\n\t}\n\tp, err := cfg.Section(\"\").GetKey(v)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn p.String()\n}\n\n// IsPasswordConfigured 检查是否配置了密码(用于图形查看)\n// 优先检查数据库中是否有激活的 Zabbix 实例配置了用户名和密码\n// 注意:查看图形需要用户名和密码,Token 方式无法用于 Web 登录\nfunc IsPasswordConfigured() bool {\n\t// 1. 优先检查数据库中是否有激活的 Zabbix 实例\n\tvar activeInstance ZabbixInstance\n\terr := DB.Where(\"is_active = ?\", true).First(&activeInstance).Error\n\tif err == nil {\n\t\t// 找到激活的实例,检查是否配置了用户名和密码(查看图形必须用密码,Token 无法用于 Web 登录)\n\t\tif strings.TrimSpace(activeInstance.User) != \"\" && strings.TrimSpace(activeInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 2. 如果数据库中没有激活实例,检查是否有任何可用的实例配置了密码\n\tvar anyInstance ZabbixInstance\n\terr = DB.Where(\"enabled = ?\", true).First(&anyInstance).Error\n\tif err == nil {\n\t\t// 找到可用的实例,检查是否配置了用户名和密码\n\t\tif strings.TrimSpace(anyInstance.User) != \"\" && strings.TrimSpace(anyInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 3. 回退到配置文件检查(兼容旧版本)\n\tpass := GetConfKey(\"zabbix_pass\")\n\tuser := GetConfKey(\"zabbix_user\")\n\treturn pass != \"\" && user != \"\"\n}\n\n// GetEncryptionKey 获取加密密钥\n// 优先从数据库读取,如果数据库中没有则从环境变量读取,最后使用默认密钥\nfunc GetEncryptionKey() string {\n\t// 1. 优先从数据库读取\n\tkey := GetConfigValueByKey(\"encryption_key\", \"\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 2. 从环境变量或配置文件读取\n\tkey = GetConfKey(\"encryption_key\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 3. 使用默认密钥(不应该到这里,因为 DatabaseInit 会生成)\n\tlogger.Log.Warn(\"使用默认加密密钥,建议在数据库中配置 encryption_key\")\n\treturn \"zbxtable-default-encryption-key-2024\"\n}\n\n// getDefaultConfigs 返回系统默认配置列表\n// 配置按功能模块分组,便于维护和管理\nfunc getDefaultConfigs() []Config {\n\tconfigs := make([]Config, 0)\n\n\t// 系统外观配置\n\tconfigs = append(configs, getSystemAppearanceConfigs()...)\n\n\t// 初始配置状态\n\tconfigs = append(configs, getInitialSetupConfigs()...)\n\n\t// Dashboard 相关配置\n\tconfigs = append(configs, getDashboardConfigs()...)\n\n\t// 邮件配置\n\tconfigs = append(configs, getEmailConfigs()...)\n\n\t// 企业微信配置\n\tconfigs = append(configs, getWeChatConfigs()...)\n\n\t// AI 配置\n\tconfigs = append(configs, getAIConfigs()...)\n\n\treturn configs\n}\n\n// getSystemAppearanceConfigs 系统外观配置\nfunc getSystemAppearanceConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"系统名称\",\n\t\t\tConfigKey: \"system_name\",\n\t\t\tConfigValue: \"ZbxTable\",\n\t\t\tComment: \"系统显示的名称\",\n\t\t},\n\t\t{\n\t\t\tName: \"系统Logo\",\n\t\t\tConfigKey: \"system_logo\",\n\t\t\tConfigValue: \"data:image/png;base64,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\",\n\t\t\tComment: \"系统Logo的base64编码数据\",\n\t\t},\n\t}\n}\n\n// getInitialSetupConfigs 初始配置状态\nfunc getInitialSetupConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"初始配置完成\",\n\t\t\tConfigKey: \"initial_setup_completed\",\n\t\t\tConfigValue: \"0\",\n\t\t\tComment: \"标记系统是否完成初始配置:1 已完成,0 未完成\",\n\t\t},\n\t}\n}\n\n// getDashboardConfigs Dashboard 相关配置\nfunc getDashboardConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"数据面板\",\n\t\t\tConfigKey: \"zbx_dash\",\n\t\t\tConfigValue: \"0\",\n\t\t\tComment: \"是否开启Zabbix看板:1 开启,0 关闭\",\n\t\t},\n\t\t{\n\t\t\tName: \"面板配置\",\n\t\t\tConfigKey: \"dash_id\",\n\t\t\tConfigValue: \"1\",\n\t\t\tComment: \"需要引入的Zabbix面板的ID,默认为1\",\n\t\t},\n\t\t{\n\t\t\tName: \"主机分类同步\",\n\t\t\tConfigKey: \"sync_inventory\",\n\t\t\tConfigValue: \"1\",\n\t\t\tComment: \"主机分类同步计划任务是否启用:1 启用,0 不启用\",\n\t\t},\n\t\t{\n\t\t\tName: \"Webhook回调地址\",\n\t\t\tConfigKey: \"webhook_url\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"webhook通知地址\",\n\t\t},\n\t}\n}\n\n// getEmailConfigs 邮件配置\nfunc getEmailConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"邮件发件人\",\n\t\t\tConfigKey: \"email_from\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"告警邮件发件人邮箱地址\",\n\t\t},\n\t\t{\n\t\t\tName: \"邮件昵称\",\n\t\t\tConfigKey: \"email_nickname\",\n\t\t\tConfigValue: \"ZbxTable\",\n\t\t\tComment: \"告警邮件显示的发件人昵称\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 密码/授权码\",\n\t\t\tConfigKey: \"email_secret\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"SMTP 登录密码或授权码\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 服务器\",\n\t\t\tConfigKey: \"email_host\",\n\t\t\tConfigValue: \"smtp.qq.com\",\n\t\t\tComment: \"SMTP 服务器地址\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 端口\",\n\t\t\tConfigKey: \"email_port\",\n\t\t\tConfigValue: \"465\",\n\t\t\tComment: \"SMTP 端口号\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 使用 SSL\",\n\t\t\tConfigKey: \"email_isSSl\",\n\t\t\tConfigValue: \"true\",\n\t\t\tComment: \"是否启用 SSL:true/false\",\n\t\t},\n\t}\n}\n\n// getWeChatConfigs 企业微信配置\nfunc getWeChatConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"企业微信开关\",\n\t\t\tConfigKey: \"wechat_enabled\",\n\t\t\tConfigValue: \"0\",\n\t\t\tComment: \"是否启用企业微信:1 启用,0 禁用\",\n\t\t},\n\t\t{\n\t\t\tName: \"企业微信 AgentID\",\n\t\t\tConfigKey: \"wechat_agentid\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"企业微信应用的 AgentID\",\n\t\t},\n\t\t{\n\t\t\tName: \"企业微信 CorpID\",\n\t\t\tConfigKey: \"wechat_corpid\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"企业微信企业ID\",\n\t\t},\n\t\t{\n\t\t\tName: \"企业微信 Secret\",\n\t\t\tConfigKey: \"wechat_secret\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"企业微信应用的 Secret\",\n\t\t},\n\t}\n}\n\n// getAIConfigs AI 配置\nfunc getAIConfigs() []Config {\n\treturn []Config{\n\t\t// AI 类型选择\n\t\t{\n\t\t\tName: \"AI 类型\",\n\t\t\tConfigKey: \"ai_type\",\n\t\t\tConfigValue: \"ollama\",\n\t\t\tComment: \"选择使用的 AI 服务类型:ollama 或 deepseek\",\n\t\t},\n\t\t// Ollama 配置\n\t\t{\n\t\t\tName: \"Ollama Host\",\n\t\t\tConfigKey: \"ollama_host\",\n\t\t\tConfigValue: \"http://localhost:11434\",\n\t\t\tComment: \"Ollama 服务地址,如 http://127.0.0.1:11434\",\n\t\t},\n\t\t{\n\t\t\tName: \"Ollama Model\",\n\t\t\tConfigKey: \"ollama_model\",\n\t\t\tConfigValue: \"deepseek-r1:32b\",\n\t\t\tComment: \"默认使用的大模型名称\",\n\t\t},\n\t\t// Deepseek 配置\n\t\t{\n\t\t\tName: \"Deepseek API Key\",\n\t\t\tConfigKey: \"deepseek_api_key\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"Deepseek API 密钥\",\n\t\t},\n\t\t{\n\t\t\tName: \"Deepseek Model\",\n\t\t\tConfigKey: \"deepseek_model\",\n\t\t\tConfigValue: \"deepseek-chat\",\n\t\t\tComment: \"Deepseek 模型名称,如 deepseek-chat\",\n\t\t},\n\t\t{\n\t\t\tName: \"Deepseek Base URL\",\n\t\t\tConfigKey: \"deepseek_base_url\",\n\t\t\tConfigValue: \"https://api.deepseek.com\",\n\t\t\tComment: \"Deepseek API 地址,默认为 https://api.deepseek.com\",\n\t\t},\n\t}\n}\n"}, "9424ed67417bdbc11c5fe6a7928e010e0cfbb0c6": {"t": 1785754909.2157228, "n": 18614, "h": "9161757b54a9c01e", "c": "package model\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\t\"zbxtable/pkg/logger\"\n\t\"zbxtable/pkg/utils\"\n\n\tzabbix \"github.com/canghai908/zabbix-go\"\n\t_ \"github.com/go-sql-driver/mysql\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t_ \"github.com/lib/pq\"\n\tworkwx \"github.com/xen0n/go-workwx\"\n\tini \"gopkg.in/ini.v1\"\n\t\"gorm.io/gorm\"\n)\n\nvar (\n\tAPI = &zabbix.API{}\n\tjson = jsoniter.Config{\n\t\tEscapeHTML: false,\n\t\tSortMapKeys: true,\n\t\tValidateJsonRawMessage: true,\n\t}.Froze()\n\tZBX_VER string\n\tZBX_V bool\n\tVersion string\n\tGitHash string\n\tBuildTime string\n\tWeApp *workwx.WorkwxApp // 不初始化,默认为 nil\n\n\tenvConfig map[string]string\n\tenvConfigOnce sync.Once\n\tenvFileExists bool\n)\n\nconst (\n\t//ms-agent 常量定义\n\tMSName = \"ms-agent\"\n\tMSUser = \"ms-agent\"\n\tMSGroup = \"MS-Agent\"\n\tMSMedia = \"MS-Agent\"\n\tMSAction = \"MS-Agent\"\n\t//webhook 常量定义\n\tWebhookName = \"ZbxTable\"\n\tWebhookAction = \"ZbxTable Webhook\"\n\tWebhookGroup = \"ZbxTable Webhook\"\n\tWebhookUser = \"zbxtable-webhook\"\n\t//down\n\tDownloadPath = \"./download/\"\n\tTplPath = \"./assets/templates/\"\n\t// 默认用户头像 (空字符串,由前端处理默认头像)\n\tDefaultAvatar = \"\"\n\t//默认更新服务器\n\tUpdateURL = \"http://dl.cactifans.com/stable/\"\n)\n\n// TableName 表名前缀\nfunc TableName(str string) string {\n\treturn fmt.Sprintf(\"%s%s\", \"zbxtable_\", str)\n}\n\n// 接收数据库信息初始化\nfunc ModelInit(dbtype, dbhost, dbuser, dbpass, dbname, dbport string) {\n\t// 直接使用 GORM\n\trunmode := GetConfKey(\"runmode\")\n\terr := InitGormDB(dbtype, dbhost, dbuser, dbpass, dbname, dbport, runmode)\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to connect database: \", err)\n\t\tos.Exit(1)\n\t}\n\tlogger.Log.Info(\"Database connected!\")\n\t// 自动迁移表\n\terr = AutoMigrate()\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to auto migrate: \", err)\n\t\tos.Exit(1)\n\t}\n\t// 基础数据初始化\n\tDatabaseInit()\n\t// 使用go-cache替代Redis(不再需要Redis连接)\n\tInitCache()\n}\n\nfunc InitWechat() {\n\t// 首先检查企业微信开关是否启用\n\twechatEnabled := GetConfigValueByKey(\"wechat_enabled\", \"0\")\n\tif wechatEnabled != \"1\" {\n\t\tlogger.Log.Info(\"WeChat is disabled in system config, skipping WeChat initialization\")\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\t// 开关已启用,从数据库读取企业微信配置\n\tagentIDStr := GetConfigValueByKey(\"wechat_agentid\", \"\")\n\tcorpid := GetConfigValueByKey(\"wechat_corpid\", \"\")\n\tsecret := GetConfigValueByKey(\"wechat_secret\", \"\") // 这里会自动解密\n\n\t// 验证必填配置项\n\tif agentIDStr == \"\" || corpid == \"\" || secret == \"\" {\n\t\tlogger.Log.Info(\"WeChat is enabled but configuration is incomplete (agentid/corpid/secret), WeChat app will not be initialized\")\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\t// 解析 AgentID\n\tAgentId, err := strconv.ParseInt(agentIDStr, 10, 64)\n\tif err != nil {\n\t\tlogger.Log.Error(\"wechat_agentid parse error:\", err)\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\n\t// 初始化企业微信客户端\n\tclient := workwx.New(corpid)\n\tWeApp = client.WithApp(secret, AgentId)\n\tWeApp.SpawnAccessTokenRefresher()\n\tlogger.Log.Info(\"WeChat inited successfully!\")\n}\n\n// DatabaseInit 数据初始化\nfunc DatabaseInit() {\n\t//数据初始化操作 - 使用 GORM\n\tvar v User\n\terr := DB.Where(\"username = ?\", \"admin\").First(&v).Error\n\t//检查权限\n\tif err == nil && v.Operation == \"\" {\n\t\terr := DB.Model(&User{}).Where(\"id = ?\", v.ID).Update(\"operation\", \"['add', 'edit', 'delete','update']\").Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"update admin operation successfully\")\n\t}\n\t//添加管理员账号\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\tlogger.Log.Info(\"the admin user does not exist, create a new admin account later!\")\n\n\t\t// 构建默认主题配置\n\t\tdefaultThemeConfig := map[string]interface{}{\n\t\t\t\"theme\": map[string]string{\n\t\t\t\t\"color\": \"#722ed1\",\n\t\t\t\t\"mode\": \"dark\",\n\t\t\t\t\"success\": \"#52c41a\",\n\t\t\t\t\"warning\": \"#faad14\",\n\t\t\t\t\"error\": \"#f5222f\",\n\t\t\t},\n\t\t\t\"animate\": map[string]interface{}{\n\t\t\t\t\"disabled\": true,\n\t\t\t\t\"name\": \"lightSpeed\",\n\t\t\t\t\"direction\": \"left\",\n\t\t\t},\n\t\t}\n\n\t\t// 将配置转换为 JSON 字符串\n\t\tthemeJSON, err := json.Marshal(defaultThemeConfig)\n\t\tif err != nil {\n\t\t\tlogger.Log.Error(\"Failed to marshal default theme config:\", err)\n\t\t\tthemeJSON = []byte(`{\"theme\":{\"color\":\"#722ed1\",\"mode\":\"dark\",\"success\":\"#52c41a\",\"warning\":\"#faad14\",\"error\":\"#f5222f\"},\"animate\":{\"disabled\":true,\"name\":\"lightSpeed\",\"direction\":\"left\"}}`)\n\t\t}\n\n\t\t// 使用结构体插入管理员账号\n\t\tpassword, _ := utils.PasswordHash(\"Zbxtable\")\n\t\tuser := User{\n\t\t\tUsername: \"admin\",\n\t\t\tPassword: password,\n\t\t\tAvatar: DefaultAvatar,\n\t\t\tRole: \"admin\",\n\t\t\tOperation: \"['add', 'edit', 'delete','update']\",\n\t\t\tTheme: string(themeJSON),\n\t\t\tStatus: 0,\n\t\t}\n\t\terr = DB.Create(&user).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"create an administrator account successfully, the admin ID is:\", user.ID)\n\t}\n\t//初始化系统数据\n\tvar cnt []System\n\terr = DB.Find(&cnt).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn\n\t}\n\tif len(cnt) == 0 {\n\t\tnow := time.Now()\n\t\tsys := []System{\n\t\t\t{Name: \"Linux操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"Windows操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"网络设备\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"物理服务器\", Status: 0, InitedAt: &now},\n\t\t}\n\t\terr := DB.Create(&sys).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init system info error!\", err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"Init system data successfully!\")\n\t}\n\t//告警默认规则初始化 default rule\n\tvar rules []Rule\n\terr = DB.Where(\"m_type = ?\", \"2\").Find(&rules).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn\n\t}\n\tif len(rules) == 0 {\n\t\t// 使用 \"*\" 作为全局默认规则,匹配所有租户\n\t\tdefaultRule := []Rule{\n\t\t\t{\n\t\t\t\tName: \"全局默认规则\",\n\t\t\t\tZIDs: \"*\",\n\t\t\t\tMType: \"2\",\n\t\t\t\tChannel: \"wechat_robot\",\n\t\t\t\tUserIds: \"1\",\n\t\t\t\tSweek: \"0,1,2,3,4,5,6\",\n\t\t\t\tStime: \"00:00\",\n\t\t\t\tEtime: \"23.59\",\n\t\t\t\tStatus: \"0\"},\n\t\t}\n\t\terr := DB.Create(&defaultRule).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init default rule error!\", err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"Init default rule successfully!\")\n\t}\n\t// 初始化加密密钥(如果不存在)\n\tvar encryptionKeyConfig Config\n\terr = DB.Where(\"config_key = ?\", \"encryption_key\").First(&encryptionKeyConfig).Error\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t// 生成随机加密密钥\n\t\trandomKey := utils.GenerateRandomKey()\n\t\tencryptionKeyConfig = Config{\n\t\t\tName: \"加密密钥\",\n\t\t\tConfigKey: \"encryption_key\",\n\t\t\tConfigValue: randomKey,\n\t\t\tComment: \"用于加密存储敏感信息(如Zabbix密码和Token)的密钥,系统自动生成,不可修改\",\n\t\t}\n\t\tif insertErr := DB.Create(&encryptionKeyConfig).Error; insertErr != nil {\n\t\t\tlogger.Log.Error(\"Init encryption_key error:\", insertErr)\n\t\t} else {\n\t\t\tlogger.Log.Info(\"Init encryption_key successfully! Key length:\", len(randomKey))\n\t\t}\n\t}\n\n\t// 获取结构化的默认配置\n\tdefaultConfigs := getDefaultConfigs()\n\n\tfor _, cfgItem := range defaultConfigs {\n\t\tvar existing Config\n\t\terr = DB.Where(\"config_key = ?\", cfgItem.ConfigKey).First(&existing).Error\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\tif insertErr := DB.Create(&cfgItem).Error; insertErr != nil {\n\t\t\t\tlogger.Log.Infof(\"Init config key %s error: %v\", cfgItem.ConfigKey, insertErr)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tlogger.Log.Infof(\"Init config key %s successfully!\", cfgItem.ConfigKey)\n\t\t} else {\n\t\t\tlogger.Log.Infof(\"Init config key %s error: %v\", cfgItem.ConfigKey, err)\n\t\t\tcontinue\n\t\t}\n\t}\n\t//默认菜单初始化\n\tInitMenuData()\n\t//检查并添加缺失的菜单项(用于版本升级)\n\tCheckAndAddMenus()\n\n}\n\nfunc GetConfKey(v string) string {\n\t// 优先从 .env 中读取(便于开发环境配置敏感信息且不提交到 git)\n\tenvConfigOnce.Do(func() {\n\t\tfile, err := os.Open(\".env\")\n\t\tif err != nil {\n\t\t\t// 没有 .env 文件时直接跳过,后面回退到 app.conf\n\t\t\tenvFileExists = false\n\t\t\treturn\n\t\t}\n\t\tdefer file.Close()\n\n\t\tenvFileExists = true\n\t\tenvConfig = make(map[string]string)\n\n\t\tscanner := bufio.NewScanner(file)\n\t\tfor scanner.Scan() {\n\t\t\tline := strings.TrimSpace(scanner.Text())\n\t\t\tif line == \"\" || strings.HasPrefix(line, \"#\") || strings.HasPrefix(line, \";\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tparts := strings.SplitN(line, \"=\", 2)\n\t\t\tif len(parts) != 2 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tkey := strings.TrimSpace(parts[0])\n\t\t\tval := strings.TrimSpace(parts[1])\n\t\t\t// 去掉可能的引号\n\t\t\tval = strings.Trim(val, `\"'`)\n\t\t\tif key != \"\" {\n\t\t\t\tenvConfig[key] = val // 即使 val 为空也认为是有效配置\n\t\t\t}\n\t\t}\n\t})\n\n\t// 如果存在 .env 文件,则所有配置完全由 .env 决定:\n\t// - key 存在:返回对应值(可以是空字符串)\n\t// - key 不存在:返回空字符串\n\tif envFileExists {\n\t\tif envConfig == nil {\n\t\t\treturn \"\"\n\t\t}\n\t\tif val, ok := envConfig[v]; ok {\n\t\t\treturn val\n\t\t}\n\t\treturn \"\"\n\t}\n\t// 其次从 config/app.conf 读取\n\tcfg, err := ini.Load(\"./config/app.conf\")\n\tif err != nil {\n\t\tlogger.Log.Error(err)\n\t\treturn \"\"\n\t}\n\tp, err := cfg.Section(\"\").GetKey(v)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn p.String()\n}\n\n// IsPasswordConfigured 检查是否配置了密码(用于图形查看)\n// 优先检查数据库中是否有激活的 Zabbix 实例配置了用户名和密码\n// 注意:查看图形需要用户名和密码,Token 方式无法用于 Web 登录\nfunc IsPasswordConfigured() bool {\n\t// 1. 优先检查数据库中是否有激活的 Zabbix 实例\n\tvar activeInstance ZabbixInstance\n\terr := DB.Where(\"is_active = ?\", true).First(&activeInstance).Error\n\tif err == nil {\n\t\t// 找到激活的实例,检查是否配置了用户名和密码(查看图形必须用密码,Token 无法用于 Web 登录)\n\t\tif strings.TrimSpace(activeInstance.User) != \"\" && strings.TrimSpace(activeInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 2. 如果数据库中没有激活实例,检查是否有任何可用的实例配置了密码\n\tvar anyInstance ZabbixInstance\n\terr = DB.Where(\"enabled = ?\", true).First(&anyInstance).Error\n\tif err == nil {\n\t\t// 找到可用的实例,检查是否配置了用户名和密码\n\t\tif strings.TrimSpace(anyInstance.User) != \"\" && strings.TrimSpace(anyInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 3. 回退到配置文件检查(兼容旧版本)\n\tpass := GetConfKey(\"zabbix_pass\")\n\tuser := GetConfKey(\"zabbix_user\")\n\treturn pass != \"\" && user != \"\"\n}\n\n// GetEncryptionKey 获取加密密钥\n// 优先从数据库读取,如果数据库中没有则从环境变量读取,最后使用默认密钥\nfunc GetEncryptionKey() string {\n\t// 1. 优先从数据库读取\n\tkey := GetConfigValueByKey(\"encryption_key\", \"\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 2. 从环境变量或配置文件读取\n\tkey = GetConfKey(\"encryption_key\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 3. 使用默认密钥(不应该到这里,因为 DatabaseInit 会生成)\n\tlogger.Log.Warn(\"使用默认加密密钥,建议在数据库中配置 encryption_key\")\n\treturn \"zbxtable-default-encryption-key-2024\"\n}\n\n// getDefaultConfigs 返回系统默认配置列表\n// 配置按功能模块分组,便于维护和管理\nfunc getDefaultConfigs() []Config {\n\tconfigs := make([]Config, 0)\n\n\t// 系统外观配置\n\tconfigs = append(configs, getSystemAppearanceConfigs()...)\n\n\t// 初始配置状态\n\tconfigs = append(configs, getInitialSetupConfigs()...)\n\n\t// Dashboard 相关配置\n\tconfigs = append(configs, getDashboardConfigs()...)\n\n\t// 邮件配置\n\tconfigs = append(configs, getEmailConfigs()...)\n\n\t// 企业微信配置\n\tconfigs = append(configs, getWeChatConfigs()...)\n\n\t// AI 配置\n\tconfigs = append(configs, getAIConfigs()...)\n\n\treturn configs\n}\n\n// getSystemAppearanceConfigs 系统外观配置\nfunc getSystemAppearanceConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"系统名称\",\n\t\t\tConfigKey: \"system_name\",\n\t\t\tConfigValue: \"ZbxTable\",\n\t\t\tComment: \"系统显示的名称\",\n\t\t},\n\t\t{\n\t\t\tName: \"系统Logo\",\n\t\t\tConfigKey: \"system_logo\",\n\t\t\tConfigValue: \"data:image/png;base64,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\",\n\t\t\tComment: \"系统Logo的base64编码数据\",\n\t\t},\n\t}\n}\n\n// getInitialSetupConfigs 初始配置状态\nfunc getInitialSetupConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"初始配置完成\",\n\t\t\tConfigKey: \"initial_setup_completed\",\n\t\t\tConfigValue: \"0\",\n\t\t\tComment: \"标记系统是否完成初始配置:1 已完成,0 未完成\",\n\t\t},\n\t}\n}\n\n// getDashboardConfigs Dashboard 相关配置\nfunc getDashboardConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"数据面板\",\n\t\t\tConfigKey: \"zbx_dash\",\n\t\t\tConfigValue: \"0\",\n\t\t\tComment: \"是否开启Zabbix看板:1 开启,0 关闭\",\n\t\t},\n\t\t{\n\t\t\tName: \"面板配置\",\n\t\t\tConfigKey: \"dash_id\",\n\t\t\tConfigValue: \"1\",\n\t\t\tComment: \"需要引入的Zabbix面板的ID,默认为1\",\n\t\t},\n\t\t{\n\t\t\tName: \"主机分类同步\",\n\t\t\tConfigKey: \"sync_inventory\",\n\t\t\tConfigValue: \"1\",\n\t\t\tComment: \"主机分类同步计划任务是否启用:1 启用,0 不启用\",\n\t\t},\n\t\t{\n\t\t\tName: \"Webhook回调地址\",\n\t\t\tConfigKey: \"webhook_url\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"webhook通知地址\",\n\t\t},\n\t}\n}\n\n// getEmailConfigs 邮件配置\nfunc getEmailConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"邮件发件人\",\n\t\t\tConfigKey: \"email_from\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"告警邮件发件人邮箱地址\",\n\t\t},\n\t\t{\n\t\t\tName: \"邮件昵称\",\n\t\t\tConfigKey: \"email_nickname\",\n\t\t\tConfigValue: \"ZbxTable\",\n\t\t\tComment: \"告警邮件显示的发件人昵称\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 密码/授权码\",\n\t\t\tConfigKey: \"email_secret\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"SMTP 登录密码或授权码\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 服务器\",\n\t\t\tConfigKey: \"email_host\",\n\t\t\tConfigValue: \"smtp.qq.com\",\n\t\t\tComment: \"SMTP 服务器地址\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 端口\",\n\t\t\tConfigKey: \"email_port\",\n\t\t\tConfigValue: \"465\",\n\t\t\tComment: \"SMTP 端口号\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 使用 SSL\",\n\t\t\tConfigKey: \"email_isSSl\",\n\t\t\tConfigValue: \"true\",\n\t\t\tComment: \"是否启用 SSL:true/false\",\n\t\t},\n\t}\n}\n\n// getWeChatConfigs 企业微信配置\nfunc getWeChatConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"企业微信开关\",\n\t\t\tConfigKey: \"wechat_enabled\",\n\t\t\tConfigValue: \"0\",\n\t\t\tComment: \"是否启用企业微信:1 启用,0 禁用\",\n\t\t},\n\t\t{\n\t\t\tName: \"企业微信 AgentID\",\n\t\t\tConfigKey: \"wechat_agentid\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"企业微信应用的 AgentID\",\n\t\t},\n\t\t{\n\t\t\tName: \"企业微信 CorpID\",\n\t\t\tConfigKey: \"wechat_corpid\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"企业微信企业ID\",\n\t\t},\n\t\t{\n\t\t\tName: \"企业微信 Secret\",\n\t\t\tConfigKey: \"wechat_secret\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"企业微信应用的 Secret\",\n\t\t},\n\t}\n}\n\n// getAIConfigs AI 配置\nfunc getAIConfigs() []Config {\n\treturn []Config{\n\t\t// AI 类型选择\n\t\t{\n\t\t\tName: \"AI 类型\",\n\t\t\tConfigKey: \"ai_type\",\n\t\t\tConfigValue: \"ollama\",\n\t\t\tComment: \"选择使用的 AI 服务类型:ollama 或 deepseek\",\n\t\t},\n\t\t// Ollama 配置\n\t\t{\n\t\t\tName: \"Ollama Host\",\n\t\t\tConfigKey: \"ollama_host\",\n\t\t\tConfigValue: \"http://localhost:11434\",\n\t\t\tComment: \"Ollama 服务地址,如 http://127.0.0.1:11434\",\n\t\t},\n\t\t{\n\t\t\tName: \"Ollama Model\",\n\t\t\tConfigKey: \"ollama_model\",\n\t\t\tConfigValue: \"deepseek-r1:32b\",\n\t\t\tComment: \"默认使用的大模型名称\",\n\t\t},\n\t\t// Deepseek 配置\n\t\t{\n\t\t\tName: \"Deepseek API Key\",\n\t\t\tConfigKey: \"deepseek_api_key\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"Deepseek API 密钥\",\n\t\t},\n\t\t{\n\t\t\tName: \"Deepseek Model\",\n\t\t\tConfigKey: \"deepseek_model\",\n\t\t\tConfigValue: \"deepseek-chat\",\n\t\t\tComment: \"Deepseek 模型名称,如 deepseek-chat\",\n\t\t},\n\t\t{\n\t\t\tName: \"Deepseek Base URL\",\n\t\t\tConfigKey: \"deepseek_base_url\",\n\t\t\tConfigValue: \"https://api.deepseek.com\",\n\t\t\tComment: \"Deepseek API 地址,默认为 https://api.deepseek.com\",\n\t\t},\n\t}\n}\n"}, "c3d90ef5deb8b0bc0714d6f2902a8d7e668be4c2": {"t": 1785754910.76444, "n": 18614, "h": "2704bb03eed25818", "c": "package model\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\t\"zbxtable/pkg/logger\"\n\t\"zbxtable/pkg/utils\"\n\n\tzabbix \"github.com/canghai908/zabbix-go\"\n\t_ \"github.com/go-sql-driver/mysql\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t_ \"github.com/lib/pq\"\n\tworkwx \"github.com/xen0n/go-workwx\"\n\tini \"gopkg.in/ini.v1\"\n\t\"gorm.io/gorm\"\n)\n\nvar (\n\tAPI = &zabbix.API{}\n\tjson = jsoniter.Config{\n\t\tEscapeHTML: false,\n\t\tSortMapKeys: true,\n\t\tValidateJsonRawMessage: true,\n\t}.Froze()\n\tZBX_VER string\n\tZBX_V bool\n\tVersion string\n\tGitHash string\n\tBuildTime string\n\tWeApp *workwx.WorkwxApp // 不初始化,默认为 nil\n\n\tenvConfig map[string]string\n\tenvConfigOnce sync.Once\n\tenvFileExists bool\n)\n\nconst (\n\t//ms-agent 常量定义\n\tMSName = \"ms-agent\"\n\tMSUser = \"ms-agent\"\n\tMSGroup = \"MS-Agent\"\n\tMSMedia = \"MS-Agent\"\n\tMSAction = \"MS-Agent\"\n\t//webhook 常量定义\n\tWebhookName = \"ZbxTable\"\n\tWebhookAction = \"ZbxTable Webhook\"\n\tWebhookGroup = \"ZbxTable Webhook\"\n\tWebhookUser = \"zbxtable-webhook\"\n\t//down\n\tDownloadPath = \"./download/\"\n\tTplPath = \"./assets/templates/\"\n\t// 默认用户头像 (空字符串,由前端处理默认头像)\n\tDefaultAvatar = \"\"\n\t//默认更新服务器\n\tUpdateURL = \"http://dl.cactifans.com/stable/\"\n)\n\n// TableName 表名前缀\nfunc TableName(str string) string {\n\treturn fmt.Sprintf(\"%s%s\", \"zbxtable_\", str)\n}\n\n// 接收数据库信息初始化\nfunc ModelInit(dbtype, dbhost, dbuser, dbpass, dbname, dbport string) {\n\t// 直接使用 GORM\n\trunmode := GetConfKey(\"runmode\")\n\terr := InitGormDB(dbtype, dbhost, dbuser, dbpass, dbname, dbport, runmode)\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to connect database: \", err)\n\t\tos.Exit(1)\n\t}\n\tlogger.Log.Info(\"Database connected!\")\n\t// 自动迁移表\n\terr = AutoMigrate()\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to auto migrate: \", err)\n\t\tos.Exit(1)\n\t}\n\t// 基础数据初始化\n\tDatabaseInit()\n\t// 使用go-cache替代Redis(不再需要Redis连接)\n\tInitCache()\n}\n\nfunc InitWechat() {\n\t// 首先检查企业微信开关是否启用\n\twechatEnabled := GetConfigValueByKey(\"wechat_enabled\", \"0\")\n\tif wechatEnabled != \"1\" {\n\t\tlogger.Log.Info(\"WeChat is disabled in system config, skipping WeChat initialization\")\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\t// 开关已启用,从数据库读取企业微信配置\n\tagentIDStr := GetConfigValueByKey(\"wechat_agentid\", \"\")\n\tcorpid := GetConfigValueByKey(\"wechat_corpid\", \"\")\n\tsecret := GetConfigValueByKey(\"wechat_secret\", \"\") // 这里会自动解密\n\n\t// 验证必填配置项\n\tif agentIDStr == \"\" || corpid == \"\" || secret == \"\" {\n\t\tlogger.Log.Info(\"WeChat is enabled but configuration is incomplete (agentid/corpid/secret), WeChat app will not be initialized\")\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\t// 解析 AgentID\n\tAgentId, err := strconv.ParseInt(agentIDStr, 10, 64)\n\tif err != nil {\n\t\tlogger.Log.Error(\"wechat_agentid parse error:\", err)\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\n\t// 初始化企业微信客户端\n\tclient := workwx.New(corpid)\n\tWeApp = client.WithApp(secret, AgentId)\n\tWeApp.SpawnAccessTokenRefresher()\n\tlogger.Log.Info(\"WeChat inited successfully!\")\n}\n\n// DatabaseInit 数据初始化\nfunc DatabaseInit() {\n\t//数据初始化操作 - 使用 GORM\n\tvar v User\n\terr := DB.Where(\"username = ?\", \"admin\").First(&v).Error\n\t//检查权限\n\tif err == nil && v.Operation == \"\" {\n\t\terr := DB.Model(&User{}).Where(\"id = ?\", v.ID).Update(\"operation\", \"['add', 'edit', 'delete','update']\").Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"update admin operation successfully\")\n\t}\n\t//添加管理员账号\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\tlogger.Log.Info(\"the admin user does not exist, create a new admin account later!\")\n\n\t\t// 构建默认主题配置\n\t\tdefaultThemeConfig := map[string]interface{}{\n\t\t\t\"theme\": map[string]string{\n\t\t\t\t\"color\": \"#722ed1\",\n\t\t\t\t\"mode\": \"dark\",\n\t\t\t\t\"success\": \"#52c41a\",\n\t\t\t\t\"warning\": \"#faad14\",\n\t\t\t\t\"error\": \"#f5222f\",\n\t\t\t},\n\t\t\t\"animate\": map[string]interface{}{\n\t\t\t\t\"disabled\": true,\n\t\t\t\t\"name\": \"lightSpeed\",\n\t\t\t\t\"direction\": \"left\",\n\t\t\t},\n\t\t}\n\n\t\t// 将配置转换为 JSON 字符串\n\t\tthemeJSON, err := json.Marshal(defaultThemeConfig)\n\t\tif err != nil {\n\t\t\tlogger.Log.Error(\"Failed to marshal default theme config:\", err)\n\t\t\tthemeJSON = []byte(`{\"theme\":{\"color\":\"#722ed1\",\"mode\":\"dark\",\"success\":\"#52c41a\",\"warning\":\"#faad14\",\"error\":\"#f5222f\"},\"animate\":{\"disabled\":true,\"name\":\"lightSpeed\",\"direction\":\"left\"}}`)\n\t\t}\n\n\t\t// 使用结构体插入管理员账号\n\t\tpassword, _ := utils.PasswordHash(\"Zbxtable\")\n\t\tuser := User{\n\t\t\tUsername: \"admin\",\n\t\t\tPassword: password,\n\t\t\tAvatar: DefaultAvatar,\n\t\t\tRole: \"admin\",\n\t\t\tOperation: \"['add', 'edit', 'delete','update']\",\n\t\t\tTheme: string(themeJSON),\n\t\t\tStatus: 0,\n\t\t}\n\t\terr = DB.Create(&user).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"create an administrator account successfully, the admin ID is:\", user.ID)\n\t}\n\t//初始化系统数据\n\tvar cnt []System\n\terr = DB.Find(&cnt).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn\n\t}\n\tif len(cnt) == 0 {\n\t\tnow := time.Now()\n\t\tsys := []System{\n\t\t\t{Name: \"Linux操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"Windows操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"网络设备\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"物理服务器\", Status: 0, InitedAt: &now},\n\t\t}\n\t\terr := DB.Create(&sys).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init system info error!\", err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"Init system data successfully!\")\n\t}\n\t//告警默认规则初始化 default rule\n\tvar rules []Rule\n\terr = DB.Where(\"m_type = ?\", \"2\").Find(&rules).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn\n\t}\n\tif len(rules) == 0 {\n\t\t// 使用 \"*\" 作为全局默认规则,匹配所有租户\n\t\tdefaultRule := []Rule{\n\t\t\t{\n\t\t\t\tName: \"全局默认规则\",\n\t\t\t\tZIDs: \"*\",\n\t\t\t\tMType: \"2\",\n\t\t\t\tChannel: \"wechat_robot\",\n\t\t\t\tUserIds: \"1\",\n\t\t\t\tSweek: \"0,1,2,3,4,5,6\",\n\t\t\t\tStime: \"00:00\",\n\t\t\t\tEtime: \"23.59\",\n\t\t\t\tStatus: \"0\"},\n\t\t}\n\t\terr := DB.Create(&defaultRule).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init default rule error!\", err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"Init default rule successfully!\")\n\t}\n\t// 初始化加密密钥(如果不存在)\n\tvar encryptionKeyConfig Config\n\terr = DB.Where(\"config_key = ?\", \"encryption_key\").First(&encryptionKeyConfig).Error\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t// 生成随机加密密钥\n\t\trandomKey := utils.GenerateRandomKey()\n\t\tencryptionKeyConfig = Config{\n\t\t\tName: \"加密密钥\",\n\t\t\tConfigKey: \"encryption_key\",\n\t\t\tConfigValue: randomKey,\n\t\t\tComment: \"用于加密存储敏感信息(如Zabbix密码和Token)的密钥,系统自动生成,不可修改\",\n\t\t}\n\t\tif insertErr := DB.Create(&encryptionKeyConfig).Error; insertErr != nil {\n\t\t\tlogger.Log.Error(\"Init encryption_key error:\", insertErr)\n\t\t} else {\n\t\t\tlogger.Log.Info(\"Init encryption_key successfully! Key length:\", len(randomKey))\n\t\t}\n\t}\n\n\t// 获取结构化的默认配置\n\tdefaultConfigs := getDefaultConfigs()\n\n\tfor _, cfgItem := range defaultConfigs {\n\t\tvar existing Config\n\t\terr = DB.Where(\"config_key = ?\", cfgItem.ConfigKey).First(&existing).Error\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\tif insertErr := DB.Create(&cfgItem).Error; insertErr != nil {\n\t\t\t\tlogger.Log.Infof(\"Init config key %s error: %v\", cfgItem.ConfigKey, insertErr)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tlogger.Log.Infof(\"Init config key %s successfully!\", cfgItem.ConfigKey)\n\t\t} else {\n\t\t\tlogger.Log.Infof(\"Init config key %s error: %v\", cfgItem.ConfigKey, err)\n\t\t\tcontinue\n\t\t}\n\t}\n\t//默认菜单初始化\n\tInitMenuData()\n\t//检查并添加缺失的菜单项(用于版本升级)\n\tCheckAndAddMenus()\n\n}\n\nfunc GetConfKey(v string) string {\n\t// 优先从 .env 中读取(便于开发环境配置敏感信息且不提交到 git)\n\tenvConfigOnce.Do(func() {\n\t\tfile, err := os.Open(\".env\")\n\t\tif err != nil {\n\t\t\t// 没有 .env 文件时直接跳过,后面回退到 app.conf\n\t\t\tenvFileExists = false\n\t\t\treturn\n\t\t}\n\t\tdefer file.Close()\n\n\t\tenvFileExists = true\n\t\tenvConfig = make(map[string]string)\n\n\t\tscanner := bufio.NewScanner(file)\n\t\tfor scanner.Scan() {\n\t\t\tline := strings.TrimSpace(scanner.Text())\n\t\t\tif line == \"\" || strings.HasPrefix(line, \"#\") || strings.HasPrefix(line, \";\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tparts := strings.SplitN(line, \"=\", 2)\n\t\t\tif len(parts) != 2 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tkey := strings.TrimSpace(parts[0])\n\t\t\tval := strings.TrimSpace(parts[1])\n\t\t\t// 去掉可能的引号\n\t\t\tval = strings.Trim(val, `\"'`)\n\t\t\tif key != \"\" {\n\t\t\t\tenvConfig[key] = val // 即使 val 为空也认为是有效配置\n\t\t\t}\n\t\t}\n\t})\n\n\t// 如果存在 .env 文件,则所有配置完全由 .env 决定:\n\t// - key 存在:返回对应值(可以是空字符串)\n\t// - key 不存在:返回空字符串\n\tif envFileExists {\n\t\tif envConfig == nil {\n\t\t\treturn \"\"\n\t\t}\n\t\tif val, ok := envConfig[v]; ok {\n\t\t\treturn val\n\t\t}\n\t\treturn \"\"\n\t}\n\t// 其次从 config/app.conf 读取\n\tcfg, err := ini.Load(\"./config/app.conf\")\n\tif err != nil {\n\t\tlogger.Log.Error(err)\n\t\treturn \"\"\n\t}\n\tp, err := cfg.Section(\"\").GetKey(v)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn p.String()\n}\n\n// IsPasswordConfigured 检查是否配置了密码(用于图形查看)\n// 优先检查数据库中是否有激活的 Zabbix 实例配置了用户名和密码\n// 注意:查看图形需要用户名和密码,Token 方式无法用于 Web 登录\nfunc IsPasswordConfigured() bool {\n\t// 1. 优先检查数据库中是否有激活的 Zabbix 实例\n\tvar activeInstance ZabbixInstance\n\terr := DB.Where(\"is_active = ?\", true).First(&activeInstance).Error\n\tif err == nil {\n\t\t// 找到激活的实例,检查是否配置了用户名和密码(查看图形必须用密码,Token 无法用于 Web 登录)\n\t\tif strings.TrimSpace(activeInstance.User) != \"\" && strings.TrimSpace(activeInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 2. 如果数据库中没有激活实例,检查是否有任何可用的实例配置了密码\n\tvar anyInstance ZabbixInstance\n\terr = DB.Where(\"enabled = ?\", true).First(&anyInstance).Error\n\tif err == nil {\n\t\t// 找到可用的实例,检查是否配置了用户名和密码\n\t\tif strings.TrimSpace(anyInstance.User) != \"\" && strings.TrimSpace(anyInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 3. 回退到配置文件检查(兼容旧版本)\n\tpass := GetConfKey(\"zabbix_pass\")\n\tuser := GetConfKey(\"zabbix_user\")\n\treturn pass != \"\" && user != \"\"\n}\n\n// GetEncryptionKey 获取加密密钥\n// 优先从数据库读取,如果数据库中没有则从环境变量读取,最后使用默认密钥\nfunc GetEncryptionKey() string {\n\t// 1. 优先从数据库读取\n\tkey := GetConfigValueByKey(\"encryption_key\", \"\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 2. 从环境变量或配置文件读取\n\tkey = GetConfKey(\"encryption_key\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 3. 使用默认密钥(不应该到这里,因为 DatabaseInit 会生成)\n\tlogger.Log.Warn(\"使用默认加密密钥,建议在数据库中配置 encryption_key\")\n\treturn \"zbxtable-default-encryption-key-2024\"\n}\n\n// getDefaultConfigs 返回系统默认配置列表\n// 配置按功能模块分组,便于维护和管理\nfunc getDefaultConfigs() []Config {\n\tconfigs := make([]Config, 0)\n\n\t// 系统外观配置\n\tconfigs = append(configs, getSystemAppearanceConfigs()...)\n\n\t// 初始配置状态\n\tconfigs = append(configs, getInitialSetupConfigs()...)\n\n\t// Dashboard 相关配置\n\tconfigs = append(configs, getDashboardConfigs()...)\n\n\t// 邮件配置\n\tconfigs = append(configs, getEmailConfigs()...)\n\n\t// 企业微信配置\n\tconfigs = append(configs, getWeChatConfigs()...)\n\n\t// AI 配置\n\tconfigs = append(configs, getAIConfigs()...)\n\n\treturn configs\n}\n\n// getSystemAppearanceConfigs 系统外观配置\nfunc getSystemAppearanceConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"系统名称\",\n\t\t\tConfigKey: \"system_name\",\n\t\t\tConfigValue: \"ZbxTable\",\n\t\t\tComment: \"系统显示的名称\",\n\t\t},\n\t\t{\n\t\t\tName: \"系统Logo\",\n\t\t\tConfigKey: \"system_logo\",\n\t\t\tConfigValue: \"data:image/png;base64,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\",\n\t\t\tComment: \"系统Logo的base64编码数据\",\n\t\t},\n\t}\n}\n\n// getInitialSetupConfigs 初始配置状态\nfunc getInitialSetupConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"初始配置完成\",\n\t\t\tConfigKey: \"initial_setup_completed\",\n\t\t\tConfigValue: \"0\",\n\t\t\tComment: \"标记系统是否完成初始配置:1 已完成,0 未完成\",\n\t\t},\n\t}\n}\n\n// getDashboardConfigs Dashboard 相关配置\nfunc getDashboardConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"数据面板\",\n\t\t\tConfigKey: \"zbx_dash\",\n\t\t\tConfigValue: \"0\",\n\t\t\tComment: \"是否开启Zabbix看板:1 开启,0 关闭\",\n\t\t},\n\t\t{\n\t\t\tName: \"面板配置\",\n\t\t\tConfigKey: \"dash_id\",\n\t\t\tConfigValue: \"1\",\n\t\t\tComment: \"需要引入的Zabbix面板的ID,默认为1\",\n\t\t},\n\t\t{\n\t\t\tName: \"主机分类同步\",\n\t\t\tConfigKey: \"sync_inventory\",\n\t\t\tConfigValue: \"1\",\n\t\t\tComment: \"主机分类同步计划任务是否启用:1 启用,0 不启用\",\n\t\t},\n\t\t{\n\t\t\tName: \"Webhook回调地址\",\n\t\t\tConfigKey: \"webhook_url\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"webhook通知地址\",\n\t\t},\n\t}\n}\n\n// getEmailConfigs 邮件配置\nfunc getEmailConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"邮件发件人\",\n\t\t\tConfigKey: \"email_from\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"告警邮件发件人邮箱地址\",\n\t\t},\n\t\t{\n\t\t\tName: \"邮件昵称\",\n\t\t\tConfigKey: \"email_nickname\",\n\t\t\tConfigValue: \"ZbxTable\",\n\t\t\tComment: \"告警邮件显示的发件人昵称\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 密码/授权码\",\n\t\t\tConfigKey: \"email_secret\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"SMTP 登录密码或授权码\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 服务器\",\n\t\t\tConfigKey: \"email_host\",\n\t\t\tConfigValue: \"smtp.qq.com\",\n\t\t\tComment: \"SMTP 服务器地址\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 端口\",\n\t\t\tConfigKey: \"email_port\",\n\t\t\tConfigValue: \"465\",\n\t\t\tComment: \"SMTP 端口号\",\n\t\t},\n\t\t{\n\t\t\tName: \"SMTP 使用 SSL\",\n\t\t\tConfigKey: \"email_isSSl\",\n\t\t\tConfigValue: \"true\",\n\t\t\tComment: \"是否启用 SSL:true/false\",\n\t\t},\n\t}\n}\n\n// getWeChatConfigs 企业微信配置\nfunc getWeChatConfigs() []Config {\n\treturn []Config{\n\t\t{\n\t\t\tName: \"企业微信开关\",\n\t\t\tConfigKey: \"wechat_enabled\",\n\t\t\tConfigValue: \"0\",\n\t\t\tComment: \"是否启用企业微信:1 启用,0 禁用\",\n\t\t},\n\t\t{\n\t\t\tName: \"企业微信 AgentID\",\n\t\t\tConfigKey: \"wechat_agentid\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"企业微信应用的 AgentID\",\n\t\t},\n\t\t{\n\t\t\tName: \"企业微信 CorpID\",\n\t\t\tConfigKey: \"wechat_corpid\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"企业微信企业ID\",\n\t\t},\n\t\t{\n\t\t\tName: \"企业微信 Secret\",\n\t\t\tConfigKey: \"wechat_secret\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"企业微信应用的 Secret\",\n\t\t},\n\t}\n}\n\n// getAIConfigs AI 配置\nfunc getAIConfigs() []Config {\n\treturn []Config{\n\t\t// AI 类型选择\n\t\t{\n\t\t\tName: \"AI 类型\",\n\t\t\tConfigKey: \"ai_type\",\n\t\t\tConfigValue: \"ollama\",\n\t\t\tComment: \"选择使用的 AI 服务类型:ollama 或 deepseek\",\n\t\t},\n\t\t// Ollama 配置\n\t\t{\n\t\t\tName: \"Ollama Host\",\n\t\t\tConfigKey: \"ollama_host\",\n\t\t\tConfigValue: \"http://localhost:11434\",\n\t\t\tComment: \"Ollama 服务地址,如 http://127.0.0.1:11434\",\n\t\t},\n\t\t{\n\t\t\tName: \"Ollama Model\",\n\t\t\tConfigKey: \"ollama_model\",\n\t\t\tConfigValue: \"deepseek-r1:32b\",\n\t\t\tComment: \"默认使用的大模型名称\",\n\t\t},\n\t\t// Deepseek 配置\n\t\t{\n\t\t\tName: \"Deepseek API Key\",\n\t\t\tConfigKey: \"deepseek_api_key\",\n\t\t\tConfigValue: \"\",\n\t\t\tComment: \"Deepseek API 密钥\",\n\t\t},\n\t\t{\n\t\t\tName: \"Deepseek Model\",\n\t\t\tConfigKey: \"deepseek_model\",\n\t\t\tConfigValue: \"deepseek-chat\",\n\t\t\tComment: \"Deepseek 模型名称,如 deepseek-chat\",\n\t\t},\n\t\t{\n\t\t\tName: \"Deepseek Base URL\",\n\t\t\tConfigKey: \"deepseek_base_url\",\n\t\t\tConfigValue: \"https://api.deepseek.com\",\n\t\t\tComment: \"Deepseek API 地址,默认为 https://api.deepseek.com\",\n\t\t},\n\t}\n}\n"}, "e95145171b92fcb09a4d1e3fba862a0525a92d18": {"t": 1785754912.4306512, "n": 16885, "h": "673207e9b2a29937", "c": "package model\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\t\"zbxtable/pkg/logger\"\n\t\"zbxtable/pkg/utils\"\n\n\tzabbix \"github.com/canghai908/zabbix-go\"\n\t_ \"github.com/go-sql-driver/mysql\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t_ \"github.com/lib/pq\"\n\tworkwx \"github.com/xen0n/go-workwx\"\n\tini \"gopkg.in/ini.v1\"\n\t\"gorm.io/gorm\"\n)\n\nvar (\n\tAPI = &zabbix.API{}\n\tjson = jsoniter.Config{\n\t\tEscapeHTML: false,\n\t\tSortMapKeys: true,\n\t\tValidateJsonRawMessage: true,\n\t}.Froze()\n\tZBX_VER string\n\tZBX_V bool\n\tVersion string\n\tGitHash string\n\tBuildTime string\n\tWeApp *workwx.WorkwxApp // 不初始化,默认为 nil\n\n\tenvConfig map[string]string\n\tenvConfigOnce sync.Once\n\tenvFileExists bool\n)\n\nconst (\n\t//ms-agent 常量定义\n\tMSName = \"ms-agent\"\n\tMSUser = \"ms-agent\"\n\tMSGroup = \"MS-Agent\"\n\tMSMedia = \"MS-Agent\"\n\tMSAction = \"MS-Agent\"\n\t//webhook 常量定义\n\tWebhookName = \"ZbxTable\"\n\tWebhookAction = \"ZbxTable Webhook\"\n\tWebhookGroup = \"ZbxTable Webhook\"\n\tWebhookUser = \"zbxtable-webhook\"\n\t//down\n\tDownloadPath = \"./download/\"\n\tTplPath = \"./assets/templates/\"\n\t// 默认用户头像 (空字符串,由前端处理默认头像)\n\tDefaultAvatar = \"\"\n\t//默认更新服务器\n\tUpdateURL = \"http://dl.cactifans.com/stable/\"\n)\n\n// TableName 表名前缀\nfunc TableName(str string) string {\n\treturn fmt.Sprintf(\"%s%s\", \"zbxtable_\", str)\n}\n\n// 接收数据库信息初始化\nfunc ModelInit(dbtype, dbhost, dbuser, dbpass, dbname, dbport string) {\n\t// 直接使用 GORM\n\trunmode := GetConfKey(\"runmode\")\n\terr := InitGormDB(dbtype, dbhost, dbuser, dbpass, dbname, dbport, runmode)\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to connect database: \", err)\n\t\tos.Exit(1)\n\t}\n\tlogger.Log.Info(\"Database connected!\")\n\t// 自动迁移表\n\terr = AutoMigrate()\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to auto migrate: \", err)\n\t\tos.Exit(1)\n\t}\n\t// 基础数据初始化\n\tDatabaseInit()\n\t// 使用go-cache替代Redis(不再需要Redis连接)\n\tInitCache()\n}\n\nfunc InitWechat() {\n\t// 首先检查企业微信开关是否启用\n\twechatEnabled := GetConfigValueByKey(\"wechat_enabled\", \"0\")\n\tif wechatEnabled != \"1\" {\n\t\tlogger.Log.Info(\"WeChat is disabled in system config, skipping WeChat initialization\")\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\t// 开关已启用,从数据库读取企业微信配置\n\tagentIDStr := GetConfigValueByKey(\"wechat_agentid\", \"\")\n\tcorpid := GetConfigValueByKey(\"wechat_corpid\", \"\")\n\tsecret := GetConfigValueByKey(\"wechat_secret\", \"\") // 这里会自动解密\n\n\t// 验证必填配置项\n\tif agentIDStr == \"\" || corpid == \"\" || secret == \"\" {\n\t\tlogger.Log.Info(\"WeChat is enabled but configuration is incomplete (agentid/corpid/secret), WeChat app will not be initialized\")\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\t// 解析 AgentID\n\tAgentId, err := strconv.ParseInt(agentIDStr, 10, 64)\n\tif err != nil {\n\t\tlogger.Log.Error(\"wechat_agentid parse error:\", err)\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\n\t// 初始化企业微信客户端\n\tclient := workwx.New(corpid)\n\tWeApp = client.WithApp(secret, AgentId)\n\tWeApp.SpawnAccessTokenRefresher()\n\tlogger.Log.Info(\"WeChat inited successfully!\")\n}\n\n// DatabaseInit 数据初始化\nfunc DatabaseInit() {\n\t//数据初始化操作 - 使用 GORM\n\tvar v User\n\terr := DB.Where(\"username = ?\", \"admin\").First(&v).Error\n\t//检查权限\n\tif err == nil && v.Operation == \"\" {\n\t\terr := DB.Model(&User{}).Where(\"id = ?\", v.ID).Update(\"operation\", \"['add', 'edit', 'delete','update']\").Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"update admin operation successfully\")\n\t}\n\t//添加管理员账号\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\tlogger.Log.Info(\"the admin user does not exist, create a new admin account later!\")\n\n\t\t// 构建默认主题配置\n\t\tdefaultThemeConfig := map[string]interface{}{\n\t\t\t\"theme\": map[string]string{\n\t\t\t\t\"color\": \"#722ed1\",\n\t\t\t\t\"mode\": \"dark\",\n\t\t\t\t\"success\": \"#52c41a\",\n\t\t\t\t\"warning\": \"#faad14\",\n\t\t\t\t\"error\": \"#f5222f\",\n\t\t\t},\n\t\t\t\"animate\": map[string]interface{}{\n\t\t\t\t\"disabled\": true,\n\t\t\t\t\"name\": \"lightSpeed\",\n\t\t\t\t\"direction\": \"left\",\n\t\t\t},\n\t\t}\n\n\t\t// 将配置转换为 JSON 字符串\n\t\tthemeJSON, err := json.Marshal(defaultThemeConfig)\n\t\tif err != nil {\n\t\t\tlogger.Log.Error(\"Failed to marshal default theme config:\", err)\n\t\t\tthemeJSON = []byte(`{\"theme\":{\"color\":\"#722ed1\",\"mode\":\"dark\",\"success\":\"#52c41a\",\"warning\":\"#faad14\",\"error\":\"#f5222f\"},\"animate\":{\"disabled\":true,\"name\":\"lightSpeed\",\"direction\":\"left\"}}`)\n\t\t}\n\n\t\t// 使用结构体插入管理员账号\n\t\tpassword, _ := utils.PasswordHash(\"Zbxtable\")\n\t\tuser := User{\n\t\t\tUsername: \"admin\",\n\t\t\tPassword: password,\n\t\t\tAvatar: DefaultAvatar,\n\t\t\tRole: \"admin\",\n\t\t\tOperation: \"['add', 'edit', 'delete','update']\",\n\t\t\tTheme: string(themeJSON),\n\t\t\tStatus: 0,\n\t\t}\n\t\terr = DB.Create(&user).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"create an administrator account successfully, the admin ID is:\", user.ID)\n\t}\n\t//初始化系统数据\n\tvar cnt []System\n\terr = DB.Find(&cnt).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn\n\t}\n\tif len(cnt) == 0 {\n\t\tnow := time.Now()\n\t\tsys := []System{\n\t\t\t{Name: \"Linux操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"Windows操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"网络设备\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"物理服务器\", Status: 0, InitedAt: &now},\n\t\t}\n\t\terr := DB.Create(&sys).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init system info error!\", err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"Init system data successfully!\")\n\t}\n\t//告警默认规则初始化 default rule\n\tvar rules []Rule\n\terr = DB.Where(\"m_type = ?\", \"2\").Find(&rules).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn\n\t}\n\tif len(rules) == 0 {\n\t\t// 使用 \"*\" 作为全局默认规则,匹配所有租户\n\t\tdefaultRule := []Rule{\n\t\t\t{\n\t\t\t\tName: \"全局默认规则\",\n\t\t\t\tZIDs: \"*\",\n\t\t\t\tMType: \"2\",\n\t\t\t\tChannel: \"wechat_robot\",\n\t\t\t\tUserIds: \"1\",\n\t\t\t\tSweek: \"0,1,2,3,4,5,6\",\n\t\t\t\tStime: \"00:00\",\n\t\t\t\tEtime: \"23.59\",\n\t\t\t\tStatus: \"0\"},\n\t\t}\n\t\terr := DB.Create(&defaultRule).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init default rule error!\", err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"Init default rule successfully!\")\n\t}\n\t// 初始化加密密钥(如果不存在)\n\tvar encryptionKeyConfig Config\n\terr = DB.Where(\"config_key = ?\", \"encryption_key\").First(&encryptionKeyConfig).Error\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t// 生成随机加密密钥\n\t\trandomKey := utils.GenerateRandomKey()\n\t\tencryptionKeyConfig = Config{\n\t\t\tName: \"加密密钥\",\n\t\t\tConfigKey: \"encryption_key\",\n\t\t\tConfigValue: randomKey,\n\t\t\tComment: \"用于加密存储敏感信息(如Zabbix密码和Token)的密钥,系统自动生成,不可修改\",\n\t\t}\n\t\tif insertErr := DB.Create(&encryptionKeyConfig).Error; insertErr != nil {\n\t\t\tlogger.Log.Error(\"Init encryption_key error:\", insertErr)\n\t\t} else {\n\t\t\tlogger.Log.Info(\"Init encryption_key successfully! Key length:\", len(randomKey))\n\t\t}\n\t}\n\n\tdefaultConfigs := []Config{\n\t\t// 系统外观配置\n\t\t{Name: \"系统名称\", ConfigKey: \"system_name\", ConfigValue: \"ZbxTable\", Comment: \"系统显示的名称\"},\n\t\t{Name: \"系统Logo\", ConfigKey: \"system_logo\", ConfigValue: \"data:image/png;base64,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\", Comment: \"系统Logo的base64编码数据\"},\n\t\t// 初始配置状态\n\t\t{Name: \"初始配置完成\", ConfigKey: \"initial_setup_completed\", ConfigValue: \"0\", Comment: \"标记系统是否完成初始配置:1 已完成,0 未完成\"},\n\t\t// Dashboard 相关\n\t\t{Name: \"数据面板\", ConfigKey: \"zbx_dash\", ConfigValue: \"0\", Comment: \"是否开启Zabbix看板:1 开启,0 关闭\"},\n\t\t{Name: \"面板配置\", ConfigKey: \"dash_id\", ConfigValue: \"1\", Comment: \"需要引入的Zabbix面板的ID,默认为1\"},\n\t\t{Name: \"主机分类同步\", ConfigKey: \"sync_inventory\", ConfigValue: \"1\", Comment: \"主机分类同步计划任务是否启用:1 启用,0 不启用\"},\n\t\t{Name: \"Webhook回调地址\", ConfigKey: \"webhook_url\", ConfigValue: \"\", Comment: \"webhook通知地址\"},\n\t\t// 邮件配置\n\t\t{Name: \"邮件发件人\", ConfigKey: \"email_from\", ConfigValue: \"\", Comment: \"告警邮件发件人邮箱地址\"},\n\t\t{Name: \"邮件昵称\", ConfigKey: \"email_nickname\", ConfigValue: \"ZbxTable\", Comment: \"告警邮件显示的发件人昵称\"},\n\t\t{Name: \"SMTP 密码/授权码\", ConfigKey: \"email_secret\", ConfigValue: \"\", Comment: \"SMTP 登录密码或授权码\"},\n\t\t{Name: \"SMTP 服务器\", ConfigKey: \"email_host\", ConfigValue: \"smtp.qq.com\", Comment: \"SMTP 服务器地址\"},\n\t\t{Name: \"SMTP 端口\", ConfigKey: \"email_port\", ConfigValue: \"465\", Comment: \"SMTP 端口号\"},\n\t\t{Name: \"SMTP 使用 SSL\", ConfigKey: \"email_isSSl\", ConfigValue: \"true\", Comment: \"是否启用 SSL:true/false\"},\n\t\t// 企业微信配置\n\t\t{Name: \"企业微信开关\", ConfigKey: \"wechat_enabled\", ConfigValue: \"0\", Comment: \"是否启用企业微信:1 启用,0 禁用\"},\n\t\t{Name: \"企业微信 AgentID\", ConfigKey: \"wechat_agentid\", ConfigValue: \"\", Comment: \"企业微信应用的 AgentID\"},\n\t\t{Name: \"企业微信 CorpID\", ConfigKey: \"wechat_corpid\", ConfigValue: \"\", Comment: \"企业微信企业ID\"},\n\t\t{Name: \"企业微信 Secret\", ConfigKey: \"wechat_secret\", ConfigValue: \"\", Comment: \"企业微信应用的 Secret\"},\n\t\t// AI 配置\n\t\t{Name: \"AI 类型\", ConfigKey: \"ai_type\", ConfigValue: \"ollama\", Comment: \"选择使用的 AI 服务类型:ollama 或 deepseek\"},\n\t\t// Ollama 配置\n\t\t{Name: \"Ollama Host\", ConfigKey: \"ollama_host\", ConfigValue: \"http://localhost:11434\", Comment: \"Ollama 服务地址,如 http://127.0.0.1:11434\"},\n\t\t{Name: \"Ollama Model\", ConfigKey: \"ollama_model\", ConfigValue: \"deepseek-r1:32b\", Comment: \"默认使用的大模型名称\"},\n\t\t// Deepseek 配置\n\t\t{Name: \"Deepseek API Key\", ConfigKey: \"deepseek_api_key\", ConfigValue: \"\", Comment: \"Deepseek API 密钥\"},\n\t\t{Name: \"Deepseek Model\", ConfigKey: \"deepseek_model\", ConfigValue: \"deepseek-chat\", Comment: \"Deepseek 模型名称,如 deepseek-chat\"},\n\t\t{Name: \"Deepseek Base URL\", ConfigKey: \"deepseek_base_url\", ConfigValue: \"https://api.deepseek.com\", Comment: \"Deepseek API 地址,默认为 https://api.deepseek.com\"},\n\t}\n\n\tfor _, cfgItem := range defaultConfigs {\n\t\tvar existing Config\n\t\terr = DB.Where(\"config_key = ?\", cfgItem.ConfigKey).First(&existing).Error\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\tif insertErr := DB.Create(&cfgItem).Error; insertErr != nil {\n\t\t\t\tlogger.Log.Infof(\"Init config key %s error: %v\", cfgItem.ConfigKey, insertErr)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tlogger.Log.Infof(\"Init config key %s successfully!\", cfgItem.ConfigKey)\n\t\t} else {\n\t\t\tlogger.Log.Infof(\"Init config key %s error: %v\", cfgItem.ConfigKey, err)\n\t\t\tcontinue\n\t\t}\n\t}\n\t//默认菜单初始化\n\tInitMenuData()\n\t//检查并添加缺失的菜单项(用于版本升级)\n\tCheckAndAddMenus()\n\n}\n\nfunc GetConfKey(v string) string {\n\t// 优先从 .env 中读取(便于开发环境配置敏感信息且不提交到 git)\n\tenvConfigOnce.Do(func() {\n\t\tfile, err := os.Open(\".env\")\n\t\tif err != nil {\n\t\t\t// 没有 .env 文件时直接跳过,后面回退到 app.conf\n\t\t\tenvFileExists = false\n\t\t\treturn\n\t\t}\n\t\tdefer file.Close()\n\n\t\tenvFileExists = true\n\t\tenvConfig = make(map[string]string)\n\n\t\tscanner := bufio.NewScanner(file)\n\t\tfor scanner.Scan() {\n\t\t\tline := strings.TrimSpace(scanner.Text())\n\t\t\tif line == \"\" || strings.HasPrefix(line, \"#\") || strings.HasPrefix(line, \";\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tparts := strings.SplitN(line, \"=\", 2)\n\t\t\tif len(parts) != 2 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tkey := strings.TrimSpace(parts[0])\n\t\t\tval := strings.TrimSpace(parts[1])\n\t\t\t// 去掉可能的引号\n\t\t\tval = strings.Trim(val, `\"'`)\n\t\t\tif key != \"\" {\n\t\t\t\tenvConfig[key] = val // 即使 val 为空也认为是有效配置\n\t\t\t}\n\t\t}\n\t})\n\n\t// 如果存在 .env 文件,则所有配置完全由 .env 决定:\n\t// - key 存在:返回对应值(可以是空字符串)\n\t// - key 不存在:返回空字符串\n\tif envFileExists {\n\t\tif envConfig == nil {\n\t\t\treturn \"\"\n\t\t}\n\t\tif val, ok := envConfig[v]; ok {\n\t\t\treturn val\n\t\t}\n\t\treturn \"\"\n\t}\n\t// 其次从 config/app.conf 读取\n\tcfg, err := ini.Load(\"./config/app.conf\")\n\tif err != nil {\n\t\tlogger.Log.Error(err)\n\t\treturn \"\"\n\t}\n\tp, err := cfg.Section(\"\").GetKey(v)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn p.String()\n}\n\n// IsPasswordConfigured 检查是否配置了密码(用于图形查看)\n// 优先检查数据库中是否有激活的 Zabbix 实例配置了用户名和密码\n// 注意:查看图形需要用户名和密码,Token 方式无法用于 Web 登录\nfunc IsPasswordConfigured() bool {\n\t// 1. 优先检查数据库中是否有激活的 Zabbix 实例\n\tvar activeInstance ZabbixInstance\n\terr := DB.Where(\"is_active = ?\", true).First(&activeInstance).Error\n\tif err == nil {\n\t\t// 找到激活的实例,检查是否配置了用户名和密码(查看图形必须用密码,Token 无法用于 Web 登录)\n\t\tif strings.TrimSpace(activeInstance.User) != \"\" && strings.TrimSpace(activeInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 2. 如果数据库中没有激活实例,检查是否有任何可用的实例配置了密码\n\tvar anyInstance ZabbixInstance\n\terr = DB.Where(\"enabled = ?\", true).First(&anyInstance).Error\n\tif err == nil {\n\t\t// 找到可用的实例,检查是否配置了用户名和密码\n\t\tif strings.TrimSpace(anyInstance.User) != \"\" && strings.TrimSpace(anyInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 3. 回退到配置文件检查(兼容旧版本)\n\tpass := GetConfKey(\"zabbix_pass\")\n\tuser := GetConfKey(\"zabbix_user\")\n\treturn pass != \"\" && user != \"\"\n}\n\n// GetEncryptionKey 获取加密密钥\n// 优先从数据库读取,如果数据库中没有则从环境变量读取,最后使用默认密钥\nfunc GetEncryptionKey() string {\n\t// 1. 优先从数据库读取\n\tkey := GetConfigValueByKey(\"encryption_key\", \"\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 2. 从环境变量或配置文件读取\n\tkey = GetConfKey(\"encryption_key\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 3. 使用默认密钥(不应该到这里,因为 DatabaseInit 会生成)\n\tlogger.Log.Warn(\"使用默认加密密钥,建议在数据库中配置 encryption_key\")\n\treturn \"zbxtable-default-encryption-key-2024\"\n}\n"}, "794e70b8544aa8c0a881f191a884d6c7f56cbd99": {"t": 1785754916.4837615, "n": 16466, "h": "45423910d1a301cc", "c": "package model\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\t\"zbxtable/pkg/logger\"\n\t\"zbxtable/pkg/utils\"\n\n\tzabbix \"github.com/canghai908/zabbix-go\"\n\t_ \"github.com/go-sql-driver/mysql\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t_ \"github.com/lib/pq\"\n\tworkwx \"github.com/xen0n/go-workwx\"\n\tini \"gopkg.in/ini.v1\"\n\t\"gorm.io/gorm\"\n)\n\nvar (\n\tAPI = &zabbix.API{}\n\tjson = jsoniter.Config{\n\t\tEscapeHTML: false,\n\t\tSortMapKeys: true,\n\t\tValidateJsonRawMessage: true,\n\t}.Froze()\n\tZBX_VER string\n\tZBX_V bool\n\tVersion string\n\tGitHash string\n\tBuildTime string\n\tWeApp *workwx.WorkwxApp // 不初始化,默认为 nil\n\n\tenvConfig map[string]string\n\tenvConfigOnce sync.Once\n\tenvFileExists bool\n)\n\nconst (\n\t//ms-agent 常量定义\n\tMSName = \"ms-agent\"\n\tMSUser = \"ms-agent\"\n\tMSGroup = \"MS-Agent\"\n\tMSMedia = \"MS-Agent\"\n\tMSAction = \"MS-Agent\"\n\t//webhook 常量定义\n\tWebhookName = \"ZbxTable\"\n\tWebhookAction = \"ZbxTable Webhook\"\n\tWebhookGroup = \"ZbxTable Webhook\"\n\tWebhookUser = \"zbxtable-webhook\"\n\t//down\n\tDownloadPath = \"./download/\"\n\tTplPath = \"./assets/templates/\"\n\t// 默认用户头像 (空字符串,由前端处理默认头像)\n\tDefaultAvatar = \"\"\n\t//默认更新服务器\n\tUpdateURL = \"http://dl.cactifans.com/stable/\"\n)\n\n// TableName 表名前缀\nfunc TableName(str string) string {\n\treturn fmt.Sprintf(\"%s%s\", \"zbxtable_\", str)\n}\n\n// 接收数据库信息初始化\nfunc ModelInit(dbtype, dbhost, dbuser, dbpass, dbname, dbport string) {\n\t// 直接使用 GORM\n\trunmode := GetConfKey(\"runmode\")\n\terr := InitGormDB(dbtype, dbhost, dbuser, dbpass, dbname, dbport, runmode)\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to connect database: \", err)\n\t\tos.Exit(1)\n\t}\n\tlogger.Log.Info(\"Database connected!\")\n\t// 自动迁移表\n\terr = AutoMigrate()\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to auto migrate: \", err)\n\t\tos.Exit(1)\n\t}\n\t// 基础数据初始化\n\tDatabaseInit()\n\t// 使用go-cache替代Redis(不再需要Redis连接)\n\tInitCache()\n}\n\nfunc InitWechat() {\n\t// 首先检查企业微信开关是否启用\n\twechatEnabled := GetConfigValueByKey(\"wechat_enabled\", \"0\")\n\tif wechatEnabled != \"1\" {\n\t\tlogger.Log.Info(\"WeChat is disabled in system config, skipping WeChat initialization\")\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\t// 开关已启用,从数据库读取企业微信配置\n\tagentIDStr := GetConfigValueByKey(\"wechat_agentid\", \"\")\n\tcorpid := GetConfigValueByKey(\"wechat_corpid\", \"\")\n\tsecret := GetConfigValueByKey(\"wechat_secret\", \"\") // 这里会自动解密\n\n\t// 验证必填配置项\n\tif agentIDStr == \"\" || corpid == \"\" || secret == \"\" {\n\t\tlogger.Log.Info(\"WeChat is enabled but configuration is incomplete (agentid/corpid/secret), WeChat app will not be initialized\")\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\t// 解析 AgentID\n\tAgentId, err := strconv.ParseInt(agentIDStr, 10, 64)\n\tif err != nil {\n\t\tlogger.Log.Error(\"wechat_agentid parse error:\", err)\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\n\t// 初始化企业微信客户端\n\tclient := workwx.New(corpid)\n\tWeApp = client.WithApp(secret, AgentId)\n\tWeApp.SpawnAccessTokenRefresher()\n\tlogger.Log.Info(\"WeChat inited successfully!\")\n}\n\n// DatabaseInit 数据初始化\nfunc DatabaseInit() {\n\t//数据初始化操作 - 使用 GORM\n\tvar v User\n\terr := DB.Where(\"username = ?\", \"admin\").First(&v).Error\n\t//检查权限\n\tif err == nil && v.Operation == \"\" {\n\t\terr := DB.Model(&User{}).Where(\"id = ?\", v.ID).Update(\"operation\", \"['add', 'edit', 'delete','update']\").Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"update admin operation successfully\")\n\t}\n\t//添加管理员账号\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\tlogger.Log.Info(\"the admin user does not exist, create a new admin account later!\")\n\n\t\t// 构建默认主题配置\n\t\tdefaultThemeConfig := map[string]interface{}{\n\t\t\t\"theme\": map[string]string{\n\t\t\t\t\"color\": \"#722ed1\",\n\t\t\t\t\"mode\": \"dark\",\n\t\t\t\t\"success\": \"#52c41a\",\n\t\t\t\t\"warning\": \"#faad14\",\n\t\t\t\t\"error\": \"#f5222f\",\n\t\t\t},\n\t\t\t\"animate\": map[string]interface{}{\n\t\t\t\t\"disabled\": true,\n\t\t\t\t\"name\": \"lightSpeed\",\n\t\t\t\t\"direction\": \"left\",\n\t\t\t},\n\t\t}\n\n\t\t// 将配置转换为 JSON 字符串\n\t\tthemeJSON, err := json.Marshal(defaultThemeConfig)\n\t\tif err != nil {\n\t\t\tlogger.Log.Error(\"Failed to marshal default theme config:\", err)\n\t\t\tthemeJSON = []byte(`{\"theme\":{\"color\":\"#722ed1\",\"mode\":\"dark\",\"success\":\"#52c41a\",\"warning\":\"#faad14\",\"error\":\"#f5222f\"},\"animate\":{\"disabled\":true,\"name\":\"lightSpeed\",\"direction\":\"left\"}}`)\n\t\t}\n\n\t\t// 使用结构体插入管理员账号\n\t\tpassword, _ := utils.PasswordHash(\"Zbxtable\")\n\t\tuser := User{\n\t\t\tUsername: \"admin\",\n\t\t\tPassword: password,\n\t\t\tAvatar: DefaultAvatar,\n\t\t\tRole: \"admin\",\n\t\t\tOperation: \"['add', 'edit', 'delete','update']\",\n\t\t\tTheme: string(themeJSON),\n\t\t\tStatus: 0,\n\t\t}\n\t\terr = DB.Create(&user).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"create an administrator account successfully, the admin ID is:\", user.ID)\n\t}\n\t//初始化系统数据\n\tvar cnt []System\n\terr = DB.Find(&cnt).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn\n\t}\n\tif len(cnt) == 0 {\n\t\tnow := time.Now()\n\t\tsys := []System{\n\t\t\t{Name: \"Linux操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"Windows操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"网络设备\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"物理服务器\", Status: 0, InitedAt: &now},\n\t\t}\n\t\terr := DB.Create(&sys).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init system info error!\", err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"Init system data successfully!\")\n\t}\n\t//告警默认规则初始化 default rule\n\tvar rules []Rule\n\terr = DB.Where(\"m_type = ?\", \"2\").Find(&rules).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn\n\t}\n\tif len(rules) == 0 {\n\t\t// 使用 \"*\" 作为全局默认规则,匹配所有租户\n\t\tdefaultRule := []Rule{\n\t\t\t{\n\t\t\t\tName: \"全局默认规则\",\n\t\t\t\tZIDs: \"*\",\n\t\t\t\tMType: \"2\",\n\t\t\t\tChannel: \"wechat_robot\",\n\t\t\t\tUserIds: \"1\",\n\t\t\t\tSweek: \"0,1,2,3,4,5,6\",\n\t\t\t\tStime: \"00:00\",\n\t\t\t\tEtime: \"23.59\",\n\t\t\t\tStatus: \"0\"},\n\t\t}\n\t\terr := DB.Create(&defaultRule).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init default rule error!\", err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"Init default rule successfully!\")\n\t}\n\t// 初始化加密密钥(如果不存在)\n\tvar encryptionKeyConfig Config\n\terr = DB.Where(\"`key` = ?\", \"encryption_key\").First(&encryptionKeyConfig).Error\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t// 生成随机加密密钥\n\t\trandomKey := utils.GenerateRandomKey()\n\t\tencryptionKeyConfig = Config{\n\t\t\tName: \"加密密钥\",\n\t\t\tKey: \"encryption_key\",\n\t\t\tValue: randomKey,\n\t\t\tComment: \"用于加密存储敏感信息(如Zabbix密码和Token)的密钥,系统自动生成,不可修改\",\n\t\t}\n\t\tif insertErr := DB.Create(&encryptionKeyConfig).Error; insertErr != nil {\n\t\t\tlogger.Log.Error(\"Init encryption_key error:\", insertErr)\n\t\t} else {\n\t\t\tlogger.Log.Info(\"Init encryption_key successfully! Key length:\", len(randomKey))\n\t\t}\n\t}\n\n\tdefaultConfigs := []Config{\n\t\t// 系统外观配置\n\t\t{Name: \"系统名称\", Key: \"system_name\", Value: \"ZbxTable\", Comment: \"系统显示的名称\"},\n\t\t{Name: \"系统Logo\", Key: \"system_logo\", Value: \"data:image/png;base64,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\", Comment: \"系统Logo的base64编码数据\"},\n\t\t// 初始配置状态\n\t\t{Name: \"初始配置完成\", Key: \"initial_setup_completed\", Value: \"0\", Comment: \"标记系统是否完成初始配置:1 已完成,0 未完成\"},\n\t\t// Dashboard 相关\n\t\t{Name: \"数据面板\", Key: \"zbx_dash\", Value: \"0\", Comment: \"是否开启Zabbix看板:1 开启,0 关闭\"},\n\t\t{Name: \"面板配置\", Key: \"dash_id\", Value: \"1\", Comment: \"需要引入的Zabbix面板的ID,默认为1\"},\n\t\t{Name: \"主机分类同步\", Key: \"sync_inventory\", Value: \"1\", Comment: \"主机分类同步计划任务是否启用:1 启用,0 不启用\"},\n\t\t{Name: \"Webhook回调地址\", Key: \"webhook_url\", Value: \"\", Comment: \"webhook通知地址\"},\n\t\t// 邮件配置\n\t\t{Name: \"邮件发件人\", Key: \"email_from\", Value: \"\", Comment: \"告警邮件发件人邮箱地址\"},\n\t\t{Name: \"邮件昵称\", Key: \"email_nickname\", Value: \"ZbxTable\", Comment: \"告警邮件显示的发件人昵称\"},\n\t\t{Name: \"SMTP 密码/授权码\", Key: \"email_secret\", Value: \"\", Comment: \"SMTP 登录密码或授权码\"},\n\t\t{Name: \"SMTP 服务器\", Key: \"email_host\", Value: \"smtp.qq.com\", Comment: \"SMTP 服务器地址\"},\n\t\t{Name: \"SMTP 端口\", Key: \"email_port\", Value: \"465\", Comment: \"SMTP 端口号\"},\n\t\t{Name: \"SMTP 使用 SSL\", Key: \"email_isSSl\", Value: \"true\", Comment: \"是否启用 SSL:true/false\"},\n\t\t// 企业微信配置\n\t\t{Name: \"企业微信开关\", Key: \"wechat_enabled\", Value: \"0\", Comment: \"是否启用企业微信:1 启用,0 禁用\"},\n\t\t{Name: \"企业微信 AgentID\", Key: \"wechat_agentid\", Value: \"\", Comment: \"企业微信应用的 AgentID\"},\n\t\t{Name: \"企业微信 CorpID\", Key: \"wechat_corpid\", Value: \"\", Comment: \"企业微信企业ID\"},\n\t\t{Name: \"企业微信 Secret\", Key: \"wechat_secret\", Value: \"\", Comment: \"企业微信应用的 Secret\"},\n\t\t// AI 配置\n\t\t{Name: \"AI 类型\", Key: \"ai_type\", Value: \"ollama\", Comment: \"选择使用的 AI 服务类型:ollama 或 deepseek\"},\n\t\t// Ollama 配置\n\t\t{Name: \"Ollama Host\", Key: \"ollama_host\", Value: \"http://localhost:11434\", Comment: \"Ollama 服务地址,如 http://127.0.0.1:11434\"},\n\t\t{Name: \"Ollama Model\", Key: \"ollama_model\", Value: \"deepseek-r1:32b\", Comment: \"默认使用的大模型名称\"},\n\t\t// Deepseek 配置\n\t\t{Name: \"Deepseek API Key\", Key: \"deepseek_api_key\", Value: \"\", Comment: \"Deepseek API 密钥\"},\n\t\t{Name: \"Deepseek Model\", Key: \"deepseek_model\", Value: \"deepseek-chat\", Comment: \"Deepseek 模型名称,如 deepseek-chat\"},\n\t\t{Name: \"Deepseek Base URL\", Key: \"deepseek_base_url\", Value: \"https://api.deepseek.com\", Comment: \"Deepseek API 地址,默认为 https://api.deepseek.com\"},\n\t}\n\n\tfor _, cfgItem := range defaultConfigs {\n\t\tvar existing Config\n\t\terr = DB.Where(\"`key` = ?\", cfgItem.Key).First(&existing).Error\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\tif insertErr := DB.Create(&cfgItem).Error; insertErr != nil {\n\t\t\t\tlogger.Log.Infof(\"Init config key %s error: %v\", cfgItem.Key, insertErr)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tlogger.Log.Infof(\"Init config key %s successfully!\", cfgItem.Key)\n\t\t}\n\t}\n\t//默认菜单初始化\n\tInitMenuData()\n\t//检查并添加缺失的菜单项(用于版本升级)\n\tCheckAndAddMenus()\n\n}\n\nfunc GetConfKey(v string) string {\n\t// 优先从 .env 中读取(便于开发环境配置敏感信息且不提交到 git)\n\tenvConfigOnce.Do(func() {\n\t\tfile, err := os.Open(\".env\")\n\t\tif err != nil {\n\t\t\t// 没有 .env 文件时直接跳过,后面回退到 app.conf\n\t\t\tenvFileExists = false\n\t\t\treturn\n\t\t}\n\t\tdefer file.Close()\n\n\t\tenvFileExists = true\n\t\tenvConfig = make(map[string]string)\n\n\t\tscanner := bufio.NewScanner(file)\n\t\tfor scanner.Scan() {\n\t\t\tline := strings.TrimSpace(scanner.Text())\n\t\t\tif line == \"\" || strings.HasPrefix(line, \"#\") || strings.HasPrefix(line, \";\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tparts := strings.SplitN(line, \"=\", 2)\n\t\t\tif len(parts) != 2 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tkey := strings.TrimSpace(parts[0])\n\t\t\tval := strings.TrimSpace(parts[1])\n\t\t\t// 去掉可能的引号\n\t\t\tval = strings.Trim(val, `\"'`)\n\t\t\tif key != \"\" {\n\t\t\t\tenvConfig[key] = val // 即使 val 为空也认为是有效配置\n\t\t\t}\n\t\t}\n\t})\n\n\t// 如果存在 .env 文件,则所有配置完全由 .env 决定:\n\t// - key 存在:返回对应值(可以是空字符串)\n\t// - key 不存在:返回空字符串\n\tif envFileExists {\n\t\tif envConfig == nil {\n\t\t\treturn \"\"\n\t\t}\n\t\tif val, ok := envConfig[v]; ok {\n\t\t\treturn val\n\t\t}\n\t\treturn \"\"\n\t}\n\t// 其次从 config/app.conf 读取\n\tcfg, err := ini.Load(\"./config/app.conf\")\n\tif err != nil {\n\t\tlogger.Log.Error(err)\n\t\treturn \"\"\n\t}\n\tp, err := cfg.Section(\"\").GetKey(v)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn p.String()\n}\n\n// IsPasswordConfigured 检查是否配置了密码(用于图形查看)\n// 优先检查数据库中是否有激活的 Zabbix 实例配置了用户名和密码\n// 注意:查看图形需要用户名和密码,Token 方式无法用于 Web 登录\nfunc IsPasswordConfigured() bool {\n\t// 1. 优先检查数据库中是否有激活的 Zabbix 实例\n\tvar activeInstance ZabbixInstance\n\terr := DB.Where(\"is_active = ?\", true).First(&activeInstance).Error\n\tif err == nil {\n\t\t// 找到激活的实例,检查是否配置了用户名和密码(查看图形必须用密码,Token 无法用于 Web 登录)\n\t\tif strings.TrimSpace(activeInstance.User) != \"\" && strings.TrimSpace(activeInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 2. 如果数据库中没有激活实例,检查是否有任何可用的实例配置了密码\n\tvar anyInstance ZabbixInstance\n\terr = DB.Where(\"enabled = ?\", true).First(&anyInstance).Error\n\tif err == nil {\n\t\t// 找到可用的实例,检查是否配置了用户名和密码\n\t\tif strings.TrimSpace(anyInstance.User) != \"\" && strings.TrimSpace(anyInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 3. 回退到配置文件检查(兼容旧版本)\n\tpass := GetConfKey(\"zabbix_pass\")\n\tuser := GetConfKey(\"zabbix_user\")\n\treturn pass != \"\" && user != \"\"\n}\n\n// GetEncryptionKey 获取加密密钥\n// 优先从数据库读取,如果数据库中没有则从环境变量读取,最后使用默认密钥\nfunc GetEncryptionKey() string {\n\t// 1. 优先从数据库读取\n\tkey := GetConfigValueByKey(\"encryption_key\", \"\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 2. 从环境变量或配置文件读取\n\tkey = GetConfKey(\"encryption_key\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 3. 使用默认密钥(不应该到这里,因为 DatabaseInit 会生成)\n\tlogger.Log.Warn(\"使用默认加密密钥,建议在数据库中配置 encryption_key\")\n\treturn \"zbxtable-default-encryption-key-2024\"\n}\n"}, "cb3a90bf48edf3f339fc62568afb33431f2c89c4": {"t": 1785754917.9655128, "n": 16408, "h": "2ab231fc40657a47", "c": "package model\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\t\"zbxtable/pkg/logger\"\n\t\"zbxtable/pkg/utils\"\n\n\tzabbix \"github.com/canghai908/zabbix-go\"\n\t_ \"github.com/go-sql-driver/mysql\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t_ \"github.com/lib/pq\"\n\tworkwx \"github.com/xen0n/go-workwx\"\n\tini \"gopkg.in/ini.v1\"\n\t\"gorm.io/gorm\"\n)\n\nvar (\n\tAPI = &zabbix.API{}\n\tjson = jsoniter.Config{\n\t\tEscapeHTML: false,\n\t\tSortMapKeys: true,\n\t\tValidateJsonRawMessage: true,\n\t}.Froze()\n\tZBX_VER string\n\tZBX_V bool\n\tVersion string\n\tGitHash string\n\tBuildTime string\n\tWeApp *workwx.WorkwxApp // 不初始化,默认为 nil\n\n\tenvConfig map[string]string\n\tenvConfigOnce sync.Once\n\tenvFileExists bool\n)\n\nconst (\n\t//ms-agent 常量定义\n\tMSName = \"ms-agent\"\n\tMSUser = \"ms-agent\"\n\tMSGroup = \"MS-Agent\"\n\tMSMedia = \"MS-Agent\"\n\tMSAction = \"MS-Agent\"\n\t//webhook 常量定义\n\tWebhookName = \"ZbxTable\"\n\tWebhookAction = \"ZbxTable Webhook\"\n\tWebhookGroup = \"ZbxTable Webhook\"\n\tWebhookUser = \"zbxtable-webhook\"\n\t//down\n\tDownloadPath = \"./download/\"\n\tTplPath = \"./assets/templates/\"\n\t// 默认用户头像 (空字符串,由前端处理默认头像)\n\tDefaultAvatar = \"\"\n)\n\n// TableName 表名前缀\nfunc TableName(str string) string {\n\treturn fmt.Sprintf(\"%s%s\", \"zbxtable_\", str)\n}\n\n// 接收数据库信息初始化\nfunc ModelInit(dbtype, dbhost, dbuser, dbpass, dbname, dbport string) {\n\t// 直接使用 GORM\n\trunmode := GetConfKey(\"runmode\")\n\terr := InitGormDB(dbtype, dbhost, dbuser, dbpass, dbname, dbport, runmode)\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to connect database: \", err)\n\t\tos.Exit(1)\n\t}\n\tlogger.Log.Info(\"Database connected!\")\n\t// 自动迁移表\n\terr = AutoMigrate()\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to auto migrate: \", err)\n\t\tos.Exit(1)\n\t}\n\t// 基础数据初始化\n\tDatabaseInit()\n\t// 使用go-cache替代Redis(不再需要Redis连接)\n\tInitCache()\n}\n\nfunc InitWechat() {\n\t// 首先检查企业微信开关是否启用\n\twechatEnabled := GetConfigValueByKey(\"wechat_enabled\", \"0\")\n\tif wechatEnabled != \"1\" {\n\t\tlogger.Log.Info(\"WeChat is disabled in system config, skipping WeChat initialization\")\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\t// 开关已启用,从数据库读取企业微信配置\n\tagentIDStr := GetConfigValueByKey(\"wechat_agentid\", \"\")\n\tcorpid := GetConfigValueByKey(\"wechat_corpid\", \"\")\n\tsecret := GetConfigValueByKey(\"wechat_secret\", \"\") // 这里会自动解密\n\n\t// 验证必填配置项\n\tif agentIDStr == \"\" || corpid == \"\" || secret == \"\" {\n\t\tlogger.Log.Info(\"WeChat is enabled but configuration is incomplete (agentid/corpid/secret), WeChat app will not be initialized\")\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\t// 解析 AgentID\n\tAgentId, err := strconv.ParseInt(agentIDStr, 10, 64)\n\tif err != nil {\n\t\tlogger.Log.Error(\"wechat_agentid parse error:\", err)\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\n\t// 初始化企业微信客户端\n\tclient := workwx.New(corpid)\n\tWeApp = client.WithApp(secret, AgentId)\n\tWeApp.SpawnAccessTokenRefresher()\n\tlogger.Log.Info(\"WeChat inited successfully!\")\n}\n\n// DatabaseInit 数据初始化\nfunc DatabaseInit() {\n\t//数据初始化操作 - 使用 GORM\n\tvar v User\n\terr := DB.Where(\"username = ?\", \"admin\").First(&v).Error\n\t//检查权限\n\tif err == nil && v.Operation == \"\" {\n\t\terr := DB.Model(&User{}).Where(\"id = ?\", v.ID).Update(\"operation\", \"['add', 'edit', 'delete','update']\").Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"update admin operation successfully\")\n\t}\n\t//添加管理员账号\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\tlogger.Log.Info(\"the admin user does not exist, create a new admin account later!\")\n\n\t\t// 构建默认主题配置\n\t\tdefaultThemeConfig := map[string]interface{}{\n\t\t\t\"theme\": map[string]string{\n\t\t\t\t\"color\": \"#722ed1\",\n\t\t\t\t\"mode\": \"dark\",\n\t\t\t\t\"success\": \"#52c41a\",\n\t\t\t\t\"warning\": \"#faad14\",\n\t\t\t\t\"error\": \"#f5222f\",\n\t\t\t},\n\t\t\t\"animate\": map[string]interface{}{\n\t\t\t\t\"disabled\": true,\n\t\t\t\t\"name\": \"lightSpeed\",\n\t\t\t\t\"direction\": \"left\",\n\t\t\t},\n\t\t}\n\n\t\t// 将配置转换为 JSON 字符串\n\t\tthemeJSON, err := json.Marshal(defaultThemeConfig)\n\t\tif err != nil {\n\t\t\tlogger.Log.Error(\"Failed to marshal default theme config:\", err)\n\t\t\tthemeJSON = []byte(`{\"theme\":{\"color\":\"#722ed1\",\"mode\":\"dark\",\"success\":\"#52c41a\",\"warning\":\"#faad14\",\"error\":\"#f5222f\"},\"animate\":{\"disabled\":true,\"name\":\"lightSpeed\",\"direction\":\"left\"}}`)\n\t\t}\n\n\t\t// 使用结构体插入管理员账号\n\t\tpassword, _ := utils.PasswordHash(\"Zbxtable\")\n\t\tuser := User{\n\t\t\tUsername: \"admin\",\n\t\t\tPassword: password,\n\t\t\tAvatar: DefaultAvatar,\n\t\t\tRole: \"admin\",\n\t\t\tOperation: \"['add', 'edit', 'delete','update']\",\n\t\t\tTheme: string(themeJSON),\n\t\t\tStatus: 0,\n\t\t}\n\t\terr = DB.Create(&user).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"create an administrator account successfully, the admin ID is:\", user.ID)\n\t}\n\t//初始化系统数据\n\tvar cnt []System\n\terr = DB.Find(&cnt).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn\n\t}\n\tif len(cnt) == 0 {\n\t\tnow := time.Now()\n\t\tsys := []System{\n\t\t\t{Name: \"Linux操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"Windows操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"网络设备\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"物理服务器\", Status: 0, InitedAt: &now},\n\t\t}\n\t\terr := DB.Create(&sys).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init system info error!\", err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"Init system data successfully!\")\n\t}\n\t//告警默认规则初始化 default rule\n\tvar rules []Rule\n\terr = DB.Where(\"m_type = ?\", \"2\").Find(&rules).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn\n\t}\n\tif len(rules) == 0 {\n\t\t// 使用 \"*\" 作为全局默认规则,匹配所有租户\n\t\tdefaultRule := []Rule{\n\t\t\t{\n\t\t\t\tName: \"全局默认规则\",\n\t\t\t\tZIDs: \"*\",\n\t\t\t\tMType: \"2\",\n\t\t\t\tChannel: \"wechat_robot\",\n\t\t\t\tUserIds: \"1\",\n\t\t\t\tSweek: \"0,1,2,3,4,5,6\",\n\t\t\t\tStime: \"00:00\",\n\t\t\t\tEtime: \"23.59\",\n\t\t\t\tStatus: \"0\"},\n\t\t}\n\t\terr := DB.Create(&defaultRule).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init default rule error!\", err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"Init default rule successfully!\")\n\t}\n\t// 初始化加密密钥(如果不存在)\n\tvar encryptionKeyConfig Config\n\terr = DB.Where(\"`key` = ?\", \"encryption_key\").First(&encryptionKeyConfig).Error\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t// 生成随机加密密钥\n\t\trandomKey := utils.GenerateRandomKey()\n\t\tencryptionKeyConfig = Config{\n\t\t\tName: \"加密密钥\",\n\t\t\tKey: \"encryption_key\",\n\t\t\tValue: randomKey,\n\t\t\tComment: \"用于加密存储敏感信息(如Zabbix密码和Token)的密钥,系统自动生成,不可修改\",\n\t\t}\n\t\tif insertErr := DB.Create(&encryptionKeyConfig).Error; insertErr != nil {\n\t\t\tlogger.Log.Error(\"Init encryption_key error:\", insertErr)\n\t\t} else {\n\t\t\tlogger.Log.Info(\"Init encryption_key successfully! Key length:\", len(randomKey))\n\t\t}\n\t}\n\n\tdefaultConfigs := []Config{\n\t\t// 系统外观配置\n\t\t{Name: \"系统名称\", Key: \"system_name\", Value: \"ZbxTable\", Comment: \"系统显示的名称\"},\n\t\t{Name: \"系统Logo\", Key: \"system_logo\", Value: \"data:image/png;base64,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\", Comment: \"系统Logo的base64编码数据\"},\n\t\t// 初始配置状态\n\t\t{Name: \"初始配置完成\", Key: \"initial_setup_completed\", Value: \"0\", Comment: \"标记系统是否完成初始配置:1 已完成,0 未完成\"},\n\t\t// Dashboard 相关\n\t\t{Name: \"数据面板\", Key: \"zbx_dash\", Value: \"0\", Comment: \"是否开启Zabbix看板:1 开启,0 关闭\"},\n\t\t{Name: \"面板配置\", Key: \"dash_id\", Value: \"1\", Comment: \"需要引入的Zabbix面板的ID,默认为1\"},\n\t\t{Name: \"主机分类同步\", Key: \"sync_inventory\", Value: \"1\", Comment: \"主机分类同步计划任务是否启用:1 启用,0 不启用\"},\n\t\t{Name: \"Webhook回调地址\", Key: \"webhook_url\", Value: \"\", Comment: \"webhook通知地址\"},\n\t\t// 邮件配置\n\t\t{Name: \"邮件发件人\", Key: \"email_from\", Value: \"\", Comment: \"告警邮件发件人邮箱地址\"},\n\t\t{Name: \"邮件昵称\", Key: \"email_nickname\", Value: \"ZbxTable\", Comment: \"告警邮件显示的发件人昵称\"},\n\t\t{Name: \"SMTP 密码/授权码\", Key: \"email_secret\", Value: \"\", Comment: \"SMTP 登录密码或授权码\"},\n\t\t{Name: \"SMTP 服务器\", Key: \"email_host\", Value: \"smtp.qq.com\", Comment: \"SMTP 服务器地址\"},\n\t\t{Name: \"SMTP 端口\", Key: \"email_port\", Value: \"465\", Comment: \"SMTP 端口号\"},\n\t\t{Name: \"SMTP 使用 SSL\", Key: \"email_isSSl\", Value: \"true\", Comment: \"是否启用 SSL:true/false\"},\n\t\t// 企业微信配置\n\t\t{Name: \"企业微信开关\", Key: \"wechat_enabled\", Value: \"0\", Comment: \"是否启用企业微信:1 启用,0 禁用\"},\n\t\t{Name: \"企业微信 AgentID\", Key: \"wechat_agentid\", Value: \"\", Comment: \"企业微信应用的 AgentID\"},\n\t\t{Name: \"企业微信 CorpID\", Key: \"wechat_corpid\", Value: \"\", Comment: \"企业微信企业ID\"},\n\t\t{Name: \"企业微信 Secret\", Key: \"wechat_secret\", Value: \"\", Comment: \"企业微信应用的 Secret\"},\n\t\t// AI 配置\n\t\t{Name: \"AI 类型\", Key: \"ai_type\", Value: \"ollama\", Comment: \"选择使用的 AI 服务类型:ollama 或 deepseek\"},\n\t\t// Ollama 配置\n\t\t{Name: \"Ollama Host\", Key: \"ollama_host\", Value: \"http://localhost:11434\", Comment: \"Ollama 服务地址,如 http://127.0.0.1:11434\"},\n\t\t{Name: \"Ollama Model\", Key: \"ollama_model\", Value: \"deepseek-r1:32b\", Comment: \"默认使用的大模型名称\"},\n\t\t// Deepseek 配置\n\t\t{Name: \"Deepseek API Key\", Key: \"deepseek_api_key\", Value: \"\", Comment: \"Deepseek API 密钥\"},\n\t\t{Name: \"Deepseek Model\", Key: \"deepseek_model\", Value: \"deepseek-chat\", Comment: \"Deepseek 模型名称,如 deepseek-chat\"},\n\t\t{Name: \"Deepseek Base URL\", Key: \"deepseek_base_url\", Value: \"https://api.deepseek.com\", Comment: \"Deepseek API 地址,默认为 https://api.deepseek.com\"},\n\t}\n\n\tfor _, cfgItem := range defaultConfigs {\n\t\tvar existing Config\n\t\terr = DB.Where(\"`key` = ?\", cfgItem.Key).First(&existing).Error\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\tif insertErr := DB.Create(&cfgItem).Error; insertErr != nil {\n\t\t\t\tlogger.Log.Infof(\"Init config key %s error: %v\", cfgItem.Key, insertErr)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tlogger.Log.Infof(\"Init config key %s successfully!\", cfgItem.Key)\n\t\t}\n\t}\n\t//默认菜单初始化\n\tInitMenuData()\n\t//检查并添加缺失的菜单项(用于版本升级)\n\tCheckAndAddMenus()\n\n}\n\nfunc GetConfKey(v string) string {\n\t// 优先从 .env 中读取(便于开发环境配置敏感信息且不提交到 git)\n\tenvConfigOnce.Do(func() {\n\t\tfile, err := os.Open(\".env\")\n\t\tif err != nil {\n\t\t\t// 没有 .env 文件时直接跳过,后面回退到 app.conf\n\t\t\tenvFileExists = false\n\t\t\treturn\n\t\t}\n\t\tdefer file.Close()\n\n\t\tenvFileExists = true\n\t\tenvConfig = make(map[string]string)\n\n\t\tscanner := bufio.NewScanner(file)\n\t\tfor scanner.Scan() {\n\t\t\tline := strings.TrimSpace(scanner.Text())\n\t\t\tif line == \"\" || strings.HasPrefix(line, \"#\") || strings.HasPrefix(line, \";\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tparts := strings.SplitN(line, \"=\", 2)\n\t\t\tif len(parts) != 2 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tkey := strings.TrimSpace(parts[0])\n\t\t\tval := strings.TrimSpace(parts[1])\n\t\t\t// 去掉可能的引号\n\t\t\tval = strings.Trim(val, `\"'`)\n\t\t\tif key != \"\" {\n\t\t\t\tenvConfig[key] = val // 即使 val 为空也认为是有效配置\n\t\t\t}\n\t\t}\n\t})\n\n\t// 如果存在 .env 文件,则所有配置完全由 .env 决定:\n\t// - key 存在:返回对应值(可以是空字符串)\n\t// - key 不存在:返回空字符串\n\tif envFileExists {\n\t\tif envConfig == nil {\n\t\t\treturn \"\"\n\t\t}\n\t\tif val, ok := envConfig[v]; ok {\n\t\t\treturn val\n\t\t}\n\t\treturn \"\"\n\t}\n\t// 其次从 config/app.conf 读取\n\tcfg, err := ini.Load(\"./config/app.conf\")\n\tif err != nil {\n\t\tlogger.Log.Error(err)\n\t\treturn \"\"\n\t}\n\tp, err := cfg.Section(\"\").GetKey(v)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn p.String()\n}\n\n// IsPasswordConfigured 检查是否配置了密码(用于图形查看)\n// 优先检查数据库中是否有激活的 Zabbix 实例配置了用户名和密码\n// 注意:查看图形需要用户名和密码,Token 方式无法用于 Web 登录\nfunc IsPasswordConfigured() bool {\n\t// 1. 优先检查数据库中是否有激活的 Zabbix 实例\n\tvar activeInstance ZabbixInstance\n\terr := DB.Where(\"is_active = ?\", true).First(&activeInstance).Error\n\tif err == nil {\n\t\t// 找到激活的实例,检查是否配置了用户名和密码(查看图形必须用密码,Token 无法用于 Web 登录)\n\t\tif strings.TrimSpace(activeInstance.User) != \"\" && strings.TrimSpace(activeInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 2. 如果数据库中没有激活实例,检查是否有任何可用的实例配置了密码\n\tvar anyInstance ZabbixInstance\n\terr = DB.Where(\"enabled = ?\", true).First(&anyInstance).Error\n\tif err == nil {\n\t\t// 找到可用的实例,检查是否配置了用户名和密码\n\t\tif strings.TrimSpace(anyInstance.User) != \"\" && strings.TrimSpace(anyInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 3. 回退到配置文件检查(兼容旧版本)\n\tpass := GetConfKey(\"zabbix_pass\")\n\tuser := GetConfKey(\"zabbix_user\")\n\treturn pass != \"\" && user != \"\"\n}\n\n// GetEncryptionKey 获取加密密钥\n// 优先从数据库读取,如果数据库中没有则从环境变量读取,最后使用默认密钥\nfunc GetEncryptionKey() string {\n\t// 1. 优先从数据库读取\n\tkey := GetConfigValueByKey(\"encryption_key\", \"\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 2. 从环境变量或配置文件读取\n\tkey = GetConfKey(\"encryption_key\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 3. 使用默认密钥(不应该到这里,因为 DatabaseInit 会生成)\n\tlogger.Log.Warn(\"使用默认加密密钥,建议在数据库中配置 encryption_key\")\n\treturn \"zbxtable-default-encryption-key-2024\"\n}\n"}, "864ef6f77456d04cdbfc360e2c9cf604cf97e791": {"t": 1785754919.9048722, "n": 16432, "h": "73ae43ab78adca76", "c": "package model\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\t\"zbxtable/pkg/logger\"\n\t\"zbxtable/pkg/utils\"\n\n\tzabbix \"github.com/canghai908/zabbix-go\"\n\t_ \"github.com/go-sql-driver/mysql\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t_ \"github.com/lib/pq\"\n\tworkwx \"github.com/xen0n/go-workwx\"\n\tini \"gopkg.in/ini.v1\"\n\t\"gorm.io/gorm\"\n)\n\nvar (\n\tAPI = &zabbix.API{}\n\tjson = jsoniter.Config{\n\t\tEscapeHTML: false,\n\t\tSortMapKeys: true,\n\t\tValidateJsonRawMessage: true,\n\t}.Froze()\n\tZBX_VER string\n\tZBX_V bool\n\tVersion string\n\tGitHash string\n\tBuildTime string\n\tWeApp *workwx.WorkwxApp // 不初始化,默认为 nil\n\n\tenvConfig map[string]string\n\tenvConfigOnce sync.Once\n\tenvFileExists bool\n)\n\nconst (\n\t//ms-agent 常量定义\n\tMSName = \"ms-agent\"\n\tMSUser = \"ms-agent\"\n\tMSGroup = \"MS-Agent\"\n\tMSMedia = \"MS-Agent\"\n\tMSAction = \"MS-Agent\"\n\t//webhook 常量定义\n\tWebhookName = \"ZbxTable\"\n\tWebhookAction = \"ZbxTable Webhook\"\n\tWebhookGroup = \"ZbxTable Webhook\"\n\tWebhookUser = \"zbxtable-webhook\"\n\t//down\n\tDownloadPath = \"./download/\"\n\tTplPath = \"./assets/templates/\"\n)\n\n// TableName 表名前缀\nfunc TableName(str string) string {\n\treturn fmt.Sprintf(\"%s%s\", \"zbxtable_\", str)\n}\n\n// 接收数据库信息初始化\nfunc ModelInit(dbtype, dbhost, dbuser, dbpass, dbname, dbport string) {\n\t// 直接使用 GORM\n\trunmode := GetConfKey(\"runmode\")\n\terr := InitGormDB(dbtype, dbhost, dbuser, dbpass, dbname, dbport, runmode)\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to connect database: \", err)\n\t\tos.Exit(1)\n\t}\n\tlogger.Log.Info(\"Database connected!\")\n\t// 自动迁移表\n\terr = AutoMigrate()\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to auto migrate: \", err)\n\t\tos.Exit(1)\n\t}\n\t// 基础数据初始化\n\tDatabaseInit()\n\t// 使用go-cache替代Redis(不再需要Redis连接)\n\tInitCache()\n}\n\nfunc InitWechat() {\n\t// 首先检查企业微信开关是否启用\n\twechatEnabled := GetConfigValueByKey(\"wechat_enabled\", \"0\")\n\tif wechatEnabled != \"1\" {\n\t\tlogger.Log.Info(\"WeChat is disabled in system config, skipping WeChat initialization\")\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\t// 开关已启用,从数据库读取企业微信配置\n\tagentIDStr := GetConfigValueByKey(\"wechat_agentid\", \"\")\n\tcorpid := GetConfigValueByKey(\"wechat_corpid\", \"\")\n\tsecret := GetConfigValueByKey(\"wechat_secret\", \"\") // 这里会自动解密\n\n\t// 验证必填配置项\n\tif agentIDStr == \"\" || corpid == \"\" || secret == \"\" {\n\t\tlogger.Log.Info(\"WeChat is enabled but configuration is incomplete (agentid/corpid/secret), WeChat app will not be initialized\")\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\t// 解析 AgentID\n\tAgentId, err := strconv.ParseInt(agentIDStr, 10, 64)\n\tif err != nil {\n\t\tlogger.Log.Error(\"wechat_agentid parse error:\", err)\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\n\t// 初始化企业微信客户端\n\tclient := workwx.New(corpid)\n\tWeApp = client.WithApp(secret, AgentId)\n\tWeApp.SpawnAccessTokenRefresher()\n\tlogger.Log.Info(\"WeChat inited successfully!\")\n}\n\n// DatabaseInit 数据初始化\nfunc DatabaseInit() {\n\t//数据初始化操作 - 使用 GORM\n\tvar v User\n\terr := DB.Where(\"username = ?\", \"admin\").First(&v).Error\n\t//检查权限\n\tif err == nil && v.Operation == \"\" {\n\t\terr := DB.Model(&User{}).Where(\"id = ?\", v.ID).Update(\"operation\", \"['add', 'edit', 'delete','update']\").Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"update admin operation successfully\")\n\t}\n\t//添加管理员账号\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\tlogger.Log.Info(\"the admin user does not exist, create a new admin account later!\")\n\n\t\t// 构建默认主题配置(使用结构化方式,避免手写 JSON 出错)\n\t\tdefaultThemeConfig := map[string]interface{}{\n\t\t\t\"theme\": map[string]string{\n\t\t\t\t\"color\": \"#1890ff\",\n\t\t\t\t\"mode\": \"dark\",\n\t\t\t\t\"success\": \"#52c41a\",\n\t\t\t\t\"warning\": \"#faad14\",\n\t\t\t\t\"error\": \"#f5222f\",\n\t\t\t},\n\t\t\t\"animate\": map[string]interface{}{\n\t\t\t\t\"disabled\": true,\n\t\t\t\t\"name\": \"lightSpeed\",\n\t\t\t\t\"direction\": \"left\",\n\t\t\t},\n\t\t}\n\n\t\t// 将配置转换为 JSON 字符串\n\t\tthemeJSON, err := json.Marshal(defaultThemeConfig)\n\t\tif err != nil {\n\t\t\tlogger.Log.Error(\"Failed to marshal default theme config:\", err)\n\t\t\t// 如果序列化失败,使用备用的硬编码 JSON\n\t\t\tthemeJSON = []byte(`{\"theme\":{\"color\":\"#722ed1\",\"mode\":\"dark\",\"success\":\"#52c41a\",\"warning\":\"#faad14\",\"error\":\"#f5222f\"},\"animate\":{\"disabled\":true,\"name\":\"lightSpeed\",\"direction\":\"left\"}}`)\n\t\t}\n\n\t\tvar user User\n\t\tuser.Username = \"admin\"\n\t\tuser.Password, _ = utils.PasswordHash(\"Zbxtable\")\n\t\tuser.Avatar = \"https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif\"\n\t\tuser.Role = \"admin\"\n\t\tuser.Operation = \"['add', 'edit', 'delete','update']\"\n\t\tuser.Theme = string(themeJSON)\n\t\tuser.Status = 0\n\t\terr = DB.Create(&user).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"create an administrator account successfully, the admin ID is:\", user.ID)\n\t}\n\t//初始化系统数据\n\tvar cnt []System\n\terr = DB.Find(&cnt).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn\n\t}\n\tif len(cnt) == 0 {\n\t\tnow := time.Now()\n\t\tsys := []System{\n\t\t\t{Name: \"Linux操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"Windows操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"网络设备\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"物理服务器\", Status: 0, InitedAt: &now},\n\t\t}\n\t\terr := DB.Create(&sys).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init system info error!\", err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"Init system data successfully!\")\n\t}\n\t//告警默认规则初始化 default rule\n\tvar rules []Rule\n\terr = DB.Where(\"m_type = ?\", \"2\").Find(&rules).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn\n\t}\n\tif len(rules) == 0 {\n\t\t// 使用 \"*\" 作为全局默认规则,匹配所有租户\n\t\tdefaultRule := []Rule{\n\t\t\t{\n\t\t\t\tName: \"全局默认规则\",\n\t\t\t\tZIDs: \"*\",\n\t\t\t\tMType: \"2\",\n\t\t\t\tChannel: \"wechat_robot\",\n\t\t\t\tUserIds: \"1\",\n\t\t\t\tSweek: \"0,1,2,3,4,5,6\",\n\t\t\t\tStime: \"00:00\",\n\t\t\t\tEtime: \"23.59\",\n\t\t\t\tStatus: \"0\"},\n\t\t}\n\t\terr := DB.Create(&defaultRule).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init default rule error!\", err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"Init default rule successfully!\")\n\t}\n\t// 初始化加密密钥(如果不存在)\n\tvar encryptionKeyConfig Config\n\terr = DB.Where(\"`key` = ?\", \"encryption_key\").First(&encryptionKeyConfig).Error\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t// 生成随机加密密钥\n\t\trandomKey := utils.GenerateRandomKey()\n\t\tencryptionKeyConfig = Config{\n\t\t\tName: \"加密密钥\",\n\t\t\tKey: \"encryption_key\",\n\t\t\tValue: randomKey,\n\t\t\tComment: \"用于加密存储敏感信息(如Zabbix密码和Token)的密钥,系统自动生成,不可修改\",\n\t\t}\n\t\tif insertErr := DB.Create(&encryptionKeyConfig).Error; insertErr != nil {\n\t\t\tlogger.Log.Error(\"Init encryption_key error:\", insertErr)\n\t\t} else {\n\t\t\tlogger.Log.Info(\"Init encryption_key successfully! Key length:\", len(randomKey))\n\t\t}\n\t}\n\n\tdefaultConfigs := []Config{\n\t\t// 系统外观配置\n\t\t{Name: \"系统名称\", Key: \"system_name\", Value: \"ZbxTable\", Comment: \"系统显示的名称\"},\n\t\t{Name: \"系统Logo\", Key: \"system_logo\", Value: \"data:image/png;base64,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\", Comment: \"系统Logo的base64编码数据\"},\n\t\t// 初始配置状态\n\t\t{Name: \"初始配置完成\", Key: \"initial_setup_completed\", Value: \"0\", Comment: \"标记系统是否完成初始配置:1 已完成,0 未完成\"},\n\t\t// Dashboard 相关\n\t\t{Name: \"数据面板\", Key: \"zbx_dash\", Value: \"0\", Comment: \"是否开启Zabbix看板:1 开启,0 关闭\"},\n\t\t{Name: \"面板配置\", Key: \"dash_id\", Value: \"1\", Comment: \"需要引入的Zabbix面板的ID,默认为1\"},\n\t\t{Name: \"主机分类同步\", Key: \"sync_inventory\", Value: \"1\", Comment: \"主机分类同步计划任务是否启用:1 启用,0 不启用\"},\n\t\t{Name: \"Webhook回调地址\", Key: \"webhook_url\", Value: \"\", Comment: \"webhook通知地址\"},\n\t\t// 邮件配置\n\t\t{Name: \"邮件发件人\", Key: \"email_from\", Value: \"\", Comment: \"告警邮件发件人邮箱地址\"},\n\t\t{Name: \"邮件昵称\", Key: \"email_nickname\", Value: \"ZbxTable\", Comment: \"告警邮件显示的发件人昵称\"},\n\t\t{Name: \"SMTP 密码/授权码\", Key: \"email_secret\", Value: \"\", Comment: \"SMTP 登录密码或授权码\"},\n\t\t{Name: \"SMTP 服务器\", Key: \"email_host\", Value: \"smtp.qq.com\", Comment: \"SMTP 服务器地址\"},\n\t\t{Name: \"SMTP 端口\", Key: \"email_port\", Value: \"465\", Comment: \"SMTP 端口号\"},\n\t\t{Name: \"SMTP 使用 SSL\", Key: \"email_isSSl\", Value: \"true\", Comment: \"是否启用 SSL:true/false\"},\n\t\t// 企业微信配置\n\t\t{Name: \"企业微信开关\", Key: \"wechat_enabled\", Value: \"0\", Comment: \"是否启用企业微信:1 启用,0 禁用\"},\n\t\t{Name: \"企业微信 AgentID\", Key: \"wechat_agentid\", Value: \"\", Comment: \"企业微信应用的 AgentID\"},\n\t\t{Name: \"企业微信 CorpID\", Key: \"wechat_corpid\", Value: \"\", Comment: \"企业微信企业ID\"},\n\t\t{Name: \"企业微信 Secret\", Key: \"wechat_secret\", Value: \"\", Comment: \"企业微信应用的 Secret\"},\n\t\t// AI 配置\n\t\t{Name: \"AI 类型\", Key: \"ai_type\", Value: \"ollama\", Comment: \"选择使用的 AI 服务类型:ollama 或 deepseek\"},\n\t\t// Ollama 配置\n\t\t{Name: \"Ollama Host\", Key: \"ollama_host\", Value: \"http://localhost:11434\", Comment: \"Ollama 服务地址,如 http://127.0.0.1:11434\"},\n\t\t{Name: \"Ollama Model\", Key: \"ollama_model\", Value: \"deepseek-r1:32b\", Comment: \"默认使用的大模型名称\"},\n\t\t// Deepseek 配置\n\t\t{Name: \"Deepseek API Key\", Key: \"deepseek_api_key\", Value: \"\", Comment: \"Deepseek API 密钥\"},\n\t\t{Name: \"Deepseek Model\", Key: \"deepseek_model\", Value: \"deepseek-chat\", Comment: \"Deepseek 模型名称,如 deepseek-chat\"},\n\t\t{Name: \"Deepseek Base URL\", Key: \"deepseek_base_url\", Value: \"https://api.deepseek.com\", Comment: \"Deepseek API 地址,默认为 https://api.deepseek.com\"},\n\t}\n\n\tfor _, cfgItem := range defaultConfigs {\n\t\tvar existing Config\n\t\terr = DB.Where(\"`key` = ?\", cfgItem.Key).First(&existing).Error\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\tif insertErr := DB.Create(&cfgItem).Error; insertErr != nil {\n\t\t\t\tlogger.Log.Infof(\"Init config key %s error: %v\", cfgItem.Key, insertErr)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tlogger.Log.Infof(\"Init config key %s successfully!\", cfgItem.Key)\n\t\t}\n\t}\n\t//默认菜单初始化\n\tInitMenuData()\n\t//检查并添加缺失的菜单项(用于版本升级)\n\tCheckAndAddMenus()\n\n}\n\nfunc GetConfKey(v string) string {\n\t// 优先从 .env 中读取(便于开发环境配置敏感信息且不提交到 git)\n\tenvConfigOnce.Do(func() {\n\t\tfile, err := os.Open(\".env\")\n\t\tif err != nil {\n\t\t\t// 没有 .env 文件时直接跳过,后面回退到 app.conf\n\t\t\tenvFileExists = false\n\t\t\treturn\n\t\t}\n\t\tdefer file.Close()\n\n\t\tenvFileExists = true\n\t\tenvConfig = make(map[string]string)\n\n\t\tscanner := bufio.NewScanner(file)\n\t\tfor scanner.Scan() {\n\t\t\tline := strings.TrimSpace(scanner.Text())\n\t\t\tif line == \"\" || strings.HasPrefix(line, \"#\") || strings.HasPrefix(line, \";\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tparts := strings.SplitN(line, \"=\", 2)\n\t\t\tif len(parts) != 2 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tkey := strings.TrimSpace(parts[0])\n\t\t\tval := strings.TrimSpace(parts[1])\n\t\t\t// 去掉可能的引号\n\t\t\tval = strings.Trim(val, `\"'`)\n\t\t\tif key != \"\" {\n\t\t\t\tenvConfig[key] = val // 即使 val 为空也认为是有效配置\n\t\t\t}\n\t\t}\n\t})\n\n\t// 如果存在 .env 文件,则所有配置完全由 .env 决定:\n\t// - key 存在:返回对应值(可以是空字符串)\n\t// - key 不存在:返回空字符串\n\tif envFileExists {\n\t\tif envConfig == nil {\n\t\t\treturn \"\"\n\t\t}\n\t\tif val, ok := envConfig[v]; ok {\n\t\t\treturn val\n\t\t}\n\t\treturn \"\"\n\t}\n\t// 其次从 config/app.conf 读取\n\tcfg, err := ini.Load(\"./config/app.conf\")\n\tif err != nil {\n\t\tlogger.Log.Error(err)\n\t\treturn \"\"\n\t}\n\tp, err := cfg.Section(\"\").GetKey(v)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn p.String()\n}\n\n// IsPasswordConfigured 检查是否配置了密码(用于图形查看)\n// 优先检查数据库中是否有激活的 Zabbix 实例配置了用户名和密码\n// 注意:查看图形需要用户名和密码,Token 方式无法用于 Web 登录\nfunc IsPasswordConfigured() bool {\n\t// 1. 优先检查数据库中是否有激活的 Zabbix 实例\n\tvar activeInstance ZabbixInstance\n\terr := DB.Where(\"is_active = ?\", true).First(&activeInstance).Error\n\tif err == nil {\n\t\t// 找到激活的实例,检查是否配置了用户名和密码(查看图形必须用密码,Token 无法用于 Web 登录)\n\t\tif strings.TrimSpace(activeInstance.User) != \"\" && strings.TrimSpace(activeInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 2. 如果数据库中没有激活实例,检查是否有任何可用的实例配置了密码\n\tvar anyInstance ZabbixInstance\n\terr = DB.Where(\"enabled = ?\", true).First(&anyInstance).Error\n\tif err == nil {\n\t\t// 找到可用的实例,检查是否配置了用户名和密码\n\t\tif strings.TrimSpace(anyInstance.User) != \"\" && strings.TrimSpace(anyInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 3. 回退到配置文件检查(兼容旧版本)\n\tpass := GetConfKey(\"zabbix_pass\")\n\tuser := GetConfKey(\"zabbix_user\")\n\treturn pass != \"\" && user != \"\"\n}\n\n// GetEncryptionKey 获取加密密钥\n// 优先从数据库读取,如果数据库中没有则从环境变量读取,最后使用默认密钥\nfunc GetEncryptionKey() string {\n\t// 1. 优先从数据库读取\n\tkey := GetConfigValueByKey(\"encryption_key\", \"\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 2. 从环境变量或配置文件读取\n\tkey = GetConfKey(\"encryption_key\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 3. 使用默认密钥(不应该到这里,因为 DatabaseInit 会生成)\n\tlogger.Log.Warn(\"使用默认加密密钥,建议在数据库中配置 encryption_key\")\n\treturn \"zbxtable-default-encryption-key-2024\"\n}\n"}, "7c3b8ac24682e4324543da0684885bfa633eb895": {"t": 1785754921.1438577, "n": 16432, "h": "d6200f49c756bca1", "c": "package model\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\t\"zbxtable/pkg/logger\"\n\t\"zbxtable/pkg/utils\"\n\n\tzabbix \"github.com/canghai908/zabbix-go\"\n\t_ \"github.com/go-sql-driver/mysql\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t_ \"github.com/lib/pq\"\n\tworkwx \"github.com/xen0n/go-workwx\"\n\tini \"gopkg.in/ini.v1\"\n\t\"gorm.io/gorm\"\n)\n\nvar (\n\tAPI = &zabbix.API{}\n\tjson = jsoniter.Config{\n\t\tEscapeHTML: false,\n\t\tSortMapKeys: true,\n\t\tValidateJsonRawMessage: true,\n\t}.Froze()\n\tZBX_VER string\n\tZBX_V bool\n\tVersion string\n\tGitHash string\n\tBuildTime string\n\tWeApp *workwx.WorkwxApp // 不初始化,默认为 nil\n\n\tenvConfig map[string]string\n\tenvConfigOnce sync.Once\n\tenvFileExists bool\n)\n\nconst (\n\t//ms-agent 常量定义\n\tMSName = \"ms-agent\"\n\tMSUser = \"ms-agent\"\n\tMSGroup = \"MS-Agent\"\n\tMSMedia = \"MS-Agent\"\n\tMSAction = \"MS-Agent\"\n\t//webhook 常量定义\n\tWebhookName = \"ZbxTable\"\n\tWebhookAction = \"ZbxTable Webhook\"\n\tWebhookGroup = \"ZbxTable Webhook\"\n\tWebhookUser = \"zbxtable-webhook\"\n\t//down\n\tDownloadPath = \"./download/\"\n\tTplPath = \"./assets/templates/\"\n)\n\n// TableName 表名前缀\nfunc TableName(str string) string {\n\treturn fmt.Sprintf(\"%s%s\", \"zbxtable_\", str)\n}\n\n// 接收数据库信息初始化\nfunc ModelInit(dbtype, dbhost, dbuser, dbpass, dbname, dbport string) {\n\t// 直接使用 GORM\n\trunmode := GetConfKey(\"runmode\")\n\terr := InitGormDB(dbtype, dbhost, dbuser, dbpass, dbname, dbport, runmode)\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to connect database: \", err)\n\t\tos.Exit(1)\n\t}\n\tlogger.Log.Info(\"Database connected!\")\n\t// 自动迁移表\n\terr = AutoMigrate()\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to auto migrate: \", err)\n\t\tos.Exit(1)\n\t}\n\t// 基础数据初始化\n\tDatabaseInit()\n\t// 使用go-cache替代Redis(不再需要Redis连接)\n\tInitCache()\n}\n\nfunc InitWechat() {\n\t// 首先检查企业微信开关是否启用\n\twechatEnabled := GetConfigValueByKey(\"wechat_enabled\", \"0\")\n\tif wechatEnabled != \"1\" {\n\t\tlogger.Log.Info(\"WeChat is disabled in system config, skipping WeChat initialization\")\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\t// 开关已启用,从数据库读取企业微信配置\n\tagentIDStr := GetConfigValueByKey(\"wechat_agentid\", \"\")\n\tcorpid := GetConfigValueByKey(\"wechat_corpid\", \"\")\n\tsecret := GetConfigValueByKey(\"wechat_secret\", \"\") // 这里会自动解密\n\n\t// 验证必填配置项\n\tif agentIDStr == \"\" || corpid == \"\" || secret == \"\" {\n\t\tlogger.Log.Info(\"WeChat is enabled but configuration is incomplete (agentid/corpid/secret), WeChat app will not be initialized\")\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\t// 解析 AgentID\n\tAgentId, err := strconv.ParseInt(agentIDStr, 10, 64)\n\tif err != nil {\n\t\tlogger.Log.Error(\"wechat_agentid parse error:\", err)\n\t\tWeApp = nil // 明确设置为 nil\n\t\treturn\n\t}\n\n\t// 初始化企业微信客户端\n\tclient := workwx.New(corpid)\n\tWeApp = client.WithApp(secret, AgentId)\n\tWeApp.SpawnAccessTokenRefresher()\n\tlogger.Log.Info(\"WeChat inited successfully!\")\n}\n\n// DatabaseInit 数据初始化\nfunc DatabaseInit() {\n\t//数据初始化操作 - 使用 GORM\n\tvar v User\n\terr := DB.Where(\"username = ?\", \"admin\").First(&v).Error\n\t//检查权限\n\tif err == nil && v.Operation == \"\" {\n\t\terr := DB.Model(&User{}).Where(\"id = ?\", v.ID).Update(\"operation\", \"['add', 'edit', 'delete','update']\").Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"update admin operation successfully\")\n\t}\n\t//添加管理员账号\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\tlogger.Log.Info(\"the admin user does not exist, create a new admin account later!\")\n\n\t\t// 构建默认主题配置(使用结构化方式,避免手写 JSON 出错)\n\t\tdefaultThemeConfig := map[string]interface{}{\n\t\t\t\"theme\": map[string]string{\n\t\t\t\t\"color\": \"#1890ff\",\n\t\t\t\t\"mode\": \"dark\",\n\t\t\t\t\"success\": \"#52c41a\",\n\t\t\t\t\"warning\": \"#faad14\",\n\t\t\t\t\"error\": \"#f5222f\",\n\t\t\t},\n\t\t\t\"animate\": map[string]interface{}{\n\t\t\t\t\"disabled\": true,\n\t\t\t\t\"name\": \"lightSpeed\",\n\t\t\t\t\"direction\": \"left\",\n\t\t\t},\n\t\t}\n\n\t\t// 将配置转换为 JSON 字符串\n\t\tthemeJSON, err := json.Marshal(defaultThemeConfig)\n\t\tif err != nil {\n\t\t\tlogger.Log.Error(\"Failed to marshal default theme config:\", err)\n\t\t\t// 如果序列化失败,使用备用的硬编码 JSON\n\t\t\tthemeJSON = []byte(`{\"theme\":{\"color\":\"#1890ff\",\"mode\":\"dark\",\"success\":\"#52c41a\",\"warning\":\"#faad14\",\"error\":\"#f5222f\"},\"animate\":{\"disabled\":true,\"name\":\"lightSpeed\",\"direction\":\"left\"}}`)\n\t\t}\n\n\t\tvar user User\n\t\tuser.Username = \"admin\"\n\t\tuser.Password, _ = utils.PasswordHash(\"Zbxtable\")\n\t\tuser.Avatar = \"https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif\"\n\t\tuser.Role = \"admin\"\n\t\tuser.Operation = \"['add', 'edit', 'delete','update']\"\n\t\tuser.Theme = string(themeJSON)\n\t\tuser.Status = 0\n\t\terr = DB.Create(&user).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"create an administrator account successfully, the admin ID is:\", user.ID)\n\t}\n\t//初始化系统数据\n\tvar cnt []System\n\terr = DB.Find(&cnt).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn\n\t}\n\tif len(cnt) == 0 {\n\t\tnow := time.Now()\n\t\tsys := []System{\n\t\t\t{Name: \"Linux操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"Windows操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"网络设备\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"物理服务器\", Status: 0, InitedAt: &now},\n\t\t}\n\t\terr := DB.Create(&sys).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init system info error!\", err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"Init system data successfully!\")\n\t}\n\t//告警默认规则初始化 default rule\n\tvar rules []Rule\n\terr = DB.Where(\"m_type = ?\", \"2\").Find(&rules).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn\n\t}\n\tif len(rules) == 0 {\n\t\t// 使用 \"*\" 作为全局默认规则,匹配所有租户\n\t\tdefaultRule := []Rule{\n\t\t\t{\n\t\t\t\tName: \"全局默认规则\",\n\t\t\t\tZIDs: \"*\",\n\t\t\t\tMType: \"2\",\n\t\t\t\tChannel: \"wechat_robot\",\n\t\t\t\tUserIds: \"1\",\n\t\t\t\tSweek: \"0,1,2,3,4,5,6\",\n\t\t\t\tStime: \"00:00\",\n\t\t\t\tEtime: \"23.59\",\n\t\t\t\tStatus: \"0\"},\n\t\t}\n\t\terr := DB.Create(&defaultRule).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init default rule error!\", err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"Init default rule successfully!\")\n\t}\n\t// 初始化加密密钥(如果不存在)\n\tvar encryptionKeyConfig Config\n\terr = DB.Where(\"`key` = ?\", \"encryption_key\").First(&encryptionKeyConfig).Error\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t// 生成随机加密密钥\n\t\trandomKey := utils.GenerateRandomKey()\n\t\tencryptionKeyConfig = Config{\n\t\t\tName: \"加密密钥\",\n\t\t\tKey: \"encryption_key\",\n\t\t\tValue: randomKey,\n\t\t\tComment: \"用于加密存储敏感信息(如Zabbix密码和Token)的密钥,系统自动生成,不可修改\",\n\t\t}\n\t\tif insertErr := DB.Create(&encryptionKeyConfig).Error; insertErr != nil {\n\t\t\tlogger.Log.Error(\"Init encryption_key error:\", insertErr)\n\t\t} else {\n\t\t\tlogger.Log.Info(\"Init encryption_key successfully! Key length:\", len(randomKey))\n\t\t}\n\t}\n\n\tdefaultConfigs := []Config{\n\t\t// 系统外观配置\n\t\t{Name: \"系统名称\", Key: \"system_name\", Value: \"ZbxTable\", Comment: \"系统显示的名称\"},\n\t\t{Name: \"系统Logo\", Key: \"system_logo\", Value: \"data:image/png;base64,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\", Comment: \"系统Logo的base64编码数据\"},\n\t\t// 初始配置状态\n\t\t{Name: \"初始配置完成\", Key: \"initial_setup_completed\", Value: \"0\", Comment: \"标记系统是否完成初始配置:1 已完成,0 未完成\"},\n\t\t// Dashboard 相关\n\t\t{Name: \"数据面板\", Key: \"zbx_dash\", Value: \"0\", Comment: \"是否开启Zabbix看板:1 开启,0 关闭\"},\n\t\t{Name: \"面板配置\", Key: \"dash_id\", Value: \"1\", Comment: \"需要引入的Zabbix面板的ID,默认为1\"},\n\t\t{Name: \"主机分类同步\", Key: \"sync_inventory\", Value: \"1\", Comment: \"主机分类同步计划任务是否启用:1 启用,0 不启用\"},\n\t\t{Name: \"Webhook回调地址\", Key: \"webhook_url\", Value: \"\", Comment: \"webhook通知地址\"},\n\t\t// 邮件配置\n\t\t{Name: \"邮件发件人\", Key: \"email_from\", Value: \"\", Comment: \"告警邮件发件人邮箱地址\"},\n\t\t{Name: \"邮件昵称\", Key: \"email_nickname\", Value: \"ZbxTable\", Comment: \"告警邮件显示的发件人昵称\"},\n\t\t{Name: \"SMTP 密码/授权码\", Key: \"email_secret\", Value: \"\", Comment: \"SMTP 登录密码或授权码\"},\n\t\t{Name: \"SMTP 服务器\", Key: \"email_host\", Value: \"smtp.qq.com\", Comment: \"SMTP 服务器地址\"},\n\t\t{Name: \"SMTP 端口\", Key: \"email_port\", Value: \"465\", Comment: \"SMTP 端口号\"},\n\t\t{Name: \"SMTP 使用 SSL\", Key: \"email_isSSl\", Value: \"true\", Comment: \"是否启用 SSL:true/false\"},\n\t\t// 企业微信配置\n\t\t{Name: \"企业微信开关\", Key: \"wechat_enabled\", Value: \"0\", Comment: \"是否启用企业微信:1 启用,0 禁用\"},\n\t\t{Name: \"企业微信 AgentID\", Key: \"wechat_agentid\", Value: \"\", Comment: \"企业微信应用的 AgentID\"},\n\t\t{Name: \"企业微信 CorpID\", Key: \"wechat_corpid\", Value: \"\", Comment: \"企业微信企业ID\"},\n\t\t{Name: \"企业微信 Secret\", Key: \"wechat_secret\", Value: \"\", Comment: \"企业微信应用的 Secret\"},\n\t\t// AI 配置\n\t\t{Name: \"AI 类型\", Key: \"ai_type\", Value: \"ollama\", Comment: \"选择使用的 AI 服务类型:ollama 或 deepseek\"},\n\t\t// Ollama 配置\n\t\t{Name: \"Ollama Host\", Key: \"ollama_host\", Value: \"http://localhost:11434\", Comment: \"Ollama 服务地址,如 http://127.0.0.1:11434\"},\n\t\t{Name: \"Ollama Model\", Key: \"ollama_model\", Value: \"deepseek-r1:32b\", Comment: \"默认使用的大模型名称\"},\n\t\t// Deepseek 配置\n\t\t{Name: \"Deepseek API Key\", Key: \"deepseek_api_key\", Value: \"\", Comment: \"Deepseek API 密钥\"},\n\t\t{Name: \"Deepseek Model\", Key: \"deepseek_model\", Value: \"deepseek-chat\", Comment: \"Deepseek 模型名称,如 deepseek-chat\"},\n\t\t{Name: \"Deepseek Base URL\", Key: \"deepseek_base_url\", Value: \"https://api.deepseek.com\", Comment: \"Deepseek API 地址,默认为 https://api.deepseek.com\"},\n\t}\n\n\tfor _, cfgItem := range defaultConfigs {\n\t\tvar existing Config\n\t\terr = DB.Where(\"`key` = ?\", cfgItem.Key).First(&existing).Error\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\tif insertErr := DB.Create(&cfgItem).Error; insertErr != nil {\n\t\t\t\tlogger.Log.Infof(\"Init config key %s error: %v\", cfgItem.Key, insertErr)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tlogger.Log.Infof(\"Init config key %s successfully!\", cfgItem.Key)\n\t\t}\n\t}\n\t//默认菜单初始化\n\tInitMenuData()\n\t//检查并添加缺失的菜单项(用于版本升级)\n\tCheckAndAddMenus()\n\n}\n\nfunc GetConfKey(v string) string {\n\t// 优先从 .env 中读取(便于开发环境配置敏感信息且不提交到 git)\n\tenvConfigOnce.Do(func() {\n\t\tfile, err := os.Open(\".env\")\n\t\tif err != nil {\n\t\t\t// 没有 .env 文件时直接跳过,后面回退到 app.conf\n\t\t\tenvFileExists = false\n\t\t\treturn\n\t\t}\n\t\tdefer file.Close()\n\n\t\tenvFileExists = true\n\t\tenvConfig = make(map[string]string)\n\n\t\tscanner := bufio.NewScanner(file)\n\t\tfor scanner.Scan() {\n\t\t\tline := strings.TrimSpace(scanner.Text())\n\t\t\tif line == \"\" || strings.HasPrefix(line, \"#\") || strings.HasPrefix(line, \";\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tparts := strings.SplitN(line, \"=\", 2)\n\t\t\tif len(parts) != 2 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tkey := strings.TrimSpace(parts[0])\n\t\t\tval := strings.TrimSpace(parts[1])\n\t\t\t// 去掉可能的引号\n\t\t\tval = strings.Trim(val, `\"'`)\n\t\t\tif key != \"\" {\n\t\t\t\tenvConfig[key] = val // 即使 val 为空也认为是有效配置\n\t\t\t}\n\t\t}\n\t})\n\n\t// 如果存在 .env 文件,则所有配置完全由 .env 决定:\n\t// - key 存在:返回对应值(可以是空字符串)\n\t// - key 不存在:返回空字符串\n\tif envFileExists {\n\t\tif envConfig == nil {\n\t\t\treturn \"\"\n\t\t}\n\t\tif val, ok := envConfig[v]; ok {\n\t\t\treturn val\n\t\t}\n\t\treturn \"\"\n\t}\n\t// 其次从 config/app.conf 读取\n\tcfg, err := ini.Load(\"./config/app.conf\")\n\tif err != nil {\n\t\tlogger.Log.Error(err)\n\t\treturn \"\"\n\t}\n\tp, err := cfg.Section(\"\").GetKey(v)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn p.String()\n}\n\n// IsPasswordConfigured 检查是否配置了密码(用于图形查看)\n// 优先检查数据库中是否有激活的 Zabbix 实例配置了用户名和密码\n// 注意:查看图形需要用户名和密码,Token 方式无法用于 Web 登录\nfunc IsPasswordConfigured() bool {\n\t// 1. 优先检查数据库中是否有激活的 Zabbix 实例\n\tvar activeInstance ZabbixInstance\n\terr := DB.Where(\"is_active = ?\", true).First(&activeInstance).Error\n\tif err == nil {\n\t\t// 找到激活的实例,检查是否配置了用户名和密码(查看图形必须用密码,Token 无法用于 Web 登录)\n\t\tif strings.TrimSpace(activeInstance.User) != \"\" && strings.TrimSpace(activeInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 2. 如果数据库中没有激活实例,检查是否有任何可用的实例配置了密码\n\tvar anyInstance ZabbixInstance\n\terr = DB.Where(\"enabled = ?\", true).First(&anyInstance).Error\n\tif err == nil {\n\t\t// 找到可用的实例,检查是否配置了用户名和密码\n\t\tif strings.TrimSpace(anyInstance.User) != \"\" && strings.TrimSpace(anyInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 3. 回退到配置文件检查(兼容旧版本)\n\tpass := GetConfKey(\"zabbix_pass\")\n\tuser := GetConfKey(\"zabbix_user\")\n\treturn pass != \"\" && user != \"\"\n}\n\n// GetEncryptionKey 获取加密密钥\n// 优先从数据库读取,如果数据库中没有则从环境变量读取,最后使用默认密钥\nfunc GetEncryptionKey() string {\n\t// 1. 优先从数据库读取\n\tkey := GetConfigValueByKey(\"encryption_key\", \"\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 2. 从环境变量或配置文件读取\n\tkey = GetConfKey(\"encryption_key\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 3. 使用默认密钥(不应该到这里,因为 DatabaseInit 会生成)\n\tlogger.Log.Warn(\"使用默认加密密钥,建议在数据库中配置 encryption_key\")\n\treturn \"zbxtable-default-encryption-key-2024\"\n}\n"}, "e996ccfc882ad5a0cf03b6b98ef5ca1990d7a5be": {"t": 1785754922.4489913, "n": 16339, "h": "8d629a359dcde43d", "c": "package model\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\t\"zbxtable/pkg/logger\"\n\t\"zbxtable/pkg/utils\"\n\n\tzabbix \"github.com/canghai908/zabbix-go\"\n\t_ \"github.com/go-sql-driver/mysql\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t_ \"github.com/lib/pq\"\n\tworkwx \"github.com/xen0n/go-workwx\"\n\tini \"gopkg.in/ini.v1\"\n\t\"gorm.io/gorm\"\n)\n\nvar (\n\tAPI = &zabbix.API{}\n\tjson = jsoniter.Config{\n\t\tEscapeHTML: false,\n\t\tSortMapKeys: true,\n\t\tValidateJsonRawMessage: true,\n\t}.Froze()\n\tZBX_VER string\n\tZBX_V bool\n\tVersion string\n\tGitHash string\n\tBuildTime string\n\tWeApp = &workwx.WorkwxApp{}\n\n\tenvConfig map[string]string\n\tenvConfigOnce sync.Once\n\tenvFileExists bool\n)\n\nconst (\n\t//ms-agent 常量定义\n\tMSName = \"ms-agent\"\n\tMSUser = \"ms-agent\"\n\tMSGroup = \"MS-Agent\"\n\tMSMedia = \"MS-Agent\"\n\tMSAction = \"MS-Agent\"\n\t//webhook 常量定义\n\tWebhookName = \"ZbxTable\"\n\tWebhookAction = \"ZbxTable Webhook\"\n\tWebhookGroup = \"ZbxTable Webhook\"\n\tWebhookUser = \"zbxtable-webhook\"\n\t//down\n\tDownloadPath = \"./download/\"\n\tTplPath = \"./assets/templates/\"\n)\n\n// TableName 表名前缀\nfunc TableName(str string) string {\n\treturn fmt.Sprintf(\"%s%s\", \"zbxtable_\", str)\n}\n\n// 接收数据库信息初始化\nfunc ModelInit(dbtype, dbhost, dbuser, dbpass, dbname, dbport string) {\n\t// 直接使用 GORM\n\trunmode := GetConfKey(\"runmode\")\n\terr := InitGormDB(dbtype, dbhost, dbuser, dbpass, dbname, dbport, runmode)\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to connect database: \", err)\n\t\tos.Exit(1)\n\t}\n\tlogger.Log.Info(\"Database connected!\")\n\t// 自动迁移表\n\terr = AutoMigrate()\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to auto migrate: \", err)\n\t\tos.Exit(1)\n\t}\n\t// 基础数据初始化\n\tDatabaseInit()\n\t// 使用go-cache替代Redis(不再需要Redis连接)\n\tInitCache()\n}\n\nfunc InitWechat() {\n\t// 首先检查企业微信开关是否启用\n\twechatEnabled := GetConfigValueByKey(\"wechat_enabled\", \"0\")\n\tif wechatEnabled != \"1\" {\n\t\tlogger.Log.Info(\"WeChat is disabled in system config, skipping WeChat initialization\")\n\t\treturn\n\t}\n\t// 开关已启用,从数据库读取企业微信配置\n\tagentIDStr := GetConfigValueByKey(\"wechat_agentid\", \"\")\n\tcorpid := GetConfigValueByKey(\"wechat_corpid\", \"\")\n\tsecret := GetConfigValueByKey(\"wechat_secret\", \"\") // 这里会自动解密\n\n\t// 验证必填配置项\n\tif agentIDStr == \"\" || corpid == \"\" || secret == \"\" {\n\t\tlogger.Log.Info(\"WeChat is enabled but configuration is incomplete (agentid/corpid/secret), WeChat app will not be initialized\")\n\t\treturn\n\t}\n\t// 解析 AgentID\n\tAgentId, err := strconv.ParseInt(agentIDStr, 10, 64)\n\tif err != nil {\n\t\tlogger.Log.Error(\"wechat_agentid parse error:\", err)\n\t\treturn\n\t}\n\n\t// 初始化企业微信客户端\n\tclient := workwx.New(corpid)\n\tWeApp = client.WithApp(secret, AgentId)\n\tWeApp.SpawnAccessTokenRefresher()\n\tlogger.Log.Info(\"WeChat inited successfully!\")\n}\n\n// DatabaseInit 数据初始化\nfunc DatabaseInit() {\n\t//数据初始化操作 - 使用 GORM\n\tvar v User\n\terr := DB.Where(\"username = ?\", \"admin\").First(&v).Error\n\t//检查权限\n\tif err == nil && v.Operation == \"\" {\n\t\terr := DB.Model(&User{}).Where(\"id = ?\", v.ID).Update(\"operation\", \"['add', 'edit', 'delete','update']\").Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"update admin operation successfully\")\n\t}\n\t//添加管理员账号\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\tlogger.Log.Info(\"the admin user does not exist, create a new admin account later!\")\n\n\t\t// 构建默认主题配置(使用结构化方式,避免手写 JSON 出错)\n\t\tdefaultThemeConfig := map[string]interface{}{\n\t\t\t\"theme\": map[string]string{\n\t\t\t\t\"color\": \"#1890ff\",\n\t\t\t\t\"mode\": \"dark\",\n\t\t\t\t\"success\": \"#52c41a\",\n\t\t\t\t\"warning\": \"#faad14\",\n\t\t\t\t\"error\": \"#f5222f\",\n\t\t\t},\n\t\t\t\"animate\": map[string]interface{}{\n\t\t\t\t\"disabled\": true,\n\t\t\t\t\"name\": \"lightSpeed\",\n\t\t\t\t\"direction\": \"left\",\n\t\t\t},\n\t\t}\n\n\t\t// 将配置转换为 JSON 字符串\n\t\tthemeJSON, err := json.Marshal(defaultThemeConfig)\n\t\tif err != nil {\n\t\t\tlogger.Log.Error(\"Failed to marshal default theme config:\", err)\n\t\t\t// 如果序列化失败,使用备用的硬编码 JSON\n\t\t\tthemeJSON = []byte(`{\"theme\":{\"color\":\"#1890ff\",\"mode\":\"dark\",\"success\":\"#52c41a\",\"warning\":\"#faad14\",\"error\":\"#f5222f\"},\"animate\":{\"disabled\":true,\"name\":\"lightSpeed\",\"direction\":\"left\"}}`)\n\t\t}\n\n\t\tvar user User\n\t\tuser.Username = \"admin\"\n\t\tuser.Password, _ = utils.PasswordHash(\"Zbxtable\")\n\t\tuser.Avatar = \"https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif\"\n\t\tuser.Role = \"admin\"\n\t\tuser.Operation = \"['add', 'edit', 'delete','update']\"\n\t\tuser.Theme = string(themeJSON)\n\t\tuser.Status = 0\n\t\terr = DB.Create(&user).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"create an administrator account successfully, the admin ID is:\", user.ID)\n\t}\n\t//初始化系统数据\n\tvar cnt []System\n\terr = DB.Find(&cnt).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn\n\t}\n\tif len(cnt) == 0 {\n\t\tnow := time.Now()\n\t\tsys := []System{\n\t\t\t{Name: \"Linux操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"Windows操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"网络设备\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"物理服务器\", Status: 0, InitedAt: &now},\n\t\t}\n\t\terr := DB.Create(&sys).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init system info error!\", err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"Init system data successfully!\")\n\t}\n\t//告警默认规则初始化 default rule\n\tvar rules []Rule\n\terr = DB.Where(\"m_type = ?\", \"2\").Find(&rules).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn\n\t}\n\tif len(rules) == 0 {\n\t\t// 使用 \"*\" 作为全局默认规则,匹配所有租户\n\t\tdefaultRule := []Rule{\n\t\t\t{\n\t\t\t\tName: \"全局默认规则\",\n\t\t\t\tZIDs: \"*\",\n\t\t\t\tMType: \"2\",\n\t\t\t\tChannel: \"wechat_robot\",\n\t\t\t\tUserIds: \"1\",\n\t\t\t\tSweek: \"0,1,2,3,4,5,6\",\n\t\t\t\tStime: \"00:00\",\n\t\t\t\tEtime: \"23.59\",\n\t\t\t\tStatus: \"0\"},\n\t\t}\n\t\terr := DB.Create(&defaultRule).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init default rule error!\", err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"Init default rule successfully!\")\n\t}\n\t// 初始化加密密钥(如果不存在)\n\tvar encryptionKeyConfig Config\n\terr = DB.Where(\"`key` = ?\", \"encryption_key\").First(&encryptionKeyConfig).Error\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t// 生成随机加密密钥\n\t\trandomKey := utils.GenerateRandomKey()\n\t\tencryptionKeyConfig = Config{\n\t\t\tName: \"加密密钥\",\n\t\t\tKey: \"encryption_key\",\n\t\t\tValue: randomKey,\n\t\t\tComment: \"用于加密存储敏感信息(如Zabbix密码和Token)的密钥,系统自动生成,不可修改\",\n\t\t}\n\t\tif insertErr := DB.Create(&encryptionKeyConfig).Error; insertErr != nil {\n\t\t\tlogger.Log.Error(\"Init encryption_key error:\", insertErr)\n\t\t} else {\n\t\t\tlogger.Log.Info(\"Init encryption_key successfully! Key length:\", len(randomKey))\n\t\t}\n\t}\n\n\tdefaultConfigs := []Config{\n\t\t// 系统外观配置\n\t\t{Name: \"系统名称\", Key: \"system_name\", Value: \"ZbxTable\", Comment: \"系统显示的名称\"},\n\t\t{Name: \"系统Logo\", Key: \"system_logo\", Value: \"data:image/png;base64,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\", Comment: \"系统Logo的base64编码数据\"},\n\t\t// 初始配置状态\n\t\t{Name: \"初始配置完成\", Key: \"initial_setup_completed\", Value: \"0\", Comment: \"标记系统是否完成初始配置:1 已完成,0 未完成\"},\n\t\t// Dashboard 相关\n\t\t{Name: \"数据面板\", Key: \"zbx_dash\", Value: \"0\", Comment: \"是否开启Zabbix看板:1 开启,0 关闭\"},\n\t\t{Name: \"面板配置\", Key: \"dash_id\", Value: \"1\", Comment: \"需要引入的Zabbix面板的ID,默认为1\"},\n\t\t{Name: \"主机分类同步\", Key: \"sync_inventory\", Value: \"1\", Comment: \"主机分类同步计划任务是否启用:1 启用,0 不启用\"},\n\t\t{Name: \"Webhook回调地址\", Key: \"webhook_url\", Value: \"\", Comment: \"webhook通知地址\"},\n\t\t// 邮件配置\n\t\t{Name: \"邮件发件人\", Key: \"email_from\", Value: \"\", Comment: \"告警邮件发件人邮箱地址\"},\n\t\t{Name: \"邮件昵称\", Key: \"email_nickname\", Value: \"ZbxTable\", Comment: \"告警邮件显示的发件人昵称\"},\n\t\t{Name: \"SMTP 密码/授权码\", Key: \"email_secret\", Value: \"\", Comment: \"SMTP 登录密码或授权码\"},\n\t\t{Name: \"SMTP 服务器\", Key: \"email_host\", Value: \"smtp.qq.com\", Comment: \"SMTP 服务器地址\"},\n\t\t{Name: \"SMTP 端口\", Key: \"email_port\", Value: \"465\", Comment: \"SMTP 端口号\"},\n\t\t{Name: \"SMTP 使用 SSL\", Key: \"email_isSSl\", Value: \"true\", Comment: \"是否启用 SSL:true/false\"},\n\t\t// 企业微信配置\n\t\t{Name: \"企业微信开关\", Key: \"wechat_enabled\", Value: \"0\", Comment: \"是否启用企业微信:1 启用,0 禁用\"},\n\t\t{Name: \"企业微信 AgentID\", Key: \"wechat_agentid\", Value: \"\", Comment: \"企业微信应用的 AgentID\"},\n\t\t{Name: \"企业微信 CorpID\", Key: \"wechat_corpid\", Value: \"\", Comment: \"企业微信企业ID\"},\n\t\t{Name: \"企业微信 Secret\", Key: \"wechat_secret\", Value: \"\", Comment: \"企业微信应用的 Secret\"},\n\t\t// AI 配置\n\t\t{Name: \"AI 类型\", Key: \"ai_type\", Value: \"ollama\", Comment: \"选择使用的 AI 服务类型:ollama 或 deepseek\"},\n\t\t// Ollama 配置\n\t\t{Name: \"Ollama Host\", Key: \"ollama_host\", Value: \"http://localhost:11434\", Comment: \"Ollama 服务地址,如 http://127.0.0.1:11434\"},\n\t\t{Name: \"Ollama Model\", Key: \"ollama_model\", Value: \"deepseek-r1:32b\", Comment: \"默认使用的大模型名称\"},\n\t\t// Deepseek 配置\n\t\t{Name: \"Deepseek API Key\", Key: \"deepseek_api_key\", Value: \"\", Comment: \"Deepseek API 密钥\"},\n\t\t{Name: \"Deepseek Model\", Key: \"deepseek_model\", Value: \"deepseek-chat\", Comment: \"Deepseek 模型名称,如 deepseek-chat\"},\n\t\t{Name: \"Deepseek Base URL\", Key: \"deepseek_base_url\", Value: \"https://api.deepseek.com\", Comment: \"Deepseek API 地址,默认为 https://api.deepseek.com\"},\n\t}\n\n\tfor _, cfgItem := range defaultConfigs {\n\t\tvar existing Config\n\t\terr = DB.Where(\"`key` = ?\", cfgItem.Key).First(&existing).Error\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\tif insertErr := DB.Create(&cfgItem).Error; insertErr != nil {\n\t\t\t\tlogger.Log.Infof(\"Init config key %s error: %v\", cfgItem.Key, insertErr)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tlogger.Log.Infof(\"Init config key %s successfully!\", cfgItem.Key)\n\t\t}\n\t}\n\t//默认菜单初始化\n\tInitMenuData()\n\t//检查并添加缺失的菜单项(用于版本升级)\n\tCheckAndAddMenus()\n\n}\n\nfunc GetConfKey(v string) string {\n\t// 优先从 .env 中读取(便于开发环境配置敏感信息且不提交到 git)\n\tenvConfigOnce.Do(func() {\n\t\tfile, err := os.Open(\".env\")\n\t\tif err != nil {\n\t\t\t// 没有 .env 文件时直接跳过,后面回退到 app.conf\n\t\t\tenvFileExists = false\n\t\t\treturn\n\t\t}\n\t\tdefer file.Close()\n\n\t\tenvFileExists = true\n\t\tenvConfig = make(map[string]string)\n\n\t\tscanner := bufio.NewScanner(file)\n\t\tfor scanner.Scan() {\n\t\t\tline := strings.TrimSpace(scanner.Text())\n\t\t\tif line == \"\" || strings.HasPrefix(line, \"#\") || strings.HasPrefix(line, \";\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tparts := strings.SplitN(line, \"=\", 2)\n\t\t\tif len(parts) != 2 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tkey := strings.TrimSpace(parts[0])\n\t\t\tval := strings.TrimSpace(parts[1])\n\t\t\t// 去掉可能的引号\n\t\t\tval = strings.Trim(val, `\"'`)\n\t\t\tif key != \"\" {\n\t\t\t\tenvConfig[key] = val // 即使 val 为空也认为是有效配置\n\t\t\t}\n\t\t}\n\t})\n\n\t// 如果存在 .env 文件,则所有配置完全由 .env 决定:\n\t// - key 存在:返回对应值(可以是空字符串)\n\t// - key 不存在:返回空字符串\n\tif envFileExists {\n\t\tif envConfig == nil {\n\t\t\treturn \"\"\n\t\t}\n\t\tif val, ok := envConfig[v]; ok {\n\t\t\treturn val\n\t\t}\n\t\treturn \"\"\n\t}\n\t// 其次从 config/app.conf 读取\n\tcfg, err := ini.Load(\"./config/app.conf\")\n\tif err != nil {\n\t\tlogger.Log.Error(err)\n\t\treturn \"\"\n\t}\n\tp, err := cfg.Section(\"\").GetKey(v)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn p.String()\n}\n\n// IsPasswordConfigured 检查是否配置了密码(用于图形查看)\n// 优先检查数据库中是否有激活的 Zabbix 实例配置了用户名和密码\n// 注意:查看图形需要用户名和密码,Token 方式无法用于 Web 登录\nfunc IsPasswordConfigured() bool {\n\t// 1. 优先检查数据库中是否有激活的 Zabbix 实例\n\tvar activeInstance ZabbixInstance\n\terr := DB.Where(\"is_active = ?\", true).First(&activeInstance).Error\n\tif err == nil {\n\t\t// 找到激活的实例,检查是否配置了用户名和密码(查看图形必须用密码,Token 无法用于 Web 登录)\n\t\tif strings.TrimSpace(activeInstance.User) != \"\" && strings.TrimSpace(activeInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 2. 如果数据库中没有激活实例,检查是否有任何可用的实例配置了密码\n\tvar anyInstance ZabbixInstance\n\terr = DB.Where(\"enabled = ?\", true).First(&anyInstance).Error\n\tif err == nil {\n\t\t// 找到可用的实例,检查是否配置了用户名和密码\n\t\tif strings.TrimSpace(anyInstance.User) != \"\" && strings.TrimSpace(anyInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 3. 回退到配置文件检查(兼容旧版本)\n\tpass := GetConfKey(\"zabbix_pass\")\n\tuser := GetConfKey(\"zabbix_user\")\n\treturn pass != \"\" && user != \"\"\n}\n\n// GetEncryptionKey 获取加密密钥\n// 优先从数据库读取,如果数据库中没有则从环境变量读取,最后使用默认密钥\nfunc GetEncryptionKey() string {\n\t// 1. 优先从数据库读取\n\tkey := GetConfigValueByKey(\"encryption_key\", \"\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 2. 从环境变量或配置文件读取\n\tkey = GetConfKey(\"encryption_key\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 3. 使用默认密钥(不应该到这里,因为 DatabaseInit 会生成)\n\tlogger.Log.Warn(\"使用默认加密密钥,建议在数据库中配置 encryption_key\")\n\treturn \"zbxtable-default-encryption-key-2024\"\n}\n"}, "38bf6c2c47dd9ec07f3431cc698f947cf674897e": {"t": 1785754923.7817984, "n": 16328, "h": "fd8f4e2153228d3a", "c": "package model\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\t\"zbxtable/pkg/logger\"\n\t\"zbxtable/pkg/utils\"\n\n\tzabbix \"github.com/canghai908/zabbix-go\"\n\t_ \"github.com/go-sql-driver/mysql\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t_ \"github.com/lib/pq\"\n\tworkwx \"github.com/xen0n/go-workwx\"\n\tini \"gopkg.in/ini.v1\"\n\t\"gorm.io/gorm\"\n)\n\nvar (\n\tAPI = &zabbix.API{}\n\tjson = jsoniter.Config{\n\t\tEscapeHTML: false,\n\t\tSortMapKeys: true,\n\t\tValidateJsonRawMessage: true,\n\t}.Froze()\n\tZBX_VER string\n\tZBX_V bool\n\tVersion string\n\tGitHash string\n\tBuildTime string\n\tWeApp = &workwx.WorkwxApp{}\n\n\tenvConfig map[string]string\n\tenvConfigOnce sync.Once\n\tenvFileExists bool\n)\n\nconst (\n\t//ms-agent 常量定义\n\tMSName = \"ms-agent\"\n\tMSUser = \"ms-agent\"\n\tMSGroup = \"MS-Agent\"\n\tMSMedia = \"MS-Agent\"\n\tMSAction = \"MS-Agent\"\n\t//webhook 常量定义\n\tWebhookName = \"ZbxTable\"\n\tWebhookAction = \"ZbxTable Webhook\"\n\tWebhookGroup = \"ZbxTable Webhook\"\n\tWebhookUser = \"zbxtable-webhook\"\n\t//down\n\tDownloadPath = \"./download/\"\n\tTplPath = \"./assets/templates/\"\n)\n\n// TableName 表名前缀\nfunc TableName(str string) string {\n\treturn fmt.Sprintf(\"%s%s\", \"zbxtable_\", str)\n}\n\n// 接收数据库信息初始化\nfunc ModelInit(dbtype, dbhost, dbuser, dbpass, dbname, dbport string) {\n\t// 直接使用 GORM\n\trunmode := GetConfKey(\"runmode\")\n\terr := InitGormDB(dbtype, dbhost, dbuser, dbpass, dbname, dbport, runmode)\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to connect database: \", err)\n\t\tos.Exit(1)\n\t}\n\tlogger.Log.Info(\"Database connected!\")\n\t// 自动迁移表\n\terr = AutoMigrate()\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to auto migrate: \", err)\n\t\tos.Exit(1)\n\t}\n\t// 基础数据初始化\n\tDatabaseInit()\n\t// 使用go-cache替代Redis(不再需要Redis连接)\n\tInitCache()\n}\n\nfunc InitWechat() {\n\t// 首先检查企业微信开关是否启用\n\twechatEnabled := GetConfigValueByKey(\"wechat_enabled\", \"0\")\n\tif wechatEnabled != \"1\" {\n\t\tlogger.Log.Info(\"WeChat is disabled in system config, skipping WeChat initialization\")\n\t\treturn\n\t}\n\t// 开关已启用,从数据库读取企业微信配置\n\tagentIDStr := GetConfigValueByKey(\"wechat_agentid\", \"\")\n\tcorpid := GetConfigValueByKey(\"wechat_corpid\", \"\")\n\tsecret := GetConfigValueByKey(\"wechat_secret\", \"\")\n\n\t// 验证必填配置项\n\tif agentIDStr == \"\" || corpid == \"\" || secret == \"\" {\n\t\tlogger.Log.Info(\"WeChat is enabled but configuration is incomplete (agentid/corpid/secret), WeChat app will not be initialized\")\n\t\treturn\n\t}\n\t// 解析 AgentID\n\tAgentId, err := strconv.ParseInt(agentIDStr, 10, 64)\n\tif err != nil {\n\t\tlogger.Log.Error(\"wechat_agentid parse error:\", err)\n\t\treturn\n\t}\n\n\t// 初始化企业微信客户端\n\tclient := workwx.New(corpid)\n\tWeApp = client.WithApp(secret, AgentId)\n\tWeApp.SpawnAccessTokenRefresher()\n\tlogger.Log.Info(\"WeChat inited successfully!\")\n}\n\n// DatabaseInit 数据初始化\nfunc DatabaseInit() {\n\t//数据初始化操作 - 使用 GORM\n\tvar v User\n\terr := DB.Where(\"username = ?\", \"admin\").First(&v).Error\n\t//检查权限\n\tif err == nil && v.Operation == \"\" {\n\t\terr := DB.Model(&User{}).Where(\"id = ?\", v.ID).Update(\"operation\", \"['add', 'edit', 'delete','update']\").Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"update admin operation successfully\")\n\t}\n\t//添加管理员账号\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\tlogger.Log.Info(\"the admin user does not exist, create a new admin account later!\")\n\n\t\t// 构建默认主题配置(使用结构化方式,避免手写 JSON 出错)\n\t\tdefaultThemeConfig := map[string]interface{}{\n\t\t\t\"theme\": map[string]string{\n\t\t\t\t\"color\": \"#1890ff\",\n\t\t\t\t\"mode\": \"dark\",\n\t\t\t\t\"success\": \"#52c41a\",\n\t\t\t\t\"warning\": \"#faad14\",\n\t\t\t\t\"error\": \"#f5222f\",\n\t\t\t},\n\t\t\t\"animate\": map[string]interface{}{\n\t\t\t\t\"disabled\": true,\n\t\t\t\t\"name\": \"lightSpeed\",\n\t\t\t\t\"direction\": \"left\",\n\t\t\t},\n\t\t}\n\n\t\t// 将配置转换为 JSON 字符串\n\t\tthemeJSON, err := json.Marshal(defaultThemeConfig)\n\t\tif err != nil {\n\t\t\tlogger.Log.Error(\"Failed to marshal default theme config:\", err)\n\t\t\t// 如果序列化失败,使用备用的硬编码 JSON\n\t\t\tthemeJSON = []byte(`{\"theme\":{\"color\":\"#1890ff\",\"mode\":\"dark\",\"success\":\"#52c41a\",\"warning\":\"#faad14\",\"error\":\"#f5222f\"},\"animate\":{\"disabled\":true,\"name\":\"lightSpeed\",\"direction\":\"left\"}}`)\n\t\t}\n\n\t\tvar user User\n\t\tuser.Username = \"admin\"\n\t\tuser.Password, _ = utils.PasswordHash(\"Zbxtable\")\n\t\tuser.Avatar = \"https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif\"\n\t\tuser.Role = \"admin\"\n\t\tuser.Operation = \"['add', 'edit', 'delete','update']\"\n\t\tuser.Theme = string(themeJSON)\n\t\tuser.Status = 0\n\t\terr = DB.Create(&user).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"create an administrator account successfully, the admin ID is:\", user.ID)\n\t}\n\t//初始化系统数据\n\tvar cnt []System\n\terr = DB.Find(&cnt).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn\n\t}\n\tif len(cnt) == 0 {\n\t\tnow := time.Now()\n\t\tsys := []System{\n\t\t\t{Name: \"Linux操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"Windows操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"网络设备\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"物理服务器\", Status: 0, InitedAt: &now},\n\t\t}\n\t\terr := DB.Create(&sys).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init system info error!\", err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"Init system data successfully!\")\n\t}\n\t//告警默认规则初始化 default rule\n\tvar rules []Rule\n\terr = DB.Where(\"m_type = ?\", \"2\").Find(&rules).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn\n\t}\n\tif len(rules) == 0 {\n\t\t// 使用 \"*\" 作为全局默认规则,匹配所有租户\n\t\tdefaultRule := []Rule{\n\t\t\t{\n\t\t\t\tName: \"全局默认规则\",\n\t\t\t\tZIDs: \"*\",\n\t\t\t\tMType: \"2\",\n\t\t\t\tChannel: \"wechat_robot\",\n\t\t\t\tUserIds: \"1\",\n\t\t\t\tSweek: \"0,1,2,3,4,5,6\",\n\t\t\t\tStime: \"00:00\",\n\t\t\t\tEtime: \"23.59\",\n\t\t\t\tStatus: \"0\"},\n\t\t}\n\t\terr := DB.Create(&defaultRule).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init default rule error!\", err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"Init default rule successfully!\")\n\t}\n\t// 初始化加密密钥(如果不存在)\n\tvar encryptionKeyConfig Config\n\terr = DB.Where(\"`key` = ?\", \"encryption_key\").First(&encryptionKeyConfig).Error\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t// 生成随机加密密钥\n\t\trandomKey := utils.GenerateRandomKey()\n\t\tencryptionKeyConfig = Config{\n\t\t\tName: \"加密密钥\",\n\t\t\tKey: \"encryption_key\",\n\t\t\tValue: randomKey,\n\t\t\tComment: \"用于加密存储敏感信息(如Zabbix密码和Token)的密钥,系统自动生成,不可修改\",\n\t\t}\n\t\tif insertErr := DB.Create(&encryptionKeyConfig).Error; insertErr != nil {\n\t\t\tlogger.Log.Error(\"Init encryption_key error:\", insertErr)\n\t\t} else {\n\t\t\tlogger.Log.Info(\"Init encryption_key successfully! Key length:\", len(randomKey))\n\t\t}\n\t}\n\n\tdefaultConfigs := []Config{\n\t\t// 系统外观配置\n\t\t{Name: \"系统名称\", Key: \"system_name\", Value: \"ZbxTable\", Comment: \"系统显示的名称\"},\n\t\t{Name: \"系统Logo\", Key: \"system_logo\", Value: \"data:image/png;base64,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\", Comment: \"系统Logo的base64编码数据\"},\n\t\t// 初始配置状态\n\t\t{Name: \"初始配置完成\", Key: \"initial_setup_completed\", Value: \"0\", Comment: \"标记系统是否完成初始配置:1 已完成,0 未完成\"},\n\t\t// Dashboard 相关\n\t\t{Name: \"数据面板\", Key: \"zbx_dash\", Value: \"0\", Comment: \"是否开启Zabbix看板:1 开启,0 关闭\"},\n\t\t{Name: \"面板配置\", Key: \"dash_id\", Value: \"1\", Comment: \"需要引入的Zabbix面板的ID,默认为1\"},\n\t\t{Name: \"主机分类同步\", Key: \"sync_inventory\", Value: \"1\", Comment: \"主机分类同步计划任务是否启用:1 启用,0 不启用\"},\n\t\t{Name: \"Webhook回调地址\", Key: \"webhook_url\", Value: \"\", Comment: \"webhook通知地址\"},\n\t\t// 邮件配置\n\t\t{Name: \"邮件发件人\", Key: \"email_from\", Value: \"\", Comment: \"告警邮件发件人邮箱地址\"},\n\t\t{Name: \"邮件昵称\", Key: \"email_nickname\", Value: \"ZbxTable\", Comment: \"告警邮件显示的发件人昵称\"},\n\t\t{Name: \"SMTP 密码/授权码\", Key: \"email_secret\", Value: \"\", Comment: \"SMTP 登录密码或授权码\"},\n\t\t{Name: \"SMTP 服务器\", Key: \"email_host\", Value: \"smtp.qq.com\", Comment: \"SMTP 服务器地址\"},\n\t\t{Name: \"SMTP 端口\", Key: \"email_port\", Value: \"465\", Comment: \"SMTP 端口号\"},\n\t\t{Name: \"SMTP 使用 SSL\", Key: \"email_isSSl\", Value: \"true\", Comment: \"是否启用 SSL:true/false\"},\n\t\t// 企业微信配置\n\t\t{Name: \"企业微信开关\", Key: \"wechat_enabled\", Value: \"0\", Comment: \"是否启用企业微信:1 启用,0 禁用\"},\n\t\t{Name: \"企业微信 AgentID\", Key: \"wechat_agentid\", Value: \"\", Comment: \"企业微信应用的 AgentID\"},\n\t\t{Name: \"企业微信 CorpID\", Key: \"wechat_corpid\", Value: \"\", Comment: \"企业微信企业ID\"},\n\t\t{Name: \"企业微信 Secret\", Key: \"wechat_secret\", Value: \"\", Comment: \"企业微信应用的 Secret\"},\n\t\t// AI 配置\n\t\t{Name: \"AI 类型\", Key: \"ai_type\", Value: \"ollama\", Comment: \"选择使用的 AI 服务类型:ollama 或 deepseek\"},\n\t\t// Ollama 配置\n\t\t{Name: \"Ollama Host\", Key: \"ollama_host\", Value: \"http://localhost:11434\", Comment: \"Ollama 服务地址,如 http://127.0.0.1:11434\"},\n\t\t{Name: \"Ollama Model\", Key: \"ollama_model\", Value: \"deepseek-r1:32b\", Comment: \"默认使用的大模型名称\"},\n\t\t// Deepseek 配置\n\t\t{Name: \"Deepseek API Key\", Key: \"deepseek_api_key\", Value: \"\", Comment: \"Deepseek API 密钥\"},\n\t\t{Name: \"Deepseek Model\", Key: \"deepseek_model\", Value: \"deepseek-chat\", Comment: \"Deepseek 模型名称,如 deepseek-chat\"},\n\t\t{Name: \"Deepseek Base URL\", Key: \"deepseek_base_url\", Value: \"https://api.deepseek.com\", Comment: \"Deepseek API 地址,默认为 https://api.deepseek.com\"},\n\t}\n\n\tfor _, cfgItem := range defaultConfigs {\n\t\tvar existing Config\n\t\terr = DB.Where(\"`key` = ?\", cfgItem.Key).First(&existing).Error\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\tif insertErr := DB.Create(&cfgItem).Error; insertErr != nil {\n\t\t\t\tlogger.Log.Infof(\"Init config key %s error: %v\", cfgItem.Key, insertErr)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tlogger.Log.Infof(\"Init config key %s successfully!\", cfgItem.Key)\n\t\t}\n\t}\n\t//默认菜单初始化\n\tInitMenuData()\n\t//检查并添加缺失的菜单项(用于版本升级)\n\tCheckAndAddMenus()\n\n}\n\nfunc GetConfKey(v string) string {\n\t// 优先从 .env 中读取(便于开发环境配置敏感信息且不提交到 git)\n\tenvConfigOnce.Do(func() {\n\t\tfile, err := os.Open(\".env\")\n\t\tif err != nil {\n\t\t\t// 没有 .env 文件时直接跳过,后面回退到 app.conf\n\t\t\tenvFileExists = false\n\t\t\treturn\n\t\t}\n\t\tdefer file.Close()\n\n\t\tenvFileExists = true\n\t\tenvConfig = make(map[string]string)\n\n\t\tscanner := bufio.NewScanner(file)\n\t\tfor scanner.Scan() {\n\t\t\tline := strings.TrimSpace(scanner.Text())\n\t\t\tif line == \"\" || strings.HasPrefix(line, \"#\") || strings.HasPrefix(line, \";\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tparts := strings.SplitN(line, \"=\", 2)\n\t\t\tif len(parts) != 2 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tkey := strings.TrimSpace(parts[0])\n\t\t\tval := strings.TrimSpace(parts[1])\n\t\t\t// 去掉可能的引号\n\t\t\tval = strings.Trim(val, `\"'`)\n\t\t\tif key != \"\" {\n\t\t\t\tenvConfig[key] = val // 即使 val 为空也认为是有效配置\n\t\t\t}\n\t\t}\n\t})\n\n\t// 如果存在 .env 文件,则所有配置完全由 .env 决定:\n\t// - key 存在:返回对应值(可以是空字符串)\n\t// - key 不存在:返回空字符串\n\tif envFileExists {\n\t\tif envConfig == nil {\n\t\t\treturn \"\"\n\t\t}\n\t\tif val, ok := envConfig[v]; ok {\n\t\t\treturn val\n\t\t}\n\t\treturn \"\"\n\t}\n\t// 其次从 config/app.conf 读取\n\tcfg, err := ini.Load(\"./config/app.conf\")\n\tif err != nil {\n\t\tlogger.Log.Error(err)\n\t\treturn \"\"\n\t}\n\tp, err := cfg.Section(\"\").GetKey(v)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn p.String()\n}\n\n// IsPasswordConfigured 检查是否配置了密码(用于图形查看)\n// 优先检查数据库中是否有激活的 Zabbix 实例配置了用户名和密码\n// 注意:查看图形需要用户名和密码,Token 方式无法用于 Web 登录\nfunc IsPasswordConfigured() bool {\n\t// 1. 优先检查数据库中是否有激活的 Zabbix 实例\n\tvar activeInstance ZabbixInstance\n\terr := DB.Where(\"is_active = ?\", true).First(&activeInstance).Error\n\tif err == nil {\n\t\t// 找到激活的实例,检查是否配置了用户名和密码(查看图形必须用密码,Token 无法用于 Web 登录)\n\t\tif strings.TrimSpace(activeInstance.User) != \"\" && strings.TrimSpace(activeInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 2. 如果数据库中没有激活实例,检查是否有任何可用的实例配置了密码\n\tvar anyInstance ZabbixInstance\n\terr = DB.Where(\"enabled = ?\", true).First(&anyInstance).Error\n\tif err == nil {\n\t\t// 找到可用的实例,检查是否配置了用户名和密码\n\t\tif strings.TrimSpace(anyInstance.User) != \"\" && strings.TrimSpace(anyInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 3. 回退到配置文件检查(兼容旧版本)\n\tpass := GetConfKey(\"zabbix_pass\")\n\tuser := GetConfKey(\"zabbix_user\")\n\treturn pass != \"\" && user != \"\"\n}\n\n// GetEncryptionKey 获取加密密钥\n// 优先从数据库读取,如果数据库中没有则从环境变量读取,最后使用默认密钥\nfunc GetEncryptionKey() string {\n\t// 1. 优先从数据库读取\n\tkey := GetConfigValueByKey(\"encryption_key\", \"\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 2. 从环境变量或配置文件读取\n\tkey = GetConfKey(\"encryption_key\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 3. 使用默认密钥(不应该到这里,因为 DatabaseInit 会生成)\n\tlogger.Log.Warn(\"使用默认加密密钥,建议在数据库中配置 encryption_key\")\n\treturn \"zbxtable-default-encryption-key-2024\"\n}\n"}, "4471dd019ef1c3da2ce87879b8c842a858fdbae5": {"t": 1785754925.3368616, "n": 11600, "h": "6a52b1fa1830ef3b", "c": "package model\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\t\"zbxtable/pkg/logger\"\n\t\"zbxtable/pkg/utils\"\n\n\tzabbix \"github.com/canghai908/zabbix-go\"\n\t_ \"github.com/go-sql-driver/mysql\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t_ \"github.com/lib/pq\"\n\tworkwx \"github.com/xen0n/go-workwx\"\n\tini \"gopkg.in/ini.v1\"\n\t\"gorm.io/gorm\"\n)\n\nvar (\n\tAPI = &zabbix.API{}\n\tjson = jsoniter.Config{\n\t\tEscapeHTML: false,\n\t\tSortMapKeys: true,\n\t\tValidateJsonRawMessage: true,\n\t}.Froze()\n\tZBX_VER string\n\tZBX_V bool\n\tVersion string\n\tGitHash string\n\tBuildTime string\n\tWeApp = &workwx.WorkwxApp{}\n\n\tenvConfig map[string]string\n\tenvConfigOnce sync.Once\n\tenvFileExists bool\n)\n\nconst (\n\t//ms-agent 常量定义\n\tMSName = \"ms-agent\"\n\tMSUser = \"ms-agent\"\n\tMSGroup = \"MS-Agent\"\n\tMSMedia = \"MS-Agent\"\n\tMSAction = \"MS-Agent\"\n\t//webhook 常量定义\n\tWebhookName = \"ZbxTable\"\n\tWebhookAction = \"ZbxTable Webhook\"\n\tWebhookGroup = \"ZbxTable Webhook\"\n\tWebhookUser = \"zbxtable-webhook\"\n\t//down\n\tDownloadPath = \"./download/\"\n\tTplPath = \"./assets/templates/\"\n)\n\n// TableName 表名前缀\nfunc TableName(str string) string {\n\treturn fmt.Sprintf(\"%s%s\", \"zbxtable_\", str)\n}\n\n// 接收数据库信息初始化\nfunc ModelInit(dbtype, dbhost, dbuser, dbpass, dbname, dbport string) {\n\t// 直接使用 GORM\n\trunmode := GetConfKey(\"runmode\")\n\terr := InitGormDB(dbtype, dbhost, dbuser, dbpass, dbname, dbport, runmode)\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to connect database: \", err)\n\t\tos.Exit(1)\n\t}\n\tlogger.Log.Info(\"Database connected!\")\n\t// 自动迁移表\n\terr = AutoMigrate()\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to auto migrate: \", err)\n\t\tos.Exit(1)\n\t}\n\t// 基础数据初始化\n\tDatabaseInit()\n\t// 使用go-cache替代Redis(不再需要Redis连接)\n\tInitCache()\n}\n\nfunc InitWechat() {\n\t// 首先检查企业微信开关是否启用\n\twechatEnabled := GetConfigValueByKey(\"wechat_enabled\", \"0\")\n\tif wechatEnabled != \"1\" {\n\t\tlogger.Log.Info(\"WeChat is disabled in system config, skipping WeChat initialization\")\n\t\treturn\n\t}\n\t// 开关已启用,从数据库读取企业微信配置\n\tagentIDStr := GetConfigValueByKey(\"wechat_agentid\", \"\")\n\tcorpid := GetConfigValueByKey(\"wechat_corpid\", \"\")\n\tsecret := GetConfigValueByKey(\"wechat_secret\", \"\")\n\n\t// 验证必填配置项\n\tif agentIDStr == \"\" || corpid == \"\" || secret == \"\" {\n\t\tlogger.Log.Info(\"WeChat is enabled but configuration is incomplete (agentid/corpid/secret), WeChat app will not be initialized\")\n\t\treturn\n\t}\n\t// 解析 AgentID\n\tAgentId, err := strconv.ParseInt(agentIDStr, 10, 64)\n\tif err != nil {\n\t\tlogger.Log.Error(\"wechat_agentid parse error:\", err)\n\t\treturn\n\t}\n\n\t// 初始化企业微信客户端\n\tclient := workwx.New(corpid)\n\tWeApp = client.WithApp(secret, AgentId)\n\tWeApp.SpawnAccessTokenRefresher()\n\tlogger.Log.Info(\"WeChat inited successfully!\")\n}\n\n// DatabaseInit 数据初始化\nfunc DatabaseInit() {\n\t//数据初始化操作 - 使用 GORM\n\tvar v User\n\terr := DB.Where(\"username = ?\", \"admin\").First(&v).Error\n\t//检查权限\n\tif err == nil && v.Operation == \"\" {\n\t\terr := DB.Model(&User{}).Where(\"id = ?\", v.ID).Update(\"operation\", \"['add', 'edit', 'delete','update']\").Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"update admin operation successfully\")\n\t}\n\t//添加管理员账号\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\tlogger.Log.Info(\"the admin user does not exist, create a new admin account later!\")\n\n\t\t// 构建默认主题配置(使用结构化方式,避免手写 JSON 出错)\n\t\tdefaultThemeConfig := map[string]interface{}{\n\t\t\t\"theme\": map[string]string{\n\t\t\t\t\"color\": \"#1890ff\",\n\t\t\t\t\"mode\": \"dark\",\n\t\t\t\t\"success\": \"#52c41a\",\n\t\t\t\t\"warning\": \"#faad14\",\n\t\t\t\t\"error\": \"#f5222f\",\n\t\t\t},\n\t\t\t\"animate\": map[string]interface{}{\n\t\t\t\t\"disabled\": true,\n\t\t\t\t\"name\": \"lightSpeed\",\n\t\t\t\t\"direction\": \"left\",\n\t\t\t},\n\t\t}\n\n\t\t// 将配置转换为 JSON 字符串\n\t\tthemeJSON, err := json.Marshal(defaultThemeConfig)\n\t\tif err != nil {\n\t\t\tlogger.Log.Error(\"Failed to marshal default theme config:\", err)\n\t\t\t// 如果序列化失败,使用备用的硬编码 JSON\n\t\t\tthemeJSON = []byte(`{\"theme\":{\"color\":\"#1890ff\",\"mode\":\"dark\",\"success\":\"#52c41a\",\"warning\":\"#faad14\",\"error\":\"#f5222f\"},\"animate\":{\"disabled\":true,\"name\":\"lightSpeed\",\"direction\":\"left\"}}`)\n\t\t}\n\n\t\tvar user User\n\t\tuser.Username = \"admin\"\n\t\tuser.Password, _ = utils.PasswordHash(\"Zbxtable\")\n\t\tuser.Avatar = \"https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif\"\n\t\tuser.Role = \"admin\"\n\t\tuser.Operation = \"['add', 'edit', 'delete','update']\"\n\t\tuser.Theme = string(themeJSON)\n\t\tuser.Status = 0\n\t\terr = DB.Create(&user).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"create an administrator account successfully, the admin ID is:\", user.ID)\n\t}\n\t//初始化系统数据\n\tvar cnt []System\n\terr = DB.Find(&cnt).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn\n\t}\n\tif len(cnt) == 0 {\n\t\tnow := time.Now()\n\t\tsys := []System{\n\t\t\t{Name: \"Linux操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"Windows操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"网络设备\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"物理服务器\", Status: 0, InitedAt: &now},\n\t\t}\n\t\terr := DB.Create(&sys).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init system info error!\", err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"Init system data successfully!\")\n\t}\n\t//告警默认规则初始化 default rule\n\tvar rules []Rule\n\terr = DB.Where(\"m_type = ?\", \"2\").Find(&rules).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn\n\t}\n\tif len(rules) == 0 {\n\t\t// 使用 \"*\" 作为全局默认规则,匹配所有租户\n\t\tdefaultRule := []Rule{\n\t\t\t{\n\t\t\t\tName: \"全局默认规则\",\n\t\t\t\tZIDs: \"*\",\n\t\t\t\tMType: \"2\",\n\t\t\t\tChannel: \"wechat_robot\",\n\t\t\t\tUserIds: \"1\",\n\t\t\t\tSweek: \"0,1,2,3,4,5,6\",\n\t\t\t\tStime: \"00:00\",\n\t\t\t\tEtime: \"23.59\",\n\t\t\t\tStatus: \"0\"},\n\t\t}\n\t\terr := DB.Create(&defaultRule).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init default rule error!\", err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"Init default rule successfully!\")\n\t}\n\t// 初始化加密密钥(如果不存在)\n\tvar encryptionKeyConfig Config\n\terr = DB.Where(\"`key` = ?\", \"encryption_key\").First(&encryptionKeyConfig).Error\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t// 生成随机加密密钥\n\t\trandomKey := utils.GenerateRandomKey()\n\t\tencryptionKeyConfig = Config{\n\t\t\tName: \"加密密钥\",\n\t\t\tKey: \"encryption_key\",\n\t\t\tValue: randomKey,\n\t\t\tComment: \"用于加密存储敏感信息(如Zabbix密码和Token)的密钥,系统自动生成,不可修改\",\n\t\t}\n\t\tif insertErr := DB.Create(&encryptionKeyConfig).Error; insertErr != nil {\n\t\t\tlogger.Log.Error(\"Init encryption_key error:\", insertErr)\n\t\t} else {\n\t\t\tlogger.Log.Info(\"Init encryption_key successfully! Key length:\", len(randomKey))\n\t\t}\n\t}\n\n\tdefaultConfigs := []Config{\n\t\t// 系统外观配置\n\t\t{Name: \"系统名称\", Key: \"system_name\", Value: \"ZbxTable\", Comment: \"系统显示的名称\"},\n\t\t{Name: \"系统Logo\", Key: \"system_logo\", Value: \"data:image/png;base64,\", Comment: \"系统Logo的base64编码数据\"},\n\t\t// 初始配置状态\n\t\t{Name: \"初始配置完成\", Key: \"initial_setup_completed\", Value: \"0\", Comment: \"标记系统是否完成初始配置:1 已完成,0 未完成\"},\n\t\t// Dashboard 相关\n\t\t{Name: \"数据面板\", Key: \"zbx_dash\", Value: \"0\", Comment: \"是否开启Zabbix看板:1 开启,0 关闭\"},\n\t\t{Name: \"面板配置\", Key: \"dash_id\", Value: \"1\", Comment: \"需要引入的Zabbix面板的ID,默认为1\"},\n\t\t{Name: \"主机分类同步\", Key: \"sync_inventory\", Value: \"1\", Comment: \"主机分类同步计划任务是否启用:1 启用,0 不启用\"},\n\t\t{Name: \"Webhook回调地址\", Key: \"webhook_url\", Value: \"\", Comment: \"webhook通知地址\"},\n\t\t// 邮件配置\n\t\t{Name: \"邮件发件人\", Key: \"email_from\", Value: \"\", Comment: \"告警邮件发件人邮箱地址\"},\n\t\t{Name: \"邮件昵称\", Key: \"email_nickname\", Value: \"ZbxTable\", Comment: \"告警邮件显示的发件人昵称\"},\n\t\t{Name: \"SMTP 密码/授权码\", Key: \"email_secret\", Value: \"\", Comment: \"SMTP 登录密码或授权码\"},\n\t\t{Name: \"SMTP 服务器\", Key: \"email_host\", Value: \"smtp.qq.com\", Comment: \"SMTP 服务器地址\"},\n\t\t{Name: \"SMTP 端口\", Key: \"email_port\", Value: \"465\", Comment: \"SMTP 端口号\"},\n\t\t{Name: \"SMTP 使用 SSL\", Key: \"email_isSSl\", Value: \"true\", Comment: \"是否启用 SSL:true/false\"},\n\t\t// 企业微信配置\n\t\t{Name: \"企业微信开关\", Key: \"wechat_enabled\", Value: \"0\", Comment: \"是否启用企业微信:1 启用,0 禁用\"},\n\t\t{Name: \"企业微信 AgentID\", Key: \"wechat_agentid\", Value: \"\", Comment: \"企业微信应用的 AgentID\"},\n\t\t{Name: \"企业微信 CorpID\", Key: \"wechat_corpid\", Value: \"\", Comment: \"企业微信企业ID\"},\n\t\t{Name: \"企业微信 Secret\", Key: \"wechat_secret\", Value: \"\", Comment: \"企业微信应用的 Secret\"},\n\t\t// AI 配置\n\t\t{Name: \"AI 类型\", Key: \"ai_type\", Value: \"ollama\", Comment: \"选择使用的 AI 服务类型:ollama 或 deepseek\"},\n\t\t// Ollama 配置\n\t\t{Name: \"Ollama Host\", Key: \"ollama_host\", Value: \"http://localhost:11434\", Comment: \"Ollama 服务地址,如 http://127.0.0.1:11434\"},\n\t\t{Name: \"Ollama Model\", Key: \"ollama_model\", Value: \"deepseek-r1:32b\", Comment: \"默认使用的大模型名称\"},\n\t\t// Deepseek 配置\n\t\t{Name: \"Deepseek API Key\", Key: \"deepseek_api_key\", Value: \"\", Comment: \"Deepseek API 密钥\"},\n\t\t{Name: \"Deepseek Model\", Key: \"deepseek_model\", Value: \"deepseek-chat\", Comment: \"Deepseek 模型名称,如 deepseek-chat\"},\n\t\t{Name: \"Deepseek Base URL\", Key: \"deepseek_base_url\", Value: \"https://api.deepseek.com\", Comment: \"Deepseek API 地址,默认为 https://api.deepseek.com\"},\n\t}\n\n\tfor _, cfgItem := range defaultConfigs {\n\t\tvar existing Config\n\t\terr = DB.Where(\"`key` = ?\", cfgItem.Key).First(&existing).Error\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\tif insertErr := DB.Create(&cfgItem).Error; insertErr != nil {\n\t\t\t\tlogger.Log.Infof(\"Init config key %s error: %v\", cfgItem.Key, insertErr)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tlogger.Log.Infof(\"Init config key %s successfully!\", cfgItem.Key)\n\t\t}\n\t}\n\t//默认菜单初始化\n\tInitMenuData()\n\t//检查并添加缺失的菜单项(用于版本升级)\n\tCheckAndAddMenus()\n\n}\n\nfunc GetConfKey(v string) string {\n\t// 优先从 .env 中读取(便于开发环境配置敏感信息且不提交到 git)\n\tenvConfigOnce.Do(func() {\n\t\tfile, err := os.Open(\".env\")\n\t\tif err != nil {\n\t\t\t// 没有 .env 文件时直接跳过,后面回退到 app.conf\n\t\t\tenvFileExists = false\n\t\t\treturn\n\t\t}\n\t\tdefer file.Close()\n\n\t\tenvFileExists = true\n\t\tenvConfig = make(map[string]string)\n\n\t\tscanner := bufio.NewScanner(file)\n\t\tfor scanner.Scan() {\n\t\t\tline := strings.TrimSpace(scanner.Text())\n\t\t\tif line == \"\" || strings.HasPrefix(line, \"#\") || strings.HasPrefix(line, \";\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tparts := strings.SplitN(line, \"=\", 2)\n\t\t\tif len(parts) != 2 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tkey := strings.TrimSpace(parts[0])\n\t\t\tval := strings.TrimSpace(parts[1])\n\t\t\t// 去掉可能的引号\n\t\t\tval = strings.Trim(val, `\"'`)\n\t\t\tif key != \"\" {\n\t\t\t\tenvConfig[key] = val // 即使 val 为空也认为是有效配置\n\t\t\t}\n\t\t}\n\t})\n\n\t// 如果存在 .env 文件,则所有配置完全由 .env 决定:\n\t// - key 存在:返回对应值(可以是空字符串)\n\t// - key 不存在:返回空字符串\n\tif envFileExists {\n\t\tif envConfig == nil {\n\t\t\treturn \"\"\n\t\t}\n\t\tif val, ok := envConfig[v]; ok {\n\t\t\treturn val\n\t\t}\n\t\treturn \"\"\n\t}\n\t// 其次从 config/app.conf 读取\n\tcfg, err := ini.Load(\"./config/app.conf\")\n\tif err != nil {\n\t\tlogger.Log.Error(err)\n\t\treturn \"\"\n\t}\n\tp, err := cfg.Section(\"\").GetKey(v)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn p.String()\n}\n\n// IsPasswordConfigured 检查是否配置了密码(用于图形查看)\n// 优先检查数据库中是否有激活的 Zabbix 实例配置了用户名和密码\n// 注意:查看图形需要用户名和密码,Token 方式无法用于 Web 登录\nfunc IsPasswordConfigured() bool {\n\t// 1. 优先检查数据库中是否有激活的 Zabbix 实例\n\tvar activeInstance ZabbixInstance\n\terr := DB.Where(\"is_active = ?\", true).First(&activeInstance).Error\n\tif err == nil {\n\t\t// 找到激活的实例,检查是否配置了用户名和密码(查看图形必须用密码,Token 无法用于 Web 登录)\n\t\tif strings.TrimSpace(activeInstance.User) != \"\" && strings.TrimSpace(activeInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 2. 如果数据库中没有激活实例,检查是否有任何可用的实例配置了密码\n\tvar anyInstance ZabbixInstance\n\terr = DB.Where(\"enabled = ?\", true).First(&anyInstance).Error\n\tif err == nil {\n\t\t// 找到可用的实例,检查是否配置了用户名和密码\n\t\tif strings.TrimSpace(anyInstance.User) != \"\" && strings.TrimSpace(anyInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 3. 回退到配置文件检查(兼容旧版本)\n\tpass := GetConfKey(\"zabbix_pass\")\n\tuser := GetConfKey(\"zabbix_user\")\n\treturn pass != \"\" && user != \"\"\n}\n\n// GetEncryptionKey 获取加密密钥\n// 优先从数据库读取,如果数据库中没有则从环境变量读取,最后使用默认密钥\nfunc GetEncryptionKey() string {\n\t// 1. 优先从数据库读取\n\tkey := GetConfigValueByKey(\"encryption_key\", \"\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 2. 从环境变量或配置文件读取\n\tkey = GetConfKey(\"encryption_key\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 3. 使用默认密钥(不应该到这里,因为 DatabaseInit 会生成)\n\tlogger.Log.Warn(\"使用默认加密密钥,建议在数据库中配置 encryption_key\")\n\treturn \"zbxtable-default-encryption-key-2024\"\n}\n"}, "4de66ee0706b14abd01317263e1a9a2ac9bfe893": {"t": 1785754926.6742547, "n": 11601, "h": "f8c8e69ba25b41a9", "c": "package model\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\t\"zbxtable/pkg/logger\"\n\t\"zbxtable/pkg/utils\"\n\n\tzabbix \"github.com/canghai908/zabbix-go\"\n\t_ \"github.com/go-sql-driver/mysql\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t_ \"github.com/lib/pq\"\n\tworkwx \"github.com/xen0n/go-workwx\"\n\tini \"gopkg.in/ini.v1\"\n\t\"gorm.io/gorm\"\n)\n\nvar (\n\tAPI = &zabbix.API{}\n\tjson = jsoniter.Config{\n\t\tEscapeHTML: false,\n\t\tSortMapKeys: true,\n\t\tValidateJsonRawMessage: true,\n\t}.Froze()\n\tZBX_VER string\n\tZBX_V bool\n\tVersion string\n\tGitHash string\n\tBuildTime string\n\tWeApp = &workwx.WorkwxApp{}\n\n\tenvConfig map[string]string\n\tenvConfigOnce sync.Once\n\tenvFileExists bool\n)\n\nconst (\n\t//ms-agent 常量定义\n\tMSName = \"ms-agent\"\n\tMSUser = \"ms-agent\"\n\tMSGroup = \"MS-Agent\"\n\tMSMedia = \"MS-Agent\"\n\tMSAction = \"MS-Agent\"\n\t//webhook 常量定义\n\tWebhookName = \"ZbxTable\"\n\tWebhookAction = \"ZbxTable Webhook\"\n\tWebhookGroup = \"ZbxTable Webhook\"\n\tWebhookUser = \"zbxtable-webhook\"\n\t//down\n\tDownloadPath = \"./download/\"\n\tTplPath = \"./assets/templates/\"\n)\n\n// TableName 表名前缀\nfunc TableName(str string) string {\n\treturn fmt.Sprintf(\"%s%s\", \"zbxtable_\", str)\n}\n\n// 接收数据库信息初始化\nfunc ModelInit(dbtype, dbhost, dbuser, dbpass, dbname, dbport string) {\n\t// 直接使用 GORM\n\trunmode := GetConfKey(\"runmode\")\n\terr := InitGormDB(dbtype, dbhost, dbuser, dbpass, dbname, dbport, runmode)\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to connect database: \", err)\n\t\tos.Exit(1)\n\t}\n\tlogger.Log.Info(\"Database connected!\")\n\t// 自动迁移表\n\terr = AutoMigrate()\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to auto migrate: \", err)\n\t\tos.Exit(1)\n\t}\n\t// 基础数据初始化\n\tDatabaseInit()\n\t// 使用go-cache替代Redis(不再需要Redis连接)\n\tInitCache()\n}\n\nfunc InitWechat() {\n\t// 首先检查企业微信开关是否启用\n\twechatEnabled := GetConfigValueByKey(\"wechat_enabled\", \"0\")\n\tif wechatEnabled != \"1\" {\n\t\tlogger.Log.Info(\"WeChat is disabled in system config, skipping WeChat initialization\")\n\t\treturn\n\t}\n\t// 开关已启用,从数据库读取企业微信配置\n\tagentIDStr := GetConfigValueByKey(\"wechat_agentid\", \"\")\n\tcorpid := GetConfigValueByKey(\"wechat_corpid\", \"\")\n\tsecret := GetConfigValueByKey(\"wechat_secret\", \"\")\n\n\t// 验证必填配置项\n\tif agentIDStr == \"\" || corpid == \"\" || secret == \"\" {\n\t\tlogger.Log.Info(\"WeChat is enabled but configuration is incomplete (agentid/corpid/secret), WeChat app will not be initialized\")\n\t\treturn\n\t}\n\t// 解析 AgentID\n\tAgentId, err := strconv.ParseInt(agentIDStr, 10, 64)\n\tif err != nil {\n\t\tlogger.Log.Error(\"wechat_agentid parse error:\", err)\n\t\treturn\n\t}\n\n\t// 初始化企业微信客户端\n\tclient := workwx.New(corpid)\n\tWeApp = client.WithApp(secret, AgentId)\n\tWeApp.SpawnAccessTokenRefresher()\n\tlogger.Log.Info(\"WeChat inited successfully!\")\n}\n\n// DatabaseInit 数据初始化\nfunc DatabaseInit() {\n\t//数据初始化操作 - 使用 GORM\n\tvar v User\n\terr := DB.Where(\"username = ?\", \"admin\").First(&v).Error\n\t//检查权限\n\tif err == nil && v.Operation == \"\" {\n\t\terr := DB.Model(&User{}).Where(\"id = ?\", v.ID).Update(\"operation\", \"['add', 'edit', 'delete','update']\").Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"update admin operation successfully\")\n\t}\n\t//添加管理员账号\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\tlogger.Log.Info(\"the admin user does not exist, create a new admin account later!\")\n\n\t\t// 构建默认主题配置(使用结构化方式,避免手写 JSON 出错)\n\t\tdefaultThemeConfig := map[string]interface{}{\n\t\t\t\"theme\": map[string]string{\n\t\t\t\t\"color\": \"#1890ff\",\n\t\t\t\t\"mode\": \"dark\",\n\t\t\t\t\"success\": \"#52c41a\",\n\t\t\t\t\"warning\": \"#faad14\",\n\t\t\t\t\"error\": \"#f5222f\",\n\t\t\t},\n\t\t\t\"animate\": map[string]interface{}{\n\t\t\t\t\"disabled\": true,\n\t\t\t\t\"name\": \"lightSpeed\",\n\t\t\t\t\"direction\": \"left\",\n\t\t\t},\n\t\t}\n\n\t\t// 将配置转换为 JSON 字符串\n\t\tthemeJSON, err := json.Marshal(defaultThemeConfig)\n\t\tif err != nil {\n\t\t\tlogger.Log.Error(\"Failed to marshal default theme config:\", err)\n\t\t\t// 如果序列化失败,使用备用的硬编码 JSON\n\t\t\tthemeJSON = []byte(`{\"theme\":{\"color\":\"#1890ff\",\"mode\":\"dark\",\"success\":\"#52c41a\",\"warning\":\"#faad14\",\"error\":\"#f5222f\"},\"animate\":{\"disabled\":true,\"name\":\"lightSpeed\",\"direction\":\"left\"}}`)\n\t\t}\n\n\t\tvar user User\n\t\tuser.Username = \"admin\"\n\t\tuser.Password, _ = utils.PasswordHash(\"Zbxtable\")\n\t\tuser.Avatar = \"https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif\"\n\t\tuser.Role = \"admin\"\n\t\tuser.Operation = \"['add', 'edit', 'delete','update']\"\n\t\tuser.Theme = string(themeJSON)\n\t\tuser.Status = 0\n\t\terr = DB.Create(&user).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"create an administrator account successfully, the admin ID is:\", user.ID)\n\t}\n\t//初始化系统数据\n\tvar cnt []System\n\terr = DB.Find(&cnt).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn\n\t}\n\tif len(cnt) == 0 {\n\t\tnow := time.Now()\n\t\tsys := []System{\n\t\t\t{Name: \"Linux操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"Windows操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"网络设备\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"物理服务器\", Status: 0, InitedAt: &now},\n\t\t}\n\t\terr := DB.Create(&sys).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init system info error!\", err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"Init system data successfully!\")\n\t}\n\t//告警默认规则初始化 default rule\n\tvar rules []Rule\n\terr = DB.Where(\"m_type = ?\", \"2\").Find(&rules).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn\n\t}\n\tif len(rules) == 0 {\n\t\t// 使用 \"*\" 作为全局默认规则,匹配所有租户\n\t\tdefaultRule := []Rule{\n\t\t\t{\n\t\t\t\tName: \"全局默认规则\",\n\t\t\t\tZIDs: \"*\",\n\t\t\t\tMType: \"2\",\n\t\t\t\tChannel: \"wechat_robot\",\n\t\t\t\tUserIds: \"1\",\n\t\t\t\tSweek: \"0,1,2,3,4,5,6\",\n\t\t\t\tStime: \"00:00\",\n\t\t\t\tEtime: \"23.59\",\n\t\t\t\tStatus: \"0\"},\n\t\t}\n\t\terr := DB.Create(&defaultRule).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init default rule error!\", err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"Init default rule successfully!\")\n\t}\n\t// 初始化加密密钥(如果不存在)\n\tvar encryptionKeyConfig Config\n\terr = DB.Where(\"`key` = ?\", \"encryption_key\").First(&encryptionKeyConfig).Error\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t// 生成随机加密密钥\n\t\trandomKey := utils.GenerateRandomKey()\n\t\tencryptionKeyConfig = Config{\n\t\t\tName: \"加密密钥\",\n\t\t\tKey: \"encryption_key\",\n\t\t\tValue: randomKey,\n\t\t\tComment: \"用于加密存储敏感信息(如Zabbix密码和Token)的密钥,系统自动生成,不可修改\",\n\t\t}\n\t\tif insertErr := DB.Create(&encryptionKeyConfig).Error; insertErr != nil {\n\t\t\tlogger.Log.Error(\"Init encryption_key error:\", insertErr)\n\t\t} else {\n\t\t\tlogger.Log.Info(\"Init encryption_key successfully! Key length:\", len(randomKey))\n\t\t}\n\t}\n\n\tdefaultConfigs := []Config{\n\t\t// 系统外观配置\n\t\t{Name: \"系统名称\", Key: \"system_name\", Value: \"ZbxTable\", Comment: \"系统显示的名称\"},\n\t\t{Name: \"系统Logo\", Key: \"system_logo\", Value: \"data:image/png;base64,\", Comment: \"系统Logo的base64编码数据\"},\n\t\t// 初始配置状态\n\t\t{Name: \"初始配置完成\", Key: \"initial_setup_completed\", Value: \"0\", Comment: \"标记系统是否完成初始配置:1 已完成,0 未完成\"},\n\t\t// Dashboard 相关\n\t\t{Name: \"数据面板\", Key: \"zbx_dash\", Value: \"0\", Comment: \"是否开启Zabbix看板:1 开启,0 关闭\"},\n\t\t{Name: \"面板配置\", Key: \"dash_id\", Value: \"1\", Comment: \"需要引入的Zabbix面板的ID,默认为1\"},\n\t\t{Name: \"主机分类同步\", Key: \"sync_inventory\", Value: \"1\", Comment: \"主机分类同步计划任务是否启用:1 启用,0 不启用\"},\n\t\t{Name: \"Webhook回调地址\", Key: \"webhook_url\", Value: \"\", Comment: \"webhook通知地址\"},\n\t\t// 邮件配置\n\t\t{Name: \"邮件发件人\", Key: \"email_from\", Value: \"\", Comment: \"告警邮件发件人邮箱地址\"},\n\t\t{Name: \"邮件昵称\", Key: \"email_nickname\", Value: \"ZbxTable\", Comment: \"告警邮件显示的发件人昵称\"},\n\t\t{Name: \"SMTP 密码/授权码\", Key: \"email_secret\", Value: \"\", Comment: \"SMTP 登录密码或授权码\"},\n\t\t{Name: \"SMTP 服务器\", Key: \"email_host\", Value: \"smtp.qq.com\", Comment: \"SMTP 服务器地址\"},\n\t\t{Name: \"SMTP 端口\", Key: \"email_port\", Value: \"465\", Comment: \"SMTP 端口号\"},\n\t\t{Name: \"SMTP 使用 SSL\", Key: \"email_isSSl\", Value: \"true\", Comment: \"是否启用 SSL:true/false\"},\n\t\t// 企业微信配置\n\t\t{Name: \"企业微信开关\", Key: \"wechat_enabled\", Value: \"0\", Comment: \"是否启用企业微信:1 启用,0 禁用\"},\n\t\t{Name: \"企业微信 AgentID\", Key: \"wechat_agentid\", Value: \"\", Comment: \"企业微信应用的 AgentID\"},\n\t\t{Name: \"企业微信 CorpID\", Key: \"wechat_corpid\", Value: \"\", Comment: \"企业微信企业ID\"},\n\t\t{Name: \"企业微信 Secret\", Key: \"wechat_secret\", Value: \"\", Comment: \"企业微信应用的 Secret\"},\n\t\t// AI 配置\n\t\t{Name: \"AI 类型\", Key: \"ai_type\", Value: \"ollama\", Comment: \"选择使用的 AI 服务类型:ollama 或 deepseek\"},\n\t\t// Ollama 配置\n\t\t{Name: \"Ollama Host\", Key: \"ollama_host\", Value: \"http://localhost:11434\", Comment: \"Ollama 服务地址,如 http://127.0.0.1:11434\"},\n\t\t{Name: \"Ollama Model\", Key: \"ollama_model\", Value: \"deepseek-r1:32b\", Comment: \"默认使用的大模型名称\"},\n\t\t// Deepseek 配置\n\t\t{Name: \"Deepseek API Key\", Key: \"deepseek_api_key\", Value: \"\", Comment: \"Deepseek API 密钥\"},\n\t\t{Name: \"Deepseek Model\", Key: \"deepseek_model\", Value: \"deepseek-chat\", Comment: \"Deepseek 模型名称,如 deepseek-chat\"},\n\t\t{Name: \"Deepseek Base URL\", Key: \"deepseek_base_url\", Value: \"https://api.deepseek.com\", Comment: \"Deepseek API 地址,默认为 https://api.deepseek.com\"},\n\t}\n\n\tfor _, cfgItem := range defaultConfigs {\n\t\tvar existing Config\n\t\terr = DB.Where(\"`key` = ?\", cfgItem.Key).First(&existing).Error\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\tif insertErr := DB.Create(&cfgItem).Error; insertErr != nil {\n\t\t\t\tlogger.Log.Infof(\"Init config key %s error: %v\", cfgItem.Key, insertErr)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tlogger.Log.Infof(\"Init config key %s successfully!\", cfgItem.Key)\n\t\t}\n\t}\n\t//默认菜单初始化\n\tInitMenuData()\n\t//检查并添加缺失的菜单项(用于版本升级)\n\tCheckAndAddMenus()\n\n}\n\nfunc GetConfKey(v string) string {\n\t// 优先从 .env 中读取(便于开发环境配置敏感信息且不提交到 git)\n\tenvConfigOnce.Do(func() {\n\t\tfile, err := os.Open(\".env\")\n\t\tif err != nil {\n\t\t\t// 没有 .env 文件时直接跳过,后面回退到 app.conf\n\t\t\tenvFileExists = false\n\t\t\treturn\n\t\t}\n\t\tdefer file.Close()\n\n\t\tenvFileExists = true\n\t\tenvConfig = make(map[string]string)\n\n\t\tscanner := bufio.NewScanner(file)\n\t\tfor scanner.Scan() {\n\t\t\tline := strings.TrimSpace(scanner.Text())\n\t\t\tif line == \"\" || strings.HasPrefix(line, \"#\") || strings.HasPrefix(line, \";\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tparts := strings.SplitN(line, \"=\", 2)\n\t\t\tif len(parts) != 2 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tkey := strings.TrimSpace(parts[0])\n\t\t\tval := strings.TrimSpace(parts[1])\n\t\t\t// 去掉可能的引号\n\t\t\tval = strings.Trim(val, `\"'`)\n\t\t\tif key != \"\" {\n\t\t\t\tenvConfig[key] = val // 即使 val 为空也认为是有效配置\n\t\t\t}\n\t\t}\n\t})\n\n\t// 如果存在 .env 文件,则所有配置完全由 .env 决定:\n\t// - key 存在:返回对应值(可以是空字符串)\n\t// - key 不存在:返回空字符串\n\tif envFileExists {\n\t\tif envConfig == nil {\n\t\t\treturn \"\"\n\t\t}\n\t\tif val, ok := envConfig[v]; ok {\n\t\t\treturn val\n\t\t}\n\t\treturn \"\"\n\t}\n\t// 其次从 config/app.conf 读取\n\tcfg, err := ini.Load(\"./config/app.conf\")\n\tif err != nil {\n\t\tlogger.Log.Error(err)\n\t\treturn \"\"\n\t}\n\tp, err := cfg.Section(\"\").GetKey(v)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn p.String()\n}\n\n// IsPasswordConfigured 检查是否配置了密码(用于图形查看)\n// 优先检查数据库中是否有激活的 Zabbix 实例配置了用户名和密码\n// 注意:查看图形需要用户名和密码,Token 方式无法用于 Web 登录\nfunc IsPasswordConfigured() bool {\n\t// 1. 优先检查数据库中是否有激活的 Zabbix 实例\n\tvar activeInstance ZabbixInstance\n\terr := DB.Where(\"is_active = ?\", true).First(&activeInstance).Error\n\tif err == nil {\n\t\t// 找到激活的实例,检查是否配置了用户名和密码(查看图形必须用密码,Token 无法用于 Web 登录)\n\t\tif strings.TrimSpace(activeInstance.User) != \"\" && strings.TrimSpace(activeInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 2. 如果数据库中没有激活实例,检查是否有任何可用的实例配置了密码\n\tvar anyInstance ZabbixInstance\n\terr = DB.Where(\"enabled = ?\", true).First(&anyInstance).Error\n\tif err == nil {\n\t\t// 找到可用的实例,检查是否配置了用户名和密码\n\t\tif strings.TrimSpace(anyInstance.User) != \"\" && strings.TrimSpace(anyInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 3. 回退到配置文件检查(兼容旧版本)\n\tpass := GetConfKey(\"zabbix_pass\")\n\tuser := GetConfKey(\"zabbix_user\")\n\treturn pass != \"\" && user != \"\"\n}\n\n// GetEncryptionKey 获取加密密钥\n// 优先从数据库读取,如果数据库中没有则从环境变量读取,最后使用默认密钥\nfunc GetEncryptionKey() string {\n\t// 1. 优先从数据库读取\n\tkey := GetConfigValueByKey(\"encryption_key\", \"\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 2. 从环境变量或配置文件读取\n\tkey = GetConfKey(\"encryption_key\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 3. 使用默认密钥(不应该到这里,因为 DatabaseInit 会生成)\n\tlogger.Log.Warn(\"使用默认加密密钥,建议在数据库中配置 encryption_key\")\n\treturn \"zbxtable-default-encryption-key-2024\"\n}\n"}, "5c892d1095b2b669b2a04873e829f9f99b2bf670": {"t": 1785754928.9390695, "n": 11488, "h": "487d48e4506e1156", "c": "package model\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\t\"zbxtable/pkg/logger\"\n\t\"zbxtable/pkg/utils\"\n\n\tzabbix \"github.com/canghai908/zabbix-go\"\n\t_ \"github.com/go-sql-driver/mysql\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t_ \"github.com/lib/pq\"\n\tworkwx \"github.com/xen0n/go-workwx\"\n\tini \"gopkg.in/ini.v1\"\n\t\"gorm.io/gorm\"\n)\n\nvar (\n\tAPI = &zabbix.API{}\n\tjson = jsoniter.Config{\n\t\tEscapeHTML: false,\n\t\tSortMapKeys: true,\n\t\tValidateJsonRawMessage: true,\n\t}.Froze()\n\tZBX_VER string\n\tZBX_V bool\n\tVersion string\n\tGitHash string\n\tBuildTime string\n\tWeApp = &workwx.WorkwxApp{}\n\n\tenvConfig map[string]string\n\tenvConfigOnce sync.Once\n\tenvFileExists bool\n)\n\nconst (\n\t//ms-agent 常量定义\n\tMSName = \"ms-agent\"\n\tMSUser = \"ms-agent\"\n\tMSGroup = \"MS-Agent\"\n\tMSMedia = \"MS-Agent\"\n\tMSAction = \"MS-Agent\"\n\t//webhook 常量定义\n\tWebhookName = \"ZbxTable\"\n\tWebhookAction = \"ZbxTable Webhook\"\n\tWebhookGroup = \"ZbxTable Webhook\"\n\tWebhookUser = \"zbxtable-webhook\"\n\t//down\n\tDownloadPath = \"./download/\"\n\tTplPath = \"./assets/templates/\"\n)\n\n// TableName 表名前缀\nfunc TableName(str string) string {\n\treturn fmt.Sprintf(\"%s%s\", \"zbxtable_\", str)\n}\n\n// 接收数据库信息初始化\nfunc ModelInit(dbtype, dbhost, dbuser, dbpass, dbname, dbport string) {\n\t// 直接使用 GORM\n\trunmode := GetConfKey(\"runmode\")\n\terr := InitGormDB(dbtype, dbhost, dbuser, dbpass, dbname, dbport, runmode)\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to connect database: \", err)\n\t\tos.Exit(1)\n\t}\n\tlogger.Log.Info(\"Database connected!\")\n\t// 自动迁移表\n\terr = AutoMigrate()\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to auto migrate: \", err)\n\t\tos.Exit(1)\n\t}\n\t// 基础数据初始化\n\tDatabaseInit()\n\t// 使用go-cache替代Redis(不再需要Redis连接)\n\tInitCache()\n}\n\nfunc InitWechat() {\n\t// 首先检查企业微信开关是否启用\n\twechatEnabled := GetConfigValueByKey(\"wechat_enabled\", \"0\")\n\tif wechatEnabled != \"1\" {\n\t\tlogger.Log.Info(\"WeChat is disabled in system config, skipping WeChat initialization\")\n\t\treturn\n\t}\n\t// 开关已启用,从数据库读取企业微信配置\n\tagentIDStr := GetConfigValueByKey(\"wechat_agentid\", \"\")\n\tcorpid := GetConfigValueByKey(\"wechat_corpid\", \"\")\n\tsecret := GetConfigValueByKey(\"wechat_secret\", \"\")\n\n\t// 验证必填配置项\n\tif agentIDStr == \"\" || corpid == \"\" || secret == \"\" {\n\t\tlogger.Log.Info(\"WeChat is enabled but configuration is incomplete (agentid/corpid/secret), WeChat app will not be initialized\")\n\t\treturn\n\t}\n\t// 解析 AgentID\n\tAgentId, err := strconv.ParseInt(agentIDStr, 10, 64)\n\tif err != nil {\n\t\tlogger.Log.Error(\"wechat_agentid parse error:\", err)\n\t\treturn\n\t}\n\n\t// 初始化企业微信客户端\n\tclient := workwx.New(corpid)\n\tWeApp = client.WithApp(secret, AgentId)\n\tWeApp.SpawnAccessTokenRefresher()\n\tlogger.Log.Info(\"WeChat inited successfully!\")\n}\n\n// DatabaseInit 数据初始化\nfunc DatabaseInit() {\n\t//数据初始化操作 - 使用 GORM\n\tvar v User\n\terr := DB.Where(\"username = ?\", \"admin\").First(&v).Error\n\t//检查权限\n\tif err == nil && v.Operation == \"\" {\n\t\terr := DB.Model(&User{}).Where(\"id = ?\", v.ID).Update(\"operation\", \"['add', 'edit', 'delete','update']\").Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"update admin operation successfully\")\n\t}\n\t//添加管理员账号\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\tlogger.Log.Info(\"the admin user does not exist, create a new admin account later!\")\n\n\t\t// 构建默认主题配置(使用结构化方式,避免手写 JSON 出错)\n\t\tdefaultThemeConfig := map[string]interface{}{\n\t\t\t\"theme\": map[string]string{\n\t\t\t\t\"color\": \"#1890ff\",\n\t\t\t\t\"mode\": \"dark\",\n\t\t\t\t\"success\": \"#52c41a\",\n\t\t\t\t\"warning\": \"#faad14\",\n\t\t\t\t\"error\": \"#f5222f\",\n\t\t\t},\n\t\t\t\"animate\": map[string]interface{}{\n\t\t\t\t\"disabled\": true,\n\t\t\t\t\"name\": \"lightSpeed\",\n\t\t\t\t\"direction\": \"left\",\n\t\t\t},\n\t\t}\n\n\t\t// 将配置转换为 JSON 字符串\n\t\tthemeJSON, err := json.Marshal(defaultThemeConfig)\n\t\tif err != nil {\n\t\t\tlogger.Log.Error(\"Failed to marshal default theme config:\", err)\n\t\t\t// 如果序列化失败,使用备用的硬编码 JSON\n\t\t\tthemeJSON = []byte(`{\"theme\":{\"color\":\"#1890ff\",\"mode\":\"dark\",\"success\":\"#52c41a\",\"warning\":\"#faad14\",\"error\":\"#f5222f\"},\"animate\":{\"disabled\":true,\"name\":\"lightSpeed\",\"direction\":\"left\"}}`)\n\t\t}\n\n\t\tvar user User\n\t\tuser.Username = \"admin\"\n\t\tuser.Password, _ = utils.PasswordHash(\"Zbxtable\")\n\t\tuser.Avatar = \"https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif\"\n\t\tuser.Role = \"admin\"\n\t\tuser.Operation = \"['add', 'edit', 'delete','update']\"\n\t\tuser.Theme = string(themeJSON)\n\t\tuser.Status = 0\n\t\terr = DB.Create(&user).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"create an administrator account successfully, the admin ID is:\", user.ID)\n\t}\n\t//初始化系统数据\n\tvar cnt []System\n\terr = DB.Find(&cnt).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn\n\t}\n\tif len(cnt) == 0 {\n\t\tnow := time.Now()\n\t\tsys := []System{\n\t\t\t{Name: \"Linux操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"Windows操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"网络设备\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"物理服务器\", Status: 0, InitedAt: &now},\n\t\t}\n\t\terr := DB.Create(&sys).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init system info error!\", err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"Init system data successfully!\")\n\t}\n\t//告警默认规则初始化 default rule\n\tvar rules []Rule\n\terr = DB.Where(\"m_type = ?\", \"2\").Find(&rules).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn\n\t}\n\tif len(rules) == 0 {\n\t\t// 使用 \"*\" 作为全局默认规则,匹配所有租户\n\t\tdefaultRule := []Rule{\n\t\t\t{\n\t\t\t\tName: \"全局默认规则\",\n\t\t\t\tZIDs: \"*\",\n\t\t\t\tMType: \"2\",\n\t\t\t\tChannel: \"wechat_robot\",\n\t\t\t\tUserIds: \"1\",\n\t\t\t\tSweek: \"0,1,2,3,4,5,6\",\n\t\t\t\tStime: \"00:00\",\n\t\t\t\tEtime: \"23.59\",\n\t\t\t\tStatus: \"0\"},\n\t\t}\n\t\terr := DB.Create(&defaultRule).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init default rule error!\", err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"Init default rule successfully!\")\n\t}\n\t// 初始化加密密钥(如果不存在)\n\tvar encryptionKeyConfig Config\n\terr = DB.Where(\"`key` = ?\", \"encryption_key\").First(&encryptionKeyConfig).Error\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t// 生成随机加密密钥\n\t\trandomKey := utils.GenerateRandomKey()\n\t\tencryptionKeyConfig = Config{\n\t\t\tName: \"加密密钥\",\n\t\t\tKey: \"encryption_key\",\n\t\t\tValue: randomKey,\n\t\t\tComment: \"用于加密存储敏感信息(如Zabbix密码和Token)的密钥,系统自动生成,不可修改\",\n\t\t}\n\t\tif insertErr := DB.Create(&encryptionKeyConfig).Error; insertErr != nil {\n\t\t\tlogger.Log.Error(\"Init encryption_key error:\", insertErr)\n\t\t} else {\n\t\t\tlogger.Log.Info(\"Init encryption_key successfully! Key length:\", len(randomKey))\n\t\t}\n\t}\n\n\tdefaultConfigs := []Config{\n\t\t// 系统外观配置\n\t\t{Name: \"系统名称\", Key: \"system_name\", Value: \"ZbxTable\", Comment: \"系统显示的名称\"},\n\t\t{Name: \"系统Logo\", Key: \"system_logo\", Value: \"data:image/png;base64,\", Comment: \"系统Logo的base64编码数据\"},\n\t\t// Dashboard 相关\n\t\t{Name: \"数据面板\", Key: \"zbx_dash\", Value: \"0\", Comment: \"是否开启Zabbix看板:1 开启,0 关闭\"},\n\t\t{Name: \"面板配置\", Key: \"dash_id\", Value: \"1\", Comment: \"需要引入的Zabbix面板的ID,默认为1\"},\n\t\t{Name: \"主机分类同步\", Key: \"sync_inventory\", Value: \"1\", Comment: \"主机分类同步计划任务是否启用:1 启用,0 不启用\"},\n\t\t{Name: \"Webhook回调地址\", Key: \"webhook_url\", Value: \"\", Comment: \"webhook通知地址\"},\n\t\t// 邮件配置\n\t\t{Name: \"邮件发件人\", Key: \"email_from\", Value: \"\", Comment: \"告警邮件发件人邮箱地址\"},\n\t\t{Name: \"邮件昵称\", Key: \"email_nickname\", Value: \"ZbxTable\", Comment: \"告警邮件显示的发件人昵称\"},\n\t\t{Name: \"SMTP 密码/授权码\", Key: \"email_secret\", Value: \"\", Comment: \"SMTP 登录密码或授权码\"},\n\t\t{Name: \"SMTP 服务器\", Key: \"email_host\", Value: \"smtp.qq.com\", Comment: \"SMTP 服务器地址\"},\n\t\t{Name: \"SMTP 端口\", Key: \"email_port\", Value: \"465\", Comment: \"SMTP 端口号\"},\n\t\t{Name: \"SMTP 使用 SSL\", Key: \"email_isSSl\", Value: \"true\", Comment: \"是否启用 SSL:true/false\"},\n\t\t// 企业微信配置\n\t\t{Name: \"企业微信开关\", Key: \"wechat_enabled\", Value: \"0\", Comment: \"是否启用企业微信:1 启用,0 禁用\"},\n\t\t{Name: \"企业微信 AgentID\", Key: \"wechat_agentid\", Value: \"\", Comment: \"企业微信应用的 AgentID\"},\n\t\t{Name: \"企业微信 CorpID\", Key: \"wechat_corpid\", Value: \"\", Comment: \"企业微信企业ID\"},\n\t\t{Name: \"企业微信 Secret\", Key: \"wechat_secret\", Value: \"\", Comment: \"企业微信应用的 Secret\"},\n\t\t// AI 配置\n\t\t{Name: \"AI 类型\", Key: \"ai_type\", Value: \"ollama\", Comment: \"选择使用的 AI 服务类型:ollama 或 deepseek\"},\n\t\t// Ollama 配置\n\t\t{Name: \"Ollama Host\", Key: \"ollama_host\", Value: \"http://localhost:11434\", Comment: \"Ollama 服务地址,如 http://127.0.0.1:11434\"},\n\t\t{Name: \"Ollama Model\", Key: \"ollama_model\", Value: \"deepseek-r1:32b\", Comment: \"默认使用的大模型名称\"},\n\t\t// Deepseek 配置\n\t\t{Name: \"Deepseek API Key\", Key: \"deepseek_api_key\", Value: \"\", Comment: \"Deepseek API 密钥\"},\n\t\t{Name: \"Deepseek Model\", Key: \"deepseek_model\", Value: \"deepseek-chat\", Comment: \"Deepseek 模型名称,如 deepseek-chat\"},\n\t\t{Name: \"Deepseek Base URL\", Key: \"deepseek_base_url\", Value: \"https://api.deepseek.com\", Comment: \"Deepseek API 地址,默认为 https://api.deepseek.com\"},\n\t}\n\n\tfor _, cfgItem := range defaultConfigs {\n\t\tvar existing Config\n\t\terr = DB.Where(\"`key` = ?\", cfgItem.Key).First(&existing).Error\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\tif insertErr := DB.Create(&cfgItem).Error; insertErr != nil {\n\t\t\t\tlogger.Log.Infof(\"Init config key %s error: %v\", cfgItem.Key, insertErr)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tlogger.Log.Infof(\"Init config key %s successfully!\", cfgItem.Key)\n\t\t}\n\t}\n\t//默认菜单初始化\n\tInitMenuData()\n\t//检查并添加缺失的菜单项(用于版本升级)\n\tCheckAndAddMenus()\n\n}\n\nfunc GetConfKey(v string) string {\n\t// 优先从 .env 中读取(便于开发环境配置敏感信息且不提交到 git)\n\tenvConfigOnce.Do(func() {\n\t\tfile, err := os.Open(\".env\")\n\t\tif err != nil {\n\t\t\t// 没有 .env 文件时直接跳过,后面回退到 app.conf\n\t\t\tenvFileExists = false\n\t\t\treturn\n\t\t}\n\t\tdefer file.Close()\n\n\t\tenvFileExists = true\n\t\tenvConfig = make(map[string]string)\n\n\t\tscanner := bufio.NewScanner(file)\n\t\tfor scanner.Scan() {\n\t\t\tline := strings.TrimSpace(scanner.Text())\n\t\t\tif line == \"\" || strings.HasPrefix(line, \"#\") || strings.HasPrefix(line, \";\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tparts := strings.SplitN(line, \"=\", 2)\n\t\t\tif len(parts) != 2 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tkey := strings.TrimSpace(parts[0])\n\t\t\tval := strings.TrimSpace(parts[1])\n\t\t\t// 去掉可能的引号\n\t\t\tval = strings.Trim(val, `\"'`)\n\t\t\tif key != \"\" {\n\t\t\t\tenvConfig[key] = val // 即使 val 为空也认为是有效配置\n\t\t\t}\n\t\t}\n\t})\n\n\t// 如果存在 .env 文件,则所有配置完全由 .env 决定:\n\t// - key 存在:返回对应值(可以是空字符串)\n\t// - key 不存在:返回空字符串\n\tif envFileExists {\n\t\tif envConfig == nil {\n\t\t\treturn \"\"\n\t\t}\n\t\tif val, ok := envConfig[v]; ok {\n\t\t\treturn val\n\t\t}\n\t\treturn \"\"\n\t}\n\t// 其次从 config/app.conf 读取\n\tcfg, err := ini.Load(\"./config/app.conf\")\n\tif err != nil {\n\t\tlogger.Log.Error(err)\n\t\treturn \"\"\n\t}\n\tp, err := cfg.Section(\"\").GetKey(v)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn p.String()\n}\n\n// IsPasswordConfigured 检查是否配置了密码(用于图形查看)\n// 优先检查数据库中是否有激活的 Zabbix 实例配置了用户名和密码\n// 注意:查看图形需要用户名和密码,Token 方式无法用于 Web 登录\nfunc IsPasswordConfigured() bool {\n\t// 1. 优先检查数据库中是否有激活的 Zabbix 实例\n\tvar activeInstance ZabbixInstance\n\terr := DB.Where(\"is_active = ?\", true).First(&activeInstance).Error\n\tif err == nil {\n\t\t// 找到激活的实例,检查是否配置了用户名和密码(查看图形必须用密码,Token 无法用于 Web 登录)\n\t\tif strings.TrimSpace(activeInstance.User) != \"\" && strings.TrimSpace(activeInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 2. 如果数据库中没有激活实例,检查是否有任何可用的实例配置了密码\n\tvar anyInstance ZabbixInstance\n\terr = DB.Where(\"enabled = ?\", true).First(&anyInstance).Error\n\tif err == nil {\n\t\t// 找到可用的实例,检查是否配置了用户名和密码\n\t\tif strings.TrimSpace(anyInstance.User) != \"\" && strings.TrimSpace(anyInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 3. 回退到配置文件检查(兼容旧版本)\n\tpass := GetConfKey(\"zabbix_pass\")\n\tuser := GetConfKey(\"zabbix_user\")\n\treturn pass != \"\" && user != \"\"\n}\n\n// GetEncryptionKey 获取加密密钥\n// 优先从数据库读取,如果数据库中没有则从环境变量读取,最后使用默认密钥\nfunc GetEncryptionKey() string {\n\t// 1. 优先从数据库读取\n\tkey := GetConfigValueByKey(\"encryption_key\", \"\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 2. 从环境变量或配置文件读取\n\tkey = GetConfKey(\"encryption_key\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 3. 使用默认密钥(不应该到这里,因为 DatabaseInit 会生成)\n\tlogger.Log.Warn(\"使用默认加密密钥,建议在数据库中配置 encryption_key\")\n\treturn \"zbxtable-default-encryption-key-2024\"\n}\n"}, "2033670f9f341e796abca936fdcff0d85eb7ca01": {"t": 1785754930.260829, "n": 10811, "h": "62d7ead47ed7b769", "c": "package model\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\t\"zbxtable/pkg/logger\"\n\t\"zbxtable/pkg/utils\"\n\n\tzabbix \"github.com/canghai908/zabbix-go\"\n\t_ \"github.com/go-sql-driver/mysql\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t_ \"github.com/lib/pq\"\n\tworkwx \"github.com/xen0n/go-workwx\"\n\tini \"gopkg.in/ini.v1\"\n\t\"gorm.io/gorm\"\n)\n\nvar (\n\tAPI = &zabbix.API{}\n\tjson = jsoniter.Config{\n\t\tEscapeHTML: false,\n\t\tSortMapKeys: true,\n\t\tValidateJsonRawMessage: true,\n\t}.Froze()\n\tZBX_VER string\n\tZBX_V bool\n\tVersion string\n\tGitHash string\n\tBuildTime string\n\tWeApp = &workwx.WorkwxApp{}\n\n\tenvConfig map[string]string\n\tenvConfigOnce sync.Once\n\tenvFileExists bool\n)\n\nconst (\n\t//ms-agent 常量定义\n\tMSName = \"ms-agent\"\n\tMSUser = \"ms-agent\"\n\tMSGroup = \"MS-Agent\"\n\tMSMedia = \"MS-Agent\"\n\tMSAction = \"MS-Agent\"\n\t//webhook 常量定义\n\tWebhookName = \"ZbxTable\"\n\tWebhookAction = \"ZbxTable Webhook\"\n\tWebhookGroup = \"ZbxTable Webhook\"\n\tWebhookUser = \"zbxtable-webhook\"\n\t//down\n\tDownloadPath = \"./download/\"\n\tTplPath = \"./assets/templates/\"\n)\n\n// TableName 表名前缀\nfunc TableName(str string) string {\n\treturn fmt.Sprintf(\"%s%s\", \"zbxtable_\", str)\n}\n\n// 接收数据库信息初始化\nfunc ModelInit(dbtype, dbhost, dbuser, dbpass, dbname, dbport string) {\n\t// 直接使用 GORM\n\trunmode := GetConfKey(\"runmode\")\n\terr := InitGormDB(dbtype, dbhost, dbuser, dbpass, dbname, dbport, runmode)\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to connect database: \", err)\n\t\tos.Exit(1)\n\t}\n\tlogger.Log.Info(\"Database connected!\")\n\t// 自动迁移表\n\terr = AutoMigrate()\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to auto migrate: \", err)\n\t\tos.Exit(1)\n\t}\n\t// 基础数据初始化\n\tDatabaseInit()\n\t// 使用go-cache替代Redis(不再需要Redis连接)\n\tInitCache()\n}\n\nfunc InitWechat() {\n\t// 首先检查企业微信开关是否启用\n\twechatEnabled := GetConfigValueByKey(\"wechat_enabled\", \"0\")\n\tif wechatEnabled != \"1\" {\n\t\tlogger.Log.Info(\"WeChat is disabled in system config, skipping WeChat initialization\")\n\t\treturn\n\t}\n\t// 开关已启用,从数据库读取企业微信配置\n\tagentIDStr := GetConfigValueByKey(\"wechat_agentid\", \"\")\n\tcorpid := GetConfigValueByKey(\"wechat_corpid\", \"\")\n\tsecret := GetConfigValueByKey(\"wechat_secret\", \"\")\n\n\t// 验证必填配置项\n\tif agentIDStr == \"\" || corpid == \"\" || secret == \"\" {\n\t\tlogger.Log.Info(\"WeChat is enabled but configuration is incomplete (agentid/corpid/secret), WeChat app will not be initialized\")\n\t\treturn\n\t}\n\t// 解析 AgentID\n\tAgentId, err := strconv.ParseInt(agentIDStr, 10, 64)\n\tif err != nil {\n\t\tlogger.Log.Error(\"wechat_agentid parse error:\", err)\n\t\treturn\n\t}\n\n\t// 初始化企业微信客户端\n\tclient := workwx.New(corpid)\n\tWeApp = client.WithApp(secret, AgentId)\n\tWeApp.SpawnAccessTokenRefresher()\n\tlogger.Log.Info(\"WeChat inited successfully!\")\n}\n\n// DatabaseInit 数据初始化\nfunc DatabaseInit() {\n\t//数据初始化操作 - 使用 GORM\n\tvar v User\n\terr := DB.Where(\"username = ?\", \"admin\").First(&v).Error\n\t//检查权限\n\tif err == nil && v.Operation == \"\" {\n\t\terr := DB.Model(&User{}).Where(\"id = ?\", v.ID).Update(\"operation\", \"['add', 'edit', 'delete','update']\").Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"update admin operation successfully\")\n\t}\n\t//添加管理员账号\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\tlogger.Log.Info(\"the admin user does not exist, create a new admin account later!\")\n\t\tvar user User\n\t\tuser.Username = \"admin\"\n\t\tuser.Password, _ = utils.PasswordHash(\"Zbxtable\")\n\t\tuser.Avatar = \"https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif\"\n\t\tuser.Role = \"admin\"\n\t\tuser.Operation = \"['add', 'edit', 'delete','update']\"\n\t\tuser.Theme = `{\"theme\":{\"color\":\"#1890ff\",\"mode\":\"dark\",\"success\":\"#52c41a\",\"warning\":\"#faad14\",\"error\":\"#f5222f\"}}`\n\t\tuser.Status = 0\n\t\terr := DB.Create(&user).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"create an administrator account successfully, the admin ID is:\", user.ID)\n\t}\n\t//初始化系统数据\n\tvar cnt []System\n\terr = DB.Find(&cnt).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn\n\t}\n\tif len(cnt) == 0 {\n\t\tnow := time.Now()\n\t\tsys := []System{\n\t\t\t{Name: \"Linux操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"Windows操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"网络设备\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"物理服务器\", Status: 0, InitedAt: &now},\n\t\t}\n\t\terr := DB.Create(&sys).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init system info error!\", err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"Init system data successfully!\")\n\t}\n\t//告警默认规则初始化 default rule\n\tvar rules []Rule\n\terr = DB.Where(\"m_type = ?\", \"2\").Find(&rules).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn\n\t}\n\tif len(rules) == 0 {\n\t\t// 使用 \"*\" 作为全局默认规则,匹配所有租户\n\t\tdefaultRule := []Rule{\n\t\t\t{\n\t\t\t\tName: \"全局默认规则\",\n\t\t\t\tZIDs: \"*\",\n\t\t\t\tMType: \"2\",\n\t\t\t\tChannel: \"wechat_robot\",\n\t\t\t\tUserIds: \"1\",\n\t\t\t\tSweek: \"0,1,2,3,4,5,6\",\n\t\t\t\tStime: \"00:00\",\n\t\t\t\tEtime: \"23.59\",\n\t\t\t\tStatus: \"0\"},\n\t\t}\n\t\terr := DB.Create(&defaultRule).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init default rule error!\", err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"Init default rule successfully!\")\n\t}\n\t// 初始化加密密钥(如果不存在)\n\tvar encryptionKeyConfig Config\n\terr = DB.Where(\"`key` = ?\", \"encryption_key\").First(&encryptionKeyConfig).Error\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t// 生成随机加密密钥\n\t\trandomKey := utils.GenerateRandomKey()\n\t\tencryptionKeyConfig = Config{\n\t\t\tName: \"加密密钥\",\n\t\t\tKey: \"encryption_key\",\n\t\t\tValue: randomKey,\n\t\t\tComment: \"用于加密存储敏感信息(如Zabbix密码和Token)的密钥,系统自动生成,不可修改\",\n\t\t}\n\t\tif insertErr := DB.Create(&encryptionKeyConfig).Error; insertErr != nil {\n\t\t\tlogger.Log.Error(\"Init encryption_key error:\", insertErr)\n\t\t} else {\n\t\t\tlogger.Log.Info(\"Init encryption_key successfully! Key length:\", len(randomKey))\n\t\t}\n\t}\n\n\tdefaultConfigs := []Config{\n\t\t// 系统外观配置\n\t\t{Name: \"系统名称\", Key: \"system_name\", Value: \"ZbxTable\", Comment: \"系统显示的名称\"},\n\t\t{Name: \"系统Logo\", Key: \"system_logo\", Value: \"data:image/png;base64,\", Comment: \"系统Logo的base64编码数据\"},\n\t\t// Dashboard 相关\n\t\t{Name: \"数据面板\", Key: \"zbx_dash\", Value: \"0\", Comment: \"是否开启Zabbix看板:1 开启,0 关闭\"},\n\t\t{Name: \"面板配置\", Key: \"dash_id\", Value: \"1\", Comment: \"需要引入的Zabbix面板的ID,默认为1\"},\n\t\t{Name: \"主机分类同步\", Key: \"sync_inventory\", Value: \"1\", Comment: \"主机分类同步计划任务是否启用:1 启用,0 不启用\"},\n\t\t{Name: \"Webhook回调地址\", Key: \"webhook_url\", Value: \"\", Comment: \"webhook通知地址\"},\n\t\t// 邮件配置\n\t\t{Name: \"邮件发件人\", Key: \"email_from\", Value: \"\", Comment: \"告警邮件发件人邮箱地址\"},\n\t\t{Name: \"邮件昵称\", Key: \"email_nickname\", Value: \"ZbxTable\", Comment: \"告警邮件显示的发件人昵称\"},\n\t\t{Name: \"SMTP 密码/授权码\", Key: \"email_secret\", Value: \"\", Comment: \"SMTP 登录密码或授权码\"},\n\t\t{Name: \"SMTP 服务器\", Key: \"email_host\", Value: \"smtp.qq.com\", Comment: \"SMTP 服务器地址\"},\n\t\t{Name: \"SMTP 端口\", Key: \"email_port\", Value: \"465\", Comment: \"SMTP 端口号\"},\n\t\t{Name: \"SMTP 使用 SSL\", Key: \"email_isSSl\", Value: \"true\", Comment: \"是否启用 SSL:true/false\"},\n\t\t// 企业微信配置\n\t\t{Name: \"企业微信开关\", Key: \"wechat_enabled\", Value: \"0\", Comment: \"是否启用企业微信:1 启用,0 禁用\"},\n\t\t{Name: \"企业微信 AgentID\", Key: \"wechat_agentid\", Value: \"\", Comment: \"企业微信应用的 AgentID\"},\n\t\t{Name: \"企业微信 CorpID\", Key: \"wechat_corpid\", Value: \"\", Comment: \"企业微信企业ID\"},\n\t\t{Name: \"企业微信 Secret\", Key: \"wechat_secret\", Value: \"\", Comment: \"企业微信应用的 Secret\"},\n\t\t// AI 配置\n\t\t{Name: \"AI 类型\", Key: \"ai_type\", Value: \"ollama\", Comment: \"选择使用的 AI 服务类型:ollama 或 deepseek\"},\n\t\t// Ollama 配置\n\t\t{Name: \"Ollama Host\", Key: \"ollama_host\", Value: \"http://localhost:11434\", Comment: \"Ollama 服务地址,如 http://127.0.0.1:11434\"},\n\t\t{Name: \"Ollama Model\", Key: \"ollama_model\", Value: \"deepseek-r1:32b\", Comment: \"默认使用的大模型名称\"},\n\t\t// Deepseek 配置\n\t\t{Name: \"Deepseek API Key\", Key: \"deepseek_api_key\", Value: \"\", Comment: \"Deepseek API 密钥\"},\n\t\t{Name: \"Deepseek Model\", Key: \"deepseek_model\", Value: \"deepseek-chat\", Comment: \"Deepseek 模型名称,如 deepseek-chat\"},\n\t\t{Name: \"Deepseek Base URL\", Key: \"deepseek_base_url\", Value: \"https://api.deepseek.com\", Comment: \"Deepseek API 地址,默认为 https://api.deepseek.com\"},\n\t}\n\n\tfor _, cfgItem := range defaultConfigs {\n\t\tvar existing Config\n\t\terr = DB.Where(\"`key` = ?\", cfgItem.Key).First(&existing).Error\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\tif insertErr := DB.Create(&cfgItem).Error; insertErr != nil {\n\t\t\t\tlogger.Log.Infof(\"Init config key %s error: %v\", cfgItem.Key, insertErr)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tlogger.Log.Infof(\"Init config key %s successfully!\", cfgItem.Key)\n\t\t}\n\t}\n\t//默认菜单初始化\n\tInitMenuData()\n\t//检查并添加缺失的菜单项(用于版本升级)\n\tCheckAndAddMenus()\n\n}\n\nfunc GetConfKey(v string) string {\n\t// 优先从 .env 中读取(便于开发环境配置敏感信息且不提交到 git)\n\tenvConfigOnce.Do(func() {\n\t\tfile, err := os.Open(\".env\")\n\t\tif err != nil {\n\t\t\t// 没有 .env 文件时直接跳过,后面回退到 app.conf\n\t\t\tenvFileExists = false\n\t\t\treturn\n\t\t}\n\t\tdefer file.Close()\n\n\t\tenvFileExists = true\n\t\tenvConfig = make(map[string]string)\n\n\t\tscanner := bufio.NewScanner(file)\n\t\tfor scanner.Scan() {\n\t\t\tline := strings.TrimSpace(scanner.Text())\n\t\t\tif line == \"\" || strings.HasPrefix(line, \"#\") || strings.HasPrefix(line, \";\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tparts := strings.SplitN(line, \"=\", 2)\n\t\t\tif len(parts) != 2 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tkey := strings.TrimSpace(parts[0])\n\t\t\tval := strings.TrimSpace(parts[1])\n\t\t\t// 去掉可能的引号\n\t\t\tval = strings.Trim(val, `\"'`)\n\t\t\tif key != \"\" {\n\t\t\t\tenvConfig[key] = val // 即使 val 为空也认为是有效配置\n\t\t\t}\n\t\t}\n\t})\n\n\t// 如果存在 .env 文件,则所有配置完全由 .env 决定:\n\t// - key 存在:返回对应值(可以是空字符串)\n\t// - key 不存在:返回空字符串\n\tif envFileExists {\n\t\tif envConfig == nil {\n\t\t\treturn \"\"\n\t\t}\n\t\tif val, ok := envConfig[v]; ok {\n\t\t\treturn val\n\t\t}\n\t\treturn \"\"\n\t}\n\t// 其次从 config/app.conf 读取\n\tcfg, err := ini.Load(\"./config/app.conf\")\n\tif err != nil {\n\t\tlogger.Log.Error(err)\n\t\treturn \"\"\n\t}\n\tp, err := cfg.Section(\"\").GetKey(v)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn p.String()\n}\n\n// IsPasswordConfigured 检查是否配置了密码(用于图形查看)\n// 优先检查数据库中是否有激活的 Zabbix 实例配置了用户名和密码\n// 注意:查看图形需要用户名和密码,Token 方式无法用于 Web 登录\nfunc IsPasswordConfigured() bool {\n\t// 1. 优先检查数据库中是否有激活的 Zabbix 实例\n\tvar activeInstance ZabbixInstance\n\terr := DB.Where(\"is_active = ?\", true).First(&activeInstance).Error\n\tif err == nil {\n\t\t// 找到激活的实例,检查是否配置了用户名和密码(查看图形必须用密码,Token 无法用于 Web 登录)\n\t\tif strings.TrimSpace(activeInstance.User) != \"\" && strings.TrimSpace(activeInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 2. 如果数据库中没有激活实例,检查是否有任何可用的实例配置了密码\n\tvar anyInstance ZabbixInstance\n\terr = DB.Where(\"enabled = ?\", true).First(&anyInstance).Error\n\tif err == nil {\n\t\t// 找到可用的实例,检查是否配置了用户名和密码\n\t\tif strings.TrimSpace(anyInstance.User) != \"\" && strings.TrimSpace(anyInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 3. 回退到配置文件检查(兼容旧版本)\n\tpass := GetConfKey(\"zabbix_pass\")\n\tuser := GetConfKey(\"zabbix_user\")\n\treturn pass != \"\" && user != \"\"\n}\n\n// GetEncryptionKey 获取加密密钥\n// 优先从数据库读取,如果数据库中没有则从环境变量读取,最后使用默认密钥\nfunc GetEncryptionKey() string {\n\t// 1. 优先从数据库读取\n\tkey := GetConfigValueByKey(\"encryption_key\", \"\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 2. 从环境变量或配置文件读取\n\tkey = GetConfKey(\"encryption_key\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 3. 使用默认密钥(不应该到这里,因为 DatabaseInit 会生成)\n\tlogger.Log.Warn(\"使用默认加密密钥,建议在数据库中配置 encryption_key\")\n\treturn \"zbxtable-default-encryption-key-2024\"\n}\n"}, "932d2f7e6967888f9611259b01c450a475aa1301": {"t": 1785754931.8143952, "n": 10327, "h": "7a6fe203c60335a3", "c": "package model\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\t\"zbxtable/pkg/logger\"\n\t\"zbxtable/pkg/utils\"\n\n\tzabbix \"github.com/canghai908/zabbix-go\"\n\t_ \"github.com/go-sql-driver/mysql\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t_ \"github.com/lib/pq\"\n\tworkwx \"github.com/xen0n/go-workwx\"\n\tini \"gopkg.in/ini.v1\"\n\t\"gorm.io/gorm\"\n)\n\nvar (\n\tAPI = &zabbix.API{}\n\tjson = jsoniter.Config{\n\t\tEscapeHTML: false,\n\t\tSortMapKeys: true,\n\t\tValidateJsonRawMessage: true,\n\t}.Froze()\n\tZBX_VER string\n\tZBX_V bool\n\tVersion string\n\tGitHash string\n\tBuildTime string\n\tWeApp = &workwx.WorkwxApp{}\n\n\tenvConfig map[string]string\n\tenvConfigOnce sync.Once\n\tenvFileExists bool\n)\n\nconst (\n\t//ms-agent 常量定义\n\tMSName = \"ms-agent\"\n\tMSUser = \"ms-agent\"\n\tMSGroup = \"MS-Agent\"\n\tMSMedia = \"MS-Agent\"\n\tMSAction = \"MS-Agent\"\n\t//webhook 常量定义\n\tWebhookName = \"ZbxTable\"\n\tWebhookAction = \"ZbxTable Webhook\"\n\tWebhookGroup = \"ZbxTable Webhook\"\n\tWebhookUser = \"zbxtable-webhook\"\n\t//down\n\tDownloadPath = \"./download/\"\n\tTplPath = \"./assets/templates/\"\n)\n\n// TableName 表名前缀\nfunc TableName(str string) string {\n\treturn fmt.Sprintf(\"%s%s\", \"zbxtable_\", str)\n}\n\n// 接收数据库信息初始化\nfunc ModelInit(dbtype, dbhost, dbuser, dbpass, dbname, dbport string) {\n\t// 直接使用 GORM\n\trunmode := GetConfKey(\"runmode\")\n\terr := InitGormDB(dbtype, dbhost, dbuser, dbpass, dbname, dbport, runmode)\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to connect database: \", err)\n\t\tos.Exit(1)\n\t}\n\tlogger.Log.Info(\"Database connected!\")\n\t// 自动迁移表\n\terr = AutoMigrate()\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to auto migrate: \", err)\n\t\tos.Exit(1)\n\t}\n\t// 基础数据初始化\n\tDatabaseInit()\n\t// 使用go-cache替代Redis(不再需要Redis连接)\n\tInitCache()\n}\n\nfunc InitWechat() {\n\t// 首先检查企业微信开关是否启用\n\twechatEnabled := GetConfigValueByKey(\"wechat_enabled\", \"0\")\n\tif wechatEnabled != \"1\" {\n\t\tlogger.Log.Info(\"WeChat is disabled in system config, skipping WeChat initialization\")\n\t\treturn\n\t}\n\t// 开关已启用,从数据库读取企业微信配置\n\tagentIDStr := GetConfigValueByKey(\"wechat_agentid\", \"\")\n\tcorpid := GetConfigValueByKey(\"wechat_corpid\", \"\")\n\tsecret := GetConfigValueByKey(\"wechat_secret\", \"\")\n\n\t// 验证必填配置项\n\tif agentIDStr == \"\" || corpid == \"\" || secret == \"\" {\n\t\tlogger.Log.Info(\"WeChat is enabled but configuration is incomplete (agentid/corpid/secret), WeChat app will not be initialized\")\n\t\treturn\n\t}\n\t// 解析 AgentID\n\tAgentId, err := strconv.ParseInt(agentIDStr, 10, 64)\n\tif err != nil {\n\t\tlogger.Log.Error(\"wechat_agentid parse error:\", err)\n\t\treturn\n\t}\n\n\t// 初始化企业微信客户端\n\tclient := workwx.New(corpid)\n\tWeApp = client.WithApp(secret, AgentId)\n\tWeApp.SpawnAccessTokenRefresher()\n\tlogger.Log.Info(\"WeChat inited successfully!\")\n}\n\n// DatabaseInit 数据初始化\nfunc DatabaseInit() {\n\t//数据初始化操作 - 使用 GORM\n\tvar v User\n\terr := DB.Where(\"username = ?\", \"admin\").First(&v).Error\n\t//检查权限\n\tif err == nil && v.Operation == \"\" {\n\t\terr := DB.Model(&User{}).Where(\"id = ?\", v.ID).Update(\"operation\", \"['add', 'edit', 'delete','update']\").Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"update admin operation successfully\")\n\t}\n\t//添加管理员账号\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\tlogger.Log.Info(\"the admin user does not exist, create a new admin account later!\")\n\t\tvar user User\n\t\tuser.Username = \"admin\"\n\t\tuser.Password, _ = utils.PasswordHash(\"Zbxtable\")\n\t\tuser.Avatar = \"https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif\"\n\t\tuser.Role = \"admin\"\n\t\tuser.Operation = \"['add', 'edit', 'delete','update']\"\n\t\tuser.Theme = `{\"theme\":{\"color\":\"#1890ff\",\"mode\":\"dark\",\"success\":\"#52c41a\",\"warning\":\"#faad14\",\"error\":\"#f5222f\"}}`\n\t\tuser.Status = 0\n\t\terr := DB.Create(&user).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"create an administrator account successfully, the admin ID is:\", user.ID)\n\t}\n\t//初始化系统数据\n\tvar cnt []System\n\terr = DB.Find(&cnt).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn\n\t}\n\tif len(cnt) == 0 {\n\t\tnow := time.Now()\n\t\tsys := []System{\n\t\t\t{Name: \"Linux操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"Windows操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"网络设备\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"物理服务器\", Status: 0, InitedAt: &now},\n\t\t}\n\t\terr := DB.Create(&sys).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init system info error!\", err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"Init system data successfully!\")\n\t}\n\t//告警默认规则初始化 default rule\n\tvar rules []Rule\n\terr = DB.Where(\"m_type = ?\", \"2\").Find(&rules).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn\n\t}\n\tif len(rules) == 0 {\n\t\t// 使用 \"*\" 作为全局默认规则,匹配所有租户\n\t\tdefaultRule := []Rule{\n\t\t\t{\n\t\t\t\tName: \"全局默认规则\",\n\t\t\t\tZIDs: \"*\",\n\t\t\t\tMType: \"2\",\n\t\t\t\tChannel: \"wechat_robot\",\n\t\t\t\tUserIds: \"1\",\n\t\t\t\tSweek: \"0,1,2,3,4,5,6\",\n\t\t\t\tStime: \"00:00\",\n\t\t\t\tEtime: \"23.59\",\n\t\t\t\tStatus: \"0\"},\n\t\t}\n\t\terr := DB.Create(&defaultRule).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init default rule error!\", err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"Init default rule successfully!\")\n\t}\n\t// 初始化加密密钥(如果不存在)\n\tvar encryptionKeyConfig Config\n\terr = DB.Where(\"`key` = ?\", \"encryption_key\").First(&encryptionKeyConfig).Error\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t// 生成随机加密密钥\n\t\trandomKey := utils.GenerateRandomKey()\n\t\tencryptionKeyConfig = Config{\n\t\t\tName: \"加密密钥\",\n\t\t\tKey: \"encryption_key\",\n\t\t\tValue: randomKey,\n\t\t\tComment: \"用于加密存储敏感信息(如Zabbix密码和Token)的密钥,系统自动生成,不可修改\",\n\t\t}\n\t\tif insertErr := DB.Create(&encryptionKeyConfig).Error; insertErr != nil {\n\t\t\tlogger.Log.Error(\"Init encryption_key error:\", insertErr)\n\t\t} else {\n\t\t\tlogger.Log.Info(\"Init encryption_key successfully! Key length:\", len(randomKey))\n\t\t}\n\t}\n\n\tdefaultConfigs := []Config{\n\t\t// 系统外观配置\n\t\t{Name: \"系统名称\", Key: \"system_name\", Value: \"ZbxTable\", Comment: \"系统显示的名称\"},\n\t\t{Name: \"系统Logo\", Key: \"system_logo\", Value: \"data:image/png;base64,\", Comment: \"系统Logo的base64编码数据\"},\n\t\t// Dashboard 相关\n\t\t{Name: \"数据面板\", Key: \"zbx_dash\", Value: \"0\", Comment: \"是否开启Zabbix看板:1 开启,0 关闭\"},\n\t\t{Name: \"面板配置\", Key: \"dash_id\", Value: \"1\", Comment: \"需要引入的Zabbix面板的ID,默认为1\"},\n\t\t{Name: \"主机分类同步\", Key: \"sync_inventory\", Value: \"1\", Comment: \"主机分类同步计划任务是否启用:1 启用,0 不启用\"},\n\t\t{Name: \"Webhook回调地址\", Key: \"webhook_url\", Value: \"\", Comment: \"webhook通知地址\"},\n\t\t// 邮件配置\n\t\t{Name: \"邮件发件人\", Key: \"email_from\", Value: \"\", Comment: \"告警邮件发件人邮箱地址\"},\n\t\t{Name: \"邮件昵称\", Key: \"email_nickname\", Value: \"ZbxTable\", Comment: \"告警邮件显示的发件人昵称\"},\n\t\t{Name: \"SMTP 密码/授权码\", Key: \"email_secret\", Value: \"\", Comment: \"SMTP 登录密码或授权码\"},\n\t\t{Name: \"SMTP 服务器\", Key: \"email_host\", Value: \"smtp.qq.com\", Comment: \"SMTP 服务器地址\"},\n\t\t{Name: \"SMTP 端口\", Key: \"email_port\", Value: \"465\", Comment: \"SMTP 端口号\"},\n\t\t{Name: \"SMTP 使用 SSL\", Key: \"email_isSSl\", Value: \"true\", Comment: \"是否启用 SSL:true/false\"},\n\t\t// 企业微信配置\n\t\t{Name: \"企业微信开关\", Key: \"wechat_enabled\", Value: \"0\", Comment: \"是否启用企业微信:1 启用,0 禁用\"},\n\t\t{Name: \"企业微信 AgentID\", Key: \"wechat_agentid\", Value: \"\", Comment: \"企业微信应用的 AgentID\"},\n\t\t{Name: \"企业微信 CorpID\", Key: \"wechat_corpid\", Value: \"\", Comment: \"企业微信企业ID\"},\n\t\t{Name: \"企业微信 Secret\", Key: \"wechat_secret\", Value: \"\", Comment: \"企业微信应用的 Secret\"},\n\t\t// Ollama 配置\n\t\t{Name: \"Ollama Host\", Key: \"ollama_host\", Value: \"http://localhost:11434\", Comment: \"Ollama 服务地址,如 http://127.0.0.1:11434\"},\n\t\t{Name: \"Ollama Model\", Key: \"ollama_model\", Value: \"deepseek-r1:32b\", Comment: \"默认使用的大模型名称\"},\n\t}\n\n\tfor _, cfgItem := range defaultConfigs {\n\t\tvar existing Config\n\t\terr = DB.Where(\"`key` = ?\", cfgItem.Key).First(&existing).Error\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\tif insertErr := DB.Create(&cfgItem).Error; insertErr != nil {\n\t\t\t\tlogger.Log.Infof(\"Init config key %s error: %v\", cfgItem.Key, insertErr)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tlogger.Log.Infof(\"Init config key %s successfully!\", cfgItem.Key)\n\t\t}\n\t}\n\t//默认菜单初始化\n\tInitMenuData()\n\t//检查并添加缺失的菜单项(用于版本升级)\n\tCheckAndAddMenus()\n\n}\n\nfunc GetConfKey(v string) string {\n\t// 优先从 .env 中读取(便于开发环境配置敏感信息且不提交到 git)\n\tenvConfigOnce.Do(func() {\n\t\tfile, err := os.Open(\".env\")\n\t\tif err != nil {\n\t\t\t// 没有 .env 文件时直接跳过,后面回退到 app.conf\n\t\t\tenvFileExists = false\n\t\t\treturn\n\t\t}\n\t\tdefer file.Close()\n\n\t\tenvFileExists = true\n\t\tenvConfig = make(map[string]string)\n\n\t\tscanner := bufio.NewScanner(file)\n\t\tfor scanner.Scan() {\n\t\t\tline := strings.TrimSpace(scanner.Text())\n\t\t\tif line == \"\" || strings.HasPrefix(line, \"#\") || strings.HasPrefix(line, \";\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tparts := strings.SplitN(line, \"=\", 2)\n\t\t\tif len(parts) != 2 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tkey := strings.TrimSpace(parts[0])\n\t\t\tval := strings.TrimSpace(parts[1])\n\t\t\t// 去掉可能的引号\n\t\t\tval = strings.Trim(val, `\"'`)\n\t\t\tif key != \"\" {\n\t\t\t\tenvConfig[key] = val // 即使 val 为空也认为是有效配置\n\t\t\t}\n\t\t}\n\t})\n\n\t// 如果存在 .env 文件,则所有配置完全由 .env 决定:\n\t// - key 存在:返回对应值(可以是空字符串)\n\t// - key 不存在:返回空字符串\n\tif envFileExists {\n\t\tif envConfig == nil {\n\t\t\treturn \"\"\n\t\t}\n\t\tif val, ok := envConfig[v]; ok {\n\t\t\treturn val\n\t\t}\n\t\treturn \"\"\n\t}\n\t// 其次从 config/app.conf 读取\n\tcfg, err := ini.Load(\"./config/app.conf\")\n\tif err != nil {\n\t\tlogger.Log.Error(err)\n\t\treturn \"\"\n\t}\n\tp, err := cfg.Section(\"\").GetKey(v)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn p.String()\n}\n\n// IsPasswordConfigured 检查是否配置了密码(用于图形查看)\n// 优先检查数据库中是否有激活的 Zabbix 实例配置了用户名和密码\n// 注意:查看图形需要用户名和密码,Token 方式无法用于 Web 登录\nfunc IsPasswordConfigured() bool {\n\t// 1. 优先检查数据库中是否有激活的 Zabbix 实例\n\tvar activeInstance ZabbixInstance\n\terr := DB.Where(\"is_active = ?\", true).First(&activeInstance).Error\n\tif err == nil {\n\t\t// 找到激活的实例,检查是否配置了用户名和密码(查看图形必须用密码,Token 无法用于 Web 登录)\n\t\tif strings.TrimSpace(activeInstance.User) != \"\" && strings.TrimSpace(activeInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 2. 如果数据库中没有激活实例,检查是否有任何可用的实例配置了密码\n\tvar anyInstance ZabbixInstance\n\terr = DB.Where(\"enabled = ?\", true).First(&anyInstance).Error\n\tif err == nil {\n\t\t// 找到可用的实例,检查是否配置了用户名和密码\n\t\tif strings.TrimSpace(anyInstance.User) != \"\" && strings.TrimSpace(anyInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 3. 回退到配置文件检查(兼容旧版本)\n\tpass := GetConfKey(\"zabbix_pass\")\n\tuser := GetConfKey(\"zabbix_user\")\n\treturn pass != \"\" && user != \"\"\n}\n\n// GetEncryptionKey 获取加密密钥\n// 优先从数据库读取,如果数据库中没有则从环境变量读取,最后使用默认密钥\nfunc GetEncryptionKey() string {\n\t// 1. 优先从数据库读取\n\tkey := GetConfigValueByKey(\"encryption_key\", \"\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 2. 从环境变量或配置文件读取\n\tkey = GetConfKey(\"encryption_key\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 3. 使用默认密钥(不应该到这里,因为 DatabaseInit 会生成)\n\tlogger.Log.Warn(\"使用默认加密密钥,建议在数据库中配置 encryption_key\")\n\treturn \"zbxtable-default-encryption-key-2024\"\n}\n"}, "bac0a296ac6049722895c4d34f1722729a1d89c5": {"t": 1785754933.0243313, "n": 10135, "h": "90aaea648ca3615b", "c": "package model\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\t\"zbxtable/pkg/logger\"\n\t\"zbxtable/pkg/utils\"\n\n\tzabbix \"github.com/canghai908/zabbix-go\"\n\t_ \"github.com/go-sql-driver/mysql\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t_ \"github.com/lib/pq\"\n\tworkwx \"github.com/xen0n/go-workwx\"\n\tini \"gopkg.in/ini.v1\"\n\t\"gorm.io/gorm\"\n)\n\nvar (\n\tAPI = &zabbix.API{}\n\tjson = jsoniter.Config{\n\t\tEscapeHTML: false,\n\t\tSortMapKeys: true,\n\t\tValidateJsonRawMessage: true,\n\t}.Froze()\n\tZBX_VER string\n\tZBX_V bool\n\tVersion string\n\tGitHash string\n\tBuildTime string\n\tWeApp = &workwx.WorkwxApp{}\n\n\tenvConfig map[string]string\n\tenvConfigOnce sync.Once\n\tenvFileExists bool\n)\n\nconst (\n\t//ms-agent 常量定义\n\tMSName = \"ms-agent\"\n\tMSUser = \"ms-agent\"\n\tMSGroup = \"MS-Agent\"\n\tMSMedia = \"MS-Agent\"\n\tMSAction = \"MS-Agent\"\n\t//webhook 常量定义\n\tWebhookName = \"ZbxTable\"\n\tWebhookAction = \"ZbxTable Webhook\"\n\tWebhookGroup = \"ZbxTable Webhook\"\n\tWebhookUser = \"zbxtable-webhook\"\n\t//down\n\tDownloadPath = \"./download/\"\n\tTplPath = \"./assets/templates/\"\n)\n\n// TableName 表名前缀\nfunc TableName(str string) string {\n\treturn fmt.Sprintf(\"%s%s\", \"zbxtable_\", str)\n}\n\n// 接收数据库信息初始化\nfunc ModelInit(dbtype, dbhost, dbuser, dbpass, dbname, dbport string) {\n\t// 直接使用 GORM\n\trunmode := GetConfKey(\"runmode\")\n\terr := InitGormDB(dbtype, dbhost, dbuser, dbpass, dbname, dbport, runmode)\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to connect database: \", err)\n\t\tos.Exit(1)\n\t}\n\tlogger.Log.Info(\"Database connected!\")\n\t// 自动迁移表\n\terr = AutoMigrate()\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to auto migrate: \", err)\n\t\tos.Exit(1)\n\t}\n\t// 基础数据初始化\n\tDatabaseInit()\n\t// 使用go-cache替代Redis(不再需要Redis连接)\n\tInitCache()\n}\n\nfunc InitWechat() {\n\t// 首先检查企业微信开关是否启用\n\twechatEnabled := GetConfigValueByKey(\"wechat_enabled\", \"0\")\n\tif wechatEnabled != \"1\" {\n\t\tlogger.Log.Info(\"WeChat is disabled in system config, skipping WeChat initialization\")\n\t\treturn\n\t}\n\t// 开关已启用,从数据库读取企业微信配置\n\tagentIDStr := GetConfigValueByKey(\"wechat_agentid\", \"\")\n\tcorpid := GetConfigValueByKey(\"wechat_corpid\", \"\")\n\tsecret := GetConfigValueByKey(\"wechat_secret\", \"\")\n\n\t// 验证必填配置项\n\tif agentIDStr == \"\" || corpid == \"\" || secret == \"\" {\n\t\tlogger.Log.Info(\"WeChat is enabled but configuration is incomplete (agentid/corpid/secret), WeChat app will not be initialized\")\n\t\treturn\n\t}\n\t// 解析 AgentID\n\tAgentId, err := strconv.ParseInt(agentIDStr, 10, 64)\n\tif err != nil {\n\t\tlogger.Log.Error(\"wechat_agentid parse error:\", err)\n\t\treturn\n\t}\n\n\t// 初始化企业微信客户端\n\tclient := workwx.New(corpid)\n\tWeApp = client.WithApp(secret, AgentId)\n\tWeApp.SpawnAccessTokenRefresher()\n\tlogger.Log.Info(\"WeChat inited successfully!\")\n}\n\n// DatabaseInit 数据初始化\nfunc DatabaseInit() {\n\t//数据初始化操作 - 使用 GORM\n\tvar v User\n\terr := DB.Where(\"username = ?\", \"admin\").First(&v).Error\n\t//检查权限\n\tif err == nil && v.Operation == \"\" {\n\t\terr := DB.Model(&User{}).Where(\"id = ?\", v.ID).Update(\"operation\", \"['add', 'edit', 'delete','update']\").Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"update admin operation successfully\")\n\t}\n\t//添加管理员账号\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\tlogger.Log.Info(\"the admin user does not exist, create a new admin account later!\")\n\t\tvar user User\n\t\tuser.Username = \"admin\"\n\t\tuser.Password, _ = utils.PasswordHash(\"Zbxtable\")\n\t\tuser.Avatar = \"https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif\"\n\t\tuser.Role = \"admin\"\n\t\tuser.Operation = \"['add', 'edit', 'delete','update']\"\n\t\tuser.Theme = `{\"theme\":{\"color\":\"#1890ff\",\"mode\":\"dark\",\"success\":\"#52c41a\",\"warning\":\"#faad14\",\"error\":\"#f5222f\"}}`\n\t\tuser.Status = 0\n\t\terr := DB.Create(&user).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"create an administrator account successfully, the admin ID is:\", user.ID)\n\t}\n\t//初始化系统数据\n\tvar cnt []System\n\terr = DB.Find(&cnt).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn\n\t}\n\tif len(cnt) == 0 {\n\t\tnow := time.Now()\n\t\tsys := []System{\n\t\t\t{Name: \"Linux操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"Windows操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"网络设备\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"物理服务器\", Status: 0, InitedAt: &now},\n\t\t}\n\t\terr := DB.Create(&sys).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init system info error!\", err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"Init system data successfully!\")\n\t}\n\t//告警默认规则初始化 default rule\n\tvar rules []Rule\n\terr = DB.Where(\"m_type = ?\", \"2\").Find(&rules).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn\n\t}\n\tif len(rules) == 0 {\n\t\t// 使用 \"*\" 作为全局默认规则,匹配所有租户\n\t\tdefaultRule := []Rule{\n\t\t\t{\n\t\t\t\tName: \"全局默认规则\",\n\t\t\t\tZIDs: \"*\",\n\t\t\t\tMType: \"2\",\n\t\t\t\tChannel: \"wechat_robot\",\n\t\t\t\tUserIds: \"1\",\n\t\t\t\tSweek: \"0,1,2,3,4,5,6\",\n\t\t\t\tStime: \"00:00\",\n\t\t\t\tEtime: \"23.59\",\n\t\t\t\tStatus: \"0\"},\n\t\t}\n\t\terr := DB.Create(&defaultRule).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init default rule error!\", err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"Init default rule successfully!\")\n\t}\n\t// 初始化加密密钥(如果不存在)\n\tvar encryptionKeyConfig Config\n\terr = DB.Where(\"`key` = ?\", \"encryption_key\").First(&encryptionKeyConfig).Error\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t// 生成随机加密密钥\n\t\trandomKey := utils.GenerateRandomKey()\n\t\tencryptionKeyConfig = Config{\n\t\t\tName: \"加密密钥\",\n\t\t\tKey: \"encryption_key\",\n\t\t\tValue: randomKey,\n\t\t\tComment: \"用于加密存储敏感信息(如Zabbix密码和Token)的密钥,系统自动生成,不可修改\",\n\t\t}\n\t\tif insertErr := DB.Create(&encryptionKeyConfig).Error; insertErr != nil {\n\t\t\tlogger.Log.Error(\"Init encryption_key error:\", insertErr)\n\t\t} else {\n\t\t\tlogger.Log.Info(\"Init encryption_key successfully! Key length:\", len(randomKey))\n\t\t}\n\t}\n\n\tdefaultConfigs := []Config{\n\t\t// Dashboard 相关\n\t\t{Name: \"数据面板\", Key: \"zbx_dash\", Value: \"0\", Comment: \"是否开启Zabbix看板:1 开启,0 关闭\"},\n\t\t{Name: \"面板配置\", Key: \"dash_id\", Value: \"1\", Comment: \"需要引入的Zabbix面板的ID,默认为1\"},\n\t\t{Name: \"主机分类同步\", Key: \"sync_inventory\", Value: \"1\", Comment: \"主机分类同步计划任务是否启用:1 启用,0 不启用\"},\n\t\t{Name: \"Webhook回调地址\", Key: \"webhook_url\", Value: \"\", Comment: \"webhook通知地址\"},\n\t\t// 邮件配置\n\t\t{Name: \"邮件发件人\", Key: \"email_from\", Value: \"\", Comment: \"告警邮件发件人邮箱地址\"},\n\t\t{Name: \"邮件昵称\", Key: \"email_nickname\", Value: \"ZbxTable\", Comment: \"告警邮件显示的发件人昵称\"},\n\t\t{Name: \"SMTP 密码/授权码\", Key: \"email_secret\", Value: \"\", Comment: \"SMTP 登录密码或授权码\"},\n\t\t{Name: \"SMTP 服务器\", Key: \"email_host\", Value: \"smtp.qq.com\", Comment: \"SMTP 服务器地址\"},\n\t\t{Name: \"SMTP 端口\", Key: \"email_port\", Value: \"465\", Comment: \"SMTP 端口号\"},\n\t\t{Name: \"SMTP 使用 SSL\", Key: \"email_isSSl\", Value: \"true\", Comment: \"是否启用 SSL:true/false\"},\n\t\t// 企业微信配置\n\t\t{Name: \"企业微信开关\", Key: \"wechat_enabled\", Value: \"0\", Comment: \"是否启用企业微信:1 启用,0 禁用\"},\n\t\t{Name: \"企业微信 AgentID\", Key: \"wechat_agentid\", Value: \"\", Comment: \"企业微信应用的 AgentID\"},\n\t\t{Name: \"企业微信 CorpID\", Key: \"wechat_corpid\", Value: \"\", Comment: \"企业微信企业ID\"},\n\t\t{Name: \"企业微信 Secret\", Key: \"wechat_secret\", Value: \"\", Comment: \"企业微信应用的 Secret\"},\n\t\t// Ollama 配置\n\t\t{Name: \"Ollama Host\", Key: \"ollama_host\", Value: \"http://localhost:11434\", Comment: \"Ollama 服务地址,如 http://127.0.0.1:11434\"},\n\t\t{Name: \"Ollama Model\", Key: \"ollama_model\", Value: \"deepseek-r1:32b\", Comment: \"默认使用的大模型名称\"},\n\t}\n\n\tfor _, cfgItem := range defaultConfigs {\n\t\tvar existing Config\n\t\terr = DB.Where(\"`key` = ?\", cfgItem.Key).First(&existing).Error\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\tif insertErr := DB.Create(&cfgItem).Error; insertErr != nil {\n\t\t\t\tlogger.Log.Infof(\"Init config key %s error: %v\", cfgItem.Key, insertErr)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tlogger.Log.Infof(\"Init config key %s successfully!\", cfgItem.Key)\n\t\t}\n\t}\n\t//默认菜单初始化\n\tInitMenuData()\n\t//检查并添加缺失的菜单项(用于版本升级)\n\tCheckAndAddMenus()\n\n}\n\nfunc GetConfKey(v string) string {\n\t// 优先从 .env 中读取(便于开发环境配置敏感信息且不提交到 git)\n\tenvConfigOnce.Do(func() {\n\t\tfile, err := os.Open(\".env\")\n\t\tif err != nil {\n\t\t\t// 没有 .env 文件时直接跳过,后面回退到 app.conf\n\t\t\tenvFileExists = false\n\t\t\treturn\n\t\t}\n\t\tdefer file.Close()\n\n\t\tenvFileExists = true\n\t\tenvConfig = make(map[string]string)\n\n\t\tscanner := bufio.NewScanner(file)\n\t\tfor scanner.Scan() {\n\t\t\tline := strings.TrimSpace(scanner.Text())\n\t\t\tif line == \"\" || strings.HasPrefix(line, \"#\") || strings.HasPrefix(line, \";\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tparts := strings.SplitN(line, \"=\", 2)\n\t\t\tif len(parts) != 2 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tkey := strings.TrimSpace(parts[0])\n\t\t\tval := strings.TrimSpace(parts[1])\n\t\t\t// 去掉可能的引号\n\t\t\tval = strings.Trim(val, `\"'`)\n\t\t\tif key != \"\" {\n\t\t\t\tenvConfig[key] = val // 即使 val 为空也认为是有效配置\n\t\t\t}\n\t\t}\n\t})\n\n\t// 如果存在 .env 文件,则所有配置完全由 .env 决定:\n\t// - key 存在:返回对应值(可以是空字符串)\n\t// - key 不存在:返回空字符串\n\tif envFileExists {\n\t\tif envConfig == nil {\n\t\t\treturn \"\"\n\t\t}\n\t\tif val, ok := envConfig[v]; ok {\n\t\t\treturn val\n\t\t}\n\t\treturn \"\"\n\t}\n\t// 其次从 config/app.conf 读取\n\tcfg, err := ini.Load(\"./config/app.conf\")\n\tif err != nil {\n\t\tlogger.Log.Error(err)\n\t\treturn \"\"\n\t}\n\tp, err := cfg.Section(\"\").GetKey(v)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn p.String()\n}\n\n// IsPasswordConfigured 检查是否配置了密码(用于图形查看)\n// 优先检查数据库中是否有激活的 Zabbix 实例配置了用户名和密码\n// 注意:查看图形需要用户名和密码,Token 方式无法用于 Web 登录\nfunc IsPasswordConfigured() bool {\n\t// 1. 优先检查数据库中是否有激活的 Zabbix 实例\n\tvar activeInstance ZabbixInstance\n\terr := DB.Where(\"is_active = ?\", true).First(&activeInstance).Error\n\tif err == nil {\n\t\t// 找到激活的实例,检查是否配置了用户名和密码(查看图形必须用密码,Token 无法用于 Web 登录)\n\t\tif strings.TrimSpace(activeInstance.User) != \"\" && strings.TrimSpace(activeInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 2. 如果数据库中没有激活实例,检查是否有任何可用的实例配置了密码\n\tvar anyInstance ZabbixInstance\n\terr = DB.Where(\"enabled = ?\", true).First(&anyInstance).Error\n\tif err == nil {\n\t\t// 找到可用的实例,检查是否配置了用户名和密码\n\t\tif strings.TrimSpace(anyInstance.User) != \"\" && strings.TrimSpace(anyInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 3. 回退到配置文件检查(兼容旧版本)\n\tpass := GetConfKey(\"zabbix_pass\")\n\tuser := GetConfKey(\"zabbix_user\")\n\treturn pass != \"\" && user != \"\"\n}\n\n// GetEncryptionKey 获取加密密钥\n// 优先从数据库读取,如果数据库中没有则从环境变量读取,最后使用默认密钥\nfunc GetEncryptionKey() string {\n\t// 1. 优先从数据库读取\n\tkey := GetConfigValueByKey(\"encryption_key\", \"\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 2. 从环境变量或配置文件读取\n\tkey = GetConfKey(\"encryption_key\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 3. 使用默认密钥(不应该到这里,因为 DatabaseInit 会生成)\n\tlogger.Log.Warn(\"使用默认加密密钥,建议在数据库中配置 encryption_key\")\n\treturn \"zbxtable-default-encryption-key-2024\"\n}\n"}, "dfb2b531347b8c1a40b411438d82859619206e2e": {"t": 1785754934.660316, "n": 10135, "h": "6f219742aa547752", "c": "package model\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\t\"zbxtable/pkg/logger\"\n\t\"zbxtable/pkg/utils\"\n\n\tzabbix \"github.com/canghai908/zabbix-go\"\n\t_ \"github.com/go-sql-driver/mysql\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t_ \"github.com/lib/pq\"\n\tworkwx \"github.com/xen0n/go-workwx\"\n\tini \"gopkg.in/ini.v1\"\n\t\"gorm.io/gorm\"\n)\n\nvar (\n\tAPI = &zabbix.API{}\n\tjson = jsoniter.Config{\n\t\tEscapeHTML: false,\n\t\tSortMapKeys: true,\n\t\tValidateJsonRawMessage: true,\n\t}.Froze()\n\tZBX_VER string\n\tZBX_V bool\n\tVersion string\n\tGitHash string\n\tBuildTime string\n\tWeApp = &workwx.WorkwxApp{}\n\n\tenvConfig map[string]string\n\tenvConfigOnce sync.Once\n\tenvFileExists bool\n)\n\nconst (\n\t//ms-agent 常量定义\n\tMSName = \"ms-agent\"\n\tMSUser = \"ms-agent\"\n\tMSGroup = \"MS-Agent\"\n\tMSMedia = \"MS-Agent\"\n\tMSAction = \"MS-Agent\"\n\t//webhook 常量定义\n\tWebhookName = \"ZbxTable\"\n\tWebhookAction = \"ZbxTable Webhook\"\n\tWebhookGroup = \"ZbxTable Webhook\"\n\tWebhookUser = \"zbxtable-webhook\"\n\t//down\n\tDownloadPath = \"./download/\"\n\tTplPath = \"./assets/templates/\"\n)\n\n// TableName 表名前缀\nfunc TableName(str string) string {\n\treturn fmt.Sprintf(\"%s%s\", \"zbxtable_\", str)\n}\n\n// 接收数据库信息初始化\nfunc ModelInit(dbtype, dbhost, dbuser, dbpass, dbname, dbport string) {\n\t// 直接使用 GORM\n\trunmode := GetConfKey(\"runmode\")\n\terr := InitGormDB(dbtype, dbhost, dbuser, dbpass, dbname, dbport, runmode)\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to connect database: \", err)\n\t\tos.Exit(1)\n\t}\n\tlogger.Log.Info(\"Database connected!\")\n\t// 自动迁移表\n\terr = AutoMigrate()\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to auto migrate: \", err)\n\t\tos.Exit(1)\n\t}\n\t// 基础数据初始化\n\tDatabaseInit()\n\t// 使用go-cache替代Redis(不再需要Redis连接)\n\tInitCache()\n}\n\nfunc InitWechat() {\n\t// 首先检查企业微信开关是否启用\n\twechatEnabled := GetConfigValueByKey(\"wechat_enabled\", \"0\")\n\tif wechatEnabled != \"1\" {\n\t\tlogger.Log.Info(\"WeChat is disabled in system config, skipping WeChat initialization\")\n\t\treturn\n\t}\n\t// 开关已启用,从数据库读取企业微信配置\n\tagentIDStr := GetConfigValueByKey(\"wechat_agentid\", \"\")\n\tcorpid := GetConfigValueByKey(\"wechat_corpid\", \"\")\n\tsecret := GetConfigValueByKey(\"wechat_secret\", \"\")\n\n\t// 验证必填配置项\n\tif agentIDStr == \"\" || corpid == \"\" || secret == \"\" {\n\t\tlogger.Log.Info(\"WeChat is enabled but configuration is incomplete (agentid/corpid/secret), WeChat app will not be initialized\")\n\t\treturn\n\t}\n\t// 解析 AgentID\n\tAgentId, err := strconv.ParseInt(agentIDStr, 10, 64)\n\tif err != nil {\n\t\tlogger.Log.Error(\"wechat_agentid parse error:\", err)\n\t\treturn\n\t}\n\n\t// 初始化企业微信客户端\n\tclient := workwx.New(corpid)\n\tWeApp = client.WithApp(secret, AgentId)\n\tWeApp.SpawnAccessTokenRefresher()\n\tlogger.Log.Info(\"WeChat inited successfully!\")\n}\n\n// DatabaseInit 数据初始化\nfunc DatabaseInit() {\n\t//数据初始化操作 - 使用 GORM\n\tvar v User\n\terr := DB.Where(\"username = ?\", \"admin\").First(&v).Error\n\t//检查权限\n\tif err == nil && v.Operation == \"\" {\n\t\terr := DB.Model(&User{}).Where(\"id = ?\", v.ID).Update(\"operation\", \"['add', 'edit', 'delete','update']\").Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"update admin operation successfully\")\n\t}\n\t//添加管理员账号\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\tlogger.Log.Info(\"the admin user does not exist, create a new admin account later!\")\n\t\tvar user User\n\t\tuser.Username = \"admin\"\n\t\tuser.Password, _ = utils.PasswordHash(\"Zbxtable\")\n\t\tuser.Avatar = \"https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif\"\n\t\tuser.Role = \"admin\"\n\t\tuser.Operation = \"['add', 'edit', 'delete','update']\"\n\t\tuser.Theme = `{\"theme\":{\"color\":\"#13c2c2\",\"mode\":\"dark\",\"success\":\"#52c41a\",\"warning\":\"#faad14\",\"error\":\"#f5222f\"}}`\n\t\tuser.Status = 0\n\t\terr := DB.Create(&user).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"create an administrator account successfully, the admin ID is:\", user.ID)\n\t}\n\t//初始化系统数据\n\tvar cnt []System\n\terr = DB.Find(&cnt).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn\n\t}\n\tif len(cnt) == 0 {\n\t\tnow := time.Now()\n\t\tsys := []System{\n\t\t\t{Name: \"Linux操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"Windows操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"网络设备\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"物理服务器\", Status: 0, InitedAt: &now},\n\t\t}\n\t\terr := DB.Create(&sys).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init system info error!\", err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"Init system data successfully!\")\n\t}\n\t//告警默认规则初始化 default rule\n\tvar rules []Rule\n\terr = DB.Where(\"m_type = ?\", \"2\").Find(&rules).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn\n\t}\n\tif len(rules) == 0 {\n\t\t// 使用 \"*\" 作为全局默认规则,匹配所有租户\n\t\tdefaultRule := []Rule{\n\t\t\t{\n\t\t\t\tName: \"全局默认规则\",\n\t\t\t\tZIDs: \"*\",\n\t\t\t\tMType: \"2\",\n\t\t\t\tChannel: \"wechat_robot\",\n\t\t\t\tUserIds: \"1\",\n\t\t\t\tSweek: \"0,1,2,3,4,5,6\",\n\t\t\t\tStime: \"00:00\",\n\t\t\t\tEtime: \"23.59\",\n\t\t\t\tStatus: \"0\"},\n\t\t}\n\t\terr := DB.Create(&defaultRule).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init default rule error!\", err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"Init default rule successfully!\")\n\t}\n\t// 初始化加密密钥(如果不存在)\n\tvar encryptionKeyConfig Config\n\terr = DB.Where(\"`key` = ?\", \"encryption_key\").First(&encryptionKeyConfig).Error\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t// 生成随机加密密钥\n\t\trandomKey := utils.GenerateRandomKey()\n\t\tencryptionKeyConfig = Config{\n\t\t\tName: \"加密密钥\",\n\t\t\tKey: \"encryption_key\",\n\t\t\tValue: randomKey,\n\t\t\tComment: \"用于加密存储敏感信息(如Zabbix密码和Token)的密钥,系统自动生成,不可修改\",\n\t\t}\n\t\tif insertErr := DB.Create(&encryptionKeyConfig).Error; insertErr != nil {\n\t\t\tlogger.Log.Error(\"Init encryption_key error:\", insertErr)\n\t\t} else {\n\t\t\tlogger.Log.Info(\"Init encryption_key successfully! Key length:\", len(randomKey))\n\t\t}\n\t}\n\n\tdefaultConfigs := []Config{\n\t\t// Dashboard 相关\n\t\t{Name: \"数据面板\", Key: \"zbx_dash\", Value: \"0\", Comment: \"是否开启Zabbix看板:1 开启,0 关闭\"},\n\t\t{Name: \"面板配置\", Key: \"dash_id\", Value: \"1\", Comment: \"需要引入的Zabbix面板的ID,默认为1\"},\n\t\t{Name: \"主机分类同步\", Key: \"sync_inventory\", Value: \"1\", Comment: \"主机分类同步计划任务是否启用:1 启用,0 不启用\"},\n\t\t{Name: \"Webhook回调地址\", Key: \"webhook_url\", Value: \"\", Comment: \"webhook通知地址\"},\n\t\t// 邮件配置\n\t\t{Name: \"邮件发件人\", Key: \"email_from\", Value: \"\", Comment: \"告警邮件发件人邮箱地址\"},\n\t\t{Name: \"邮件昵称\", Key: \"email_nickname\", Value: \"ZbxTable\", Comment: \"告警邮件显示的发件人昵称\"},\n\t\t{Name: \"SMTP 密码/授权码\", Key: \"email_secret\", Value: \"\", Comment: \"SMTP 登录密码或授权码\"},\n\t\t{Name: \"SMTP 服务器\", Key: \"email_host\", Value: \"smtp.qq.com\", Comment: \"SMTP 服务器地址\"},\n\t\t{Name: \"SMTP 端口\", Key: \"email_port\", Value: \"465\", Comment: \"SMTP 端口号\"},\n\t\t{Name: \"SMTP 使用 SSL\", Key: \"email_isSSl\", Value: \"true\", Comment: \"是否启用 SSL:true/false\"},\n\t\t// 企业微信配置\n\t\t{Name: \"企业微信开关\", Key: \"wechat_enabled\", Value: \"0\", Comment: \"是否启用企业微信:1 启用,0 禁用\"},\n\t\t{Name: \"企业微信 AgentID\", Key: \"wechat_agentid\", Value: \"\", Comment: \"企业微信应用的 AgentID\"},\n\t\t{Name: \"企业微信 CorpID\", Key: \"wechat_corpid\", Value: \"\", Comment: \"企业微信企业ID\"},\n\t\t{Name: \"企业微信 Secret\", Key: \"wechat_secret\", Value: \"\", Comment: \"企业微信应用的 Secret\"},\n\t\t// Ollama 配置\n\t\t{Name: \"Ollama Host\", Key: \"ollama_host\", Value: \"http://localhost:11434\", Comment: \"Ollama 服务地址,如 http://127.0.0.1:11434\"},\n\t\t{Name: \"Ollama Model\", Key: \"ollama_model\", Value: \"deepseek-r1:32b\", Comment: \"默认使用的大模型名称\"},\n\t}\n\n\tfor _, cfgItem := range defaultConfigs {\n\t\tvar existing Config\n\t\terr = DB.Where(\"`key` = ?\", cfgItem.Key).First(&existing).Error\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\tif insertErr := DB.Create(&cfgItem).Error; insertErr != nil {\n\t\t\t\tlogger.Log.Infof(\"Init config key %s error: %v\", cfgItem.Key, insertErr)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tlogger.Log.Infof(\"Init config key %s successfully!\", cfgItem.Key)\n\t\t}\n\t}\n\t//默认菜单初始化\n\tInitMenuData()\n\t//检查并添加缺失的菜单项(用于版本升级)\n\tCheckAndAddMenus()\n\n}\n\nfunc GetConfKey(v string) string {\n\t// 优先从 .env 中读取(便于开发环境配置敏感信息且不提交到 git)\n\tenvConfigOnce.Do(func() {\n\t\tfile, err := os.Open(\".env\")\n\t\tif err != nil {\n\t\t\t// 没有 .env 文件时直接跳过,后面回退到 app.conf\n\t\t\tenvFileExists = false\n\t\t\treturn\n\t\t}\n\t\tdefer file.Close()\n\n\t\tenvFileExists = true\n\t\tenvConfig = make(map[string]string)\n\n\t\tscanner := bufio.NewScanner(file)\n\t\tfor scanner.Scan() {\n\t\t\tline := strings.TrimSpace(scanner.Text())\n\t\t\tif line == \"\" || strings.HasPrefix(line, \"#\") || strings.HasPrefix(line, \";\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tparts := strings.SplitN(line, \"=\", 2)\n\t\t\tif len(parts) != 2 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tkey := strings.TrimSpace(parts[0])\n\t\t\tval := strings.TrimSpace(parts[1])\n\t\t\t// 去掉可能的引号\n\t\t\tval = strings.Trim(val, `\"'`)\n\t\t\tif key != \"\" {\n\t\t\t\tenvConfig[key] = val // 即使 val 为空也认为是有效配置\n\t\t\t}\n\t\t}\n\t})\n\n\t// 如果存在 .env 文件,则所有配置完全由 .env 决定:\n\t// - key 存在:返回对应值(可以是空字符串)\n\t// - key 不存在:返回空字符串\n\tif envFileExists {\n\t\tif envConfig == nil {\n\t\t\treturn \"\"\n\t\t}\n\t\tif val, ok := envConfig[v]; ok {\n\t\t\treturn val\n\t\t}\n\t\treturn \"\"\n\t}\n\t// 其次从 config/app.conf 读取\n\tcfg, err := ini.Load(\"./config/app.conf\")\n\tif err != nil {\n\t\tlogger.Log.Error(err)\n\t\treturn \"\"\n\t}\n\tp, err := cfg.Section(\"\").GetKey(v)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn p.String()\n}\n\n// IsPasswordConfigured 检查是否配置了密码(用于图形查看)\n// 优先检查数据库中是否有激活的 Zabbix 实例配置了用户名和密码\n// 注意:查看图形需要用户名和密码,Token 方式无法用于 Web 登录\nfunc IsPasswordConfigured() bool {\n\t// 1. 优先检查数据库中是否有激活的 Zabbix 实例\n\tvar activeInstance ZabbixInstance\n\terr := DB.Where(\"is_active = ?\", true).First(&activeInstance).Error\n\tif err == nil {\n\t\t// 找到激活的实例,检查是否配置了用户名和密码(查看图形必须用密码,Token 无法用于 Web 登录)\n\t\tif strings.TrimSpace(activeInstance.User) != \"\" && strings.TrimSpace(activeInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 2. 如果数据库中没有激活实例,检查是否有任何可用的实例配置了密码\n\tvar anyInstance ZabbixInstance\n\terr = DB.Where(\"enabled = ?\", true).First(&anyInstance).Error\n\tif err == nil {\n\t\t// 找到可用的实例,检查是否配置了用户名和密码\n\t\tif strings.TrimSpace(anyInstance.User) != \"\" && strings.TrimSpace(anyInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 3. 回退到配置文件检查(兼容旧版本)\n\tpass := GetConfKey(\"zabbix_pass\")\n\tuser := GetConfKey(\"zabbix_user\")\n\treturn pass != \"\" && user != \"\"\n}\n\n// GetEncryptionKey 获取加密密钥\n// 优先从数据库读取,如果数据库中没有则从环境变量读取,最后使用默认密钥\nfunc GetEncryptionKey() string {\n\t// 1. 优先从数据库读取\n\tkey := GetConfigValueByKey(\"encryption_key\", \"\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 2. 从环境变量或配置文件读取\n\tkey = GetConfKey(\"encryption_key\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 3. 使用默认密钥(不应该到这里,因为 DatabaseInit 会生成)\n\tlogger.Log.Warn(\"使用默认加密密钥,建议在数据库中配置 encryption_key\")\n\treturn \"zbxtable-default-encryption-key-2024\"\n}\n"}, "6a7d6651e26ecadcdb5858f3b3650be975bb6590": {"t": 1785754936.0473988, "n": 10016, "h": "abd90d9887686b79", "c": "package model\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\t\"zbxtable/pkg/logger\"\n\t\"zbxtable/pkg/utils\"\n\n\tzabbix \"github.com/canghai908/zabbix-go\"\n\t_ \"github.com/go-sql-driver/mysql\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t_ \"github.com/lib/pq\"\n\tworkwx \"github.com/xen0n/go-workwx\"\n\tini \"gopkg.in/ini.v1\"\n\t\"gorm.io/gorm\"\n)\n\nvar (\n\tAPI = &zabbix.API{}\n\tjson = jsoniter.Config{\n\t\tEscapeHTML: false,\n\t\tSortMapKeys: true,\n\t\tValidateJsonRawMessage: true,\n\t}.Froze()\n\tZBX_VER string\n\tZBX_V bool\n\tVersion string\n\tGitHash string\n\tBuildTime string\n\tWeApp = &workwx.WorkwxApp{}\n\n\tenvConfig map[string]string\n\tenvConfigOnce sync.Once\n\tenvFileExists bool\n)\n\nconst (\n\t//ms-agent 常量定义\n\tMSName = \"ms-agent\"\n\tMSUser = \"ms-agent\"\n\tMSGroup = \"MS-Agent\"\n\tMSMedia = \"MS-Agent\"\n\tMSAction = \"MS-Agent\"\n\t//webhook 常量定义\n\tWebhookName = \"ZbxTable\"\n\tWebhookAction = \"ZbxTable Webhook\"\n\tWebhookGroup = \"ZbxTable Webhook\"\n\tWebhookUser = \"zbxtable-webhook\"\n\t//down\n\tDownloadPath = \"./download/\"\n\tTplPath = \"./assets/templates/\"\n)\n\n// TableName 表名前缀\nfunc TableName(str string) string {\n\treturn fmt.Sprintf(\"%s%s\", \"zbxtable_\", str)\n}\n\n// 接收数据库信息初始化\nfunc ModelInit(dbtype, dbhost, dbuser, dbpass, dbname, dbport string) {\n\t// 直接使用 GORM\n\trunmode := GetConfKey(\"runmode\")\n\terr := InitGormDB(dbtype, dbhost, dbuser, dbpass, dbname, dbport, runmode)\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to connect database: \", err)\n\t\tos.Exit(1)\n\t}\n\tlogger.Log.Info(\"Database connected!\")\n\t// 自动迁移表\n\terr = AutoMigrate()\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to auto migrate: \", err)\n\t\tos.Exit(1)\n\t}\n\t// 基础数据初始化\n\tDatabaseInit()\n\t// 使用go-cache替代Redis(不再需要Redis连接)\n\tInitCache()\n}\n\nfunc InitWechat() {\n\t// 首先检查企业微信开关是否启用\n\twechatEnabled := GetConfigValueByKey(\"wechat_enabled\", \"0\")\n\tif wechatEnabled != \"1\" {\n\t\tlogger.Log.Info(\"WeChat is disabled in system config, skipping WeChat initialization\")\n\t\treturn\n\t}\n\t// 开关已启用,从数据库读取企业微信配置\n\tagentIDStr := GetConfigValueByKey(\"wechat_agentid\", \"\")\n\tcorpid := GetConfigValueByKey(\"wechat_corpid\", \"\")\n\tsecret := GetConfigValueByKey(\"wechat_secret\", \"\")\n\n\t// 验证必填配置项\n\tif agentIDStr == \"\" || corpid == \"\" || secret == \"\" {\n\t\tlogger.Log.Info(\"WeChat is enabled but configuration is incomplete (agentid/corpid/secret), WeChat app will not be initialized\")\n\t\treturn\n\t}\n\t// 解析 AgentID\n\tAgentId, err := strconv.ParseInt(agentIDStr, 10, 64)\n\tif err != nil {\n\t\tlogger.Log.Error(\"wechat_agentid parse error:\", err)\n\t\treturn\n\t}\n\n\t// 初始化企业微信客户端\n\tclient := workwx.New(corpid)\n\tWeApp = client.WithApp(secret, AgentId)\n\tWeApp.SpawnAccessTokenRefresher()\n\tlogger.Log.Info(\"WeChat inited successfully!\")\n}\n\n// DatabaseInit 数据初始化\nfunc DatabaseInit() {\n\t//数据初始化操作 - 使用 GORM\n\tvar v User\n\terr := DB.Where(\"username = ?\", \"admin\").First(&v).Error\n\t//检查权限\n\tif err == nil && v.Operation == \"\" {\n\t\terr := DB.Model(&User{}).Where(\"id = ?\", v.ID).Update(\"operation\", \"['add', 'edit', 'delete','update']\").Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"update admin operation successfully\")\n\t}\n\t//添加管理员账号\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\tlogger.Log.Info(\"the admin user does not exist, create a new admin account later!\")\n\t\tvar user User\n\t\tuser.Username = \"admin\"\n\t\tuser.Password, _ = utils.PasswordHash(\"Zbxtable\")\n\t\tuser.Avatar = \"https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif\"\n\t\tuser.Role = \"admin\"\n\t\tuser.Operation = \"['add', 'edit', 'delete','update']\"\n\t\tuser.Status = 0\n\t\terr := DB.Create(&user).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"create an administrator account successfully, the admin ID is:\", user.ID)\n\t}\n\t//初始化系统数据\n\tvar cnt []System\n\terr = DB.Find(&cnt).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn\n\t}\n\tif len(cnt) == 0 {\n\t\tnow := time.Now()\n\t\tsys := []System{\n\t\t\t{Name: \"Linux操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"Windows操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"网络设备\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"物理服务器\", Status: 0, InitedAt: &now},\n\t\t}\n\t\terr := DB.Create(&sys).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init system info error!\", err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"Init system data successfully!\")\n\t}\n\t//告警默认规则初始化 default rule\n\tvar rules []Rule\n\terr = DB.Where(\"m_type = ?\", \"2\").Find(&rules).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn\n\t}\n\tif len(rules) == 0 {\n\t\t// 使用 \"*\" 作为全局默认规则,匹配所有租户\n\t\tdefaultRule := []Rule{\n\t\t\t{\n\t\t\t\tName: \"全局默认规则\",\n\t\t\t\tZIDs: \"*\",\n\t\t\t\tMType: \"2\",\n\t\t\t\tChannel: \"wechat_robot\",\n\t\t\t\tUserIds: \"1\",\n\t\t\t\tSweek: \"0,1,2,3,4,5,6\",\n\t\t\t\tStime: \"00:00\",\n\t\t\t\tEtime: \"23.59\",\n\t\t\t\tStatus: \"0\"},\n\t\t}\n\t\terr := DB.Create(&defaultRule).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init default rule error!\", err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"Init default rule successfully!\")\n\t}\n\t// 初始化加密密钥(如果不存在)\n\tvar encryptionKeyConfig Config\n\terr = DB.Where(\"`key` = ?\", \"encryption_key\").First(&encryptionKeyConfig).Error\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t// 生成随机加密密钥\n\t\trandomKey := utils.GenerateRandomKey()\n\t\tencryptionKeyConfig = Config{\n\t\t\tName: \"加密密钥\",\n\t\t\tKey: \"encryption_key\",\n\t\t\tValue: randomKey,\n\t\t\tComment: \"用于加密存储敏感信息(如Zabbix密码和Token)的密钥,系统自动生成,不可修改\",\n\t\t}\n\t\tif insertErr := DB.Create(&encryptionKeyConfig).Error; insertErr != nil {\n\t\t\tlogger.Log.Error(\"Init encryption_key error:\", insertErr)\n\t\t} else {\n\t\t\tlogger.Log.Info(\"Init encryption_key successfully! Key length:\", len(randomKey))\n\t\t}\n\t}\n\n\tdefaultConfigs := []Config{\n\t\t// Dashboard 相关\n\t\t{Name: \"数据面板\", Key: \"zbx_dash\", Value: \"0\", Comment: \"是否开启Zabbix看板:1 开启,0 关闭\"},\n\t\t{Name: \"面板配置\", Key: \"dash_id\", Value: \"1\", Comment: \"需要引入的Zabbix面板的ID,默认为1\"},\n\t\t{Name: \"主机分类同步\", Key: \"sync_inventory\", Value: \"1\", Comment: \"主机分类同步计划任务是否启用:1 启用,0 不启用\"},\n\t\t{Name: \"Webhook回调地址\", Key: \"webhook_url\", Value: \"\", Comment: \"webhook通知地址\"},\n\t\t// 邮件配置\n\t\t{Name: \"邮件发件人\", Key: \"email_from\", Value: \"\", Comment: \"告警邮件发件人邮箱地址\"},\n\t\t{Name: \"邮件昵称\", Key: \"email_nickname\", Value: \"ZbxTable\", Comment: \"告警邮件显示的发件人昵称\"},\n\t\t{Name: \"SMTP 密码/授权码\", Key: \"email_secret\", Value: \"\", Comment: \"SMTP 登录密码或授权码\"},\n\t\t{Name: \"SMTP 服务器\", Key: \"email_host\", Value: \"smtp.qq.com\", Comment: \"SMTP 服务器地址\"},\n\t\t{Name: \"SMTP 端口\", Key: \"email_port\", Value: \"465\", Comment: \"SMTP 端口号\"},\n\t\t{Name: \"SMTP 使用 SSL\", Key: \"email_isSSl\", Value: \"true\", Comment: \"是否启用 SSL:true/false\"},\n\t\t// 企业微信配置\n\t\t{Name: \"企业微信开关\", Key: \"wechat_enabled\", Value: \"0\", Comment: \"是否启用企业微信:1 启用,0 禁用\"},\n\t\t{Name: \"企业微信 AgentID\", Key: \"wechat_agentid\", Value: \"\", Comment: \"企业微信应用的 AgentID\"},\n\t\t{Name: \"企业微信 CorpID\", Key: \"wechat_corpid\", Value: \"\", Comment: \"企业微信企业ID\"},\n\t\t{Name: \"企业微信 Secret\", Key: \"wechat_secret\", Value: \"\", Comment: \"企业微信应用的 Secret\"},\n\t\t// Ollama 配置\n\t\t{Name: \"Ollama Host\", Key: \"ollama_host\", Value: \"http://localhost:11434\", Comment: \"Ollama 服务地址,如 http://127.0.0.1:11434\"},\n\t\t{Name: \"Ollama Model\", Key: \"ollama_model\", Value: \"deepseek-r1:32b\", Comment: \"默认使用的大模型名称\"},\n\t}\n\n\tfor _, cfgItem := range defaultConfigs {\n\t\tvar existing Config\n\t\terr = DB.Where(\"`key` = ?\", cfgItem.Key).First(&existing).Error\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\tif insertErr := DB.Create(&cfgItem).Error; insertErr != nil {\n\t\t\t\tlogger.Log.Infof(\"Init config key %s error: %v\", cfgItem.Key, insertErr)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tlogger.Log.Infof(\"Init config key %s successfully!\", cfgItem.Key)\n\t\t}\n\t}\n\t//默认菜单初始化\n\tInitMenuData()\n\t//检查并添加缺失的菜单项(用于版本升级)\n\tCheckAndAddMenus()\n\n}\n\nfunc GetConfKey(v string) string {\n\t// 优先从 .env 中读取(便于开发环境配置敏感信息且不提交到 git)\n\tenvConfigOnce.Do(func() {\n\t\tfile, err := os.Open(\".env\")\n\t\tif err != nil {\n\t\t\t// 没有 .env 文件时直接跳过,后面回退到 app.conf\n\t\t\tenvFileExists = false\n\t\t\treturn\n\t\t}\n\t\tdefer file.Close()\n\n\t\tenvFileExists = true\n\t\tenvConfig = make(map[string]string)\n\n\t\tscanner := bufio.NewScanner(file)\n\t\tfor scanner.Scan() {\n\t\t\tline := strings.TrimSpace(scanner.Text())\n\t\t\tif line == \"\" || strings.HasPrefix(line, \"#\") || strings.HasPrefix(line, \";\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tparts := strings.SplitN(line, \"=\", 2)\n\t\t\tif len(parts) != 2 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tkey := strings.TrimSpace(parts[0])\n\t\t\tval := strings.TrimSpace(parts[1])\n\t\t\t// 去掉可能的引号\n\t\t\tval = strings.Trim(val, `\"'`)\n\t\t\tif key != \"\" {\n\t\t\t\tenvConfig[key] = val // 即使 val 为空也认为是有效配置\n\t\t\t}\n\t\t}\n\t})\n\n\t// 如果存在 .env 文件,则所有配置完全由 .env 决定:\n\t// - key 存在:返回对应值(可以是空字符串)\n\t// - key 不存在:返回空字符串\n\tif envFileExists {\n\t\tif envConfig == nil {\n\t\t\treturn \"\"\n\t\t}\n\t\tif val, ok := envConfig[v]; ok {\n\t\t\treturn val\n\t\t}\n\t\treturn \"\"\n\t}\n\t// 其次从 config/app.conf 读取\n\tcfg, err := ini.Load(\"./config/app.conf\")\n\tif err != nil {\n\t\tlogger.Log.Error(err)\n\t\treturn \"\"\n\t}\n\tp, err := cfg.Section(\"\").GetKey(v)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn p.String()\n}\n\n// IsPasswordConfigured 检查是否配置了密码(用于图形查看)\n// 优先检查数据库中是否有激活的 Zabbix 实例配置了用户名和密码\n// 注意:查看图形需要用户名和密码,Token 方式无法用于 Web 登录\nfunc IsPasswordConfigured() bool {\n\t// 1. 优先检查数据库中是否有激活的 Zabbix 实例\n\tvar activeInstance ZabbixInstance\n\terr := DB.Where(\"is_active = ?\", true).First(&activeInstance).Error\n\tif err == nil {\n\t\t// 找到激活的实例,检查是否配置了用户名和密码(查看图形必须用密码,Token 无法用于 Web 登录)\n\t\tif strings.TrimSpace(activeInstance.User) != \"\" && strings.TrimSpace(activeInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 2. 如果数据库中没有激活实例,检查是否有任何可用的实例配置了密码\n\tvar anyInstance ZabbixInstance\n\terr = DB.Where(\"enabled = ?\", true).First(&anyInstance).Error\n\tif err == nil {\n\t\t// 找到可用的实例,检查是否配置了用户名和密码\n\t\tif strings.TrimSpace(anyInstance.User) != \"\" && strings.TrimSpace(anyInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 3. 回退到配置文件检查(兼容旧版本)\n\tpass := GetConfKey(\"zabbix_pass\")\n\tuser := GetConfKey(\"zabbix_user\")\n\treturn pass != \"\" && user != \"\"\n}\n\n// GetEncryptionKey 获取加密密钥\n// 优先从数据库读取,如果数据库中没有则从环境变量读取,最后使用默认密钥\nfunc GetEncryptionKey() string {\n\t// 1. 优先从数据库读取\n\tkey := GetConfigValueByKey(\"encryption_key\", \"\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 2. 从环境变量或配置文件读取\n\tkey = GetConfKey(\"encryption_key\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 3. 使用默认密钥(不应该到这里,因为 DatabaseInit 会生成)\n\tlogger.Log.Warn(\"使用默认加密密钥,建议在数据库中配置 encryption_key\")\n\treturn \"zbxtable-default-encryption-key-2024\"\n}\n"}, "c3159e8b1d48ff2b297fcffcbf707385d044e9e7": {"t": 1785754937.5503979, "n": 10052, "h": "9bd92f4da6c47f62", "c": "package model\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\t\"zbxtable/pkg/logger\"\n\t\"zbxtable/pkg/utils\"\n\n\tzabbix \"github.com/canghai908/zabbix-go\"\n\t_ \"github.com/go-sql-driver/mysql\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t_ \"github.com/lib/pq\"\n\tworkwx \"github.com/xen0n/go-workwx\"\n\tini \"gopkg.in/ini.v1\"\n\t\"gorm.io/gorm\"\n)\n\nvar (\n\tAPI = &zabbix.API{}\n\tjson = jsoniter.Config{\n\t\tEscapeHTML: false,\n\t\tSortMapKeys: true,\n\t\tValidateJsonRawMessage: true,\n\t}.Froze()\n\tZBX_VER string\n\tZBX_V bool\n\tVersion string\n\tGitHash string\n\tBuildTime string\n\tWeApp = &workwx.WorkwxApp{}\n\n\tenvConfig map[string]string\n\tenvConfigOnce sync.Once\n\tenvFileExists bool\n)\n\nconst (\n\t//ms-agent 常量定义\n\tMSName = \"ms-agent\"\n\tMSUser = \"ms-agent\"\n\tMSGroup = \"MS-Agent\"\n\tMSMedia = \"MS-Agent\"\n\tMSAction = \"MS-Agent\"\n\t//webhook 常量定义\n\tWebhookName = \"ZbxTable\"\n\tWebhookAction = \"ZbxTable Webhook\"\n\tWebhookGroup = \"ZbxTable Webhook\"\n\tWebhookUser = \"zbxtable-webhook\"\n\t//down\n\tDownloadPath = \"./download/\"\n\tTplPath = \"./assets/templates/\"\n)\n\n// TableName 表名前缀\nfunc TableName(str string) string {\n\treturn fmt.Sprintf(\"%s%s\", \"zbxtable_\", str)\n}\n\n// 接收数据库信息初始化\nfunc ModelInit(dbtype, dbhost, dbuser, dbpass, dbname, dbport string) {\n\t// 直接使用 GORM\n\trunmode := GetConfKey(\"runmode\")\n\terr := InitGormDB(dbtype, dbhost, dbuser, dbpass, dbname, dbport, runmode)\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to connect database: \", err)\n\t\tos.Exit(1)\n\t}\n\tlogger.Log.Info(\"Database connected!\")\n\t// 自动迁移表\n\terr = AutoMigrate()\n\tif err != nil {\n\t\tlogger.Log.Error(\"Failed to auto migrate: \", err)\n\t\tos.Exit(1)\n\t}\n\t// 基础数据初始化\n\tDatabaseInit()\n\t// 使用go-cache替代Redis(不再需要Redis连接)\n\tInitCache()\n}\n\nfunc InitWechat() {\n\t// 首先检查企业微信开关是否启用\n\twechatEnabled := GetConfigValueByKey(\"wechat_enabled\", \"0\")\n\tif wechatEnabled != \"1\" {\n\t\tlogger.Log.Info(\"WeChat is disabled in system config, skipping WeChat initialization\")\n\t\treturn\n\t}\n\t// 开关已启用,从数据库读取企业微信配置\n\tagentIDStr := GetConfigValueByKey(\"wechat_agentid\", \"\")\n\tcorpid := GetConfigValueByKey(\"wechat_corpid\", \"\")\n\tsecret := GetConfigValueByKey(\"wechat_secret\", \"\")\n\n\t// 验证必填配置项\n\tif agentIDStr == \"\" || corpid == \"\" || secret == \"\" {\n\t\tlogger.Log.Info(\"WeChat is enabled but configuration is incomplete (agentid/corpid/secret), WeChat app will not be initialized\")\n\t\treturn\n\t}\n\t// 解析 AgentID\n\tAgentId, err := strconv.ParseInt(agentIDStr, 10, 64)\n\tif err != nil {\n\t\tlogger.Log.Error(\"wechat_agentid parse error:\", err)\n\t\treturn\n\t}\n\n\t// 初始化企业微信客户端\n\tclient := workwx.New(corpid)\n\tWeApp = client.WithApp(secret, AgentId)\n\tWeApp.SpawnAccessTokenRefresher()\n\tlogger.Log.Info(\"WeChat inited successfully!\")\n}\n\n// DatabaseInit 数据初始化\nfunc DatabaseInit() {\n\t//数据初始化操作 - 使用 GORM\n\tvar v Manager\n\terr := DB.Where(\"username = ?\", \"admin\").First(&v).Error\n\t//检查权限\n\tif err == nil && v.Operation == \"\" {\n\t\terr := DB.Model(&Manager{}).Where(\"id = ?\", v.ID).Update(\"operation\", \"['add', 'edit', 'delete','update']\").Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"update admin operation successfully\")\n\t}\n\t//添加管理员账号\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\tlogger.Log.Info(\"the admin user does not exist, create a new admin account later!\")\n\t\tvar manager Manager\n\t\tmanager.Username = \"admin\"\n\t\tmanager.Password, _ = utils.PasswordHash(\"Zbxtable\")\n\t\tmanager.Avatar = \"https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif\"\n\t\tmanager.Role = \"admin\"\n\t\tmanager.Operation = \"['add', 'edit', 'delete','update']\"\n\t\tmanager.Status = 0\n\t\terr := DB.Create(&manager).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"create an administrator account successfully, the admin ID is:\", manager.ID)\n\t}\n\t//初始化系统数据\n\tvar cnt []System\n\terr = DB.Find(&cnt).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn\n\t}\n\tif len(cnt) == 0 {\n\t\tnow := time.Now()\n\t\tsys := []System{\n\t\t\t{Name: \"Linux操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"Windows操作系统\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"网络设备\", Status: 0, InitedAt: &now},\n\t\t\t{Name: \"物理服务器\", Status: 0, InitedAt: &now},\n\t\t}\n\t\terr := DB.Create(&sys).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init system info error!\", err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"Init system data successfully!\")\n\t}\n\t//告警默认规则初始化 default rule\n\tvar rules []Rule\n\terr = DB.Where(\"m_type = ?\", \"2\").Find(&rules).Error\n\tif err != nil {\n\t\tlogger.Log.Info(err)\n\t\treturn\n\t}\n\tif len(rules) == 0 {\n\t\t// 使用 \"*\" 作为全局默认规则,匹配所有租户\n\t\tdefaultRule := []Rule{\n\t\t\t{\n\t\t\t\tName: \"全局默认规则\",\n\t\t\t\tZIDs: \"*\",\n\t\t\t\tMType: \"2\",\n\t\t\t\tChannel: \"wechat_robot\",\n\t\t\t\tUserIds: \"1\",\n\t\t\t\tSweek: \"0,1,2,3,4,5,6\",\n\t\t\t\tStime: \"00:00\",\n\t\t\t\tEtime: \"23.59\",\n\t\t\t\tStatus: \"0\"},\n\t\t}\n\t\terr := DB.Create(&defaultRule).Error\n\t\tif err != nil {\n\t\t\tlogger.Log.Info(\"Init default rule error!\", err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Log.Info(\"Init default rule successfully!\")\n\t}\n\t// 初始化加密密钥(如果不存在)\n\tvar encryptionKeyConfig Config\n\terr = DB.Where(\"`key` = ?\", \"encryption_key\").First(&encryptionKeyConfig).Error\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t// 生成随机加密密钥\n\t\trandomKey := utils.GenerateRandomKey()\n\t\tencryptionKeyConfig = Config{\n\t\t\tName: \"加密密钥\",\n\t\t\tKey: \"encryption_key\",\n\t\t\tValue: randomKey,\n\t\t\tComment: \"用于加密存储敏感信息(如Zabbix密码和Token)的密钥,系统自动生成,不可修改\",\n\t\t}\n\t\tif insertErr := DB.Create(&encryptionKeyConfig).Error; insertErr != nil {\n\t\t\tlogger.Log.Error(\"Init encryption_key error:\", insertErr)\n\t\t} else {\n\t\t\tlogger.Log.Info(\"Init encryption_key successfully! Key length:\", len(randomKey))\n\t\t}\n\t}\n\n\tdefaultConfigs := []Config{\n\t\t// Dashboard 相关\n\t\t{Name: \"数据面板\", Key: \"zbx_dash\", Value: \"0\", Comment: \"是否开启Zabbix看板:1 开启,0 关闭\"},\n\t\t{Name: \"面板配置\", Key: \"dash_id\", Value: \"1\", Comment: \"需要引入的Zabbix面板的ID,默认为1\"},\n\t\t{Name: \"主机分类同步\", Key: \"sync_inventory\", Value: \"1\", Comment: \"主机分类同步计划任务是否启用:1 启用,0 不启用\"},\n\t\t{Name: \"Webhook回调地址\", Key: \"webhook_url\", Value: \"\", Comment: \"webhook通知地址\"},\n\t\t// 邮件配置\n\t\t{Name: \"邮件发件人\", Key: \"email_from\", Value: \"\", Comment: \"告警邮件发件人邮箱地址\"},\n\t\t{Name: \"邮件昵称\", Key: \"email_nickname\", Value: \"ZbxTable\", Comment: \"告警邮件显示的发件人昵称\"},\n\t\t{Name: \"SMTP 密码/授权码\", Key: \"email_secret\", Value: \"\", Comment: \"SMTP 登录密码或授权码\"},\n\t\t{Name: \"SMTP 服务器\", Key: \"email_host\", Value: \"smtp.qq.com\", Comment: \"SMTP 服务器地址\"},\n\t\t{Name: \"SMTP 端口\", Key: \"email_port\", Value: \"465\", Comment: \"SMTP 端口号\"},\n\t\t{Name: \"SMTP 使用 SSL\", Key: \"email_isSSl\", Value: \"true\", Comment: \"是否启用 SSL:true/false\"},\n\t\t// 企业微信配置\n\t\t{Name: \"企业微信开关\", Key: \"wechat_enabled\", Value: \"0\", Comment: \"是否启用企业微信:1 启用,0 禁用\"},\n\t\t{Name: \"企业微信 AgentID\", Key: \"wechat_agentid\", Value: \"\", Comment: \"企业微信应用的 AgentID\"},\n\t\t{Name: \"企业微信 CorpID\", Key: \"wechat_corpid\", Value: \"\", Comment: \"企业微信企业ID\"},\n\t\t{Name: \"企业微信 Secret\", Key: \"wechat_secret\", Value: \"\", Comment: \"企业微信应用的 Secret\"},\n\t\t// Ollama 配置\n\t\t{Name: \"Ollama Host\", Key: \"ollama_host\", Value: \"http://localhost:11434\", Comment: \"Ollama 服务地址,如 http://127.0.0.1:11434\"},\n\t\t{Name: \"Ollama Model\", Key: \"ollama_model\", Value: \"deepseek-r1:32b\", Comment: \"默认使用的大模型名称\"},\n\t}\n\n\tfor _, cfgItem := range defaultConfigs {\n\t\tvar existing Config\n\t\terr = DB.Where(\"`key` = ?\", cfgItem.Key).First(&existing).Error\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\tif insertErr := DB.Create(&cfgItem).Error; insertErr != nil {\n\t\t\t\tlogger.Log.Infof(\"Init config key %s error: %v\", cfgItem.Key, insertErr)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tlogger.Log.Infof(\"Init config key %s successfully!\", cfgItem.Key)\n\t\t}\n\t}\n\t//默认菜单初始化\n\tInitMenuData()\n\t//检查并添加缺失的菜单项(用于版本升级)\n\tCheckAndAddMenus()\n\n}\n\nfunc GetConfKey(v string) string {\n\t// 优先从 .env 中读取(便于开发环境配置敏感信息且不提交到 git)\n\tenvConfigOnce.Do(func() {\n\t\tfile, err := os.Open(\".env\")\n\t\tif err != nil {\n\t\t\t// 没有 .env 文件时直接跳过,后面回退到 app.conf\n\t\t\tenvFileExists = false\n\t\t\treturn\n\t\t}\n\t\tdefer file.Close()\n\n\t\tenvFileExists = true\n\t\tenvConfig = make(map[string]string)\n\n\t\tscanner := bufio.NewScanner(file)\n\t\tfor scanner.Scan() {\n\t\t\tline := strings.TrimSpace(scanner.Text())\n\t\t\tif line == \"\" || strings.HasPrefix(line, \"#\") || strings.HasPrefix(line, \";\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tparts := strings.SplitN(line, \"=\", 2)\n\t\t\tif len(parts) != 2 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tkey := strings.TrimSpace(parts[0])\n\t\t\tval := strings.TrimSpace(parts[1])\n\t\t\t// 去掉可能的引号\n\t\t\tval = strings.Trim(val, `\"'`)\n\t\t\tif key != \"\" {\n\t\t\t\tenvConfig[key] = val // 即使 val 为空也认为是有效配置\n\t\t\t}\n\t\t}\n\t})\n\n\t// 如果存在 .env 文件,则所有配置完全由 .env 决定:\n\t// - key 存在:返回对应值(可以是空字符串)\n\t// - key 不存在:返回空字符串\n\tif envFileExists {\n\t\tif envConfig == nil {\n\t\t\treturn \"\"\n\t\t}\n\t\tif val, ok := envConfig[v]; ok {\n\t\t\treturn val\n\t\t}\n\t\treturn \"\"\n\t}\n\t// 其次从 config/app.conf 读取\n\tcfg, err := ini.Load(\"./config/app.conf\")\n\tif err != nil {\n\t\tlogger.Log.Error(err)\n\t\treturn \"\"\n\t}\n\tp, err := cfg.Section(\"\").GetKey(v)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn p.String()\n}\n\n// IsPasswordConfigured 检查是否配置了密码(用于图形查看)\n// 优先检查数据库中是否有激活的 Zabbix 实例配置了用户名和密码\n// 注意:查看图形需要用户名和密码,Token 方式无法用于 Web 登录\nfunc IsPasswordConfigured() bool {\n\t// 1. 优先检查数据库中是否有激活的 Zabbix 实例\n\tvar activeInstance ZabbixInstance\n\terr := DB.Where(\"is_active = ?\", true).First(&activeInstance).Error\n\tif err == nil {\n\t\t// 找到激活的实例,检查是否配置了用户名和密码(查看图形必须用密码,Token 无法用于 Web 登录)\n\t\tif strings.TrimSpace(activeInstance.User) != \"\" && strings.TrimSpace(activeInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 2. 如果数据库中没有激活实例,检查是否有任何可用的实例配置了密码\n\tvar anyInstance ZabbixInstance\n\terr = DB.Where(\"enabled = ?\", true).First(&anyInstance).Error\n\tif err == nil {\n\t\t// 找到可用的实例,检查是否配置了用户名和密码\n\t\tif strings.TrimSpace(anyInstance.User) != \"\" && strings.TrimSpace(anyInstance.Pass) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// 3. 回退到配置文件检查(兼容旧版本)\n\tpass := GetConfKey(\"zabbix_pass\")\n\tuser := GetConfKey(\"zabbix_user\")\n\treturn pass != \"\" && user != \"\"\n}\n\n// GetEncryptionKey 获取加密密钥\n// 优先从数据库读取,如果数据库中没有则从环境变量读取,最后使用默认密钥\nfunc GetEncryptionKey() string {\n\t// 1. 优先从数据库读取\n\tkey := GetConfigValueByKey(\"encryption_key\", \"\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 2. 从环境变量或配置文件读取\n\tkey = GetConfKey(\"encryption_key\")\n\tif key != \"\" {\n\t\treturn key\n\t}\n\n\t// 3. 使用默认密钥(不应该到这里,因为 DatabaseInit 会生成)\n\tlogger.Log.Warn(\"使用默认加密密钥,建议在数据库中配置 encryption_key\")\n\treturn \"zbxtable-default-encryption-key-2024\"\n}\n"}}, "internal/handler/system.go": {"bcdc3997c4da6f884243c76a79541b3930b0018d": {"t": 1785745618.1494324, "n": 14440, "h": "6e5c15be4d56e5a3", "c": "package handler\n\nimport (\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"zbxtable/internal/model\"\n\t\"zbxtable/pkg/response\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/tidwall/gjson\"\n)\n\n// GetAllSystem 获取系统配置列表\nfunc GetAllSystem(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tcnt, val, err := model.GetALlSystem()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"ok\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = cnt\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetSystemByID 获取单个系统配置\nfunc GetSystemByID(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\tvar SystemRes model.SystemList\n\tval, err := model.GetSystemByID(int64(id))\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"ok\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// UpdateSystem 更新系统配置(支持多实例)\nfunc parseSystemBody(body string) model.System {\n\tzid, _ := strconv.Atoi(gjson.Get(body, \"zid\").String())\n\treturn model.System{\n\t\tZID: zid,\n\t\tName: gjson.Get(body, \"name\").String(),\n\t\tTypeCode: gjson.Get(body, \"type_code\").String(),\n\t\tGroupID: gjson.Get(body, \"group_id\").String(),\n\t\tCPUCore: gjson.Get(body, \"cpu_core\").String(),\n\t\tCPUUtilizationID: gjson.Get(body, \"cpu_utilization_id\").String(),\n\t\tMemoryTotalID: gjson.Get(body, \"memory_total_id\").String(),\n\t\tMemoryUsedID: gjson.Get(body, \"memory_used_id\").String(),\n\t\tMemoryUtilizationID: gjson.Get(body, \"memory_utilization_id\").String(),\n\t\tUptimeID: gjson.Get(body, \"uptime_id\").String(),\n\t\tModel: gjson.Get(body, \"model\").String(),\n\t\tPingTemplateID: gjson.Get(body, \"ping_template_id\").String(),\n\t\tAutoInit: int(gjson.Get(body, \"auto_init\").Int()),\n\t\tInitCron: gjson.Get(body, \"init_cron\").String(),\n\t\tInitOnNewHost: int(gjson.Get(body, \"init_on_new_host\").Int()),\n\t\tMaxRetry: int(gjson.Get(body, \"max_retry\").Int()),\n\t}\n}\n\nfunc UpdateSystem(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\n\tif gjson.Get(string(body), \"zid\").String() == \"\" {\n\t\tresponse.BadRequest(c, \"请选择 Zabbix 实例\")\n\t\treturn\n\t}\n\n\tv := parseSystemBody(string(body))\n\tv.ID = int64(id)\n\n\tvar SystemRes model.SystemList\n\tif err = model.CreateOrUpdateSystem(&v); err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"更新成功\"\n\t\t// 调度器重载已在 model.CreateOrUpdateSystem 内触发,此处无需重复\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// SystemInit 初始化系统(支持多实例)\nfunc SystemInit(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\n\t// 读取请求体获取实例ID\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tzidStr := gjson.Get(string(body), \"zid\").String()\n\tif zidStr == \"\" {\n\t\tresponse.BadRequest(c, \"请选择 Zabbix 实例\")\n\t\treturn\n\t}\n\tzid, _ := strconv.Atoi(zidStr)\n\n\tvar SystemRes model.SystemList\n\terr = model.SystemInitWithInstance(int64(id), int(zid))\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t\tSystemRes.Data.Total = 0\n\t\tSystemRes.Data.Items = \"\"\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"初始化完成\"\n\t\tSystemRes.Data.Total = 0\n\t\tSystemRes.Data.Items = \"\"\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// CreateSystem 创建新的资产绑定配置\nfunc CreateSystem(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\tif gjson.Get(string(body), \"zid\").String() == \"\" {\n\t\tresponse.BadRequest(c, \"请选择 Zabbix 实例\")\n\t\treturn\n\t}\n\tif gjson.Get(string(body), \"type_code\").String() == \"\" {\n\t\tresponse.BadRequest(c, \"请选择资产类型\")\n\t\treturn\n\t}\n\tv := parseSystemBody(string(body))\n\tif err := model.CreateSystem(&v); err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"创建成功\", v)\n}\n\n// GetSystemHistory 获取资产绑定初始化历史\nfunc GetSystemHistory(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\tpage, _ := strconv.Atoi(c.DefaultQuery(\"page\", \"1\"))\n\tpageSize, _ := strconv.Atoi(c.DefaultQuery(\"limit\", \"20\"))\n\tif page < 1 {\n\t\tpage = 1\n\t}\n\tif pageSize < 1 || pageSize > 100 {\n\t\tpageSize = 20\n\t}\n\n\tlist, total, err := model.GetSystemHistory(id, page, pageSize)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.Success(c, map[string]interface{}{\"items\": list, \"total\": total})\n}\n\n// DeleteSystem 删除资产绑定配置\nfunc DeleteSystem(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\tif err := model.DeleteSystem(id); err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"删除成功\", nil)\n}\n\n// GetEgress 获取出口带宽配置\nfunc GetEgress(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tval, err := model.GetEgress()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"获取成功\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetAllConfig 获取系统参数配置\nfunc GetAllConfig(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tval, err := model.GetConfigList()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tfilteredConfigs := sanitizeConfigsForResponse(val)\n\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"获取成功\"\n\t\tSystemRes.Data.Items = filteredConfigs\n\t\tSystemRes.Data.Total = int64(len(filteredConfigs))\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\nfunc sanitizeConfigsForResponse(configs []model.Config) []model.Config {\n\tfilteredConfigs := make([]model.Config, 0, len(configs))\n\tfor _, config := range configs {\n\t\tif config.ConfigKey == \"encryption_key\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tif modelSensitiveConfigKey(config.ConfigKey) && config.ConfigValue != \"\" {\n\t\t\tconfig.ConfigValue = \"********\"\n\t\t}\n\n\t\tfilteredConfigs = append(filteredConfigs, config)\n\t}\n\treturn filteredConfigs\n}\n\nfunc modelSensitiveConfigKey(key string) bool {\n\tswitch key {\n\tcase \"email_secret\", \"wechat_secret\", \"deepseek_api_key\", \"custom_api_key\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\n// UpdateConfig 更新系统参数配置\nfunc UpdateConfig(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tvalue := gjson.Get(string(body), \"config_value\").String()\n\n\t// 如果值是星号(脱敏标记),则不更新\n\t// 这表示前端没有修改该敏感字段\n\tif value == \"********\" {\n\t\tvar SystemRes model.SystemList\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"未修改敏感字段,保持原值\"\n\t\tSystemRes.Data.Items = \"\"\n\t\tSystemRes.Data.Total = 1\n\t\tc.JSON(http.StatusOK, SystemRes)\n\t\treturn\n\t}\n\n\tvar SystemRes model.SystemList\n\tv := model.Config{ID: int64(id), ConfigValue: value}\n\terr = model.UpdateConfig(&v)\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"更新成功\"\n\t\tSystemRes.Data.Items = \"\"\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// ============ 新的出口配置 API ============\n\n// GetAllEgressConfigs 获取所有出口配置\nfunc GetAllEgressConfigs(c *gin.Context) {\n\tconfigs, err := model.GetAllEgress()\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.Success(c, configs)\n}\n\n// GetEgressConfigByID 根据ID获取出口配置\nfunc GetEgressConfigByID(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\n\tconfig, err := model.GetEgressConfigByID(id)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.Success(c, config)\n}\n\n// AddEgressConfig 添加出口配置\nfunc AddEgressConfig(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tname := gjson.Get(string(body), \"name\").String()\n\tzidStr := gjson.Get(string(body), \"zid\").String()\n\tzid, _ := strconv.Atoi(zidStr)\n\thostID := gjson.Get(string(body), \"host_id\").String()\n\tinItemID := gjson.Get(string(body), \"in_item_id\").String()\n\toutItemID := gjson.Get(string(body), \"out_item_id\").String()\n\tsortOrder := int(gjson.Get(string(body), \"sort_order\").Int())\n\n\tif name == \"\" || zidStr == \"\" || hostID == \"\" || inItemID == \"\" || outItemID == \"\" {\n\t\tresponse.BadRequest(c, \"缺少必填字段\")\n\t\treturn\n\t}\n\n\tconfig := &model.Egress{\n\t\tName: name,\n\t\tZID: zid,\n\t\tHostID: hostID,\n\t\tInItemID: inItemID,\n\t\tOutItemID: outItemID,\n\t\tStatus: 1,\n\t\tSortOrder: sortOrder,\n\t}\n\n\terr = model.AddEgress(config)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"添加成功\", config)\n}\n\n// UpdateEgressConfigHandler 更新出口配置\nfunc UpdateEgressConfigHandler(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tname := gjson.Get(string(body), \"name\").String()\n\tzidStr := gjson.Get(string(body), \"zid\").String()\n\thostID := gjson.Get(string(body), \"host_id\").String()\n\tinItemID := gjson.Get(string(body), \"in_item_id\").String()\n\toutItemID := gjson.Get(string(body), \"out_item_id\").String()\n\tstatus := int(gjson.Get(string(body), \"status\").Int())\n\tsortOrder := int(gjson.Get(string(body), \"sort_order\").Int())\n\n\tif name == \"\" || zidStr == \"\" || hostID == \"\" || inItemID == \"\" || outItemID == \"\" {\n\t\tresponse.BadRequest(c, \"缺少必填字段\")\n\t\treturn\n\t}\n\tzid, _ := strconv.Atoi(zidStr)\n\n\tconfig := &model.Egress{\n\t\tID: id,\n\t\tName: name,\n\t\tZID: zid,\n\t\tHostID: hostID,\n\t\tInItemID: inItemID,\n\t\tOutItemID: outItemID,\n\t\tStatus: status,\n\t\tSortOrder: sortOrder,\n\t}\n\n\terr = model.UpdateEgress(config)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"更新成功\", config)\n}\n\n// DeleteEgressConfigHandler 删除出口配置\nfunc DeleteEgressConfigHandler(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\n\terr = model.DeleteEgressConfig(id)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"删除成功\", nil)\n}\n\n// UploadLogo 上传系统Logo(保存到 ./upload/logo 并返回访问路径)\nfunc UploadLogo(c *gin.Context) {\n\tfile, err := c.FormFile(\"file\")\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"文件上传失败: \"+err.Error())\n\t\treturn\n\t}\n\n\t// 检查文件类型\n\tcontentType := file.Header.Get(\"Content-Type\")\n\tif contentType != \"image/png\" && contentType != \"image/jpeg\" && contentType != \"image/jpg\" && contentType != \"image/svg+xml\" {\n\t\tresponse.BadRequest(c, \"只支持 PNG、JPG、JPEG 或 SVG 格式的图片\")\n\t\treturn\n\t}\n\n\t// 检查文件大小(限制为2MB)\n\tif file.Size > 2*1024*1024 {\n\t\tresponse.BadRequest(c, \"文件大小不能超过2MB\")\n\t\treturn\n\t}\n\n\tresult, err := model.SaveLogoImage(file)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\n\tresponse.SuccessWithMessage(c, \"上传成功\", gin.H{\n\t\t\"url\": result.Path,\n\t})\n}\n\n// GetPublicSystemInfo 获取系统公开信息(无需认证)\nfunc GetPublicSystemInfo(c *gin.Context) {\n\t// 默认值\n\tsystemName := \"ZbxTable\"\n\tsystemSubtitle := \"Zabbix 监控数据可视化平台\"\n\tsystemLogo := \"/logo.png\"\n\tdemoMode := \"false\"\n\n\t// 尝试从数据库获取配置(如果数据库已初始化)\n\tif model.DB != nil {\n\t\tsystemName = model.GetConfigValueByKey(\"system_name\", \"ZbxTable\")\n\t\tsystemSubtitle = model.GetConfigValueByKey(\"system_subtitle\", \"Zabbix 监控数据可视化平台\")\n\t\tsystemLogo = model.GetConfigValueByKey(\"system_logo\", \"/logo.png\")\n\t\t// 增加 demo_mode 返回\n\t\tdemoMode = model.GetConfKey(\"demo_mode\")\n\t\tif demoMode == \"\" {\n\t\t\tdemoMode = model.GetConfigValueByKey(\"demo_mode\", \"false\")\n\t\t}\n\t}\n\n\tresponse.Success(c, gin.H{\n\t\t\"system_name\": systemName,\n\t\t\"system_subtitle\": systemSubtitle,\n\t\t\"system_logo\": systemLogo,\n\t\t\"demo_mode\": demoMode == \"true\",\n\t})\n}\n\n// GetInitialSetupStatus 获取初始配置状态\nfunc GetInitialSetupStatus(c *gin.Context) {\n\t// 检查是否完成初始配置\n\tsetupCompleted := model.GetConfigValueByKey(\"initial_setup_completed\", \"0\")\n\n\t// 检查webhook_url是否已配置\n\twebhookURL := model.GetConfigValueByKey(\"webhook_url\", \"\")\n\twebhookConfigured := webhookURL != \"\"\n\n\t// 检查是否有Zabbix实例\n\tinstances, err := model.GetAllZabbixInstances()\n\tzabbixConfigured := err == nil && len(instances) > 0\n\n\tresponse.Success(c, gin.H{\n\t\t\"setup_completed\": setupCompleted == \"1\",\n\t\t\"webhook_configured\": webhookConfigured,\n\t\t\"zabbix_configured\": zabbixConfigured,\n\t})\n}\n\n// CompleteInitialSetup 标记初始配置已完成\nfunc CompleteInitialSetup(c *gin.Context) {\n\t// 查找initial_setup_completed配置项\n\tconfigs, err := model.GetConfigList()\n\tif err != nil {\n\t\tresponse.InternalError(c, \"获取配置失败\")\n\t\treturn\n\t}\n\n\tvar configID int64\n\tfound := false\n\tfor _, config := range configs {\n\t\tif config.ConfigKey == \"initial_setup_completed\" {\n\t\t\tconfigID = config.ID\n\t\t\tfound = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif !found {\n\t\tresponse.InternalError(c, \"配置项不存在\")\n\t\treturn\n\t}\n\n\t// 更新配置\n\tv := model.Config{ID: configID, ConfigValue: \"1\"}\n\terr = model.UpdateConfig(&v)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"更新配置失败\")\n\t\treturn\n\t}\n\n\tresponse.SuccessWithMessage(c, \"初始配置已完成\", nil)\n}\n\n// TestEmailConfig 测试邮件配置\nfunc TestEmailConfig(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\ttestEmail := gjson.Get(string(body), \"test_email\").String()\n\tif testEmail == \"\" {\n\t\tresponse.BadRequest(c, \"请输入测试邮箱地址\")\n\t\treturn\n\t}\n\n\t// 发送测试邮件\n\terr = model.SendTestEmail(testEmail)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"邮件发送失败: \"+err.Error())\n\t\treturn\n\t}\n\n\tresponse.SuccessWithMessage(c, \"测试邮件发送成功,请检查邮箱\", nil)\n}\n\n// TestWechatConfig 测试企业微信配置\nfunc TestWechatConfig(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\ttestUserID := gjson.Get(string(body), \"test_user_id\").String()\n\tif testUserID == \"\" {\n\t\tresponse.BadRequest(c, \"请输入测试用户ID\")\n\t\treturn\n\t}\n\n\t// 发送测试企业微信消息\n\terr = model.SendTestWechat(testUserID)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"企业微信消息发送失败: \"+err.Error())\n\t\treturn\n\t}\n\n\tresponse.SuccessWithMessage(c, \"测试消息发送成功,请检查企业微信\", nil)\n}\n"}, "7d796f32870cd59162ccfcc4cddefbd8111b1c0a": {"t": 1785754884.2411952, "n": 14440, "h": "6e5c15be4d56e5a3", "c": "package handler\n\nimport (\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"zbxtable/internal/model\"\n\t\"zbxtable/pkg/response\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/tidwall/gjson\"\n)\n\n// GetAllSystem 获取系统配置列表\nfunc GetAllSystem(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tcnt, val, err := model.GetALlSystem()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"ok\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = cnt\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetSystemByID 获取单个系统配置\nfunc GetSystemByID(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\tvar SystemRes model.SystemList\n\tval, err := model.GetSystemByID(int64(id))\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"ok\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// UpdateSystem 更新系统配置(支持多实例)\nfunc parseSystemBody(body string) model.System {\n\tzid, _ := strconv.Atoi(gjson.Get(body, \"zid\").String())\n\treturn model.System{\n\t\tZID: zid,\n\t\tName: gjson.Get(body, \"name\").String(),\n\t\tTypeCode: gjson.Get(body, \"type_code\").String(),\n\t\tGroupID: gjson.Get(body, \"group_id\").String(),\n\t\tCPUCore: gjson.Get(body, \"cpu_core\").String(),\n\t\tCPUUtilizationID: gjson.Get(body, \"cpu_utilization_id\").String(),\n\t\tMemoryTotalID: gjson.Get(body, \"memory_total_id\").String(),\n\t\tMemoryUsedID: gjson.Get(body, \"memory_used_id\").String(),\n\t\tMemoryUtilizationID: gjson.Get(body, \"memory_utilization_id\").String(),\n\t\tUptimeID: gjson.Get(body, \"uptime_id\").String(),\n\t\tModel: gjson.Get(body, \"model\").String(),\n\t\tPingTemplateID: gjson.Get(body, \"ping_template_id\").String(),\n\t\tAutoInit: int(gjson.Get(body, \"auto_init\").Int()),\n\t\tInitCron: gjson.Get(body, \"init_cron\").String(),\n\t\tInitOnNewHost: int(gjson.Get(body, \"init_on_new_host\").Int()),\n\t\tMaxRetry: int(gjson.Get(body, \"max_retry\").Int()),\n\t}\n}\n\nfunc UpdateSystem(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\n\tif gjson.Get(string(body), \"zid\").String() == \"\" {\n\t\tresponse.BadRequest(c, \"请选择 Zabbix 实例\")\n\t\treturn\n\t}\n\n\tv := parseSystemBody(string(body))\n\tv.ID = int64(id)\n\n\tvar SystemRes model.SystemList\n\tif err = model.CreateOrUpdateSystem(&v); err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"更新成功\"\n\t\t// 调度器重载已在 model.CreateOrUpdateSystem 内触发,此处无需重复\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// SystemInit 初始化系统(支持多实例)\nfunc SystemInit(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\n\t// 读取请求体获取实例ID\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tzidStr := gjson.Get(string(body), \"zid\").String()\n\tif zidStr == \"\" {\n\t\tresponse.BadRequest(c, \"请选择 Zabbix 实例\")\n\t\treturn\n\t}\n\tzid, _ := strconv.Atoi(zidStr)\n\n\tvar SystemRes model.SystemList\n\terr = model.SystemInitWithInstance(int64(id), int(zid))\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t\tSystemRes.Data.Total = 0\n\t\tSystemRes.Data.Items = \"\"\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"初始化完成\"\n\t\tSystemRes.Data.Total = 0\n\t\tSystemRes.Data.Items = \"\"\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// CreateSystem 创建新的资产绑定配置\nfunc CreateSystem(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\tif gjson.Get(string(body), \"zid\").String() == \"\" {\n\t\tresponse.BadRequest(c, \"请选择 Zabbix 实例\")\n\t\treturn\n\t}\n\tif gjson.Get(string(body), \"type_code\").String() == \"\" {\n\t\tresponse.BadRequest(c, \"请选择资产类型\")\n\t\treturn\n\t}\n\tv := parseSystemBody(string(body))\n\tif err := model.CreateSystem(&v); err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"创建成功\", v)\n}\n\n// GetSystemHistory 获取资产绑定初始化历史\nfunc GetSystemHistory(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\tpage, _ := strconv.Atoi(c.DefaultQuery(\"page\", \"1\"))\n\tpageSize, _ := strconv.Atoi(c.DefaultQuery(\"limit\", \"20\"))\n\tif page < 1 {\n\t\tpage = 1\n\t}\n\tif pageSize < 1 || pageSize > 100 {\n\t\tpageSize = 20\n\t}\n\n\tlist, total, err := model.GetSystemHistory(id, page, pageSize)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.Success(c, map[string]interface{}{\"items\": list, \"total\": total})\n}\n\n// DeleteSystem 删除资产绑定配置\nfunc DeleteSystem(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\tif err := model.DeleteSystem(id); err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"删除成功\", nil)\n}\n\n// GetEgress 获取出口带宽配置\nfunc GetEgress(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tval, err := model.GetEgress()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"获取成功\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetAllConfig 获取系统参数配置\nfunc GetAllConfig(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tval, err := model.GetConfigList()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tfilteredConfigs := sanitizeConfigsForResponse(val)\n\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"获取成功\"\n\t\tSystemRes.Data.Items = filteredConfigs\n\t\tSystemRes.Data.Total = int64(len(filteredConfigs))\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\nfunc sanitizeConfigsForResponse(configs []model.Config) []model.Config {\n\tfilteredConfigs := make([]model.Config, 0, len(configs))\n\tfor _, config := range configs {\n\t\tif config.ConfigKey == \"encryption_key\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tif modelSensitiveConfigKey(config.ConfigKey) && config.ConfigValue != \"\" {\n\t\t\tconfig.ConfigValue = \"********\"\n\t\t}\n\n\t\tfilteredConfigs = append(filteredConfigs, config)\n\t}\n\treturn filteredConfigs\n}\n\nfunc modelSensitiveConfigKey(key string) bool {\n\tswitch key {\n\tcase \"email_secret\", \"wechat_secret\", \"deepseek_api_key\", \"custom_api_key\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\n// UpdateConfig 更新系统参数配置\nfunc UpdateConfig(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tvalue := gjson.Get(string(body), \"config_value\").String()\n\n\t// 如果值是星号(脱敏标记),则不更新\n\t// 这表示前端没有修改该敏感字段\n\tif value == \"********\" {\n\t\tvar SystemRes model.SystemList\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"未修改敏感字段,保持原值\"\n\t\tSystemRes.Data.Items = \"\"\n\t\tSystemRes.Data.Total = 1\n\t\tc.JSON(http.StatusOK, SystemRes)\n\t\treturn\n\t}\n\n\tvar SystemRes model.SystemList\n\tv := model.Config{ID: int64(id), ConfigValue: value}\n\terr = model.UpdateConfig(&v)\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"更新成功\"\n\t\tSystemRes.Data.Items = \"\"\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// ============ 新的出口配置 API ============\n\n// GetAllEgressConfigs 获取所有出口配置\nfunc GetAllEgressConfigs(c *gin.Context) {\n\tconfigs, err := model.GetAllEgress()\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.Success(c, configs)\n}\n\n// GetEgressConfigByID 根据ID获取出口配置\nfunc GetEgressConfigByID(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\n\tconfig, err := model.GetEgressConfigByID(id)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.Success(c, config)\n}\n\n// AddEgressConfig 添加出口配置\nfunc AddEgressConfig(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tname := gjson.Get(string(body), \"name\").String()\n\tzidStr := gjson.Get(string(body), \"zid\").String()\n\tzid, _ := strconv.Atoi(zidStr)\n\thostID := gjson.Get(string(body), \"host_id\").String()\n\tinItemID := gjson.Get(string(body), \"in_item_id\").String()\n\toutItemID := gjson.Get(string(body), \"out_item_id\").String()\n\tsortOrder := int(gjson.Get(string(body), \"sort_order\").Int())\n\n\tif name == \"\" || zidStr == \"\" || hostID == \"\" || inItemID == \"\" || outItemID == \"\" {\n\t\tresponse.BadRequest(c, \"缺少必填字段\")\n\t\treturn\n\t}\n\n\tconfig := &model.Egress{\n\t\tName: name,\n\t\tZID: zid,\n\t\tHostID: hostID,\n\t\tInItemID: inItemID,\n\t\tOutItemID: outItemID,\n\t\tStatus: 1,\n\t\tSortOrder: sortOrder,\n\t}\n\n\terr = model.AddEgress(config)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"添加成功\", config)\n}\n\n// UpdateEgressConfigHandler 更新出口配置\nfunc UpdateEgressConfigHandler(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tname := gjson.Get(string(body), \"name\").String()\n\tzidStr := gjson.Get(string(body), \"zid\").String()\n\thostID := gjson.Get(string(body), \"host_id\").String()\n\tinItemID := gjson.Get(string(body), \"in_item_id\").String()\n\toutItemID := gjson.Get(string(body), \"out_item_id\").String()\n\tstatus := int(gjson.Get(string(body), \"status\").Int())\n\tsortOrder := int(gjson.Get(string(body), \"sort_order\").Int())\n\n\tif name == \"\" || zidStr == \"\" || hostID == \"\" || inItemID == \"\" || outItemID == \"\" {\n\t\tresponse.BadRequest(c, \"缺少必填字段\")\n\t\treturn\n\t}\n\tzid, _ := strconv.Atoi(zidStr)\n\n\tconfig := &model.Egress{\n\t\tID: id,\n\t\tName: name,\n\t\tZID: zid,\n\t\tHostID: hostID,\n\t\tInItemID: inItemID,\n\t\tOutItemID: outItemID,\n\t\tStatus: status,\n\t\tSortOrder: sortOrder,\n\t}\n\n\terr = model.UpdateEgress(config)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"更新成功\", config)\n}\n\n// DeleteEgressConfigHandler 删除出口配置\nfunc DeleteEgressConfigHandler(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\n\terr = model.DeleteEgressConfig(id)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"删除成功\", nil)\n}\n\n// UploadLogo 上传系统Logo(保存到 ./upload/logo 并返回访问路径)\nfunc UploadLogo(c *gin.Context) {\n\tfile, err := c.FormFile(\"file\")\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"文件上传失败: \"+err.Error())\n\t\treturn\n\t}\n\n\t// 检查文件类型\n\tcontentType := file.Header.Get(\"Content-Type\")\n\tif contentType != \"image/png\" && contentType != \"image/jpeg\" && contentType != \"image/jpg\" && contentType != \"image/svg+xml\" {\n\t\tresponse.BadRequest(c, \"只支持 PNG、JPG、JPEG 或 SVG 格式的图片\")\n\t\treturn\n\t}\n\n\t// 检查文件大小(限制为2MB)\n\tif file.Size > 2*1024*1024 {\n\t\tresponse.BadRequest(c, \"文件大小不能超过2MB\")\n\t\treturn\n\t}\n\n\tresult, err := model.SaveLogoImage(file)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\n\tresponse.SuccessWithMessage(c, \"上传成功\", gin.H{\n\t\t\"url\": result.Path,\n\t})\n}\n\n// GetPublicSystemInfo 获取系统公开信息(无需认证)\nfunc GetPublicSystemInfo(c *gin.Context) {\n\t// 默认值\n\tsystemName := \"ZbxTable\"\n\tsystemSubtitle := \"Zabbix 监控数据可视化平台\"\n\tsystemLogo := \"/logo.png\"\n\tdemoMode := \"false\"\n\n\t// 尝试从数据库获取配置(如果数据库已初始化)\n\tif model.DB != nil {\n\t\tsystemName = model.GetConfigValueByKey(\"system_name\", \"ZbxTable\")\n\t\tsystemSubtitle = model.GetConfigValueByKey(\"system_subtitle\", \"Zabbix 监控数据可视化平台\")\n\t\tsystemLogo = model.GetConfigValueByKey(\"system_logo\", \"/logo.png\")\n\t\t// 增加 demo_mode 返回\n\t\tdemoMode = model.GetConfKey(\"demo_mode\")\n\t\tif demoMode == \"\" {\n\t\t\tdemoMode = model.GetConfigValueByKey(\"demo_mode\", \"false\")\n\t\t}\n\t}\n\n\tresponse.Success(c, gin.H{\n\t\t\"system_name\": systemName,\n\t\t\"system_subtitle\": systemSubtitle,\n\t\t\"system_logo\": systemLogo,\n\t\t\"demo_mode\": demoMode == \"true\",\n\t})\n}\n\n// GetInitialSetupStatus 获取初始配置状态\nfunc GetInitialSetupStatus(c *gin.Context) {\n\t// 检查是否完成初始配置\n\tsetupCompleted := model.GetConfigValueByKey(\"initial_setup_completed\", \"0\")\n\n\t// 检查webhook_url是否已配置\n\twebhookURL := model.GetConfigValueByKey(\"webhook_url\", \"\")\n\twebhookConfigured := webhookURL != \"\"\n\n\t// 检查是否有Zabbix实例\n\tinstances, err := model.GetAllZabbixInstances()\n\tzabbixConfigured := err == nil && len(instances) > 0\n\n\tresponse.Success(c, gin.H{\n\t\t\"setup_completed\": setupCompleted == \"1\",\n\t\t\"webhook_configured\": webhookConfigured,\n\t\t\"zabbix_configured\": zabbixConfigured,\n\t})\n}\n\n// CompleteInitialSetup 标记初始配置已完成\nfunc CompleteInitialSetup(c *gin.Context) {\n\t// 查找initial_setup_completed配置项\n\tconfigs, err := model.GetConfigList()\n\tif err != nil {\n\t\tresponse.InternalError(c, \"获取配置失败\")\n\t\treturn\n\t}\n\n\tvar configID int64\n\tfound := false\n\tfor _, config := range configs {\n\t\tif config.ConfigKey == \"initial_setup_completed\" {\n\t\t\tconfigID = config.ID\n\t\t\tfound = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif !found {\n\t\tresponse.InternalError(c, \"配置项不存在\")\n\t\treturn\n\t}\n\n\t// 更新配置\n\tv := model.Config{ID: configID, ConfigValue: \"1\"}\n\terr = model.UpdateConfig(&v)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"更新配置失败\")\n\t\treturn\n\t}\n\n\tresponse.SuccessWithMessage(c, \"初始配置已完成\", nil)\n}\n\n// TestEmailConfig 测试邮件配置\nfunc TestEmailConfig(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\ttestEmail := gjson.Get(string(body), \"test_email\").String()\n\tif testEmail == \"\" {\n\t\tresponse.BadRequest(c, \"请输入测试邮箱地址\")\n\t\treturn\n\t}\n\n\t// 发送测试邮件\n\terr = model.SendTestEmail(testEmail)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"邮件发送失败: \"+err.Error())\n\t\treturn\n\t}\n\n\tresponse.SuccessWithMessage(c, \"测试邮件发送成功,请检查邮箱\", nil)\n}\n\n// TestWechatConfig 测试企业微信配置\nfunc TestWechatConfig(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\ttestUserID := gjson.Get(string(body), \"test_user_id\").String()\n\tif testUserID == \"\" {\n\t\tresponse.BadRequest(c, \"请输入测试用户ID\")\n\t\treturn\n\t}\n\n\t// 发送测试企业微信消息\n\terr = model.SendTestWechat(testUserID)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"企业微信消息发送失败: \"+err.Error())\n\t\treturn\n\t}\n\n\tresponse.SuccessWithMessage(c, \"测试消息发送成功,请检查企业微信\", nil)\n}\n"}, "b8180028a12561811542b690573791bee4619413": {"t": 1785754886.6459286, "n": 14924, "h": "e061140597a37b4d", "c": "package handler\n\nimport (\n\t\"encoding/base64\"\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"zbxtable/internal/model\"\n\t\"zbxtable/pkg/response\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/tidwall/gjson\"\n)\n\n// GetAllSystem 获取系统配置列表\nfunc GetAllSystem(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tcnt, val, err := model.GetALlSystem()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"ok\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = cnt\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetSystemByID 获取单个系统配置\nfunc GetSystemByID(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\tvar SystemRes model.SystemList\n\tval, err := model.GetSystemByID(int64(id))\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"ok\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// UpdateSystem 更新系统配置(支持多实例)\nfunc parseSystemBody(body string) model.System {\n\tzid, _ := strconv.Atoi(gjson.Get(body, \"zid\").String())\n\treturn model.System{\n\t\tZID: zid,\n\t\tName: gjson.Get(body, \"name\").String(),\n\t\tTypeCode: gjson.Get(body, \"type_code\").String(),\n\t\tGroupID: gjson.Get(body, \"group_id\").String(),\n\t\tCPUCore: gjson.Get(body, \"cpu_core\").String(),\n\t\tCPUUtilizationID: gjson.Get(body, \"cpu_utilization_id\").String(),\n\t\tMemoryTotalID: gjson.Get(body, \"memory_total_id\").String(),\n\t\tMemoryUsedID: gjson.Get(body, \"memory_used_id\").String(),\n\t\tMemoryUtilizationID: gjson.Get(body, \"memory_utilization_id\").String(),\n\t\tUptimeID: gjson.Get(body, \"uptime_id\").String(),\n\t\tModel: gjson.Get(body, \"model\").String(),\n\t\tPingTemplateID: gjson.Get(body, \"ping_template_id\").String(),\n\t\tAutoInit: int(gjson.Get(body, \"auto_init\").Int()),\n\t\tInitCron: gjson.Get(body, \"init_cron\").String(),\n\t\tInitOnNewHost: int(gjson.Get(body, \"init_on_new_host\").Int()),\n\t\tMaxRetry: int(gjson.Get(body, \"max_retry\").Int()),\n\t}\n}\n\nfunc UpdateSystem(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\n\tif gjson.Get(string(body), \"zid\").String() == \"\" {\n\t\tresponse.BadRequest(c, \"请选择 Zabbix 实例\")\n\t\treturn\n\t}\n\n\tv := parseSystemBody(string(body))\n\tv.ID = int64(id)\n\n\tvar SystemRes model.SystemList\n\tif err = model.CreateOrUpdateSystem(&v); err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"更新成功\"\n\t\t// 调度器重载已在 model.CreateOrUpdateSystem 内触发,此处无需重复\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// SystemInit 初始化系统(支持多实例)\nfunc SystemInit(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\n\t// 读取请求体获取实例ID\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tzidStr := gjson.Get(string(body), \"zid\").String()\n\tif zidStr == \"\" {\n\t\tresponse.BadRequest(c, \"请选择 Zabbix 实例\")\n\t\treturn\n\t}\n\tzid, _ := strconv.Atoi(zidStr)\n\n\tvar SystemRes model.SystemList\n\terr = model.SystemInitWithInstance(int64(id), int(zid))\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t\tSystemRes.Data.Total = 0\n\t\tSystemRes.Data.Items = \"\"\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"初始化完成\"\n\t\tSystemRes.Data.Total = 0\n\t\tSystemRes.Data.Items = \"\"\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// CreateSystem 创建新的资产绑定配置\nfunc CreateSystem(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\tif gjson.Get(string(body), \"zid\").String() == \"\" {\n\t\tresponse.BadRequest(c, \"请选择 Zabbix 实例\")\n\t\treturn\n\t}\n\tif gjson.Get(string(body), \"type_code\").String() == \"\" {\n\t\tresponse.BadRequest(c, \"请选择资产类型\")\n\t\treturn\n\t}\n\tv := parseSystemBody(string(body))\n\tif err := model.CreateSystem(&v); err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"创建成功\", v)\n}\n\n// GetSystemHistory 获取资产绑定初始化历史\nfunc GetSystemHistory(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\tpage, _ := strconv.Atoi(c.DefaultQuery(\"page\", \"1\"))\n\tpageSize, _ := strconv.Atoi(c.DefaultQuery(\"limit\", \"20\"))\n\tif page < 1 {\n\t\tpage = 1\n\t}\n\tif pageSize < 1 || pageSize > 100 {\n\t\tpageSize = 20\n\t}\n\n\tlist, total, err := model.GetSystemHistory(id, page, pageSize)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.Success(c, map[string]interface{}{\"items\": list, \"total\": total})\n}\n\n// DeleteSystem 删除资产绑定配置\nfunc DeleteSystem(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\tif err := model.DeleteSystem(id); err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"删除成功\", nil)\n}\n\n// GetEgress 获取出口带宽配置\nfunc GetEgress(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tval, err := model.GetEgress()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"获取成功\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetAllConfig 获取系统参数配置\nfunc GetAllConfig(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tval, err := model.GetConfigList()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tfilteredConfigs := sanitizeConfigsForResponse(val)\n\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"获取成功\"\n\t\tSystemRes.Data.Items = filteredConfigs\n\t\tSystemRes.Data.Total = int64(len(filteredConfigs))\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\nfunc sanitizeConfigsForResponse(configs []model.Config) []model.Config {\n\tfilteredConfigs := make([]model.Config, 0, len(configs))\n\tfor _, config := range configs {\n\t\tif config.ConfigKey == \"encryption_key\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tif modelSensitiveConfigKey(config.ConfigKey) && config.ConfigValue != \"\" {\n\t\t\tconfig.ConfigValue = \"********\"\n\t\t}\n\n\t\tfilteredConfigs = append(filteredConfigs, config)\n\t}\n\treturn filteredConfigs\n}\n\nfunc modelSensitiveConfigKey(key string) bool {\n\tswitch key {\n\tcase \"email_secret\", \"wechat_secret\", \"deepseek_api_key\", \"custom_api_key\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\n// UpdateConfig 更新系统参数配置\nfunc UpdateConfig(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tvalue := gjson.Get(string(body), \"config_value\").String()\n\n\t// 如果值是星号(脱敏标记),则不更新\n\t// 这表示前端没有修改该敏感字段\n\tif value == \"********\" {\n\t\tvar SystemRes model.SystemList\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"未修改敏感字段,保持原值\"\n\t\tSystemRes.Data.Items = \"\"\n\t\tSystemRes.Data.Total = 1\n\t\tc.JSON(http.StatusOK, SystemRes)\n\t\treturn\n\t}\n\n\tvar SystemRes model.SystemList\n\tv := model.Config{ID: int64(id), ConfigValue: value}\n\terr = model.UpdateConfig(&v)\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"更新成功\"\n\t\tSystemRes.Data.Items = \"\"\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// ============ 新的出口配置 API ============\n\n// GetAllEgressConfigs 获取所有出口配置\nfunc GetAllEgressConfigs(c *gin.Context) {\n\tconfigs, err := model.GetAllEgress()\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.Success(c, configs)\n}\n\n// GetEgressConfigByID 根据ID获取出口配置\nfunc GetEgressConfigByID(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\n\tconfig, err := model.GetEgressConfigByID(id)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.Success(c, config)\n}\n\n// AddEgressConfig 添加出口配置\nfunc AddEgressConfig(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tname := gjson.Get(string(body), \"name\").String()\n\tzidStr := gjson.Get(string(body), \"zid\").String()\n\tzid, _ := strconv.Atoi(zidStr)\n\thostID := gjson.Get(string(body), \"host_id\").String()\n\tinItemID := gjson.Get(string(body), \"in_item_id\").String()\n\toutItemID := gjson.Get(string(body), \"out_item_id\").String()\n\tsortOrder := int(gjson.Get(string(body), \"sort_order\").Int())\n\n\tif name == \"\" || zidStr == \"\" || hostID == \"\" || inItemID == \"\" || outItemID == \"\" {\n\t\tresponse.BadRequest(c, \"缺少必填字段\")\n\t\treturn\n\t}\n\n\tconfig := &model.Egress{\n\t\tName: name,\n\t\tZID: zid,\n\t\tHostID: hostID,\n\t\tInItemID: inItemID,\n\t\tOutItemID: outItemID,\n\t\tStatus: 1,\n\t\tSortOrder: sortOrder,\n\t}\n\n\terr = model.AddEgress(config)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"添加成功\", config)\n}\n\n// UpdateEgressConfigHandler 更新出口配置\nfunc UpdateEgressConfigHandler(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tname := gjson.Get(string(body), \"name\").String()\n\tzidStr := gjson.Get(string(body), \"zid\").String()\n\thostID := gjson.Get(string(body), \"host_id\").String()\n\tinItemID := gjson.Get(string(body), \"in_item_id\").String()\n\toutItemID := gjson.Get(string(body), \"out_item_id\").String()\n\tstatus := int(gjson.Get(string(body), \"status\").Int())\n\tsortOrder := int(gjson.Get(string(body), \"sort_order\").Int())\n\n\tif name == \"\" || zidStr == \"\" || hostID == \"\" || inItemID == \"\" || outItemID == \"\" {\n\t\tresponse.BadRequest(c, \"缺少必填字段\")\n\t\treturn\n\t}\n\tzid, _ := strconv.Atoi(zidStr)\n\n\tconfig := &model.Egress{\n\t\tID: id,\n\t\tName: name,\n\t\tZID: zid,\n\t\tHostID: hostID,\n\t\tInItemID: inItemID,\n\t\tOutItemID: outItemID,\n\t\tStatus: status,\n\t\tSortOrder: sortOrder,\n\t}\n\n\terr = model.UpdateEgress(config)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"更新成功\", config)\n}\n\n// DeleteEgressConfigHandler 删除出口配置\nfunc DeleteEgressConfigHandler(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\n\terr = model.DeleteEgressConfig(id)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"删除成功\", nil)\n}\n\n// UploadLogo 上传系统Logo(存储为base64到数据库)\nfunc UploadLogo(c *gin.Context) {\n\tfile, err := c.FormFile(\"file\")\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"文件上传失败: \"+err.Error())\n\t\treturn\n\t}\n\n\t// 检查文件类型\n\tcontentType := file.Header.Get(\"Content-Type\")\n\tif contentType != \"image/png\" && contentType != \"image/jpeg\" && contentType != \"image/jpg\" && contentType != \"image/svg+xml\" {\n\t\tresponse.BadRequest(c, \"只支持 PNG、JPG、JPEG 或 SVG 格式的图片\")\n\t\treturn\n\t}\n\n\t// 检查文件大小(限制为2MB)\n\tif file.Size > 2*1024*1024 {\n\t\tresponse.BadRequest(c, \"文件大小不能超过2MB\")\n\t\treturn\n\t}\n\n\t// 打开文件并读取内容\n\tfileContent, err := file.Open()\n\tif err != nil {\n\t\tresponse.InternalError(c, \"读取文件失败: \"+err.Error())\n\t\treturn\n\t}\n\tdefer fileContent.Close()\n\n\t// 读取文件字节\n\tfileBytes, err := io.ReadAll(fileContent)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"读取文件内容失败: \"+err.Error())\n\t\treturn\n\t}\n\n\t// 转换为base64\n\tbase64String := base64.StdEncoding.EncodeToString(fileBytes)\n\n\t// 构建完整的Data URL格式(包含MIME类型)\n\tvar dataURL string\n\tswitch contentType {\n\tcase \"image/svg+xml\":\n\t\tdataURL = \"data:image/svg+xml;base64,\" + base64String\n\tcase \"image/png\":\n\t\tdataURL = \"data:image/png;base64,\" + base64String\n\tcase \"image/jpeg\", \"image/jpg\":\n\t\tdataURL = \"data:image/jpeg;base64,\" + base64String\n\tdefault:\n\t\tdataURL = \"data:image/png;base64,\" + base64String\n\t}\n\n\t// 返回base64编码的图片数据\n\tresponse.SuccessWithMessage(c, \"上传成功\", gin.H{\n\t\t\"url\": dataURL,\n\t})\n}\n\n// GetPublicSystemInfo 获取系统公开信息(无需认证)\nfunc GetPublicSystemInfo(c *gin.Context) {\n\t// 默认值\n\tsystemName := \"ZbxTable\"\n\tsystemLogo := \"/logo.png\"\n\tdemoMode := \"false\"\n\n\t// 尝试从数据库获取配置(如果数据库已初始化)\n\tif model.DB != nil {\n\t\tsystemName = model.GetConfigValueByKey(\"system_name\", \"ZbxTable\")\n\t\tsystemLogo = model.GetConfigValueByKey(\"system_logo\", \"/logo.png\")\n\t\t// 增加 demo_mode 返回\n\t\tdemoMode = model.GetConfKey(\"demo_mode\")\n\t\tif demoMode == \"\" {\n\t\t\tdemoMode = model.GetConfigValueByKey(\"demo_mode\", \"false\")\n\t\t}\n\t}\n\n\tresponse.Success(c, gin.H{\n\t\t\"system_name\": systemName,\n\t\t\"system_logo\": systemLogo,\n\t\t\"demo_mode\": demoMode == \"true\",\n\t})\n}\n\n// GetInitialSetupStatus 获取初始配置状态\nfunc GetInitialSetupStatus(c *gin.Context) {\n\t// 检查是否完成初始配置\n\tsetupCompleted := model.GetConfigValueByKey(\"initial_setup_completed\", \"0\")\n\n\t// 检查webhook_url是否已配置\n\twebhookURL := model.GetConfigValueByKey(\"webhook_url\", \"\")\n\twebhookConfigured := webhookURL != \"\"\n\n\t// 检查是否有Zabbix实例\n\tinstances, err := model.GetAllZabbixInstances()\n\tzabbixConfigured := err == nil && len(instances) > 0\n\n\tresponse.Success(c, gin.H{\n\t\t\"setup_completed\": setupCompleted == \"1\",\n\t\t\"webhook_configured\": webhookConfigured,\n\t\t\"zabbix_configured\": zabbixConfigured,\n\t})\n}\n\n// CompleteInitialSetup 标记初始配置已完成\nfunc CompleteInitialSetup(c *gin.Context) {\n\t// 查找initial_setup_completed配置项\n\tconfigs, err := model.GetConfigList()\n\tif err != nil {\n\t\tresponse.InternalError(c, \"获取配置失败\")\n\t\treturn\n\t}\n\n\tvar configID int64\n\tfound := false\n\tfor _, config := range configs {\n\t\tif config.ConfigKey == \"initial_setup_completed\" {\n\t\t\tconfigID = config.ID\n\t\t\tfound = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif !found {\n\t\tresponse.InternalError(c, \"配置项不存在\")\n\t\treturn\n\t}\n\n\t// 更新配置\n\tv := model.Config{ID: configID, ConfigValue: \"1\"}\n\terr = model.UpdateConfig(&v)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"更新配置失败\")\n\t\treturn\n\t}\n\n\tresponse.SuccessWithMessage(c, \"初始配置已完成\", nil)\n}\n\n// TestEmailConfig 测试邮件配置\nfunc TestEmailConfig(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\ttestEmail := gjson.Get(string(body), \"test_email\").String()\n\tif testEmail == \"\" {\n\t\tresponse.BadRequest(c, \"请输入测试邮箱地址\")\n\t\treturn\n\t}\n\n\t// 发送测试邮件\n\terr = model.SendTestEmail(testEmail)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"邮件发送失败: \"+err.Error())\n\t\treturn\n\t}\n\n\tresponse.SuccessWithMessage(c, \"测试邮件发送成功,请检查邮箱\", nil)\n}\n\n// TestWechatConfig 测试企业微信配置\nfunc TestWechatConfig(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\ttestUserID := gjson.Get(string(body), \"test_user_id\").String()\n\tif testUserID == \"\" {\n\t\tresponse.BadRequest(c, \"请输入测试用户ID\")\n\t\treturn\n\t}\n\n\t// 发送测试企业微信消息\n\terr = model.SendTestWechat(testUserID)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"企业微信消息发送失败: \"+err.Error())\n\t\treturn\n\t}\n\n\tresponse.SuccessWithMessage(c, \"测试消息发送成功,请检查企业微信\", nil)\n}\n"}, "6a555a99b0280f3cd63b1281b32175f5525ca6b2": {"t": 1785754888.5566611, "n": 14873, "h": "3c649f681d96a2bc", "c": "package handler\n\nimport (\n\t\"encoding/base64\"\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"zbxtable/internal/model\"\n\t\"zbxtable/pkg/response\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/tidwall/gjson\"\n)\n\n// GetAllSystem 获取系统配置列表\nfunc GetAllSystem(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tcnt, val, err := model.GetALlSystem()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"ok\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = cnt\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetSystemByID 获取单个系统配置\nfunc GetSystemByID(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\tvar SystemRes model.SystemList\n\tval, err := model.GetSystemByID(int64(id))\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"ok\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// UpdateSystem 更新系统配置(支持多实例)\nfunc parseSystemBody(body string) model.System {\n\tzid, _ := strconv.Atoi(gjson.Get(body, \"zid\").String())\n\treturn model.System{\n\t\tZID: zid,\n\t\tName: gjson.Get(body, \"name\").String(),\n\t\tTypeCode: gjson.Get(body, \"type_code\").String(),\n\t\tGroupID: gjson.Get(body, \"group_id\").String(),\n\t\tCPUCore: gjson.Get(body, \"cpu_core\").String(),\n\t\tCPUUtilizationID: gjson.Get(body, \"cpu_utilization_id\").String(),\n\t\tMemoryTotalID: gjson.Get(body, \"memory_total_id\").String(),\n\t\tMemoryUsedID: gjson.Get(body, \"memory_used_id\").String(),\n\t\tMemoryUtilizationID: gjson.Get(body, \"memory_utilization_id\").String(),\n\t\tUptimeID: gjson.Get(body, \"uptime_id\").String(),\n\t\tModel: gjson.Get(body, \"model\").String(),\n\t\tPingTemplateID: gjson.Get(body, \"ping_template_id\").String(),\n\t\tAutoInit: int(gjson.Get(body, \"auto_init\").Int()),\n\t\tInitCron: gjson.Get(body, \"init_cron\").String(),\n\t\tInitOnNewHost: int(gjson.Get(body, \"init_on_new_host\").Int()),\n\t\tMaxRetry: int(gjson.Get(body, \"max_retry\").Int()),\n\t}\n}\n\nfunc UpdateSystem(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\n\tif gjson.Get(string(body), \"zid\").String() == \"\" {\n\t\tresponse.BadRequest(c, \"请选择 Zabbix 实例\")\n\t\treturn\n\t}\n\n\tv := parseSystemBody(string(body))\n\tv.ID = int64(id)\n\n\tvar SystemRes model.SystemList\n\tif err = model.CreateOrUpdateSystem(&v); err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"更新成功\"\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// SystemInit 初始化系统(支持多实例)\nfunc SystemInit(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\n\t// 读取请求体获取实例ID\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tzidStr := gjson.Get(string(body), \"zid\").String()\n\tif zidStr == \"\" {\n\t\tresponse.BadRequest(c, \"请选择 Zabbix 实例\")\n\t\treturn\n\t}\n\tzid, _ := strconv.Atoi(zidStr)\n\n\tvar SystemRes model.SystemList\n\terr = model.SystemInitWithInstance(int64(id), int(zid))\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t\tSystemRes.Data.Total = 0\n\t\tSystemRes.Data.Items = \"\"\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"初始化完成\"\n\t\tSystemRes.Data.Total = 0\n\t\tSystemRes.Data.Items = \"\"\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// CreateSystem 创建新的资产绑定配置\nfunc CreateSystem(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\tif gjson.Get(string(body), \"zid\").String() == \"\" {\n\t\tresponse.BadRequest(c, \"请选择 Zabbix 实例\")\n\t\treturn\n\t}\n\tif gjson.Get(string(body), \"type_code\").String() == \"\" {\n\t\tresponse.BadRequest(c, \"请选择资产类型\")\n\t\treturn\n\t}\n\tv := parseSystemBody(string(body))\n\tif err := model.CreateSystem(&v); err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"创建成功\", v)\n}\n\n// GetSystemHistory 获取资产绑定初始化历史\nfunc GetSystemHistory(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\tpage, _ := strconv.Atoi(c.DefaultQuery(\"page\", \"1\"))\n\tpageSize, _ := strconv.Atoi(c.DefaultQuery(\"limit\", \"20\"))\n\tif page < 1 {\n\t\tpage = 1\n\t}\n\tif pageSize < 1 || pageSize > 100 {\n\t\tpageSize = 20\n\t}\n\n\tlist, total, err := model.GetSystemHistory(id, page, pageSize)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.Success(c, map[string]interface{}{\"items\": list, \"total\": total})\n}\n\n// DeleteSystem 删除资产绑定配置\nfunc DeleteSystem(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\tif err := model.DeleteSystem(id); err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"删除成功\", nil)\n}\n\n// GetEgress 获取出口带宽配置\nfunc GetEgress(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tval, err := model.GetEgress()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"获取成功\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetAllConfig 获取系统参数配置\nfunc GetAllConfig(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tval, err := model.GetConfigList()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tfilteredConfigs := sanitizeConfigsForResponse(val)\n\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"获取成功\"\n\t\tSystemRes.Data.Items = filteredConfigs\n\t\tSystemRes.Data.Total = int64(len(filteredConfigs))\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\nfunc sanitizeConfigsForResponse(configs []model.Config) []model.Config {\n\tfilteredConfigs := make([]model.Config, 0, len(configs))\n\tfor _, config := range configs {\n\t\tif config.ConfigKey == \"encryption_key\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tif modelSensitiveConfigKey(config.ConfigKey) && config.ConfigValue != \"\" {\n\t\t\tconfig.ConfigValue = \"********\"\n\t\t}\n\n\t\tfilteredConfigs = append(filteredConfigs, config)\n\t}\n\treturn filteredConfigs\n}\n\nfunc modelSensitiveConfigKey(key string) bool {\n\tswitch key {\n\tcase \"email_secret\", \"wechat_secret\", \"deepseek_api_key\", \"custom_api_key\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\n// UpdateConfig 更新系统参数配置\nfunc UpdateConfig(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tvalue := gjson.Get(string(body), \"config_value\").String()\n\n\t// 如果值是星号(脱敏标记),则不更新\n\t// 这表示前端没有修改该敏感字段\n\tif value == \"********\" {\n\t\tvar SystemRes model.SystemList\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"未修改敏感字段,保持原值\"\n\t\tSystemRes.Data.Items = \"\"\n\t\tSystemRes.Data.Total = 1\n\t\tc.JSON(http.StatusOK, SystemRes)\n\t\treturn\n\t}\n\n\tvar SystemRes model.SystemList\n\tv := model.Config{ID: int64(id), ConfigValue: value}\n\terr = model.UpdateConfig(&v)\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"更新成功\"\n\t\tSystemRes.Data.Items = \"\"\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// ============ 新的出口配置 API ============\n\n// GetAllEgressConfigs 获取所有出口配置\nfunc GetAllEgressConfigs(c *gin.Context) {\n\tconfigs, err := model.GetAllEgress()\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.Success(c, configs)\n}\n\n// GetEgressConfigByID 根据ID获取出口配置\nfunc GetEgressConfigByID(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\n\tconfig, err := model.GetEgressConfigByID(id)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.Success(c, config)\n}\n\n// AddEgressConfig 添加出口配置\nfunc AddEgressConfig(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tname := gjson.Get(string(body), \"name\").String()\n\tzidStr := gjson.Get(string(body), \"zid\").String()\n\tzid, _ := strconv.Atoi(zidStr)\n\thostID := gjson.Get(string(body), \"host_id\").String()\n\tinItemID := gjson.Get(string(body), \"in_item_id\").String()\n\toutItemID := gjson.Get(string(body), \"out_item_id\").String()\n\tsortOrder := int(gjson.Get(string(body), \"sort_order\").Int())\n\n\tif name == \"\" || zidStr == \"\" || hostID == \"\" || inItemID == \"\" || outItemID == \"\" {\n\t\tresponse.BadRequest(c, \"缺少必填字段\")\n\t\treturn\n\t}\n\n\tconfig := &model.Egress{\n\t\tName: name,\n\t\tZID: zid,\n\t\tHostID: hostID,\n\t\tInItemID: inItemID,\n\t\tOutItemID: outItemID,\n\t\tStatus: 1,\n\t\tSortOrder: sortOrder,\n\t}\n\n\terr = model.AddEgress(config)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"添加成功\", config)\n}\n\n// UpdateEgressConfigHandler 更新出口配置\nfunc UpdateEgressConfigHandler(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tname := gjson.Get(string(body), \"name\").String()\n\tzidStr := gjson.Get(string(body), \"zid\").String()\n\thostID := gjson.Get(string(body), \"host_id\").String()\n\tinItemID := gjson.Get(string(body), \"in_item_id\").String()\n\toutItemID := gjson.Get(string(body), \"out_item_id\").String()\n\tstatus := int(gjson.Get(string(body), \"status\").Int())\n\tsortOrder := int(gjson.Get(string(body), \"sort_order\").Int())\n\n\tif name == \"\" || zidStr == \"\" || hostID == \"\" || inItemID == \"\" || outItemID == \"\" {\n\t\tresponse.BadRequest(c, \"缺少必填字段\")\n\t\treturn\n\t}\n\tzid, _ := strconv.Atoi(zidStr)\n\n\tconfig := &model.Egress{\n\t\tID: id,\n\t\tName: name,\n\t\tZID: zid,\n\t\tHostID: hostID,\n\t\tInItemID: inItemID,\n\t\tOutItemID: outItemID,\n\t\tStatus: status,\n\t\tSortOrder: sortOrder,\n\t}\n\n\terr = model.UpdateEgress(config)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"更新成功\", config)\n}\n\n// DeleteEgressConfigHandler 删除出口配置\nfunc DeleteEgressConfigHandler(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\n\terr = model.DeleteEgressConfig(id)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"删除成功\", nil)\n}\n\n// UploadLogo 上传系统Logo(存储为base64到数据库)\nfunc UploadLogo(c *gin.Context) {\n\tfile, err := c.FormFile(\"file\")\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"文件上传失败: \"+err.Error())\n\t\treturn\n\t}\n\n\t// 检查文件类型\n\tcontentType := file.Header.Get(\"Content-Type\")\n\tif contentType != \"image/png\" && contentType != \"image/jpeg\" && contentType != \"image/jpg\" && contentType != \"image/svg+xml\" {\n\t\tresponse.BadRequest(c, \"只支持 PNG、JPG、JPEG 或 SVG 格式的图片\")\n\t\treturn\n\t}\n\n\t// 检查文件大小(限制为2MB)\n\tif file.Size > 2*1024*1024 {\n\t\tresponse.BadRequest(c, \"文件大小不能超过2MB\")\n\t\treturn\n\t}\n\n\t// 打开文件并读取内容\n\tfileContent, err := file.Open()\n\tif err != nil {\n\t\tresponse.InternalError(c, \"读取文件失败: \"+err.Error())\n\t\treturn\n\t}\n\tdefer fileContent.Close()\n\n\t// 读取文件字节\n\tfileBytes, err := io.ReadAll(fileContent)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"读取文件内容失败: \"+err.Error())\n\t\treturn\n\t}\n\n\t// 转换为base64\n\tbase64String := base64.StdEncoding.EncodeToString(fileBytes)\n\n\t// 构建完整的Data URL格式(包含MIME类型)\n\tvar dataURL string\n\tswitch contentType {\n\tcase \"image/svg+xml\":\n\t\tdataURL = \"data:image/svg+xml;base64,\" + base64String\n\tcase \"image/png\":\n\t\tdataURL = \"data:image/png;base64,\" + base64String\n\tcase \"image/jpeg\", \"image/jpg\":\n\t\tdataURL = \"data:image/jpeg;base64,\" + base64String\n\tdefault:\n\t\tdataURL = \"data:image/png;base64,\" + base64String\n\t}\n\n\t// 返回base64编码的图片数据\n\tresponse.SuccessWithMessage(c, \"上传成功\", gin.H{\n\t\t\"url\": dataURL,\n\t})\n}\n\n// GetPublicSystemInfo 获取系统公开信息(无需认证)\nfunc GetPublicSystemInfo(c *gin.Context) {\n\t// 默认值\n\tsystemName := \"ZbxTable\"\n\tsystemLogo := \"/logo.png\"\n\tdemoMode := \"false\"\n\n\t// 尝试从数据库获取配置(如果数据库已初始化)\n\tif model.DB != nil {\n\t\tsystemName = model.GetConfigValueByKey(\"system_name\", \"ZbxTable\")\n\t\tsystemLogo = model.GetConfigValueByKey(\"system_logo\", \"/logo.png\")\n\t\t// 增加 demo_mode 返回\n\t\tdemoMode = model.GetConfKey(\"demo_mode\")\n\t\tif demoMode == \"\" {\n\t\t\tdemoMode = model.GetConfigValueByKey(\"demo_mode\", \"false\")\n\t\t}\n\t}\n\n\tresponse.Success(c, gin.H{\n\t\t\"system_name\": systemName,\n\t\t\"system_logo\": systemLogo,\n\t\t\"demo_mode\": demoMode == \"true\",\n\t})\n}\n\n// GetInitialSetupStatus 获取初始配置状态\nfunc GetInitialSetupStatus(c *gin.Context) {\n\t// 检查是否完成初始配置\n\tsetupCompleted := model.GetConfigValueByKey(\"initial_setup_completed\", \"0\")\n\n\t// 检查webhook_url是否已配置\n\twebhookURL := model.GetConfigValueByKey(\"webhook_url\", \"\")\n\twebhookConfigured := webhookURL != \"\"\n\n\t// 检查是否有Zabbix实例\n\tinstances, err := model.GetAllZabbixInstances()\n\tzabbixConfigured := err == nil && len(instances) > 0\n\n\tresponse.Success(c, gin.H{\n\t\t\"setup_completed\": setupCompleted == \"1\",\n\t\t\"webhook_configured\": webhookConfigured,\n\t\t\"zabbix_configured\": zabbixConfigured,\n\t})\n}\n\n// CompleteInitialSetup 标记初始配置已完成\nfunc CompleteInitialSetup(c *gin.Context) {\n\t// 查找initial_setup_completed配置项\n\tconfigs, err := model.GetConfigList()\n\tif err != nil {\n\t\tresponse.InternalError(c, \"获取配置失败\")\n\t\treturn\n\t}\n\n\tvar configID int64\n\tfound := false\n\tfor _, config := range configs {\n\t\tif config.ConfigKey == \"initial_setup_completed\" {\n\t\t\tconfigID = config.ID\n\t\t\tfound = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif !found {\n\t\tresponse.InternalError(c, \"配置项不存在\")\n\t\treturn\n\t}\n\n\t// 更新配置\n\tv := model.Config{ID: configID, ConfigValue: \"1\"}\n\terr = model.UpdateConfig(&v)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"更新配置失败\")\n\t\treturn\n\t}\n\n\tresponse.SuccessWithMessage(c, \"初始配置已完成\", nil)\n}\n\n// TestEmailConfig 测试邮件配置\nfunc TestEmailConfig(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\ttestEmail := gjson.Get(string(body), \"test_email\").String()\n\tif testEmail == \"\" {\n\t\tresponse.BadRequest(c, \"请输入测试邮箱地址\")\n\t\treturn\n\t}\n\n\t// 发送测试邮件\n\terr = model.SendTestEmail(testEmail)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"邮件发送失败: \"+err.Error())\n\t\treturn\n\t}\n\n\tresponse.SuccessWithMessage(c, \"测试邮件发送成功,请检查邮箱\", nil)\n}\n\n// TestWechatConfig 测试企业微信配置\nfunc TestWechatConfig(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\ttestUserID := gjson.Get(string(body), \"test_user_id\").String()\n\tif testUserID == \"\" {\n\t\tresponse.BadRequest(c, \"请输入测试用户ID\")\n\t\treturn\n\t}\n\n\t// 发送测试企业微信消息\n\terr = model.SendTestWechat(testUserID)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"企业微信消息发送失败: \"+err.Error())\n\t\treturn\n\t}\n\n\tresponse.SuccessWithMessage(c, \"测试消息发送成功,请检查企业微信\", nil)\n}\n"}, "da91f1a55b83b5d0bfe3e4cb33dec689c3a35d5f": {"t": 1785754891.0571325, "n": 14804, "h": "168d2c8b8b8bfb6a", "c": "package handler\n\nimport (\n\t\"encoding/base64\"\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"zbxtable/internal/model\"\n\t\"zbxtable/pkg/response\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/tidwall/gjson\"\n)\n\n// GetAllSystem 获取系统配置列表\nfunc GetAllSystem(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tcnt, val, err := model.GetALlSystem()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"ok\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = cnt\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetSystemByID 获取单个系统配置\nfunc GetSystemByID(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\tvar SystemRes model.SystemList\n\tval, err := model.GetSystemByID(int64(id))\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"ok\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// UpdateSystem 更新系统配置(支持多实例)\nfunc parseSystemBody(body string) model.System {\n\tzid, _ := strconv.Atoi(gjson.Get(body, \"zid\").String())\n\treturn model.System{\n\t\tZID: zid,\n\t\tName: gjson.Get(body, \"name\").String(),\n\t\tTypeCode: gjson.Get(body, \"type_code\").String(),\n\t\tGroupID: gjson.Get(body, \"group_id\").String(),\n\t\tCPUCore: gjson.Get(body, \"cpu_core\").String(),\n\t\tCPUUtilizationID: gjson.Get(body, \"cpu_utilization_id\").String(),\n\t\tMemoryTotalID: gjson.Get(body, \"memory_total_id\").String(),\n\t\tMemoryUsedID: gjson.Get(body, \"memory_used_id\").String(),\n\t\tMemoryUtilizationID: gjson.Get(body, \"memory_utilization_id\").String(),\n\t\tUptimeID: gjson.Get(body, \"uptime_id\").String(),\n\t\tModel: gjson.Get(body, \"model\").String(),\n\t\tPingTemplateID: gjson.Get(body, \"ping_template_id\").String(),\n\t\tAutoInit: int(gjson.Get(body, \"auto_init\").Int()),\n\t\tInitCron: gjson.Get(body, \"init_cron\").String(),\n\t\tInitOnNewHost: int(gjson.Get(body, \"init_on_new_host\").Int()),\n\t\tMaxRetry: int(gjson.Get(body, \"max_retry\").Int()),\n\t}\n}\n\nfunc UpdateSystem(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\n\tif gjson.Get(string(body), \"zid\").String() == \"\" {\n\t\tresponse.BadRequest(c, \"请选择 Zabbix 实例\")\n\t\treturn\n\t}\n\n\tv := parseSystemBody(string(body))\n\tv.ID = int64(id)\n\n\tvar SystemRes model.SystemList\n\tif err = model.CreateOrUpdateSystem(&v); err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"更新成功\"\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// SystemInit 初始化系统(支持多实例)\nfunc SystemInit(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\n\t// 读取请求体获取实例ID\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tzidStr := gjson.Get(string(body), \"zid\").String()\n\tif zidStr == \"\" {\n\t\tresponse.BadRequest(c, \"请选择 Zabbix 实例\")\n\t\treturn\n\t}\n\tzid, _ := strconv.Atoi(zidStr)\n\n\tvar SystemRes model.SystemList\n\terr = model.SystemInitWithInstance(int64(id), int(zid))\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t\tSystemRes.Data.Total = 0\n\t\tSystemRes.Data.Items = \"\"\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"初始化完成\"\n\t\tSystemRes.Data.Total = 0\n\t\tSystemRes.Data.Items = \"\"\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// CreateSystem 创建新的资产绑定配置\nfunc CreateSystem(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\tif gjson.Get(string(body), \"zid\").String() == \"\" {\n\t\tresponse.BadRequest(c, \"请选择 Zabbix 实例\")\n\t\treturn\n\t}\n\tif gjson.Get(string(body), \"type_code\").String() == \"\" {\n\t\tresponse.BadRequest(c, \"请选择资产类型\")\n\t\treturn\n\t}\n\tv := parseSystemBody(string(body))\n\tif err := model.CreateSystem(&v); err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"创建成功\", v)\n}\n\n// GetSystemHistory 获取资产绑定初始化历史\nfunc GetSystemHistory(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\tpage, _ := strconv.Atoi(c.DefaultQuery(\"page\", \"1\"))\n\tpageSize, _ := strconv.Atoi(c.DefaultQuery(\"limit\", \"20\"))\n\tif page < 1 {\n\t\tpage = 1\n\t}\n\tif pageSize < 1 || pageSize > 100 {\n\t\tpageSize = 20\n\t}\n\n\tlist, total, err := model.GetSystemHistory(id, page, pageSize)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.Success(c, map[string]interface{}{\"items\": list, \"total\": total})\n}\n\n// DeleteSystem 删除资产绑定配置\nfunc DeleteSystem(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\tif err := model.DeleteSystem(id); err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"删除成功\", nil)\n}\n\n// GetEgress 获取出口带宽配置\nfunc GetEgress(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tval, err := model.GetEgress()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"获取成功\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetAllConfig 获取系统参数配置\nfunc GetAllConfig(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tval, err := model.GetConfigList()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\t// 定义敏感字段列表\n\t\tsensitiveKeys := map[string]bool{\n\t\t\t\"email_secret\": true, // SMTP 密码/授权码\n\t\t\t\"wechat_secret\": true, // 企业微信 Secret\n\t\t\t\"deepseek_api_key\": true, // Deepseek API Key\n\t\t\t\"encryption_key\": true, // 加密密钥(完全隐藏)\n\t\t}\n\n\t\t// 过滤和脱敏处理\n\t\tfilteredConfigs := []model.Config{}\n\t\tfor _, config := range val {\n\t\t\t// 完全隐藏加密密钥配置项\n\t\t\tif config.ConfigKey == \"encryption_key\" {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 对敏感字段进行脱敏处理\n\t\t\tif sensitiveKeys[config.ConfigKey] && config.ConfigValue != \"\" {\n\t\t\t\tconfig.ConfigValue = \"********\" // 替换为星号\n\t\t\t}\n\n\t\t\tfilteredConfigs = append(filteredConfigs, config)\n\t\t}\n\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"获取成功\"\n\t\tSystemRes.Data.Items = filteredConfigs\n\t\tSystemRes.Data.Total = int64(len(filteredConfigs))\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// UpdateConfig 更新系统参数配置\nfunc UpdateConfig(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tvalue := gjson.Get(string(body), \"config_value\").String()\n\n\t// 如果值是星号(脱敏标记),则不更新\n\t// 这表示前端没有修改该敏感字段\n\tif value == \"********\" {\n\t\tvar SystemRes model.SystemList\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"未修改敏感字段,保持原值\"\n\t\tSystemRes.Data.Items = \"\"\n\t\tSystemRes.Data.Total = 1\n\t\tc.JSON(http.StatusOK, SystemRes)\n\t\treturn\n\t}\n\n\tvar SystemRes model.SystemList\n\tv := model.Config{ID: int64(id), ConfigValue: value}\n\terr = model.UpdateConfig(&v)\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"更新成功\"\n\t\tSystemRes.Data.Items = \"\"\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// ============ 新的出口配置 API ============\n\n// GetAllEgressConfigs 获取所有出口配置\nfunc GetAllEgressConfigs(c *gin.Context) {\n\tconfigs, err := model.GetAllEgress()\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.Success(c, configs)\n}\n\n// GetEgressConfigByID 根据ID获取出口配置\nfunc GetEgressConfigByID(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\n\tconfig, err := model.GetEgressConfigByID(id)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.Success(c, config)\n}\n\n// AddEgressConfig 添加出口配置\nfunc AddEgressConfig(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tname := gjson.Get(string(body), \"name\").String()\n\tzidStr := gjson.Get(string(body), \"zid\").String()\n\tzid, _ := strconv.Atoi(zidStr)\n\thostID := gjson.Get(string(body), \"host_id\").String()\n\tinItemID := gjson.Get(string(body), \"in_item_id\").String()\n\toutItemID := gjson.Get(string(body), \"out_item_id\").String()\n\tsortOrder := int(gjson.Get(string(body), \"sort_order\").Int())\n\n\tif name == \"\" || zidStr == \"\" || hostID == \"\" || inItemID == \"\" || outItemID == \"\" {\n\t\tresponse.BadRequest(c, \"缺少必填字段\")\n\t\treturn\n\t}\n\n\tconfig := &model.Egress{\n\t\tName: name,\n\t\tZID: zid,\n\t\tHostID: hostID,\n\t\tInItemID: inItemID,\n\t\tOutItemID: outItemID,\n\t\tStatus: 1,\n\t\tSortOrder: sortOrder,\n\t}\n\n\terr = model.AddEgress(config)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"添加成功\", config)\n}\n\n// UpdateEgressConfigHandler 更新出口配置\nfunc UpdateEgressConfigHandler(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tname := gjson.Get(string(body), \"name\").String()\n\tzidStr := gjson.Get(string(body), \"zid\").String()\n\thostID := gjson.Get(string(body), \"host_id\").String()\n\tinItemID := gjson.Get(string(body), \"in_item_id\").String()\n\toutItemID := gjson.Get(string(body), \"out_item_id\").String()\n\tstatus := int(gjson.Get(string(body), \"status\").Int())\n\tsortOrder := int(gjson.Get(string(body), \"sort_order\").Int())\n\n\tif name == \"\" || zidStr == \"\" || hostID == \"\" || inItemID == \"\" || outItemID == \"\" {\n\t\tresponse.BadRequest(c, \"缺少必填字段\")\n\t\treturn\n\t}\n\tzid, _ := strconv.Atoi(zidStr)\n\n\tconfig := &model.Egress{\n\t\tID: id,\n\t\tName: name,\n\t\tZID: zid,\n\t\tHostID: hostID,\n\t\tInItemID: inItemID,\n\t\tOutItemID: outItemID,\n\t\tStatus: status,\n\t\tSortOrder: sortOrder,\n\t}\n\n\terr = model.UpdateEgress(config)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"更新成功\", config)\n}\n\n// DeleteEgressConfigHandler 删除出口配置\nfunc DeleteEgressConfigHandler(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\n\terr = model.DeleteEgressConfig(id)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"删除成功\", nil)\n}\n\n// UploadLogo 上传系统Logo(存储为base64到数据库)\nfunc UploadLogo(c *gin.Context) {\n\tfile, err := c.FormFile(\"file\")\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"文件上传失败: \"+err.Error())\n\t\treturn\n\t}\n\n\t// 检查文件类型\n\tcontentType := file.Header.Get(\"Content-Type\")\n\tif contentType != \"image/png\" && contentType != \"image/jpeg\" && contentType != \"image/jpg\" && contentType != \"image/svg+xml\" {\n\t\tresponse.BadRequest(c, \"只支持 PNG、JPG、JPEG 或 SVG 格式的图片\")\n\t\treturn\n\t}\n\n\t// 检查文件大小(限制为2MB)\n\tif file.Size > 2*1024*1024 {\n\t\tresponse.BadRequest(c, \"文件大小不能超过2MB\")\n\t\treturn\n\t}\n\n\t// 打开文件并读取内容\n\tfileContent, err := file.Open()\n\tif err != nil {\n\t\tresponse.InternalError(c, \"读取文件失败: \"+err.Error())\n\t\treturn\n\t}\n\tdefer fileContent.Close()\n\n\t// 读取文件字节\n\tfileBytes, err := io.ReadAll(fileContent)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"读取文件内容失败: \"+err.Error())\n\t\treturn\n\t}\n\n\t// 转换为base64\n\tbase64String := base64.StdEncoding.EncodeToString(fileBytes)\n\n\t// 构建完整的Data URL格式(包含MIME类型)\n\tvar dataURL string\n\tswitch contentType {\n\tcase \"image/svg+xml\":\n\t\tdataURL = \"data:image/svg+xml;base64,\" + base64String\n\tcase \"image/png\":\n\t\tdataURL = \"data:image/png;base64,\" + base64String\n\tcase \"image/jpeg\", \"image/jpg\":\n\t\tdataURL = \"data:image/jpeg;base64,\" + base64String\n\tdefault:\n\t\tdataURL = \"data:image/png;base64,\" + base64String\n\t}\n\n\t// 返回base64编码的图片数据\n\tresponse.SuccessWithMessage(c, \"上传成功\", gin.H{\n\t\t\"url\": dataURL,\n\t})\n}\n\n// GetPublicSystemInfo 获取系统公开信息(无需认证)\nfunc GetPublicSystemInfo(c *gin.Context) {\n\t// 默认值\n\tsystemName := \"ZbxTable\"\n\tsystemLogo := \"/logo.png\"\n\tdemoMode := \"false\"\n\n\t// 尝试从数据库获取配置(如果数据库已初始化)\n\tif model.DB != nil {\n\t\tsystemName = model.GetConfigValueByKey(\"system_name\", \"ZbxTable\")\n\t\tsystemLogo = model.GetConfigValueByKey(\"system_logo\", \"/logo.png\")\n\t\t// 增加 demo_mode 返回\n\t\tdemoMode = model.GetConfKey(\"demo_mode\")\n\t\tif demoMode == \"\" {\n\t\t\tdemoMode = model.GetConfigValueByKey(\"demo_mode\", \"false\")\n\t\t}\n\t}\n\n\tresponse.Success(c, gin.H{\n\t\t\"system_name\": systemName,\n\t\t\"system_logo\": systemLogo,\n\t\t\"demo_mode\": demoMode == \"true\",\n\t})\n}\n\n// GetInitialSetupStatus 获取初始配置状态\nfunc GetInitialSetupStatus(c *gin.Context) {\n\t// 检查是否完成初始配置\n\tsetupCompleted := model.GetConfigValueByKey(\"initial_setup_completed\", \"0\")\n\n\t// 检查webhook_url是否已配置\n\twebhookURL := model.GetConfigValueByKey(\"webhook_url\", \"\")\n\twebhookConfigured := webhookURL != \"\"\n\n\t// 检查是否有Zabbix实例\n\tinstances, err := model.GetAllZabbixInstances()\n\tzabbixConfigured := err == nil && len(instances) > 0\n\n\tresponse.Success(c, gin.H{\n\t\t\"setup_completed\": setupCompleted == \"1\",\n\t\t\"webhook_configured\": webhookConfigured,\n\t\t\"zabbix_configured\": zabbixConfigured,\n\t})\n}\n\n// CompleteInitialSetup 标记初始配置已完成\nfunc CompleteInitialSetup(c *gin.Context) {\n\t// 查找initial_setup_completed配置项\n\tconfigs, err := model.GetConfigList()\n\tif err != nil {\n\t\tresponse.InternalError(c, \"获取配置失败\")\n\t\treturn\n\t}\n\n\tvar configID int64\n\tfound := false\n\tfor _, config := range configs {\n\t\tif config.ConfigKey == \"initial_setup_completed\" {\n\t\t\tconfigID = config.ID\n\t\t\tfound = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif !found {\n\t\tresponse.InternalError(c, \"配置项不存在\")\n\t\treturn\n\t}\n\n\t// 更新配置\n\tv := model.Config{ID: configID, ConfigValue: \"1\"}\n\terr = model.UpdateConfig(&v)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"更新配置失败\")\n\t\treturn\n\t}\n\n\tresponse.SuccessWithMessage(c, \"初始配置已完成\", nil)\n}\n\n// TestEmailConfig 测试邮件配置\nfunc TestEmailConfig(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\ttestEmail := gjson.Get(string(body), \"test_email\").String()\n\tif testEmail == \"\" {\n\t\tresponse.BadRequest(c, \"请输入测试邮箱地址\")\n\t\treturn\n\t}\n\n\t// 发送测试邮件\n\terr = model.SendTestEmail(testEmail)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"邮件发送失败: \"+err.Error())\n\t\treturn\n\t}\n\n\tresponse.SuccessWithMessage(c, \"测试邮件发送成功,请检查邮箱\", nil)\n}\n\n// TestWechatConfig 测试企业微信配置\nfunc TestWechatConfig(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\ttestUserID := gjson.Get(string(body), \"test_user_id\").String()\n\tif testUserID == \"\" {\n\t\tresponse.BadRequest(c, \"请输入测试用户ID\")\n\t\treturn\n\t}\n\n\t// 发送测试企业微信消息\n\terr = model.SendTestWechat(testUserID)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"企业微信消息发送失败: \"+err.Error())\n\t\treturn\n\t}\n\n\tresponse.SuccessWithMessage(c, \"测试消息发送成功,请检查企业微信\", nil)\n}\n"}, "ee7e679ebb6bf3c448826fd9844c5488f73a943e": {"t": 1785754892.2469552, "n": 13172, "h": "f2eec73ade91ce35", "c": "package handler\n\nimport (\n\t\"encoding/base64\"\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"zbxtable/internal/model\"\n\t\"zbxtable/pkg/response\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/tidwall/gjson\"\n)\n\n// GetAllSystem 获取系统配置列表\nfunc GetAllSystem(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tcnt, val, err := model.GetALlSystem()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"ok\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = cnt\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetSystemByID 获取单个系统配置\nfunc GetSystemByID(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\tvar SystemRes model.SystemList\n\tval, err := model.GetSystemByID(int64(id))\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"ok\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// UpdateSystem 更新系统配置(支持多实例)\nfunc UpdateSystem(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\n\t// 获取实例ID\n\tzidStr := gjson.Get(string(body), \"zid\").String()\n\tif zidStr == \"\" {\n\t\tresponse.BadRequest(c, \"请选择 Zabbix 实例\")\n\t\treturn\n\t}\n\tzid, _ := strconv.ParseInt(zidStr, 10, 64)\n\tcpuCore := gjson.Get(string(body), \"cpu_core\").String()\n\tuptimeId := gjson.Get(string(body), \"uptime_id\").String()\n\tcpuUtilizationId := gjson.Get(string(body), \"cpu_utilization_id\").String()\n\tgroupId := gjson.Get(string(body), \"group_id\").String()\n\tmemoryTotalId := gjson.Get(string(body), \"memory_total_id\").String()\n\tmemoryUsedId := gjson.Get(string(body), \"memory_used_id\").String()\n\tmemoryUtilizationId := gjson.Get(string(body), \"memory_utilization_id\").String()\n\tmode := gjson.Get(string(body), \"model\").String()\n\tpingTemplateId := gjson.Get(string(body), \"ping_template_id\").String()\n\n\tvar SystemRes model.SystemList\n\tv := model.System{\n\t\tID: int64(id),\n\t\tZID: int(zid),\n\t\tCPUCore: cpuCore,\n\t\tCPUUtilizationID: cpuUtilizationId,\n\t\tGroupID: groupId,\n\t\tMemoryTotalID: memoryTotalId,\n\t\tUptimeID: uptimeId,\n\t\tModel: mode,\n\t\tMemoryUsedID: memoryUsedId,\n\t\tMemoryUtilizationID: memoryUtilizationId,\n\t\tPingTemplateID: pingTemplateId,\n\t}\n\terr = model.CreateOrUpdateSystem(&v)\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"更新成功\"\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// SystemInit 初始化系统(支持多实例)\nfunc SystemInit(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\n\t// 读取请求体获取实例ID\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tzidStr := gjson.Get(string(body), \"zid\").String()\n\tif zidStr == \"\" {\n\t\tresponse.BadRequest(c, \"请选择 Zabbix 实例\")\n\t\treturn\n\t}\n\tzid, _ := strconv.Atoi(zidStr)\n\n\tvar SystemRes model.SystemList\n\terr = model.SystemInitWithInstance(int64(id), int(zid))\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t\tSystemRes.Data.Total = 0\n\t\tSystemRes.Data.Items = \"\"\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"初始化完成\"\n\t\tSystemRes.Data.Total = 0\n\t\tSystemRes.Data.Items = \"\"\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetEgress 获取出口带宽配置\nfunc GetEgress(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tval, err := model.GetEgress()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"获取成功\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetAllConfig 获取系统参数配置\nfunc GetAllConfig(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tval, err := model.GetConfigList()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\t// 定义敏感字段列表\n\t\tsensitiveKeys := map[string]bool{\n\t\t\t\"email_secret\": true, // SMTP 密码/授权码\n\t\t\t\"wechat_secret\": true, // 企业微信 Secret\n\t\t\t\"deepseek_api_key\": true, // Deepseek API Key\n\t\t\t\"encryption_key\": true, // 加密密钥(完全隐藏)\n\t\t}\n\n\t\t// 过滤和脱敏处理\n\t\tfilteredConfigs := []model.Config{}\n\t\tfor _, config := range val {\n\t\t\t// 完全隐藏加密密钥配置项\n\t\t\tif config.ConfigKey == \"encryption_key\" {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 对敏感字段进行脱敏处理\n\t\t\tif sensitiveKeys[config.ConfigKey] && config.ConfigValue != \"\" {\n\t\t\t\tconfig.ConfigValue = \"********\" // 替换为星号\n\t\t\t}\n\n\t\t\tfilteredConfigs = append(filteredConfigs, config)\n\t\t}\n\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"获取成功\"\n\t\tSystemRes.Data.Items = filteredConfigs\n\t\tSystemRes.Data.Total = int64(len(filteredConfigs))\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// UpdateConfig 更新系统参数配置\nfunc UpdateConfig(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tvalue := gjson.Get(string(body), \"config_value\").String()\n\n\t// 如果值是星号(脱敏标记),则不更新\n\t// 这表示前端没有修改该敏感字段\n\tif value == \"********\" {\n\t\tvar SystemRes model.SystemList\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"未修改敏感字段,保持原值\"\n\t\tSystemRes.Data.Items = \"\"\n\t\tSystemRes.Data.Total = 1\n\t\tc.JSON(http.StatusOK, SystemRes)\n\t\treturn\n\t}\n\n\tvar SystemRes model.SystemList\n\tv := model.Config{ID: int64(id), ConfigValue: value}\n\terr = model.UpdateConfig(&v)\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"更新成功\"\n\t\tSystemRes.Data.Items = \"\"\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// ============ 新的出口配置 API ============\n\n// GetAllEgressConfigs 获取所有出口配置\nfunc GetAllEgressConfigs(c *gin.Context) {\n\tconfigs, err := model.GetAllEgress()\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.Success(c, configs)\n}\n\n// GetEgressConfigByID 根据ID获取出口配置\nfunc GetEgressConfigByID(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\n\tconfig, err := model.GetEgressConfigByID(id)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.Success(c, config)\n}\n\n// AddEgressConfig 添加出口配置\nfunc AddEgressConfig(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tname := gjson.Get(string(body), \"name\").String()\n\tzidStr := gjson.Get(string(body), \"zid\").String()\n\tzid, _ := strconv.Atoi(zidStr)\n\thostID := gjson.Get(string(body), \"host_id\").String()\n\tinItemID := gjson.Get(string(body), \"in_item_id\").String()\n\toutItemID := gjson.Get(string(body), \"out_item_id\").String()\n\tsortOrder := int(gjson.Get(string(body), \"sort_order\").Int())\n\n\tif name == \"\" || zidStr == \"\" || hostID == \"\" || inItemID == \"\" || outItemID == \"\" {\n\t\tresponse.BadRequest(c, \"缺少必填字段\")\n\t\treturn\n\t}\n\n\tconfig := &model.Egress{\n\t\tName: name,\n\t\tZID: zid,\n\t\tHostID: hostID,\n\t\tInItemID: inItemID,\n\t\tOutItemID: outItemID,\n\t\tStatus: 1,\n\t\tSortOrder: sortOrder,\n\t}\n\n\terr = model.AddEgress(config)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"添加成功\", config)\n}\n\n// UpdateEgressConfigHandler 更新出口配置\nfunc UpdateEgressConfigHandler(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tname := gjson.Get(string(body), \"name\").String()\n\tzidStr := gjson.Get(string(body), \"zid\").String()\n\thostID := gjson.Get(string(body), \"host_id\").String()\n\tinItemID := gjson.Get(string(body), \"in_item_id\").String()\n\toutItemID := gjson.Get(string(body), \"out_item_id\").String()\n\tstatus := int(gjson.Get(string(body), \"status\").Int())\n\tsortOrder := int(gjson.Get(string(body), \"sort_order\").Int())\n\n\tif name == \"\" || zidStr == \"\" || hostID == \"\" || inItemID == \"\" || outItemID == \"\" {\n\t\tresponse.BadRequest(c, \"缺少必填字段\")\n\t\treturn\n\t}\n\tzid, _ := strconv.Atoi(zidStr)\n\n\tconfig := &model.Egress{\n\t\tID: id,\n\t\tName: name,\n\t\tZID: zid,\n\t\tHostID: hostID,\n\t\tInItemID: inItemID,\n\t\tOutItemID: outItemID,\n\t\tStatus: status,\n\t\tSortOrder: sortOrder,\n\t}\n\n\terr = model.UpdateEgress(config)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"更新成功\", config)\n}\n\n// DeleteEgressConfigHandler 删除出口配置\nfunc DeleteEgressConfigHandler(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\n\terr = model.DeleteEgressConfig(id)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"删除成功\", nil)\n}\n\n// UploadLogo 上传系统Logo(存储为base64到数据库)\nfunc UploadLogo(c *gin.Context) {\n\tfile, err := c.FormFile(\"file\")\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"文件上传失败: \"+err.Error())\n\t\treturn\n\t}\n\n\t// 检查文件类型\n\tcontentType := file.Header.Get(\"Content-Type\")\n\tif contentType != \"image/png\" && contentType != \"image/jpeg\" && contentType != \"image/jpg\" && contentType != \"image/svg+xml\" {\n\t\tresponse.BadRequest(c, \"只支持 PNG、JPG、JPEG 或 SVG 格式的图片\")\n\t\treturn\n\t}\n\n\t// 检查文件大小(限制为2MB)\n\tif file.Size > 2*1024*1024 {\n\t\tresponse.BadRequest(c, \"文件大小不能超过2MB\")\n\t\treturn\n\t}\n\n\t// 打开文件并读取内容\n\tfileContent, err := file.Open()\n\tif err != nil {\n\t\tresponse.InternalError(c, \"读取文件失败: \"+err.Error())\n\t\treturn\n\t}\n\tdefer fileContent.Close()\n\n\t// 读取文件字节\n\tfileBytes, err := io.ReadAll(fileContent)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"读取文件内容失败: \"+err.Error())\n\t\treturn\n\t}\n\n\t// 转换为base64\n\tbase64String := base64.StdEncoding.EncodeToString(fileBytes)\n\n\t// 构建完整的Data URL格式(包含MIME类型)\n\tvar dataURL string\n\tswitch contentType {\n\tcase \"image/svg+xml\":\n\t\tdataURL = \"data:image/svg+xml;base64,\" + base64String\n\tcase \"image/png\":\n\t\tdataURL = \"data:image/png;base64,\" + base64String\n\tcase \"image/jpeg\", \"image/jpg\":\n\t\tdataURL = \"data:image/jpeg;base64,\" + base64String\n\tdefault:\n\t\tdataURL = \"data:image/png;base64,\" + base64String\n\t}\n\n\t// 返回base64编码的图片数据\n\tresponse.SuccessWithMessage(c, \"上传成功\", gin.H{\n\t\t\"url\": dataURL,\n\t})\n}\n\n// GetPublicSystemInfo 获取系统公开信息(无需认证)\nfunc GetPublicSystemInfo(c *gin.Context) {\n\t// 默认值\n\tsystemName := \"ZbxTable\"\n\tsystemLogo := \"/logo.png\"\n\tdemoMode := \"false\"\n\n\t// 尝试从数据库获取配置(如果数据库已初始化)\n\tif model.DB != nil {\n\t\tsystemName = model.GetConfigValueByKey(\"system_name\", \"ZbxTable\")\n\t\tsystemLogo = model.GetConfigValueByKey(\"system_logo\", \"/logo.png\")\n\t\t// 增加 demo_mode 返回\n\t\tdemoMode = model.GetConfKey(\"demo_mode\")\n\t\tif demoMode == \"\" {\n\t\t\tdemoMode = model.GetConfigValueByKey(\"demo_mode\", \"false\")\n\t\t}\n\t}\n\n\tresponse.Success(c, gin.H{\n\t\t\"system_name\": systemName,\n\t\t\"system_logo\": systemLogo,\n\t\t\"demo_mode\": demoMode == \"true\",\n\t})\n}\n\n// GetInitialSetupStatus 获取初始配置状态\nfunc GetInitialSetupStatus(c *gin.Context) {\n\t// 检查是否完成初始配置\n\tsetupCompleted := model.GetConfigValueByKey(\"initial_setup_completed\", \"0\")\n\n\t// 检查webhook_url是否已配置\n\twebhookURL := model.GetConfigValueByKey(\"webhook_url\", \"\")\n\twebhookConfigured := webhookURL != \"\"\n\n\t// 检查是否有Zabbix实例\n\tinstances, err := model.GetAllZabbixInstances()\n\tzabbixConfigured := err == nil && len(instances) > 0\n\n\tresponse.Success(c, gin.H{\n\t\t\"setup_completed\": setupCompleted == \"1\",\n\t\t\"webhook_configured\": webhookConfigured,\n\t\t\"zabbix_configured\": zabbixConfigured,\n\t})\n}\n\n// CompleteInitialSetup 标记初始配置已完成\nfunc CompleteInitialSetup(c *gin.Context) {\n\t// 查找initial_setup_completed配置项\n\tconfigs, err := model.GetConfigList()\n\tif err != nil {\n\t\tresponse.InternalError(c, \"获取配置失败\")\n\t\treturn\n\t}\n\n\tvar configID int64\n\tfound := false\n\tfor _, config := range configs {\n\t\tif config.ConfigKey == \"initial_setup_completed\" {\n\t\t\tconfigID = config.ID\n\t\t\tfound = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif !found {\n\t\tresponse.InternalError(c, \"配置项不存在\")\n\t\treturn\n\t}\n\n\t// 更新配置\n\tv := model.Config{ID: configID, ConfigValue: \"1\"}\n\terr = model.UpdateConfig(&v)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"更新配置失败\")\n\t\treturn\n\t}\n\n\tresponse.SuccessWithMessage(c, \"初始配置已完成\", nil)\n}\n\n// TestEmailConfig 测试邮件配置\nfunc TestEmailConfig(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\ttestEmail := gjson.Get(string(body), \"test_email\").String()\n\tif testEmail == \"\" {\n\t\tresponse.BadRequest(c, \"请输入测试邮箱地址\")\n\t\treturn\n\t}\n\n\t// 发送测试邮件\n\terr = model.SendTestEmail(testEmail)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"邮件发送失败: \"+err.Error())\n\t\treturn\n\t}\n\n\tresponse.SuccessWithMessage(c, \"测试邮件发送成功,请检查邮箱\", nil)\n}\n\n// TestWechatConfig 测试企业微信配置\nfunc TestWechatConfig(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\ttestUserID := gjson.Get(string(body), \"test_user_id\").String()\n\tif testUserID == \"\" {\n\t\tresponse.BadRequest(c, \"请输入测试用户ID\")\n\t\treturn\n\t}\n\n\t// 发送测试企业微信消息\n\terr = model.SendTestWechat(testUserID)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"企业微信消息发送失败: \"+err.Error())\n\t\treturn\n\t}\n\n\tresponse.SuccessWithMessage(c, \"测试消息发送成功,请检查企业微信\", nil)\n}\n"}, "3f1b74075fed0c95be23b2a7e624627a4b8b7df1": {"t": 1785754893.8144739, "n": 13194, "h": "31ae60d137be71ae", "c": "package handler\n\nimport (\n\t\"encoding/base64\"\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"zbxtable/internal/model\"\n\t\"zbxtable/pkg/response\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/tidwall/gjson\"\n)\n\n// GetAllSystem 获取系统配置列表\nfunc GetAllSystem(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tcnt, val, err := model.GetALlSystem()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"ok\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = cnt\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetSystemByID 获取单个系统配置\nfunc GetSystemByID(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\tvar SystemRes model.SystemList\n\tval, err := model.GetSystemByID(int64(id))\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"ok\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// UpdateSystem 更新系统配置(支持多实例)\nfunc UpdateSystem(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\n\t// 获取实例ID\n\tzidStr := gjson.Get(string(body), \"zid\").String()\n\tif zidStr == \"\" {\n\t\tresponse.BadRequest(c, \"请选择 Zabbix 实例\")\n\t\treturn\n\t}\n\tzid, _ := strconv.ParseInt(zidStr, 10, 64)\n\tcpuCore := gjson.Get(string(body), \"cpu_core\").String()\n\tuptimeId := gjson.Get(string(body), \"uptime_id\").String()\n\tcpuUtilizationId := gjson.Get(string(body), \"cpu_utilization_id\").String()\n\tgroupId := gjson.Get(string(body), \"group_id\").String()\n\tmemoryTotalId := gjson.Get(string(body), \"memory_total_id\").String()\n\tmemoryUsedId := gjson.Get(string(body), \"memory_used_id\").String()\n\tmemoryUtilizationId := gjson.Get(string(body), \"memory_utilization_id\").String()\n\tmode := gjson.Get(string(body), \"model\").String()\n\tpingTemplateId := gjson.Get(string(body), \"ping_template_id\").String()\n\n\tvar SystemRes model.SystemList\n\tv := model.System{\n\t\tID: int64(id),\n\t\tZID: int(zid),\n\t\tCPUCore: cpuCore,\n\t\tCPUUtilizationID: cpuUtilizationId,\n\t\tGroupID: groupId,\n\t\tMemoryTotalID: memoryTotalId,\n\t\tUptimeID: uptimeId,\n\t\tModel: mode,\n\t\tMemoryUsedID: memoryUsedId,\n\t\tMemoryUtilizationID: memoryUtilizationId,\n\t\tPingTemplateID: pingTemplateId,\n\t}\n\terr = model.CreateOrUpdateSystem(&v)\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"更新成功\"\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// SystemInit 初始化系统(支持多实例)\nfunc SystemInit(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\n\t// 读取请求体获取实例ID\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tzidStr := gjson.Get(string(body), \"zid\").String()\n\tif zidStr == \"\" {\n\t\tresponse.BadRequest(c, \"请选择 Zabbix 实例\")\n\t\treturn\n\t}\n\tzid, _ := strconv.Atoi(zidStr)\n\n\tvar SystemRes model.SystemList\n\terr = model.SystemInitWithInstance(int64(id), int(zid))\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t\tSystemRes.Data.Total = 0\n\t\tSystemRes.Data.Items = \"\"\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"初始化完成\"\n\t\tSystemRes.Data.Total = 0\n\t\tSystemRes.Data.Items = \"\"\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetEgress 获取出口带宽配置\nfunc GetEgress(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tval, err := model.GetEgress()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"获取成功\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetAllConfig 获取系统参数配置\nfunc GetAllConfig(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tval, err := model.GetConfigList()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\t// 定义敏感字段列表\n\t\tsensitiveKeys := map[string]bool{\n\t\t\t\"email_secret\": true, // SMTP 密码/授权码\n\t\t\t\"wechat_secret\": true, // 企业微信 Secret\n\t\t\t\"deepseek_api_key\": true, // Deepseek API Key\n\t\t\t\"encryption_key\": true, // 加密密钥(完全隐藏)\n\t\t}\n\n\t\t// 过滤和脱敏处理\n\t\tfilteredConfigs := []model.Config{}\n\t\tfor _, config := range val {\n\t\t\t// 完全隐藏加密密钥配置项\n\t\t\tif config.ConfigKey == \"encryption_key\" {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 对敏感字段进行脱敏处理\n\t\t\tif sensitiveKeys[config.ConfigKey] && config.ConfigValue != \"\" {\n\t\t\t\tconfig.ConfigValue = \"********\" // 替换为星号\n\t\t\t}\n\n\t\t\tfilteredConfigs = append(filteredConfigs, config)\n\t\t}\n\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"获取成功\"\n\t\tSystemRes.Data.Items = filteredConfigs\n\t\tSystemRes.Data.Total = int64(len(filteredConfigs))\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// UpdateConfig 更新系统参数配置\nfunc UpdateConfig(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tvalue := gjson.Get(string(body), \"config_value\").String()\n\n\t// 如果值是星号(脱敏标记),则不更新\n\t// 这表示前端没有修改该敏感字段\n\tif value == \"********\" {\n\t\tvar SystemRes model.SystemList\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"未修改敏感字段,保持原值\"\n\t\tSystemRes.Data.Items = \"\"\n\t\tSystemRes.Data.Total = 1\n\t\tc.JSON(http.StatusOK, SystemRes)\n\t\treturn\n\t}\n\n\tvar SystemRes model.SystemList\n\tv := model.Config{ID: int64(id), ConfigValue: value}\n\terr = model.UpdateConfig(&v)\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"更新成功\"\n\t\tSystemRes.Data.Items = \"\"\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// ============ 新的出口配置 API ============\n\n// GetAllEgressConfigs 获取所有出口配置\nfunc GetAllEgressConfigs(c *gin.Context) {\n\tconfigs, err := model.GetAllEgress()\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.Success(c, configs)\n}\n\n// GetEgressConfigByID 根据ID获取出口配置\nfunc GetEgressConfigByID(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\n\tconfig, err := model.GetEgressConfigByID(id)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.Success(c, config)\n}\n\n// AddEgressConfig 添加出口配置\nfunc AddEgressConfig(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tname := gjson.Get(string(body), \"name\").String()\n\tzidStr := gjson.Get(string(body), \"zid\").String()\n\tzid, _ := strconv.Atoi(zidStr)\n\thostID := gjson.Get(string(body), \"host_id\").String()\n\tinItemID := gjson.Get(string(body), \"in_item_id\").String()\n\toutItemID := gjson.Get(string(body), \"out_item_id\").String()\n\tsortOrder := int(gjson.Get(string(body), \"sort_order\").Int())\n\n\tif name == \"\" || zidStr == \"\" || hostID == \"\" || inItemID == \"\" || outItemID == \"\" {\n\t\tresponse.BadRequest(c, \"缺少必填字段\")\n\t\treturn\n\t}\n\n\tconfig := &model.Egress{\n\t\tName: name,\n\t\tZID: zid,\n\t\tHostID: hostID,\n\t\tInItemID: inItemID,\n\t\tOutItemID: outItemID,\n\t\tStatus: 1,\n\t\tSortOrder: sortOrder,\n\t}\n\n\terr = model.AddEgress(config)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"添加成功\", config)\n}\n\n// UpdateEgressConfigHandler 更新出口配置\nfunc UpdateEgressConfigHandler(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tname := gjson.Get(string(body), \"name\").String()\n\tzidStr := gjson.Get(string(body), \"zid\").String()\n\thostID := gjson.Get(string(body), \"host_id\").String()\n\tinItemID := gjson.Get(string(body), \"in_item_id\").String()\n\toutItemID := gjson.Get(string(body), \"out_item_id\").String()\n\tstatus := int(gjson.Get(string(body), \"status\").Int())\n\tsortOrder := int(gjson.Get(string(body), \"sort_order\").Int())\n\n\tif name == \"\" || zidStr == \"\" || hostID == \"\" || inItemID == \"\" || outItemID == \"\" {\n\t\tresponse.BadRequest(c, \"缺少必填字段\")\n\t\treturn\n\t}\n\tzid, _ := strconv.Atoi(zidStr)\n\n\tconfig := &model.Egress{\n\t\tID: id,\n\t\tName: name,\n\t\tZID: zid,\n\t\tHostID: hostID,\n\t\tInItemID: inItemID,\n\t\tOutItemID: outItemID,\n\t\tStatus: status,\n\t\tSortOrder: sortOrder,\n\t}\n\n\terr = model.UpdateEgress(config)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"更新成功\", config)\n}\n\n// DeleteEgressConfigHandler 删除出口配置\nfunc DeleteEgressConfigHandler(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\n\terr = model.DeleteEgressConfig(id)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"删除成功\", nil)\n}\n\n// UploadLogo 上传系统Logo(存储为base64到数据库)\nfunc UploadLogo(c *gin.Context) {\n\tfile, err := c.FormFile(\"file\")\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"文件上传失败: \"+err.Error())\n\t\treturn\n\t}\n\n\t// 检查文件类型\n\tcontentType := file.Header.Get(\"Content-Type\")\n\tif contentType != \"image/png\" && contentType != \"image/jpeg\" && contentType != \"image/jpg\" && contentType != \"image/svg+xml\" {\n\t\tresponse.BadRequest(c, \"只支持 PNG、JPG、JPEG 或 SVG 格式的图片\")\n\t\treturn\n\t}\n\n\t// 检查文件大小(限制为2MB)\n\tif file.Size > 2*1024*1024 {\n\t\tresponse.BadRequest(c, \"文件大小不能超过2MB\")\n\t\treturn\n\t}\n\n\t// 打开文件并读取内容\n\tfileContent, err := file.Open()\n\tif err != nil {\n\t\tresponse.InternalError(c, \"读取文件失败: \"+err.Error())\n\t\treturn\n\t}\n\tdefer fileContent.Close()\n\n\t// 读取文件字节\n\tfileBytes, err := io.ReadAll(fileContent)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"读取文件内容失败: \"+err.Error())\n\t\treturn\n\t}\n\n\t// 转换为base64\n\tbase64String := base64.StdEncoding.EncodeToString(fileBytes)\n\n\t// 构建完整的Data URL格式(包含MIME类型)\n\tvar dataURL string\n\tswitch contentType {\n\tcase \"image/svg+xml\":\n\t\tdataURL = \"data:image/svg+xml;base64,\" + base64String\n\tcase \"image/png\":\n\t\tdataURL = \"data:image/png;base64,\" + base64String\n\tcase \"image/jpeg\", \"image/jpg\":\n\t\tdataURL = \"data:image/jpeg;base64,\" + base64String\n\tdefault:\n\t\tdataURL = \"data:image/png;base64,\" + base64String\n\t}\n\n\t// 返回base64编码的图片数据\n\tresponse.SuccessWithMessage(c, \"上传成功\", gin.H{\n\t\t\"url\": dataURL,\n\t})\n}\n\n// GetPublicSystemInfo 获取系统公开信息(无需认证)\nfunc GetPublicSystemInfo(c *gin.Context) {\n\t// 默认值\n\tsystemName := \"ZbxTable\"\n\tsystemLogo := \"/static/img/logo.png\"\n\tdemoMode := \"false\"\n\n\t// 尝试从数据库获取配置(如果数据库已初始化)\n\tif model.DB != nil {\n\t\tsystemName = model.GetConfigValueByKey(\"system_name\", \"ZbxTable\")\n\t\tsystemLogo = model.GetConfigValueByKey(\"system_logo\", \"/static/img/logo.png\")\n\t\t// 增加 demo_mode 返回\n\t\tdemoMode = model.GetConfKey(\"demo_mode\")\n\t\tif demoMode == \"\" {\n\t\t\tdemoMode = model.GetConfigValueByKey(\"demo_mode\", \"false\")\n\t\t}\n\t}\n\n\tresponse.Success(c, gin.H{\n\t\t\"system_name\": systemName,\n\t\t\"system_logo\": systemLogo,\n\t\t\"demo_mode\": demoMode == \"true\",\n\t})\n}\n\n// GetInitialSetupStatus 获取初始配置状态\nfunc GetInitialSetupStatus(c *gin.Context) {\n\t// 检查是否完成初始配置\n\tsetupCompleted := model.GetConfigValueByKey(\"initial_setup_completed\", \"0\")\n\n\t// 检查webhook_url是否已配置\n\twebhookURL := model.GetConfigValueByKey(\"webhook_url\", \"\")\n\twebhookConfigured := webhookURL != \"\"\n\n\t// 检查是否有Zabbix实例\n\tinstances, err := model.GetAllZabbixInstances()\n\tzabbixConfigured := err == nil && len(instances) > 0\n\n\tresponse.Success(c, gin.H{\n\t\t\"setup_completed\": setupCompleted == \"1\",\n\t\t\"webhook_configured\": webhookConfigured,\n\t\t\"zabbix_configured\": zabbixConfigured,\n\t})\n}\n\n// CompleteInitialSetup 标记初始配置已完成\nfunc CompleteInitialSetup(c *gin.Context) {\n\t// 查找initial_setup_completed配置项\n\tconfigs, err := model.GetConfigList()\n\tif err != nil {\n\t\tresponse.InternalError(c, \"获取配置失败\")\n\t\treturn\n\t}\n\n\tvar configID int64\n\tfound := false\n\tfor _, config := range configs {\n\t\tif config.ConfigKey == \"initial_setup_completed\" {\n\t\t\tconfigID = config.ID\n\t\t\tfound = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif !found {\n\t\tresponse.InternalError(c, \"配置项不存在\")\n\t\treturn\n\t}\n\n\t// 更新配置\n\tv := model.Config{ID: configID, ConfigValue: \"1\"}\n\terr = model.UpdateConfig(&v)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"更新配置失败\")\n\t\treturn\n\t}\n\n\tresponse.SuccessWithMessage(c, \"初始配置已完成\", nil)\n}\n\n// TestEmailConfig 测试邮件配置\nfunc TestEmailConfig(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\ttestEmail := gjson.Get(string(body), \"test_email\").String()\n\tif testEmail == \"\" {\n\t\tresponse.BadRequest(c, \"请输入测试邮箱地址\")\n\t\treturn\n\t}\n\n\t// 发送测试邮件\n\terr = model.SendTestEmail(testEmail)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"邮件发送失败: \"+err.Error())\n\t\treturn\n\t}\n\n\tresponse.SuccessWithMessage(c, \"测试邮件发送成功,请检查邮箱\", nil)\n}\n\n// TestWechatConfig 测试企业微信配置\nfunc TestWechatConfig(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\ttestUserID := gjson.Get(string(body), \"test_user_id\").String()\n\tif testUserID == \"\" {\n\t\tresponse.BadRequest(c, \"请输入测试用户ID\")\n\t\treturn\n\t}\n\n\t// 发送测试企业微信消息\n\terr = model.SendTestWechat(testUserID)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"企业微信消息发送失败: \"+err.Error())\n\t\treturn\n\t}\n\n\tresponse.SuccessWithMessage(c, \"测试消息发送成功,请检查企业微信\", nil)\n}\n"}, "e95145171b92fcb09a4d1e3fba862a0525a92d18": {"t": 1785754895.4045477, "n": 12984, "h": "a11655ab8448083b", "c": "package handler\n\nimport (\n\t\"encoding/base64\"\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"zbxtable/internal/model\"\n\t\"zbxtable/pkg/response\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/tidwall/gjson\"\n)\n\n// GetAllSystem 获取系统配置列表\nfunc GetAllSystem(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tcnt, val, err := model.GetALlSystem()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"ok\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = cnt\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetSystemByID 获取单个系统配置\nfunc GetSystemByID(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\tvar SystemRes model.SystemList\n\tval, err := model.GetSystemByID(int64(id))\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"ok\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// UpdateSystem 更新系统配置(支持多实例)\nfunc UpdateSystem(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\n\t// 获取实例ID\n\tzidStr := gjson.Get(string(body), \"zid\").String()\n\tif zidStr == \"\" {\n\t\tresponse.BadRequest(c, \"请选择 Zabbix 实例\")\n\t\treturn\n\t}\n\tzid, _ := strconv.ParseInt(zidStr, 10, 64)\n\tcpuCore := gjson.Get(string(body), \"cpu_core\").String()\n\tuptimeId := gjson.Get(string(body), \"uptime_id\").String()\n\tcpuUtilizationId := gjson.Get(string(body), \"cpu_utilization_id\").String()\n\tgroupId := gjson.Get(string(body), \"group_id\").String()\n\tmemoryTotalId := gjson.Get(string(body), \"memory_total_id\").String()\n\tmemoryUsedId := gjson.Get(string(body), \"memory_used_id\").String()\n\tmemoryUtilizationId := gjson.Get(string(body), \"memory_utilization_id\").String()\n\tmode := gjson.Get(string(body), \"model\").String()\n\tpingTemplateId := gjson.Get(string(body), \"ping_template_id\").String()\n\n\tvar SystemRes model.SystemList\n\tv := model.System{\n\t\tID: int64(id),\n\t\tZID: int(zid),\n\t\tCPUCore: cpuCore,\n\t\tCPUUtilizationID: cpuUtilizationId,\n\t\tGroupID: groupId,\n\t\tMemoryTotalID: memoryTotalId,\n\t\tUptimeID: uptimeId,\n\t\tModel: mode,\n\t\tMemoryUsedID: memoryUsedId,\n\t\tMemoryUtilizationID: memoryUtilizationId,\n\t\tPingTemplateID: pingTemplateId,\n\t}\n\terr = model.CreateOrUpdateSystem(&v)\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"更新成功\"\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// SystemInit 初始化系统(支持多实例)\nfunc SystemInit(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\n\t// 读取请求体获取实例ID\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tzidStr := gjson.Get(string(body), \"zid\").String()\n\tif zidStr == \"\" {\n\t\tresponse.BadRequest(c, \"请选择 Zabbix 实例\")\n\t\treturn\n\t}\n\tzid, _ := strconv.Atoi(zidStr)\n\n\tvar SystemRes model.SystemList\n\terr = model.SystemInitWithInstance(int64(id), int(zid))\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t\tSystemRes.Data.Total = 0\n\t\tSystemRes.Data.Items = \"\"\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"初始化完成\"\n\t\tSystemRes.Data.Total = 0\n\t\tSystemRes.Data.Items = \"\"\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetEgress 获取出口带宽配置\nfunc GetEgress(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tval, err := model.GetEgress()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"获取成功\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetAllConfig 获取系统参数配置\nfunc GetAllConfig(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tval, err := model.GetConfigList()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\t// 定义敏感字段列表\n\t\tsensitiveKeys := map[string]bool{\n\t\t\t\"email_secret\": true, // SMTP 密码/授权码\n\t\t\t\"wechat_secret\": true, // 企业微信 Secret\n\t\t\t\"deepseek_api_key\": true, // Deepseek API Key\n\t\t\t\"encryption_key\": true, // 加密密钥(完全隐藏)\n\t\t}\n\n\t\t// 过滤和脱敏处理\n\t\tfilteredConfigs := []model.Config{}\n\t\tfor _, config := range val {\n\t\t\t// 完全隐藏加密密钥配置项\n\t\t\tif config.ConfigKey == \"encryption_key\" {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 对敏感字段进行脱敏处理\n\t\t\tif sensitiveKeys[config.ConfigKey] && config.ConfigValue != \"\" {\n\t\t\t\tconfig.ConfigValue = \"********\" // 替换为星号\n\t\t\t}\n\n\t\t\tfilteredConfigs = append(filteredConfigs, config)\n\t\t}\n\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"获取成功\"\n\t\tSystemRes.Data.Items = filteredConfigs\n\t\tSystemRes.Data.Total = int64(len(filteredConfigs))\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// UpdateConfig 更新系统参数配置\nfunc UpdateConfig(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tvalue := gjson.Get(string(body), \"config_value\").String()\n\n\t// 如果值是星号(脱敏标记),则不更新\n\t// 这表示前端没有修改该敏感字段\n\tif value == \"********\" {\n\t\tvar SystemRes model.SystemList\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"未修改敏感字段,保持原值\"\n\t\tSystemRes.Data.Items = \"\"\n\t\tSystemRes.Data.Total = 1\n\t\tc.JSON(http.StatusOK, SystemRes)\n\t\treturn\n\t}\n\n\tvar SystemRes model.SystemList\n\tv := model.Config{ID: int64(id), ConfigValue: value}\n\terr = model.UpdateConfig(&v)\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"更新成功\"\n\t\tSystemRes.Data.Items = \"\"\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// ============ 新的出口配置 API ============\n\n// GetAllEgressConfigs 获取所有出口配置\nfunc GetAllEgressConfigs(c *gin.Context) {\n\tconfigs, err := model.GetAllEgress()\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.Success(c, configs)\n}\n\n// GetEgressConfigByID 根据ID获取出口配置\nfunc GetEgressConfigByID(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\n\tconfig, err := model.GetEgressConfigByID(id)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.Success(c, config)\n}\n\n// AddEgressConfig 添加出口配置\nfunc AddEgressConfig(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tname := gjson.Get(string(body), \"name\").String()\n\tzidStr := gjson.Get(string(body), \"zid\").String()\n\tzid, _ := strconv.Atoi(zidStr)\n\thostID := gjson.Get(string(body), \"host_id\").String()\n\tinItemID := gjson.Get(string(body), \"in_item_id\").String()\n\toutItemID := gjson.Get(string(body), \"out_item_id\").String()\n\tsortOrder := int(gjson.Get(string(body), \"sort_order\").Int())\n\n\tif name == \"\" || zidStr == \"\" || hostID == \"\" || inItemID == \"\" || outItemID == \"\" {\n\t\tresponse.BadRequest(c, \"缺少必填字段\")\n\t\treturn\n\t}\n\n\tconfig := &model.Egress{\n\t\tName: name,\n\t\tZID: zid,\n\t\tHostID: hostID,\n\t\tInItemID: inItemID,\n\t\tOutItemID: outItemID,\n\t\tStatus: 1,\n\t\tSortOrder: sortOrder,\n\t}\n\n\terr = model.AddEgress(config)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"添加成功\", config)\n}\n\n// UpdateEgressConfigHandler 更新出口配置\nfunc UpdateEgressConfigHandler(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tname := gjson.Get(string(body), \"name\").String()\n\tzidStr := gjson.Get(string(body), \"zid\").String()\n\thostID := gjson.Get(string(body), \"host_id\").String()\n\tinItemID := gjson.Get(string(body), \"in_item_id\").String()\n\toutItemID := gjson.Get(string(body), \"out_item_id\").String()\n\tstatus := int(gjson.Get(string(body), \"status\").Int())\n\tsortOrder := int(gjson.Get(string(body), \"sort_order\").Int())\n\n\tif name == \"\" || zidStr == \"\" || hostID == \"\" || inItemID == \"\" || outItemID == \"\" {\n\t\tresponse.BadRequest(c, \"缺少必填字段\")\n\t\treturn\n\t}\n\tzid, _ := strconv.Atoi(zidStr)\n\n\tconfig := &model.Egress{\n\t\tID: id,\n\t\tName: name,\n\t\tZID: zid,\n\t\tHostID: hostID,\n\t\tInItemID: inItemID,\n\t\tOutItemID: outItemID,\n\t\tStatus: status,\n\t\tSortOrder: sortOrder,\n\t}\n\n\terr = model.UpdateEgress(config)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"更新成功\", config)\n}\n\n// DeleteEgressConfigHandler 删除出口配置\nfunc DeleteEgressConfigHandler(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\n\terr = model.DeleteEgressConfig(id)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"删除成功\", nil)\n}\n\n// UploadLogo 上传系统Logo(存储为base64到数据库)\nfunc UploadLogo(c *gin.Context) {\n\tfile, err := c.FormFile(\"file\")\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"文件上传失败: \"+err.Error())\n\t\treturn\n\t}\n\n\t// 检查文件类型\n\tcontentType := file.Header.Get(\"Content-Type\")\n\tif contentType != \"image/png\" && contentType != \"image/jpeg\" && contentType != \"image/jpg\" && contentType != \"image/svg+xml\" {\n\t\tresponse.BadRequest(c, \"只支持 PNG、JPG、JPEG 或 SVG 格式的图片\")\n\t\treturn\n\t}\n\n\t// 检查文件大小(限制为2MB)\n\tif file.Size > 2*1024*1024 {\n\t\tresponse.BadRequest(c, \"文件大小不能超过2MB\")\n\t\treturn\n\t}\n\n\t// 打开文件并读取内容\n\tfileContent, err := file.Open()\n\tif err != nil {\n\t\tresponse.InternalError(c, \"读取文件失败: \"+err.Error())\n\t\treturn\n\t}\n\tdefer fileContent.Close()\n\n\t// 读取文件字节\n\tfileBytes, err := io.ReadAll(fileContent)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"读取文件内容失败: \"+err.Error())\n\t\treturn\n\t}\n\n\t// 转换为base64\n\tbase64String := base64.StdEncoding.EncodeToString(fileBytes)\n\n\t// 构建完整的Data URL格式(包含MIME类型)\n\tvar dataURL string\n\tswitch contentType {\n\tcase \"image/svg+xml\":\n\t\tdataURL = \"data:image/svg+xml;base64,\" + base64String\n\tcase \"image/png\":\n\t\tdataURL = \"data:image/png;base64,\" + base64String\n\tcase \"image/jpeg\", \"image/jpg\":\n\t\tdataURL = \"data:image/jpeg;base64,\" + base64String\n\tdefault:\n\t\tdataURL = \"data:image/png;base64,\" + base64String\n\t}\n\n\t// 返回base64编码的图片数据\n\tresponse.SuccessWithMessage(c, \"上传成功\", gin.H{\n\t\t\"url\": dataURL,\n\t})\n}\n\n// GetPublicSystemInfo 获取系统公开信息(无需认证)\nfunc GetPublicSystemInfo(c *gin.Context) {\n\t// 默认值\n\tsystemName := \"ZbxTable\"\n\tsystemLogo := \"/static/img/logo.png\"\n\n\t// 尝试从数据库获取配置(如果数据库已初始化)\n\tif model.DB != nil {\n\t\tsystemName = model.GetConfigValueByKey(\"system_name\", \"ZbxTable\")\n\t\tsystemLogo = model.GetConfigValueByKey(\"system_logo\", \"/static/img/logo.png\")\n\t}\n\n\tresponse.Success(c, gin.H{\n\t\t\"system_name\": systemName,\n\t\t\"system_logo\": systemLogo,\n\t})\n}\n\n// GetInitialSetupStatus 获取初始配置状态\nfunc GetInitialSetupStatus(c *gin.Context) {\n\t// 检查是否完成初始配置\n\tsetupCompleted := model.GetConfigValueByKey(\"initial_setup_completed\", \"0\")\n\n\t// 检查webhook_url是否已配置\n\twebhookURL := model.GetConfigValueByKey(\"webhook_url\", \"\")\n\twebhookConfigured := webhookURL != \"\"\n\n\t// 检查是否有Zabbix实例\n\tinstances, err := model.GetAllZabbixInstances()\n\tzabbixConfigured := err == nil && len(instances) > 0\n\n\tresponse.Success(c, gin.H{\n\t\t\"setup_completed\": setupCompleted == \"1\",\n\t\t\"webhook_configured\": webhookConfigured,\n\t\t\"zabbix_configured\": zabbixConfigured,\n\t})\n}\n\n// CompleteInitialSetup 标记初始配置已完成\nfunc CompleteInitialSetup(c *gin.Context) {\n\t// 查找initial_setup_completed配置项\n\tconfigs, err := model.GetConfigList()\n\tif err != nil {\n\t\tresponse.InternalError(c, \"获取配置失败\")\n\t\treturn\n\t}\n\n\tvar configID int64\n\tfound := false\n\tfor _, config := range configs {\n\t\tif config.ConfigKey == \"initial_setup_completed\" {\n\t\t\tconfigID = config.ID\n\t\t\tfound = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif !found {\n\t\tresponse.InternalError(c, \"配置项不存在\")\n\t\treturn\n\t}\n\n\t// 更新配置\n\tv := model.Config{ID: configID, ConfigValue: \"1\"}\n\terr = model.UpdateConfig(&v)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"更新配置失败\")\n\t\treturn\n\t}\n\n\tresponse.SuccessWithMessage(c, \"初始配置已完成\", nil)\n}\n\n// TestEmailConfig 测试邮件配置\nfunc TestEmailConfig(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\ttestEmail := gjson.Get(string(body), \"test_email\").String()\n\tif testEmail == \"\" {\n\t\tresponse.BadRequest(c, \"请输入测试邮箱地址\")\n\t\treturn\n\t}\n\n\t// 发送测试邮件\n\terr = model.SendTestEmail(testEmail)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"邮件发送失败: \"+err.Error())\n\t\treturn\n\t}\n\n\tresponse.SuccessWithMessage(c, \"测试邮件发送成功,请检查邮箱\", nil)\n}\n\n// TestWechatConfig 测试企业微信配置\nfunc TestWechatConfig(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\ttestUserID := gjson.Get(string(body), \"test_user_id\").String()\n\tif testUserID == \"\" {\n\t\tresponse.BadRequest(c, \"请输入测试用户ID\")\n\t\treturn\n\t}\n\n\t// 发送测试企业微信消息\n\terr = model.SendTestWechat(testUserID)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"企业微信消息发送失败: \"+err.Error())\n\t\treturn\n\t}\n\n\tresponse.SuccessWithMessage(c, \"测试消息发送成功,请检查企业微信\", nil)\n}\n"}, "8bfb5b8ac688fa9cae258e25cc7b5155ac6dc950": {"t": 1785754896.9651883, "n": 12935, "h": "aacf7b4ba3aefd56", "c": "package handler\n\nimport (\n\t\"encoding/base64\"\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"zbxtable/internal/model\"\n\t\"zbxtable/pkg/response\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/tidwall/gjson\"\n)\n\n// GetAllSystem 获取系统配置列表\nfunc GetAllSystem(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tcnt, val, err := model.GetALlSystem()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"ok\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = cnt\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetSystemByID 获取单个系统配置\nfunc GetSystemByID(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\tvar SystemRes model.SystemList\n\tval, err := model.GetSystemByID(int64(id))\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"ok\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// UpdateSystem 更新系统配置(支持多实例)\nfunc UpdateSystem(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\n\t// 获取实例ID\n\tzidStr := gjson.Get(string(body), \"zid\").String()\n\tif zidStr == \"\" {\n\t\tresponse.BadRequest(c, \"请选择 Zabbix 实例\")\n\t\treturn\n\t}\n\tzid, _ := strconv.ParseInt(zidStr, 10, 64)\n\tcpuCore := gjson.Get(string(body), \"cpu_core\").String()\n\tuptimeId := gjson.Get(string(body), \"uptime_id\").String()\n\tcpuUtilizationId := gjson.Get(string(body), \"cpu_utilization_id\").String()\n\tgroupId := gjson.Get(string(body), \"group_id\").String()\n\tmemoryTotalId := gjson.Get(string(body), \"memory_total_id\").String()\n\tmemoryUsedId := gjson.Get(string(body), \"memory_used_id\").String()\n\tmemoryUtilizationId := gjson.Get(string(body), \"memory_utilization_id\").String()\n\tmode := gjson.Get(string(body), \"model\").String()\n\tpingTemplateId := gjson.Get(string(body), \"ping_template_id\").String()\n\n\tvar SystemRes model.SystemList\n\tv := model.System{\n\t\tID: int64(id),\n\t\tZID: int(zid),\n\t\tCPUCore: cpuCore,\n\t\tCPUUtilizationID: cpuUtilizationId,\n\t\tGroupID: groupId,\n\t\tMemoryTotalID: memoryTotalId,\n\t\tUptimeID: uptimeId,\n\t\tModel: mode,\n\t\tMemoryUsedID: memoryUsedId,\n\t\tMemoryUtilizationID: memoryUtilizationId,\n\t\tPingTemplateID: pingTemplateId,\n\t}\n\terr = model.CreateOrUpdateSystem(&v)\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"更新成功\"\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// SystemInit 初始化系统(支持多实例)\nfunc SystemInit(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\n\t// 读取请求体获取实例ID\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tzidStr := gjson.Get(string(body), \"zid\").String()\n\tif zidStr == \"\" {\n\t\tresponse.BadRequest(c, \"请选择 Zabbix 实例\")\n\t\treturn\n\t}\n\tzid, _ := strconv.Atoi(zidStr)\n\n\tvar SystemRes model.SystemList\n\terr = model.SystemInitWithInstance(int64(id), int(zid))\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t\tSystemRes.Data.Total = 0\n\t\tSystemRes.Data.Items = \"\"\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"初始化完成\"\n\t\tSystemRes.Data.Total = 0\n\t\tSystemRes.Data.Items = \"\"\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetEgress 获取出口带宽配置\nfunc GetEgress(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tval, err := model.GetEgress()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"获取成功\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetAllConfig 获取系统参数配置\nfunc GetAllConfig(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tval, err := model.GetConfigList()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\t// 定义敏感字段列表\n\t\tsensitiveKeys := map[string]bool{\n\t\t\t\"email_secret\": true, // SMTP 密码/授权码\n\t\t\t\"wechat_secret\": true, // 企业微信 Secret\n\t\t\t\"deepseek_api_key\": true, // Deepseek API Key\n\t\t\t\"encryption_key\": true, // 加密密钥(完全隐藏)\n\t\t}\n\n\t\t// 过滤和脱敏处理\n\t\tfilteredConfigs := []model.Config{}\n\t\tfor _, config := range val {\n\t\t\t// 完全隐藏加密密钥配置项\n\t\t\tif config.Key == \"encryption_key\" {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 对敏感字段进行脱敏处理\n\t\t\tif sensitiveKeys[config.Key] && config.Value != \"\" {\n\t\t\t\tconfig.Value = \"********\" // 替换为星号\n\t\t\t}\n\n\t\t\tfilteredConfigs = append(filteredConfigs, config)\n\t\t}\n\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"获取成功\"\n\t\tSystemRes.Data.Items = filteredConfigs\n\t\tSystemRes.Data.Total = int64(len(filteredConfigs))\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// UpdateConfig 更新系统参数配置\nfunc UpdateConfig(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tvalue := gjson.Get(string(body), \"value\").String()\n\n\t// 如果值是星号(脱敏标记),则不更新\n\t// 这表示前端没有修改该敏感字段\n\tif value == \"********\" {\n\t\tvar SystemRes model.SystemList\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"未修改敏感字段,保持原值\"\n\t\tSystemRes.Data.Items = \"\"\n\t\tSystemRes.Data.Total = 1\n\t\tc.JSON(http.StatusOK, SystemRes)\n\t\treturn\n\t}\n\n\tvar SystemRes model.SystemList\n\tv := model.Config{ID: int64(id), Value: value}\n\terr = model.UpdateConfig(&v)\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"更新成功\"\n\t\tSystemRes.Data.Items = \"\"\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// ============ 新的出口配置 API ============\n\n// GetAllEgressConfigs 获取所有出口配置\nfunc GetAllEgressConfigs(c *gin.Context) {\n\tconfigs, err := model.GetAllEgress()\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.Success(c, configs)\n}\n\n// GetEgressConfigByID 根据ID获取出口配置\nfunc GetEgressConfigByID(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\n\tconfig, err := model.GetEgressConfigByID(id)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.Success(c, config)\n}\n\n// AddEgressConfig 添加出口配置\nfunc AddEgressConfig(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tname := gjson.Get(string(body), \"name\").String()\n\tzidStr := gjson.Get(string(body), \"zid\").String()\n\tzid, _ := strconv.Atoi(zidStr)\n\thostID := gjson.Get(string(body), \"host_id\").String()\n\tinItemID := gjson.Get(string(body), \"in_item_id\").String()\n\toutItemID := gjson.Get(string(body), \"out_item_id\").String()\n\tsortOrder := int(gjson.Get(string(body), \"sort_order\").Int())\n\n\tif name == \"\" || zidStr == \"\" || hostID == \"\" || inItemID == \"\" || outItemID == \"\" {\n\t\tresponse.BadRequest(c, \"缺少必填字段\")\n\t\treturn\n\t}\n\n\tconfig := &model.Egress{\n\t\tName: name,\n\t\tZID: zid,\n\t\tHostID: hostID,\n\t\tInItemID: inItemID,\n\t\tOutItemID: outItemID,\n\t\tStatus: 1,\n\t\tSortOrder: sortOrder,\n\t}\n\n\terr = model.AddEgress(config)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"添加成功\", config)\n}\n\n// UpdateEgressConfigHandler 更新出口配置\nfunc UpdateEgressConfigHandler(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tname := gjson.Get(string(body), \"name\").String()\n\tzidStr := gjson.Get(string(body), \"zid\").String()\n\thostID := gjson.Get(string(body), \"host_id\").String()\n\tinItemID := gjson.Get(string(body), \"in_item_id\").String()\n\toutItemID := gjson.Get(string(body), \"out_item_id\").String()\n\tstatus := int(gjson.Get(string(body), \"status\").Int())\n\tsortOrder := int(gjson.Get(string(body), \"sort_order\").Int())\n\n\tif name == \"\" || zidStr == \"\" || hostID == \"\" || inItemID == \"\" || outItemID == \"\" {\n\t\tresponse.BadRequest(c, \"缺少必填字段\")\n\t\treturn\n\t}\n\tzid, _ := strconv.Atoi(zidStr)\n\n\tconfig := &model.Egress{\n\t\tID: id,\n\t\tName: name,\n\t\tZID: zid,\n\t\tHostID: hostID,\n\t\tInItemID: inItemID,\n\t\tOutItemID: outItemID,\n\t\tStatus: status,\n\t\tSortOrder: sortOrder,\n\t}\n\n\terr = model.UpdateEgress(config)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"更新成功\", config)\n}\n\n// DeleteEgressConfigHandler 删除出口配置\nfunc DeleteEgressConfigHandler(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\n\terr = model.DeleteEgressConfig(id)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"删除成功\", nil)\n}\n\n// UploadLogo 上传系统Logo(存储为base64到数据库)\nfunc UploadLogo(c *gin.Context) {\n\tfile, err := c.FormFile(\"file\")\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"文件上传失败: \"+err.Error())\n\t\treturn\n\t}\n\n\t// 检查文件类型\n\tcontentType := file.Header.Get(\"Content-Type\")\n\tif contentType != \"image/png\" && contentType != \"image/jpeg\" && contentType != \"image/jpg\" && contentType != \"image/svg+xml\" {\n\t\tresponse.BadRequest(c, \"只支持 PNG、JPG、JPEG 或 SVG 格式的图片\")\n\t\treturn\n\t}\n\n\t// 检查文件大小(限制为2MB)\n\tif file.Size > 2*1024*1024 {\n\t\tresponse.BadRequest(c, \"文件大小不能超过2MB\")\n\t\treturn\n\t}\n\n\t// 打开文件并读取内容\n\tfileContent, err := file.Open()\n\tif err != nil {\n\t\tresponse.InternalError(c, \"读取文件失败: \"+err.Error())\n\t\treturn\n\t}\n\tdefer fileContent.Close()\n\n\t// 读取文件字节\n\tfileBytes, err := io.ReadAll(fileContent)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"读取文件内容失败: \"+err.Error())\n\t\treturn\n\t}\n\n\t// 转换为base64\n\tbase64String := base64.StdEncoding.EncodeToString(fileBytes)\n\n\t// 构建完整的Data URL格式(包含MIME类型)\n\tvar dataURL string\n\tswitch contentType {\n\tcase \"image/svg+xml\":\n\t\tdataURL = \"data:image/svg+xml;base64,\" + base64String\n\tcase \"image/png\":\n\t\tdataURL = \"data:image/png;base64,\" + base64String\n\tcase \"image/jpeg\", \"image/jpg\":\n\t\tdataURL = \"data:image/jpeg;base64,\" + base64String\n\tdefault:\n\t\tdataURL = \"data:image/png;base64,\" + base64String\n\t}\n\n\t// 返回base64编码的图片数据\n\tresponse.SuccessWithMessage(c, \"上传成功\", gin.H{\n\t\t\"url\": dataURL,\n\t})\n}\n\n// GetPublicSystemInfo 获取系统公开信息(无需认证)\nfunc GetPublicSystemInfo(c *gin.Context) {\n\t// 默认值\n\tsystemName := \"ZbxTable\"\n\tsystemLogo := \"/static/img/logo.png\"\n\n\t// 尝试从数据库获取配置(如果数据库已初始化)\n\tif model.DB != nil {\n\t\tsystemName = model.GetConfigValueByKey(\"system_name\", \"ZbxTable\")\n\t\tsystemLogo = model.GetConfigValueByKey(\"system_logo\", \"/static/img/logo.png\")\n\t}\n\n\tresponse.Success(c, gin.H{\n\t\t\"system_name\": systemName,\n\t\t\"system_logo\": systemLogo,\n\t})\n}\n\n// GetInitialSetupStatus 获取初始配置状态\nfunc GetInitialSetupStatus(c *gin.Context) {\n\t// 检查是否完成初始配置\n\tsetupCompleted := model.GetConfigValueByKey(\"initial_setup_completed\", \"0\")\n\n\t// 检查webhook_url是否已配置\n\twebhookURL := model.GetConfigValueByKey(\"webhook_url\", \"\")\n\twebhookConfigured := webhookURL != \"\"\n\n\t// 检查是否有Zabbix实例\n\tinstances, err := model.GetAllZabbixInstances()\n\tzabbixConfigured := err == nil && len(instances) > 0\n\n\tresponse.Success(c, gin.H{\n\t\t\"setup_completed\": setupCompleted == \"1\",\n\t\t\"webhook_configured\": webhookConfigured,\n\t\t\"zabbix_configured\": zabbixConfigured,\n\t})\n}\n\n// CompleteInitialSetup 标记初始配置已完成\nfunc CompleteInitialSetup(c *gin.Context) {\n\t// 查找initial_setup_completed配置项\n\tconfigs, err := model.GetConfigList()\n\tif err != nil {\n\t\tresponse.InternalError(c, \"获取配置失败\")\n\t\treturn\n\t}\n\n\tvar configID int64\n\tfound := false\n\tfor _, config := range configs {\n\t\tif config.Key == \"initial_setup_completed\" {\n\t\t\tconfigID = config.ID\n\t\t\tfound = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif !found {\n\t\tresponse.InternalError(c, \"配置项不存在\")\n\t\treturn\n\t}\n\n\t// 更新配置\n\tv := model.Config{ID: configID, Value: \"1\"}\n\terr = model.UpdateConfig(&v)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"更新配置失败\")\n\t\treturn\n\t}\n\n\tresponse.SuccessWithMessage(c, \"初始配置已完成\", nil)\n}\n\n// TestEmailConfig 测试邮件配置\nfunc TestEmailConfig(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\ttestEmail := gjson.Get(string(body), \"test_email\").String()\n\tif testEmail == \"\" {\n\t\tresponse.BadRequest(c, \"请输入测试邮箱地址\")\n\t\treturn\n\t}\n\n\t// 发送测试邮件\n\terr = model.SendTestEmail(testEmail)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"邮件发送失败: \"+err.Error())\n\t\treturn\n\t}\n\n\tresponse.SuccessWithMessage(c, \"测试邮件发送成功,请检查邮箱\", nil)\n}\n\n// TestWechatConfig 测试企业微信配置\nfunc TestWechatConfig(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\ttestUserID := gjson.Get(string(body), \"test_user_id\").String()\n\tif testUserID == \"\" {\n\t\tresponse.BadRequest(c, \"请输入测试用户ID\")\n\t\treturn\n\t}\n\n\t// 发送测试企业微信消息\n\terr = model.SendTestWechat(testUserID)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"企业微信消息发送失败: \"+err.Error())\n\t\treturn\n\t}\n\n\tresponse.SuccessWithMessage(c, \"测试消息发送成功,请检查企业微信\", nil)\n}\n"}, "e996ccfc882ad5a0cf03b6b98ef5ca1990d7a5be": {"t": 1785754898.2683678, "n": 12841, "h": "2eb314360fe7b36f", "c": "package handler\n\nimport (\n\t\"encoding/base64\"\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"zbxtable/internal/model\"\n\t\"zbxtable/pkg/response\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/tidwall/gjson\"\n)\n\n// GetAllSystem 获取系统配置列表\nfunc GetAllSystem(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tcnt, val, err := model.GetALlSystem()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"ok\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = cnt\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetSystemByID 获取单个系统配置\nfunc GetSystemByID(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\tvar SystemRes model.SystemList\n\tval, err := model.GetSystemByID(int64(id))\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"ok\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// UpdateSystem 更新系统配置(支持多实例)\nfunc UpdateSystem(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\n\t// 获取实例ID\n\tzidStr := gjson.Get(string(body), \"zid\").String()\n\tif zidStr == \"\" {\n\t\tresponse.BadRequest(c, \"请选择 Zabbix 实例\")\n\t\treturn\n\t}\n\tzid, _ := strconv.ParseInt(zidStr, 10, 64)\n\tcpuCore := gjson.Get(string(body), \"cpu_core\").String()\n\tuptimeId := gjson.Get(string(body), \"uptime_id\").String()\n\tcpuUtilizationId := gjson.Get(string(body), \"cpu_utilization_id\").String()\n\tgroupId := gjson.Get(string(body), \"group_id\").String()\n\tmemoryTotalId := gjson.Get(string(body), \"memory_total_id\").String()\n\tmemoryUsedId := gjson.Get(string(body), \"memory_used_id\").String()\n\tmemoryUtilizationId := gjson.Get(string(body), \"memory_utilization_id\").String()\n\tmode := gjson.Get(string(body), \"model\").String()\n\tpingTemplateId := gjson.Get(string(body), \"ping_template_id\").String()\n\n\tvar SystemRes model.SystemList\n\tv := model.System{\n\t\tID: int64(id),\n\t\tZID: int(zid),\n\t\tCPUCore: cpuCore,\n\t\tCPUUtilizationID: cpuUtilizationId,\n\t\tGroupID: groupId,\n\t\tMemoryTotalID: memoryTotalId,\n\t\tUptimeID: uptimeId,\n\t\tModel: mode,\n\t\tMemoryUsedID: memoryUsedId,\n\t\tMemoryUtilizationID: memoryUtilizationId,\n\t\tPingTemplateID: pingTemplateId,\n\t}\n\terr = model.CreateOrUpdateSystem(&v)\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"更新成功\"\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// SystemInit 初始化系统(支持多实例)\nfunc SystemInit(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\n\t// 读取请求体获取实例ID\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tzidStr := gjson.Get(string(body), \"zid\").String()\n\tif zidStr == \"\" {\n\t\tresponse.BadRequest(c, \"请选择 Zabbix 实例\")\n\t\treturn\n\t}\n\tzid, _ := strconv.Atoi(zidStr)\n\n\tvar SystemRes model.SystemList\n\terr = model.SystemInitWithInstance(int64(id), int(zid))\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t\tSystemRes.Data.Total = 0\n\t\tSystemRes.Data.Items = \"\"\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"初始化完成\"\n\t\tSystemRes.Data.Total = 0\n\t\tSystemRes.Data.Items = \"\"\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetEgress 获取出口带宽配置\nfunc GetEgress(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tval, err := model.GetEgress()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"获取成功\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetAllConfig 获取系统参数配置\nfunc GetAllConfig(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tval, err := model.GetConfigList()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\t// 定义敏感字段列表\n\t\tsensitiveKeys := map[string]bool{\n\t\t\t\"email_secret\": true, // SMTP 密码/授权码\n\t\t\t\"wechat_secret\": true, // 企业微信 Secret\n\t\t\t\"deepseek_api_key\": true, // Deepseek API Key\n\t\t\t\"encryption_key\": true, // 加密密钥(完全隐藏)\n\t\t}\n\t\t\n\t\t// 过滤和脱敏处理\n\t\tfilteredConfigs := []model.Config{}\n\t\tfor _, config := range val {\n\t\t\t// 完全隐藏加密密钥配置项\n\t\t\tif config.Key == \"encryption_key\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t\n\t\t\t// 对敏感字段进行脱敏处理\n\t\t\tif sensitiveKeys[config.Key] && config.Value != \"\" {\n\t\t\t\tconfig.Value = \"********\" // 替换为星号\n\t\t\t}\n\t\t\t\n\t\t\tfilteredConfigs = append(filteredConfigs, config)\n\t\t}\n\t\t\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"获取成功\"\n\t\tSystemRes.Data.Items = filteredConfigs\n\t\tSystemRes.Data.Total = int64(len(filteredConfigs))\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// UpdateConfig 更新系统参数配置\nfunc UpdateConfig(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tvalue := gjson.Get(string(body), \"value\").String()\n\t\n\t// 如果值是星号(脱敏标记),则不更新\n\t// 这表示前端没有修改该敏感字段\n\tif value == \"********\" {\n\t\tvar SystemRes model.SystemList\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"未修改敏感字段,保持原值\"\n\t\tSystemRes.Data.Items = \"\"\n\t\tSystemRes.Data.Total = 1\n\t\tc.JSON(http.StatusOK, SystemRes)\n\t\treturn\n\t}\n\t\n\tvar SystemRes model.SystemList\n\tv := model.Config{ID: int64(id), Value: value}\n\terr = model.UpdateConfig(&v)\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"更新成功\"\n\t\tSystemRes.Data.Items = \"\"\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// ============ 新的出口配置 API ============\n\n// GetAllEgressConfigs 获取所有出口配置\nfunc GetAllEgressConfigs(c *gin.Context) {\n\tconfigs, err := model.GetAllEgress()\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.Success(c, configs)\n}\n\n// GetEgressConfigByID 根据ID获取出口配置\nfunc GetEgressConfigByID(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\n\tconfig, err := model.GetEgressConfigByID(id)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.Success(c, config)\n}\n\n// AddEgressConfig 添加出口配置\nfunc AddEgressConfig(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tname := gjson.Get(string(body), \"name\").String()\n\tzidStr := gjson.Get(string(body), \"zid\").String()\n\tzid, _ := strconv.Atoi(zidStr)\n\thostID := gjson.Get(string(body), \"host_id\").String()\n\tinItemID := gjson.Get(string(body), \"in_item_id\").String()\n\toutItemID := gjson.Get(string(body), \"out_item_id\").String()\n\tsortOrder := int(gjson.Get(string(body), \"sort_order\").Int())\n\n\tif name == \"\" || zidStr == \"\" || hostID == \"\" || inItemID == \"\" || outItemID == \"\" {\n\t\tresponse.BadRequest(c, \"缺少必填字段\")\n\t\treturn\n\t}\n\n\tconfig := &model.Egress{\n\t\tName: name,\n\t\tZID: zid,\n\t\tHostID: hostID,\n\t\tInItemID: inItemID,\n\t\tOutItemID: outItemID,\n\t\tStatus: 1,\n\t\tSortOrder: sortOrder,\n\t}\n\n\terr = model.AddEgress(config)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"添加成功\", config)\n}\n\n// UpdateEgressConfigHandler 更新出口配置\nfunc UpdateEgressConfigHandler(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tname := gjson.Get(string(body), \"name\").String()\n\tzidStr := gjson.Get(string(body), \"zid\").String()\n\thostID := gjson.Get(string(body), \"host_id\").String()\n\tinItemID := gjson.Get(string(body), \"in_item_id\").String()\n\toutItemID := gjson.Get(string(body), \"out_item_id\").String()\n\tstatus := int(gjson.Get(string(body), \"status\").Int())\n\tsortOrder := int(gjson.Get(string(body), \"sort_order\").Int())\n\n\tif name == \"\" || zidStr == \"\" || hostID == \"\" || inItemID == \"\" || outItemID == \"\" {\n\t\tresponse.BadRequest(c, \"缺少必填字段\")\n\t\treturn\n\t}\n\tzid, _ := strconv.Atoi(zidStr)\n\n\tconfig := &model.Egress{\n\t\tID: id,\n\t\tName: name,\n\t\tZID: zid,\n\t\tHostID: hostID,\n\t\tInItemID: inItemID,\n\t\tOutItemID: outItemID,\n\t\tStatus: status,\n\t\tSortOrder: sortOrder,\n\t}\n\n\terr = model.UpdateEgress(config)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"更新成功\", config)\n}\n\n// DeleteEgressConfigHandler 删除出口配置\nfunc DeleteEgressConfigHandler(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\n\terr = model.DeleteEgressConfig(id)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"删除成功\", nil)\n}\n\n// UploadLogo 上传系统Logo(存储为base64到数据库)\nfunc UploadLogo(c *gin.Context) {\n\tfile, err := c.FormFile(\"file\")\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"文件上传失败: \"+err.Error())\n\t\treturn\n\t}\n\n\t// 检查文件类型\n\tcontentType := file.Header.Get(\"Content-Type\")\n\tif contentType != \"image/png\" && contentType != \"image/jpeg\" && contentType != \"image/jpg\" && contentType != \"image/svg+xml\" {\n\t\tresponse.BadRequest(c, \"只支持 PNG、JPG、JPEG 或 SVG 格式的图片\")\n\t\treturn\n\t}\n\n\t// 检查文件大小(限制为2MB)\n\tif file.Size > 2*1024*1024 {\n\t\tresponse.BadRequest(c, \"文件大小不能超过2MB\")\n\t\treturn\n\t}\n\n\t// 打开文件并读取内容\n\tfileContent, err := file.Open()\n\tif err != nil {\n\t\tresponse.InternalError(c, \"读取文件失败: \"+err.Error())\n\t\treturn\n\t}\n\tdefer fileContent.Close()\n\n\t// 读取文件字节\n\tfileBytes, err := io.ReadAll(fileContent)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"读取文件内容失败: \"+err.Error())\n\t\treturn\n\t}\n\n\t// 转换为base64\n\tbase64String := base64.StdEncoding.EncodeToString(fileBytes)\n\n\t// 构建完整的Data URL格式(包含MIME类型)\n\tvar dataURL string\n\tswitch contentType {\n\tcase \"image/svg+xml\":\n\t\tdataURL = \"data:image/svg+xml;base64,\" + base64String\n\tcase \"image/png\":\n\t\tdataURL = \"data:image/png;base64,\" + base64String\n\tcase \"image/jpeg\", \"image/jpg\":\n\t\tdataURL = \"data:image/jpeg;base64,\" + base64String\n\tdefault:\n\t\tdataURL = \"data:image/png;base64,\" + base64String\n\t}\n\n\t// 返回base64编码的图片数据\n\tresponse.SuccessWithMessage(c, \"上传成功\", gin.H{\n\t\t\"url\": dataURL,\n\t})\n}\n\n// GetPublicSystemInfo 获取系统公开信息(无需认证)\nfunc GetPublicSystemInfo(c *gin.Context) {\n\t// 获取系统名称和Logo配置\n\tsystemName := model.GetConfigValueByKey(\"system_name\", \"ZbxTable\")\n\tsystemLogo := model.GetConfigValueByKey(\"system_logo\", \"/static/img/logo.png\")\n\n\tresponse.Success(c, gin.H{\n\t\t\"system_name\": systemName,\n\t\t\"system_logo\": systemLogo,\n\t})\n}\n\n// GetInitialSetupStatus 获取初始配置状态\nfunc GetInitialSetupStatus(c *gin.Context) {\n\t// 检查是否完成初始配置\n\tsetupCompleted := model.GetConfigValueByKey(\"initial_setup_completed\", \"0\")\n\n\t// 检查webhook_url是否已配置\n\twebhookURL := model.GetConfigValueByKey(\"webhook_url\", \"\")\n\twebhookConfigured := webhookURL != \"\"\n\n\t// 检查是否有Zabbix实例\n\tinstances, err := model.GetAllZabbixInstances()\n\tzabbixConfigured := err == nil && len(instances) > 0\n\n\tresponse.Success(c, gin.H{\n\t\t\"setup_completed\": setupCompleted == \"1\",\n\t\t\"webhook_configured\": webhookConfigured,\n\t\t\"zabbix_configured\": zabbixConfigured,\n\t})\n}\n\n// CompleteInitialSetup 标记初始配置已完成\nfunc CompleteInitialSetup(c *gin.Context) {\n\t// 查找initial_setup_completed配置项\n\tconfigs, err := model.GetConfigList()\n\tif err != nil {\n\t\tresponse.InternalError(c, \"获取配置失败\")\n\t\treturn\n\t}\n\n\tvar configID int64\n\tfound := false\n\tfor _, config := range configs {\n\t\tif config.Key == \"initial_setup_completed\" {\n\t\t\tconfigID = config.ID\n\t\t\tfound = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif !found {\n\t\tresponse.InternalError(c, \"配置项不存在\")\n\t\treturn\n\t}\n\n\t// 更新配置\n\tv := model.Config{ID: configID, Value: \"1\"}\n\terr = model.UpdateConfig(&v)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"更新配置失败\")\n\t\treturn\n\t}\n\n\tresponse.SuccessWithMessage(c, \"初始配置已完成\", nil)\n}\n\n// TestEmailConfig 测试邮件配置\nfunc TestEmailConfig(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\ttestEmail := gjson.Get(string(body), \"test_email\").String()\n\tif testEmail == \"\" {\n\t\tresponse.BadRequest(c, \"请输入测试邮箱地址\")\n\t\treturn\n\t}\n\n\t// 发送测试邮件\n\terr = model.SendTestEmail(testEmail)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"邮件发送失败: \"+err.Error())\n\t\treturn\n\t}\n\n\tresponse.SuccessWithMessage(c, \"测试邮件发送成功,请检查邮箱\", nil)\n}\n\n// TestWechatConfig 测试企业微信配置\nfunc TestWechatConfig(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\ttestUserID := gjson.Get(string(body), \"test_user_id\").String()\n\tif testUserID == \"\" {\n\t\tresponse.BadRequest(c, \"请输入测试用户ID\")\n\t\treturn\n\t}\n\n\t// 发送测试企业微信消息\n\terr = model.SendTestWechat(testUserID)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"企业微信消息发送失败: \"+err.Error())\n\t\treturn\n\t}\n\n\tresponse.SuccessWithMessage(c, \"测试消息发送成功,请检查企业微信\", nil)\n}\n"}, "4de66ee0706b14abd01317263e1a9a2ac9bfe893": {"t": 1785754899.642504, "n": 10955, "h": "5719899afedb26b5", "c": "package handler\n\nimport (\n\t\"encoding/base64\"\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"zbxtable/internal/model\"\n\t\"zbxtable/pkg/response\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/tidwall/gjson\"\n)\n\n// GetAllSystem 获取系统配置列表\nfunc GetAllSystem(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tcnt, val, err := model.GetALlSystem()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"ok\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = cnt\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetSystemByID 获取单个系统配置\nfunc GetSystemByID(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\tvar SystemRes model.SystemList\n\tval, err := model.GetSystemByID(int64(id))\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"ok\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// UpdateSystem 更新系统配置(支持多实例)\nfunc UpdateSystem(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\n\t// 获取实例ID\n\tzidStr := gjson.Get(string(body), \"zid\").String()\n\tif zidStr == \"\" {\n\t\tresponse.BadRequest(c, \"请选择 Zabbix 实例\")\n\t\treturn\n\t}\n\tzid, _ := strconv.ParseInt(zidStr, 10, 64)\n\tcpuCore := gjson.Get(string(body), \"cpu_core\").String()\n\tuptimeId := gjson.Get(string(body), \"uptime_id\").String()\n\tcpuUtilizationId := gjson.Get(string(body), \"cpu_utilization_id\").String()\n\tgroupId := gjson.Get(string(body), \"group_id\").String()\n\tmemoryTotalId := gjson.Get(string(body), \"memory_total_id\").String()\n\tmemoryUsedId := gjson.Get(string(body), \"memory_used_id\").String()\n\tmemoryUtilizationId := gjson.Get(string(body), \"memory_utilization_id\").String()\n\tmode := gjson.Get(string(body), \"model\").String()\n\tpingTemplateId := gjson.Get(string(body), \"ping_template_id\").String()\n\n\tvar SystemRes model.SystemList\n\tv := model.System{\n\t\tID: int64(id),\n\t\tZID: int(zid),\n\t\tCPUCore: cpuCore,\n\t\tCPUUtilizationID: cpuUtilizationId,\n\t\tGroupID: groupId,\n\t\tMemoryTotalID: memoryTotalId,\n\t\tUptimeID: uptimeId,\n\t\tModel: mode,\n\t\tMemoryUsedID: memoryUsedId,\n\t\tMemoryUtilizationID: memoryUtilizationId,\n\t\tPingTemplateID: pingTemplateId,\n\t}\n\terr = model.CreateOrUpdateSystem(&v)\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"更新成功\"\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// SystemInit 初始化系统(支持多实例)\nfunc SystemInit(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\n\t// 读取请求体获取实例ID\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tzidStr := gjson.Get(string(body), \"zid\").String()\n\tif zidStr == \"\" {\n\t\tresponse.BadRequest(c, \"请选择 Zabbix 实例\")\n\t\treturn\n\t}\n\tzid, _ := strconv.Atoi(zidStr)\n\n\tvar SystemRes model.SystemList\n\terr = model.SystemInitWithInstance(int64(id), int(zid))\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t\tSystemRes.Data.Total = 0\n\t\tSystemRes.Data.Items = \"\"\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"初始化完成\"\n\t\tSystemRes.Data.Total = 0\n\t\tSystemRes.Data.Items = \"\"\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetEgress 获取出口带宽配置\nfunc GetEgress(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tval, err := model.GetEgress()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"获取成功\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetAllConfig 获取系统参数配置\nfunc GetAllConfig(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tval, err := model.GetConfigList()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"获取成功\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = int64(len(val))\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// UpdateConfig 更新系统参数配置\nfunc UpdateConfig(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tvalue := gjson.Get(string(body), \"value\").String()\n\tvar SystemRes model.SystemList\n\tv := model.Config{ID: int64(id), Value: value}\n\terr = model.UpdateConfig(&v)\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"更新成功\"\n\t\tSystemRes.Data.Items = \"\"\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// ============ 新的出口配置 API ============\n\n// GetAllEgressConfigs 获取所有出口配置\nfunc GetAllEgressConfigs(c *gin.Context) {\n\tconfigs, err := model.GetAllEgress()\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.Success(c, configs)\n}\n\n// GetEgressConfigByID 根据ID获取出口配置\nfunc GetEgressConfigByID(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\n\tconfig, err := model.GetEgressConfigByID(id)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.Success(c, config)\n}\n\n// AddEgressConfig 添加出口配置\nfunc AddEgressConfig(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tname := gjson.Get(string(body), \"name\").String()\n\tzidStr := gjson.Get(string(body), \"zid\").String()\n\tzid, _ := strconv.Atoi(zidStr)\n\thostID := gjson.Get(string(body), \"host_id\").String()\n\tinItemID := gjson.Get(string(body), \"in_item_id\").String()\n\toutItemID := gjson.Get(string(body), \"out_item_id\").String()\n\tsortOrder := int(gjson.Get(string(body), \"sort_order\").Int())\n\n\tif name == \"\" || zidStr == \"\" || hostID == \"\" || inItemID == \"\" || outItemID == \"\" {\n\t\tresponse.BadRequest(c, \"缺少必填字段\")\n\t\treturn\n\t}\n\n\tconfig := &model.Egress{\n\t\tName: name,\n\t\tZID: zid,\n\t\tHostID: hostID,\n\t\tInItemID: inItemID,\n\t\tOutItemID: outItemID,\n\t\tStatus: 1,\n\t\tSortOrder: sortOrder,\n\t}\n\n\terr = model.AddEgress(config)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"添加成功\", config)\n}\n\n// UpdateEgressConfigHandler 更新出口配置\nfunc UpdateEgressConfigHandler(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tname := gjson.Get(string(body), \"name\").String()\n\tzidStr := gjson.Get(string(body), \"zid\").String()\n\thostID := gjson.Get(string(body), \"host_id\").String()\n\tinItemID := gjson.Get(string(body), \"in_item_id\").String()\n\toutItemID := gjson.Get(string(body), \"out_item_id\").String()\n\tstatus := int(gjson.Get(string(body), \"status\").Int())\n\tsortOrder := int(gjson.Get(string(body), \"sort_order\").Int())\n\n\tif name == \"\" || zidStr == \"\" || hostID == \"\" || inItemID == \"\" || outItemID == \"\" {\n\t\tresponse.BadRequest(c, \"缺少必填字段\")\n\t\treturn\n\t}\n\tzid, _ := strconv.Atoi(zidStr)\n\n\tconfig := &model.Egress{\n\t\tID: id,\n\t\tName: name,\n\t\tZID: zid,\n\t\tHostID: hostID,\n\t\tInItemID: inItemID,\n\t\tOutItemID: outItemID,\n\t\tStatus: status,\n\t\tSortOrder: sortOrder,\n\t}\n\n\terr = model.UpdateEgress(config)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"更新成功\", config)\n}\n\n// DeleteEgressConfigHandler 删除出口配置\nfunc DeleteEgressConfigHandler(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\n\terr = model.DeleteEgressConfig(id)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"删除成功\", nil)\n}\n\n// UploadLogo 上传系统Logo(存储为base64到数据库)\nfunc UploadLogo(c *gin.Context) {\n\tfile, err := c.FormFile(\"file\")\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"文件上传失败: \"+err.Error())\n\t\treturn\n\t}\n\n\t// 检查文件类型\n\tcontentType := file.Header.Get(\"Content-Type\")\n\tif contentType != \"image/png\" && contentType != \"image/jpeg\" && contentType != \"image/jpg\" && contentType != \"image/svg+xml\" {\n\t\tresponse.BadRequest(c, \"只支持 PNG、JPG、JPEG 或 SVG 格式的图片\")\n\t\treturn\n\t}\n\n\t// 检查文件大小(限制为2MB)\n\tif file.Size > 2*1024*1024 {\n\t\tresponse.BadRequest(c, \"文件大小不能超过2MB\")\n\t\treturn\n\t}\n\n\t// 打开文件并读取内容\n\tfileContent, err := file.Open()\n\tif err != nil {\n\t\tresponse.InternalError(c, \"读取文件失败: \"+err.Error())\n\t\treturn\n\t}\n\tdefer fileContent.Close()\n\n\t// 读取文件字节\n\tfileBytes, err := io.ReadAll(fileContent)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"读取文件内容失败: \"+err.Error())\n\t\treturn\n\t}\n\n\t// 转换为base64\n\tbase64String := base64.StdEncoding.EncodeToString(fileBytes)\n\n\t// 构建完整的Data URL格式(包含MIME类型)\n\tvar dataURL string\n\tswitch contentType {\n\tcase \"image/svg+xml\":\n\t\tdataURL = \"data:image/svg+xml;base64,\" + base64String\n\tcase \"image/png\":\n\t\tdataURL = \"data:image/png;base64,\" + base64String\n\tcase \"image/jpeg\", \"image/jpg\":\n\t\tdataURL = \"data:image/jpeg;base64,\" + base64String\n\tdefault:\n\t\tdataURL = \"data:image/png;base64,\" + base64String\n\t}\n\n\t// 返回base64编码的图片数据\n\tresponse.SuccessWithMessage(c, \"上传成功\", gin.H{\n\t\t\"url\": dataURL,\n\t})\n}\n\n// GetPublicSystemInfo 获取系统公开信息(无需认证)\nfunc GetPublicSystemInfo(c *gin.Context) {\n\t// 获取系统名称和Logo配置\n\tsystemName := model.GetConfigValueByKey(\"system_name\", \"ZbxTable\")\n\tsystemLogo := model.GetConfigValueByKey(\"system_logo\", \"/static/img/logo.png\")\n\n\tresponse.Success(c, gin.H{\n\t\t\"system_name\": systemName,\n\t\t\"system_logo\": systemLogo,\n\t})\n}\n\n// GetInitialSetupStatus 获取初始配置状态\nfunc GetInitialSetupStatus(c *gin.Context) {\n\t// 检查是否完成初始配置\n\tsetupCompleted := model.GetConfigValueByKey(\"initial_setup_completed\", \"0\")\n\n\t// 检查webhook_url是否已配置\n\twebhookURL := model.GetConfigValueByKey(\"webhook_url\", \"\")\n\twebhookConfigured := webhookURL != \"\"\n\n\t// 检查是否有Zabbix实例\n\tinstances, err := model.GetAllZabbixInstances()\n\tzabbixConfigured := err == nil && len(instances) > 0\n\n\tresponse.Success(c, gin.H{\n\t\t\"setup_completed\": setupCompleted == \"1\",\n\t\t\"webhook_configured\": webhookConfigured,\n\t\t\"zabbix_configured\": zabbixConfigured,\n\t})\n}\n\n// CompleteInitialSetup 标记初始配置已完成\nfunc CompleteInitialSetup(c *gin.Context) {\n\t// 查找initial_setup_completed配置项\n\tconfigs, err := model.GetConfigList()\n\tif err != nil {\n\t\tresponse.InternalError(c, \"获取配置失败\")\n\t\treturn\n\t}\n\n\tvar configID int64\n\tfound := false\n\tfor _, config := range configs {\n\t\tif config.Key == \"initial_setup_completed\" {\n\t\t\tconfigID = config.ID\n\t\t\tfound = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif !found {\n\t\tresponse.InternalError(c, \"配置项不存在\")\n\t\treturn\n\t}\n\n\t// 更新配置\n\tv := model.Config{ID: configID, Value: \"1\"}\n\terr = model.UpdateConfig(&v)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"更新配置失败\")\n\t\treturn\n\t}\n\n\tresponse.SuccessWithMessage(c, \"初始配置已完成\", nil)\n}\n"}, "932d2f7e6967888f9611259b01c450a475aa1301": {"t": 1785754901.104894, "n": 9712, "h": "58539e451fb12026", "c": "package handler\n\nimport (\n\t\"encoding/base64\"\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"zbxtable/internal/model\"\n\t\"zbxtable/pkg/response\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/tidwall/gjson\"\n)\n\n// GetAllSystem 获取系统配置列表\nfunc GetAllSystem(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tcnt, val, err := model.GetALlSystem()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"ok\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = cnt\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetSystemByID 获取单个系统配置\nfunc GetSystemByID(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\tvar SystemRes model.SystemList\n\tval, err := model.GetSystemByID(int64(id))\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"ok\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// UpdateSystem 更新系统配置(支持多实例)\nfunc UpdateSystem(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\n\t// 获取实例ID\n\tzidStr := gjson.Get(string(body), \"zid\").String()\n\tif zidStr == \"\" {\n\t\tresponse.BadRequest(c, \"请选择 Zabbix 实例\")\n\t\treturn\n\t}\n\tzid, _ := strconv.ParseInt(zidStr, 10, 64)\n\tcpuCore := gjson.Get(string(body), \"cpu_core\").String()\n\tuptimeId := gjson.Get(string(body), \"uptime_id\").String()\n\tcpuUtilizationId := gjson.Get(string(body), \"cpu_utilization_id\").String()\n\tgroupId := gjson.Get(string(body), \"group_id\").String()\n\tmemoryTotalId := gjson.Get(string(body), \"memory_total_id\").String()\n\tmemoryUsedId := gjson.Get(string(body), \"memory_used_id\").String()\n\tmemoryUtilizationId := gjson.Get(string(body), \"memory_utilization_id\").String()\n\tmode := gjson.Get(string(body), \"model\").String()\n\tpingTemplateId := gjson.Get(string(body), \"ping_template_id\").String()\n\n\tvar SystemRes model.SystemList\n\tv := model.System{\n\t\tID: int64(id),\n\t\tZID: int(zid),\n\t\tCPUCore: cpuCore,\n\t\tCPUUtilizationID: cpuUtilizationId,\n\t\tGroupID: groupId,\n\t\tMemoryTotalID: memoryTotalId,\n\t\tUptimeID: uptimeId,\n\t\tModel: mode,\n\t\tMemoryUsedID: memoryUsedId,\n\t\tMemoryUtilizationID: memoryUtilizationId,\n\t\tPingTemplateID: pingTemplateId,\n\t}\n\terr = model.CreateOrUpdateSystem(&v)\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"更新成功\"\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// SystemInit 初始化系统(支持多实例)\nfunc SystemInit(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\n\t// 读取请求体获取实例ID\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tzidStr := gjson.Get(string(body), \"zid\").String()\n\tif zidStr == \"\" {\n\t\tresponse.BadRequest(c, \"请选择 Zabbix 实例\")\n\t\treturn\n\t}\n\tzid, _ := strconv.Atoi(zidStr)\n\n\tvar SystemRes model.SystemList\n\terr = model.SystemInitWithInstance(int64(id), int(zid))\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t\tSystemRes.Data.Total = 0\n\t\tSystemRes.Data.Items = \"\"\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"初始化完成\"\n\t\tSystemRes.Data.Total = 0\n\t\tSystemRes.Data.Items = \"\"\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetEgress 获取出口带宽配置\nfunc GetEgress(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tval, err := model.GetEgress()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"获取成功\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetAllConfig 获取系统参数配置\nfunc GetAllConfig(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tval, err := model.GetConfigList()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"获取成功\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = int64(len(val))\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// UpdateConfig 更新系统参数配置\nfunc UpdateConfig(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tvalue := gjson.Get(string(body), \"value\").String()\n\tvar SystemRes model.SystemList\n\tv := model.Config{ID: int64(id), Value: value}\n\terr = model.UpdateConfig(&v)\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"更新成功\"\n\t\tSystemRes.Data.Items = \"\"\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// ============ 新的出口配置 API ============\n\n// GetAllEgressConfigs 获取所有出口配置\nfunc GetAllEgressConfigs(c *gin.Context) {\n\tconfigs, err := model.GetAllEgress()\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.Success(c, configs)\n}\n\n// GetEgressConfigByID 根据ID获取出口配置\nfunc GetEgressConfigByID(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\n\tconfig, err := model.GetEgressConfigByID(id)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.Success(c, config)\n}\n\n// AddEgressConfig 添加出口配置\nfunc AddEgressConfig(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tname := gjson.Get(string(body), \"name\").String()\n\tzidStr := gjson.Get(string(body), \"zid\").String()\n\tzid, _ := strconv.Atoi(zidStr)\n\thostID := gjson.Get(string(body), \"host_id\").String()\n\tinItemID := gjson.Get(string(body), \"in_item_id\").String()\n\toutItemID := gjson.Get(string(body), \"out_item_id\").String()\n\tsortOrder := int(gjson.Get(string(body), \"sort_order\").Int())\n\n\tif name == \"\" || zidStr == \"\" || hostID == \"\" || inItemID == \"\" || outItemID == \"\" {\n\t\tresponse.BadRequest(c, \"缺少必填字段\")\n\t\treturn\n\t}\n\n\tconfig := &model.Egress{\n\t\tName: name,\n\t\tZID: zid,\n\t\tHostID: hostID,\n\t\tInItemID: inItemID,\n\t\tOutItemID: outItemID,\n\t\tStatus: 1,\n\t\tSortOrder: sortOrder,\n\t}\n\n\terr = model.AddEgress(config)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"添加成功\", config)\n}\n\n// UpdateEgressConfigHandler 更新出口配置\nfunc UpdateEgressConfigHandler(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tname := gjson.Get(string(body), \"name\").String()\n\tzidStr := gjson.Get(string(body), \"zid\").String()\n\thostID := gjson.Get(string(body), \"host_id\").String()\n\tinItemID := gjson.Get(string(body), \"in_item_id\").String()\n\toutItemID := gjson.Get(string(body), \"out_item_id\").String()\n\tstatus := int(gjson.Get(string(body), \"status\").Int())\n\tsortOrder := int(gjson.Get(string(body), \"sort_order\").Int())\n\n\tif name == \"\" || zidStr == \"\" || hostID == \"\" || inItemID == \"\" || outItemID == \"\" {\n\t\tresponse.BadRequest(c, \"缺少必填字段\")\n\t\treturn\n\t}\n\tzid, _ := strconv.Atoi(zidStr)\n\n\tconfig := &model.Egress{\n\t\tID: id,\n\t\tName: name,\n\t\tZID: zid,\n\t\tHostID: hostID,\n\t\tInItemID: inItemID,\n\t\tOutItemID: outItemID,\n\t\tStatus: status,\n\t\tSortOrder: sortOrder,\n\t}\n\n\terr = model.UpdateEgress(config)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"更新成功\", config)\n}\n\n// DeleteEgressConfigHandler 删除出口配置\nfunc DeleteEgressConfigHandler(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\n\terr = model.DeleteEgressConfig(id)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"删除成功\", nil)\n}\n\n// UploadLogo 上传系统Logo(存储为base64到数据库)\nfunc UploadLogo(c *gin.Context) {\n\tfile, err := c.FormFile(\"file\")\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"文件上传失败: \"+err.Error())\n\t\treturn\n\t}\n\n\t// 检查文件类型\n\tcontentType := file.Header.Get(\"Content-Type\")\n\tif contentType != \"image/png\" && contentType != \"image/jpeg\" && contentType != \"image/jpg\" && contentType != \"image/svg+xml\" {\n\t\tresponse.BadRequest(c, \"只支持 PNG、JPG、JPEG 或 SVG 格式的图片\")\n\t\treturn\n\t}\n\n\t// 检查文件大小(限制为2MB)\n\tif file.Size > 2*1024*1024 {\n\t\tresponse.BadRequest(c, \"文件大小不能超过2MB\")\n\t\treturn\n\t}\n\n\t// 打开文件并读取内容\n\tfileContent, err := file.Open()\n\tif err != nil {\n\t\tresponse.InternalError(c, \"读取文件失败: \"+err.Error())\n\t\treturn\n\t}\n\tdefer fileContent.Close()\n\n\t// 读取文件字节\n\tfileBytes, err := io.ReadAll(fileContent)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"读取文件内容失败: \"+err.Error())\n\t\treturn\n\t}\n\n\t// 转换为base64\n\tbase64String := base64.StdEncoding.EncodeToString(fileBytes)\n\n\t// 构建完整的Data URL格式(包含MIME类型)\n\tvar dataURL string\n\tswitch contentType {\n\tcase \"image/svg+xml\":\n\t\tdataURL = \"data:image/svg+xml;base64,\" + base64String\n\tcase \"image/png\":\n\t\tdataURL = \"data:image/png;base64,\" + base64String\n\tcase \"image/jpeg\", \"image/jpg\":\n\t\tdataURL = \"data:image/jpeg;base64,\" + base64String\n\tdefault:\n\t\tdataURL = \"data:image/png;base64,\" + base64String\n\t}\n\n\t// 返回base64编码的图片数据\n\tresponse.SuccessWithMessage(c, \"上传成功\", gin.H{\n\t\t\"url\": dataURL,\n\t})\n}\n\n// GetPublicSystemInfo 获取系统公开信息(无需认证)\nfunc GetPublicSystemInfo(c *gin.Context) {\n\t// 获取系统名称和Logo配置\n\tsystemName := model.GetConfigValueByKey(\"system_name\", \"ZbxTable\")\n\tsystemLogo := model.GetConfigValueByKey(\"system_logo\", \"/static/img/logo.png\")\n\n\tresponse.Success(c, gin.H{\n\t\t\"system_name\": systemName,\n\t\t\"system_logo\": systemLogo,\n\t})\n}\n"}, "314a866ed072240c00b0556b0b9314b5d3a39a31": {"t": 1785754902.3798242, "n": 7972, "h": "9022bddf7e804edb", "c": "package handler\n\nimport (\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"zbxtable/internal/model\"\n\t\"zbxtable/pkg/response\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/tidwall/gjson\"\n)\n\n// GetAllSystem 获取系统配置列表\nfunc GetAllSystem(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tcnt, val, err := model.GetALlSystem()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"ok\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = cnt\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetSystemByID 获取单个系统配置\nfunc GetSystemByID(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\tvar SystemRes model.SystemList\n\tval, err := model.GetSystemByID(int64(id))\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"ok\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// UpdateSystem 更新系统配置(支持多实例)\nfunc UpdateSystem(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\n\t// 获取实例ID\n\tzidStr := gjson.Get(string(body), \"zid\").String()\n\tif zidStr == \"\" {\n\t\tresponse.BadRequest(c, \"请选择 Zabbix 实例\")\n\t\treturn\n\t}\n\tzid, _ := strconv.ParseInt(zidStr, 10, 64)\n\tcpuCore := gjson.Get(string(body), \"cpu_core\").String()\n\tuptimeId := gjson.Get(string(body), \"uptime_id\").String()\n\tcpuUtilizationId := gjson.Get(string(body), \"cpu_utilization_id\").String()\n\tgroupId := gjson.Get(string(body), \"group_id\").String()\n\tmemoryTotalId := gjson.Get(string(body), \"memory_total_id\").String()\n\tmemoryUsedId := gjson.Get(string(body), \"memory_used_id\").String()\n\tmemoryUtilizationId := gjson.Get(string(body), \"memory_utilization_id\").String()\n\tmode := gjson.Get(string(body), \"model\").String()\n\tpingTemplateId := gjson.Get(string(body), \"ping_template_id\").String()\n\n\tvar SystemRes model.SystemList\n\tv := model.System{\n\t\tID: int64(id),\n\t\tZID: int(zid),\n\t\tCPUCore: cpuCore,\n\t\tCPUUtilizationID: cpuUtilizationId,\n\t\tGroupID: groupId,\n\t\tMemoryTotalID: memoryTotalId,\n\t\tUptimeID: uptimeId,\n\t\tModel: mode,\n\t\tMemoryUsedID: memoryUsedId,\n\t\tMemoryUtilizationID: memoryUtilizationId,\n\t\tPingTemplateID: pingTemplateId,\n\t}\n\terr = model.CreateOrUpdateSystem(&v)\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"更新成功\"\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// SystemInit 初始化系统(支持多实例)\nfunc SystemInit(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\n\t// 读取请求体获取实例ID\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tzidStr := gjson.Get(string(body), \"zid\").String()\n\tif zidStr == \"\" {\n\t\tresponse.BadRequest(c, \"请选择 Zabbix 实例\")\n\t\treturn\n\t}\n\tzid, _ := strconv.Atoi(zidStr)\n\n\tvar SystemRes model.SystemList\n\terr = model.SystemInitWithInstance(int64(id), int(zid))\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t\tSystemRes.Data.Total = 0\n\t\tSystemRes.Data.Items = \"\"\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"初始化完成\"\n\t\tSystemRes.Data.Total = 0\n\t\tSystemRes.Data.Items = \"\"\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetEgress 获取出口带宽配置\nfunc GetEgress(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tval, err := model.GetEgress()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"获取成功\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetAllConfig 获取系统参数配置\nfunc GetAllConfig(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tval, err := model.GetConfigList()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"获取成功\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = int64(len(val))\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// UpdateConfig 更新系统参数配置\nfunc UpdateConfig(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tvalue := gjson.Get(string(body), \"value\").String()\n\tvar SystemRes model.SystemList\n\tv := model.Config{ID: int64(id), Value: value}\n\terr = model.UpdateConfig(&v)\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"更新成功\"\n\t\tSystemRes.Data.Items = \"\"\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// ============ 新的出口配置 API ============\n\n// GetAllEgressConfigs 获取所有出口配置\nfunc GetAllEgressConfigs(c *gin.Context) {\n\tconfigs, err := model.GetAllEgress()\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.Success(c, configs)\n}\n\n// GetEgressConfigByID 根据ID获取出口配置\nfunc GetEgressConfigByID(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\n\tconfig, err := model.GetEgressConfigByID(id)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.Success(c, config)\n}\n\n// AddEgressConfig 添加出口配置\nfunc AddEgressConfig(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tname := gjson.Get(string(body), \"name\").String()\n\tzidStr := gjson.Get(string(body), \"zid\").String()\n\tzid, _ := strconv.Atoi(zidStr)\n\thostID := gjson.Get(string(body), \"host_id\").String()\n\tinItemID := gjson.Get(string(body), \"in_item_id\").String()\n\toutItemID := gjson.Get(string(body), \"out_item_id\").String()\n\tsortOrder := int(gjson.Get(string(body), \"sort_order\").Int())\n\n\tif name == \"\" || zidStr == \"\" || hostID == \"\" || inItemID == \"\" || outItemID == \"\" {\n\t\tresponse.BadRequest(c, \"缺少必填字段\")\n\t\treturn\n\t}\n\n\tconfig := &model.Egress{\n\t\tName: name,\n\t\tZID: zid,\n\t\tHostID: hostID,\n\t\tInItemID: inItemID,\n\t\tOutItemID: outItemID,\n\t\tStatus: 1,\n\t\tSortOrder: sortOrder,\n\t}\n\n\terr = model.AddEgress(config)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"添加成功\", config)\n}\n\n// UpdateEgressConfigHandler 更新出口配置\nfunc UpdateEgressConfigHandler(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tname := gjson.Get(string(body), \"name\").String()\n\tzidStr := gjson.Get(string(body), \"zid\").String()\n\thostID := gjson.Get(string(body), \"host_id\").String()\n\tinItemID := gjson.Get(string(body), \"in_item_id\").String()\n\toutItemID := gjson.Get(string(body), \"out_item_id\").String()\n\tstatus := int(gjson.Get(string(body), \"status\").Int())\n\tsortOrder := int(gjson.Get(string(body), \"sort_order\").Int())\n\n\tif name == \"\" || zidStr == \"\" || hostID == \"\" || inItemID == \"\" || outItemID == \"\" {\n\t\tresponse.BadRequest(c, \"缺少必填字段\")\n\t\treturn\n\t}\n\tzid, _ := strconv.Atoi(zidStr)\n\n\tconfig := &model.Egress{\n\t\tID: id,\n\t\tName: name,\n\t\tZID: zid,\n\t\tHostID: hostID,\n\t\tInItemID: inItemID,\n\t\tOutItemID: outItemID,\n\t\tStatus: status,\n\t\tSortOrder: sortOrder,\n\t}\n\n\terr = model.UpdateEgress(config)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"更新成功\", config)\n}\n\n// DeleteEgressConfigHandler 删除出口配置\nfunc DeleteEgressConfigHandler(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\n\terr = model.DeleteEgressConfig(id)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"删除成功\", nil)\n}\n"}, "59e0cda7340199f89dd5256f69eaf279a386d024": {"t": 1785754905.8504095, "n": 8910, "h": "c336c9979f66efc8", "c": "package handler\n\nimport (\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"zbxtable/internal/model\"\n\t\"zbxtable/pkg/response\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/tidwall/gjson\"\n)\n\n// GetAllSystem 获取系统配置列表\nfunc GetAllSystem(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tcnt, val, err := model.GetALlSystem()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"ok\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = cnt\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetSystemByID 获取单个系统配置\nfunc GetSystemByID(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\tvar SystemRes model.SystemList\n\tval, err := model.GetSystemByID(int64(id))\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"ok\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// UpdateSystem 更新系统配置(支持多实例)\nfunc UpdateSystem(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\n\t// 获取实例ID\n\tzidStr := gjson.Get(string(body), \"zid\").String()\n\tif zidStr == \"\" {\n\t\tresponse.BadRequest(c, \"请选择 Zabbix 实例\")\n\t\treturn\n\t}\n\tzid, _ := strconv.ParseInt(zidStr, 10, 64)\n\tcpuCore := gjson.Get(string(body), \"cpu_core\").String()\n\tuptimeId := gjson.Get(string(body), \"uptime_id\").String()\n\tcpuUtilizationId := gjson.Get(string(body), \"cpu_utilization_id\").String()\n\tgroupId := gjson.Get(string(body), \"group_id\").String()\n\tmemoryTotalId := gjson.Get(string(body), \"memory_total_id\").String()\n\tmemoryUsedId := gjson.Get(string(body), \"memory_used_id\").String()\n\tmemoryUtilizationId := gjson.Get(string(body), \"memory_utilization_id\").String()\n\tmode := gjson.Get(string(body), \"model\").String()\n\tpingTemplateId := gjson.Get(string(body), \"ping_template_id\").String()\n\n\tvar SystemRes model.SystemList\n\tv := model.System{\n\t\tID: int64(id),\n\t\tZID: int(zid),\n\t\tCPUCore: cpuCore,\n\t\tCPUUtilizationID: cpuUtilizationId,\n\t\tGroupID: groupId,\n\t\tMemoryTotalID: memoryTotalId,\n\t\tUptimeID: uptimeId,\n\t\tModel: mode,\n\t\tMemoryUsedID: memoryUsedId,\n\t\tMemoryUtilizationID: memoryUtilizationId,\n\t\tPingTemplateID: pingTemplateId,\n\t}\n\terr = model.CreateOrUpdateSystem(&v)\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"更新成功\"\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// SystemInit 初始化系统(支持多实例)\nfunc SystemInit(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\n\t// 读取请求体获取实例ID\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tzidStr := gjson.Get(string(body), \"zid\").String()\n\tif zidStr == \"\" {\n\t\tresponse.BadRequest(c, \"请选择 Zabbix 实例\")\n\t\treturn\n\t}\n\tzid, _ := strconv.Atoi(zidStr)\n\n\tvar SystemRes model.SystemList\n\terr = model.SystemInitWithInstance(int64(id), int(zid))\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t\tSystemRes.Data.Total = 0\n\t\tSystemRes.Data.Items = \"\"\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"初始化完成\"\n\t\tSystemRes.Data.Total = 0\n\t\tSystemRes.Data.Items = \"\"\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetEgress 获取出口带宽配置\nfunc GetEgress(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tval, err := model.GetEgress()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"获取成功\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// UpdateEgress 更新带宽配置\nfunc UpdateEgress(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tnameone := gjson.Get(string(body), \"name_one\").String()\n\tin_one := gjson.Get(string(body), \"in_one\").String()\n\tout_one := gjson.Get(string(body), \"out_one\").String()\n\tnametwo := gjson.Get(string(body), \"name_two\").String()\n\tin_two := gjson.Get(string(body), \"in_two\").String()\n\tout_two := gjson.Get(string(body), \"out_two\").String()\n\tvar SystemRes model.SystemList\n\tv := model.Egress{ID: 1,\n\t\tNameOne: nameone, InOne: in_one, OutOne: out_one,\n\t\tNameTwo: nametwo, InTwo: in_two, OutTwo: out_two}\n\terr = model.UpdateEgress(&v)\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"更新成功\"\n\t\tSystemRes.Data.Items = \"\"\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetAllConfig 获取系统参数配置\nfunc GetAllConfig(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tval, err := model.GetConfigList()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"获取成功\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = int64(len(val))\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// UpdateConfig 更新系统参数配置\nfunc UpdateConfig(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tvalue := gjson.Get(string(body), \"value\").String()\n\tvar SystemRes model.SystemList\n\tv := model.Config{ID: int64(id), Value: value}\n\terr = model.UpdateConfig(&v)\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"更新成功\"\n\t\tSystemRes.Data.Items = \"\"\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// ============ 新的出口配置 API ============\n\n// GetAllEgressConfigs 获取所有出口配置\nfunc GetAllEgressConfigs(c *gin.Context) {\n\tconfigs, err := model.GetAllEgressConfigs()\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.Success(c, configs)\n}\n\n// GetEgressConfigByID 根据ID获取出口配置\nfunc GetEgressConfigByID(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\n\tconfig, err := model.GetEgressConfigByID(id)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.Success(c, config)\n}\n\n// AddEgressConfig 添加出口配置\nfunc AddEgressConfig(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tname := gjson.Get(string(body), \"name\").String()\n\ttenantID := gjson.Get(string(body), \"tenant_id\").String()\n\thostID := gjson.Get(string(body), \"host_id\").String()\n\tinItemID := gjson.Get(string(body), \"in_item_id\").String()\n\toutItemID := gjson.Get(string(body), \"out_item_id\").String()\n\tsortOrder := int(gjson.Get(string(body), \"sort_order\").Int())\n\n\tif name == \"\" || tenantID == \"\" || hostID == \"\" || inItemID == \"\" || outItemID == \"\" {\n\t\tresponse.BadRequest(c, \"缺少必填字段\")\n\t\treturn\n\t}\n\n\tconfig := &model.EgressConfig{\n\t\tName: name,\n\t\tInstanceID: tenantID,\n\t\tHostID: hostID,\n\t\tInItemID: inItemID,\n\t\tOutItemID: outItemID,\n\t\tStatus: 1,\n\t\tSortOrder: sortOrder,\n\t}\n\n\terr = model.AddEgressConfig(config)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"添加成功\", config)\n}\n\n// UpdateEgressConfigHandler 更新出口配置\nfunc UpdateEgressConfigHandler(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tname := gjson.Get(string(body), \"name\").String()\n\ttenantID := gjson.Get(string(body), \"tenant_id\").String()\n\thostID := gjson.Get(string(body), \"host_id\").String()\n\tinItemID := gjson.Get(string(body), \"in_item_id\").String()\n\toutItemID := gjson.Get(string(body), \"out_item_id\").String()\n\tstatus := int(gjson.Get(string(body), \"status\").Int())\n\tsortOrder := int(gjson.Get(string(body), \"sort_order\").Int())\n\n\tif name == \"\" || tenantID == \"\" || hostID == \"\" || inItemID == \"\" || outItemID == \"\" {\n\t\tresponse.BadRequest(c, \"缺少必填字段\")\n\t\treturn\n\t}\n\n\tconfig := &model.EgressConfig{\n\t\tID: id,\n\t\tName: name,\n\t\tInstanceID: tenantID,\n\t\tHostID: hostID,\n\t\tInItemID: inItemID,\n\t\tOutItemID: outItemID,\n\t\tStatus: status,\n\t\tSortOrder: sortOrder,\n\t}\n\n\terr = model.UpdateEgressConfig(config)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"更新成功\", config)\n}\n\n// DeleteEgressConfigHandler 删除出口配置\nfunc DeleteEgressConfigHandler(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\n\terr = model.DeleteEgressConfig(id)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"删除成功\", nil)\n}\n"}, "67cdc6d758af94c4a3506989ea3eca9bb38e9dc9": {"t": 1785754907.1790843, "n": 9163, "h": "b801387df09ce22c", "c": "package handler\n\nimport (\n\t\"zbxtable/pkg/response\"\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"zbxtable/internal/model\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/tidwall/gjson\"\n)\n\n// GetAllSystem 获取系统配置列表\nfunc GetAllSystem(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tcnt, val, err := model.GetALlSystem()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"ok\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = cnt\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetSystemByID 获取单个系统配置\nfunc GetSystemByID(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\tvar SystemRes model.SystemList\n\tval, err := model.GetSystemByID(int64(id))\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"ok\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// UpdateSystem 更新系统配置(支持多实例)\nfunc UpdateSystem(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\t\n\t// 获取实例ID\n\tinstanceIDStr := gjson.Get(string(body), \"instance_id\").String()\n\tif instanceIDStr == \"\" {\n\t\tresponse.BadRequest(c, \"请选择 Zabbix 实例\")\n\t\treturn\n\t}\n\t\n\t// 查找实例获取数字ID\n\ttenant, err := model.GetZabbixTenantByTenantID(instanceIDStr)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"实例不存在\")\n\t\treturn\n\t}\n\t\n\tcpuCore := gjson.Get(string(body), \"cpu_core\").String()\n\tuptimeId := gjson.Get(string(body), \"uptime_id\").String()\n\tcpuUtilizationId := gjson.Get(string(body), \"cpu_utilization_id\").String()\n\tgroupId := gjson.Get(string(body), \"group_id\").String()\n\tmemoryTotalId := gjson.Get(string(body), \"memory_total_id\").String()\n\tmemoryUsedId := gjson.Get(string(body), \"memory_used_id\").String()\n\tmemoryUtilizationId := gjson.Get(string(body), \"memory_utilization_id\").String()\n\tmode := gjson.Get(string(body), \"model\").String()\n\tpingTemplateId := gjson.Get(string(body), \"ping_template_id\").String()\n\n\tvar SystemRes model.SystemList\n\tv := model.System{\n\t\tID: int64(id),\n\t\tInstanceID: tenant.ID,\n\t\tCPUCore: cpuCore,\n\t\tCPUUtilizationID: cpuUtilizationId,\n\t\tGroupID: groupId,\n\t\tMemoryTotalID: memoryTotalId,\n\t\tUptimeID: uptimeId,\n\t\tModel: mode,\n\t\tMemoryUsedID: memoryUsedId,\n\t\tMemoryUtilizationID: memoryUtilizationId,\n\t\tPingTemplateID: pingTemplateId,\n\t}\n\terr = model.CreateOrUpdateSystem(&v)\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"更新成功\"\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// SystemInit 初始化系统(支持多实例)\nfunc SystemInit(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\t\n\t// 读取请求体获取实例ID\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\t\n\tinstanceIDStr := gjson.Get(string(body), \"instance_id\").String()\n\tif instanceIDStr == \"\" {\n\t\tresponse.BadRequest(c, \"请选择 Zabbix 实例\")\n\t\treturn\n\t}\n\t\n\t// 查找实例获取数字ID\n\ttenant, err := model.GetZabbixTenantByTenantID(instanceIDStr)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"实例不存在\")\n\t\treturn\n\t}\n\t\n\tvar SystemRes model.SystemList\n\terr = model.SystemInitWithInstance(int64(id), tenant.ID)\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t\tSystemRes.Data.Total = 0\n\t\tSystemRes.Data.Items = \"\"\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"初始化完成\"\n\t\tSystemRes.Data.Total = 0\n\t\tSystemRes.Data.Items = \"\"\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetEgress 获取出口带宽配置\nfunc GetEgress(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tval, err := model.GetEgress()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"获取成功\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// UpdateEgress 更新带宽配置\nfunc UpdateEgress(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tnameone := gjson.Get(string(body), \"name_one\").String()\n\tin_one := gjson.Get(string(body), \"in_one\").String()\n\tout_one := gjson.Get(string(body), \"out_one\").String()\n\tnametwo := gjson.Get(string(body), \"name_two\").String()\n\tin_two := gjson.Get(string(body), \"in_two\").String()\n\tout_two := gjson.Get(string(body), \"out_two\").String()\n\tvar SystemRes model.SystemList\n\tv := model.Egress{ID: 1,\n\t\tNameOne: nameone, InOne: in_one, OutOne: out_one,\n\t\tNameTwo: nametwo, InTwo: in_two, OutTwo: out_two}\n\terr = model.UpdateEgress(&v)\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"更新成功\"\n\t\tSystemRes.Data.Items = \"\"\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetAllConfig 获取系统参数配置\nfunc GetAllConfig(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tval, err := model.GetConfigList()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"获取成功\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = int64(len(val))\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// UpdateConfig 更新系统参数配置\nfunc UpdateConfig(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tvalue := gjson.Get(string(body), \"value\").String()\n\tvar SystemRes model.SystemList\n\tv := model.Config{ID: int64(id), Value: value}\n\terr = model.UpdateConfig(&v)\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"更新成功\"\n\t\tSystemRes.Data.Items = \"\"\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// ============ 新的出口配置 API ============\n\n// GetAllEgressConfigs 获取所有出口配置\nfunc GetAllEgressConfigs(c *gin.Context) {\n\tconfigs, err := model.GetAllEgressConfigs()\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.Success(c, configs)\n}\n\n// GetEgressConfigByID 根据ID获取出口配置\nfunc GetEgressConfigByID(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\n\tconfig, err := model.GetEgressConfigByID(id)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.Success(c, config)\n}\n\n// AddEgressConfig 添加出口配置\nfunc AddEgressConfig(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tname := gjson.Get(string(body), \"name\").String()\n\ttenantID := gjson.Get(string(body), \"tenant_id\").String()\n\thostID := gjson.Get(string(body), \"host_id\").String()\n\tinItemID := gjson.Get(string(body), \"in_item_id\").String()\n\toutItemID := gjson.Get(string(body), \"out_item_id\").String()\n\tsortOrder := int(gjson.Get(string(body), \"sort_order\").Int())\n\n\tif name == \"\" || tenantID == \"\" || hostID == \"\" || inItemID == \"\" || outItemID == \"\" {\n\t\tresponse.BadRequest(c, \"缺少必填字段\")\n\t\treturn\n\t}\n\n\tconfig := &model.EgressConfig{\n\t\tName: name,\n\t\tTenantID: tenantID,\n\t\tHostID: hostID,\n\t\tInItemID: inItemID,\n\t\tOutItemID: outItemID,\n\t\tStatus: 1,\n\t\tSortOrder: sortOrder,\n\t}\n\n\terr = model.AddEgressConfig(config)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"添加成功\", config)\n}\n\n// UpdateEgressConfigHandler 更新出口配置\nfunc UpdateEgressConfigHandler(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tname := gjson.Get(string(body), \"name\").String()\n\ttenantID := gjson.Get(string(body), \"tenant_id\").String()\n\thostID := gjson.Get(string(body), \"host_id\").String()\n\tinItemID := gjson.Get(string(body), \"in_item_id\").String()\n\toutItemID := gjson.Get(string(body), \"out_item_id\").String()\n\tstatus := int(gjson.Get(string(body), \"status\").Int())\n\tsortOrder := int(gjson.Get(string(body), \"sort_order\").Int())\n\n\tif name == \"\" || tenantID == \"\" || hostID == \"\" || inItemID == \"\" || outItemID == \"\" {\n\t\tresponse.BadRequest(c, \"缺少必填字段\")\n\t\treturn\n\t}\n\n\tconfig := &model.EgressConfig{\n\t\tID: id,\n\t\tName: name,\n\t\tTenantID: tenantID,\n\t\tHostID: hostID,\n\t\tInItemID: inItemID,\n\t\tOutItemID: outItemID,\n\t\tStatus: status,\n\t\tSortOrder: sortOrder,\n\t}\n\n\terr = model.UpdateEgressConfig(config)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"更新成功\", config)\n}\n\n// DeleteEgressConfigHandler 删除出口配置\nfunc DeleteEgressConfigHandler(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\tresponse.BadRequest(c, \"无效的ID\")\n\t\treturn\n\t}\n\n\terr = model.DeleteEgressConfig(id)\n\tif err != nil {\n\t\tresponse.InternalError(c, err.Error())\n\t\treturn\n\t}\n\tresponse.SuccessWithMessage(c, \"删除成功\", nil)\n}\n"}, "8f8be1a749d36e95f77ef207c16b3539be2af297": {"t": 1785754908.6338804, "n": 5928, "h": "116ec190759b5006", "c": "package handler\n\nimport (\n\t\"zbxtable/pkg/response\"\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"zbxtable/internal/model\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/tidwall/gjson\"\n)\n\n// GetAllSystem 获取系统配置列表\nfunc GetAllSystem(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tcnt, val, err := model.GetALlSystem()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"ok\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = cnt\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetSystemByID 获取单个系统配置\nfunc GetSystemByID(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\tvar SystemRes model.SystemList\n\tval, err := model.GetSystemByID(int64(id))\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"ok\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// UpdateSystem 更新系统配置(支持多实例)\nfunc UpdateSystem(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\t\n\t// 获取实例ID\n\tinstanceIDStr := gjson.Get(string(body), \"instance_id\").String()\n\tif instanceIDStr == \"\" {\n\t\tresponse.BadRequest(c, \"请选择 Zabbix 实例\")\n\t\treturn\n\t}\n\t\n\t// 查找实例获取数字ID\n\ttenant, err := model.GetZabbixTenantByTenantID(instanceIDStr)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"实例不存在\")\n\t\treturn\n\t}\n\t\n\tcpuCore := gjson.Get(string(body), \"cpu_core\").String()\n\tuptimeId := gjson.Get(string(body), \"uptime_id\").String()\n\tcpuUtilizationId := gjson.Get(string(body), \"cpu_utilization_id\").String()\n\tgroupId := gjson.Get(string(body), \"group_id\").String()\n\tmemoryTotalId := gjson.Get(string(body), \"memory_total_id\").String()\n\tmemoryUsedId := gjson.Get(string(body), \"memory_used_id\").String()\n\tmemoryUtilizationId := gjson.Get(string(body), \"memory_utilization_id\").String()\n\tmode := gjson.Get(string(body), \"model\").String()\n\tpingTemplateId := gjson.Get(string(body), \"ping_template_id\").String()\n\n\tvar SystemRes model.SystemList\n\tv := model.System{\n\t\tID: int64(id),\n\t\tInstanceID: tenant.ID,\n\t\tCPUCore: cpuCore,\n\t\tCPUUtilizationID: cpuUtilizationId,\n\t\tGroupID: groupId,\n\t\tMemoryTotalID: memoryTotalId,\n\t\tUptimeID: uptimeId,\n\t\tModel: mode,\n\t\tMemoryUsedID: memoryUsedId,\n\t\tMemoryUtilizationID: memoryUtilizationId,\n\t\tPingTemplateID: pingTemplateId,\n\t}\n\terr = model.CreateOrUpdateSystem(&v)\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"更新成功\"\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// SystemInit 初始化系统(支持多实例)\nfunc SystemInit(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\t\n\t// 读取请求体获取实例ID\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\t\n\tinstanceIDStr := gjson.Get(string(body), \"instance_id\").String()\n\tif instanceIDStr == \"\" {\n\t\tresponse.BadRequest(c, \"请选择 Zabbix 实例\")\n\t\treturn\n\t}\n\t\n\t// 查找实例获取数字ID\n\ttenant, err := model.GetZabbixTenantByTenantID(instanceIDStr)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"实例不存在\")\n\t\treturn\n\t}\n\t\n\tvar SystemRes model.SystemList\n\terr = model.SystemInitWithInstance(int64(id), tenant.ID)\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t\tSystemRes.Data.Total = 0\n\t\tSystemRes.Data.Items = \"\"\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"初始化完成\"\n\t\tSystemRes.Data.Total = 0\n\t\tSystemRes.Data.Items = \"\"\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetEgress 获取出口带宽配置\nfunc GetEgress(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tval, err := model.GetEgress()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"获取成功\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// UpdateEgress 更新带宽配置\nfunc UpdateEgress(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tnameone := gjson.Get(string(body), \"name_one\").String()\n\tin_one := gjson.Get(string(body), \"in_one\").String()\n\tout_one := gjson.Get(string(body), \"out_one\").String()\n\tnametwo := gjson.Get(string(body), \"name_two\").String()\n\tin_two := gjson.Get(string(body), \"in_two\").String()\n\tout_two := gjson.Get(string(body), \"out_two\").String()\n\tvar SystemRes model.SystemList\n\tv := model.Egress{ID: 1,\n\t\tNameOne: nameone, InOne: in_one, OutOne: out_one,\n\t\tNameTwo: nametwo, InTwo: in_two, OutTwo: out_two}\n\terr = model.UpdateEgress(&v)\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"更新成功\"\n\t\tSystemRes.Data.Items = \"\"\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetAllConfig 获取系统参数配置\nfunc GetAllConfig(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tval, err := model.GetConfigList()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"获取成功\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = int64(len(val))\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// UpdateConfig 更新系统参数配置\nfunc UpdateConfig(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tvalue := gjson.Get(string(body), \"value\").String()\n\tvar SystemRes model.SystemList\n\tv := model.Config{ID: int64(id), Value: value}\n\terr = model.UpdateConfig(&v)\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"更新成功\"\n\t\tSystemRes.Data.Items = \"\"\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n"}, "d3b75876f510931c43e2e666ded22df248942372": {"t": 1785754911.160579, "n": 6125, "h": "3d7de2cdc4e057dc", "c": "package handler\n\nimport (\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"zbxtable/internal/model\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/tidwall/gjson\"\n)\n\n// GetAllSystem 获取系统配置列表\nfunc GetAllSystem(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tcnt, val, err := model.GetALlSystem()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"ok\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = cnt\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetSystemByID 获取单个系统配置\nfunc GetSystemByID(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\tvar SystemRes model.SystemList\n\tval, err := model.GetSystemByID(int64(id))\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"ok\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// UpdateSystem 更新系统配置(支持多实例)\nfunc UpdateSystem(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\"code\": 500, \"message\": \"请求体读取失败\"})\n\t\treturn\n\t}\n\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\t\n\t// 获取实例ID\n\tinstanceIDStr := gjson.Get(string(body), \"instance_id\").String()\n\tif instanceIDStr == \"\" {\n\t\tc.JSON(http.StatusOK, gin.H{\"code\": 400, \"message\": \"请选择 Zabbix 实例\"})\n\t\treturn\n\t}\n\t\n\t// 查找实例获取数字ID\n\ttenant, err := model.GetZabbixTenantByTenantID(instanceIDStr)\n\tif err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\"code\": 500, \"message\": \"实例不存在\"})\n\t\treturn\n\t}\n\t\n\tcpuCore := gjson.Get(string(body), \"cpu_core\").String()\n\tuptimeId := gjson.Get(string(body), \"uptime_id\").String()\n\tcpuUtilizationId := gjson.Get(string(body), \"cpu_utilization_id\").String()\n\tgroupId := gjson.Get(string(body), \"group_id\").String()\n\tmemoryTotalId := gjson.Get(string(body), \"memory_total_id\").String()\n\tmemoryUsedId := gjson.Get(string(body), \"memory_used_id\").String()\n\tmemoryUtilizationId := gjson.Get(string(body), \"memory_utilization_id\").String()\n\tmode := gjson.Get(string(body), \"model\").String()\n\tpingTemplateId := gjson.Get(string(body), \"ping_template_id\").String()\n\n\tvar SystemRes model.SystemList\n\tv := model.System{\n\t\tID: int64(id),\n\t\tInstanceID: tenant.ID,\n\t\tCPUCore: cpuCore,\n\t\tCPUUtilizationID: cpuUtilizationId,\n\t\tGroupID: groupId,\n\t\tMemoryTotalID: memoryTotalId,\n\t\tUptimeID: uptimeId,\n\t\tModel: mode,\n\t\tMemoryUsedID: memoryUsedId,\n\t\tMemoryUtilizationID: memoryUtilizationId,\n\t\tPingTemplateID: pingTemplateId,\n\t}\n\terr = model.CreateOrUpdateSystem(&v)\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"更新成功\"\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// SystemInit 初始化系统(支持多实例)\nfunc SystemInit(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\t\n\t// 读取请求体获取实例ID\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\"code\": 500, \"message\": \"请求体读取失败\"})\n\t\treturn\n\t}\n\t\n\tinstanceIDStr := gjson.Get(string(body), \"instance_id\").String()\n\tif instanceIDStr == \"\" {\n\t\tc.JSON(http.StatusOK, gin.H{\"code\": 400, \"message\": \"请选择 Zabbix 实例\"})\n\t\treturn\n\t}\n\t\n\t// 查找实例获取数字ID\n\ttenant, err := model.GetZabbixTenantByTenantID(instanceIDStr)\n\tif err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\"code\": 500, \"message\": \"实例不存在\"})\n\t\treturn\n\t}\n\t\n\tvar SystemRes model.SystemList\n\terr = model.SystemInitWithInstance(int64(id), tenant.ID)\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t\tSystemRes.Data.Total = 0\n\t\tSystemRes.Data.Items = \"\"\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"初始化完成\"\n\t\tSystemRes.Data.Total = 0\n\t\tSystemRes.Data.Items = \"\"\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetEgress 获取出口带宽配置\nfunc GetEgress(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tval, err := model.GetEgress()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"获取成功\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// UpdateEgress 更新带宽配置\nfunc UpdateEgress(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\"code\": 500, \"message\": \"请求体读取失败\"})\n\t\treturn\n\t}\n\n\tnameone := gjson.Get(string(body), \"name_one\").String()\n\tin_one := gjson.Get(string(body), \"in_one\").String()\n\tout_one := gjson.Get(string(body), \"out_one\").String()\n\tnametwo := gjson.Get(string(body), \"name_two\").String()\n\tin_two := gjson.Get(string(body), \"in_two\").String()\n\tout_two := gjson.Get(string(body), \"out_two\").String()\n\tvar SystemRes model.SystemList\n\tv := model.Egress{ID: 1,\n\t\tNameOne: nameone, InOne: in_one, OutOne: out_one,\n\t\tNameTwo: nametwo, InTwo: in_two, OutTwo: out_two}\n\terr = model.UpdateEgress(&v)\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"更新成功\"\n\t\tSystemRes.Data.Items = \"\"\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetAllConfig 获取系统参数配置\nfunc GetAllConfig(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tval, err := model.GetConfigList()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"获取成功\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = int64(len(val))\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// UpdateConfig 更新系统参数配置\nfunc UpdateConfig(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\"code\": 500, \"message\": \"请求体读取失败\"})\n\t\treturn\n\t}\n\n\tvalue := gjson.Get(string(body), \"value\").String()\n\tvar SystemRes model.SystemList\n\tv := model.Config{ID: int64(id), Value: value}\n\terr = model.UpdateConfig(&v)\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"更新成功\"\n\t\tSystemRes.Data.Items = \"\"\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n"}, "f507b9d7748c5aaff10657b4336f9efe45493357": {"t": 1785754912.6652696, "n": 5071, "h": "949f4d52efad9935", "c": "package handler\n\nimport (\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"zbxtable/internal/model\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/tidwall/gjson\"\n)\n\n// GetAllSystem 获取系统配置列表\nfunc GetAllSystem(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tcnt, val, err := model.GetALlSystem()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"ok\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = cnt\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetSystemByID 获取单个系统配置\nfunc GetSystemByID(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\tvar SystemRes model.SystemList\n\tval, err := model.GetSystemByID(int64(id))\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"ok\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// UpdateSystem 更新系统配置\nfunc UpdateSystem(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\"code\": 500, \"message\": \"请求体读取失败\"})\n\t\treturn\n\t}\n\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\tcpuCore := gjson.Get(string(body), \"cpu_core\").String()\n\tuptimeId := gjson.Get(string(body), \"uptime_id\").String()\n\tcpuUtilizationId := gjson.Get(string(body), \"cpu_utilization_id\").String()\n\tgroupId := gjson.Get(string(body), \"group_id\").String()\n\tmemoryTotalId := gjson.Get(string(body), \"memory_total_id\").String()\n\tmemoryUsedId := gjson.Get(string(body), \"memory_used_id\").String()\n\tmemoryUtilizationId := gjson.Get(string(body), \"memory_utilization_id\").String()\n\tmode := gjson.Get(string(body), \"model\").String()\n\tpingTemplateId := gjson.Get(string(body), \"ping_template_id\").String()\n\n\tvar SystemRes model.SystemList\n\tv := model.System{ID: int64(id), CPUCore: cpuCore, CPUUtilizationID: cpuUtilizationId,\n\t\tGroupID: groupId, MemoryTotalID: memoryTotalId, UptimeID: uptimeId, Model: mode,\n\t\tMemoryUsedID: memoryUsedId, MemoryUtilizationID: memoryUtilizationId,\n\t\tPingTemplateID: pingTemplateId}\n\terr = model.UpdateSystem(&v)\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"更新成功\"\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// SystemInit 初始化系统\nfunc SystemInit(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\tvar SystemRes model.SystemList\n\terr := model.SystemInit(int64(id))\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t\tSystemRes.Data.Total = 0\n\t\tSystemRes.Data.Items = \"\"\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"初始化完成\"\n\t\tSystemRes.Data.Total = 0\n\t\tSystemRes.Data.Items = \"\"\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetEgress 获取出口带宽配置\nfunc GetEgress(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tval, err := model.GetEgress()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"获取成功\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// UpdateEgress 更新带宽配置\nfunc UpdateEgress(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\"code\": 500, \"message\": \"请求体读取失败\"})\n\t\treturn\n\t}\n\n\tnameone := gjson.Get(string(body), \"name_one\").String()\n\tin_one := gjson.Get(string(body), \"in_one\").String()\n\tout_one := gjson.Get(string(body), \"out_one\").String()\n\tnametwo := gjson.Get(string(body), \"name_two\").String()\n\tin_two := gjson.Get(string(body), \"in_two\").String()\n\tout_two := gjson.Get(string(body), \"out_two\").String()\n\tvar SystemRes model.SystemList\n\tv := model.Egress{ID: 1,\n\t\tNameOne: nameone, InOne: in_one, OutOne: out_one,\n\t\tNameTwo: nametwo, InTwo: in_two, OutTwo: out_two}\n\terr = model.UpdateEgress(&v)\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"更新成功\"\n\t\tSystemRes.Data.Items = \"\"\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetAllConfig 获取系统参数配置\nfunc GetAllConfig(c *gin.Context) {\n\tvar SystemRes model.SystemList\n\tval, err := model.GetConfigList()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"获取成功\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = int64(len(val))\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// UpdateConfig 更新系统参数配置\nfunc UpdateConfig(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\"code\": 500, \"message\": \"请求体读取失败\"})\n\t\treturn\n\t}\n\n\tvalue := gjson.Get(string(body), \"value\").String()\n\tvar SystemRes model.SystemList\n\tv := model.Config{ID: int64(id), Value: value}\n\terr = model.UpdateConfig(&v)\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"更新成功\"\n\t\tSystemRes.Data.Items = \"\"\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n"}, "54928571d31b4cc10676191ae02c19c271ab8824": {"t": 1785754913.9113488, "n": 5096, "h": "2d9f76c94e4f3c1c", "c": "package handler\n\nimport (\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"zbxtable/internal/model\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/tidwall/gjson\"\n)\n\n// GetAllSystem 获取系统配置列表\nfunc GetAllSystem(c *gin.Context) {\n\tvar SystemRes models.SystemList\n\tcnt, val, err := models.GetALlSystem()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"ok\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = cnt\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetSystemByID 获取单个系统配置\nfunc GetSystemByID(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\tvar SystemRes models.SystemList\n\tval, err := models.GetSystemByID(int64(id))\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"ok\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// UpdateSystem 更新系统配置\nfunc UpdateSystem(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\"code\": 500, \"message\": \"请求体读取失败\"})\n\t\treturn\n\t}\n\t\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\tcpuCore := gjson.Get(string(body), \"cpu_core\").String()\n\tuptimeId := gjson.Get(string(body), \"uptime_id\").String()\n\tcpuUtilizationId := gjson.Get(string(body), \"cpu_utilization_id\").String()\n\tgroupId := gjson.Get(string(body), \"group_id\").String()\n\tmemoryTotalId := gjson.Get(string(body), \"memory_total_id\").String()\n\tmemoryUsedId := gjson.Get(string(body), \"memory_used_id\").String()\n\tmemoryUtilizationId := gjson.Get(string(body), \"memory_utilization_id\").String()\n\tmodel := gjson.Get(string(body), \"model\").String()\n\tpingTemplateId := gjson.Get(string(body), \"ping_template_id\").String()\n\t\n\tvar SystemRes models.SystemList\n\tv := models.System{ID: int64(id), CPUCore: cpuCore, CPUUtilizationID: cpuUtilizationId,\n\t\tGroupID: groupId, MemoryTotalID: memoryTotalId, UptimeID: uptimeId, Model: model,\n\t\tMemoryUsedID: memoryUsedId, MemoryUtilizationID: memoryUtilizationId,\n\t\tPingTemplateID: pingTemplateId}\n\terr = models.UpdateSystem(&v)\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"更新成功\"\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// SystemInit 初始化系统\nfunc SystemInit(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\tvar SystemRes models.SystemList\n\terr := models.SystemInit(int64(id))\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t\tSystemRes.Data.Total = 0\n\t\tSystemRes.Data.Items = \"\"\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"初始化完成\"\n\t\tSystemRes.Data.Total = 0\n\t\tSystemRes.Data.Items = \"\"\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetEgress 获取出口带宽配置\nfunc GetEgress(c *gin.Context) {\n\tvar SystemRes models.SystemList\n\tval, err := models.GetEgress()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"获取成功\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// UpdateEgress 更新带宽配置\nfunc UpdateEgress(c *gin.Context) {\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\"code\": 500, \"message\": \"请求体读取失败\"})\n\t\treturn\n\t}\n\t\n\tnameone := gjson.Get(string(body), \"name_one\").String()\n\tin_one := gjson.Get(string(body), \"in_one\").String()\n\tout_one := gjson.Get(string(body), \"out_one\").String()\n\tnametwo := gjson.Get(string(body), \"name_two\").String()\n\tin_two := gjson.Get(string(body), \"in_two\").String()\n\tout_two := gjson.Get(string(body), \"out_two\").String()\n\tvar SystemRes models.SystemList\n\tv := models.Egress{ID: 1,\n\t\tNameOne: nameone, InOne: in_one, OutOne: out_one,\n\t\tNameTwo: nametwo, InTwo: in_two, OutTwo: out_two}\n\terr = models.UpdateEgress(&v)\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"更新成功\"\n\t\tSystemRes.Data.Items = \"\"\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// GetAllConfig 获取系统参数配置\nfunc GetAllConfig(c *gin.Context) {\n\tvar SystemRes models.SystemList\n\tval, err := models.GetConfigList()\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"获取成功\"\n\t\tSystemRes.Data.Items = val\n\t\tSystemRes.Data.Total = int64(len(val))\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n\n// UpdateConfig 更新系统参数配置\nfunc UpdateConfig(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, _ := strconv.Atoi(idStr)\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\"code\": 500, \"message\": \"请求体读取失败\"})\n\t\treturn\n\t}\n\t\n\tvalue := gjson.Get(string(body), \"value\").String()\n\tvar SystemRes models.SystemList\n\tv := models.Config{ID: int64(id), Value: value}\n\terr = models.UpdateConfig(&v)\n\tif err != nil {\n\t\tSystemRes.Code = 500\n\t\tSystemRes.Message = err.Error()\n\t} else {\n\t\tSystemRes.Code = 200\n\t\tSystemRes.Message = \"更新成功\"\n\t\tSystemRes.Data.Items = \"\"\n\t\tSystemRes.Data.Total = 1\n\t}\n\tc.JSON(http.StatusOK, SystemRes)\n}\n"}}, "internal/handler/ai.go": {"bcdc3997c4da6f884243c76a79541b3930b0018d": {"t": 1785745618.675846, "n": 13319, "h": "b918c4bf47a8ca01", "c": "package handler\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strings\"\n\t\"zbxtable/internal/model\"\n\t\"zbxtable/pkg/response\"\n\n\t\"github.com/gin-gonic/gin\"\n)\n\nvar alarmPromptVarPattern = regexp.MustCompile(`\\{\\{\\s*([a-zA-Z0-9_]+)\\s*\\}\\}`)\nvar aiConfigValueByKey = model.GetConfigValueByKey\nvar aiHTTPClient = &http.Client{}\n\nfunc extractAlarmContext(rawMessage string) (string, string) {\n\tparts := strings.SplitN(rawMessage, \"\\n\\n\", 2)\n\tif len(parts) == 2 {\n\t\tctx := strings.TrimSpace(parts[0])\n\t\tif strings.Contains(ctx, \"告警上下文\") || strings.Contains(ctx, \"Alarm Context\") {\n\t\t\treturn ctx, strings.TrimSpace(parts[1])\n\t\t}\n\t}\n\treturn \"\", strings.TrimSpace(rawMessage)\n}\n\ntype AlarmContextPayload struct {\n\tHostname string `json:\"hostname\"`\n\tHostIP string `json:\"host_ip\"`\n\tMessage string `json:\"message\"`\n\tDetail string `json:\"detail\"`\n\tLevel string `json:\"level\"`\n\tStatus string `json:\"status\"`\n}\n\nfunc parseContextFields(alarmContext string) map[string]string {\n\tfields := map[string]string{}\n\tif alarmContext == \"\" {\n\t\treturn fields\n\t}\n\tfor _, line := range strings.Split(alarmContext, \"\\n\") {\n\t\tline = strings.TrimSpace(line)\n\t\tif line == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tkv := strings.SplitN(line, \":\", 2)\n\t\tif len(kv) != 2 {\n\t\t\tcontinue\n\t\t}\n\t\tk := strings.TrimSpace(kv[0])\n\t\tv := strings.TrimSpace(kv[1])\n\t\tswitch k {\n\t\tcase \"设备名称\", \"Device Name\":\n\t\t\tfields[\"hostname\"] = v\n\t\tcase \"IP\":\n\t\t\tfields[\"host_ip\"] = v\n\t\tcase \"告警描述\", \"Description\":\n\t\t\tfields[\"message\"] = v\n\t\tcase \"告警详情\", \"Detail\":\n\t\t\tfields[\"detail\"] = v\n\t\t}\n\t}\n\treturn fields\n}\n\nfunc renderAlarmPromptTemplate(template string, rawMessage string, alarmCtx *AlarmContextPayload) string {\n\tt := strings.TrimSpace(template)\n\tif t == \"\" {\n\t\treturn rawMessage\n\t}\n\n\talarmContext, userMessage := extractAlarmContext(rawMessage)\n\tctxFields := parseContextFields(alarmContext)\n\tif alarmCtx != nil {\n\t\tif alarmCtx.Hostname != \"\" {\n\t\t\tctxFields[\"hostname\"] = alarmCtx.Hostname\n\t\t}\n\t\tif alarmCtx.HostIP != \"\" {\n\t\t\tctxFields[\"host_ip\"] = alarmCtx.HostIP\n\t\t}\n\t\tif alarmCtx.Message != \"\" {\n\t\t\tctxFields[\"message\"] = alarmCtx.Message\n\t\t}\n\t\tif alarmCtx.Detail != \"\" {\n\t\t\tctxFields[\"detail\"] = alarmCtx.Detail\n\t\t}\n\t\tif alarmCtx.Level != \"\" {\n\t\t\tctxFields[\"level\"] = alarmCtx.Level\n\t\t}\n\t\tif alarmCtx.Status != \"\" {\n\t\t\tctxFields[\"status\"] = alarmCtx.Status\n\t\t}\n\t}\n\tif alarmContext == \"\" && len(ctxFields) > 0 {\n\t\talarmContext = strings.TrimSpace(\n\t\t\tfmt.Sprintf(\"告警上下文:\\n设备名称: %s\\nIP: %s\\n告警描述: %s\\n告警详情: %s\",\n\t\t\t\tctxFields[\"hostname\"], ctxFields[\"host_ip\"], ctxFields[\"message\"], ctxFields[\"detail\"]),\n\t\t)\n\t}\n\n\tmapping := map[string]string{\n\t\t\"hostname\": ctxFields[\"hostname\"],\n\t\t\"host_ip\": ctxFields[\"host_ip\"],\n\t\t\"message\": ctxFields[\"message\"],\n\t\t\"detail\": ctxFields[\"detail\"],\n\t\t\"level\": ctxFields[\"level\"],\n\t\t\"status\": ctxFields[\"status\"],\n\t\t\"alarm_context\": alarmContext,\n\t}\n\n\trendered := alarmPromptVarPattern.ReplaceAllStringFunc(t, func(token string) string {\n\t\tmatched := alarmPromptVarPattern.FindStringSubmatch(token)\n\t\tif len(matched) < 2 {\n\t\t\treturn token\n\t\t}\n\t\tkey := matched[1]\n\t\tif val, ok := mapping[key]; ok {\n\t\t\treturn val\n\t\t}\n\t\treturn token\n\t})\n\n\tif userMessage != \"\" {\n\t\trendered = strings.TrimSpace(rendered + \"\\n\\n\" + userMessage)\n\t}\n\treturn rendered\n}\n\n// AIChat 流式聊天接口\nfunc AIChat(c *gin.Context) {\n\t// 设置响应头\n\tc.Header(\"Content-Type\", \"text/event-stream\")\n\tc.Header(\"Cache-Control\", \"no-cache\")\n\tc.Header(\"Connection\", \"keep-alive\")\n\n\t// 解析请求体\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tvar userReq struct {\n\t\tMessage string `json:\"message\"`\n\t\tAlarmContext *AlarmContextPayload `json:\"alarm_context\"`\n\t}\n\tif err := json.Unmarshal(body, &userReq); err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = err.Error()\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\t// 告警分析提示词(后端兜底渲染,避免仅依赖前端)\n\talarmPromptTpl := aiConfigValueByKey(\"alarm_analysis_prompt\", \"\")\n\tfinalMessage := renderAlarmPromptTemplate(alarmPromptTpl, userReq.Message, userReq.AlarmContext)\n\n\t// 获取 AI 类型配置\n\taiType := aiConfigValueByKey(\"ai_type\", \"ollama\")\n\tif aiType == \"\" {\n\t\taiType = \"ollama\"\n\t}\n\n\t// 根据 AI 类型调用不同的服务\n\tswitch aiType {\n\tcase \"deepseek\":\n\t\thandleDeepseekChat(c, finalMessage)\n\tcase \"custom\":\n\t\thandleCustomChat(c, finalMessage)\n\tcase \"ollama\":\n\t\thandleOllamaChat(c, finalMessage)\n\tdefault:\n\t\t// 默认使用 Ollama\n\t\thandleOllamaChat(c, finalMessage)\n\t}\n}\n\n// handleOllamaChat 处理 Ollama AI 请求\nfunc handleOllamaChat(c *gin.Context, message string) {\n\t// 构造系统提示词\n\tsystemPrompt := struct {\n\t\tRole string `json:\"role\"`\n\t\tContent string `json:\"content\"`\n\t}{\n\t\tRole: \"system\",\n\t\tContent: \"你是一个专业的运维分析师,请分析用户提供的问题并给出专业的建议。\",\n\t}\n\n\t// 从配置获取 Ollama 地址\n\tollamaHost := aiConfigValueByKey(\"ollama_host\", model.GetConfKey(\"ollama_host\"))\n\tif ollamaHost == \"\" {\n\t\tollamaHost = \"http://localhost:11434\"\n\t}\n\tollamaModel := aiConfigValueByKey(\"ollama_model\", model.GetConfKey(\"ollama_model\"))\n\tif ollamaModel == \"\" {\n\t\tollamaModel = \"deepseek-r1:32b\"\n\t}\n\n\t// 构造 Ollama 请求\n\tollamaReq := struct {\n\t\tModel string `json:\"model\"`\n\t\tMessages []any `json:\"messages\"`\n\t\tStream bool `json:\"stream\"`\n\t}{\n\t\tModel: ollamaModel,\n\t\tStream: true,\n\t\tMessages: []any{\n\t\t\tsystemPrompt,\n\t\t\tstruct {\n\t\t\t\tRole string `json:\"role\"`\n\t\t\t\tContent string `json:\"content\"`\n\t\t\t}{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: message,\n\t\t\t},\n\t\t},\n\t}\n\n\tjsonData, err := json.Marshal(ollamaReq)\n\tif err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = err.Error()\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\tollamaURL := fmt.Sprintf(\"%s/api/chat\", ollamaHost)\n\n\t// 发送请求到 Ollama\n\tresp, err := http.Post(ollamaURL, \"application/json\", bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = fmt.Sprintf(\"Ollama 服务连接失败: %v\", err)\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\tdefer resp.Body.Close()\n\n\t// 创建 reader\n\treader := bufio.NewReader(resp.Body)\n\n\t// 流式读取响应\n\tfor {\n\t\tline, err := reader.ReadBytes('\\n')\n\t\tif err != nil {\n\t\t\tif err == io.EOF {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\treturn\n\t\t}\n\n\t\t// 解析响应\n\t\tvar response struct {\n\t\t\tModel string `json:\"model\"`\n\t\t\tCreatedAt string `json:\"created_at\"`\n\t\t\tMessage struct {\n\t\t\t\tRole string `json:\"role\"`\n\t\t\t\tContent string `json:\"content\"`\n\t\t\t} `json:\"message\"`\n\t\t\tDone bool `json:\"done\"`\n\t\t}\n\t\tif err := json.Unmarshal(line, &response); err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\t// 发送数据\n\t\tif response.Message.Content != \"\" {\n\t\t\tc.Writer.WriteString(response.Message.Content)\n\t\t\tc.Writer.Flush()\n\t\t}\n\n\t\t// 如果响应完成,退出循环\n\t\tif response.Done {\n\t\t\tbreak\n\t\t}\n\t}\n}\n\n// handleDeepseekChat 处理 Deepseek AI 请求\nfunc handleDeepseekChat(c *gin.Context, message string) {\n\t// 从配置获取 Deepseek 参数(会自动解密)\n\tapiKey := aiConfigValueByKey(\"deepseek_api_key\", \"\")\n\tif apiKey == \"\" {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = \"Deepseek API Key 未配置\"\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\tdeepseekModel := aiConfigValueByKey(\"deepseek_model\", \"deepseek-chat\")\n\tif deepseekModel == \"\" {\n\t\tdeepseekModel = \"deepseek-chat\"\n\t}\n\n\tbaseURL := aiConfigValueByKey(\"deepseek_base_url\", \"https://api.deepseek.com\")\n\tif baseURL == \"\" {\n\t\tbaseURL = \"https://api.deepseek.com\"\n\t}\n\n\t// 构造 Deepseek 请求\n\tdeepseekReq := struct {\n\t\tModel string `json:\"model\"`\n\t\tMessages []struct {\n\t\t\tRole string `json:\"role\"`\n\t\t\tContent string `json:\"content\"`\n\t\t} `json:\"messages\"`\n\t\tStream bool `json:\"stream\"`\n\t}{\n\t\tModel: deepseekModel,\n\t\tStream: true,\n\t\tMessages: []struct {\n\t\t\tRole string `json:\"role\"`\n\t\t\tContent string `json:\"content\"`\n\t\t}{\n\t\t\t{\n\t\t\t\tRole: \"system\",\n\t\t\t\tContent: \"你是一个专业的运维分析师,请分析用户提供的问题并给出专业的建议。\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: message,\n\t\t\t},\n\t\t},\n\t}\n\n\tjsonData, err := json.Marshal(deepseekReq)\n\tif err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = err.Error()\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\t// 构造 Deepseek API URL\n\tdeepseekURL := fmt.Sprintf(\"%s/chat/completions\", strings.TrimSuffix(baseURL, \"/\"))\n\n\t// 创建 HTTP 请求\n\treq, err := http.NewRequest(\"POST\", deepseekURL, bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = err.Error()\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\t// 设置请求头\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"Bearer %s\", apiKey))\n\n\t// 发送请求到 Deepseek\n\tresp, err := aiHTTPClient.Do(req)\n\tif err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = fmt.Sprintf(\"Deepseek 服务连接失败: %v\", err)\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\tdefer resp.Body.Close()\n\n\t// 检查响应状态\n\tif resp.StatusCode != http.StatusOK {\n\t\tbodyBytes, _ := io.ReadAll(resp.Body)\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = resp.StatusCode\n\t\tAIRes.Message = fmt.Sprintf(\"Deepseek API 错误: %s\", string(bodyBytes))\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\tstreamOpenAICompatibleResponse(c, resp.Body)\n}\n\nfunc openAICompatibleMessages(message string) []struct {\n\tRole string `json:\"role\"`\n\tContent string `json:\"content\"`\n} {\n\treturn []struct {\n\t\tRole string `json:\"role\"`\n\t\tContent string `json:\"content\"`\n\t}{\n\t\t{\n\t\t\tRole: \"system\",\n\t\t\tContent: \"你是一个专业的运维分析师,请分析用户提供的问题并给出专业的建议。\",\n\t\t},\n\t\t{\n\t\t\tRole: \"user\",\n\t\t\tContent: message,\n\t\t},\n\t}\n}\n\nfunc streamOpenAICompatibleResponse(c *gin.Context, body io.Reader) {\n\treader := bufio.NewReader(body)\n\n\tfor {\n\t\tline, err := reader.ReadBytes('\\n')\n\t\tif err != nil {\n\t\t\tif err == io.EOF {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\treturn\n\t\t}\n\n\t\tlineStr := strings.TrimSpace(string(line))\n\t\tif lineStr == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tlineStr = strings.TrimPrefix(lineStr, \"data: \")\n\t\tif lineStr == \"[DONE]\" {\n\t\t\tbreak\n\t\t}\n\n\t\tvar response struct {\n\t\t\tID string `json:\"id\"`\n\t\t\tObject string `json:\"object\"`\n\t\t\tCreated int64 `json:\"created\"`\n\t\t\tModel string `json:\"model\"`\n\t\t\tChoices []struct {\n\t\t\t\tIndex int `json:\"index\"`\n\t\t\t\tDelta struct {\n\t\t\t\t\tRole string `json:\"role\"`\n\t\t\t\t\tContent string `json:\"content\"`\n\t\t\t\t} `json:\"delta\"`\n\t\t\t\tFinishReason string `json:\"finish_reason\"`\n\t\t\t} `json:\"choices\"`\n\t\t}\n\n\t\tif err := json.Unmarshal([]byte(lineStr), &response); err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tif len(response.Choices) > 0 && response.Choices[0].Delta.Content != \"\" {\n\t\t\tc.Writer.WriteString(response.Choices[0].Delta.Content)\n\t\t\tc.Writer.Flush()\n\t\t}\n\n\t\tif len(response.Choices) > 0 && response.Choices[0].FinishReason != \"\" {\n\t\t\tbreak\n\t\t}\n\t}\n}\n\nfunc handleCustomChat(c *gin.Context, message string) {\n\tapiKey := aiConfigValueByKey(\"custom_api_key\", \"\")\n\tif apiKey == \"\" {\n\t\twriteAIConfigError(c, \"Custom API Key 未配置\")\n\t\treturn\n\t}\n\n\tcustomModel := aiConfigValueByKey(\"custom_model\", \"\")\n\tif customModel == \"\" {\n\t\twriteAIConfigError(c, \"Custom Model 未配置\")\n\t\treturn\n\t}\n\n\tbaseURL := aiConfigValueByKey(\"custom_base_url\", \"\")\n\tif baseURL == \"\" {\n\t\twriteAIConfigError(c, \"Custom Base URL 未配置\")\n\t\treturn\n\t}\n\n\tcustomReq := struct {\n\t\tModel string `json:\"model\"`\n\t\tMessages []struct {\n\t\t\tRole string `json:\"role\"`\n\t\t\tContent string `json:\"content\"`\n\t\t} `json:\"messages\"`\n\t\tStream bool `json:\"stream\"`\n\t}{\n\t\tModel: customModel,\n\t\tMessages: openAICompatibleMessages(message),\n\t\tStream: true,\n\t}\n\n\tjsonData, err := json.Marshal(customReq)\n\tif err != nil {\n\t\twriteAIConfigError(c, err.Error())\n\t\treturn\n\t}\n\n\tcustomURL := fmt.Sprintf(\"%s/chat/completions\", strings.TrimSuffix(baseURL, \"/\"))\n\treq, err := http.NewRequest(\"POST\", customURL, bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\twriteAIConfigError(c, err.Error())\n\t\treturn\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"Bearer %s\", apiKey))\n\n\tresp, err := aiHTTPClient.Do(req)\n\tif err != nil {\n\t\twriteAIConfigError(c, fmt.Sprintf(\"Custom AI 服务连接失败: %v\", err))\n\t\treturn\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\tbodyBytes, _ := io.ReadAll(resp.Body)\n\t\twriteAIStatusError(c, resp.StatusCode, fmt.Sprintf(\"Custom AI API 错误: %s\", string(bodyBytes)))\n\t\treturn\n\t}\n\n\tstreamOpenAICompatibleResponse(c, resp.Body)\n}\n\nfunc writeAIConfigError(c *gin.Context, message string) {\n\tvar AIRes struct {\n\t\tCode int `json:\"code\"`\n\t\tMessage string `json:\"message\"`\n\t\tData string `json:\"data\"`\n\t}\n\tAIRes.Code = 500\n\tAIRes.Message = message\n\tc.JSON(http.StatusOK, AIRes)\n}\n\nfunc writeAIStatusError(c *gin.Context, statusCode int, message string) {\n\tvar AIRes struct {\n\t\tCode int `json:\"code\"`\n\t\tMessage string `json:\"message\"`\n\t\tData string `json:\"data\"`\n\t}\n\tAIRes.Code = statusCode\n\tAIRes.Message = message\n\tc.JSON(http.StatusOK, AIRes)\n}\n"}, "6a555a99b0280f3cd63b1281b32175f5525ca6b2": {"t": 1785754884.4468513, "n": 13319, "h": "b918c4bf47a8ca01", "c": "package handler\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strings\"\n\t\"zbxtable/internal/model\"\n\t\"zbxtable/pkg/response\"\n\n\t\"github.com/gin-gonic/gin\"\n)\n\nvar alarmPromptVarPattern = regexp.MustCompile(`\\{\\{\\s*([a-zA-Z0-9_]+)\\s*\\}\\}`)\nvar aiConfigValueByKey = model.GetConfigValueByKey\nvar aiHTTPClient = &http.Client{}\n\nfunc extractAlarmContext(rawMessage string) (string, string) {\n\tparts := strings.SplitN(rawMessage, \"\\n\\n\", 2)\n\tif len(parts) == 2 {\n\t\tctx := strings.TrimSpace(parts[0])\n\t\tif strings.Contains(ctx, \"告警上下文\") || strings.Contains(ctx, \"Alarm Context\") {\n\t\t\treturn ctx, strings.TrimSpace(parts[1])\n\t\t}\n\t}\n\treturn \"\", strings.TrimSpace(rawMessage)\n}\n\ntype AlarmContextPayload struct {\n\tHostname string `json:\"hostname\"`\n\tHostIP string `json:\"host_ip\"`\n\tMessage string `json:\"message\"`\n\tDetail string `json:\"detail\"`\n\tLevel string `json:\"level\"`\n\tStatus string `json:\"status\"`\n}\n\nfunc parseContextFields(alarmContext string) map[string]string {\n\tfields := map[string]string{}\n\tif alarmContext == \"\" {\n\t\treturn fields\n\t}\n\tfor _, line := range strings.Split(alarmContext, \"\\n\") {\n\t\tline = strings.TrimSpace(line)\n\t\tif line == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tkv := strings.SplitN(line, \":\", 2)\n\t\tif len(kv) != 2 {\n\t\t\tcontinue\n\t\t}\n\t\tk := strings.TrimSpace(kv[0])\n\t\tv := strings.TrimSpace(kv[1])\n\t\tswitch k {\n\t\tcase \"设备名称\", \"Device Name\":\n\t\t\tfields[\"hostname\"] = v\n\t\tcase \"IP\":\n\t\t\tfields[\"host_ip\"] = v\n\t\tcase \"告警描述\", \"Description\":\n\t\t\tfields[\"message\"] = v\n\t\tcase \"告警详情\", \"Detail\":\n\t\t\tfields[\"detail\"] = v\n\t\t}\n\t}\n\treturn fields\n}\n\nfunc renderAlarmPromptTemplate(template string, rawMessage string, alarmCtx *AlarmContextPayload) string {\n\tt := strings.TrimSpace(template)\n\tif t == \"\" {\n\t\treturn rawMessage\n\t}\n\n\talarmContext, userMessage := extractAlarmContext(rawMessage)\n\tctxFields := parseContextFields(alarmContext)\n\tif alarmCtx != nil {\n\t\tif alarmCtx.Hostname != \"\" {\n\t\t\tctxFields[\"hostname\"] = alarmCtx.Hostname\n\t\t}\n\t\tif alarmCtx.HostIP != \"\" {\n\t\t\tctxFields[\"host_ip\"] = alarmCtx.HostIP\n\t\t}\n\t\tif alarmCtx.Message != \"\" {\n\t\t\tctxFields[\"message\"] = alarmCtx.Message\n\t\t}\n\t\tif alarmCtx.Detail != \"\" {\n\t\t\tctxFields[\"detail\"] = alarmCtx.Detail\n\t\t}\n\t\tif alarmCtx.Level != \"\" {\n\t\t\tctxFields[\"level\"] = alarmCtx.Level\n\t\t}\n\t\tif alarmCtx.Status != \"\" {\n\t\t\tctxFields[\"status\"] = alarmCtx.Status\n\t\t}\n\t}\n\tif alarmContext == \"\" && len(ctxFields) > 0 {\n\t\talarmContext = strings.TrimSpace(\n\t\t\tfmt.Sprintf(\"告警上下文:\\n设备名称: %s\\nIP: %s\\n告警描述: %s\\n告警详情: %s\",\n\t\t\t\tctxFields[\"hostname\"], ctxFields[\"host_ip\"], ctxFields[\"message\"], ctxFields[\"detail\"]),\n\t\t)\n\t}\n\n\tmapping := map[string]string{\n\t\t\"hostname\": ctxFields[\"hostname\"],\n\t\t\"host_ip\": ctxFields[\"host_ip\"],\n\t\t\"message\": ctxFields[\"message\"],\n\t\t\"detail\": ctxFields[\"detail\"],\n\t\t\"level\": ctxFields[\"level\"],\n\t\t\"status\": ctxFields[\"status\"],\n\t\t\"alarm_context\": alarmContext,\n\t}\n\n\trendered := alarmPromptVarPattern.ReplaceAllStringFunc(t, func(token string) string {\n\t\tmatched := alarmPromptVarPattern.FindStringSubmatch(token)\n\t\tif len(matched) < 2 {\n\t\t\treturn token\n\t\t}\n\t\tkey := matched[1]\n\t\tif val, ok := mapping[key]; ok {\n\t\t\treturn val\n\t\t}\n\t\treturn token\n\t})\n\n\tif userMessage != \"\" {\n\t\trendered = strings.TrimSpace(rendered + \"\\n\\n\" + userMessage)\n\t}\n\treturn rendered\n}\n\n// AIChat 流式聊天接口\nfunc AIChat(c *gin.Context) {\n\t// 设置响应头\n\tc.Header(\"Content-Type\", \"text/event-stream\")\n\tc.Header(\"Cache-Control\", \"no-cache\")\n\tc.Header(\"Connection\", \"keep-alive\")\n\n\t// 解析请求体\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tvar userReq struct {\n\t\tMessage string `json:\"message\"`\n\t\tAlarmContext *AlarmContextPayload `json:\"alarm_context\"`\n\t}\n\tif err := json.Unmarshal(body, &userReq); err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = err.Error()\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\t// 告警分析提示词(后端兜底渲染,避免仅依赖前端)\n\talarmPromptTpl := aiConfigValueByKey(\"alarm_analysis_prompt\", \"\")\n\tfinalMessage := renderAlarmPromptTemplate(alarmPromptTpl, userReq.Message, userReq.AlarmContext)\n\n\t// 获取 AI 类型配置\n\taiType := aiConfigValueByKey(\"ai_type\", \"ollama\")\n\tif aiType == \"\" {\n\t\taiType = \"ollama\"\n\t}\n\n\t// 根据 AI 类型调用不同的服务\n\tswitch aiType {\n\tcase \"deepseek\":\n\t\thandleDeepseekChat(c, finalMessage)\n\tcase \"custom\":\n\t\thandleCustomChat(c, finalMessage)\n\tcase \"ollama\":\n\t\thandleOllamaChat(c, finalMessage)\n\tdefault:\n\t\t// 默认使用 Ollama\n\t\thandleOllamaChat(c, finalMessage)\n\t}\n}\n\n// handleOllamaChat 处理 Ollama AI 请求\nfunc handleOllamaChat(c *gin.Context, message string) {\n\t// 构造系统提示词\n\tsystemPrompt := struct {\n\t\tRole string `json:\"role\"`\n\t\tContent string `json:\"content\"`\n\t}{\n\t\tRole: \"system\",\n\t\tContent: \"你是一个专业的运维分析师,请分析用户提供的问题并给出专业的建议。\",\n\t}\n\n\t// 从配置获取 Ollama 地址\n\tollamaHost := aiConfigValueByKey(\"ollama_host\", model.GetConfKey(\"ollama_host\"))\n\tif ollamaHost == \"\" {\n\t\tollamaHost = \"http://localhost:11434\"\n\t}\n\tollamaModel := aiConfigValueByKey(\"ollama_model\", model.GetConfKey(\"ollama_model\"))\n\tif ollamaModel == \"\" {\n\t\tollamaModel = \"deepseek-r1:32b\"\n\t}\n\n\t// 构造 Ollama 请求\n\tollamaReq := struct {\n\t\tModel string `json:\"model\"`\n\t\tMessages []any `json:\"messages\"`\n\t\tStream bool `json:\"stream\"`\n\t}{\n\t\tModel: ollamaModel,\n\t\tStream: true,\n\t\tMessages: []any{\n\t\t\tsystemPrompt,\n\t\t\tstruct {\n\t\t\t\tRole string `json:\"role\"`\n\t\t\t\tContent string `json:\"content\"`\n\t\t\t}{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: message,\n\t\t\t},\n\t\t},\n\t}\n\n\tjsonData, err := json.Marshal(ollamaReq)\n\tif err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = err.Error()\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\tollamaURL := fmt.Sprintf(\"%s/api/chat\", ollamaHost)\n\n\t// 发送请求到 Ollama\n\tresp, err := http.Post(ollamaURL, \"application/json\", bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = fmt.Sprintf(\"Ollama 服务连接失败: %v\", err)\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\tdefer resp.Body.Close()\n\n\t// 创建 reader\n\treader := bufio.NewReader(resp.Body)\n\n\t// 流式读取响应\n\tfor {\n\t\tline, err := reader.ReadBytes('\\n')\n\t\tif err != nil {\n\t\t\tif err == io.EOF {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\treturn\n\t\t}\n\n\t\t// 解析响应\n\t\tvar response struct {\n\t\t\tModel string `json:\"model\"`\n\t\t\tCreatedAt string `json:\"created_at\"`\n\t\t\tMessage struct {\n\t\t\t\tRole string `json:\"role\"`\n\t\t\t\tContent string `json:\"content\"`\n\t\t\t} `json:\"message\"`\n\t\t\tDone bool `json:\"done\"`\n\t\t}\n\t\tif err := json.Unmarshal(line, &response); err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\t// 发送数据\n\t\tif response.Message.Content != \"\" {\n\t\t\tc.Writer.WriteString(response.Message.Content)\n\t\t\tc.Writer.Flush()\n\t\t}\n\n\t\t// 如果响应完成,退出循环\n\t\tif response.Done {\n\t\t\tbreak\n\t\t}\n\t}\n}\n\n// handleDeepseekChat 处理 Deepseek AI 请求\nfunc handleDeepseekChat(c *gin.Context, message string) {\n\t// 从配置获取 Deepseek 参数(会自动解密)\n\tapiKey := aiConfigValueByKey(\"deepseek_api_key\", \"\")\n\tif apiKey == \"\" {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = \"Deepseek API Key 未配置\"\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\tdeepseekModel := aiConfigValueByKey(\"deepseek_model\", \"deepseek-chat\")\n\tif deepseekModel == \"\" {\n\t\tdeepseekModel = \"deepseek-chat\"\n\t}\n\n\tbaseURL := aiConfigValueByKey(\"deepseek_base_url\", \"https://api.deepseek.com\")\n\tif baseURL == \"\" {\n\t\tbaseURL = \"https://api.deepseek.com\"\n\t}\n\n\t// 构造 Deepseek 请求\n\tdeepseekReq := struct {\n\t\tModel string `json:\"model\"`\n\t\tMessages []struct {\n\t\t\tRole string `json:\"role\"`\n\t\t\tContent string `json:\"content\"`\n\t\t} `json:\"messages\"`\n\t\tStream bool `json:\"stream\"`\n\t}{\n\t\tModel: deepseekModel,\n\t\tStream: true,\n\t\tMessages: []struct {\n\t\t\tRole string `json:\"role\"`\n\t\t\tContent string `json:\"content\"`\n\t\t}{\n\t\t\t{\n\t\t\t\tRole: \"system\",\n\t\t\t\tContent: \"你是一个专业的运维分析师,请分析用户提供的问题并给出专业的建议。\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: message,\n\t\t\t},\n\t\t},\n\t}\n\n\tjsonData, err := json.Marshal(deepseekReq)\n\tif err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = err.Error()\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\t// 构造 Deepseek API URL\n\tdeepseekURL := fmt.Sprintf(\"%s/chat/completions\", strings.TrimSuffix(baseURL, \"/\"))\n\n\t// 创建 HTTP 请求\n\treq, err := http.NewRequest(\"POST\", deepseekURL, bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = err.Error()\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\t// 设置请求头\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"Bearer %s\", apiKey))\n\n\t// 发送请求到 Deepseek\n\tresp, err := aiHTTPClient.Do(req)\n\tif err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = fmt.Sprintf(\"Deepseek 服务连接失败: %v\", err)\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\tdefer resp.Body.Close()\n\n\t// 检查响应状态\n\tif resp.StatusCode != http.StatusOK {\n\t\tbodyBytes, _ := io.ReadAll(resp.Body)\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = resp.StatusCode\n\t\tAIRes.Message = fmt.Sprintf(\"Deepseek API 错误: %s\", string(bodyBytes))\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\tstreamOpenAICompatibleResponse(c, resp.Body)\n}\n\nfunc openAICompatibleMessages(message string) []struct {\n\tRole string `json:\"role\"`\n\tContent string `json:\"content\"`\n} {\n\treturn []struct {\n\t\tRole string `json:\"role\"`\n\t\tContent string `json:\"content\"`\n\t}{\n\t\t{\n\t\t\tRole: \"system\",\n\t\t\tContent: \"你是一个专业的运维分析师,请分析用户提供的问题并给出专业的建议。\",\n\t\t},\n\t\t{\n\t\t\tRole: \"user\",\n\t\t\tContent: message,\n\t\t},\n\t}\n}\n\nfunc streamOpenAICompatibleResponse(c *gin.Context, body io.Reader) {\n\treader := bufio.NewReader(body)\n\n\tfor {\n\t\tline, err := reader.ReadBytes('\\n')\n\t\tif err != nil {\n\t\t\tif err == io.EOF {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\treturn\n\t\t}\n\n\t\tlineStr := strings.TrimSpace(string(line))\n\t\tif lineStr == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tlineStr = strings.TrimPrefix(lineStr, \"data: \")\n\t\tif lineStr == \"[DONE]\" {\n\t\t\tbreak\n\t\t}\n\n\t\tvar response struct {\n\t\t\tID string `json:\"id\"`\n\t\t\tObject string `json:\"object\"`\n\t\t\tCreated int64 `json:\"created\"`\n\t\t\tModel string `json:\"model\"`\n\t\t\tChoices []struct {\n\t\t\t\tIndex int `json:\"index\"`\n\t\t\t\tDelta struct {\n\t\t\t\t\tRole string `json:\"role\"`\n\t\t\t\t\tContent string `json:\"content\"`\n\t\t\t\t} `json:\"delta\"`\n\t\t\t\tFinishReason string `json:\"finish_reason\"`\n\t\t\t} `json:\"choices\"`\n\t\t}\n\n\t\tif err := json.Unmarshal([]byte(lineStr), &response); err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tif len(response.Choices) > 0 && response.Choices[0].Delta.Content != \"\" {\n\t\t\tc.Writer.WriteString(response.Choices[0].Delta.Content)\n\t\t\tc.Writer.Flush()\n\t\t}\n\n\t\tif len(response.Choices) > 0 && response.Choices[0].FinishReason != \"\" {\n\t\t\tbreak\n\t\t}\n\t}\n}\n\nfunc handleCustomChat(c *gin.Context, message string) {\n\tapiKey := aiConfigValueByKey(\"custom_api_key\", \"\")\n\tif apiKey == \"\" {\n\t\twriteAIConfigError(c, \"Custom API Key 未配置\")\n\t\treturn\n\t}\n\n\tcustomModel := aiConfigValueByKey(\"custom_model\", \"\")\n\tif customModel == \"\" {\n\t\twriteAIConfigError(c, \"Custom Model 未配置\")\n\t\treturn\n\t}\n\n\tbaseURL := aiConfigValueByKey(\"custom_base_url\", \"\")\n\tif baseURL == \"\" {\n\t\twriteAIConfigError(c, \"Custom Base URL 未配置\")\n\t\treturn\n\t}\n\n\tcustomReq := struct {\n\t\tModel string `json:\"model\"`\n\t\tMessages []struct {\n\t\t\tRole string `json:\"role\"`\n\t\t\tContent string `json:\"content\"`\n\t\t} `json:\"messages\"`\n\t\tStream bool `json:\"stream\"`\n\t}{\n\t\tModel: customModel,\n\t\tMessages: openAICompatibleMessages(message),\n\t\tStream: true,\n\t}\n\n\tjsonData, err := json.Marshal(customReq)\n\tif err != nil {\n\t\twriteAIConfigError(c, err.Error())\n\t\treturn\n\t}\n\n\tcustomURL := fmt.Sprintf(\"%s/chat/completions\", strings.TrimSuffix(baseURL, \"/\"))\n\treq, err := http.NewRequest(\"POST\", customURL, bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\twriteAIConfigError(c, err.Error())\n\t\treturn\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"Bearer %s\", apiKey))\n\n\tresp, err := aiHTTPClient.Do(req)\n\tif err != nil {\n\t\twriteAIConfigError(c, fmt.Sprintf(\"Custom AI 服务连接失败: %v\", err))\n\t\treturn\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\tbodyBytes, _ := io.ReadAll(resp.Body)\n\t\twriteAIStatusError(c, resp.StatusCode, fmt.Sprintf(\"Custom AI API 错误: %s\", string(bodyBytes)))\n\t\treturn\n\t}\n\n\tstreamOpenAICompatibleResponse(c, resp.Body)\n}\n\nfunc writeAIConfigError(c *gin.Context, message string) {\n\tvar AIRes struct {\n\t\tCode int `json:\"code\"`\n\t\tMessage string `json:\"message\"`\n\t\tData string `json:\"data\"`\n\t}\n\tAIRes.Code = 500\n\tAIRes.Message = message\n\tc.JSON(http.StatusOK, AIRes)\n}\n\nfunc writeAIStatusError(c *gin.Context, statusCode int, message string) {\n\tvar AIRes struct {\n\t\tCode int `json:\"code\"`\n\t\tMessage string `json:\"message\"`\n\t\tData string `json:\"data\"`\n\t}\n\tAIRes.Code = statusCode\n\tAIRes.Message = message\n\tc.JSON(http.StatusOK, AIRes)\n}\n"}, "c9354e4b48722f18efbcc267f153c217a2b67af2": {"t": 1785754885.909354, "n": 10786, "h": "f9761fddc72c5edc", "c": "package handler\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strings\"\n\t\"zbxtable/internal/model\"\n\t\"zbxtable/pkg/response\"\n\n\t\"github.com/gin-gonic/gin\"\n)\n\nvar alarmPromptVarPattern = regexp.MustCompile(`\\{\\{\\s*([a-zA-Z0-9_]+)\\s*\\}\\}`)\n\nfunc extractAlarmContext(rawMessage string) (string, string) {\n\tparts := strings.SplitN(rawMessage, \"\\n\\n\", 2)\n\tif len(parts) == 2 {\n\t\tctx := strings.TrimSpace(parts[0])\n\t\tif strings.Contains(ctx, \"告警上下文\") || strings.Contains(ctx, \"Alarm Context\") {\n\t\t\treturn ctx, strings.TrimSpace(parts[1])\n\t\t}\n\t}\n\treturn \"\", strings.TrimSpace(rawMessage)\n}\n\ntype AlarmContextPayload struct {\n\tHostname string `json:\"hostname\"`\n\tHostIP string `json:\"host_ip\"`\n\tMessage string `json:\"message\"`\n\tDetail string `json:\"detail\"`\n\tLevel string `json:\"level\"`\n\tStatus string `json:\"status\"`\n}\n\nfunc parseContextFields(alarmContext string) map[string]string {\n\tfields := map[string]string{}\n\tif alarmContext == \"\" {\n\t\treturn fields\n\t}\n\tfor _, line := range strings.Split(alarmContext, \"\\n\") {\n\t\tline = strings.TrimSpace(line)\n\t\tif line == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tkv := strings.SplitN(line, \":\", 2)\n\t\tif len(kv) != 2 {\n\t\t\tcontinue\n\t\t}\n\t\tk := strings.TrimSpace(kv[0])\n\t\tv := strings.TrimSpace(kv[1])\n\t\tswitch k {\n\t\tcase \"设备名称\", \"Device Name\":\n\t\t\tfields[\"hostname\"] = v\n\t\tcase \"IP\":\n\t\t\tfields[\"host_ip\"] = v\n\t\tcase \"告警描述\", \"Description\":\n\t\t\tfields[\"message\"] = v\n\t\tcase \"告警详情\", \"Detail\":\n\t\t\tfields[\"detail\"] = v\n\t\t}\n\t}\n\treturn fields\n}\n\nfunc renderAlarmPromptTemplate(template string, rawMessage string, alarmCtx *AlarmContextPayload) string {\n\tt := strings.TrimSpace(template)\n\tif t == \"\" {\n\t\treturn rawMessage\n\t}\n\n\talarmContext, userMessage := extractAlarmContext(rawMessage)\n\tctxFields := parseContextFields(alarmContext)\n\tif alarmCtx != nil {\n\t\tif alarmCtx.Hostname != \"\" {\n\t\t\tctxFields[\"hostname\"] = alarmCtx.Hostname\n\t\t}\n\t\tif alarmCtx.HostIP != \"\" {\n\t\t\tctxFields[\"host_ip\"] = alarmCtx.HostIP\n\t\t}\n\t\tif alarmCtx.Message != \"\" {\n\t\t\tctxFields[\"message\"] = alarmCtx.Message\n\t\t}\n\t\tif alarmCtx.Detail != \"\" {\n\t\t\tctxFields[\"detail\"] = alarmCtx.Detail\n\t\t}\n\t\tif alarmCtx.Level != \"\" {\n\t\t\tctxFields[\"level\"] = alarmCtx.Level\n\t\t}\n\t\tif alarmCtx.Status != \"\" {\n\t\t\tctxFields[\"status\"] = alarmCtx.Status\n\t\t}\n\t}\n\tif alarmContext == \"\" && len(ctxFields) > 0 {\n\t\talarmContext = strings.TrimSpace(\n\t\t\tfmt.Sprintf(\"告警上下文:\\n设备名称: %s\\nIP: %s\\n告警描述: %s\\n告警详情: %s\",\n\t\t\t\tctxFields[\"hostname\"], ctxFields[\"host_ip\"], ctxFields[\"message\"], ctxFields[\"detail\"]),\n\t\t)\n\t}\n\n\tmapping := map[string]string{\n\t\t\"hostname\": ctxFields[\"hostname\"],\n\t\t\"host_ip\": ctxFields[\"host_ip\"],\n\t\t\"message\": ctxFields[\"message\"],\n\t\t\"detail\": ctxFields[\"detail\"],\n\t\t\"level\": ctxFields[\"level\"],\n\t\t\"status\": ctxFields[\"status\"],\n\t\t\"alarm_context\": alarmContext,\n\t}\n\n\trendered := alarmPromptVarPattern.ReplaceAllStringFunc(t, func(token string) string {\n\t\tmatched := alarmPromptVarPattern.FindStringSubmatch(token)\n\t\tif len(matched) < 2 {\n\t\t\treturn token\n\t\t}\n\t\tkey := matched[1]\n\t\tif val, ok := mapping[key]; ok {\n\t\t\treturn val\n\t\t}\n\t\treturn token\n\t})\n\n\tif userMessage != \"\" {\n\t\trendered = strings.TrimSpace(rendered + \"\\n\\n\" + userMessage)\n\t}\n\treturn rendered\n}\n\n// AIChat 流式聊天接口\nfunc AIChat(c *gin.Context) {\n\t// 设置响应头\n\tc.Header(\"Content-Type\", \"text/event-stream\")\n\tc.Header(\"Cache-Control\", \"no-cache\")\n\tc.Header(\"Connection\", \"keep-alive\")\n\n\t// 解析请求体\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tvar userReq struct {\n\t\tMessage string `json:\"message\"`\n\t\tAlarmContext *AlarmContextPayload `json:\"alarm_context\"`\n\t}\n\tif err := json.Unmarshal(body, &userReq); err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = err.Error()\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\t// 告警分析提示词(后端兜底渲染,避免仅依赖前端)\n\talarmPromptTpl := model.GetConfigValueByKey(\"alarm_analysis_prompt\", \"\")\n\tfinalMessage := renderAlarmPromptTemplate(alarmPromptTpl, userReq.Message, userReq.AlarmContext)\n\n\t// 获取 AI 类型配置\n\taiType := model.GetConfigValueByKey(\"ai_type\", \"ollama\")\n\tif aiType == \"\" {\n\t\taiType = \"ollama\"\n\t}\n\n\t// 根据 AI 类型调用不同的服务\n\tswitch aiType {\n\tcase \"deepseek\":\n\t\thandleDeepseekChat(c, finalMessage)\n\tcase \"ollama\":\n\t\thandleOllamaChat(c, finalMessage)\n\tdefault:\n\t\t// 默认使用 Ollama\n\t\thandleOllamaChat(c, finalMessage)\n\t}\n}\n\n// handleOllamaChat 处理 Ollama AI 请求\nfunc handleOllamaChat(c *gin.Context, message string) {\n\t// 构造系统提示词\n\tsystemPrompt := struct {\n\t\tRole string `json:\"role\"`\n\t\tContent string `json:\"content\"`\n\t}{\n\t\tRole: \"system\",\n\t\tContent: \"你是一个专业的运维分析师,请分析用户提供的问题并给出专业的建议。\",\n\t}\n\n\t// 从配置获取 Ollama 地址\n\tollamaHost := model.GetConfigValueByKey(\"ollama_host\", model.GetConfKey(\"ollama_host\"))\n\tif ollamaHost == \"\" {\n\t\tollamaHost = \"http://localhost:11434\"\n\t}\n\tollamaModel := model.GetConfigValueByKey(\"ollama_model\", model.GetConfKey(\"ollama_model\"))\n\tif ollamaModel == \"\" {\n\t\tollamaModel = \"deepseek-r1:32b\"\n\t}\n\n\t// 构造 Ollama 请求\n\tollamaReq := struct {\n\t\tModel string `json:\"model\"`\n\t\tMessages []any `json:\"messages\"`\n\t\tStream bool `json:\"stream\"`\n\t}{\n\t\tModel: ollamaModel,\n\t\tStream: true,\n\t\tMessages: []any{\n\t\t\tsystemPrompt,\n\t\t\tstruct {\n\t\t\t\tRole string `json:\"role\"`\n\t\t\t\tContent string `json:\"content\"`\n\t\t\t}{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: message,\n\t\t\t},\n\t\t},\n\t}\n\n\tjsonData, err := json.Marshal(ollamaReq)\n\tif err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = err.Error()\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\tollamaURL := fmt.Sprintf(\"%s/api/chat\", ollamaHost)\n\n\t// 发送请求到 Ollama\n\tresp, err := http.Post(ollamaURL, \"application/json\", bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = fmt.Sprintf(\"Ollama 服务连接失败: %v\", err)\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\tdefer resp.Body.Close()\n\n\t// 创建 reader\n\treader := bufio.NewReader(resp.Body)\n\n\t// 流式读取响应\n\tfor {\n\t\tline, err := reader.ReadBytes('\\n')\n\t\tif err != nil {\n\t\t\tif err == io.EOF {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\treturn\n\t\t}\n\n\t\t// 解析响应\n\t\tvar response struct {\n\t\t\tModel string `json:\"model\"`\n\t\t\tCreatedAt string `json:\"created_at\"`\n\t\t\tMessage struct {\n\t\t\t\tRole string `json:\"role\"`\n\t\t\t\tContent string `json:\"content\"`\n\t\t\t} `json:\"message\"`\n\t\t\tDone bool `json:\"done\"`\n\t\t}\n\t\tif err := json.Unmarshal(line, &response); err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\t// 发送数据\n\t\tif response.Message.Content != \"\" {\n\t\t\tc.Writer.WriteString(response.Message.Content)\n\t\t\tc.Writer.Flush()\n\t\t}\n\n\t\t// 如果响应完成,退出循环\n\t\tif response.Done {\n\t\t\tbreak\n\t\t}\n\t}\n}\n\n// handleDeepseekChat 处理 Deepseek AI 请求\nfunc handleDeepseekChat(c *gin.Context, message string) {\n\t// 从配置获取 Deepseek 参数(会自动解密)\n\tapiKey := model.GetConfigValueByKey(\"deepseek_api_key\", \"\")\n\tif apiKey == \"\" {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = \"Deepseek API Key 未配置\"\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\tdeepseekModel := model.GetConfigValueByKey(\"deepseek_model\", \"deepseek-chat\")\n\tif deepseekModel == \"\" {\n\t\tdeepseekModel = \"deepseek-chat\"\n\t}\n\n\tbaseURL := model.GetConfigValueByKey(\"deepseek_base_url\", \"https://api.deepseek.com\")\n\tif baseURL == \"\" {\n\t\tbaseURL = \"https://api.deepseek.com\"\n\t}\n\n\t// 构造 Deepseek 请求\n\tdeepseekReq := struct {\n\t\tModel string `json:\"model\"`\n\t\tMessages []struct {\n\t\t\tRole string `json:\"role\"`\n\t\t\tContent string `json:\"content\"`\n\t\t} `json:\"messages\"`\n\t\tStream bool `json:\"stream\"`\n\t}{\n\t\tModel: deepseekModel,\n\t\tStream: true,\n\t\tMessages: []struct {\n\t\t\tRole string `json:\"role\"`\n\t\t\tContent string `json:\"content\"`\n\t\t}{\n\t\t\t{\n\t\t\t\tRole: \"system\",\n\t\t\t\tContent: \"你是一个专业的运维分析师,请分析用户提供的问题并给出专业的建议。\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: message,\n\t\t\t},\n\t\t},\n\t}\n\n\tjsonData, err := json.Marshal(deepseekReq)\n\tif err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = err.Error()\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\t// 构造 Deepseek API URL\n\tdeepseekURL := fmt.Sprintf(\"%s/chat/completions\", strings.TrimSuffix(baseURL, \"/\"))\n\n\t// 创建 HTTP 请求\n\treq, err := http.NewRequest(\"POST\", deepseekURL, bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = err.Error()\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\t// 设置请求头\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"Bearer %s\", apiKey))\n\n\t// 发送请求到 Deepseek\n\tclient := &http.Client{}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = fmt.Sprintf(\"Deepseek 服务连接失败: %v\", err)\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\tdefer resp.Body.Close()\n\n\t// 检查响应状态\n\tif resp.StatusCode != http.StatusOK {\n\t\tbodyBytes, _ := io.ReadAll(resp.Body)\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = resp.StatusCode\n\t\tAIRes.Message = fmt.Sprintf(\"Deepseek API 错误: %s\", string(bodyBytes))\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\t// 创建 reader\n\treader := bufio.NewReader(resp.Body)\n\n\t// 流式读取响应\n\tfor {\n\t\tline, err := reader.ReadBytes('\\n')\n\t\tif err != nil {\n\t\t\tif err == io.EOF {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\treturn\n\t\t}\n\n\t\t// 跳过空行\n\t\tlineStr := strings.TrimSpace(string(line))\n\t\tif lineStr == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Deepseek 使用 SSE 格式,需要去掉 \"data: \" 前缀\n\t\tlineStr = strings.TrimPrefix(lineStr, \"data: \")\n\n\t\t// 检查是否是结束标记\n\t\tif lineStr == \"[DONE]\" {\n\t\t\tbreak\n\t\t}\n\n\t\t// 解析响应\n\t\tvar response struct {\n\t\t\tID string `json:\"id\"`\n\t\t\tObject string `json:\"object\"`\n\t\t\tCreated int64 `json:\"created\"`\n\t\t\tModel string `json:\"model\"`\n\t\t\tChoices []struct {\n\t\t\t\tIndex int `json:\"index\"`\n\t\t\t\tDelta struct {\n\t\t\t\t\tRole string `json:\"role\"`\n\t\t\t\t\tContent string `json:\"content\"`\n\t\t\t\t} `json:\"delta\"`\n\t\t\t\tFinishReason string `json:\"finish_reason\"`\n\t\t\t} `json:\"choices\"`\n\t\t}\n\n\t\tif err := json.Unmarshal([]byte(lineStr), &response); err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\t// 发送数据\n\t\tif len(response.Choices) > 0 && response.Choices[0].Delta.Content != \"\" {\n\t\t\tc.Writer.WriteString(response.Choices[0].Delta.Content)\n\t\t\tc.Writer.Flush()\n\t\t}\n\n\t\t// 检查是否完成\n\t\tif len(response.Choices) > 0 && response.Choices[0].FinishReason != \"\" {\n\t\t\tbreak\n\t\t}\n\t}\n}\n"}, "4988a0d4c8988e6d4788dcc710968b8f3910bd8b": {"t": 1785754888.329914, "n": 10846, "h": "37a2ce0a48aa62ab", "c": "package handler\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strings\"\n\t\"zbxtable/internal/model\"\n\t\"zbxtable/pkg/response\"\n\n\t\"github.com/gin-gonic/gin\"\n\tjsoniter \"github.com/json-iterator/go\"\n)\n\nvar alarmPromptVarPattern = regexp.MustCompile(`\\{\\{\\s*([a-zA-Z0-9_]+)\\s*\\}\\}`)\n\nfunc extractAlarmContext(rawMessage string) (string, string) {\n\tparts := strings.SplitN(rawMessage, \"\\n\\n\", 2)\n\tif len(parts) == 2 {\n\t\tctx := strings.TrimSpace(parts[0])\n\t\tif strings.Contains(ctx, \"告警上下文\") || strings.Contains(ctx, \"Alarm Context\") {\n\t\t\treturn ctx, strings.TrimSpace(parts[1])\n\t\t}\n\t}\n\treturn \"\", strings.TrimSpace(rawMessage)\n}\n\ntype AlarmContextPayload struct {\n\tHostname string `json:\"hostname\"`\n\tHostIP string `json:\"host_ip\"`\n\tMessage string `json:\"message\"`\n\tDetail string `json:\"detail\"`\n\tLevel string `json:\"level\"`\n\tStatus string `json:\"status\"`\n}\n\nfunc parseContextFields(alarmContext string) map[string]string {\n\tfields := map[string]string{}\n\tif alarmContext == \"\" {\n\t\treturn fields\n\t}\n\tfor _, line := range strings.Split(alarmContext, \"\\n\") {\n\t\tline = strings.TrimSpace(line)\n\t\tif line == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tkv := strings.SplitN(line, \":\", 2)\n\t\tif len(kv) != 2 {\n\t\t\tcontinue\n\t\t}\n\t\tk := strings.TrimSpace(kv[0])\n\t\tv := strings.TrimSpace(kv[1])\n\t\tswitch k {\n\t\tcase \"设备名称\", \"Device Name\":\n\t\t\tfields[\"hostname\"] = v\n\t\tcase \"IP\":\n\t\t\tfields[\"host_ip\"] = v\n\t\tcase \"告警描述\", \"Description\":\n\t\t\tfields[\"message\"] = v\n\t\tcase \"告警详情\", \"Detail\":\n\t\t\tfields[\"detail\"] = v\n\t\t}\n\t}\n\treturn fields\n}\n\nfunc renderAlarmPromptTemplate(template string, rawMessage string, alarmCtx *AlarmContextPayload) string {\n\tt := strings.TrimSpace(template)\n\tif t == \"\" {\n\t\treturn rawMessage\n\t}\n\n\talarmContext, userMessage := extractAlarmContext(rawMessage)\n\tctxFields := parseContextFields(alarmContext)\n\tif alarmCtx != nil {\n\t\tif alarmCtx.Hostname != \"\" {\n\t\t\tctxFields[\"hostname\"] = alarmCtx.Hostname\n\t\t}\n\t\tif alarmCtx.HostIP != \"\" {\n\t\t\tctxFields[\"host_ip\"] = alarmCtx.HostIP\n\t\t}\n\t\tif alarmCtx.Message != \"\" {\n\t\t\tctxFields[\"message\"] = alarmCtx.Message\n\t\t}\n\t\tif alarmCtx.Detail != \"\" {\n\t\t\tctxFields[\"detail\"] = alarmCtx.Detail\n\t\t}\n\t\tif alarmCtx.Level != \"\" {\n\t\t\tctxFields[\"level\"] = alarmCtx.Level\n\t\t}\n\t\tif alarmCtx.Status != \"\" {\n\t\t\tctxFields[\"status\"] = alarmCtx.Status\n\t\t}\n\t}\n\tif alarmContext == \"\" && len(ctxFields) > 0 {\n\t\talarmContext = strings.TrimSpace(\n\t\t\tfmt.Sprintf(\"告警上下文:\\n设备名称: %s\\nIP: %s\\n告警描述: %s\\n告警详情: %s\",\n\t\t\t\tctxFields[\"hostname\"], ctxFields[\"host_ip\"], ctxFields[\"message\"], ctxFields[\"detail\"]),\n\t\t)\n\t}\n\n\tmapping := map[string]string{\n\t\t\"hostname\": ctxFields[\"hostname\"],\n\t\t\"host_ip\": ctxFields[\"host_ip\"],\n\t\t\"message\": ctxFields[\"message\"],\n\t\t\"detail\": ctxFields[\"detail\"],\n\t\t\"level\": ctxFields[\"level\"],\n\t\t\"status\": ctxFields[\"status\"],\n\t\t\"alarm_context\": alarmContext,\n\t}\n\n\trendered := alarmPromptVarPattern.ReplaceAllStringFunc(t, func(token string) string {\n\t\tmatched := alarmPromptVarPattern.FindStringSubmatch(token)\n\t\tif len(matched) < 2 {\n\t\t\treturn token\n\t\t}\n\t\tkey := matched[1]\n\t\tif val, ok := mapping[key]; ok {\n\t\t\treturn val\n\t\t}\n\t\treturn token\n\t})\n\n\tif userMessage != \"\" {\n\t\trendered = strings.TrimSpace(rendered + \"\\n\\n\" + userMessage)\n\t}\n\treturn rendered\n}\n\n// AIChat 流式聊天接口\nfunc AIChat(c *gin.Context) {\n\t// 设置响应头\n\tc.Header(\"Content-Type\", \"text/event-stream\")\n\tc.Header(\"Cache-Control\", \"no-cache\")\n\tc.Header(\"Connection\", \"keep-alive\")\n\n\t// 解析请求体\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tvar userReq struct {\n\t\tMessage string `json:\"message\"`\n\t\tAlarmContext *AlarmContextPayload `json:\"alarm_context\"`\n\t}\n\tif err := jsoniter.Unmarshal(body, &userReq); err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = err.Error()\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\t// 告警分析提示词(后端兜底渲染,避免仅依赖前端)\n\talarmPromptTpl := model.GetConfigValueByKey(\"alarm_analysis_prompt\", \"\")\n\tfinalMessage := renderAlarmPromptTemplate(alarmPromptTpl, userReq.Message, userReq.AlarmContext)\n\n\t// 获取 AI 类型配置\n\taiType := model.GetConfigValueByKey(\"ai_type\", \"ollama\")\n\tif aiType == \"\" {\n\t\taiType = \"ollama\"\n\t}\n\n\t// 根据 AI 类型调用不同的服务\n\tswitch aiType {\n\tcase \"deepseek\":\n\t\thandleDeepseekChat(c, finalMessage)\n\tcase \"ollama\":\n\t\thandleOllamaChat(c, finalMessage)\n\tdefault:\n\t\t// 默认使用 Ollama\n\t\thandleOllamaChat(c, finalMessage)\n\t}\n}\n\n// handleOllamaChat 处理 Ollama AI 请求\nfunc handleOllamaChat(c *gin.Context, message string) {\n\t// 构造系统提示词\n\tsystemPrompt := struct {\n\t\tRole string `json:\"role\"`\n\t\tContent string `json:\"content\"`\n\t}{\n\t\tRole: \"system\",\n\t\tContent: \"你是一个专业的运维分析师,请分析用户提供的问题并给出专业的建议。\",\n\t}\n\n\t// 从配置获取 Ollama 地址\n\tollamaHost := model.GetConfigValueByKey(\"ollama_host\", model.GetConfKey(\"ollama_host\"))\n\tif ollamaHost == \"\" {\n\t\tollamaHost = \"http://localhost:11434\"\n\t}\n\tollamaModel := model.GetConfigValueByKey(\"ollama_model\", model.GetConfKey(\"ollama_model\"))\n\tif ollamaModel == \"\" {\n\t\tollamaModel = \"deepseek-r1:32b\"\n\t}\n\n\t// 构造 Ollama 请求\n\tollamaReq := struct {\n\t\tModel string `json:\"model\"`\n\t\tMessages []any `json:\"messages\"`\n\t\tStream bool `json:\"stream\"`\n\t}{\n\t\tModel: ollamaModel,\n\t\tStream: true,\n\t\tMessages: []any{\n\t\t\tsystemPrompt,\n\t\t\tstruct {\n\t\t\t\tRole string `json:\"role\"`\n\t\t\t\tContent string `json:\"content\"`\n\t\t\t}{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: message,\n\t\t\t},\n\t\t},\n\t}\n\n\tjsonData, err := jsoniter.Marshal(ollamaReq)\n\tif err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = err.Error()\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\tollamaURL := fmt.Sprintf(\"%s/api/chat\", ollamaHost)\n\n\t// 发送请求到 Ollama\n\tresp, err := http.Post(ollamaURL, \"application/json\", bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = fmt.Sprintf(\"Ollama 服务连接失败: %v\", err)\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\tdefer resp.Body.Close()\n\n\t// 创建 reader\n\treader := bufio.NewReader(resp.Body)\n\n\t// 流式读取响应\n\tfor {\n\t\tline, err := reader.ReadBytes('\\n')\n\t\tif err != nil {\n\t\t\tif err == io.EOF {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\treturn\n\t\t}\n\n\t\t// 解析响应\n\t\tvar response struct {\n\t\t\tModel string `json:\"model\"`\n\t\t\tCreatedAt string `json:\"created_at\"`\n\t\t\tMessage struct {\n\t\t\t\tRole string `json:\"role\"`\n\t\t\t\tContent string `json:\"content\"`\n\t\t\t} `json:\"message\"`\n\t\t\tDone bool `json:\"done\"`\n\t\t}\n\t\tif err := jsoniter.Unmarshal(line, &response); err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\t// 发送数据\n\t\tif response.Message.Content != \"\" {\n\t\t\tc.Writer.WriteString(response.Message.Content)\n\t\t\tc.Writer.Flush()\n\t\t}\n\n\t\t// 如果响应完成,退出循环\n\t\tif response.Done {\n\t\t\tbreak\n\t\t}\n\t}\n}\n\n// handleDeepseekChat 处理 Deepseek AI 请求\nfunc handleDeepseekChat(c *gin.Context, message string) {\n\t// 从配置获取 Deepseek 参数(会自动解密)\n\tapiKey := model.GetConfigValueByKey(\"deepseek_api_key\", \"\")\n\tif apiKey == \"\" {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = \"Deepseek API Key 未配置\"\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\tdeepseekModel := model.GetConfigValueByKey(\"deepseek_model\", \"deepseek-chat\")\n\tif deepseekModel == \"\" {\n\t\tdeepseekModel = \"deepseek-chat\"\n\t}\n\n\tbaseURL := model.GetConfigValueByKey(\"deepseek_base_url\", \"https://api.deepseek.com\")\n\tif baseURL == \"\" {\n\t\tbaseURL = \"https://api.deepseek.com\"\n\t}\n\n\t// 构造 Deepseek 请求\n\tdeepseekReq := struct {\n\t\tModel string `json:\"model\"`\n\t\tMessages []struct {\n\t\t\tRole string `json:\"role\"`\n\t\t\tContent string `json:\"content\"`\n\t\t} `json:\"messages\"`\n\t\tStream bool `json:\"stream\"`\n\t}{\n\t\tModel: deepseekModel,\n\t\tStream: true,\n\t\tMessages: []struct {\n\t\t\tRole string `json:\"role\"`\n\t\t\tContent string `json:\"content\"`\n\t\t}{\n\t\t\t{\n\t\t\t\tRole: \"system\",\n\t\t\t\tContent: \"你是一个专业的运维分析师,请分析用户提供的问题并给出专业的建议。\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: message,\n\t\t\t},\n\t\t},\n\t}\n\n\tjsonData, err := jsoniter.Marshal(deepseekReq)\n\tif err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = err.Error()\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\t// 构造 Deepseek API URL\n\tdeepseekURL := fmt.Sprintf(\"%s/chat/completions\", strings.TrimSuffix(baseURL, \"/\"))\n\n\t// 创建 HTTP 请求\n\treq, err := http.NewRequest(\"POST\", deepseekURL, bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = err.Error()\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\t// 设置请求头\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"Bearer %s\", apiKey))\n\n\t// 发送请求到 Deepseek\n\tclient := &http.Client{}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = fmt.Sprintf(\"Deepseek 服务连接失败: %v\", err)\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\tdefer resp.Body.Close()\n\n\t// 检查响应状态\n\tif resp.StatusCode != http.StatusOK {\n\t\tbodyBytes, _ := io.ReadAll(resp.Body)\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = resp.StatusCode\n\t\tAIRes.Message = fmt.Sprintf(\"Deepseek API 错误: %s\", string(bodyBytes))\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\t// 创建 reader\n\treader := bufio.NewReader(resp.Body)\n\n\t// 流式读取响应\n\tfor {\n\t\tline, err := reader.ReadBytes('\\n')\n\t\tif err != nil {\n\t\t\tif err == io.EOF {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\treturn\n\t\t}\n\n\t\t// 跳过空行\n\t\tlineStr := strings.TrimSpace(string(line))\n\t\tif lineStr == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Deepseek 使用 SSE 格式,需要去掉 \"data: \" 前缀\n\t\tlineStr = strings.TrimPrefix(lineStr, \"data: \")\n\n\t\t// 检查是否是结束标记\n\t\tif lineStr == \"[DONE]\" {\n\t\t\tbreak\n\t\t}\n\n\t\t// 解析响应\n\t\tvar response struct {\n\t\t\tID string `json:\"id\"`\n\t\t\tObject string `json:\"object\"`\n\t\t\tCreated int64 `json:\"created\"`\n\t\t\tModel string `json:\"model\"`\n\t\t\tChoices []struct {\n\t\t\t\tIndex int `json:\"index\"`\n\t\t\t\tDelta struct {\n\t\t\t\t\tRole string `json:\"role\"`\n\t\t\t\t\tContent string `json:\"content\"`\n\t\t\t\t} `json:\"delta\"`\n\t\t\t\tFinishReason string `json:\"finish_reason\"`\n\t\t\t} `json:\"choices\"`\n\t\t}\n\n\t\tif err := jsoniter.Unmarshal([]byte(lineStr), &response); err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\t// 发送数据\n\t\tif len(response.Choices) > 0 && response.Choices[0].Delta.Content != \"\" {\n\t\t\tc.Writer.WriteString(response.Choices[0].Delta.Content)\n\t\t\tc.Writer.Flush()\n\t\t}\n\n\t\t// 检查是否完成\n\t\tif len(response.Choices) > 0 && response.Choices[0].FinishReason != \"\" {\n\t\t\tbreak\n\t\t}\n\t}\n}\n"}, "794e70b8544aa8c0a881f191a884d6c7f56cbd99": {"t": 1785754889.7609231, "n": 7575, "h": "e6d562e232319c09", "c": "package handler\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\t\"zbxtable/internal/model\"\n\t\"zbxtable/pkg/response\"\n\n\t\"github.com/gin-gonic/gin\"\n\tjsoniter \"github.com/json-iterator/go\"\n)\n\n// AIChat 流式聊天接口\nfunc AIChat(c *gin.Context) {\n\t// 设置响应头\n\tc.Header(\"Content-Type\", \"text/event-stream\")\n\tc.Header(\"Cache-Control\", \"no-cache\")\n\tc.Header(\"Connection\", \"keep-alive\")\n\n\t// 解析请求体\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tvar userReq struct {\n\t\tMessage string `json:\"message\"`\n\t}\n\tif err := jsoniter.Unmarshal(body, &userReq); err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = err.Error()\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\t// 获取 AI 类型配置\n\taiType := model.GetConfigValueByKey(\"ai_type\", \"ollama\")\n\tif aiType == \"\" {\n\t\taiType = \"ollama\"\n\t}\n\n\t// 根据 AI 类型调用不同的服务\n\tswitch aiType {\n\tcase \"deepseek\":\n\t\thandleDeepseekChat(c, userReq.Message)\n\tcase \"ollama\":\n\t\thandleOllamaChat(c, userReq.Message)\n\tdefault:\n\t\t// 默认使用 Ollama\n\t\thandleOllamaChat(c, userReq.Message)\n\t}\n}\n\n// handleOllamaChat 处理 Ollama AI 请求\nfunc handleOllamaChat(c *gin.Context, message string) {\n\t// 构造系统提示词\n\tsystemPrompt := struct {\n\t\tRole string `json:\"role\"`\n\t\tContent string `json:\"content\"`\n\t}{\n\t\tRole: \"system\",\n\t\tContent: \"你是一个专业的运维分析师,请分析用户提供的问题并给出专业的建议。\",\n\t}\n\n\t// 从配置获取 Ollama 地址\n\tollamaHost := model.GetConfigValueByKey(\"ollama_host\", model.GetConfKey(\"ollama_host\"))\n\tif ollamaHost == \"\" {\n\t\tollamaHost = \"http://localhost:11434\"\n\t}\n\tollamaModel := model.GetConfigValueByKey(\"ollama_model\", model.GetConfKey(\"ollama_model\"))\n\tif ollamaModel == \"\" {\n\t\tollamaModel = \"deepseek-r1:32b\"\n\t}\n\n\t// 构造 Ollama 请求\n\tollamaReq := struct {\n\t\tModel string `json:\"model\"`\n\t\tMessages []interface{} `json:\"messages\"`\n\t\tStream bool `json:\"stream\"`\n\t}{\n\t\tModel: ollamaModel,\n\t\tStream: true,\n\t\tMessages: []interface{}{\n\t\t\tsystemPrompt,\n\t\t\tstruct {\n\t\t\t\tRole string `json:\"role\"`\n\t\t\t\tContent string `json:\"content\"`\n\t\t\t}{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: message,\n\t\t\t},\n\t\t},\n\t}\n\n\tjsonData, err := jsoniter.Marshal(ollamaReq)\n\tif err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = err.Error()\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\tollamaURL := fmt.Sprintf(\"%s/api/chat\", ollamaHost)\n\n\t// 发送请求到 Ollama\n\tresp, err := http.Post(ollamaURL, \"application/json\", bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = fmt.Sprintf(\"Ollama 服务连接失败: %v\", err)\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\tdefer resp.Body.Close()\n\n\t// 创建 reader\n\treader := bufio.NewReader(resp.Body)\n\n\t// 流式读取响应\n\tfor {\n\t\tline, err := reader.ReadBytes('\\n')\n\t\tif err != nil {\n\t\t\tif err == io.EOF {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\treturn\n\t\t}\n\n\t\t// 解析响应\n\t\tvar response struct {\n\t\t\tModel string `json:\"model\"`\n\t\t\tCreatedAt string `json:\"created_at\"`\n\t\t\tMessage struct {\n\t\t\t\tRole string `json:\"role\"`\n\t\t\t\tContent string `json:\"content\"`\n\t\t\t} `json:\"message\"`\n\t\t\tDone bool `json:\"done\"`\n\t\t}\n\t\tif err := jsoniter.Unmarshal(line, &response); err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\t// 发送数据\n\t\tif response.Message.Content != \"\" {\n\t\t\tc.Writer.WriteString(response.Message.Content)\n\t\t\tc.Writer.Flush()\n\t\t}\n\n\t\t// 如果响应完成,退出循环\n\t\tif response.Done {\n\t\t\tbreak\n\t\t}\n\t}\n}\n\n// handleDeepseekChat 处理 Deepseek AI 请求\nfunc handleDeepseekChat(c *gin.Context, message string) {\n\t// 从配置获取 Deepseek 参数(会自动解密)\n\tapiKey := model.GetConfigValueByKey(\"deepseek_api_key\", \"\")\n\tif apiKey == \"\" {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = \"Deepseek API Key 未配置\"\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\tdeepseekModel := model.GetConfigValueByKey(\"deepseek_model\", \"deepseek-chat\")\n\tif deepseekModel == \"\" {\n\t\tdeepseekModel = \"deepseek-chat\"\n\t}\n\n\tbaseURL := model.GetConfigValueByKey(\"deepseek_base_url\", \"https://api.deepseek.com\")\n\tif baseURL == \"\" {\n\t\tbaseURL = \"https://api.deepseek.com\"\n\t}\n\n\t// 构造 Deepseek 请求\n\tdeepseekReq := struct {\n\t\tModel string `json:\"model\"`\n\t\tMessages []struct {\n\t\t\tRole string `json:\"role\"`\n\t\t\tContent string `json:\"content\"`\n\t\t} `json:\"messages\"`\n\t\tStream bool `json:\"stream\"`\n\t}{\n\t\tModel: deepseekModel,\n\t\tStream: true,\n\t\tMessages: []struct {\n\t\t\tRole string `json:\"role\"`\n\t\t\tContent string `json:\"content\"`\n\t\t}{\n\t\t\t{\n\t\t\t\tRole: \"system\",\n\t\t\t\tContent: \"你是一个专业的运维分析师,请分析用户提供的问题并给出专业的建议。\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: message,\n\t\t\t},\n\t\t},\n\t}\n\n\tjsonData, err := jsoniter.Marshal(deepseekReq)\n\tif err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = err.Error()\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\t// 构造 Deepseek API URL\n\tdeepseekURL := fmt.Sprintf(\"%s/chat/completions\", strings.TrimSuffix(baseURL, \"/\"))\n\n\t// 创建 HTTP 请求\n\treq, err := http.NewRequest(\"POST\", deepseekURL, bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = err.Error()\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\t// 设置请求头\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"Bearer %s\", apiKey))\n\n\t// 发送请求到 Deepseek\n\tclient := &http.Client{}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = fmt.Sprintf(\"Deepseek 服务连接失败: %v\", err)\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\tdefer resp.Body.Close()\n\n\t// 检查响应状态\n\tif resp.StatusCode != http.StatusOK {\n\t\tbodyBytes, _ := io.ReadAll(resp.Body)\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = resp.StatusCode\n\t\tAIRes.Message = fmt.Sprintf(\"Deepseek API 错误: %s\", string(bodyBytes))\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\t// 创建 reader\n\treader := bufio.NewReader(resp.Body)\n\n\t// 流式读取响应\n\tfor {\n\t\tline, err := reader.ReadBytes('\\n')\n\t\tif err != nil {\n\t\t\tif err == io.EOF {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\treturn\n\t\t}\n\n\t\t// 跳过空行\n\t\tlineStr := strings.TrimSpace(string(line))\n\t\tif lineStr == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Deepseek 使用 SSE 格式,需要去掉 \"data: \" 前缀\n\t\tlineStr = strings.TrimPrefix(lineStr, \"data: \")\n\n\t\t// 检查是否是结束标记\n\t\tif lineStr == \"[DONE]\" {\n\t\t\tbreak\n\t\t}\n\n\t\t// 解析响应\n\t\tvar response struct {\n\t\t\tID string `json:\"id\"`\n\t\t\tObject string `json:\"object\"`\n\t\t\tCreated int64 `json:\"created\"`\n\t\t\tModel string `json:\"model\"`\n\t\t\tChoices []struct {\n\t\t\t\tIndex int `json:\"index\"`\n\t\t\t\tDelta struct {\n\t\t\t\t\tRole string `json:\"role\"`\n\t\t\t\t\tContent string `json:\"content\"`\n\t\t\t\t} `json:\"delta\"`\n\t\t\t\tFinishReason string `json:\"finish_reason\"`\n\t\t\t} `json:\"choices\"`\n\t\t}\n\n\t\tif err := jsoniter.Unmarshal([]byte(lineStr), &response); err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\t// 发送数据\n\t\tif len(response.Choices) > 0 && response.Choices[0].Delta.Content != \"\" {\n\t\t\tc.Writer.WriteString(response.Choices[0].Delta.Content)\n\t\t\tc.Writer.Flush()\n\t\t}\n\n\t\t// 检查是否完成\n\t\tif len(response.Choices) > 0 && response.Choices[0].FinishReason != \"\" {\n\t\t\tbreak\n\t\t}\n\t}\n}\n"}, "e996ccfc882ad5a0cf03b6b98ef5ca1990d7a5be": {"t": 1785754891.18534, "n": 7624, "h": "2c528e41463072f2", "c": "package handler\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\t\"zbxtable/internal/model\"\n\t\"zbxtable/pkg/response\"\n\n\t\"github.com/gin-gonic/gin\"\n\tjsoniter \"github.com/json-iterator/go\"\n)\n\n// AIChat 流式聊天接口\nfunc AIChat(c *gin.Context) {\n\t// 设置响应头\n\tc.Header(\"Content-Type\", \"text/event-stream\")\n\tc.Header(\"Cache-Control\", \"no-cache\")\n\tc.Header(\"Connection\", \"keep-alive\")\n\n\t// 解析请求体\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tvar userReq struct {\n\t\tMessage string `json:\"message\"`\n\t}\n\tif err := jsoniter.Unmarshal(body, &userReq); err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = err.Error()\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\t// 获取 AI 类型配置\n\taiType := model.GetConfigValueByKey(\"ai_type\", \"ollama\")\n\tif aiType == \"\" {\n\t\taiType = \"ollama\"\n\t}\n\n\t// 根据 AI 类型调用不同的服务\n\tswitch aiType {\n\tcase \"deepseek\":\n\t\thandleDeepseekChat(c, userReq.Message)\n\tcase \"ollama\":\n\t\thandleOllamaChat(c, userReq.Message)\n\tdefault:\n\t\t// 默认使用 Ollama\n\t\thandleOllamaChat(c, userReq.Message)\n\t}\n}\n\n// handleOllamaChat 处理 Ollama AI 请求\nfunc handleOllamaChat(c *gin.Context, message string) {\n\t// 构造系统提示词\n\tsystemPrompt := struct {\n\t\tRole string `json:\"role\"`\n\t\tContent string `json:\"content\"`\n\t}{\n\t\tRole: \"system\",\n\t\tContent: \"你是一个专业的运维分析师,请分析用户提供的问题并给出专业的建议。\",\n\t}\n\n\t// 从配置获取 Ollama 地址\n\tollamaHost := model.GetConfigValueByKey(\"ollama_host\", model.GetConfKey(\"ollama_host\"))\n\tif ollamaHost == \"\" {\n\t\tollamaHost = \"http://localhost:11434\"\n\t}\n\tollamaModel := model.GetConfigValueByKey(\"ollama_model\", model.GetConfKey(\"ollama_model\"))\n\tif ollamaModel == \"\" {\n\t\tollamaModel = \"deepseek-r1:32b\"\n\t}\n\n\t// 构造 Ollama 请求\n\tollamaReq := struct {\n\t\tModel string `json:\"model\"`\n\t\tMessages []interface{} `json:\"messages\"`\n\t\tStream bool `json:\"stream\"`\n\t}{\n\t\tModel: ollamaModel,\n\t\tStream: true,\n\t\tMessages: []interface{}{\n\t\t\tsystemPrompt,\n\t\t\tstruct {\n\t\t\t\tRole string `json:\"role\"`\n\t\t\t\tContent string `json:\"content\"`\n\t\t\t}{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: message,\n\t\t\t},\n\t\t},\n\t}\n\n\tjsonData, err := jsoniter.Marshal(ollamaReq)\n\tif err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = err.Error()\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\tollamaURL := fmt.Sprintf(\"%s/api/chat\", ollamaHost)\n\n\t// 发送请求到 Ollama\n\tresp, err := http.Post(ollamaURL, \"application/json\", bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = fmt.Sprintf(\"Ollama 服务连接失败: %v\", err)\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\tdefer resp.Body.Close()\n\n\t// 创建 reader\n\treader := bufio.NewReader(resp.Body)\n\n\t// 流式读取响应\n\tfor {\n\t\tline, err := reader.ReadBytes('\\n')\n\t\tif err != nil {\n\t\t\tif err == io.EOF {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\treturn\n\t\t}\n\n\t\t// 解析响应\n\t\tvar response struct {\n\t\t\tModel string `json:\"model\"`\n\t\t\tCreatedAt string `json:\"created_at\"`\n\t\t\tMessage struct {\n\t\t\t\tRole string `json:\"role\"`\n\t\t\t\tContent string `json:\"content\"`\n\t\t\t} `json:\"message\"`\n\t\t\tDone bool `json:\"done\"`\n\t\t}\n\t\tif err := jsoniter.Unmarshal(line, &response); err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\t// 发送数据\n\t\tif response.Message.Content != \"\" {\n\t\t\tc.Writer.WriteString(response.Message.Content)\n\t\t\tc.Writer.Flush()\n\t\t}\n\n\t\t// 如果响应完成,退出循环\n\t\tif response.Done {\n\t\t\tbreak\n\t\t}\n\t}\n}\n\n// handleDeepseekChat 处理 Deepseek AI 请求\nfunc handleDeepseekChat(c *gin.Context, message string) {\n\t// 从配置获取 Deepseek 参数(会自动解密)\n\tapiKey := model.GetConfigValueByKey(\"deepseek_api_key\", \"\")\n\tif apiKey == \"\" {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = \"Deepseek API Key 未配置\"\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\tdeepseekModel := model.GetConfigValueByKey(\"deepseek_model\", \"deepseek-chat\")\n\tif deepseekModel == \"\" {\n\t\tdeepseekModel = \"deepseek-chat\"\n\t}\n\n\tbaseURL := model.GetConfigValueByKey(\"deepseek_base_url\", \"https://api.deepseek.com\")\n\tif baseURL == \"\" {\n\t\tbaseURL = \"https://api.deepseek.com\"\n\t}\n\n\t// 构造 Deepseek 请求\n\tdeepseekReq := struct {\n\t\tModel string `json:\"model\"`\n\t\tMessages []struct {\n\t\t\tRole string `json:\"role\"`\n\t\t\tContent string `json:\"content\"`\n\t\t} `json:\"messages\"`\n\t\tStream bool `json:\"stream\"`\n\t}{\n\t\tModel: deepseekModel,\n\t\tStream: true,\n\t\tMessages: []struct {\n\t\t\tRole string `json:\"role\"`\n\t\t\tContent string `json:\"content\"`\n\t\t}{\n\t\t\t{\n\t\t\t\tRole: \"system\",\n\t\t\t\tContent: \"你是一个专业的运维分析师,请分析用户提供的问题并给出专业的建议。\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: message,\n\t\t\t},\n\t\t},\n\t}\n\n\tjsonData, err := jsoniter.Marshal(deepseekReq)\n\tif err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = err.Error()\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\t// 构造 Deepseek API URL\n\tdeepseekURL := fmt.Sprintf(\"%s/chat/completions\", strings.TrimSuffix(baseURL, \"/\"))\n\n\t// 创建 HTTP 请求\n\treq, err := http.NewRequest(\"POST\", deepseekURL, bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = err.Error()\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\t// 设置请求头\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"Bearer %s\", apiKey))\n\n\t// 发送请求到 Deepseek\n\tclient := &http.Client{}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = fmt.Sprintf(\"Deepseek 服务连接失败: %v\", err)\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\tdefer resp.Body.Close()\n\n\t// 检查响应状态\n\tif resp.StatusCode != http.StatusOK {\n\t\tbodyBytes, _ := io.ReadAll(resp.Body)\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = resp.StatusCode\n\t\tAIRes.Message = fmt.Sprintf(\"Deepseek API 错误: %s\", string(bodyBytes))\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\t// 创建 reader\n\treader := bufio.NewReader(resp.Body)\n\n\t// 流式读取响应\n\tfor {\n\t\tline, err := reader.ReadBytes('\\n')\n\t\tif err != nil {\n\t\t\tif err == io.EOF {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\treturn\n\t\t}\n\n\t\t// 跳过空行\n\t\tlineStr := strings.TrimSpace(string(line))\n\t\tif lineStr == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Deepseek 使用 SSE 格式,需要去掉 \"data: \" 前缀\n\t\tif strings.HasPrefix(lineStr, \"data: \") {\n\t\t\tlineStr = strings.TrimPrefix(lineStr, \"data: \")\n\t\t}\n\n\t\t// 检查是否是结束标记\n\t\tif lineStr == \"[DONE]\" {\n\t\t\tbreak\n\t\t}\n\n\t\t// 解析响应\n\t\tvar response struct {\n\t\t\tID string `json:\"id\"`\n\t\t\tObject string `json:\"object\"`\n\t\t\tCreated int64 `json:\"created\"`\n\t\t\tModel string `json:\"model\"`\n\t\t\tChoices []struct {\n\t\t\t\tIndex int `json:\"index\"`\n\t\t\t\tDelta struct {\n\t\t\t\t\tRole string `json:\"role\"`\n\t\t\t\t\tContent string `json:\"content\"`\n\t\t\t\t} `json:\"delta\"`\n\t\t\t\tFinishReason string `json:\"finish_reason\"`\n\t\t\t} `json:\"choices\"`\n\t\t}\n\n\t\tif err := jsoniter.Unmarshal([]byte(lineStr), &response); err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\t// 发送数据\n\t\tif len(response.Choices) > 0 && response.Choices[0].Delta.Content != \"\" {\n\t\t\tc.Writer.WriteString(response.Choices[0].Delta.Content)\n\t\t\tc.Writer.Flush()\n\t\t}\n\n\t\t// 检查是否完成\n\t\tif len(response.Choices) > 0 && response.Choices[0].FinishReason != \"\" {\n\t\t\tbreak\n\t\t}\n\t}\n}\n"}, "4471dd019ef1c3da2ce87879b8c842a858fdbae5": {"t": 1785754892.8599405, "n": 7617, "h": "0aa1a66bbc3d1952", "c": "package handler\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\t\"zbxtable/internal/model\"\n\t\"zbxtable/pkg/response\"\n\n\t\"github.com/gin-gonic/gin\"\n\tjsoniter \"github.com/json-iterator/go\"\n)\n\n// AIChat 流式聊天接口\nfunc AIChat(c *gin.Context) {\n\t// 设置响应头\n\tc.Header(\"Content-Type\", \"text/event-stream\")\n\tc.Header(\"Cache-Control\", \"no-cache\")\n\tc.Header(\"Connection\", \"keep-alive\")\n\n\t// 解析请求体\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tvar userReq struct {\n\t\tMessage string `json:\"message\"`\n\t}\n\tif err := jsoniter.Unmarshal(body, &userReq); err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = err.Error()\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\t// 获取 AI 类型配置\n\taiType := model.GetConfigValueByKey(\"ai_type\", \"ollama\")\n\tif aiType == \"\" {\n\t\taiType = \"ollama\"\n\t}\n\n\t// 根据 AI 类型调用不同的服务\n\tswitch aiType {\n\tcase \"deepseek\":\n\t\thandleDeepseekChat(c, userReq.Message)\n\tcase \"ollama\":\n\t\thandleOllamaChat(c, userReq.Message)\n\tdefault:\n\t\t// 默认使用 Ollama\n\t\thandleOllamaChat(c, userReq.Message)\n\t}\n}\n\n// handleOllamaChat 处理 Ollama AI 请求\nfunc handleOllamaChat(c *gin.Context, message string) {\n\t// 构造系统提示词\n\tsystemPrompt := struct {\n\t\tRole string `json:\"role\"`\n\t\tContent string `json:\"content\"`\n\t}{\n\t\tRole: \"system\",\n\t\tContent: \"你是一个专业的运维分析师,请分析用户提供的问题并给出专业的建议。\",\n\t}\n\n\t// 从配置获取 Ollama 地址\n\tollamaHost := model.GetConfigValueByKey(\"ollama_host\", model.GetConfKey(\"ollama_host\"))\n\tif ollamaHost == \"\" {\n\t\tollamaHost = \"http://localhost:11434\"\n\t}\n\tollamaModel := model.GetConfigValueByKey(\"ollama_model\", model.GetConfKey(\"ollama_model\"))\n\tif ollamaModel == \"\" {\n\t\tollamaModel = \"deepseek-r1:32b\"\n\t}\n\n\t// 构造 Ollama 请求\n\tollamaReq := struct {\n\t\tModel string `json:\"model\"`\n\t\tMessages []interface{} `json:\"messages\"`\n\t\tStream bool `json:\"stream\"`\n\t}{\n\t\tModel: ollamaModel,\n\t\tStream: true,\n\t\tMessages: []interface{}{\n\t\t\tsystemPrompt,\n\t\t\tstruct {\n\t\t\t\tRole string `json:\"role\"`\n\t\t\t\tContent string `json:\"content\"`\n\t\t\t}{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: message,\n\t\t\t},\n\t\t},\n\t}\n\n\tjsonData, err := jsoniter.Marshal(ollamaReq)\n\tif err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = err.Error()\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\tollamaURL := fmt.Sprintf(\"%s/api/chat\", ollamaHost)\n\n\t// 发送请求到 Ollama\n\tresp, err := http.Post(ollamaURL, \"application/json\", bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = fmt.Sprintf(\"Ollama 服务连接失败: %v\", err)\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\tdefer resp.Body.Close()\n\n\t// 创建 reader\n\treader := bufio.NewReader(resp.Body)\n\n\t// 流式读取响应\n\tfor {\n\t\tline, err := reader.ReadBytes('\\n')\n\t\tif err != nil {\n\t\t\tif err == io.EOF {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\treturn\n\t\t}\n\n\t\t// 解析响应\n\t\tvar response struct {\n\t\t\tModel string `json:\"model\"`\n\t\t\tCreatedAt string `json:\"created_at\"`\n\t\t\tMessage struct {\n\t\t\t\tRole string `json:\"role\"`\n\t\t\t\tContent string `json:\"content\"`\n\t\t\t} `json:\"message\"`\n\t\t\tDone bool `json:\"done\"`\n\t\t}\n\t\tif err := jsoniter.Unmarshal(line, &response); err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\t// 发送数据\n\t\tif response.Message.Content != \"\" {\n\t\t\tc.Writer.WriteString(response.Message.Content)\n\t\t\tc.Writer.Flush()\n\t\t}\n\n\t\t// 如果响应完成,退出循环\n\t\tif response.Done {\n\t\t\tbreak\n\t\t}\n\t}\n}\n\n// handleDeepseekChat 处理 Deepseek AI 请求\nfunc handleDeepseekChat(c *gin.Context, message string) {\n\t// 从配置获取 Deepseek 参数\n\tapiKey := model.GetConfigValueByKey(\"deepseek_api_key\", \"\")\n\tif apiKey == \"\" {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = \"Deepseek API Key 未配置\"\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\tdeepseekModel := model.GetConfigValueByKey(\"deepseek_model\", \"deepseek-chat\")\n\tif deepseekModel == \"\" {\n\t\tdeepseekModel = \"deepseek-chat\"\n\t}\n\n\tbaseURL := model.GetConfigValueByKey(\"deepseek_base_url\", \"https://api.deepseek.com\")\n\tif baseURL == \"\" {\n\t\tbaseURL = \"https://api.deepseek.com\"\n\t}\n\n\t// 构造 Deepseek 请求\n\tdeepseekReq := struct {\n\t\tModel string `json:\"model\"`\n\t\tMessages []struct {\n\t\t\tRole string `json:\"role\"`\n\t\t\tContent string `json:\"content\"`\n\t\t} `json:\"messages\"`\n\t\tStream bool `json:\"stream\"`\n\t}{\n\t\tModel: deepseekModel,\n\t\tStream: true,\n\t\tMessages: []struct {\n\t\t\tRole string `json:\"role\"`\n\t\t\tContent string `json:\"content\"`\n\t\t}{\n\t\t\t{\n\t\t\t\tRole: \"system\",\n\t\t\t\tContent: \"你是一个专业的运维分析师,请分析用户提供的问题并给出专业的建议。\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: message,\n\t\t\t},\n\t\t},\n\t}\n\n\tjsonData, err := jsoniter.Marshal(deepseekReq)\n\tif err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = err.Error()\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\t// 构造 Deepseek API URL\n\tdeepseekURL := fmt.Sprintf(\"%s/chat/completions\", strings.TrimSuffix(baseURL, \"/\"))\n\n\t// 创建 HTTP 请求\n\treq, err := http.NewRequest(\"POST\", deepseekURL, bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = err.Error()\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\t// 设置请求头\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"Bearer %s\", apiKey))\n\n\t// 发送请求到 Deepseek\n\tclient := &http.Client{}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = fmt.Sprintf(\"Deepseek 服务连接失败: %v\", err)\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\tdefer resp.Body.Close()\n\n\t// 检查响应状态\n\tif resp.StatusCode != http.StatusOK {\n\t\tbodyBytes, _ := io.ReadAll(resp.Body)\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = resp.StatusCode\n\t\tAIRes.Message = fmt.Sprintf(\"Deepseek API 错误: %s\", string(bodyBytes))\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\t// 创建 reader\n\treader := bufio.NewReader(resp.Body)\n\n\t// 流式读取响应\n\tfor {\n\t\tline, err := reader.ReadBytes('\\n')\n\t\tif err != nil {\n\t\t\tif err == io.EOF {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\treturn\n\t\t}\n\n\t\t// 跳过空行\n\t\tlineStr := strings.TrimSpace(string(line))\n\t\tif lineStr == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Deepseek 使用 SSE 格式,需要去掉 \"data: \" 前缀\n\t\tif strings.HasPrefix(lineStr, \"data: \") {\n\t\t\tlineStr = strings.TrimPrefix(lineStr, \"data: \")\n\t\t}\n\n\t\t// 检查是否是结束标记\n\t\tif lineStr == \"[DONE]\" {\n\t\t\tbreak\n\t\t}\n\n\t\t// 解析响应\n\t\tvar response struct {\n\t\t\tID string `json:\"id\"`\n\t\t\tObject string `json:\"object\"`\n\t\t\tCreated int64 `json:\"created\"`\n\t\t\tModel string `json:\"model\"`\n\t\t\tChoices []struct {\n\t\t\t\tIndex int `json:\"index\"`\n\t\t\t\tDelta struct {\n\t\t\t\t\tRole string `json:\"role\"`\n\t\t\t\t\tContent string `json:\"content\"`\n\t\t\t\t} `json:\"delta\"`\n\t\t\t\tFinishReason string `json:\"finish_reason\"`\n\t\t\t} `json:\"choices\"`\n\t\t}\n\n\t\tif err := jsoniter.Unmarshal([]byte(lineStr), &response); err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\t// 发送数据\n\t\tif len(response.Choices) > 0 && response.Choices[0].Delta.Content != \"\" {\n\t\t\tc.Writer.WriteString(response.Choices[0].Delta.Content)\n\t\t\tc.Writer.Flush()\n\t\t}\n\n\t\t// 检查是否完成\n\t\tif len(response.Choices) > 0 && response.Choices[0].FinishReason != \"\" {\n\t\t\tbreak\n\t\t}\n\t}\n}\n"}, "2033670f9f341e796abca936fdcff0d85eb7ca01": {"t": 1785754894.7409415, "n": 7617, "h": "e7caae448c9a5dc6", "c": "package handler\n\nimport (\n\t\"zbxtable/pkg/response\"\n\t\"bufio\"\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\t\"zbxtable/internal/model\"\n\n\t\"github.com/gin-gonic/gin\"\n\tjsoniter \"github.com/json-iterator/go\"\n)\n\n// AIChat 流式聊天接口\nfunc AIChat(c *gin.Context) {\n\t// 设置响应头\n\tc.Header(\"Content-Type\", \"text/event-stream\")\n\tc.Header(\"Cache-Control\", \"no-cache\")\n\tc.Header(\"Connection\", \"keep-alive\")\n\n\t// 解析请求体\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tvar userReq struct {\n\t\tMessage string `json:\"message\"`\n\t}\n\tif err := jsoniter.Unmarshal(body, &userReq); err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = err.Error()\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\t// 获取 AI 类型配置\n\taiType := model.GetConfigValueByKey(\"ai_type\", \"ollama\")\n\tif aiType == \"\" {\n\t\taiType = \"ollama\"\n\t}\n\n\t// 根据 AI 类型调用不同的服务\n\tswitch aiType {\n\tcase \"deepseek\":\n\t\thandleDeepseekChat(c, userReq.Message)\n\tcase \"ollama\":\n\t\thandleOllamaChat(c, userReq.Message)\n\tdefault:\n\t\t// 默认使用 Ollama\n\t\thandleOllamaChat(c, userReq.Message)\n\t}\n}\n\n// handleOllamaChat 处理 Ollama AI 请求\nfunc handleOllamaChat(c *gin.Context, message string) {\n\t// 构造系统提示词\n\tsystemPrompt := struct {\n\t\tRole string `json:\"role\"`\n\t\tContent string `json:\"content\"`\n\t}{\n\t\tRole: \"system\",\n\t\tContent: \"你是一个专业的运维分析师,请分析用户提供的问题并给出专业的建议。\",\n\t}\n\n\t// 从配置获取 Ollama 地址\n\tollamaHost := model.GetConfigValueByKey(\"ollama_host\", model.GetConfKey(\"ollama_host\"))\n\tif ollamaHost == \"\" {\n\t\tollamaHost = \"http://localhost:11434\"\n\t}\n\tollamaModel := model.GetConfigValueByKey(\"ollama_model\", model.GetConfKey(\"ollama_model\"))\n\tif ollamaModel == \"\" {\n\t\tollamaModel = \"deepseek-r1:32b\"\n\t}\n\n\t// 构造 Ollama 请求\n\tollamaReq := struct {\n\t\tModel string `json:\"model\"`\n\t\tMessages []interface{} `json:\"messages\"`\n\t\tStream bool `json:\"stream\"`\n\t}{\n\t\tModel: ollamaModel,\n\t\tStream: true,\n\t\tMessages: []interface{}{\n\t\t\tsystemPrompt,\n\t\t\tstruct {\n\t\t\t\tRole string `json:\"role\"`\n\t\t\t\tContent string `json:\"content\"`\n\t\t\t}{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: message,\n\t\t\t},\n\t\t},\n\t}\n\n\tjsonData, err := jsoniter.Marshal(ollamaReq)\n\tif err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = err.Error()\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\tollamaURL := fmt.Sprintf(\"%s/api/chat\", ollamaHost)\n\n\t// 发送请求到 Ollama\n\tresp, err := http.Post(ollamaURL, \"application/json\", bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = fmt.Sprintf(\"Ollama 服务连接失败: %v\", err)\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\tdefer resp.Body.Close()\n\n\t// 创建 reader\n\treader := bufio.NewReader(resp.Body)\n\n\t// 流式读取响应\n\tfor {\n\t\tline, err := reader.ReadBytes('\\n')\n\t\tif err != nil {\n\t\t\tif err == io.EOF {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\treturn\n\t\t}\n\n\t\t// 解析响应\n\t\tvar response struct {\n\t\t\tModel string `json:\"model\"`\n\t\t\tCreatedAt string `json:\"created_at\"`\n\t\t\tMessage struct {\n\t\t\t\tRole string `json:\"role\"`\n\t\t\t\tContent string `json:\"content\"`\n\t\t\t} `json:\"message\"`\n\t\t\tDone bool `json:\"done\"`\n\t\t}\n\t\tif err := jsoniter.Unmarshal(line, &response); err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\t// 发送数据\n\t\tif response.Message.Content != \"\" {\n\t\t\tc.Writer.WriteString(response.Message.Content)\n\t\t\tc.Writer.Flush()\n\t\t}\n\n\t\t// 如果响应完成,退出循环\n\t\tif response.Done {\n\t\t\tbreak\n\t\t}\n\t}\n}\n\n// handleDeepseekChat 处理 Deepseek AI 请求\nfunc handleDeepseekChat(c *gin.Context, message string) {\n\t// 从配置获取 Deepseek 参数\n\tapiKey := model.GetConfigValueByKey(\"deepseek_api_key\", \"\")\n\tif apiKey == \"\" {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = \"Deepseek API Key 未配置\"\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\tdeepseekModel := model.GetConfigValueByKey(\"deepseek_model\", \"deepseek-chat\")\n\tif deepseekModel == \"\" {\n\t\tdeepseekModel = \"deepseek-chat\"\n\t}\n\n\tbaseURL := model.GetConfigValueByKey(\"deepseek_base_url\", \"https://api.deepseek.com\")\n\tif baseURL == \"\" {\n\t\tbaseURL = \"https://api.deepseek.com\"\n\t}\n\n\t// 构造 Deepseek 请求\n\tdeepseekReq := struct {\n\t\tModel string `json:\"model\"`\n\t\tMessages []struct {\n\t\t\tRole string `json:\"role\"`\n\t\t\tContent string `json:\"content\"`\n\t\t} `json:\"messages\"`\n\t\tStream bool `json:\"stream\"`\n\t}{\n\t\tModel: deepseekModel,\n\t\tStream: true,\n\t\tMessages: []struct {\n\t\t\tRole string `json:\"role\"`\n\t\t\tContent string `json:\"content\"`\n\t\t}{\n\t\t\t{\n\t\t\t\tRole: \"system\",\n\t\t\t\tContent: \"你是一个专业的运维分析师,请分析用户提供的问题并给出专业的建议。\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: message,\n\t\t\t},\n\t\t},\n\t}\n\n\tjsonData, err := jsoniter.Marshal(deepseekReq)\n\tif err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = err.Error()\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\t// 构造 Deepseek API URL\n\tdeepseekURL := fmt.Sprintf(\"%s/chat/completions\", strings.TrimSuffix(baseURL, \"/\"))\n\n\t// 创建 HTTP 请求\n\treq, err := http.NewRequest(\"POST\", deepseekURL, bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = err.Error()\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\t// 设置请求头\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"Bearer %s\", apiKey))\n\n\t// 发送请求到 Deepseek\n\tclient := &http.Client{}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = fmt.Sprintf(\"Deepseek 服务连接失败: %v\", err)\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\tdefer resp.Body.Close()\n\n\t// 检查响应状态\n\tif resp.StatusCode != http.StatusOK {\n\t\tbodyBytes, _ := io.ReadAll(resp.Body)\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = resp.StatusCode\n\t\tAIRes.Message = fmt.Sprintf(\"Deepseek API 错误: %s\", string(bodyBytes))\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\t// 创建 reader\n\treader := bufio.NewReader(resp.Body)\n\n\t// 流式读取响应\n\tfor {\n\t\tline, err := reader.ReadBytes('\\n')\n\t\tif err != nil {\n\t\t\tif err == io.EOF {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\treturn\n\t\t}\n\n\t\t// 跳过空行\n\t\tlineStr := strings.TrimSpace(string(line))\n\t\tif lineStr == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Deepseek 使用 SSE 格式,需要去掉 \"data: \" 前缀\n\t\tif strings.HasPrefix(lineStr, \"data: \") {\n\t\t\tlineStr = strings.TrimPrefix(lineStr, \"data: \")\n\t\t}\n\n\t\t// 检查是否是结束标记\n\t\tif lineStr == \"[DONE]\" {\n\t\t\tbreak\n\t\t}\n\n\t\t// 解析响应\n\t\tvar response struct {\n\t\t\tID string `json:\"id\"`\n\t\t\tObject string `json:\"object\"`\n\t\t\tCreated int64 `json:\"created\"`\n\t\t\tModel string `json:\"model\"`\n\t\t\tChoices []struct {\n\t\t\t\tIndex int `json:\"index\"`\n\t\t\t\tDelta struct {\n\t\t\t\t\tRole string `json:\"role\"`\n\t\t\t\t\tContent string `json:\"content\"`\n\t\t\t\t} `json:\"delta\"`\n\t\t\t\tFinishReason string `json:\"finish_reason\"`\n\t\t\t} `json:\"choices\"`\n\t\t}\n\n\t\tif err := jsoniter.Unmarshal([]byte(lineStr), &response); err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\t// 发送数据\n\t\tif len(response.Choices) > 0 && response.Choices[0].Delta.Content != \"\" {\n\t\t\tc.Writer.WriteString(response.Choices[0].Delta.Content)\n\t\t\tc.Writer.Flush()\n\t\t}\n\n\t\t// 检查是否完成\n\t\tif len(response.Choices) > 0 && response.Choices[0].FinishReason != \"\" {\n\t\t\tbreak\n\t\t}\n\t}\n}\n"}, "8f8be1a749d36e95f77ef207c16b3539be2af297": {"t": 1785754896.208867, "n": 3125, "h": "d83430768ad6aca4", "c": "package handler\n\nimport (\n\t\"zbxtable/pkg/response\"\n\t\"bufio\"\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"zbxtable/internal/model\"\n\n\t\"github.com/gin-gonic/gin\"\n\tjsoniter \"github.com/json-iterator/go\"\n)\n\n// AIChat 流式聊天接口\nfunc AIChat(c *gin.Context) {\n\t// 设置响应头\n\tc.Header(\"Content-Type\", \"text/event-stream\")\n\tc.Header(\"Cache-Control\", \"no-cache\")\n\tc.Header(\"Connection\", \"keep-alive\")\n\n\t// 解析请求体\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"请求体读取失败\")\n\t\treturn\n\t}\n\n\tvar userReq struct {\n\t\tMessage string `json:\"message\"`\n\t}\n\tif err := jsoniter.Unmarshal(body, &userReq); err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = err.Error()\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\t// 构造系统提示词\n\tsystemPrompt := struct {\n\t\tRole string `json:\"role\"`\n\t\tContent string `json:\"content\"`\n\t}{\n\t\tRole: \"system\",\n\t\tContent: \"你是一个专业的运维分析师,请分析用户提供的问题并给出专业的建议。\",\n\t}\n\n\t// 从配置或系统配置表获取Ollama地址\n\tollamaHost := model.GetConfigValueByKey(\"ollama_host\", model.GetConfKey(\"ollama_host\"))\n\tif ollamaHost == \"\" {\n\t\tollamaHost = \"http://localhost:11434\"\n\t}\n\tollamaModel := model.GetConfigValueByKey(\"ollama_model\", model.GetConfKey(\"ollama_model\"))\n\tif ollamaModel == \"\" {\n\t\tollamaModel = \"deepseek-r1:32b\"\n\t}\n\n\t// 构造Ollama请求\n\tollamaReq := struct {\n\t\tModel string `json:\"model\"`\n\t\tMessages []interface{} `json:\"messages\"`\n\t\tStream bool `json:\"stream\"`\n\t}{\n\t\tModel: ollamaModel,\n\t\tStream: true,\n\t\tMessages: []interface{}{\n\t\t\tsystemPrompt,\n\t\t\tstruct {\n\t\t\t\tRole string `json:\"role\"`\n\t\t\t\tContent string `json:\"content\"`\n\t\t\t}{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: userReq.Message,\n\t\t\t},\n\t\t},\n\t}\n\n\tjsonData, err := jsoniter.Marshal(ollamaReq)\n\tif err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = err.Error()\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\tollamaURL := fmt.Sprintf(\"%s/api/chat\", ollamaHost)\n\n\t// 发送请求到Ollama\n\tresp, err := http.Post(ollamaURL, \"application/json\", bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = err.Error()\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\tdefer resp.Body.Close()\n\n\t// 创建reader\n\treader := bufio.NewReader(resp.Body)\n\n\t// 流式读取响应\n\tfor {\n\t\tline, err := reader.ReadBytes('\\n')\n\t\tif err != nil {\n\t\t\tif err == io.EOF {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\treturn\n\t\t}\n\n\t\t// 解析响应\n\t\tvar response struct {\n\t\t\tModel string `json:\"model\"`\n\t\t\tCreatedAt string `json:\"created_at\"`\n\t\t\tMessage struct {\n\t\t\t\tRole string `json:\"role\"`\n\t\t\t\tContent string `json:\"content\"`\n\t\t\t} `json:\"message\"`\n\t\t\tDone bool `json:\"done\"`\n\t\t}\n\t\tif err := jsoniter.Unmarshal(line, &response); err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\t// 发送数据\n\t\tif response.Message.Content != \"\" {\n\t\t\tc.Writer.WriteString(response.Message.Content)\n\t\t\tc.Writer.Flush()\n\t\t}\n\n\t\t// 如果响应完成,退出循环\n\t\tif response.Done {\n\t\t\tbreak\n\t\t}\n\t}\n}\n"}, "f507b9d7748c5aaff10657b4336f9efe45493357": {"t": 1785754903.219563, "n": 3127, "h": "5b79dc452c3c2da7", "c": "package handler\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"zbxtable/internal/model\"\n\n\t\"github.com/gin-gonic/gin\"\n\tjsoniter \"github.com/json-iterator/go\"\n)\n\n// AIChat 流式聊天接口\nfunc AIChat(c *gin.Context) {\n\t// 设置响应头\n\tc.Header(\"Content-Type\", \"text/event-stream\")\n\tc.Header(\"Cache-Control\", \"no-cache\")\n\tc.Header(\"Connection\", \"keep-alive\")\n\n\t// 解析请求体\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\"code\": 500, \"message\": \"请求体读取失败\"})\n\t\treturn\n\t}\n\n\tvar userReq struct {\n\t\tMessage string `json:\"message\"`\n\t}\n\tif err := jsoniter.Unmarshal(body, &userReq); err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = err.Error()\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\t// 构造系统提示词\n\tsystemPrompt := struct {\n\t\tRole string `json:\"role\"`\n\t\tContent string `json:\"content\"`\n\t}{\n\t\tRole: \"system\",\n\t\tContent: \"你是一个专业的运维分析师,请分析用户提供的问题并给出专业的建议。\",\n\t}\n\n\t// 从配置或系统配置表获取Ollama地址\n\tollamaHost := model.GetConfigValueByKey(\"ollama_host\", model.GetConfKey(\"ollama_host\"))\n\tif ollamaHost == \"\" {\n\t\tollamaHost = \"http://localhost:11434\"\n\t}\n\tollamaModel := model.GetConfigValueByKey(\"ollama_model\", model.GetConfKey(\"ollama_model\"))\n\tif ollamaModel == \"\" {\n\t\tollamaModel = \"deepseek-r1:32b\"\n\t}\n\n\t// 构造Ollama请求\n\tollamaReq := struct {\n\t\tModel string `json:\"model\"`\n\t\tMessages []interface{} `json:\"messages\"`\n\t\tStream bool `json:\"stream\"`\n\t}{\n\t\tModel: ollamaModel,\n\t\tStream: true,\n\t\tMessages: []interface{}{\n\t\t\tsystemPrompt,\n\t\t\tstruct {\n\t\t\t\tRole string `json:\"role\"`\n\t\t\t\tContent string `json:\"content\"`\n\t\t\t}{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: userReq.Message,\n\t\t\t},\n\t\t},\n\t}\n\n\tjsonData, err := jsoniter.Marshal(ollamaReq)\n\tif err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = err.Error()\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\tollamaURL := fmt.Sprintf(\"%s/api/chat\", ollamaHost)\n\n\t// 发送请求到Ollama\n\tresp, err := http.Post(ollamaURL, \"application/json\", bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = err.Error()\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\tdefer resp.Body.Close()\n\n\t// 创建reader\n\treader := bufio.NewReader(resp.Body)\n\n\t// 流式读取响应\n\tfor {\n\t\tline, err := reader.ReadBytes('\\n')\n\t\tif err != nil {\n\t\t\tif err == io.EOF {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\treturn\n\t\t}\n\n\t\t// 解析响应\n\t\tvar response struct {\n\t\t\tModel string `json:\"model\"`\n\t\t\tCreatedAt string `json:\"created_at\"`\n\t\t\tMessage struct {\n\t\t\t\tRole string `json:\"role\"`\n\t\t\t\tContent string `json:\"content\"`\n\t\t\t} `json:\"message\"`\n\t\t\tDone bool `json:\"done\"`\n\t\t}\n\t\tif err := jsoniter.Unmarshal(line, &response); err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\t// 发送数据\n\t\tif response.Message.Content != \"\" {\n\t\t\tc.Writer.WriteString(response.Message.Content)\n\t\t\tc.Writer.Flush()\n\t\t}\n\n\t\t// 如果响应完成,退出循环\n\t\tif response.Done {\n\t\t\tbreak\n\t\t}\n\t}\n}\n"}, "54928571d31b4cc10676191ae02c19c271ab8824": {"t": 1785754906.448336, "n": 3135, "h": "c99c913a5d883d00", "c": "package handler\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"zbxtable/internal/model\"\n\n\t\"github.com/gin-gonic/gin\"\n\tjsoniter \"github.com/json-iterator/go\"\n)\n\n// AIChat 流式聊天接口\nfunc AIChat(c *gin.Context) {\n\t// 设置响应头\n\tc.Header(\"Content-Type\", \"text/event-stream\")\n\tc.Header(\"Cache-Control\", \"no-cache\")\n\tc.Header(\"Connection\", \"keep-alive\")\n\n\t// 解析请求体\n\tbody, err := io.ReadAll(c.Request.Body)\n\tif err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\"code\": 500, \"message\": \"请求体读取失败\"})\n\t\treturn\n\t}\n\t\n\tvar userReq struct {\n\t\tMessage string `json:\"message\"`\n\t}\n\tif err := jsoniter.Unmarshal(body, &userReq); err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = err.Error()\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\t// 构造系统提示词\n\tsystemPrompt := struct {\n\t\tRole string `json:\"role\"`\n\t\tContent string `json:\"content\"`\n\t}{\n\t\tRole: \"system\",\n\t\tContent: \"你是一个专业的运维分析师,请分析用户提供的问题并给出专业的建议。\",\n\t}\n\n\t// 从配置或系统配置表获取Ollama地址\n\tollamaHost := models.GetConfigValueByKey(\"ollama_host\", models.GetConfKey(\"ollama_host\"))\n\tif ollamaHost == \"\" {\n\t\tollamaHost = \"http://localhost:11434\"\n\t}\n\tollamaModel := models.GetConfigValueByKey(\"ollama_model\", models.GetConfKey(\"ollama_model\"))\n\tif ollamaModel == \"\" {\n\t\tollamaModel = \"deepseek-r1:32b\"\n\t}\n\n\t// 构造Ollama请求\n\tollamaReq := struct {\n\t\tModel string `json:\"model\"`\n\t\tMessages []interface{} `json:\"messages\"`\n\t\tStream bool `json:\"stream\"`\n\t}{\n\t\tModel: ollamaModel,\n\t\tStream: true,\n\t\tMessages: []interface{}{\n\t\t\tsystemPrompt,\n\t\t\tstruct {\n\t\t\t\tRole string `json:\"role\"`\n\t\t\t\tContent string `json:\"content\"`\n\t\t\t}{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: userReq.Message,\n\t\t\t},\n\t\t},\n\t}\n\n\tjsonData, err := jsoniter.Marshal(ollamaReq)\n\tif err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = err.Error()\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\n\tollamaURL := fmt.Sprintf(\"%s/api/chat\", ollamaHost)\n\n\t// 发送请求到Ollama\n\tresp, err := http.Post(ollamaURL, \"application/json\", bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\tvar AIRes struct {\n\t\t\tCode int `json:\"code\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t\tData string `json:\"data\"`\n\t\t}\n\t\tAIRes.Code = 500\n\t\tAIRes.Message = err.Error()\n\t\tc.JSON(http.StatusOK, AIRes)\n\t\treturn\n\t}\n\tdefer resp.Body.Close()\n\n\t// 创建reader\n\treader := bufio.NewReader(resp.Body)\n\n\t// 流式读取响应\n\tfor {\n\t\tline, err := reader.ReadBytes('\\n')\n\t\tif err != nil {\n\t\t\tif err == io.EOF {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\treturn\n\t\t}\n\n\t\t// 解析响应\n\t\tvar response struct {\n\t\t\tModel string `json:\"model\"`\n\t\t\tCreatedAt string `json:\"created_at\"`\n\t\t\tMessage struct {\n\t\t\t\tRole string `json:\"role\"`\n\t\t\t\tContent string `json:\"content\"`\n\t\t\t} `json:\"message\"`\n\t\t\tDone bool `json:\"done\"`\n\t\t}\n\t\t\t\t\tif err := jsoniter.Unmarshal(line, &response); err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\t// 发送数据\n\t\tif response.Message.Content != \"\" {\n\t\t\tc.Writer.WriteString(response.Message.Content)\n\t\t\tc.Writer.Flush()\n\t\t}\n\n\t\t// 如果响应完成,退出循环\n\t\tif response.Done {\n\t\t\tbreak\n\t\t}\n\t}\n}\n"}}}}