remove dead sora quota and dashboard wrappers
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-21 11:11:34 +08:00
parent 7d1d185a2f
commit 96d046d5c1
4 changed files with 27 additions and 142 deletions

View File

@@ -8,32 +8,27 @@ import (
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
)
// SoraQuotaService 管理 Sora 用户存储配额。
// 仅支持系统默认配额(用户级和分组级配额已移除)。
// SoraQuotaService manages Sora storage quota using only the system default quota.
type SoraQuotaService struct {
settingService *SettingService
}
// NewSoraQuotaService 创建配额服务实例。
func NewSoraQuotaService(settingService *SettingService) *SoraQuotaService {
return &SoraQuotaService{
settingService: settingService,
}
}
// QuotaInfo 返回给客户端的配额信息。
type QuotaInfo struct {
QuotaBytes int64 `json:"quota_bytes"` // 总配额0 表示无限制)
UsedBytes int64 `json:"used_bytes"` // 已使用已移除追踪功能始终为0
AvailableBytes int64 `json:"available_bytes"` // 剩余可用(无限制时为 0
QuotaSource string `json:"quota_source"` // 配额来源system / unlimited
Source string `json:"source,omitempty"` // 兼容旧字段
QuotaBytes int64 `json:"quota_bytes"`
UsedBytes int64 `json:"used_bytes"`
AvailableBytes int64 `json:"available_bytes"`
QuotaSource string `json:"quota_source"`
Source string `json:"source,omitempty"`
}
// ErrSoraStorageQuotaExceeded 表示配额不足。
var ErrSoraStorageQuotaExceeded = errors.New("sora storage quota exceeded")
// QuotaExceededError 包含配额不足的上下文信息。
type QuotaExceededError struct {
QuotaBytes int64
UsedBytes int64
@@ -41,47 +36,39 @@ type QuotaExceededError struct {
func (e *QuotaExceededError) Error() string {
if e == nil {
return "存储配额不足"
return "storage quota exceeded"
}
return fmt.Sprintf("存储配额不足(已用 %d / 配额 %d 字节)", e.UsedBytes, e.QuotaBytes)
return fmt.Sprintf("storage quota exceeded (used %d / quota %d bytes)", e.UsedBytes, e.QuotaBytes)
}
// GetQuota 获取用户的存储配额信息。
// 仅使用系统默认值(用户级和分组级配额已移除)。
func (s *SoraQuotaService) GetQuota(ctx context.Context, userID int64) (*QuotaInfo, error) {
info := &QuotaInfo{
UsedBytes: 0, // 已移除用量追踪
UsedBytes: 0,
}
// 系统默认值
defaultQuota := s.getSystemDefaultQuota(ctx)
if defaultQuota > 0 {
info.QuotaBytes = defaultQuota
info.QuotaSource = "system"
info.Source = info.QuotaSource
info.AvailableBytes = defaultQuota // 无用量追踪,可用等于配额
info.AvailableBytes = defaultQuota
return info, nil
}
// 无配额限制
info.QuotaSource = "unlimited"
info.Source = info.QuotaSource
info.AvailableBytes = 0
return info, nil
}
// CheckQuota 检查用户是否有足够的存储配额。
// 返回 nil 表示配额充足或无限制。
func (s *SoraQuotaService) CheckQuota(ctx context.Context, userID int64, additionalBytes int64) error {
quota, err := s.GetQuota(ctx, userID)
if err != nil {
return err
}
// 0 表示无限制
if quota.QuotaBytes == 0 {
return nil
}
// 无用量追踪,只要请求大小不超过配额即可
if additionalBytes > quota.QuotaBytes {
return &QuotaExceededError{
QuotaBytes: quota.QuotaBytes,
@@ -91,7 +78,6 @@ func (s *SoraQuotaService) CheckQuota(ctx context.Context, userID int64, additio
return nil
}
// AddUsage 累加用量(已移除追踪功能,仅记录日志)。
func (s *SoraQuotaService) AddUsage(ctx context.Context, userID int64, bytes int64) error {
if bytes <= 0 {
return nil
@@ -100,7 +86,6 @@ func (s *SoraQuotaService) AddUsage(ctx context.Context, userID int64, bytes int
return nil
}
// ReleaseUsage 释放用量(已移除追踪功能,仅记录日志)。
func (s *SoraQuotaService) ReleaseUsage(ctx context.Context, userID int64, bytes int64) error {
if bytes <= 0 {
return nil
@@ -120,12 +105,6 @@ func (s *SoraQuotaService) getSystemDefaultQuota(ctx context.Context) int64 {
return settings.DefaultStorageQuotaBytes
}
// GetQuotaFromSettings 从系统设置获取默认配额(供外部使用)。
func (s *SoraQuotaService) GetQuotaFromSettings(ctx context.Context) int64 {
return s.getSystemDefaultQuota(ctx)
}
// SetUserSoraQuota 设置用户级配额(已移除此功能)。
func SetUserSoraQuota(ctx context.Context, userRepo UserRepository, userID int64, quotaBytes int64) error {
return errors.New("user-level sora quota setting is no longer supported")
}

View File

@@ -110,7 +110,10 @@ func TestCheckQuota_Exceeded(t *testing.T) {
err := svc.CheckQuota(context.Background(), 1, 2048)
require.Error(t, err)
require.Contains(t, err.Error(), "配额不足")
var qe *QuotaExceededError
require.ErrorAs(t, err, &qe)
require.Equal(t, int64(1024), qe.QuotaBytes)
require.Equal(t, int64(0), qe.UsedBytes)
}
func TestCheckQuota_NoLimit(t *testing.T) {
@@ -171,11 +174,6 @@ func TestGetQuotaFromSettings_WithSettings(t *testing.T) {
// ==================== SetUserSoraQuota ====================
// 用户级配额设置已移除
func TestSetUserSoraQuota_NotSupported(t *testing.T) {
err := SetUserSoraQuota(context.Background(), nil, 1, 1024)
require.Error(t, err)
require.Contains(t, err.Error(), "no longer supported")
}
// ==================== GetQuota — 字段完整性 ====================
@@ -265,7 +263,7 @@ func TestReleaseUsage_NegativeBytes(t *testing.T) {
func TestQuotaExceededError_NilSafe(t *testing.T) {
var e *QuotaExceededError
msg := e.Error()
require.Contains(t, msg, "配额不足")
require.Contains(t, msg, "storage quota exceeded")
}
func TestQuotaExceededError_Format(t *testing.T) {
@@ -273,4 +271,4 @@ func TestQuotaExceededError_Format(t *testing.T) {
msg := e.Error()
require.Contains(t, msg, "512")
require.Contains(t, msg, "1024")
}
}

View File

@@ -9,7 +9,6 @@ import type {
TrendDataPoint,
ModelStat,
GroupStat,
ApiKeyUsageTrendPoint,
UserUsageTrendPoint,
UserSpendingRankingResponse,
UserBreakdownItem,
@@ -30,23 +29,6 @@ export interface TrendParams {
billing_type?: number | null
}
export interface TrendResponse {
trend: TrendDataPoint[]
start_date: string
end_date: string
granularity: string
}
/**
* Get usage trend data
* @param params - Query parameters for filtering
* @returns Usage trend data
*/
export async function getUsageTrend(params?: TrendParams): Promise<TrendResponse> {
const { data } = await apiClient.get<TrendResponse>('/admin/dashboard/trend', { params })
return data
}
export interface ModelStatsParams {
start_date?: string
end_date?: string
@@ -77,24 +59,6 @@ export async function getModelStats(params?: ModelStatsParams): Promise<ModelSta
return data
}
export interface GroupStatsParams {
start_date?: string
end_date?: string
user_id?: number
api_key_id?: number
account_id?: number
group_id?: number
request_type?: UsageRequestType
stream?: boolean
billing_type?: number | null
}
export interface GroupStatsResponse {
groups: GroupStat[]
start_date: string
end_date: string
}
export interface DashboardSnapshotV2Params extends TrendParams {
include_stats?: boolean
include_trend?: boolean
@@ -120,16 +84,6 @@ export interface DashboardSnapshotV2Response {
users_trend?: UserUsageTrendPoint[]
}
/**
* Get group usage statistics
* @param params - Query parameters for filtering
* @returns Group usage statistics
*/
export async function getGroupStats(params?: GroupStatsParams): Promise<GroupStatsResponse> {
const { data } = await apiClient.get<GroupStatsResponse>('/admin/dashboard/groups', { params })
return data
}
export interface UserBreakdownParams {
start_date?: string
end_date?: string
@@ -171,31 +125,6 @@ export async function getSnapshotV2(params?: DashboardSnapshotV2Params): Promise
return data
}
export interface ApiKeyTrendParams extends TrendParams {
limit?: number
}
export interface ApiKeyTrendResponse {
trend: ApiKeyUsageTrendPoint[]
start_date: string
end_date: string
granularity: string
}
/**
* Get API key usage trend data
* @param params - Query parameters for filtering
* @returns API key usage trend data
*/
export async function getApiKeyUsageTrend(
params?: ApiKeyTrendParams
): Promise<ApiKeyTrendResponse> {
const { data } = await apiClient.get<ApiKeyTrendResponse>('/admin/dashboard/api-keys-trend', {
params
})
return data
}
export interface UserTrendParams extends TrendParams {
limit?: number
}
@@ -260,43 +189,13 @@ export async function getBatchUsersUsage(userIds: number[]): Promise<BatchUsersU
return data
}
export interface BatchApiKeyUsageStats {
api_key_id: number
today_actual_cost: number
total_actual_cost: number
}
export interface BatchApiKeysUsageResponse {
stats: Record<string, BatchApiKeyUsageStats>
}
/**
* Get batch usage stats for multiple API keys
* @param apiKeyIds - Array of API key IDs
* @returns Usage stats map keyed by API key ID
*/
export async function getBatchApiKeysUsage(
apiKeyIds: number[]
): Promise<BatchApiKeysUsageResponse> {
const { data } = await apiClient.post<BatchApiKeysUsageResponse>(
'/admin/dashboard/api-keys-usage',
{
api_key_ids: apiKeyIds
}
)
return data
}
export const dashboardAPI = {
getUsageTrend,
getModelStats,
getGroupStats,
getUserBreakdown,
getSnapshotV2,
getApiKeyUsageTrend,
getUserUsageTrend,
getUserSpendingRanking,
getBatchUsersUsage,
getBatchApiKeysUsage
getBatchUsersUsage
}
export default dashboardAPI

View File

@@ -8,6 +8,8 @@ const routerPath = resolve(dirname(fileURLToPath(import.meta.url)), '../index.ts
const routerSource = readFileSync(routerPath, 'utf8')
const adminApiIndexPath = resolve(dirname(fileURLToPath(import.meta.url)), '../../api/admin/index.ts')
const adminApiIndexSource = readFileSync(adminApiIndexPath, 'utf8')
const adminDashboardApiPath = resolve(dirname(fileURLToPath(import.meta.url)), '../../api/admin/dashboard.ts')
const adminDashboardApiSource = readFileSync(adminDashboardApiPath, 'utf8')
describe('deprecated admin features', () => {
it('does not expose the deprecated data-management admin route', () => {
@@ -21,4 +23,11 @@ describe('deprecated admin features', () => {
expect(adminApiIndexSource).not.toContain('dataManagementAPI,')
expect(adminApiIndexSource).not.toContain("from './dataManagement'")
})
it('does not expose dead dashboard admin wrappers', () => {
expect(adminDashboardApiSource).not.toContain('getUsageTrend,')
expect(adminDashboardApiSource).not.toContain('getGroupStats,')
expect(adminDashboardApiSource).not.toContain('getApiKeyUsageTrend,')
expect(adminDashboardApiSource).not.toContain('getBatchApiKeysUsage')
})
})