refactor: 彻底移除 Sora 视频生成模块(全栈清理)
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

## 后端变更
- 删除 21 个 sora_*.go 服务文件(service/handler/repository/routes)
- 删除 Sora 相关 migration 文件(046/047/063/090)
- 清理 config 中的 sora_* 配置项和平台常量
- 清理 wire 依赖注入中的 Sora 组件
- 修复 wire_gen.go 语法错误(缺少逗号和闭合括号)
- 移除 go.mod 中的 go-sora2api 依赖
- 更新 ent schema usage_log.go 注释

## 前端变更
- 删除 SoraView、SoraAdminView 及 8 个 Sora 子组件
- 删除 sora API 层和路由配置
- 清理 UserEditModal 中的 Sora 存储配额 UI
- 清理 types/index.ts 中 Sora 相关类型定义
- 清理 stores/app.ts 默认配置
- 清理 i18n 翻译文件 en.ts/zh.ts (~110 行)
- 更新相关测试文件

## 文档更新
- README.md / README_CN.md / README_JA.md: 移除 Sora 状态说明和配置段落
- PROJECT_DIFF.md: 移除 Sora 相关差异描述

## 验证结果
-  Go 编译通过 (go build ./...)
-  TypeScript 类型检查通过 (vue-tsc --noEmit)
-  后端测试全通过 (0 failures)
-  前端测试全通过 (59 files, 329 tests, 0 failures)
-  前端生产构建成功 (23.81s)
This commit is contained in:
2026-05-10 14:15:45 +08:00
parent 1da074cfd6
commit 0e057904e6
96 changed files with 726 additions and 20525 deletions

View File

@@ -1,142 +0,0 @@
package admin
import (
"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,
}
}
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 returns aggregate admin Sora statistics.
func (h *SoraHandler) GetSystemStats(c *gin.Context) {
ctx := c.Request.Context()
users, _, err := h.userRepo.List(ctx, pagination.PaginationParams{Page: 1, PageSize: 10000})
if err != nil {
response.Error(c, 500, "Failed to get users")
return
}
resp := SoraSystemStatsResponse{
TotalUsers: int64(len(users)),
TotalGenerations: 0,
TotalStorageBytes: 0,
ActiveGenerations: 0,
ByStatus: map[string]int64{},
ByModel: map[string]int64{},
}
response.Success(c, resp)
}
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 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")
users, result, err := h.userRepo.ListWithFilters(ctx, pagination.PaginationParams{
Page: page,
PageSize: pageSize,
}, service.UserListFilters{Search: search})
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,
AvailableBytes: availableBytes,
QuotaSource: quotaSource,
GenerationsCount: 0,
ActiveCount: activeCount,
TotalFileSizeBytes: 0,
}
}
response.Paginated(c, results, result.Total, page, pageSize)
}
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 returns admin-visible generation rows.
func (h *SoraHandler) ListGenerations(c *gin.Context) {
response.Paginated(c, []SoraGenerationAdminResponse{}, int64(0), 1, 20)
}

View File

