package service import ( "bytes" "context" "encoding/csv" "fmt" "time" "github.com/xuri/excelize/v2" "github.com/user-management-system/internal/domain" "github.com/user-management-system/internal/repository" ) // LoginLogService 登录日志服务 type LoginLogService struct { loginLogRepo *repository.LoginLogRepository } // NewLoginLogService 创建登录日志服务 func NewLoginLogService(loginLogRepo *repository.LoginLogRepository) *LoginLogService { return &LoginLogService{loginLogRepo: loginLogRepo} } // RecordLogin 记录登录日志 func (s *LoginLogService) RecordLogin(ctx context.Context, req *RecordLoginRequest) error { log := &domain.LoginLog{ LoginType: req.LoginType, DeviceID: req.DeviceID, IP: req.IP, Location: req.Location, Status: req.Status, FailReason: req.FailReason, } if req.UserID != 0 { log.UserID = &req.UserID } return s.loginLogRepo.Create(ctx, log) } // RecordLoginRequest 记录登录请求 type RecordLoginRequest struct { UserID int64 `json:"user_id"` LoginType int `json:"login_type"` // 1-用户名, 2-邮箱, 3-手机 DeviceID string `json:"device_id"` IP string `json:"ip"` Location string `json:"location"` Status int `json:"status"` // 0-失败, 1-成功 FailReason string `json:"fail_reason"` } // ListLoginLogRequest 登录日志列表请求 type ListLoginLogRequest struct { UserID int64 `json:"user_id"` Status int `json:"status"` Page int `json:"page"` PageSize int `json:"page_size"` StartAt string `json:"start_at"` EndAt string `json:"end_at"` } // GetLoginLogs 获取登录日志列表 func (s *LoginLogService) GetLoginLogs(ctx context.Context, req *ListLoginLogRequest) ([]*domain.LoginLog, int64, error) { if req.Page <= 0 { req.Page = 1 } if req.PageSize <= 0 { req.PageSize = 20 } offset := (req.Page - 1) * req.PageSize // 按用户 ID 查询 if req.UserID > 0 { return s.loginLogRepo.ListByUserID(ctx, req.UserID, 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.loginLogRepo.ListByTimeRange(ctx, start, end, offset, req.PageSize) } } // 按状态查询 if req.Status == 0 || req.Status == 1 { return s.loginLogRepo.ListByStatus(ctx, req.Status, offset, req.PageSize) } return s.loginLogRepo.List(ctx, offset, req.PageSize) } // GetMyLoginLogs 获取当前用户的登录日志 func (s *LoginLogService) GetMyLoginLogs(ctx context.Context, userID int64, page, pageSize int) ([]*domain.LoginLog, int64, error) { if page <= 0 { page = 1 } if pageSize <= 0 { pageSize = 20 } offset := (page - 1) * pageSize return s.loginLogRepo.ListByUserID(ctx, userID, offset, pageSize) } // CleanupOldLogs 清理旧日志(保留最近 N 天) func (s *LoginLogService) CleanupOldLogs(ctx context.Context, retentionDays int) error { return s.loginLogRepo.DeleteOlderThan(ctx, retentionDays) } // ExportLoginLogRequest 导出登录日志请求 type ExportLoginLogRequest struct { UserID int64 `form:"user_id"` Status int `form:"status"` Format string `form:"format"` StartAt string `form:"start_at"` EndAt string `form:"end_at"` } // ExportLoginLogs 导出登录日志 func (s *LoginLogService) ExportLoginLogs(ctx context.Context, req *ExportLoginLogRequest) ([]byte, string, string, error) { format := "csv" if req.Format == "xlsx" { format = "xlsx" } var startAt, endAt *time.Time if req.StartAt != "" { if t, err := time.Parse(time.RFC3339, req.StartAt); err == nil { startAt = &t } } if req.EndAt != "" { if t, err := time.Parse(time.RFC3339, req.EndAt); err == nil { endAt = &t } } logs, err := s.loginLogRepo.ListAllForExport(ctx, req.UserID, req.Status, startAt, endAt) if err != nil { return nil, "", "", fmt.Errorf("查询登录日志失败: %w", err) } filename := fmt.Sprintf("login_logs_%s.%s", time.Now().Format("20060102_150405"), format) if format == "xlsx" { data, err := buildLoginLogXLSXExport(logs) if err != nil { return nil, "", "", err } return data, filename, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", nil } data, err := buildLoginLogCSVExport(logs) if err != nil { return nil, "", "", err } return data, filename, "text/csv; charset=utf-8", nil } func buildLoginLogCSVExport(logs []*domain.LoginLog) ([]byte, error) { headers := []string{"ID", "用户ID", "登录方式", "设备ID", "IP地址", "位置", "状态", "失败原因", "时间"} rows := make([][]string, 0, len(logs)+1) rows = append(rows, headers) for _, log := range logs { rows = append(rows, []string{ fmt.Sprintf("%d", log.ID), fmt.Sprintf("%d", derefInt64(log.UserID)), loginTypeLabel(log.LoginType), log.DeviceID, log.IP, log.Location, loginStatusLabel(log.Status), log.FailReason, log.CreatedAt.Format("2006-01-02 15:04:05"), }) } var buf bytes.Buffer buf.Write([]byte{0xEF, 0xBB, 0xBF}) writer := csv.NewWriter(&buf) if err := writer.WriteAll(rows); err != nil { return nil, fmt.Errorf("写CSV失败: %w", err) } return buf.Bytes(), nil } func buildLoginLogXLSXExport(logs []*domain.LoginLog) ([]byte, error) { file := excelize.NewFile() defer file.Close() sheet := file.GetSheetName(file.GetActiveSheetIndex()) if sheet == "" { sheet = "Sheet1" } headers := []string{"ID", "用户ID", "登录方式", "设备ID", "IP地址", "位置", "状态", "失败原因", "时间"} for idx, header := range headers { cell, _ := excelize.CoordinatesToCellName(idx+1, 1) _ = file.SetCellValue(sheet, cell, header) } for rowIdx, log := range logs { row := []string{ fmt.Sprintf("%d", log.ID), fmt.Sprintf("%d", derefInt64(log.UserID)), loginTypeLabel(log.LoginType), log.DeviceID, log.IP, log.Location, loginStatusLabel(log.Status), log.FailReason, log.CreatedAt.Format("2006-01-02 15:04:05"), } for colIdx, value := range row { cell, _ := excelize.CoordinatesToCellName(colIdx+1, rowIdx+2) _ = file.SetCellValue(sheet, cell, value) } } var buf bytes.Buffer if _, err := file.WriteTo(&buf); err != nil { return nil, fmt.Errorf("生成Excel失败: %w", err) } return buf.Bytes(), nil } func loginTypeLabel(t int) string { switch t { case 1: return "密码登录" case 2: return "邮箱验证码" case 3: return "手机验证码" case 4: return "OAuth" default: return "未知" } } func loginStatusLabel(s int) string { if s == 1 { return "成功" } return "失败" } func derefInt64(v *int64) int64 { if v == nil { return 0 } return *v }