Files
user-system/internal/service/stats.go
long-agent 8095307d82 fix: P0/P1 security and quality fixes
P0-01: Add ESCAPE clause to LIKE queries in operation_log.go and device.go
P0-02: Add atomic Increment to L1Cache and L2Cache interfaces
P0-07: Add TOTP verification step after password login
P1-01: Sanitize error messages in error.go middleware
P1-03: Remove err.Error() from export error messages
P1-04: Add error return to CountByResultSince in login_log.go
P1-05: Add transactional DeleteCascade to RoleRepository
P1-06: Add PasswordChangedAt tracking for JWT token invalidation
P1-07: Wrap theme SetDefault in database transaction
P1-08: Use config values for database pool parameters
P1-09: Add rows.Err() checks in social_account_repo.go
P1-10: Validate sortOrder with map in user.go ORDER BY
P1-11: Add GORM tags to Announcement struct
P1-15: Add pageSize upper limit (100) to device and log handlers
2026-04-18 15:33:12 +08:00

141 lines
4.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"context"
"time"
"github.com/user-management-system/internal/domain"
)
// Interfaces for dependency inversion (DIP) — service layer depends on these abstractions, not concrete types.
type statsUserRepository interface {
List(ctx context.Context, offset, limit int) ([]*domain.User, int64, error)
ListByStatus(ctx context.Context, status domain.UserStatus, offset, limit int) ([]*domain.User, int64, error)
ListCreatedAfter(ctx context.Context, since time.Time, offset, limit int) ([]*domain.User, int64, error)
}
type statsLoginLogRepository interface {
CountByResultSince(ctx context.Context, success bool, since time.Time) (int64, error)
}
// StatsService 统计服务
type StatsService struct {
userRepo statsUserRepository
loginLogRepo statsLoginLogRepository
}
// NewStatsService 创建统计服务
func NewStatsService(
userRepo statsUserRepository,
loginLogRepo statsLoginLogRepository,
) *StatsService {
return &StatsService{
userRepo: userRepo,
loginLogRepo: loginLogRepo,
}
}
// UserStats 用户统计数据
type UserStats struct {
TotalUsers int64 `json:"total_users"`
ActiveUsers int64 `json:"active_users"`
InactiveUsers int64 `json:"inactive_users"`
LockedUsers int64 `json:"locked_users"`
DisabledUsers int64 `json:"disabled_users"`
NewUsersToday int64 `json:"new_users_today"`
NewUsersWeek int64 `json:"new_users_week"`
NewUsersMonth int64 `json:"new_users_month"`
}
// LoginStats 登录统计数据
type LoginStats struct {
LoginsTodaySuccess int64 `json:"logins_today_success"`
LoginsTodayFailed int64 `json:"logins_today_failed"`
LoginsWeek int64 `json:"logins_week"`
}
// DashboardStats 仪表盘综合统计
type DashboardStats struct {
Users UserStats `json:"users"`
Logins LoginStats `json:"logins"`
}
// GetUserStats 获取用户统计
func (s *StatsService) GetUserStats(ctx context.Context) (*UserStats, error) {
stats := &UserStats{}
// 统计总用户数
_, total, err := s.userRepo.List(ctx, 0, 1)
if err != nil {
return nil, err
}
stats.TotalUsers = total
// 按状态统计
statusCounts := map[domain.UserStatus]*int64{
domain.UserStatusActive: &stats.ActiveUsers,
domain.UserStatusInactive: &stats.InactiveUsers,
domain.UserStatusLocked: &stats.LockedUsers,
domain.UserStatusDisabled: &stats.DisabledUsers,
}
for status, countPtr := range statusCounts {
_, cnt, err := s.userRepo.ListByStatus(ctx, status, 0, 1)
if err == nil {
*countPtr = cnt
}
}
// 今日新增
stats.NewUsersToday = s.countNewUsers(ctx, daysAgo(0))
// 本周新增
stats.NewUsersWeek = s.countNewUsers(ctx, daysAgo(7))
// 本月新增
stats.NewUsersMonth = s.countNewUsers(ctx, daysAgo(30))
return stats, nil
}
// countNewUsers 统计指定时间之后的新增用户数
func (s *StatsService) countNewUsers(ctx context.Context, since time.Time) int64 {
_, count, err := s.userRepo.ListCreatedAfter(ctx, since, 0, 0)
if err != nil {
return 0
}
return count
}
// GetDashboardStats 获取仪表盘综合统计
func (s *StatsService) GetDashboardStats(ctx context.Context) (*DashboardStats, error) {
userStats, err := s.GetUserStats(ctx)
if err != nil {
return nil, err
}
loginStats := &LoginStats{}
// 今日登录成功/失败
today := daysAgo(0)
if s.loginLogRepo != nil {
if successCount, err := s.loginLogRepo.CountByResultSince(ctx, true, today); err == nil {
loginStats.LoginsTodaySuccess = successCount
}
if failedCount, err := s.loginLogRepo.CountByResultSince(ctx, false, today); err == nil {
loginStats.LoginsTodayFailed = failedCount
}
if weekCount, err := s.loginLogRepo.CountByResultSince(ctx, true, daysAgo(7)); err == nil {
loginStats.LoginsWeek = weekCount
}
}
return &DashboardStats{
Users: *userStats,
Logins: *loginStats,
}, nil
}
// daysAgo 返回N天前的时间当天0点
func daysAgo(n int) time.Time {
now := time.Now()
start := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
return start.AddDate(0, 0, -n)
}