Files
user-system/internal/repository/operation_log.go
long-agent 5ca3633be4 feat: 系统全面优化 - 设备管理/登录日志导出/性能监控/设置页面
后端:
- 新增全局设备管理 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 个测试包)
2026-04-07 12:08:16 +08:00

140 lines
4.8 KiB
Go

package repository
import (
"context"
"time"
"gorm.io/gorm"
"github.com/user-management-system/internal/domain"
"github.com/user-management-system/internal/pagination"
)
// OperationLogRepository 操作日志仓储
type OperationLogRepository struct {
db *gorm.DB
}
// NewOperationLogRepository 创建操作日志仓储
func NewOperationLogRepository(db *gorm.DB) *OperationLogRepository {
return &OperationLogRepository{db: db}
}
// Create 创建操作日志
func (r *OperationLogRepository) Create(ctx context.Context, log *domain.OperationLog) error {
return r.db.WithContext(ctx).Create(log).Error
}
// GetByID 根据ID获取操作日志
func (r *OperationLogRepository) GetByID(ctx context.Context, id int64) (*domain.OperationLog, error) {
var log domain.OperationLog
if err := r.db.WithContext(ctx).First(&log, id).Error; err != nil {
return nil, err
}
return &log, nil
}
// ListByUserID 获取用户的操作日志列表
func (r *OperationLogRepository) ListByUserID(ctx context.Context, userID int64, offset, limit int) ([]*domain.OperationLog, int64, error) {
var logs []*domain.OperationLog
var total int64
query := r.db.WithContext(ctx).Model(&domain.OperationLog{}).Where("user_id = ?", userID)
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
if err := query.Order("created_at DESC").Offset(offset).Limit(limit).Find(&logs).Error; err != nil {
return nil, 0, err
}
return logs, total, nil
}
// List 获取操作日志列表(管理员用)
func (r *OperationLogRepository) List(ctx context.Context, offset, limit int) ([]*domain.OperationLog, int64, error) {
var logs []*domain.OperationLog
var total int64
query := r.db.WithContext(ctx).Model(&domain.OperationLog{})
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
if err := query.Order("created_at DESC").Offset(offset).Limit(limit).Find(&logs).Error; err != nil {
return nil, 0, err
}
return logs, total, nil
}
// ListByMethod 按HTTP方法查询操作日志
func (r *OperationLogRepository) ListByMethod(ctx context.Context, method string, offset, limit int) ([]*domain.OperationLog, int64, error) {
var logs []*domain.OperationLog
var total int64
query := r.db.WithContext(ctx).Model(&domain.OperationLog{}).Where("request_method = ?", method)
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
if err := query.Order("created_at DESC").Offset(offset).Limit(limit).Find(&logs).Error; err != nil {
return nil, 0, err
}
return logs, total, nil
}
// ListByTimeRange 按时间范围查询操作日志
func (r *OperationLogRepository) ListByTimeRange(ctx context.Context, start, end time.Time, offset, limit int) ([]*domain.OperationLog, int64, error) {
var logs []*domain.OperationLog
var total int64
query := r.db.WithContext(ctx).Model(&domain.OperationLog{}).
Where("created_at >= ? AND created_at <= ?", start, end)
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
if err := query.Order("created_at DESC").Offset(offset).Limit(limit).Find(&logs).Error; err != nil {
return nil, 0, err
}
return logs, total, nil
}
// DeleteOlderThan 删除指定天数前的日志
func (r *OperationLogRepository) DeleteOlderThan(ctx context.Context, days int) error {
cutoff := time.Now().AddDate(0, 0, -days)
return r.db.WithContext(ctx).Where("created_at < ?", cutoff).Delete(&domain.OperationLog{}).Error
}
// Search 按关键词搜索操作日志
func (r *OperationLogRepository) Search(ctx context.Context, keyword string, offset, limit int) ([]*domain.OperationLog, int64, error) {
var logs []*domain.OperationLog
var total int64
query := r.db.WithContext(ctx).Model(&domain.OperationLog{}).
Where("operation_name LIKE ? OR request_path LIKE ? OR operation_type LIKE ?",
"%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%")
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
if err := query.Order("created_at DESC").Offset(offset).Limit(limit).Find(&logs).Error; err != nil {
return nil, 0, err
}
return logs, total, nil
}
// ListCursor 游标分页查询操作日志(管理员用)
// Uses keyset pagination: WHERE (created_at < ? OR (created_at = ? AND id < ?))
func (r *OperationLogRepository) ListCursor(ctx context.Context, limit int, cursor *pagination.Cursor) ([]*domain.OperationLog, bool, error) {
var logs []*domain.OperationLog
query := r.db.WithContext(ctx).Model(&domain.OperationLog{})
if cursor != nil && cursor.LastID > 0 {
query = query.Where(
"(created_at < ? OR (created_at = ? AND id < ?))",
cursor.LastValue, cursor.LastValue, cursor.LastID,
)
}
if err := query.Order("created_at DESC, id DESC").Limit(limit + 1).Find(&logs).Error; err != nil {
return nil, false, err
}
hasMore := len(logs) > limit
if hasMore {
logs = logs[:limit]
}
return logs, hasMore, nil
}