diff --git a/config.example.yml b/config.example.yml new file mode 100644 index 0000000..7e1446e --- /dev/null +++ b/config.example.yml @@ -0,0 +1,93 @@ +# 应用基础配置 +app: + name: "PigFarmController" # 应用名称 + version: "1.0.0" # 应用版本 + jwt_secret: "your_jwt_secret_key_here" # JWT 签名密钥,请务必修改为强密码 + +# 服务器配置 +server: + port: 8080 # 服务器监听端口 + mode: "debug" # 运行模式: debug, release, test + +# 日志配置 +log: + level: "info" # 日志级别: debug, info, warn, error, dpanic, panic, fatal + format: "console" # 日志输出格式: console, json + enable_file: true # 是否同时输出到文件 + file_path: "app_logs/pig_farm_controller.log" # 日志文件路径 + max_size: 100 # 单个日志文件最大大小 (MB) + max_backups: 7 # 最多保留的旧日志文件数量 + max_age: 7 # 最多保留的旧日志文件天数 + compress: true # 是否压缩旧日志文件 + +# 数据库配置 +database: + host: "localhost" # 数据库主机地址 + port: 5432 # 数据库端口 + username: "postgres" # 数据库用户名 + password: "your_db_password" # 数据库密码 + dbname: "pig_farm_controller_db" # 数据库名称 + sslmode: "disable" # SSL模式: disable, require, verify-ca, verify-full + is_timescaledb: false # 是否为 TimescaleDB + max_open_conns: 100 # 最大开放连接数 + max_idle_conns: 10 # 最大空闲连接数 + conn_max_lifetime: 300 # 连接最大生命周期 (秒) + +# WebSocket配置 +websocket: + timeout: 60 # WebSocket请求超时时间 (秒) + heartbeat_interval: 30 # 心跳检测间隔 (秒) + +# 心跳配置 +heartbeat: + interval: 10 # 心跳间隔 (秒) + concurrency: 5 # 请求并发数 + +# ChirpStack API 配置 +chirp_stack: + api_host: "http://localhost:8080" # ChirpStack API 主机地址 + api_token: "your_chirpstack_api_token" # ChirpStack API Token + fport: 10 # ChirpStack FPort + api_timeout: 5 # API 请求超时时间 (秒) + collection_request_timeout: 10 # 采集请求超时时间 (秒) + +# 任务调度配置 +task: + interval: 5 # 任务调度间隔 (秒) + num_workers: 5 # 任务执行器并发工作数量 + +# Lora 配置 +lora: + mode: "lora_mesh" # Lora 运行模式: lora_wan, lora_mesh + +# Lora Mesh 配置 +lora_mesh: + uart_port: "/dev/ttyUSB0" # UART 串口路径 + baud_rate: 115200 # 波特率 + timeout: 5 # 超时时间 (秒) + lora_mesh_mode: "transparent" # Lora Mesh 模式: transparent, command + max_chunk_size: 200 # 最大数据块大小 + reassembly_timeout: 10 # 重组超时时间 (秒) + +# 通知服务配置 +notify: + primary: "log" # 首选通知渠道: smtp, wechat, lark, log (如果其他渠道未启用,log 会自动成为首选) + failureThreshold: 2 # 连续失败多少次后触发广播模式 + smtp: + enabled: false # 是否启用 SMTP 邮件通知 + host: "smtp.example.com" # SMTP 服务器地址 + port: 587 # SMTP 服务器端口 + username: "your_email@example.com" # 发件人邮箱地址 + password: "your_email_password" # 发件人邮箱授权码或密码 + sender: "PigFarm Alarm " # 发件人名称和地址 + + wechat: + enabled: false # 是否启用企业微信通知 + corpID: "wwxxxxxxxxxxxx" # 企业ID (CorpID) + agentID: "1000001" # 应用ID (AgentID) + secret: "your_wechat_app_secret" # 应用密钥 (Secret) + + lark: + enabled: false # 是否启用飞书通知 + appID: "cli_xxxxxxxxxx" # 应用 ID + appSecret: "your_lark_app_secret" # 应用密钥 diff --git a/docs/docs.go b/docs/docs.go index 460adbc..0b8fdf8 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -3923,6 +3923,64 @@ const docTemplate = `{ } } } + }, + "/api/v1/users/{id}/notifications/test": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "为指定用户发送一条特定渠道的测试消息,以验证其配置是否正确。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "发送测试通知", + "parameters": [ + { + "type": "integer", + "description": "用户ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "请求体", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SendTestNotificationRequest" + } + } + ], + "responses": { + "200": { + "description": "成功响应", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + } + } + } } }, "definitions": { @@ -5591,6 +5649,22 @@ const docTemplate = `{ } } }, + "dto.SendTestNotificationRequest": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "description": "Type 指定要测试的通知渠道\n@enum(smtp, wechat, lark, log)", + "allOf": [ + { + "$ref": "#/definitions/notify.NotifierType" + } + ] + } + } + }, "dto.SensorDataDTO": { "type": "object", "properties": { @@ -6530,6 +6604,23 @@ const docTemplate = `{ "$ref": "#/definitions/models.SensorType" } } + }, + "notify.NotifierType": { + "type": "string", + "enum": [ + "smtp", + "wechat", + "lark", + "sms", + "log" + ], + "x-enum-varnames": [ + "NotifierTypeSMTP", + "NotifierTypeWeChat", + "NotifierTypeLark", + "NotifierTypeSMS", + "NotifierTypeLog" + ] } }, "securityDefinitions": { diff --git a/docs/swagger.json b/docs/swagger.json index 067a487..e0be324 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -3915,6 +3915,64 @@ } } } + }, + "/api/v1/users/{id}/notifications/test": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "为指定用户发送一条特定渠道的测试消息,以验证其配置是否正确。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "发送测试通知", + "parameters": [ + { + "type": "integer", + "description": "用户ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "请求体", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SendTestNotificationRequest" + } + } + ], + "responses": { + "200": { + "description": "成功响应", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + } + } + } } }, "definitions": { @@ -5583,6 +5641,22 @@ } } }, + "dto.SendTestNotificationRequest": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "description": "Type 指定要测试的通知渠道\n@enum(smtp, wechat, lark, log)", + "allOf": [ + { + "$ref": "#/definitions/notify.NotifierType" + } + ] + } + } + }, "dto.SensorDataDTO": { "type": "object", "properties": { @@ -6522,6 +6596,23 @@ "$ref": "#/definitions/models.SensorType" } } + }, + "notify.NotifierType": { + "type": "string", + "enum": [ + "smtp", + "wechat", + "lark", + "sms", + "log" + ], + "x-enum-varnames": [ + "NotifierTypeSMTP", + "NotifierTypeWeChat", + "NotifierTypeLark", + "NotifierTypeSMS", + "NotifierTypeLog" + ] } }, "securityDefinitions": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 0953c91..e4eaa07 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1125,6 +1125,17 @@ definitions: - traderName - unitPrice type: object + dto.SendTestNotificationRequest: + properties: + type: + allOf: + - $ref: '#/definitions/notify.NotifierType' + description: |- + Type 指定要测试的通知渠道 + @enum(smtp, wechat, lark, log) + required: + - type + type: object dto.SensorDataDTO: properties: data: @@ -1816,6 +1827,20 @@ definitions: type: $ref: '#/definitions/models.SensorType' type: object + notify.NotifierType: + enum: + - smtp + - wechat + - lark + - sms + - log + type: string + x-enum-varnames: + - NotifierTypeSMTP + - NotifierTypeWeChat + - NotifierTypeLark + - NotifierTypeSMS + - NotifierTypeLog info: contact: email: divano@example.com @@ -4086,6 +4111,40 @@ paths: summary: 获取指定用户的操作历史 tags: - 用户管理 + /api/v1/users/{id}/notifications/test: + post: + consumes: + - application/json + description: 为指定用户发送一条特定渠道的测试消息,以验证其配置是否正确。 + parameters: + - description: 用户ID + in: path + name: id + required: true + type: integer + - description: 请求体 + in: body + name: body + required: true + schema: + $ref: '#/definitions/dto.SendTestNotificationRequest' + produces: + - application/json + responses: + "200": + description: 成功响应 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + type: string + type: object + security: + - BearerAuth: [] + summary: 发送测试通知 + tags: + - 用户管理 /api/v1/users/login: post: consumes: diff --git a/internal/app/api/api.go b/internal/app/api/api.go index 6037bbb..835bf82 100644 --- a/internal/app/api/api.go +++ b/internal/app/api/api.go @@ -28,6 +28,7 @@ import ( "git.huangwc.com/pig/pig-farm-controller/internal/app/webhook" "git.huangwc.com/pig/pig-farm-controller/internal/domain/audit" domain_device "git.huangwc.com/pig/pig-farm-controller/internal/domain/device" + domain_notify "git.huangwc.com/pig/pig-farm-controller/internal/domain/notify" "git.huangwc.com/pig/pig-farm-controller/internal/domain/task" "git.huangwc.com/pig/pig-farm-controller/internal/domain/token" "git.huangwc.com/pig/pig-farm-controller/internal/infra/config" @@ -68,9 +69,9 @@ func NewAPI(cfg config.ServerConfig, pigFarmService service.PigFarmService, pigBatchService service.PigBatchService, monitorService service.MonitorService, - userActionLogRepository repository.UserActionLogRepository, tokenService token.TokenService, auditService audit.Service, + notifyService domain_notify.Service, deviceService domain_device.Service, listenHandler webhook.ListenHandler, analysisTaskManager *task.AnalysisPlanTaskManager) *API { @@ -96,7 +97,7 @@ func NewAPI(cfg config.ServerConfig, config: cfg, listenHandler: listenHandler, // 在 NewAPI 中初始化用户控制器,并将其作为 API 结构体的成员 - userController: user.NewController(userRepo, monitorService, logger, tokenService), + userController: user.NewController(userRepo, monitorService, logger, tokenService, notifyService), // 在 NewAPI 中初始化设备控制器,并将其作为 API 结构体的成员 deviceController: device.NewController(deviceRepository, areaControllerRepository, deviceTemplateRepository, deviceService, logger), // 在 NewAPI 中初始化计划控制器,并将其作为 API 结构体的成员 diff --git a/internal/app/api/router.go b/internal/app/api/router.go index e5ac36e..a7a2c33 100644 --- a/internal/app/api/router.go +++ b/internal/app/api/router.go @@ -57,6 +57,7 @@ func (a *API) setupRoutes() { userGroup := authGroup.Group("/users") { userGroup.GET("/:id/history", a.userController.ListUserHistory) // 获取用户操作历史 + userGroup.POST("/:id/notifications/test", a.userController.SendTestNotification) } a.logger.Info("用户相关接口注册成功 (需要认证和审计)") diff --git a/internal/app/controller/user/user_controller.go b/internal/app/controller/user/user_controller.go index 5ec7d55..5fca24a 100644 --- a/internal/app/controller/user/user_controller.go +++ b/internal/app/controller/user/user_controller.go @@ -7,6 +7,7 @@ import ( "git.huangwc.com/pig/pig-farm-controller/internal/app/controller" "git.huangwc.com/pig/pig-farm-controller/internal/app/dto" "git.huangwc.com/pig/pig-farm-controller/internal/app/service" + domain_notify "git.huangwc.com/pig/pig-farm-controller/internal/domain/notify" "git.huangwc.com/pig/pig-farm-controller/internal/domain/token" "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" @@ -19,16 +20,24 @@ import ( type Controller struct { userRepo repository.UserRepository monitorService service.MonitorService - tokenService token.TokenService // 注入 token 服务 + tokenService token.TokenService + notifyService domain_notify.Service logger *logs.Logger } // NewController 创建用户控制器实例 -func NewController(userRepo repository.UserRepository, monitorService service.MonitorService, logger *logs.Logger, tokenService token.TokenService) *Controller { +func NewController( + userRepo repository.UserRepository, + monitorService service.MonitorService, + logger *logs.Logger, + tokenService token.TokenService, + notifyService domain_notify.Service, +) *Controller { return &Controller{ userRepo: userRepo, monitorService: monitorService, tokenService: tokenService, + notifyService: notifyService, logger: logger, } } @@ -192,3 +201,46 @@ func (c *Controller) ListUserHistory(ctx *gin.Context) { c.logger.Infof("%s: 成功获取用户 %d 的操作历史, 数量: %d", actionType, userID, len(data)) controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取用户操作历史成功", resp, actionType, "获取用户操作历史成功", opts) } + +// SendTestNotification godoc +// @Summary 发送测试通知 +// @Description 为指定用户发送一条特定渠道的测试消息,以验证其配置是否正确。 +// @Tags 用户管理 +// @Security BearerAuth +// @Accept json +// @Produce json +// @Param id path int true "用户ID" +// @Param body body dto.SendTestNotificationRequest true "请求体" +// @Success 200 {object} controller.Response{data=string} "成功响应" +// @Router /api/v1/users/{id}/notifications/test [post] +func (c *Controller) SendTestNotification(ctx *gin.Context) { + const actionType = "发送测试通知" + + // 1. 从 URL 中获取用户 ID + userID, err := strconv.ParseUint(ctx.Param("id"), 10, 32) + if err != nil { + c.logger.Errorf("%s: 无效的用户ID格式: %v", actionType, err) + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的用户ID格式", actionType, "无效的用户ID格式", ctx.Param("id")) + return + } + + // 2. 从请求体 (JSON Body) 中获取要测试的通知类型 + var req dto.SendTestNotificationRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err) + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "请求体格式错误或缺少 'type' 字段: "+err.Error(), actionType, "请求体绑定失败", req) + return + } + + // 3. 调用领域服务 + err = c.notifyService.SendTestMessage(uint(userID), req.Type) + if err != nil { + c.logger.Errorf("%s: 服务层调用失败: %v", actionType, err) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "发送测试消息失败: "+err.Error(), actionType, "服务层调用失败", gin.H{"userID": userID, "type": req.Type}) + return + } + + // 4. 返回成功响应 + c.logger.Infof("%s: 成功为用户 %d 发送类型为 %s 的测试消息", actionType, userID, req.Type) + controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "测试消息已发送,请检查您的接收端。", nil, actionType, "测试消息发送成功", gin.H{"userID": userID, "type": req.Type}) +} diff --git a/internal/app/dto/notification_dto.go b/internal/app/dto/notification_dto.go new file mode 100644 index 0000000..bbefb8e --- /dev/null +++ b/internal/app/dto/notification_dto.go @@ -0,0 +1,10 @@ +package dto + +import "git.huangwc.com/pig/pig-farm-controller/internal/infra/notify" + +// SendTestNotificationRequest 定义了发送测试通知请求的 JSON 结构 +type SendTestNotificationRequest struct { + // Type 指定要测试的通知渠道 + // @enum(smtp, wechat, lark, log) + Type notify.NotifierType `json:"type" binding:"required"` +} diff --git a/internal/core/application.go b/internal/core/application.go index 573cad6..d51f5e6 100644 --- a/internal/core/application.go +++ b/internal/core/application.go @@ -12,6 +12,7 @@ import ( "git.huangwc.com/pig/pig-farm-controller/internal/app/webhook" "git.huangwc.com/pig/pig-farm-controller/internal/domain/audit" "git.huangwc.com/pig/pig-farm-controller/internal/domain/device" + domain_notify "git.huangwc.com/pig/pig-farm-controller/internal/domain/notify" "git.huangwc.com/pig/pig-farm-controller/internal/domain/pig" "git.huangwc.com/pig/pig-farm-controller/internal/domain/task" "git.huangwc.com/pig/pig-farm-controller/internal/domain/token" @@ -19,6 +20,7 @@ import ( "git.huangwc.com/pig/pig-farm-controller/internal/infra/database" "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/notify" "git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" "git.huangwc.com/pig/pig-farm-controller/internal/infra/transport" "git.huangwc.com/pig/pig-farm-controller/internal/infra/transport/lora" @@ -41,6 +43,9 @@ type Application struct { // Lora Mesh 监听器 loraMeshCommunicator transport.Listener + + // 通知服务 + NotifyService domain_notify.Service } // NewApplication 创建并初始化一个新的 Application 实例。 @@ -117,6 +122,12 @@ func NewApplication(configPath string) (*Application, error) { // 初始化审计服务 auditService := audit.NewService(userActionLogRepo, logger) + // 初始化通知服务 + notifyService, err := initNotifyService(cfg.Notify, logger, userRepo) + if err != nil { + return nil, fmt.Errorf("初始化通知服务失败: %w", err) + } + // --- 初始化 LoRa 相关组件 --- var listenHandler webhook.ListenHandler var comm transport.Communicator @@ -176,9 +187,9 @@ func NewApplication(configPath string) (*Application, error) { pigFarmService, pigBatchService, monitorService, - userActionLogRepo, tokenService, auditService, + notifyService, generalDeviceService, listenHandler, analysisPlanTaskManager, @@ -197,11 +208,92 @@ func NewApplication(configPath string) (*Application, error) { pendingCollectionRepo: pendingCollectionRepo, analysisPlanTaskManager: analysisPlanTaskManager, loraMeshCommunicator: loraListener, + NotifyService: notifyService, } return app, nil } +// initNotifyService 根据配置初始化并返回一个通知领域服务。 +// 它确保至少有一个 LogNotifier 总是可用,并根据配置启用其他通知器。 +func initNotifyService( + cfg config.NotifyConfig, + log *logs.Logger, + userRepo repository.UserRepository, +) (domain_notify.Service, error) { + var availableNotifiers []notify.Notifier + + // 1. 总是创建 LogNotifier 作为所有告警的最终记录渠道 + logNotifier := notify.NewLogNotifier(log) + availableNotifiers = append(availableNotifiers, logNotifier) + log.Info("Log通知器已启用 (作为所有告警的最终记录渠道)") + + // 2. 根据配置,按需创建并收集所有启用的其他 Notifier 实例 + if cfg.SMTP.Enabled { + smtpNotifier := notify.NewSMTPNotifier( + cfg.SMTP.Host, + cfg.SMTP.Port, + cfg.SMTP.Username, + cfg.SMTP.Password, + cfg.SMTP.Sender, + ) + availableNotifiers = append(availableNotifiers, smtpNotifier) + log.Info("SMTP通知器已启用") + } + + if cfg.WeChat.Enabled { + wechatNotifier := notify.NewWechatNotifier( + cfg.WeChat.CorpID, + cfg.WeChat.AgentID, + cfg.WeChat.Secret, + ) + availableNotifiers = append(availableNotifiers, wechatNotifier) + log.Info("企业微信通知器已启用") + } + + if cfg.Lark.Enabled { + larkNotifier := notify.NewLarkNotifier( + cfg.Lark.AppID, + cfg.Lark.AppSecret, + ) + availableNotifiers = append(availableNotifiers, larkNotifier) + log.Info("飞书通知器已启用") + } + + // 3. 动态确定首选通知器 + var primaryNotifier notify.Notifier + primaryNotifierType := notify.NotifierType(cfg.Primary) + + // 检查用户指定的主渠道是否已启用 + for _, n := range availableNotifiers { + if n.Type() == primaryNotifierType { + primaryNotifier = n + break + } + } + + // 如果用户指定的主渠道未启用或未指定,则自动选择第一个可用的 (这将是 LogNotifier,如果其他都未启用) + if primaryNotifier == nil { + primaryNotifier = availableNotifiers[0] // 确保总能找到一个,因为 LogNotifier 总是存在的 + log.Warnf("配置的首选渠道 '%s' 未启用或未指定,已自动降级使用 '%s' 作为首选渠道。", cfg.Primary, primaryNotifier.Type()) + } + + // 4. 使用创建的 Notifier 列表来组装领域服务 + notifyService, err := domain_notify.NewFailoverService( + log, + userRepo, + availableNotifiers, + primaryNotifier.Type(), + cfg.FailureThreshold, + ) + if err != nil { + return nil, fmt.Errorf("创建故障转移通知服务失败: %w", err) + } + + log.Infof("通知服务初始化成功,首选渠道: %s, 故障阈值: %d", primaryNotifier.Type(), cfg.FailureThreshold) + return notifyService, nil +} + // Start 启动应用的所有组件并阻塞,直到接收到关闭信号。 func (app *Application) Start() error { app.Logger.Info("应用启动中...") diff --git a/internal/domain/notify/notify.go b/internal/domain/notify/notify.go index c0ad55d..d1d1e95 100644 --- a/internal/domain/notify/notify.go +++ b/internal/domain/notify/notify.go @@ -215,6 +215,8 @@ func getAddressForNotifier(notifierType notify.NotifierType, contact models.Cont return contact.WeChat case notify.NotifierTypeLark: return contact.Feishu + case notify.NotifierTypeLog: + return "log" // LogNotifier不需要具体的地址,但为了函数签名一致性,返回一个无意义的非空字符串以绕过配置存在检查 default: return "" } diff --git a/internal/infra/config/config.go b/internal/infra/config/config.go index 0fd56ff..d4d583c 100644 --- a/internal/infra/config/config.go +++ b/internal/infra/config/config.go @@ -41,6 +41,9 @@ type Config struct { // LoraMesh LoraMesh配置 LoraMesh LoraMeshConfig `yaml:"lora_mesh"` + + // Notify 通知服务配置 + Notify NotifyConfig `yaml:"notify"` } // AppConfig 代表应用基础配置 @@ -158,6 +161,40 @@ type LoraMeshConfig struct { ReassemblyTimeout int `yaml:"reassembly_timeout"` } +// NotifyConfig 包含了所有与通知服务相关的配置 +type NotifyConfig struct { + Primary string `yaml:"primary"` // 首选通知渠道 (e.g., "smtp", "wechat", "lark", "log") + FailureThreshold int `yaml:"failureThreshold"` // 连续失败多少次后触发广播模式 + SMTP SMTPConfig `yaml:"smtp"` + WeChat WeChatConfig `yaml:"wechat"` + Lark LarkConfig `yaml:"lark"` +} + +// SMTPConfig SMTP邮件配置 +type SMTPConfig struct { + Enabled bool `yaml:"enabled"` + Host string `yaml:"host"` + Port int `yaml:"port"` + Username string `yaml:"username"` + Password string `yaml:"password"` + Sender string `yaml:"sender"` +} + +// WeChatConfig 企业微信应用配置 +type WeChatConfig struct { + Enabled bool `yaml:"enabled"` + CorpID string `yaml:"corpID"` + AgentID string `yaml:"agentID"` + Secret string `yaml:"secret"` +} + +// LarkConfig 飞书应用配置 +type LarkConfig struct { + Enabled bool `yaml:"enabled"` + AppID string `yaml:"appID"` + AppSecret string `yaml:"appSecret"` +} + // NewConfig 创建并返回一个新的配置实例 func NewConfig() *Config { // 默认值可以在这里设置,但我们优先使用配置文件中的值 diff --git a/internal/infra/notify/lark.go b/internal/infra/notify/lark.go index 8922b78..586aa91 100644 --- a/internal/infra/notify/lark.go +++ b/internal/infra/notify/lark.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "net/http" - "strings" "sync" "time" ) diff --git a/internal/infra/notify/log_notifier.go b/internal/infra/notify/log_notifier.go new file mode 100644 index 0000000..887e524 --- /dev/null +++ b/internal/infra/notify/log_notifier.go @@ -0,0 +1,37 @@ +package notify + +import ( + "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" +) + +// logNotifier 实现了 Notifier 接口,用于将告警信息记录到日志中。 +type logNotifier struct { + logger *logs.Logger +} + +// NewLogNotifier 创建一个新的 logNotifier 实例。 +// 它接收一个日志记录器,用于实际的日志输出。 +func NewLogNotifier(logger *logs.Logger) Notifier { + return &logNotifier{ + logger: logger, + } +} + +// Send 将告警内容以结构化的方式记录到日志中。 +// toAddr 参数在这里表示告警的预期接收者地址,也会被记录。 +func (l *logNotifier) Send(content AlarmContent, toAddr string) error { + l.logger.Infow("告警已记录到日志", + "notifierType", NotifierTypeLog, + "title", content.Title, + "message", content.Message, + "level", content.Level.String(), + "timestamp", content.Timestamp.Format(DefaultTimeFormat), + "toAddr", toAddr, + ) + return nil // 记录日志操作本身不应失败 +} + +// Type 返回通知器的类型。 +func (l *logNotifier) Type() NotifierType { + return NotifierTypeLog +} diff --git a/internal/infra/notify/notify.go b/internal/infra/notify/notify.go index 1e99a7f..84b9f9d 100644 --- a/internal/infra/notify/notify.go +++ b/internal/infra/notify/notify.go @@ -14,11 +14,13 @@ type NotifierType string const ( // NotifierTypeSMTP 表示 SMTP 邮件通知器。 - NotifierTypeSMTP NotifierType = "邮件" + NotifierTypeSMTP NotifierType = "smtp" // NotifierTypeWeChat 表示企业微信通知器。 - NotifierTypeWeChat NotifierType = "企业微信" + NotifierTypeWeChat NotifierType = "wechat" // NotifierTypeLark 表示飞书通知器。 - NotifierTypeLark NotifierType = "飞书" + NotifierTypeLark NotifierType = "lark" + // NotifierTypeLog 表示日志通知器,作为最终的告警记录渠道。 + NotifierTypeLog NotifierType = "log" ) // AlarmContent 定义了通知的内容