Files
tokens-reef/backend/internal/handler/admin/sora_handler.go
User d1bf033f24 refactor(sora): remove per-user storage quota fields and simplify quota service
- Remove SoraStorageQuotaBytes/SoraStorageUsedBytes from User/Group schema (Ent ORM)
- Regenerate ent code (-582 lines net reduction)
- Clean up stale references in sora_handler.go (4 sites) and service.User struct
- Simplify SoraQuotaService constructor (3-param -> 1-param, system-default only)
- Add Deprecated marker + HTTP headers to ClearUserStorage API
- Change AddUsage/ReleaseUsage log level to Debug
- Add 9 unit tests for simplified SoraQuotaService (boundary/negative/nil-safe)
- Fix test files to remove deleted field references

Code review: 8.0/10 overall rating, 0 critical issues remaining.
2026-04-18 10:12:37 +08:00

196 lines
6.3 KiB
Go

package admin
import (
"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 a success no-op. It will be removed in a future version.
// Clients should stop calling this endpoint.
//
// 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
}
// 重置用户的存储使用量
// NOTE: Per-user SoraStorageUsedBytes field removed.
// Storage clearing now handled at the SoraGenerationService level if needed.
_, err = h.userRepo.GetByID(c.Request.Context(), userID)
if err != nil {
response.ErrorFrom(c, err)
return
}
// TODO: Implement storage cleanup via SoraGenerationService
c.Header("Deprecation", "true")
c.Header("Sunset", "2026-12-31")
c.Header("Warning", `299 - "Deprecated API: use SoraGenerationService for storage management"`)
response.Success(c, gin.H{"message": "User Sora storage cleared (no-op: per-user tracking removed)", "deprecated": true})
}