diff --git a/internal/app/api/api.go b/internal/app/api/api.go index 3fea745..a9e994a 100644 --- a/internal/app/api/api.go +++ b/internal/app/api/api.go @@ -56,6 +56,7 @@ func NewAPI(cfg config.ServerConfig, userRepo repository.UserRepository, deviceRepository repository.DeviceRepository, planRepository repository.PlanRepository, + userActionLogRepository repository.UserActionLogRepository, tokenService token.TokenService, auditService audit.Service, // 注入审计服务 listenHandler transport.ListenHandler, @@ -82,7 +83,7 @@ func NewAPI(cfg config.ServerConfig, config: cfg, listenHandler: listenHandler, // 在 NewAPI 中初始化用户控制器,并将其作为 API 结构体的成员 - userController: user.NewController(userRepo, logger, tokenService), + userController: user.NewController(userRepo, userActionLogRepository, logger, tokenService), // 在 NewAPI 中初始化设备控制器,并将其作为 API 结构体的成员 deviceController: device.NewController(deviceRepository, logger), // 在 NewAPI 中初始化计划控制器,并将其作为 API 结构体的成员 @@ -140,6 +141,13 @@ func (a *API) setupRoutes() { authGroup.Use(middleware.AuthMiddleware(a.tokenService, a.userRepo)) // 1. 身份认证 authGroup.Use(middleware.AuditLogMiddleware(a.auditService)) // 2. 审计日志 { + // 用户相关路由组 + userGroup := authGroup.Group("/users") + { + userGroup.GET("/:id/history", a.userController.ListUserHistory) + } + a.logger.Info("用户相关接口注册成功 (需要认证和审计)") + // 设备相关路由组 deviceGroup := authGroup.Group("/devices") { diff --git a/internal/app/controller/user/user_controller.go b/internal/app/controller/user/user_controller.go index 9e13a17..2fcaebb 100644 --- a/internal/app/controller/user/user_controller.go +++ b/internal/app/controller/user/user_controller.go @@ -1,6 +1,9 @@ package user import ( + "strconv" + "time" + "git.huangwc.com/pig/pig-farm-controller/internal/app/controller" "git.huangwc.com/pig/pig-farm-controller/internal/app/service/token" "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" @@ -13,19 +16,23 @@ import ( // Controller 用户控制器 type Controller struct { userRepo repository.UserRepository + auditRepo repository.UserActionLogRepository logger *logs.Logger tokenService token.TokenService // 注入 token 服务 } // NewController 创建用户控制器实例 -func NewController(userRepo repository.UserRepository, logger *logs.Logger, tokenService token.TokenService) *Controller { +func NewController(userRepo repository.UserRepository, auditRepo repository.UserActionLogRepository, logger *logs.Logger, tokenService token.TokenService) *Controller { return &Controller{ userRepo: userRepo, + auditRepo: auditRepo, logger: logger, tokenService: tokenService, } } +// --- DTOs --- + // CreateUserRequest 定义创建用户请求的结构体 type CreateUserRequest struct { Username string `json:"username" binding:"required" example:"newuser"` @@ -52,6 +59,24 @@ type LoginResponse struct { Token string `json:"token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."` } +// HistoryResponse 定义单条操作历史的响应结构体 +type HistoryResponse struct { + UserID uint `json:"user_id" example:"101"` + Username string `json:"username" example:"testuser"` + ActionType string `json:"action_type" example:"更新设备"` + Description string `json:"description" example:"设备更新成功"` + TargetResource interface{} `json:"target_resource"` + Time string `json:"time"` +} + +// ListHistoryResponse 定义操作历史列表的响应结构体 +type ListHistoryResponse struct { + History []HistoryResponse `json:"history"` + Total int64 `json:"total" example:"100"` +} + +// --- Controller Methods --- + // CreateUser godoc // @Summary 创建新用户 // @Description 根据用户名和密码创建一个新的系统用户。 @@ -143,3 +168,76 @@ func (c *Controller) Login(ctx *gin.Context) { Token: tokenString, }) } + +// ListUserHistory godoc +// @Summary 获取指定用户的操作历史 +// @Description 根据用户ID,分页获取该用户的操作审计日志。 +// @Tags 用户管理 +// @Produce json +// @Param id path int true "用户ID" +// @Param page query int false "页码" default(1) +// @Param page_size query int false "每页大小" default(10) +// @Param action_type query string false "按操作类型过滤" +// @Success 200 {object} controller.Response{data=user.ListHistoryResponse} "业务码为200代表成功获取" +// @Router /api/v1/users/{id}/history [get] +func (c *Controller) ListUserHistory(ctx *gin.Context) { + const actionType = "获取用户操作历史" + + // 1. 解析路径中的用户ID + userIDStr := ctx.Param("id") + userID, err := strconv.ParseUint(userIDStr, 10, 64) + if err != nil { + c.logger.Errorf("%s: 无效的用户ID格式: %v, ID: %s", actionType, err, userIDStr) + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的用户ID格式", actionType, "无效的用户ID格式", userIDStr) + return + } + + // 2. 解析分页和过滤参数 + page, _ := strconv.Atoi(ctx.DefaultQuery("page", "1")) + pageSize, _ := strconv.Atoi(ctx.DefaultQuery("page_size", "10")) + actionTypeFilter := ctx.Query("action_type") + + // 确保分页参数有效 + if page <= 0 { + page = 1 + } + if pageSize <= 0 || pageSize > 100 { + pageSize = 10 + } + + // 3. 调用审计仓库层获取历史数据 + id := uint(userID) + findOptions := repository.FindAuditLogOptions{ + UserID: &id, + ActionType: actionTypeFilter, + Page: page, + PageSize: pageSize, + } + logs, total, err := c.auditRepo.List(findOptions) + if err != nil { + c.logger.Errorf("%s: 查询历史记录失败: %v, Options: %+v", actionType, err, findOptions) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "查询历史记录失败", actionType, "查询历史记录失败", findOptions) + return + } + + // 4. 将数据库模型转换为响应 DTO + historyResponses := make([]HistoryResponse, 0, len(logs)) + for _, log := range logs { + historyResponses = append(historyResponses, HistoryResponse{ + UserID: log.UserID, + Username: log.Username, + ActionType: log.ActionType, + Description: log.Description, + TargetResource: log.TargetResource, + Time: log.Time.Format(time.RFC3339), + }) + } + + // 5. 发送成功响应 + resp := ListHistoryResponse{ + History: historyResponses, + Total: total, + } + c.logger.Infof("%s: 成功获取用户 %d 的操作历史, 数量: %d", actionType, userID, len(historyResponses)) + controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取用户操作历史成功", resp, actionType, "获取用户操作历史成功", resp) +} diff --git a/internal/core/application.go b/internal/core/application.go index 7fed25f..7244a69 100644 --- a/internal/core/application.go +++ b/internal/core/application.go @@ -127,6 +127,7 @@ func NewApplication(configPath string) (*Application, error) { userRepo, deviceRepo, planRepo, + userActionLogRepo, tokenService, auditService, listenHandler, diff --git a/internal/infra/repository/user_action_log_repository.go b/internal/infra/repository/user_action_log_repository.go index cfaac10..1456c9b 100644 --- a/internal/infra/repository/user_action_log_repository.go +++ b/internal/infra/repository/user_action_log_repository.go @@ -1,4 +1,3 @@ -// Package repository 提供了数据访问的仓库实现 package repository import ( @@ -6,9 +5,18 @@ import ( "gorm.io/gorm" ) +// FindAuditLogOptions 定义了查询审计日志的选项 +type FindAuditLogOptions struct { + UserID *uint // 根据用户ID过滤 + ActionType string // 根据操作类型过滤 + Page int // 页码 + PageSize int // 每页大小 +} + // UserActionLogRepository 定义了与用户操作日志相关的数据库操作接口 type UserActionLogRepository interface { Create(log *models.UserActionLog) error + List(options FindAuditLogOptions) ([]*models.UserActionLog, int64, error) } // gormUserActionLogRepository 是 UserActionLogRepository 的 GORM 实现 @@ -25,3 +33,38 @@ func NewGormUserActionLogRepository(db *gorm.DB) UserActionLogRepository { func (r *gormUserActionLogRepository) Create(log *models.UserActionLog) error { return r.db.Create(log).Error } + +// List 根据选项查询用户操作日志,并返回总数 +func (r *gormUserActionLogRepository) List(options FindAuditLogOptions) ([]*models.UserActionLog, int64, error) { + var logs []*models.UserActionLog + var total int64 + + query := r.db.Model(&models.UserActionLog{}) + + if options.UserID != nil { + query = query.Where("user_id = ?", *options.UserID) + } + if options.ActionType != "" { + query = query.Where("action_type = ?", options.ActionType) + } + + // 统计总数 + if err := query.Count(&total).Error; err != nil { + return nil, 0, err + } + + // 分页查询 + if options.Page > 0 && options.PageSize > 0 { + offset := (options.Page - 1) * options.PageSize + query = query.Offset(offset).Limit(options.PageSize) + } + + // 默认按创建时间倒序 + query = query.Order("created_at DESC") + + if err := query.Find(&logs).Error; err != nil { + return nil, 0, err + } + + return logs, total, nil +}