后端: - 新增全局设备管理 API(DeviceHandler.GetAllDevices) - 新增登录日志导出功能(LogHandler.ExportLoginLogs, CSV/XLSX) - 新增设置服务(SettingsService)和设置页面 API - 设备管理支持多条件筛选(状态/信任状态/关键词) - 登录日志支持流式导出防 OOM - 操作日志支持按方法/时间范围搜索 - 主题配置服务(ThemeService) - 增强监控健康检查(Prometheus metrics + SLO) - 移除旧 ratelimit.go(已迁移至 robustness) - 修复 SocialAccount NULL 扫描问题 - 新增 API 契约测试、Handler 测试、Settings 测试 前端: - 新增管理员设备管理页面(DevicesPage) - 新增管理员登录日志导出功能 - 新增系统设置页面(SettingsPage) - 设备管理支持筛选和分页 - 增强 HTTP 响应类型 测试: - 业务逻辑测试 68 个(含并发 CONC_001~003) - 规模测试 16 个(P99 百分位统计) - E2E 测试、集成测试、契约测试 - 性能基准测试、鲁棒性测试 全面测试通过(38 个测试包)
156 lines
4.6 KiB
Go
156 lines
4.6 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/user-management-system/internal/domain"
|
|
"github.com/user-management-system/internal/pagination"
|
|
"github.com/user-management-system/internal/repository"
|
|
)
|
|
|
|
// OperationLogService 操作日志服务
|
|
type OperationLogService struct {
|
|
operationLogRepo *repository.OperationLogRepository
|
|
}
|
|
|
|
// NewOperationLogService 创建操作日志服务
|
|
func NewOperationLogService(operationLogRepo *repository.OperationLogRepository) *OperationLogService {
|
|
return &OperationLogService{operationLogRepo: operationLogRepo}
|
|
}
|
|
|
|
// RecordOperation 记录操作日志
|
|
func (s *OperationLogService) RecordOperation(ctx context.Context, req *RecordOperationRequest) error {
|
|
log := &domain.OperationLog{
|
|
OperationType: req.OperationType,
|
|
OperationName: req.OperationName,
|
|
RequestMethod: req.RequestMethod,
|
|
RequestPath: req.RequestPath,
|
|
RequestParams: req.RequestParams,
|
|
ResponseStatus: req.ResponseStatus,
|
|
IP: req.IP,
|
|
UserAgent: req.UserAgent,
|
|
}
|
|
if req.UserID != 0 {
|
|
log.UserID = &req.UserID
|
|
}
|
|
return s.operationLogRepo.Create(ctx, log)
|
|
}
|
|
|
|
// RecordOperationRequest 记录操作请求
|
|
type RecordOperationRequest struct {
|
|
UserID int64 `json:"user_id"`
|
|
OperationType string `json:"operation_type"`
|
|
OperationName string `json:"operation_name"`
|
|
RequestMethod string `json:"request_method"`
|
|
RequestPath string `json:"request_path"`
|
|
RequestParams string `json:"request_params"`
|
|
ResponseStatus int `json:"response_status"`
|
|
IP string `json:"ip"`
|
|
UserAgent string `json:"user_agent"`
|
|
}
|
|
|
|
// ListOperationLogRequest 操作日志列表请求
|
|
type ListOperationLogRequest struct {
|
|
UserID int64 `json:"user_id" form:"user_id"`
|
|
Method string `json:"method" form:"method"`
|
|
Keyword string `json:"keyword" form:"keyword"`
|
|
Page int `json:"page" form:"page"`
|
|
PageSize int `json:"page_size" form:"page_size"`
|
|
StartAt string `json:"start_at" form:"start_at"`
|
|
EndAt string `json:"end_at" form:"end_at"`
|
|
Cursor string `form:"cursor"` // Opaque cursor for keyset pagination
|
|
Size int `form:"size"` // Page size when using cursor mode
|
|
}
|
|
|
|
// GetOperationLogs 获取操作日志列表
|
|
func (s *OperationLogService) GetOperationLogs(ctx context.Context, req *ListOperationLogRequest) ([]*domain.OperationLog, int64, error) {
|
|
if req.Page <= 0 {
|
|
req.Page = 1
|
|
}
|
|
if req.PageSize <= 0 {
|
|
req.PageSize = 20
|
|
}
|
|
offset := (req.Page - 1) * req.PageSize
|
|
|
|
// 按关键词搜索
|
|
if req.Keyword != "" {
|
|
return s.operationLogRepo.Search(ctx, req.Keyword, offset, req.PageSize)
|
|
}
|
|
|
|
// 按用户 ID 查询
|
|
if req.UserID > 0 {
|
|
return s.operationLogRepo.ListByUserID(ctx, req.UserID, offset, req.PageSize)
|
|
}
|
|
|
|
// 按 HTTP 方法查询
|
|
if req.Method != "" {
|
|
return s.operationLogRepo.ListByMethod(ctx, req.Method, offset, req.PageSize)
|
|
}
|
|
|
|
// 按时间范围查询
|
|
if req.StartAt != "" && req.EndAt != "" {
|
|
start, err1 := time.Parse(time.RFC3339, req.StartAt)
|
|
end, err2 := time.Parse(time.RFC3339, req.EndAt)
|
|
if err1 == nil && err2 == nil {
|
|
return s.operationLogRepo.ListByTimeRange(ctx, start, end, offset, req.PageSize)
|
|
}
|
|
}
|
|
|
|
return s.operationLogRepo.List(ctx, offset, req.PageSize)
|
|
}
|
|
|
|
// GetOperationLogsCursor 游标分页获取操作日志列表(推荐使用)
|
|
func (s *OperationLogService) GetOperationLogsCursor(ctx context.Context, req *ListOperationLogRequest) (*CursorResult, error) {
|
|
size := pagination.ClampPageSize(req.Size)
|
|
|
|
cursor, err := pagination.Decode(req.Cursor)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid cursor: %w", err)
|
|
}
|
|
|
|
var items interface{}
|
|
var hasMore bool
|
|
|
|
logs, hm, err := s.operationLogRepo.ListCursor(ctx, size, cursor)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
items = logs
|
|
hasMore = hm
|
|
|
|
nextCursor := ""
|
|
switch items := items.(type) {
|
|
case []*domain.OperationLog:
|
|
if len(items) > 0 {
|
|
last := items[len(items)-1]
|
|
nextCursor = pagination.BuildNextCursor(last.ID, last.CreatedAt)
|
|
}
|
|
}
|
|
|
|
return &CursorResult{
|
|
Items: items,
|
|
NextCursor: nextCursor,
|
|
HasMore: hasMore,
|
|
PageSize: size,
|
|
}, nil
|
|
}
|
|
|
|
// GetMyOperationLogs 获取当前用户的操作日志
|
|
func (s *OperationLogService) GetMyOperationLogs(ctx context.Context, userID int64, page, pageSize int) ([]*domain.OperationLog, int64, error) {
|
|
if page <= 0 {
|
|
page = 1
|
|
}
|
|
if pageSize <= 0 {
|
|
pageSize = 20
|
|
}
|
|
offset := (page - 1) * pageSize
|
|
return s.operationLogRepo.ListByUserID(ctx, userID, offset, pageSize)
|
|
}
|
|
|
|
// CleanupOldLogs 清理旧日志(保留最近 N 天)
|
|
func (s *OperationLogService) CleanupOldLogs(ctx context.Context, retentionDays int) error {
|
|
return s.operationLogRepo.DeleteOlderThan(ctx, retentionDays)
|
|
}
|