Files
user-system/internal/service/login_log.go

258 lines
6.6 KiB
Go

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
}