package service import ( "strings" "time" "gorm.io/gorm" "nl-pms-api/internal/commonservice" "nl-pms-api/internal/model" ) // AdminService 运营后台:统计、用户、团队。 type AdminService struct { DB *gorm.DB } type StatsPoint struct { Date string `json:"date"` Count int64 `json:"count"` } type TokenPoint struct { Date string `json:"date"` PromptTokens int64 `json:"promptTokens"` CompletionTokens int64 `json:"completionTokens"` Calls int64 `json:"calls"` } type OverviewStats struct { UserCount int64 `json:"userCount"` TeamCount int64 `json:"teamCount"` DAUToday int64 `json:"dauToday"` DAUSeries []StatsPoint `json:"dauSeries"` TokenToday TokenPoint `json:"tokenToday"` TokenSeries []TokenPoint `json:"tokenSeries"` } // Overview 概览统计。 func (s *AdminService) Overview(days int) (*OverviewStats, error) { if days <= 0 || days > 90 { days = 14 } out := &OverviewStats{ DAUSeries: []StatsPoint{}, TokenSeries: []TokenPoint{}, } s.DB.Model(&model.User{}).Count(&out.UserCount) s.DB.Model(&model.Team{}).Count(&out.TeamCount) today := time.Now().In(time.Local).Format("2006-01-02") s.DB.Model(&model.UserDailyActive{}).Where("active_date = ?", today).Count(&out.DAUToday) start := time.Now().In(time.Local).AddDate(0, 0, -(days - 1)).Format("2006-01-02") type dauRow struct { ActiveDate string Cnt int64 } var dauRows []dauRow s.DB.Model(&model.UserDailyActive{}). Select("active_date, COUNT(*) AS cnt"). Where("active_date >= ?", start). Group("active_date"). Order("active_date"). Scan(&dauRows) dauMap := map[string]int64{} for _, r := range dauRows { dauMap[r.ActiveDate] = r.Cnt } type tokRow struct { UsageDate string PromptTokens int64 CompletionTokens int64 Calls int64 } var tokRows []tokRow s.DB.Model(&model.AIUsageDaily{}). Select("usage_date, SUM(prompt_tokens) AS prompt_tokens, SUM(completion_tokens) AS completion_tokens, SUM(calls) AS calls"). Where("usage_date >= ?", start). Group("usage_date"). Order("usage_date"). Scan(&tokRows) tokMap := map[string]tokRow{} for _, r := range tokRows { tokMap[r.UsageDate] = r } for i := 0; i < days; i++ { d := time.Now().In(time.Local).AddDate(0, 0, -(days - 1 - i)).Format("2006-01-02") out.DAUSeries = append(out.DAUSeries, StatsPoint{Date: d, Count: dauMap[d]}) t := tokMap[d] pt := TokenPoint{ Date: d, PromptTokens: t.PromptTokens, CompletionTokens: t.CompletionTokens, Calls: t.Calls, } out.TokenSeries = append(out.TokenSeries, pt) if d == today { out.TokenToday = pt } } return out, nil } type AdminUserItem struct { ID int64 `json:"id"` Username string `json:"username"` Nickname string `json:"nickname"` AIBanned int `json:"aiBanned"` Disabled int `json:"disabled"` LastLoginIP string `json:"lastLoginIp"` LastSeenAt string `json:"lastSeenAt"` CreatedAt string `json:"createdAt"` } // ListUsers 用户列表。 func (s *AdminService) ListUsers() ([]AdminUserItem, error) { var users []model.User if err := s.DB.Order("id ASC").Find(&users).Error; err != nil { return nil, commonservice.Internal("QUERY_FAILED") } ids := make([]int64, 0, len(users)) for _, u := range users { ids = append(ids, u.ID) } nick := map[int64]string{} if len(ids) > 0 { var profiles []model.UserProfile s.DB.Where("user_id IN ?", ids).Find(&profiles) for _, p := range profiles { nick[p.UserID] = p.Nickname } } out := make([]AdminUserItem, 0, len(users)) for _, u := range users { out = append(out, AdminUserItem{ ID: u.ID, Username: u.Username, Nickname: nick[u.ID], AIBanned: u.AIBanned, Disabled: u.Disabled, LastLoginIP: u.LastLoginIP, LastSeenAt: u.LastSeenAt, CreatedAt: u.CreatedAt, }) } return out, nil } // PatchUser 更新用户 ai_banned / disabled(不可禁用自己)。 func (s *AdminService) PatchUser(actorID, targetID int64, aiBanned, disabled *int) error { if actorID != commonservice.AdminUserID { return commonservice.Forbidden("FORBIDDEN") } if targetID <= 0 { return commonservice.BadRequest("BAD_REQUEST") } if targetID == commonservice.AdminUserID && disabled != nil && *disabled != 0 { return commonservice.BadRequest("CANNOT_DISABLE_ADMIN") } updates := map[string]any{} if aiBanned != nil { if *aiBanned != 0 { updates["ai_banned"] = 1 } else { updates["ai_banned"] = 0 } } if disabled != nil { if *disabled != 0 { updates["disabled"] = 1 } else { updates["disabled"] = 0 } } if len(updates) == 0 { return commonservice.BadRequest("BAD_REQUEST") } res := s.DB.Model(&model.User{}).Where("id = ?", targetID).Updates(updates) if res.Error != nil { return commonservice.Internal("SAVE_FAILED") } if res.RowsAffected == 0 { return commonservice.NotFound("NOT_FOUND") } return nil } type AdminTeamItem struct { ID int64 `json:"id"` Name string `json:"name"` OwnerID int64 `json:"ownerId"` OwnerName string `json:"ownerName"` AIBanned int `json:"aiBanned"` Members int64 `json:"members"` CreatedAt string `json:"createdAt"` } // ListTeams 团队列表。 func (s *AdminService) ListTeams() ([]AdminTeamItem, error) { var teams []model.Team if err := s.DB.Order("id ASC").Find(&teams).Error; err != nil { return nil, commonservice.Internal("QUERY_FAILED") } out := make([]AdminTeamItem, 0, len(teams)) for _, t := range teams { item := AdminTeamItem{ ID: t.ID, Name: t.Name, OwnerID: t.OwnerID, AIBanned: t.AIBanned, CreatedAt: t.CreatedAt, } var u model.User if s.DB.Select("username").First(&u, t.OwnerID).Error == nil { item.OwnerName = u.Username } s.DB.Model(&model.TeamMember{}).Where("team_id = ?", t.ID).Count(&item.Members) out = append(out, item) } return out, nil } // PatchTeam 更新团队 ai_banned。 func (s *AdminService) PatchTeam(actorID, teamID int64, aiBanned *int) error { if actorID != commonservice.AdminUserID { return commonservice.Forbidden("FORBIDDEN") } if teamID <= 0 || aiBanned == nil { return commonservice.BadRequest("BAD_REQUEST") } v := 0 if *aiBanned != 0 { v = 1 } res := s.DB.Model(&model.Team{}).Where("id = ?", teamID).Update("ai_banned", v) if res.Error != nil { return commonservice.Internal("SAVE_FAILED") } if res.RowsAffected == 0 { return commonservice.NotFound("NOT_FOUND") } return nil } // AIPolicy 当前用户是否允许使用 AI。 type AIPolicy struct { Allowed bool `json:"allowed"` Reason string `json:"reason,omitempty"` } // GetAIPolicy 综合用户禁用/禁 AI 与所属团队禁 AI。 func (s *AdminService) GetAIPolicy(userID int64) (*AIPolicy, error) { var u model.User if err := s.DB.Select("id", "disabled", "ai_banned").First(&u, userID).Error; err != nil { return nil, commonservice.Unauthorized("UNAUTHORIZED") } if u.Disabled != 0 { return &AIPolicy{Allowed: false, Reason: "ACCOUNT_DISABLED"}, nil } if u.AIBanned != 0 { return &AIPolicy{Allowed: false, Reason: "USER_AI_BANNED"}, nil } var n int64 s.DB.Table("team_members"). Joins("JOIN teams ON teams.id = team_members.team_id"). Where("team_members.user_id = ? AND teams.ai_banned = 1", userID). Count(&n) if n > 0 { return &AIPolicy{Allowed: false, Reason: "TEAM_AI_BANNED"}, nil } return &AIPolicy{Allowed: true}, nil } // ReportAIUsage 累加当日 token 用量。 func (s *AdminService) ReportAIUsage(userID, teamID int64, provider string, prompt, completion int64, estimated bool) error { if userID <= 0 { return commonservice.Unauthorized("UNAUTHORIZED") } provider = strings.TrimSpace(provider) if provider == "" { provider = "unknown" } if prompt < 0 { prompt = 0 } if completion < 0 { completion = 0 } today := time.Now().In(time.Local).Format("2006-01-02") est := 0 if estimated { est = 1 } q := `INSERT INTO ai_usage_daily(user_id,team_id,usage_date,provider,prompt_tokens,completion_tokens,calls,estimated) VALUES(?,?,?,?,?,?,1,?) ON DUPLICATE KEY UPDATE prompt_tokens=prompt_tokens+VALUES(prompt_tokens), completion_tokens=completion_tokens+VALUES(completion_tokens), calls=calls+1, estimated=IF(VALUES(estimated)=1,1,estimated)` if err := s.DB.Exec(q, userID, teamID, today, provider, prompt, completion, est).Error; err != nil { return commonservice.Internal("SAVE_FAILED") } return nil }