feat(channel): 渠道管理全链路集成 — 模型映射、定价、限制、用量统计
- 渠道模型映射:支持精确匹配和通配符映射,按平台隔离 - 渠道模型定价:支持 token/按次/图片三种计费模式,区间分层定价 - 模型限制:渠道可限制仅允许定价列表中的模型 - 计费模型来源:支持 requested/upstream 两种计费模型选择 - 用量统计:usage_logs 新增 channel_id/model_mapping_chain/billing_tier/billing_mode 字段 - Dashboard 支持 model_source 维度(requested/upstream/mapping)查看模型统计 - 全部 gateway handler 统一接入 ResolveChannelMappingAndRestrict - 修复测试:同步 SoraGenerationRepository 接口、SQL INSERT 参数、scan 字段
This commit is contained in:
@@ -125,6 +125,13 @@ func (r *stubSoraGenRepo) CountByUserAndStatus(_ context.Context, _ int64, _ []s
|
||||
return r.countValue, nil
|
||||
}
|
||||
|
||||
func (r *stubSoraGenRepo) CountByStorageType(_ context.Context, _ string, _ []string) (int64, error) {
|
||||
if r.countErr != nil {
|
||||
return 0, r.countErr
|
||||
}
|
||||
return r.countValue, nil
|
||||
}
|
||||
|
||||
// ==================== 辅助函数 ====================
|
||||
|
||||
func newTestSoraClientHandler(repo *stubSoraGenRepo) *SoraClientHandler {
|
||||
@@ -1657,8 +1664,8 @@ func TestStoreMediaWithDegradation_S3SuccessSingleURL(t *testing.T) {
|
||||
fakeS3 := newFakeS3Server("ok")
|
||||
defer fakeS3.Close()
|
||||
|
||||
s3Storage := newS3StorageForHandler(fakeS3.URL)
|
||||
h := &SoraClientHandler{s3Storage: s3Storage}
|
||||
objectStorage := newS3StorageForHandler(fakeS3.URL)
|
||||
h := &SoraClientHandler{objectStorage: objectStorage}
|
||||
|
||||
storedURL, storedURLs, storageType, s3Keys, fileSize := h.storeMediaWithDegradation(
|
||||
context.Background(), 1, "video", sourceServer.URL+"/v.mp4", nil,
|
||||
@@ -1679,8 +1686,8 @@ func TestStoreMediaWithDegradation_S3SuccessMultiURL(t *testing.T) {
|
||||
fakeS3 := newFakeS3Server("ok")
|
||||
defer fakeS3.Close()
|
||||
|
||||
s3Storage := newS3StorageForHandler(fakeS3.URL)
|
||||
h := &SoraClientHandler{s3Storage: s3Storage}
|
||||
objectStorage := newS3StorageForHandler(fakeS3.URL)
|
||||
h := &SoraClientHandler{objectStorage: objectStorage}
|
||||
|
||||
urls := []string{sourceServer.URL + "/a.mp4", sourceServer.URL + "/b.mp4"}
|
||||
storedURL, storedURLs, storageType, s3Keys, fileSize := h.storeMediaWithDegradation(
|
||||
@@ -1704,8 +1711,8 @@ func TestStoreMediaWithDegradation_S3DownloadFails(t *testing.T) {
|
||||
}))
|
||||
defer badSource.Close()
|
||||
|
||||
s3Storage := newS3StorageForHandler(fakeS3.URL)
|
||||
h := &SoraClientHandler{s3Storage: s3Storage}
|
||||
objectStorage := newS3StorageForHandler(fakeS3.URL)
|
||||
h := &SoraClientHandler{objectStorage: objectStorage}
|
||||
|
||||
_, _, storageType, _, _ := h.storeMediaWithDegradation(
|
||||
context.Background(), 1, "video", badSource.URL+"/missing.mp4", nil,
|
||||
@@ -1719,8 +1726,8 @@ func TestStoreMediaWithDegradation_S3FailsSingleURL(t *testing.T) {
|
||||
fakeS3 := newFakeS3Server("fail")
|
||||
defer fakeS3.Close()
|
||||
|
||||
s3Storage := newS3StorageForHandler(fakeS3.URL)
|
||||
h := &SoraClientHandler{s3Storage: s3Storage}
|
||||
objectStorage := newS3StorageForHandler(fakeS3.URL)
|
||||
h := &SoraClientHandler{objectStorage: objectStorage}
|
||||
|
||||
_, _, storageType, s3Keys, _ := h.storeMediaWithDegradation(
|
||||
context.Background(), 1, "video", sourceServer.URL+"/v.mp4", nil,
|
||||
@@ -1736,8 +1743,8 @@ func TestStoreMediaWithDegradation_S3PartialFailureCleanup(t *testing.T) {
|
||||
fakeS3 := newFakeS3Server("fail-second")
|
||||
defer fakeS3.Close()
|
||||
|
||||
s3Storage := newS3StorageForHandler(fakeS3.URL)
|
||||
h := &SoraClientHandler{s3Storage: s3Storage}
|
||||
objectStorage := newS3StorageForHandler(fakeS3.URL)
|
||||
h := &SoraClientHandler{objectStorage: objectStorage}
|
||||
|
||||
urls := []string{sourceServer.URL + "/a.mp4", sourceServer.URL + "/b.mp4"}
|
||||
_, _, storageType, s3Keys, _ := h.storeMediaWithDegradation(
|
||||
@@ -1808,7 +1815,7 @@ func TestStoreMediaWithDegradation_S3FailsFallbackToLocal(t *testing.T) {
|
||||
fakeS3 := newFakeS3Server("fail")
|
||||
defer fakeS3.Close()
|
||||
|
||||
s3Storage := newS3StorageForHandler(fakeS3.URL)
|
||||
objectStorage := newS3StorageForHandler(fakeS3.URL)
|
||||
cfg := &config.Config{
|
||||
Sora: config.SoraConfig{
|
||||
Storage: config.SoraStorageConfig{
|
||||
@@ -1821,8 +1828,8 @@ func TestStoreMediaWithDegradation_S3FailsFallbackToLocal(t *testing.T) {
|
||||
}
|
||||
mediaStorage := service.NewSoraMediaStorage(cfg)
|
||||
h := &SoraClientHandler{
|
||||
s3Storage: s3Storage,
|
||||
mediaStorage: mediaStorage,
|
||||
objectStorage: objectStorage,
|
||||
mediaStorage: mediaStorage,
|
||||
}
|
||||
|
||||
_, _, storageType, _, _ := h.storeMediaWithDegradation(
|
||||
@@ -1846,9 +1853,9 @@ func TestSaveToStorage_S3EnabledButUploadFails(t *testing.T) {
|
||||
StorageType: "upstream",
|
||||
MediaURL: sourceServer.URL + "/v.mp4",
|
||||
}
|
||||
s3Storage := newS3StorageForHandler(fakeS3.URL)
|
||||
objectStorage := newS3StorageForHandler(fakeS3.URL)
|
||||
genService := service.NewSoraGenerationService(repo, nil, nil)
|
||||
h := &SoraClientHandler{genService: genService, s3Storage: s3Storage}
|
||||
h := &SoraClientHandler{genService: genService, objectStorage: objectStorage}
|
||||
|
||||
c, rec := makeGinContext("POST", "/api/v1/sora/generations/1/save", "", 1)
|
||||
c.Params = gin.Params{{Key: "id", Value: "1"}}
|
||||
@@ -1872,9 +1879,9 @@ func TestSaveToStorage_UpstreamURLExpired(t *testing.T) {
|
||||
StorageType: "upstream",
|
||||
MediaURL: expiredServer.URL + "/v.mp4",
|
||||
}
|
||||
s3Storage := newS3StorageForHandler(fakeS3.URL)
|
||||
objectStorage := newS3StorageForHandler(fakeS3.URL)
|
||||
genService := service.NewSoraGenerationService(repo, nil, nil)
|
||||
h := &SoraClientHandler{genService: genService, s3Storage: s3Storage}
|
||||
h := &SoraClientHandler{genService: genService, objectStorage: objectStorage}
|
||||
|
||||
c, rec := makeGinContext("POST", "/api/v1/sora/generations/1/save", "", 1)
|
||||
c.Params = gin.Params{{Key: "id", Value: "1"}}
|
||||
@@ -1896,9 +1903,9 @@ func TestSaveToStorage_S3EnabledUploadSuccess(t *testing.T) {
|
||||
StorageType: "upstream",
|
||||
MediaURL: sourceServer.URL + "/v.mp4",
|
||||
}
|
||||
s3Storage := newS3StorageForHandler(fakeS3.URL)
|
||||
objectStorage := newS3StorageForHandler(fakeS3.URL)
|
||||
genService := service.NewSoraGenerationService(repo, nil, nil)
|
||||
h := &SoraClientHandler{genService: genService, s3Storage: s3Storage}
|
||||
h := &SoraClientHandler{genService: genService, objectStorage: objectStorage}
|
||||
|
||||
c, rec := makeGinContext("POST", "/api/v1/sora/generations/1/save", "", 1)
|
||||
c.Params = gin.Params{{Key: "id", Value: "1"}}
|
||||
@@ -1906,7 +1913,7 @@ func TestSaveToStorage_S3EnabledUploadSuccess(t *testing.T) {
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
resp := parseResponse(t, rec)
|
||||
data := resp["data"].(map[string]any)
|
||||
require.Contains(t, data["message"], "S3")
|
||||
require.Contains(t, data["message"], "云存储")
|
||||
require.NotEmpty(t, data["object_key"])
|
||||
// 验证记录已更新为 S3 存储
|
||||
require.Equal(t, service.SoraStorageTypeS3, repo.gens[1].StorageType)
|
||||
@@ -1928,9 +1935,9 @@ func TestSaveToStorage_S3EnabledUploadSuccess_MultiMediaURLs(t *testing.T) {
|
||||
sourceServer.URL + "/v2.mp4",
|
||||
},
|
||||
}
|
||||
s3Storage := newS3StorageForHandler(fakeS3.URL)
|
||||
objectStorage := newS3StorageForHandler(fakeS3.URL)
|
||||
genService := service.NewSoraGenerationService(repo, nil, nil)
|
||||
h := &SoraClientHandler{genService: genService, s3Storage: s3Storage}
|
||||
h := &SoraClientHandler{genService: genService, objectStorage: objectStorage}
|
||||
|
||||
c, rec := makeGinContext("POST", "/api/v1/sora/generations/1/save", "", 1)
|
||||
c.Params = gin.Params{{Key: "id", Value: "1"}}
|
||||
@@ -1956,7 +1963,7 @@ func TestSaveToStorage_S3EnabledUploadSuccessWithQuota(t *testing.T) {
|
||||
StorageType: "upstream",
|
||||
MediaURL: sourceServer.URL + "/v.mp4",
|
||||
}
|
||||
s3Storage := newS3StorageForHandler(fakeS3.URL)
|
||||
objectStorage := newS3StorageForHandler(fakeS3.URL)
|
||||
genService := service.NewSoraGenerationService(repo, nil, nil)
|
||||
|
||||
userRepo := newStubUserRepoForHandler()
|
||||
@@ -1966,7 +1973,7 @@ func TestSaveToStorage_S3EnabledUploadSuccessWithQuota(t *testing.T) {
|
||||
SoraStorageUsedBytes: 0,
|
||||
}
|
||||
quotaService := service.NewSoraQuotaService(userRepo, nil, nil)
|
||||
h := &SoraClientHandler{genService: genService, s3Storage: s3Storage, quotaService: quotaService}
|
||||
h := &SoraClientHandler{genService: genService, objectStorage: objectStorage, quotaService: quotaService}
|
||||
|
||||
c, rec := makeGinContext("POST", "/api/v1/sora/generations/1/save", "", 1)
|
||||
c.Params = gin.Params{{Key: "id", Value: "1"}}
|
||||
@@ -1990,9 +1997,9 @@ func TestSaveToStorage_S3UploadSuccessMarkCompletedFails(t *testing.T) {
|
||||
}
|
||||
// S3 上传成功后,MarkCompleted 会调用 repo.Update → 失败
|
||||
repo.updateErr = fmt.Errorf("db error")
|
||||
s3Storage := newS3StorageForHandler(fakeS3.URL)
|
||||
objectStorage := newS3StorageForHandler(fakeS3.URL)
|
||||
genService := service.NewSoraGenerationService(repo, nil, nil)
|
||||
h := &SoraClientHandler{genService: genService, s3Storage: s3Storage}
|
||||
h := &SoraClientHandler{genService: genService, objectStorage: objectStorage}
|
||||
|
||||
c, rec := makeGinContext("POST", "/api/v1/sora/generations/1/save", "", 1)
|
||||
c.Params = gin.Params{{Key: "id", Value: "1"}}
|
||||
@@ -2007,8 +2014,8 @@ func TestGetStorageStatus_S3EnabledNotHealthy(t *testing.T) {
|
||||
fakeS3 := newFakeS3Server("fail")
|
||||
defer fakeS3.Close()
|
||||
|
||||
s3Storage := newS3StorageForHandler(fakeS3.URL)
|
||||
h := &SoraClientHandler{s3Storage: s3Storage}
|
||||
objectStorage := newS3StorageForHandler(fakeS3.URL)
|
||||
h := &SoraClientHandler{objectStorage: objectStorage}
|
||||
|
||||
c, rec := makeGinContext("GET", "/api/v1/sora/storage-status", "", 0)
|
||||
h.GetStorageStatus(c)
|
||||
@@ -2023,8 +2030,8 @@ func TestGetStorageStatus_S3EnabledHealthy(t *testing.T) {
|
||||
fakeS3 := newFakeS3Server("ok")
|
||||
defer fakeS3.Close()
|
||||
|
||||
s3Storage := newS3StorageForHandler(fakeS3.URL)
|
||||
h := &SoraClientHandler{s3Storage: s3Storage}
|
||||
objectStorage := newS3StorageForHandler(fakeS3.URL)
|
||||
h := &SoraClientHandler{objectStorage: objectStorage}
|
||||
|
||||
c, rec := makeGinContext("GET", "/api/v1/sora/storage-status", "", 0)
|
||||
h.GetStorageStatus(c)
|
||||
@@ -2453,7 +2460,7 @@ func TestProcessGeneration_FullSuccessWithS3(t *testing.T) {
|
||||
},
|
||||
}
|
||||
soraGatewayService := newMinimalSoraGatewayService(soraClient)
|
||||
s3Storage := newS3StorageForHandler(fakeS3.URL)
|
||||
objectStorage := newS3StorageForHandler(fakeS3.URL)
|
||||
|
||||
userRepo := newStubUserRepoForHandler()
|
||||
userRepo.users[1] = &service.User{
|
||||
@@ -2465,7 +2472,7 @@ func TestProcessGeneration_FullSuccessWithS3(t *testing.T) {
|
||||
genService: genService,
|
||||
gatewayService: gatewayService,
|
||||
soraGatewayService: soraGatewayService,
|
||||
s3Storage: s3Storage,
|
||||
objectStorage: objectStorage,
|
||||
quotaService: quotaService,
|
||||
}
|
||||
|
||||
@@ -2515,7 +2522,7 @@ func TestProcessGeneration_MarkCompletedFails(t *testing.T) {
|
||||
// ==================== cleanupStoredMedia 直接测试 ====================
|
||||
|
||||
func TestCleanupStoredMedia_S3Path(t *testing.T) {
|
||||
// S3 清理路径:s3Storage 为 nil 时不 panic
|
||||
// S3 清理路径:objectStorage 为 nil 时不 panic
|
||||
h := &SoraClientHandler{}
|
||||
// 不应 panic
|
||||
h.cleanupStoredMedia(context.Background(), service.SoraStorageTypeS3, []string{"key1"}, nil)
|
||||
@@ -2962,7 +2969,7 @@ func TestSaveToStorage_QuotaExceeded(t *testing.T) {
|
||||
StorageType: "upstream",
|
||||
MediaURL: sourceServer.URL + "/v.mp4",
|
||||
}
|
||||
s3Storage := newS3StorageForHandler(fakeS3.URL)
|
||||
objectStorage := newS3StorageForHandler(fakeS3.URL)
|
||||
genService := service.NewSoraGenerationService(repo, nil, nil)
|
||||
|
||||
// 用户配额已满
|
||||
@@ -2973,7 +2980,7 @@ func TestSaveToStorage_QuotaExceeded(t *testing.T) {
|
||||
SoraStorageUsedBytes: 10,
|
||||
}
|
||||
quotaService := service.NewSoraQuotaService(userRepo, nil, nil)
|
||||
h := &SoraClientHandler{genService: genService, s3Storage: s3Storage, quotaService: quotaService}
|
||||
h := &SoraClientHandler{genService: genService, objectStorage: objectStorage, quotaService: quotaService}
|
||||
|
||||
c, rec := makeGinContext("POST", "/api/v1/sora/generations/1/save", "", 1)
|
||||
c.Params = gin.Params{{Key: "id", Value: "1"}}
|
||||
@@ -2995,13 +3002,13 @@ func TestSaveToStorage_QuotaNonQuotaError(t *testing.T) {
|
||||
StorageType: "upstream",
|
||||
MediaURL: sourceServer.URL + "/v.mp4",
|
||||
}
|
||||
s3Storage := newS3StorageForHandler(fakeS3.URL)
|
||||
objectStorage := newS3StorageForHandler(fakeS3.URL)
|
||||
genService := service.NewSoraGenerationService(repo, nil, nil)
|
||||
|
||||
// 用户不存在 → GetByID 失败 → AddUsage 返回普通 error
|
||||
userRepo := newStubUserRepoForHandler()
|
||||
quotaService := service.NewSoraQuotaService(userRepo, nil, nil)
|
||||
h := &SoraClientHandler{genService: genService, s3Storage: s3Storage, quotaService: quotaService}
|
||||
h := &SoraClientHandler{genService: genService, objectStorage: objectStorage, quotaService: quotaService}
|
||||
|
||||
c, rec := makeGinContext("POST", "/api/v1/sora/generations/1/save", "", 1)
|
||||
c.Params = gin.Params{{Key: "id", Value: "1"}}
|
||||
@@ -3022,9 +3029,9 @@ func TestSaveToStorage_EmptyMediaURLs(t *testing.T) {
|
||||
MediaURL: "",
|
||||
MediaURLs: []string{},
|
||||
}
|
||||
s3Storage := newS3StorageForHandler(fakeS3.URL)
|
||||
objectStorage := newS3StorageForHandler(fakeS3.URL)
|
||||
genService := service.NewSoraGenerationService(repo, nil, nil)
|
||||
h := &SoraClientHandler{genService: genService, s3Storage: s3Storage}
|
||||
h := &SoraClientHandler{genService: genService, objectStorage: objectStorage}
|
||||
|
||||
c, rec := makeGinContext("POST", "/api/v1/sora/generations/1/save", "", 1)
|
||||
c.Params = gin.Params{{Key: "id", Value: "1"}}
|
||||
@@ -3049,9 +3056,9 @@ func TestSaveToStorage_MultiURL_SecondUploadFails(t *testing.T) {
|
||||
MediaURL: sourceServer.URL + "/v1.mp4",
|
||||
MediaURLs: []string{sourceServer.URL + "/v1.mp4", sourceServer.URL + "/v2.mp4"},
|
||||
}
|
||||
s3Storage := newS3StorageForHandler(fakeS3.URL)
|
||||
objectStorage := newS3StorageForHandler(fakeS3.URL)
|
||||
genService := service.NewSoraGenerationService(repo, nil, nil)
|
||||
h := &SoraClientHandler{genService: genService, s3Storage: s3Storage}
|
||||
h := &SoraClientHandler{genService: genService, objectStorage: objectStorage}
|
||||
|
||||
c, rec := makeGinContext("POST", "/api/v1/sora/generations/1/save", "", 1)
|
||||
c.Params = gin.Params{{Key: "id", Value: "1"}}
|
||||
@@ -3074,7 +3081,7 @@ func TestSaveToStorage_MarkCompletedFailsWithQuotaRollback(t *testing.T) {
|
||||
MediaURL: sourceServer.URL + "/v.mp4",
|
||||
}
|
||||
repo.updateErr = fmt.Errorf("db error")
|
||||
s3Storage := newS3StorageForHandler(fakeS3.URL)
|
||||
objectStorage := newS3StorageForHandler(fakeS3.URL)
|
||||
genService := service.NewSoraGenerationService(repo, nil, nil)
|
||||
|
||||
userRepo := newStubUserRepoForHandler()
|
||||
@@ -3084,7 +3091,7 @@ func TestSaveToStorage_MarkCompletedFailsWithQuotaRollback(t *testing.T) {
|
||||
SoraStorageUsedBytes: 0,
|
||||
}
|
||||
quotaService := service.NewSoraQuotaService(userRepo, nil, nil)
|
||||
h := &SoraClientHandler{genService: genService, s3Storage: s3Storage, quotaService: quotaService}
|
||||
h := &SoraClientHandler{genService: genService, objectStorage: objectStorage, quotaService: quotaService}
|
||||
|
||||
c, rec := makeGinContext("POST", "/api/v1/sora/generations/1/save", "", 1)
|
||||
c.Params = gin.Params{{Key: "id", Value: "1"}}
|
||||
@@ -3097,8 +3104,8 @@ func TestSaveToStorage_MarkCompletedFailsWithQuotaRollback(t *testing.T) {
|
||||
func TestCleanupStoredMedia_WithS3Storage_ActualDelete(t *testing.T) {
|
||||
fakeS3 := newFakeS3Server("ok")
|
||||
defer fakeS3.Close()
|
||||
s3Storage := newS3StorageForHandler(fakeS3.URL)
|
||||
h := &SoraClientHandler{s3Storage: s3Storage}
|
||||
objectStorage := newS3StorageForHandler(fakeS3.URL)
|
||||
h := &SoraClientHandler{objectStorage: objectStorage}
|
||||
|
||||
h.cleanupStoredMedia(context.Background(), service.SoraStorageTypeS3, []string{"key1", "key2"}, nil)
|
||||
}
|
||||
@@ -3106,8 +3113,8 @@ func TestCleanupStoredMedia_WithS3Storage_ActualDelete(t *testing.T) {
|
||||
func TestCleanupStoredMedia_S3DeleteFails_LogOnly(t *testing.T) {
|
||||
fakeS3 := newFakeS3Server("fail")
|
||||
defer fakeS3.Close()
|
||||
s3Storage := newS3StorageForHandler(fakeS3.URL)
|
||||
h := &SoraClientHandler{s3Storage: s3Storage}
|
||||
objectStorage := newS3StorageForHandler(fakeS3.URL)
|
||||
h := &SoraClientHandler{objectStorage: objectStorage}
|
||||
|
||||
h.cleanupStoredMedia(context.Background(), service.SoraStorageTypeS3, []string{"key1"}, nil)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user