初步实现审计
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/user" | ||||
| 	"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/token" | ||||
| 	"git.huangwc.com/pig/pig-farm-controller/internal/app/service/transport" | ||||
| @@ -38,6 +39,7 @@ type API struct { | ||||
| 	logger              *logs.Logger                  // 日志记录器,用于输出日志信息 | ||||
| 	userRepo            repository.UserRepository     // 用户数据仓库接口,用于用户数据操作 | ||||
| 	tokenService        token.TokenService            // Token 服务接口,用于 JWT token 的生成和解析 | ||||
| 	auditService        audit.Service                 // 审计服务,用于记录用户操作 | ||||
| 	httpServer          *http.Server                  // 标准库的 HTTP 服务器实例,用于启动和停止服务 | ||||
| 	config              config.ServerConfig           // API 服务器的配置,使用 infra/config 包中的 ServerConfig | ||||
| 	userController      *user.Controller              // 用户控制器实例 | ||||
| @@ -55,6 +57,7 @@ func NewAPI(cfg config.ServerConfig, | ||||
| 	deviceRepository repository.DeviceRepository, | ||||
| 	planRepository repository.PlanRepository, | ||||
| 	tokenService token.TokenService, | ||||
| 	auditService audit.Service, // 注入审计服务 | ||||
| 	listenHandler transport.ListenHandler, | ||||
| 	analysisTaskManager *task.AnalysisPlanTaskManager) *API { | ||||
| 	// 设置 Gin 模式,例如 gin.ReleaseMode (生产模式) 或 gin.DebugMode (开发模式) | ||||
| @@ -75,6 +78,7 @@ func NewAPI(cfg config.ServerConfig, | ||||
| 		logger:        logger, | ||||
| 		userRepo:      userRepo, | ||||
| 		tokenService:  tokenService, | ||||
| 		auditService:  auditService, | ||||
| 		config:        cfg, | ||||
| 		listenHandler: listenHandler, | ||||
| 		// 在 NewAPI 中初始化用户控制器,并将其作为 API 结构体的成员 | ||||
| @@ -133,7 +137,8 @@ func (a *API) setupRoutes() { | ||||
| 	// --- Authenticated Routes --- | ||||
| 	// 所有在此注册的路由都需要通过 JWT 身份验证 | ||||
| 	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") | ||||
| @@ -144,7 +149,7 @@ func (a *API) setupRoutes() { | ||||
| 			deviceGroup.PUT("/:id", a.deviceController.UpdateDevice) | ||||
| 			deviceGroup.DELETE("/:id", a.deviceController.DeleteDevice) | ||||
| 		} | ||||
| 		a.logger.Info("设备相关接口注册成功 (需要认证)") | ||||
| 		a.logger.Info("设备相关接口注册成功 (需要认证和审计)") | ||||
|  | ||||
| 		// 计划相关路由组 | ||||
| 		planGroup := authGroup.Group("/plans") | ||||
| @@ -157,7 +162,7 @@ func (a *API) setupRoutes() { | ||||
| 			planGroup.POST("/:id/start", a.planController.StartPlan) | ||||
| 			planGroup.POST("/:id/stop", a.planController.StopPlan) | ||||
| 		} | ||||
| 		a.logger.Info("计划相关接口注册成功 (需要认证)") | ||||
| 		a.logger.Info("计划相关接口注册成功 (需要认证和审计)") | ||||
| 	} | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -3,37 +3,40 @@ package controller | ||||
| import ( | ||||
| 	"net/http" | ||||
|  | ||||
| 	"git.huangwc.com/pig/pig-farm-controller/internal/app/middleware" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| ) | ||||
|  | ||||
| // --- 业务状态码 --- | ||||
| type ResponseCode int | ||||
|  | ||||
| const ( | ||||
| 	// 成功状态码 (2000-2999) | ||||
| 	CodeSuccess = 2000 // 操作成功 | ||||
| 	CodeCreated = 2001 // 创建成功 | ||||
| 	CodeSuccess ResponseCode = 2000 // 操作成功 | ||||
| 	CodeCreated ResponseCode = 2001 // 创建成功 | ||||
|  | ||||
| 	// 客户端错误状态码 (4000-4999) | ||||
| 	CodeBadRequest   = 4000 // 请求参数错误 | ||||
| 	CodeUnauthorized = 4001 // 未授权 | ||||
| 	CodeNotFound     = 4004 // 资源未找到 | ||||
| 	CodeConflict     = 4009 // 资源冲突 | ||||
| 	CodeBadRequest   ResponseCode = 4000 // 请求参数错误 | ||||
| 	CodeUnauthorized ResponseCode = 4001 // 未授权 | ||||
| 	CodeNotFound     ResponseCode = 4004 // 资源未找到 | ||||
| 	CodeConflict     ResponseCode = 4009 // 资源冲突 | ||||
|  | ||||
| 	// 服务器错误状态码 (5000-5999) | ||||
| 	CodeInternalError      = 5000 // 服务器内部错误 | ||||
| 	CodeServiceUnavailable = 5003 // 服务不可用 | ||||
| 	CodeInternalError      ResponseCode = 5000 // 服务器内部错误 | ||||
| 	CodeServiceUnavailable ResponseCode = 5003 // 服务不可用 | ||||
| ) | ||||
|  | ||||
| // --- 通用响应结构 --- | ||||
|  | ||||
| // Response 定义统一的API响应结构体 | ||||
| type Response struct { | ||||
| 	Code    int         `json:"code"`    // 业务状态码 | ||||
| 	Message string      `json:"message"` // 提示信息 | ||||
| 	Data    interface{} `json:"data"`    // 业务数据 | ||||
| 	Code    ResponseCode `json:"code"`    // 业务状态码 | ||||
| 	Message string       `json:"message"` // 提示信息 | ||||
| 	Data    interface{}  `json:"data"`    // 业务数据 | ||||
| } | ||||
|  | ||||
| // SendResponse 发送统一格式的JSON响应 | ||||
| func SendResponse(ctx *gin.Context, code int, message string, data interface{}) { | ||||
| // SendResponse 发送统一格式的JSON响应 (基础函数,不带审计) | ||||
| func SendResponse(ctx *gin.Context, code ResponseCode, message string, data interface{}) { | ||||
| 	ctx.JSON(http.StatusOK, Response{ | ||||
| 		Code:    code, | ||||
| 		Message: message, | ||||
| @@ -41,7 +44,37 @@ func SendResponse(ctx *gin.Context, code int, message string, data interface{}) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // SendErrorResponse 发送统一格式的错误响应 | ||||
| func SendErrorResponse(ctx *gin.Context, code int, message string) { | ||||
| // SendErrorResponse 发送统一格式的错误响应 (基础函数,不带审计) | ||||
| func SendErrorResponse(ctx *gin.Context, code ResponseCode, message string) { | ||||
| 	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" | ||||
| 	"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/infra/repository" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	// ContextUserIDKey 是存储在 gin.Context 中的用户ID的键名 | ||||
| 	ContextUserIDKey = "userID" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| // AuthMiddleware 创建一个Gin中间件,用于JWT身份验证 | ||||
| // 它依赖于 TokenService 来解析和验证 token | ||||
| func AuthMiddleware(tokenService token.TokenService) gin.HandlerFunc { | ||||
| // 它依赖于 TokenService 来解析和验证 token,并使用 UserRepository 来获取完整的用户信息 | ||||
| func AuthMiddleware(tokenService token.TokenService, userRepo repository.UserRepository) gin.HandlerFunc { | ||||
| 	return func(c *gin.Context) { | ||||
| 		// 从 Authorization header 获取 token | ||||
| 		authHeader := c.GetHeader("Authorization") | ||||
| @@ -41,8 +39,21 @@ func AuthMiddleware(tokenService token.TokenService) gin.HandlerFunc { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		// 将解析出的用户ID存储在 context 中,以便后续的处理函数使用 | ||||
| 		c.Set(ContextUserIDKey, claims.UserID) | ||||
| 		// 根据 token 中的用户ID,从数据库中获取完整的用户信息 | ||||
| 		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() | ||||
|   | ||||
							
								
								
									
										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" | ||||
|  | ||||
| 	"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/task" | ||||
| 	"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()) | ||||
|  | ||||
| 	// 初始化审计日志仓库 | ||||
| 	userActionLogRepo := repository.NewGormUserActionLogRepository(storage.GetDB()) | ||||
|  | ||||
| 	// 初始化审计服务 | ||||
| 	auditService := audit.NewService(userActionLogRepo, logger) | ||||
|  | ||||
| 	// 初始化设备上行监听器 | ||||
| 	listenHandler := transport.NewChirpStackListener(logger, sensorDataRepo, deviceRepo, deviceCommandLogRepo, pendingCollectionRepo) | ||||
|  | ||||
| @@ -121,6 +128,7 @@ func NewApplication(configPath string) (*Application, error) { | ||||
| 		deviceRepo, | ||||
| 		planRepo, | ||||
| 		tokenService, | ||||
| 		auditService, | ||||
| 		listenHandler, | ||||
| 		analysisPlanTaskManager, | ||||
| 	) | ||||
|   | ||||
| @@ -159,6 +159,7 @@ func (ps *PostgresStorage) creatingHyperTable() error { | ||||
| 		{models.PlanExecutionLog{}, "started_at"}, | ||||
| 		{models.TaskExecutionLog{}, "started_at"}, | ||||
| 		{models.PendingCollection{}, "created_at"}, | ||||
| 		{models.UserActionLog{}, "time"}, | ||||
| 	} | ||||
|  | ||||
| 	for _, table := range tablesToConvert { | ||||
| @@ -187,6 +188,7 @@ func (ps *PostgresStorage) applyCompressionPolicies() error { | ||||
| 		{models.PlanExecutionLog{}, "plan_id"}, | ||||
| 		{models.TaskExecutionLog{}, "task_id"}, | ||||
| 		{models.PendingCollection{}, "device_id"}, | ||||
| 		{models.UserActionLog{}, "user_id"}, | ||||
| 	} | ||||
|  | ||||
| 	for _, policy := range policies { | ||||
|   | ||||
| @@ -3,6 +3,7 @@ package models | ||||
| import ( | ||||
| 	"time" | ||||
|  | ||||
| 	"gorm.io/datatypes" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| @@ -140,3 +141,32 @@ type PendingCollection struct { | ||||
| func (PendingCollection) TableName() string { | ||||
| 	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