B3 (HIGH): sora_generation_service.go - Add panic recovery to parallel S3 URL fetching goroutines. Without recovery, a panic in GetAccessURL would skip wg.Done() causing wg.Wait() to hang indefinitely. B2 (MEDIUM): subscription_service.go:549 - Replace bare goroutine with safego.Go() for consistent panic recovery pattern. All other async calls in this file already use safego. B4 (MEDIUM): admin/sora_handler.go - Change ClearUserStorage response from 200 no-op to 410 Gone. The per-user storage quota was fully removed; returning success was misleading to callers.
200 lines
6.3 KiB
Go
200 lines
6.3 KiB
Go
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,
|
|
})
|
|
}
|