package admin import ( "net/http" "strconv" "github.com/Wei-Shaw/sub2api/internal/pkg/pagination" "github.com/Wei-Shaw/sub2api/internal/pkg/response" "github.com/Wei-Shaw/sub2api/internal/service" "github.com/gin-gonic/gin" ) // SoraHandler handles admin Sora statistics and management type SoraHandler struct { soraGenService *service.SoraGenerationService soraQuotaService *service.SoraQuotaService userRepo service.UserRepository } // NewSoraHandler creates a new admin Sora handler func NewSoraHandler( soraGenService *service.SoraGenerationService, soraQuotaService *service.SoraQuotaService, userRepo service.UserRepository, ) *SoraHandler { return &SoraHandler{ soraGenService: soraGenService, soraQuotaService: soraQuotaService, userRepo: userRepo, } } // SoraSystemStatsResponse 系统级 Sora 统计 type SoraSystemStatsResponse struct { TotalUsers int64 `json:"total_users"` TotalGenerations int64 `json:"total_generations"` TotalStorageBytes int64 `json:"total_storage_bytes"` ActiveGenerations int64 `json:"active_generations"` ByStatus map[string]int64 `json:"by_status"` ByModel map[string]int64 `json:"by_model"` } // GetSystemStats 获取 Sora 系统统计 // GET /api/v1/admin/sora/stats func (h *SoraHandler) GetSystemStats(c *gin.Context) { ctx := c.Request.Context() // 获取所有用户的 Sora 统计 users, _, err := h.userRepo.List(ctx, pagination.PaginationParams{Page: 1, PageSize: 10000}) if err != nil { response.Error(c, 500, "Failed to get users") return } var totalStorageBytes int64 byStatus := make(map[string]int64) byModel := make(map[string]int64) // 遍历用户统计 // NOTE: Per-user storage tracking removed; totalStorageBytes now sourced from SoraGenerationService if needed. _ = users // suppress unused warning until real aggregation is implemented resp := SoraSystemStatsResponse{ TotalUsers: int64(len(users)), TotalGenerations: 0, TotalStorageBytes: totalStorageBytes, ActiveGenerations: 0, ByStatus: byStatus, ByModel: byModel, } response.Success(c, resp) } // SoraUserStatsResponse 用户级 Sora 统计 type SoraUserStatsResponse struct { UserID int64 `json:"user_id"` Username string `json:"username"` Email string `json:"email"` QuotaBytes int64 `json:"quota_bytes"` UsedBytes int64 `json:"used_bytes"` AvailableBytes int64 `json:"available_bytes"` QuotaSource string `json:"quota_source"` GenerationsCount int64 `json:"generations_count"` ActiveCount int64 `json:"active_count"` TotalFileSizeBytes int64 `json:"total_file_size_bytes"` } // ListUserStats 获取用户 Sora 使用统计列表 // GET /api/v1/admin/sora/users func (h *SoraHandler) ListUserStats(c *gin.Context) { ctx := c.Request.Context() page, pageSize := response.ParsePagination(c) search := c.Query("search") filters := service.UserListFilters{ Search: search, } users, result, err := h.userRepo.ListWithFilters(ctx, pagination.PaginationParams{ Page: page, PageSize: pageSize, }, filters) if err != nil { response.Error(c, 500, "Failed to get users") return } results := make([]SoraUserStatsResponse, len(users)) for i, u := range users { quota, _ := h.soraQuotaService.GetQuota(ctx, u.ID) activeCount, _ := h.soraGenService.CountActiveByUser(ctx, u.ID) quotaBytes := int64(0) availableBytes := int64(0) quotaSource := "unlimited" if quota != nil { quotaBytes = quota.QuotaBytes availableBytes = quota.AvailableBytes quotaSource = quota.QuotaSource } results[i] = SoraUserStatsResponse{ UserID: u.ID, Username: u.Username, Email: u.Email, QuotaBytes: quotaBytes, UsedBytes: 0, // per-user usage removed; use SoraGenerationService for real data AvailableBytes: availableBytes, QuotaSource: quotaSource, GenerationsCount: 0, ActiveCount: activeCount, TotalFileSizeBytes: 0, // per-user usage removed; use SoraGenerationService for real data } } response.Paginated(c, results, result.Total, page, pageSize) } // SoraGenerationAdminResponse 管理员视角的生成记录 type SoraGenerationAdminResponse struct { ID int64 `json:"id"` UserID int64 `json:"user_id"` Username string `json:"username"` Email string `json:"email"` Model string `json:"model"` Prompt string `json:"prompt"` MediaType string `json:"media_type"` Status string `json:"status"` StorageType string `json:"storage_type"` MediaURL string `json:"media_url"` FileSizeBytes int64 `json:"file_size_bytes"` ErrorMessage string `json:"error_message"` CreatedAt string `json:"created_at"` CompletedAt *string `json:"completed_at"` } // ListGenerations 获取 Sora 生成记录列表(管理员视角) // GET /api/v1/admin/sora/generations func (h *SoraHandler) ListGenerations(c *gin.Context) { // 简化实现:返回空列表 // 完整实现需要扩展 repository 支持 admin 级别的查询 response.Paginated(c, []SoraGenerationAdminResponse{}, int64(0), 1, 20) } // ClearUserStorage 清除用户的 Sora 存储空间(已弃用)。 // // Deprecated: Per-user storage tracking has been removed. // This endpoint now returns 410 Gone. Per-user Sora storage quota tracking was // fully removed in the Sora storage refactoring. Storage management is now // handled at the system-default level via SoraQuotaService. // // DELETE /api/v1/admin/sora/users/:id/storage func (h *SoraHandler) ClearUserStorage(c *gin.Context) { userID, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { response.BadRequest(c, "Invalid user ID") return } // Verify user exists before responding ctx := c.Request.Context() if _, err := h.userRepo.GetByID(ctx, userID); err != nil { response.ErrorFrom(c, err) return } c.Header("Deprecation", "true") c.Header("Sunset", "2026-12-31") c.Header("Warning", `299 - "Gone: per-user storage tracking removed, see SoraQuotaService"`) c.JSON(http.StatusGone, gin.H{ "error": "This endpoint is no longer available", "message": "Per-user Sora storage quota tracking has been removed. Storage is now managed at system level.", "sunset": "2026-12-31", "deprecated": true, }) }