diff --git a/backend/internal/handler/admin/sora_handler.go b/backend/internal/handler/admin/sora_handler.go index a154d2aa..dfc2053a 100644 --- a/backend/internal/handler/admin/sora_handler.go +++ b/backend/internal/handler/admin/sora_handler.go @@ -1,9 +1,6 @@ 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" @@ -11,14 +8,14 @@ import ( "github.com/gin-gonic/gin" ) -// SoraHandler handles admin Sora statistics and management +// 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 +// NewSoraHandler creates a new admin Sora handler. func NewSoraHandler( soraGenService *service.SoraGenerationService, soraQuotaService *service.SoraQuotaService, @@ -31,7 +28,6 @@ func NewSoraHandler( } } -// SoraSystemStatsResponse 系统级 Sora 统计 type SoraSystemStatsResponse struct { TotalUsers int64 `json:"total_users"` TotalGenerations int64 `json:"total_generations"` @@ -41,39 +37,28 @@ type SoraSystemStatsResponse struct { ByModel map[string]int64 `json:"by_model"` } -// GetSystemStats 获取 Sora 系统统计 -// GET /api/v1/admin/sora/stats +// GetSystemStats returns aggregate admin Sora statistics. 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, + TotalStorageBytes: 0, ActiveGenerations: 0, - ByStatus: byStatus, - ByModel: byModel, + ByStatus: map[string]int64{}, + ByModel: map[string]int64{}, } response.Success(c, resp) } -// SoraUserStatsResponse 用户级 Sora 统计 type SoraUserStatsResponse struct { UserID int64 `json:"user_id"` Username string `json:"username"` @@ -87,21 +72,16 @@ type SoraUserStatsResponse struct { TotalFileSizeBytes int64 `json:"total_file_size_bytes"` } -// ListUserStats 获取用户 Sora 使用统计列表 -// GET /api/v1/admin/sora/users +// ListUserStats returns per-user admin Sora usage rows. 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) + }, service.UserListFilters{Search: search}) if err != nil { response.Error(c, 500, "Failed to get users") return @@ -127,19 +107,18 @@ func (h *SoraHandler) ListUserStats(c *gin.Context) { Username: u.Username, Email: u.Email, QuotaBytes: quotaBytes, - UsedBytes: 0, // per-user usage removed; use SoraGenerationService for real data + UsedBytes: 0, AvailableBytes: availableBytes, QuotaSource: quotaSource, GenerationsCount: 0, ActiveCount: activeCount, - TotalFileSizeBytes: 0, // per-user usage removed; use SoraGenerationService for real data + TotalFileSizeBytes: 0, } } response.Paginated(c, results, result.Total, page, pageSize) } -// SoraGenerationAdminResponse 管理员视角的生成记录 type SoraGenerationAdminResponse struct { ID int64 `json:"id"` UserID int64 `json:"user_id"` @@ -157,43 +136,7 @@ type SoraGenerationAdminResponse struct { CompletedAt *string `json:"completed_at"` } -// ListGenerations 获取 Sora 生成记录列表(管理员视角) -// GET /api/v1/admin/sora/generations +// ListGenerations returns admin-visible generation rows. 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, - }) -} diff --git a/backend/internal/handler/admin/sora_handler_test.go b/backend/internal/handler/admin/sora_handler_test.go index f394c9e2..538ed592 100644 --- a/backend/internal/handler/admin/sora_handler_test.go +++ b/backend/internal/handler/admin/sora_handler_test.go @@ -25,35 +25,6 @@ func TestSoraHandler_ListGenerations(t *testing.T) { assert.Contains(t, w.Body.String(), "items") } -func TestSoraHandler_ClearUserStorage_InvalidUserID(t *testing.T) { - gin.SetMode(gin.TestMode) - - handler := &SoraHandler{} - - testCases := []struct { - name string - userID string - expected int - }{ - {"empty string", "", http.StatusBadRequest}, - {"non-numeric", "abc", http.StatusBadRequest}, - {"float", "1.5", http.StatusBadRequest}, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - w := httptest.NewRecorder() - c, _ := gin.CreateTestContext(w) - c.Request = httptest.NewRequest(http.MethodDelete, "/admin/sora/users/"+tc.userID+"/storage", nil) - c.Params = gin.Params{{Key: "id", Value: tc.userID}} - - handler.ClearUserStorage(c) - - assert.Equal(t, tc.expected, w.Code) - }) - } -} - func TestSoraSystemStatsResponse_Fields(t *testing.T) { resp := SoraSystemStatsResponse{ TotalUsers: 10, diff --git a/backend/internal/server/routes/admin.go b/backend/internal/server/routes/admin.go index 47dde749..e700da99 100644 --- a/backend/internal/server/routes/admin.go +++ b/backend/internal/server/routes/admin.go @@ -99,7 +99,6 @@ func registerSoraRoutes(admin *gin.RouterGroup, h *handler.Handlers) { sora.GET("/stats", h.Admin.Sora.GetSystemStats) sora.GET("/users", h.Admin.Sora.ListUserStats) sora.GET("/generations", h.Admin.Sora.ListGenerations) - sora.DELETE("/users/:id/storage", h.Admin.Sora.ClearUserStorage) } } diff --git a/backend/internal/server/routes/admin_routes_test.go b/backend/internal/server/routes/admin_routes_test.go index 7a6fc894..4fb9d930 100644 --- a/backend/internal/server/routes/admin_routes_test.go +++ b/backend/internal/server/routes/admin_routes_test.go @@ -36,6 +36,7 @@ func TestRegisterAdminRoutes_OmitsDeprecatedMockEndpoints(t *testing.T) { "GET /api/v1/admin/data-management/agent/health", "GET /api/v1/admin/data-management/config", "POST /api/v1/admin/data-management/backups", + "DELETE /api/v1/admin/sora/users/:id/storage", } for _, route := range deprecatedRoutes { diff --git a/backend/internal/service/proxy_service.go b/backend/internal/service/proxy_service.go index 80045187..c254897a 100644 --- a/backend/internal/service/proxy_service.go +++ b/backend/internal/service/proxy_service.go @@ -2,7 +2,6 @@ package service import ( "context" - "fmt" infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors" "github.com/Wei-Shaw/sub2api/internal/pkg/pagination" @@ -13,6 +12,8 @@ var ( ErrProxyInUse = infraerrors.Conflict("PROXY_IN_USE", "proxy is in use by accounts") ) +// ProxyRepository defines the shared proxy persistence contract used across +// admin management and OAuth-related services. type ProxyRepository interface { Create(ctx context.Context, proxy *Proxy) error GetByID(ctx context.Context, id int64) (*Proxy, error) @@ -30,165 +31,3 @@ type ProxyRepository interface { CountAccountsByProxyID(ctx context.Context, proxyID int64) (int64, error) ListAccountSummariesByProxyID(ctx context.Context, proxyID int64) ([]ProxyAccountSummary, error) } - -// CreateProxyRequest 创建代理请求 -type CreateProxyRequest struct { - Name string `json:"name"` - Protocol string `json:"protocol"` - Host string `json:"host"` - Port int `json:"port"` - Username string `json:"username"` - Password string `json:"password"` -} - -// UpdateProxyRequest 更新代理请求 -type UpdateProxyRequest struct { - Name *string `json:"name"` - Protocol *string `json:"protocol"` - Host *string `json:"host"` - Port *int `json:"port"` - Username *string `json:"username"` - Password *string `json:"password"` - Status *string `json:"status"` -} - -// ProxyService 代理管理服务 -type ProxyService struct { - proxyRepo ProxyRepository -} - -// NewProxyService 创建代理服务实例 -func NewProxyService(proxyRepo ProxyRepository) *ProxyService { - return &ProxyService{ - proxyRepo: proxyRepo, - } -} - -// Create 创建代理 -func (s *ProxyService) Create(ctx context.Context, req CreateProxyRequest) (*Proxy, error) { - // 创建代理 - proxy := &Proxy{ - Name: req.Name, - Protocol: req.Protocol, - Host: req.Host, - Port: req.Port, - Username: req.Username, - Password: req.Password, - Status: StatusActive, - } - - if err := s.proxyRepo.Create(ctx, proxy); err != nil { - return nil, fmt.Errorf("create proxy: %w", err) - } - - return proxy, nil -} - -// GetByID 根据ID获取代理 -func (s *ProxyService) GetByID(ctx context.Context, id int64) (*Proxy, error) { - proxy, err := s.proxyRepo.GetByID(ctx, id) - if err != nil { - return nil, fmt.Errorf("get proxy: %w", err) - } - return proxy, nil -} - -// List 获取代理列表 -func (s *ProxyService) List(ctx context.Context, params pagination.PaginationParams) ([]Proxy, *pagination.PaginationResult, error) { - proxies, pagination, err := s.proxyRepo.List(ctx, params) - if err != nil { - return nil, nil, fmt.Errorf("list proxies: %w", err) - } - return proxies, pagination, nil -} - -// ListActive 获取活跃代理列表 -func (s *ProxyService) ListActive(ctx context.Context) ([]Proxy, error) { - proxies, err := s.proxyRepo.ListActive(ctx) - if err != nil { - return nil, fmt.Errorf("list active proxies: %w", err) - } - return proxies, nil -} - -// Update 更新代理 -func (s *ProxyService) Update(ctx context.Context, id int64, req UpdateProxyRequest) (*Proxy, error) { - proxy, err := s.proxyRepo.GetByID(ctx, id) - if err != nil { - return nil, fmt.Errorf("get proxy: %w", err) - } - - // 更新字段 - if req.Name != nil { - proxy.Name = *req.Name - } - - if req.Protocol != nil { - proxy.Protocol = *req.Protocol - } - - if req.Host != nil { - proxy.Host = *req.Host - } - - if req.Port != nil { - proxy.Port = *req.Port - } - - if req.Username != nil { - proxy.Username = *req.Username - } - - if req.Password != nil { - proxy.Password = *req.Password - } - - if req.Status != nil { - proxy.Status = *req.Status - } - - if err := s.proxyRepo.Update(ctx, proxy); err != nil { - return nil, fmt.Errorf("update proxy: %w", err) - } - - return proxy, nil -} - -// Delete 删除代理 -func (s *ProxyService) Delete(ctx context.Context, id int64) error { - // 检查代理是否存在 - _, err := s.proxyRepo.GetByID(ctx, id) - if err != nil { - return fmt.Errorf("get proxy: %w", err) - } - - if err := s.proxyRepo.Delete(ctx, id); err != nil { - return fmt.Errorf("delete proxy: %w", err) - } - - return nil -} - -// TestConnection 测试代理连接(需要实现具体测试逻辑) -func (s *ProxyService) TestConnection(ctx context.Context, id int64) error { - proxy, err := s.proxyRepo.GetByID(ctx, id) - if err != nil { - return fmt.Errorf("get proxy: %w", err) - } - - // TODO: 实现代理连接测试逻辑 - // 可以尝试通过代理发送测试请求 - _ = proxy - - return nil -} - -// GetURL 获取代理URL -func (s *ProxyService) GetURL(ctx context.Context, id int64) (string, error) { - proxy, err := s.proxyRepo.GetByID(ctx, id) - if err != nil { - return "", fmt.Errorf("get proxy: %w", err) - } - - return proxy.URL(), nil -} diff --git a/backend/internal/service/wire.go b/backend/internal/service/wire.go index 9a7213d6..8b6472c4 100644 --- a/backend/internal/service/wire.go +++ b/backend/internal/service/wire.go @@ -395,7 +395,6 @@ var ProviderSet = wire.NewSet( ProvideAPIKeyAuthCacheInvalidator, NewGroupService, NewAccountService, - NewProxyService, NewRedeemService, NewPromoService, NewUsageService, diff --git a/frontend/src/api/admin/sora.ts b/frontend/src/api/admin/sora.ts index ad67803a..cf4eb4f2 100644 --- a/frontend/src/api/admin/sora.ts +++ b/frontend/src/api/admin/sora.ts @@ -1,6 +1,5 @@ /** * Admin Sora API - * 管理员 Sora 统计和用户配额管理接口 */ import { apiClient } from '../client' @@ -46,17 +45,11 @@ export interface SoraGenerationAdmin { } const soraAdminAPI = { - /** - * 获取 Sora 系统统计 - */ async getSystemStats(): Promise { const { data } = await apiClient.get<{ data: SoraSystemStats }>('/admin/sora/stats') return data.data }, - /** - * 获取用户 Sora 使用统计列表 - */ async listUserStats(params?: { page?: number page_size?: number @@ -68,9 +61,6 @@ const soraAdminAPI = { return data }, - /** - * 获取 Sora 生成记录列表(管理员视角) - */ async listGenerations(params?: { page?: number page_size?: number @@ -82,14 +72,7 @@ const soraAdminAPI = { }): Promise> { const { data } = await apiClient.get>('/admin/sora/generations', { params }) return data - }, - - /** - * 删除用户的 Sora 存储空间(管理员操作) - */ - async clearUserStorage(userId: number): Promise { - await apiClient.delete(`/admin/sora/users/${userId}/storage`) - }, + } } export default soraAdminAPI diff --git a/frontend/src/views/admin/SoraAdminView.vue b/frontend/src/views/admin/SoraAdminView.vue index 73791f2a..fbf53e54 100644 --- a/frontend/src/views/admin/SoraAdminView.vue +++ b/frontend/src/views/admin/SoraAdminView.vue @@ -5,7 +5,6 @@ import soraAdminAPI, { type SoraSystemStats, type SoraUserStats, type SoraGenera import AppLayout from '@/components/layout/AppLayout.vue' import LoadingSpinner from '@/components/common/LoadingSpinner.vue' import Icon from '@/components/icons/Icon.vue' -import ConfirmDialog from '@/components/common/ConfirmDialog.vue' const { t } = useI18n() @@ -16,11 +15,6 @@ const userStats = ref([]) const generations = ref([]) const activeTab = ref<'overview' | 'users' | 'generations'>('overview') -// Confirm dialog state -const showConfirmDialog = ref(false) -const confirmDialogMessage = ref('') -const pendingClearUserId = ref(null) - // Pagination const userPage = ref(1) const userPageSize = ref(20) @@ -102,33 +96,6 @@ async function fetchGenerations() { } } -// Confirm dialog handlers -function confirmClearStorage(userId: number) { - pendingClearUserId.value = userId - confirmDialogMessage.value = t('admin.sora.confirmClearStorage') - showConfirmDialog.value = true -} - -async function handleConfirmClear() { - if (pendingClearUserId.value === null) return - const userId = pendingClearUserId.value - showConfirmDialog.value = false - pendingClearUserId.value = null - - try { - await soraAdminAPI.clearUserStorage(userId) - await fetchUserStats() - } catch (err) { - console.error('Failed to clear user storage:', err) - alert(t('common.error')) - } -} - -function handleCancelClear() { - showConfirmDialog.value = false - pendingClearUserId.value = null -} - async function loadAll() { loading.value = true await Promise.all([ @@ -359,14 +326,7 @@ onMounted(loadAll) {{ formatBytes(user.quota_bytes) }} {{ formatBytes(user.used_bytes) }} {{ user.generations_count }} - - - + - @@ -453,15 +413,5 @@ onMounted(loadAll) - - - diff --git a/frontend/src/views/admin/__tests__/SoraAdminView.spec.ts b/frontend/src/views/admin/__tests__/SoraAdminView.spec.ts index f7b0d506..e0fb7b99 100644 --- a/frontend/src/views/admin/__tests__/SoraAdminView.spec.ts +++ b/frontend/src/views/admin/__tests__/SoraAdminView.spec.ts @@ -8,21 +8,18 @@ import type { SoraSystemStats, SoraUserStats, SoraGenerationAdmin } from '@/api/ const { mockGetSystemStats, mockListUserStats, - mockListGenerations, - mockClearUserStorage + mockListGenerations } = vi.hoisted(() => ({ mockGetSystemStats: vi.fn(), mockListUserStats: vi.fn(), - mockListGenerations: vi.fn(), - mockClearUserStorage: vi.fn() + mockListGenerations: vi.fn() })) vi.mock('@/api/admin/sora', () => ({ default: { getSystemStats: mockGetSystemStats, listUserStats: mockListUserStats, - listGenerations: mockListGenerations, - clearUserStorage: mockClearUserStorage + listGenerations: mockListGenerations } })) @@ -209,6 +206,18 @@ describe('SoraAdminView', () => { expect(wrapper.text()).toContain('user1') expect(wrapper.text()).toContain('user1@example.com') }) + + it('does not render the deprecated clear storage action', async () => { + const wrapper = mountComponent() + await flushPromises() + + const tabs = wrapper.findAll('button') + const userTab = tabs.find(b => b.text().includes('admin.sora.userStats')) + await userTab?.trigger('click') + await flushPromises() + + expect(wrapper.text()).not.toContain('admin.sora.clearStorage') + }) }) describe('生成记录标签页', () => {