日志发送逻辑及测试消息发送接口
This commit is contained in:
@@ -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 结构体的成员
|
||||
|
||||
@@ -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("用户相关接口注册成功 (需要认证和审计)")
|
||||
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
|
||||
10
internal/app/dto/notification_dto.go
Normal file
10
internal/app/dto/notification_dto.go
Normal file
@@ -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"`
|
||||
}
|
||||
@@ -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("应用启动中...")
|
||||
|
||||
@@ -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 ""
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
// 默认值可以在这里设置,但我们优先使用配置文件中的值
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
37
internal/infra/notify/log_notifier.go
Normal file
37
internal/infra/notify/log_notifier.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -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 定义了通知的内容
|
||||
|
||||
Reference in New Issue
Block a user