初步实现审计
This commit is contained in:
		| @@ -20,6 +20,7 @@ import ( | |||||||
| 	"git.huangwc.com/pig/pig-farm-controller/internal/app/controller/plan" | 	"git.huangwc.com/pig/pig-farm-controller/internal/app/controller/plan" | ||||||
| 	"git.huangwc.com/pig/pig-farm-controller/internal/app/controller/user" | 	"git.huangwc.com/pig/pig-farm-controller/internal/app/controller/user" | ||||||
| 	"git.huangwc.com/pig/pig-farm-controller/internal/app/middleware" | 	"git.huangwc.com/pig/pig-farm-controller/internal/app/middleware" | ||||||
|  | 	"git.huangwc.com/pig/pig-farm-controller/internal/app/service/audit" | ||||||
| 	"git.huangwc.com/pig/pig-farm-controller/internal/app/service/task" | 	"git.huangwc.com/pig/pig-farm-controller/internal/app/service/task" | ||||||
| 	"git.huangwc.com/pig/pig-farm-controller/internal/app/service/token" | 	"git.huangwc.com/pig/pig-farm-controller/internal/app/service/token" | ||||||
| 	"git.huangwc.com/pig/pig-farm-controller/internal/app/service/transport" | 	"git.huangwc.com/pig/pig-farm-controller/internal/app/service/transport" | ||||||
| @@ -38,6 +39,7 @@ type API struct { | |||||||
| 	logger              *logs.Logger                  // 日志记录器,用于输出日志信息 | 	logger              *logs.Logger                  // 日志记录器,用于输出日志信息 | ||||||
| 	userRepo            repository.UserRepository     // 用户数据仓库接口,用于用户数据操作 | 	userRepo            repository.UserRepository     // 用户数据仓库接口,用于用户数据操作 | ||||||
| 	tokenService        token.TokenService            // Token 服务接口,用于 JWT token 的生成和解析 | 	tokenService        token.TokenService            // Token 服务接口,用于 JWT token 的生成和解析 | ||||||
|  | 	auditService        audit.Service                 // 审计服务,用于记录用户操作 | ||||||
| 	httpServer          *http.Server                  // 标准库的 HTTP 服务器实例,用于启动和停止服务 | 	httpServer          *http.Server                  // 标准库的 HTTP 服务器实例,用于启动和停止服务 | ||||||
| 	config              config.ServerConfig           // API 服务器的配置,使用 infra/config 包中的 ServerConfig | 	config              config.ServerConfig           // API 服务器的配置,使用 infra/config 包中的 ServerConfig | ||||||
| 	userController      *user.Controller              // 用户控制器实例 | 	userController      *user.Controller              // 用户控制器实例 | ||||||
| @@ -55,6 +57,7 @@ func NewAPI(cfg config.ServerConfig, | |||||||
| 	deviceRepository repository.DeviceRepository, | 	deviceRepository repository.DeviceRepository, | ||||||
| 	planRepository repository.PlanRepository, | 	planRepository repository.PlanRepository, | ||||||
| 	tokenService token.TokenService, | 	tokenService token.TokenService, | ||||||
|  | 	auditService audit.Service, // 注入审计服务 | ||||||
| 	listenHandler transport.ListenHandler, | 	listenHandler transport.ListenHandler, | ||||||
| 	analysisTaskManager *task.AnalysisPlanTaskManager) *API { | 	analysisTaskManager *task.AnalysisPlanTaskManager) *API { | ||||||
| 	// 设置 Gin 模式,例如 gin.ReleaseMode (生产模式) 或 gin.DebugMode (开发模式) | 	// 设置 Gin 模式,例如 gin.ReleaseMode (生产模式) 或 gin.DebugMode (开发模式) | ||||||
| @@ -75,6 +78,7 @@ func NewAPI(cfg config.ServerConfig, | |||||||
| 		logger:        logger, | 		logger:        logger, | ||||||
| 		userRepo:      userRepo, | 		userRepo:      userRepo, | ||||||
| 		tokenService:  tokenService, | 		tokenService:  tokenService, | ||||||
|  | 		auditService:  auditService, | ||||||
| 		config:        cfg, | 		config:        cfg, | ||||||
| 		listenHandler: listenHandler, | 		listenHandler: listenHandler, | ||||||
| 		// 在 NewAPI 中初始化用户控制器,并将其作为 API 结构体的成员 | 		// 在 NewAPI 中初始化用户控制器,并将其作为 API 结构体的成员 | ||||||
| @@ -133,7 +137,8 @@ func (a *API) setupRoutes() { | |||||||
| 	// --- Authenticated Routes --- | 	// --- Authenticated Routes --- | ||||||
| 	// 所有在此注册的路由都需要通过 JWT 身份验证 | 	// 所有在此注册的路由都需要通过 JWT 身份验证 | ||||||
| 	authGroup := a.engine.Group("/api/v1") | 	authGroup := a.engine.Group("/api/v1") | ||||||
| 	authGroup.Use(middleware.AuthMiddleware(a.tokenService)) | 	authGroup.Use(middleware.AuthMiddleware(a.tokenService, a.userRepo)) // 1. 身份认证 | ||||||
|  | 	authGroup.Use(middleware.AuditLogMiddleware(a.auditService))         // 2. 审计日志 | ||||||
| 	{ | 	{ | ||||||
| 		// 设备相关路由组 | 		// 设备相关路由组 | ||||||
| 		deviceGroup := authGroup.Group("/devices") | 		deviceGroup := authGroup.Group("/devices") | ||||||
| @@ -144,7 +149,7 @@ func (a *API) setupRoutes() { | |||||||
| 			deviceGroup.PUT("/:id", a.deviceController.UpdateDevice) | 			deviceGroup.PUT("/:id", a.deviceController.UpdateDevice) | ||||||
| 			deviceGroup.DELETE("/:id", a.deviceController.DeleteDevice) | 			deviceGroup.DELETE("/:id", a.deviceController.DeleteDevice) | ||||||
| 		} | 		} | ||||||
| 		a.logger.Info("设备相关接口注册成功 (需要认证)") | 		a.logger.Info("设备相关接口注册成功 (需要认证和审计)") | ||||||
|  |  | ||||||
| 		// 计划相关路由组 | 		// 计划相关路由组 | ||||||
| 		planGroup := authGroup.Group("/plans") | 		planGroup := authGroup.Group("/plans") | ||||||
| @@ -157,7 +162,7 @@ func (a *API) setupRoutes() { | |||||||
| 			planGroup.POST("/:id/start", a.planController.StartPlan) | 			planGroup.POST("/:id/start", a.planController.StartPlan) | ||||||
| 			planGroup.POST("/:id/stop", a.planController.StopPlan) | 			planGroup.POST("/:id/stop", a.planController.StopPlan) | ||||||
| 		} | 		} | ||||||
| 		a.logger.Info("计划相关接口注册成功 (需要认证)") | 		a.logger.Info("计划相关接口注册成功 (需要认证和审计)") | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,37 +3,40 @@ package controller | |||||||
| import ( | import ( | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  |  | ||||||
|  | 	"git.huangwc.com/pig/pig-farm-controller/internal/app/middleware" | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // --- 业务状态码 --- | // --- 业务状态码 --- | ||||||
|  | type ResponseCode int | ||||||
|  |  | ||||||
| const ( | const ( | ||||||
| 	// 成功状态码 (2000-2999) | 	// 成功状态码 (2000-2999) | ||||||
| 	CodeSuccess = 2000 // 操作成功 | 	CodeSuccess ResponseCode = 2000 // 操作成功 | ||||||
| 	CodeCreated = 2001 // 创建成功 | 	CodeCreated ResponseCode = 2001 // 创建成功 | ||||||
|  |  | ||||||
| 	// 客户端错误状态码 (4000-4999) | 	// 客户端错误状态码 (4000-4999) | ||||||
| 	CodeBadRequest   = 4000 // 请求参数错误 | 	CodeBadRequest   ResponseCode = 4000 // 请求参数错误 | ||||||
| 	CodeUnauthorized = 4001 // 未授权 | 	CodeUnauthorized ResponseCode = 4001 // 未授权 | ||||||
| 	CodeNotFound     = 4004 // 资源未找到 | 	CodeNotFound     ResponseCode = 4004 // 资源未找到 | ||||||
| 	CodeConflict     = 4009 // 资源冲突 | 	CodeConflict     ResponseCode = 4009 // 资源冲突 | ||||||
|  |  | ||||||
| 	// 服务器错误状态码 (5000-5999) | 	// 服务器错误状态码 (5000-5999) | ||||||
| 	CodeInternalError      = 5000 // 服务器内部错误 | 	CodeInternalError      ResponseCode = 5000 // 服务器内部错误 | ||||||
| 	CodeServiceUnavailable = 5003 // 服务不可用 | 	CodeServiceUnavailable ResponseCode = 5003 // 服务不可用 | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // --- 通用响应结构 --- | // --- 通用响应结构 --- | ||||||
|  |  | ||||||
| // Response 定义统一的API响应结构体 | // Response 定义统一的API响应结构体 | ||||||
| type Response struct { | type Response struct { | ||||||
| 	Code    int         `json:"code"`    // 业务状态码 | 	Code    ResponseCode `json:"code"`    // 业务状态码 | ||||||
| 	Message string       `json:"message"` // 提示信息 | 	Message string       `json:"message"` // 提示信息 | ||||||
| 	Data    interface{}  `json:"data"`    // 业务数据 | 	Data    interface{}  `json:"data"`    // 业务数据 | ||||||
| } | } | ||||||
|  |  | ||||||
| // SendResponse 发送统一格式的JSON响应 | // SendResponse 发送统一格式的JSON响应 (基础函数,不带审计) | ||||||
| func SendResponse(ctx *gin.Context, code int, message string, data interface{}) { | func SendResponse(ctx *gin.Context, code ResponseCode, message string, data interface{}) { | ||||||
| 	ctx.JSON(http.StatusOK, Response{ | 	ctx.JSON(http.StatusOK, Response{ | ||||||
| 		Code:    code, | 		Code:    code, | ||||||
| 		Message: message, | 		Message: message, | ||||||
| @@ -41,7 +44,37 @@ func SendResponse(ctx *gin.Context, code int, message string, data interface{}) | |||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|  |  | ||||||
| // SendErrorResponse 发送统一格式的错误响应 | // SendErrorResponse 发送统一格式的错误响应 (基础函数,不带审计) | ||||||
| func SendErrorResponse(ctx *gin.Context, code int, message string) { | func SendErrorResponse(ctx *gin.Context, code ResponseCode, message string) { | ||||||
| 	SendResponse(ctx, code, message, nil) | 	SendResponse(ctx, code, message, nil) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // --- 带审计功能的响应函数 --- | ||||||
|  |  | ||||||
|  | // setAuditDetails 是一个内部辅助函数,用于在 gin.Context 中设置业务相关的审计信息。 | ||||||
|  | func setAuditDetails(c *gin.Context, actionType, description string, targetResource interface{}) { | ||||||
|  | 	// 只有当 actionType 不为空时,才设置审计信息,这作为触发审计的标志 | ||||||
|  | 	if actionType != "" { | ||||||
|  | 		c.Set(middleware.ContextAuditActionType, actionType) | ||||||
|  | 		c.Set(middleware.ContextAuditDescription, description) | ||||||
|  | 		c.Set(middleware.ContextAuditTargetResource, targetResource) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // SendSuccessWithAudit 发送成功的响应,并设置审计日志所需的信息。 | ||||||
|  | // 这是控制器中用于记录成功操作并返回响应的首选函数。 | ||||||
|  | func SendSuccessWithAudit(ctx *gin.Context, code ResponseCode, message string, data interface{}, actionType, description string, targetResource interface{}) { | ||||||
|  | 	// 1. 设置审计信息 | ||||||
|  | 	setAuditDetails(ctx, actionType, description, targetResource) | ||||||
|  | 	// 2. 发送响应 | ||||||
|  | 	SendResponse(ctx, code, message, data) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // SendErrorWithAudit 发送失败的响应,并设置审计日志所需的信息。 | ||||||
|  | // 这是控制器中用于记录失败操作并返回响应的首选函数。 | ||||||
|  | func SendErrorWithAudit(ctx *gin.Context, code ResponseCode, message string, actionType, description string, targetResource interface{}) { | ||||||
|  | 	// 1. 设置审计信息 | ||||||
|  | 	setAuditDetails(ctx, actionType, description, targetResource) | ||||||
|  | 	// 2. 发送响应 | ||||||
|  | 	SendErrorResponse(ctx, code, message) | ||||||
|  | } | ||||||
|   | |||||||
							
								
								
									
										98
									
								
								internal/app/middleware/audit.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								internal/app/middleware/audit.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,98 @@ | |||||||
|  | // Package middleware 存放 gin 中间件 | ||||||
|  | package middleware | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"io" | ||||||
|  | 	"strconv" | ||||||
|  |  | ||||||
|  | 	"git.huangwc.com/pig/pig-farm-controller/internal/app/service/audit" | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // --- 审计日志相关上下文键 --- | ||||||
|  | const ( | ||||||
|  | 	ContextAuditActionType     = "auditActionType" | ||||||
|  | 	ContextAuditTargetResource = "auditTargetResource" | ||||||
|  | 	ContextAuditDescription    = "auditDescription" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // AuditLogMiddleware 创建一个Gin中间件,用于在请求结束后记录用户操作审计日志 | ||||||
|  | func AuditLogMiddleware(auditService audit.Service) gin.HandlerFunc { | ||||||
|  | 	return func(c *gin.Context) { | ||||||
|  | 		// 使用自定义的 response body writer 来捕获响应体 | ||||||
|  | 		blw := &bodyLogWriter{body: bytes.NewBufferString(""), ResponseWriter: c.Writer} | ||||||
|  | 		c.Writer = blw | ||||||
|  |  | ||||||
|  | 		// 首先执行请求链中的后续处理程序(即业务控制器) | ||||||
|  | 		c.Next() | ||||||
|  |  | ||||||
|  | 		// --- 在这里,请求已经处理完毕 --- | ||||||
|  |  | ||||||
|  | 		// 从上下文中尝试获取由控制器设置的业务审计信息 | ||||||
|  | 		actionType, exists := c.Get(ContextAuditActionType) | ||||||
|  | 		if !exists { | ||||||
|  | 			// 如果上下文中没有 actionType,说明此接口无需记录审计日志,直接返回 | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// 获取其他审计信息 | ||||||
|  | 		description, _ := c.Get(ContextAuditDescription) | ||||||
|  | 		targetResource, _ := c.Get(ContextAuditTargetResource) | ||||||
|  |  | ||||||
|  | 		// 判断操作状态 | ||||||
|  | 		status := "success" | ||||||
|  | 		resultDetails := "" | ||||||
|  | 		if c.Writer.Status() >= 400 { | ||||||
|  | 			status = "failed" | ||||||
|  | 			// 尝试从捕获的响应体中解析错误信息 | ||||||
|  | 			var errResp struct { | ||||||
|  | 				Error string `json:"error"` | ||||||
|  | 			} | ||||||
|  | 			if err := json.Unmarshal(blw.body.Bytes(), &errResp); err == nil { | ||||||
|  | 				resultDetails = errResp.Error | ||||||
|  | 			} else { | ||||||
|  | 				resultDetails = "HTTP Status: " + strconv.Itoa(c.Writer.Status()) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// 调用审计服务记录日志(异步) | ||||||
|  | 		auditService.LogAction( | ||||||
|  | 			c, | ||||||
|  | 			actionType.(string), | ||||||
|  | 			description.(string), | ||||||
|  | 			targetResource, | ||||||
|  | 			status, | ||||||
|  | 			resultDetails, | ||||||
|  | 		) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // bodyLogWriter 是一个自定义的 gin.ResponseWriter,用于捕获响应体 | ||||||
|  | // 这对于在操作失败时记录详细的错误信息非常有用 | ||||||
|  | type bodyLogWriter struct { | ||||||
|  | 	gin.ResponseWriter | ||||||
|  | 	body *bytes.Buffer | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (w bodyLogWriter) Write(b []byte) (int, error) { | ||||||
|  | 	w.body.Write(b) | ||||||
|  | 	return w.ResponseWriter.Write(b) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (w bodyLogWriter) WriteString(s string) (int, error) { | ||||||
|  | 	w.body.WriteString(s) | ||||||
|  | 	return w.ResponseWriter.WriteString(s) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ReadBody 用于安全地读取请求体,并防止其被重复读取 | ||||||
|  | func ReadBody(c *gin.Context) ([]byte, error) { | ||||||
|  | 	bodyBytes, err := io.ReadAll(c.Request.Body) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	// 将读取的内容放回 Body 中,以便后续的处理函数可以再次读取 | ||||||
|  | 	c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) | ||||||
|  | 	return bodyBytes, nil | ||||||
|  | } | ||||||
| @@ -5,18 +5,16 @@ import ( | |||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
|  | 	"git.huangwc.com/pig/pig-farm-controller/internal/app/service/audit" | ||||||
| 	"git.huangwc.com/pig/pig-farm-controller/internal/app/service/token" | 	"git.huangwc.com/pig/pig-farm-controller/internal/app/service/token" | ||||||
|  | 	"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| ) | 	"gorm.io/gorm" | ||||||
|  |  | ||||||
| const ( |  | ||||||
| 	// ContextUserIDKey 是存储在 gin.Context 中的用户ID的键名 |  | ||||||
| 	ContextUserIDKey = "userID" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // AuthMiddleware 创建一个Gin中间件,用于JWT身份验证 | // AuthMiddleware 创建一个Gin中间件,用于JWT身份验证 | ||||||
| // 它依赖于 TokenService 来解析和验证 token | // 它依赖于 TokenService 来解析和验证 token,并使用 UserRepository 来获取完整的用户信息 | ||||||
| func AuthMiddleware(tokenService token.TokenService) gin.HandlerFunc { | func AuthMiddleware(tokenService token.TokenService, userRepo repository.UserRepository) gin.HandlerFunc { | ||||||
| 	return func(c *gin.Context) { | 	return func(c *gin.Context) { | ||||||
| 		// 从 Authorization header 获取 token | 		// 从 Authorization header 获取 token | ||||||
| 		authHeader := c.GetHeader("Authorization") | 		authHeader := c.GetHeader("Authorization") | ||||||
| @@ -41,8 +39,21 @@ func AuthMiddleware(tokenService token.TokenService) gin.HandlerFunc { | |||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// 将解析出的用户ID存储在 context 中,以便后续的处理函数使用 | 		// 根据 token 中的用户ID,从数据库中获取完整的用户信息 | ||||||
| 		c.Set(ContextUserIDKey, claims.UserID) | 		user, err := userRepo.FindByID(claims.UserID) | ||||||
|  | 		if err != nil { | ||||||
|  | 			if err == gorm.ErrRecordNotFound { | ||||||
|  | 				// Token有效,但对应的用户已不存在 | ||||||
|  | 				c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "授权用户不存在"}) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 			// 其他数据库查询错误 | ||||||
|  | 			c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "获取用户信息失败"}) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// 将完整的用户对象存储在 context 中,以便后续的处理函数使用 | ||||||
|  | 		c.Set(audit.ContextUserKey, user) | ||||||
|  |  | ||||||
| 		// 继续处理请求链中的下一个处理程序 | 		// 继续处理请求链中的下一个处理程序 | ||||||
| 		c.Next() | 		c.Next() | ||||||
|   | |||||||
							
								
								
									
										83
									
								
								internal/app/service/audit/service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								internal/app/service/audit/service.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,83 @@ | |||||||
|  | // Package audit 提供了用户操作审计相关的功能 | ||||||
|  | package audit | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"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/repository" | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"gorm.io/datatypes" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	// ContextUserKey 是存储在 gin.Context 中的用户对象的键名 | ||||||
|  | 	ContextUserKey = "user" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // Service 定义了审计服务的接口 | ||||||
|  | type Service interface { | ||||||
|  | 	LogAction(c *gin.Context, actionType, description string, targetResource interface{}, status string, resultDetails string) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // service 是 Service 接口的实现 | ||||||
|  | type service struct { | ||||||
|  | 	userActionLogRepository repository.UserActionLogRepository | ||||||
|  | 	logger                  *logs.Logger | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // NewService 创建一个新的审计服务实例 | ||||||
|  | func NewService(repo repository.UserActionLogRepository, logger *logs.Logger) Service { | ||||||
|  | 	return &service{userActionLogRepository: repo, logger: logger} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // LogAction 记录一个用户操作。它在一个新的 goroutine 中异步执行,以避免阻塞主请求。 | ||||||
|  | func (s *service) LogAction(c *gin.Context, actionType, description string, targetResource interface{}, status string, resultDetails string) { | ||||||
|  | 	// 从 context 中获取预加载的用户信息 | ||||||
|  | 	userCtx, exists := c.Get(ContextUserKey) | ||||||
|  | 	if !exists { | ||||||
|  | 		// 如果上下文中没有用户信息(例如,在未认证的路由上调用了此函数),则不记录日志 | ||||||
|  | 		s.logger.Warnw("无法记录审计日志:上下文中缺少用户信息") | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	user, ok := userCtx.(*models.User) | ||||||
|  | 	if !ok { | ||||||
|  | 		s.logger.Errorw("无法记录审计日志:上下文中的用户对象类型不正确") | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 将 targetResource 转换为 JSONB,如果失败则记录错误但继续 | ||||||
|  | 	var targetResourceJSON datatypes.JSON | ||||||
|  | 	if targetResource != nil { | ||||||
|  | 		bytes, err := json.Marshal(targetResource) | ||||||
|  | 		if err != nil { | ||||||
|  | 			s.logger.Errorw("无法记录审计日志:序列化 targetResource 失败", "error", err) | ||||||
|  | 		} else { | ||||||
|  | 			targetResourceJSON = bytes | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	log := &models.UserActionLog{ | ||||||
|  | 		Time:           time.Now(), | ||||||
|  | 		UserID:         user.ID, | ||||||
|  | 		Username:       user.Username, // 用户名快照 | ||||||
|  | 		SourceIP:       c.ClientIP(), | ||||||
|  | 		ActionType:     actionType, | ||||||
|  | 		TargetResource: targetResourceJSON, | ||||||
|  | 		Description:    description, | ||||||
|  | 		Status:         status, | ||||||
|  | 		HTTPPath:       c.Request.URL.Path, | ||||||
|  | 		HTTPMethod:     c.Request.Method, | ||||||
|  | 		ResultDetails:  resultDetails, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 异步写入数据库,不阻塞当前请求 | ||||||
|  | 	go func() { | ||||||
|  | 		if err := s.userActionLogRepository.Create(log); err != nil { | ||||||
|  | 			s.logger.Errorw("异步保存审计日志失败", "error", err) | ||||||
|  | 		} | ||||||
|  | 	}() | ||||||
|  | } | ||||||
| @@ -8,6 +8,7 @@ import ( | |||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"git.huangwc.com/pig/pig-farm-controller/internal/app/api" | 	"git.huangwc.com/pig/pig-farm-controller/internal/app/api" | ||||||
|  | 	"git.huangwc.com/pig/pig-farm-controller/internal/app/service/audit" | ||||||
| 	"git.huangwc.com/pig/pig-farm-controller/internal/app/service/device" | 	"git.huangwc.com/pig/pig-farm-controller/internal/app/service/device" | ||||||
| 	"git.huangwc.com/pig/pig-farm-controller/internal/app/service/task" | 	"git.huangwc.com/pig/pig-farm-controller/internal/app/service/task" | ||||||
| 	"git.huangwc.com/pig/pig-farm-controller/internal/app/service/token" | 	"git.huangwc.com/pig/pig-farm-controller/internal/app/service/token" | ||||||
| @@ -81,6 +82,12 @@ func NewApplication(configPath string) (*Application, error) { | |||||||
| 	// 初始化待采集请求仓库 | 	// 初始化待采集请求仓库 | ||||||
| 	pendingCollectionRepo := repository.NewGormPendingCollectionRepository(storage.GetDB()) | 	pendingCollectionRepo := repository.NewGormPendingCollectionRepository(storage.GetDB()) | ||||||
|  |  | ||||||
|  | 	// 初始化审计日志仓库 | ||||||
|  | 	userActionLogRepo := repository.NewGormUserActionLogRepository(storage.GetDB()) | ||||||
|  |  | ||||||
|  | 	// 初始化审计服务 | ||||||
|  | 	auditService := audit.NewService(userActionLogRepo, logger) | ||||||
|  |  | ||||||
| 	// 初始化设备上行监听器 | 	// 初始化设备上行监听器 | ||||||
| 	listenHandler := transport.NewChirpStackListener(logger, sensorDataRepo, deviceRepo, deviceCommandLogRepo, pendingCollectionRepo) | 	listenHandler := transport.NewChirpStackListener(logger, sensorDataRepo, deviceRepo, deviceCommandLogRepo, pendingCollectionRepo) | ||||||
|  |  | ||||||
| @@ -121,6 +128,7 @@ func NewApplication(configPath string) (*Application, error) { | |||||||
| 		deviceRepo, | 		deviceRepo, | ||||||
| 		planRepo, | 		planRepo, | ||||||
| 		tokenService, | 		tokenService, | ||||||
|  | 		auditService, | ||||||
| 		listenHandler, | 		listenHandler, | ||||||
| 		analysisPlanTaskManager, | 		analysisPlanTaskManager, | ||||||
| 	) | 	) | ||||||
|   | |||||||
| @@ -159,6 +159,7 @@ func (ps *PostgresStorage) creatingHyperTable() error { | |||||||
| 		{models.PlanExecutionLog{}, "started_at"}, | 		{models.PlanExecutionLog{}, "started_at"}, | ||||||
| 		{models.TaskExecutionLog{}, "started_at"}, | 		{models.TaskExecutionLog{}, "started_at"}, | ||||||
| 		{models.PendingCollection{}, "created_at"}, | 		{models.PendingCollection{}, "created_at"}, | ||||||
|  | 		{models.UserActionLog{}, "time"}, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	for _, table := range tablesToConvert { | 	for _, table := range tablesToConvert { | ||||||
| @@ -187,6 +188,7 @@ func (ps *PostgresStorage) applyCompressionPolicies() error { | |||||||
| 		{models.PlanExecutionLog{}, "plan_id"}, | 		{models.PlanExecutionLog{}, "plan_id"}, | ||||||
| 		{models.TaskExecutionLog{}, "task_id"}, | 		{models.TaskExecutionLog{}, "task_id"}, | ||||||
| 		{models.PendingCollection{}, "device_id"}, | 		{models.PendingCollection{}, "device_id"}, | ||||||
|  | 		{models.UserActionLog{}, "user_id"}, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	for _, policy := range policies { | 	for _, policy := range policies { | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ package models | |||||||
| import ( | import ( | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
|  | 	"gorm.io/datatypes" | ||||||
| 	"gorm.io/gorm" | 	"gorm.io/gorm" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -140,3 +141,32 @@ type PendingCollection struct { | |||||||
| func (PendingCollection) TableName() string { | func (PendingCollection) TableName() string { | ||||||
| 	return "pending_collections" | 	return "pending_collections" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // --- 用户审计日志 --- | ||||||
|  |  | ||||||
|  | // UserActionLog 记录用户的操作历史,用于审计 | ||||||
|  | type UserActionLog struct { | ||||||
|  | 	// Time 是操作发生的时间,作为主键和超表的时间分区键 | ||||||
|  | 	Time time.Time `gorm:"primaryKey" json:"time"` | ||||||
|  |  | ||||||
|  | 	// --- Who (谁) --- | ||||||
|  | 	UserID   uint   `gorm:"index" json:"user_id,omitempty"` | ||||||
|  | 	Username string `json:"username,omitempty"` // 操作发生时用户名的快照 | ||||||
|  |  | ||||||
|  | 	// --- Where (何地) --- | ||||||
|  | 	SourceIP string `json:"source_ip,omitempty"` | ||||||
|  |  | ||||||
|  | 	// --- What (什么) & How (如何) --- | ||||||
|  | 	ActionType     string         `gorm:"index" json:"action_type,omitempty"`          // 标准化的操作类型,如 "CREATE_DEVICE" | ||||||
|  | 	TargetResource datatypes.JSON `gorm:"type:jsonb" json:"target_resource,omitempty"` // 被操作的资源, e.g., {"type": "device", "id": 123} | ||||||
|  | 	Description    string         `json:"description,omitempty"`                       // 人类可读的操作描述 | ||||||
|  | 	Status         string         `json:"status,omitempty"`                            // success 或 failed | ||||||
|  | 	HTTPPath       string         `json:"http_path,omitempty"`                         // 请求的API路径 | ||||||
|  | 	HTTPMethod     string         `json:"http_method,omitempty"`                       // 请求的HTTP方法 | ||||||
|  | 	ResultDetails  string         `json:"result_details,omitempty"`                    // 结果详情,如失败时的错误信息 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // TableName 自定义 GORM 使用的数据库表名 | ||||||
|  | func (UserActionLog) TableName() string { | ||||||
|  | 	return "user_action_logs" | ||||||
|  | } | ||||||
|   | |||||||
							
								
								
									
										27
									
								
								internal/infra/repository/user_action_log_repository.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								internal/infra/repository/user_action_log_repository.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | // Package repository 提供了数据访问的仓库实现 | ||||||
|  | package repository | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"git.huangwc.com/pig/pig-farm-controller/internal/infra/models" | ||||||
|  | 	"gorm.io/gorm" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // UserActionLogRepository 定义了与用户操作日志相关的数据库操作接口 | ||||||
|  | type UserActionLogRepository interface { | ||||||
|  | 	Create(log *models.UserActionLog) error | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // gormUserActionLogRepository 是 UserActionLogRepository 的 GORM 实现 | ||||||
|  | type gormUserActionLogRepository struct { | ||||||
|  | 	db *gorm.DB | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // NewGormUserActionLogRepository 创建一个新的 UserActionLogRepository GORM 实现实例 | ||||||
|  | func NewGormUserActionLogRepository(db *gorm.DB) UserActionLogRepository { | ||||||
|  | 	return &gormUserActionLogRepository{db: db} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Create 创建一条新的用户操作日志记录 | ||||||
|  | func (r *gormUserActionLogRepository) Create(log *models.UserActionLog) error { | ||||||
|  | 	return r.db.Create(log).Error | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user