@@ -1,262 +0,0 @@
package admin
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
)
func TestSoraHandler_ListGenerations(t *testing.T) {
gin.SetMode(gin.TestMode)
handler := &SoraHandler{}
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest(http.MethodGet, "/admin/sora/generations", nil)
handler.ListGenerations(c)
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "items")
}
func TestSoraSystemStatsResponse_Fields(t *testing.T) {
resp := SoraSystemStatsResponse{
TotalUsers: 10,
TotalGenerations: 100,
TotalStorageBytes: 1024 * 1024 * 1024,
ActiveGenerations: 5,
ByStatus: map[string]int64{"completed": 80, "failed": 20},
ByModel: map[string]int64{"sora2": 50, "sora1": 50},
}
assert.Equal(t, int64(10), resp.TotalUsers)
assert.Equal(t, int64(100), resp.TotalGenerations)
assert.Equal(t, int64(1024*1024*1024), resp.TotalStorageBytes)
assert.Equal(t, int64(5), resp.ActiveGenerations)
assert.Equal(t, int64(80), resp.ByStatus["completed"])
assert.Equal(t, int64(50), resp.ByModel["sora2"])
}
func TestSoraUserStatsResponse_Fields(t *testing.T) {
resp := SoraUserStatsResponse{
UserID: 1,
Username: "testuser",
Email: "test@example.com",
QuotaBytes: 10 * 1024 * 1024 * 1024,
UsedBytes: 1 * 1024 * 1024 * 1024,
AvailableBytes: 9 * 1024 * 1024 * 1024,
QuotaSource: "user",
GenerationsCount: 10,
ActiveCount: 2,
TotalFileSizeBytes: 1 * 1024 * 1024 * 1024,
}
assert.Equal(t, int64(1), resp.UserID)
assert.Equal(t, "testuser", resp.Username)
assert.Equal(t, "test@example.com", resp.Email)
assert.Equal(t, int64(10*1024*1024*1024), resp.QuotaBytes)
assert.Equal(t, int64(1*1024*1024*1024), resp.UsedBytes)
assert.Equal(t, "user", resp.QuotaSource)
assert.Equal(t, int64(10), resp.GenerationsCount)
assert.Equal(t, int64(2), resp.ActiveCount)
}
func TestSoraGenerationAdminResponse_Fields(t *testing.T) {
completedAt := "2024-01-01T12:00:00Z"
resp := SoraGenerationAdminResponse{
ID: 1,
UserID: 100,
Username: "testuser",
Email: "test@example.com",
Model: "sora2",
Prompt: "A beautiful sunset",
MediaType: "video",
Status: "completed",
StorageType: "s3",
MediaURL: "https://example.com/video.mp4",
FileSizeBytes: 1024 * 1024 * 10,
ErrorMessage: "",
CreatedAt: "2024-01-01T10:00:00Z",
CompletedAt: &completedAt,
}
assert.Equal(t, int64(1), resp.ID)
assert.Equal(t, int64(100), resp.UserID)
assert.Equal(t, "testuser", resp.Username)
assert.Equal(t, "sora2", resp.Model)
assert.Equal(t, "video", resp.MediaType)
assert.Equal(t, "completed", resp.Status)
assert.Equal(t, "s3", resp.StorageType)
assert.Equal(t, int64(1024*1024*10), resp.FileSizeBytes)
assert.NotNil(t, resp.CompletedAt)
}
func TestSoraGenerationAdminResponse_NilCompletedAt(t *testing.T) {
resp := SoraGenerationAdminResponse{
ID: 1,
UserID: 100,
Username: "testuser",
Email: "test@example.com",
Model: "sora2",
Prompt: "A beautiful sunset",
MediaType: "video",
Status: "pending",
StorageType: "upstream",
CreatedAt: "2024-01-01T10:00:00Z",
CompletedAt: nil,
}
assert.Equal(t, "pending", resp.Status)
assert.Nil(t, resp.CompletedAt)
}
func TestNewSoraHandler(t *testing.T) {
handler := NewSoraHandler(nil, nil, nil)
assert.NotNil(t, handler)
assert.Nil(t, handler.soraGenService)
assert.Nil(t, handler.soraQuotaService)
assert.Nil(t, handler.userRepo)
}
func TestUser_SoraFields(t *testing.T) {
user := &service.User{
ID: 1,
Email: "test@example.com",
}
assert.Equal(t, int64(1), user.ID)
assert.Equal(t, "test@example.com", user.Email)
}
func TestQuotaInfo_Fields(t *testing.T) {
quota := &service.QuotaInfo{
QuotaBytes: 10 * 1024 * 1024 * 1024,
UsedBytes: 1 * 1024 * 1024 * 1024,
AvailableBytes: 9 * 1024 * 1024 * 1024,
QuotaSource: "user",
}
assert.Equal(t, int64(10*1024*1024*1024), quota.QuotaBytes)
assert.Equal(t, int64(1*1024*1024*1024), quota.UsedBytes)
assert.Equal(t, "user", quota.QuotaSource)
}
func TestSoraSystemStatsResponse_JSON(t *testing.T) {
resp := SoraSystemStatsResponse{
TotalUsers: 10,
TotalGenerations: 100,
TotalStorageBytes: 1024,
ActiveGenerations: 5,
ByStatus: map[string]int64{"completed": 80},
ByModel: map[string]int64{"sora2": 50},
}
// Verify JSON tags by checking field values
assert.Equal(t, int64(10), resp.TotalUsers)
assert.Equal(t, int64(100), resp.TotalGenerations)
assert.Equal(t, int64(1024), resp.TotalStorageBytes)
assert.Equal(t, int64(5), resp.ActiveGenerations)
}
func TestSoraUserStatsResponse_JSON(t *testing.T) {
resp := SoraUserStatsResponse{
UserID: 1,
Username: "testuser",
Email: "test@example.com",
QuotaBytes: 1024,
UsedBytes: 512,
AvailableBytes: 512,
QuotaSource: "user",
GenerationsCount: 10,
ActiveCount: 2,
TotalFileSizeBytes: 1024,
}
// Verify all fields
assert.Equal(t, int64(1), resp.UserID)
assert.Equal(t, "testuser", resp.Username)
assert.Equal(t, "test@example.com", resp.Email)
assert.Equal(t, int64(1024), resp.QuotaBytes)
assert.Equal(t, int64(512), resp.UsedBytes)
assert.Equal(t, int64(512), resp.AvailableBytes)
assert.Equal(t, "user", resp.QuotaSource)
assert.Equal(t, int64(10), resp.GenerationsCount)
assert.Equal(t, int64(2), resp.ActiveCount)
assert.Equal(t, int64(1024), resp.TotalFileSizeBytes)
}
func TestSoraSystemStatsResponse_EmptyMaps(t *testing.T) {
resp := SoraSystemStatsResponse{
TotalUsers: 0,
TotalGenerations: 0,
TotalStorageBytes: 0,
ActiveGenerations: 0,
ByStatus: map[string]int64{},
ByModel: map[string]int64{},
}
assert.Equal(t, int64(0), resp.TotalUsers)
assert.Equal(t, int64(0), resp.TotalGenerations)
assert.Equal(t, int64(0), resp.TotalStorageBytes)
assert.Equal(t, int64(0), resp.ActiveGenerations)
assert.NotNil(t, resp.ByStatus)
assert.NotNil(t, resp.ByModel)
}
func TestSoraUserStatsResponse_QuotaSources(t *testing.T) {
sources := []string{"user", "group", "system", "unlimited"}
for _, source := range sources {
resp := SoraUserStatsResponse{
UserID: 1,
QuotaSource: source,
}
assert.Equal(t, source, resp.QuotaSource)
}
}
func TestSoraGenerationAdminResponse_Statuses(t *testing.T) {
statuses := []string{"pending", "generating", "completed", "failed", "cancelled"}
for _, status := range statuses {
resp := SoraGenerationAdminResponse{
ID: 1,
Status: status,
}
assert.Equal(t, status, resp.Status)
}
}
func TestSoraGenerationAdminResponse_MediaTypes(t *testing.T) {
mediaTypes := []string{"video", "image"}
for _, mt := range mediaTypes {
resp := SoraGenerationAdminResponse{
ID: 1,
MediaType: mt,
}
assert.Equal(t, mt, resp.MediaType)
}
}
func TestSoraGenerationAdminResponse_StorageTypes(t *testing.T) {
storageTypes := []string{"s3", "upstream"}
for _, st := range storageTypes {
resp := SoraGenerationAdminResponse{
ID: 1,
StorageType: st,
}
assert.Equal(t, st, resp.StorageType)
}
}