remove dead proxy service and sora storage action
Some checks failed
CI / test (push) Has been cancelled
CI / golangci-lint (push) Has been cancelled
Security Scan / backend-security (push) Has been cancelled
Security Scan / frontend-security (push) Has been cancelled

This commit is contained in:
2026-04-20 23:05:30 +08:00
parent 4a105650c8
commit 7bf0ed8681
9 changed files with 31 additions and 337 deletions

View File

@@ -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,
})
}

View File

@@ -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,

View File

@@ -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)
}
}

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -395,7 +395,6 @@ var ProviderSet = wire.NewSet(
ProvideAPIKeyAuthCacheInvalidator,
NewGroupService,
NewAccountService,
NewProxyService,
NewRedeemService,
NewPromoService,
NewUsageService,

View File

@@ -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<SoraSystemStats> {
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<BasePaginationResponse<SoraGenerationAdmin>> {
const { data } = await apiClient.get<BasePaginationResponse<SoraGenerationAdmin>>('/admin/sora/generations', { params })
return data
},
/**
* 删除用户的 Sora 存储空间(管理员操作)
*/
async clearUserStorage(userId: number): Promise<void> {
await apiClient.delete(`/admin/sora/users/${userId}/storage`)
},
}
}
export default soraAdminAPI

View File

@@ -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<SoraUserStats[]>([])
const generations = ref<SoraGenerationAdmin[]>([])
const activeTab = ref<'overview' | 'users' | 'generations'>('overview')
// Confirm dialog state
const showConfirmDialog = ref(false)
const confirmDialogMessage = ref('')
const pendingClearUserId = ref<number | null>(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)
<td class="px-4 py-3 text-sm text-gray-900 dark:text-white">{{ formatBytes(user.quota_bytes) }}</td>
<td class="px-4 py-3 text-sm text-gray-900 dark:text-white">{{ formatBytes(user.used_bytes) }}</td>
<td class="px-4 py-3 text-sm text-gray-900 dark:text-white">{{ user.generations_count }}</td>
<td class="px-4 py-3 text-sm">
<button
class="btn btn-danger btn-sm"
@click="confirmClearStorage(user.user_id)"
>
{{ t('admin.sora.clearStorage') }}
</button>
</td>
<td class="px-4 py-3 text-sm text-gray-500 dark:text-gray-400">-</td>
</tr>
</tbody>
</table>
@@ -453,15 +413,5 @@ onMounted(loadAll)
</div>
</template>
</div>
<!-- Confirm Dialog -->
<ConfirmDialog
:show="showConfirmDialog"
:title="t('admin.sora.clearStorage')"
:message="confirmDialogMessage"
:danger="true"
@confirm="handleConfirmClear"
@cancel="handleCancelClear"
/>
</AppLayout>
</template>

View File

@@ -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('生成记录标签页', () => {