refactor: 彻底移除 Sora 视频生成模块(全栈清理)
## 后端变更 - 删除 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:
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user