Files

1 line
1.0 MiB
Plaintext
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"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,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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"}, "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,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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"}, "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,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: \"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,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\", 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"}}}}