Files
tokens-reef/docs/reviews/DETAILED_MERGE_PLAN_2026-04-14.md
User d96a9f384a
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
feat: merge sub2apipro features and add Chinese model pricing
## Merged Features from sub2apipro
- Sora video generation integration (OpenAI Sora API)
- Group management enhancements
- Usage log improvements
- Security headers middleware

## Chinese Model Pricing Updates
- GLM-5, GLM-5-Turbo, GLM-5.1, GLM-4.7, GLM-4.5-Air
- Baichuan4, Baichuan4-Turbo, Baichuan4-Air, Baichuan-M3-Plus
- DeepSeek-V3, DeepSeek-V3.2, DeepSeek-R1
- Qwen3-8B (free), Qwen2.5-72B-Instruct

## URL Whitelist Additions
- api.baichuan-ai.com (百川智能)
- api.siliconflow.cn (硅基流动)
- api.z.ai (智谱国际)
- api.groq.com (Groq加速推理)

## Documentation
- Added merge guide (docs/MERGE_GUIDE.md)
- Added quick reference (docs/MERGE_QUICKREF.md)
- Added review reports (docs/reviews/)
2026-04-15 12:02:07 +08:00

41 KiB
Raw Blame History

Sub2API v0.1.112 与本地版本详细合并方案

生成日期: 2026-04-14 官方版本: v0.1.112 (D:\project\sub2api-latest) 本地版本: D:\project\sub2apipro 合并策略: 官方版本为基准 + 选择性合并本地修改


一、文件差异总览

1.1 差异统计

层级 官方独有 本地独有 差异文件 高风险冲突
Schema 5 0 3 2
Handler 15 4 12 3
Service 16 15 5 2
Repository 4 2 3 2
Middleware 0 0 1 0
Domain 1 0 1 1
前端Views 11 3 3 1
前端Components 24 8 5 2
前端API/Types 4 2 8 3
总计 80 37 41 16

1.2 功能对比矩阵

功能模块 官方版本 本地版本 合并策略
支付系统 完整 必须引入
订阅计划 完整 必须引入
TLS指纹配置 完整 必须引入
渠道管理 完整 必须引入
OIDC OAuth 完整 必须引入
OpenAI Messages Dispatch 完整 必须引入
Sora视频生成 ⚠️ 标注不可用 完整实现 保留+禁用
Sora存储配额 完整 必须保留

二、高风险冲突点详细分析

2.1 Schema层冲突

冲突1: group.go (高风险)

官方新增字段:

field.Bool("require_oauth_only").Default(false)
field.Bool("require_privacy_set").Default(false)
field.JSON("messages_dispatch_model_config", domain.OpenAIMessagesDispatchModelConfig{})

本地新增字段:

field.Float("sora_image_price_360").Optional().Nillable()
field.Float("sora_image_price_540").Optional().Nillable()
field.Float("sora_video_price_per_request").Optional().Nillable()
field.Float("sora_video_price_per_request_hd").Optional().Nillable()
field.Int64("sora_storage_quota_bytes").Default(0)

合并方案: 合并双方所有字段,无冲突 数据库迁移: 需要添加8个新字段

冲突2: usage_log.go (高风险)

官方新增字段:

field.Int64("channel_id").Optional().Nillable()
field.String("model_mapping_chain").MaxLen(500).Optional().Nillable()
field.String("billing_tier").MaxLen(50).Optional().Nillable()
field.String("billing_mode").MaxLen(20).Optional().Nillable()

本地新增字段:

field.String("media_type").MaxLen(16).Optional().Nillable()

合并方案: 合并双方所有字段,无冲突 数据库迁移: 高频写入表,建议在低峰期迁移

冲突3: user.go (中风险)

官方新增: payment_orders edge 本地新增: sora_storage_quota_bytes, sora_storage_used_bytes 字段

合并方案: 合并双方修改


2.2 Handler层冲突

冲突1: wire.go (高风险)

官方新增Handler注册:

PaymentHandler,
ChannelHandler,
TLSFingerprintProfileHandler,
PaymentWebhookHandler,

本地新增Handler注册:

SoraGatewayHandler,
SoraClientHandler,

合并方案: 手动合并保留双方所有Handler

冲突2: endpoint.go (中风险)

官方新增: OpenAI Responses API路径处理 本地新增: PlatformSora支持和/sora路径前缀

合并方案:

  1. 使用官方版本为基准
  2. 手动添加Sora平台判断逻辑

冲突3: admin/setting_handler.go (高风险)

差异: 官方版本大幅扩展(+16KB),新增支付/渠道等配置管理

合并方案: 使用官方版本,验证本地自定义配置是否丢失


2.3 Service层冲突

冲突1: wire.go (中风险)

官方: 无SoraAccountRepo注入 本地: 注入SoraAccountRepo用于双表同步

合并方案: 保留本地注入

冲突2: gateway_service.go (中风险)

差异:

  • 官方: defaultMaxLineSize=10MB, 新增3个请求头
  • 本地: defaultMaxLineSize=500MB, 新增MediaType/MediaURL字段

合并方案:

  1. 使用官方的请求头白名单
  2. 保留本地的媒体字段
  3. 评估500MB vs 10MB的业务需求后决定

2.4 前端冲突

冲突1: router/index.ts (高风险)

官方新增路由: 支付、渠道、OIDC回调 本地新增路由: Sora

合并方案: 合并双方所有路由

冲突2: 类型定义 (高风险)

GroupPlatform/AccountPlatform类型不兼容:

  • 官方: 'anthropic' | 'openai' | 'gemini' | 'antigravity'
  • 本地: 上述 + 'sora'

合并方案: 扩展官方类型,添加'sora'

冲突3: stores/app.ts (中风险)

官方新增: payment_enabled等支付状态 本地新增: sora_client_enabled等Sora状态

合并方案: 合并双方状态


三、合并执行计划

Phase 1: 准备阶段 (Day 1)

步骤1.1: 环境准备

# 1. 备份本地版本
cp -r D:/project/sub2apipro D:/project/sub2apipro_backup_$(date +%Y%m%d)

# 2. 创建合并工作目录
cp -r D:/project/sub2api-latest D:/project/sub2api-merge

# 3. 初始化Git
cd D:/project/sub2api-merge
git init
git add .
git commit -m "chore: 官方版本 v0.1.112 基准"
git checkout -b merge-local-changes

步骤1.2: 依赖准备

# 合并 go.mod
# 官方依赖 + 本地Sora依赖

需要保留的本地依赖:

// Sora相关
github.com/DouDOU-start/go-sora2api v1.1.0

// TLS指纹模拟
github.com/bogdanfinn/fhttp v0.6.8
github.com/bogdanfinn/tls-client v1.14.0
github.com/bogdanfinn/utls v1.7.7-barnius

Phase 2: Schema层合并 (Day 2)

步骤2.1: 合并group.go

// 在官方版本基础上添加本地字段
// 位置: backend/ent/schema/group.go

// === 本地新增字段 (保留) ===
field.Float("sora_image_price_360").Optional().Nillable().
    SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}),
field.Float("sora_image_price_540").Optional().Nillable().
    SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}),
field.Float("sora_video_price_per_request").Optional().Nillable().
    SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}),
field.Float("sora_video_price_per_request_hd").Optional().Nillable().
    SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}),
field.Int64("sora_storage_quota_bytes").Default(0),

步骤2.2: 合并usage_log.go

// 在官方版本基础上添加本地字段
// 位置: backend/ent/schema/usage_log.go

// === 本地新增字段 (保留) ===
field.String("media_type").MaxLen(16).Optional().Nillable(),

步骤2.3: 合并user.go

// 在官方版本基础上添加本地字段
// 位置: backend/ent/schema/user.go

// === 本地新增字段 (保留) ===
field.Int64("sora_storage_quota_bytes").Default(0),
field.Int64("sora_storage_used_bytes").Default(0),

步骤2.4: 复制官方独有Schema文件

# 这些文件官方独有,直接复制
cp D:/project/sub2api-latest/backend/ent/schema/payment_order.go \
   D:/project/sub2api-merge/backend/ent/schema/

cp D:/project/sub2api-latest/backend/ent/schema/payment_provider_instance.go \
   D:/project/sub2api-merge/backend/ent/schema/

cp D:/project/sub2api-latest/backend/ent/schema/payment_audit_log.go \
   D:/project/sub2api-merge/backend/ent/schema/

cp D:/project/sub2api-latest/backend/ent/schema/subscription_plan.go \
   D:/project/sub2api-merge/backend/ent/schema/

cp D:/project/sub2api-latest/backend/ent/schema/tls_fingerprint_profile.go \
   D:/project/sub2api-merge/backend/ent/schema/

步骤2.5: 生成Ent代码

cd D:/project/sub2api-merge/backend
go generate ./ent

Phase 3: 后端代码合并 (Day 3-4)

步骤3.1: Handler层合并

直接复制的官方文件 (无冲突):

# 支付相关Handler
payment_handler.go
payment_webhook_handler.go
admin/payment_handler.go
admin/channel_handler.go
admin/tls_fingerprint_profile_handler.go

# 新增API Handler
gateway_handler_chat_completions.go
gateway_handler_responses.go

# OIDC认证
auth_oidc_oauth.go

需要保留的本地文件:

# Sora相关Handler (必须保留)
sora_client_handler.go
sora_gateway_handler.go

需要手动合并的文件:

  1. wire.go:
// 合并双方的Handler注册
func ProvideHandlers(...) []*Handler {
    return []*Handler{
        // === 官方Handler ===
        &PaymentHandler{},
        &ChannelHandler{},
        &TLSFingerprintProfileHandler{},
        &PaymentWebhookHandler{},
        
        // === 本地Handler (保留) ===
        &SoraGatewayHandler{},
        &SoraClientHandler{},
        
        // ... 其他共同Handler
    }
}
  1. endpoint.go:
// 添加Sora平台判断
func oauthPlatformFromPath(path string) Platform {
    switch {
    // === 官方逻辑 ===
    case strings.Contains(path, "/anthropic"):
        return PlatformAnthropic
    // ...
    
    // === 本地新增 (保留) ===
    case strings.Contains(path, "/sora"):
        return PlatformSora
    
    default:
        return ""
    }
}

步骤3.2: Service层合并

直接复制的官方文件 (支付系统):

payment_service.go
payment_order.go
payment_order_lifecycle.go
payment_order_expiry_service.go
payment_fulfillment.go
payment_refund.go
payment_stats.go
payment_config_service.go
payment_config_providers.go
payment_config_plans.go
payment_config_limits.go
billing_service.go
channel.go
channel_service.go

需要保留的本地文件 (Sora系统):

sora_gateway_service.go
sora_generation_service.go
sora_sdk_client.go
sora_models.go
sora_account_service.go
sora_quota_service.go
sora_s3_storage.go
sora_media_storage.go
sora_media_sign.go
sora_media_cleanup_service.go
sora_upstream_forwarder.go

需要手动合并的文件:

  1. service/wire.go:
// 保留本地的SoraAccountRepo注入
func ProvideTokenRefreshService(
    // 官方参数...
    soraAccountRepo repository.SoraAccountRepository, // 本地新增
) *TokenRefreshService {
    // ...
}
  1. gateway_service.go:
// 合并双方的修改
const defaultMaxLineSize = 10 * 1024 * 1024 // 使用官方值

// 保留本地的媒体字段
type ForwardResult struct {
    // 官方字段...
    
    // 本地新增 (保留)
    MediaType string
    MediaURL  string
}

步骤3.3: Repository层合并

直接复制的官方文件:

channel_repo.go
channel_repo_pricing.go
tls_fingerprint_profile_repo.go
tls_fingerprint_profile_cache.go

需要保留的本地文件:

sora_account_repo.go
sora_generation_repo.go

需要手动合并的文件:

  1. user_repo.go - 添加Sora存储方法:
// === 本地新增方法 (保留) ===
func (r *userRepository) AddSoraStorageUsageWithQuota(ctx context.Context, userID int64, deltaBytes int64, effectiveQuota int64) (int64, error) {
    // ...
}

func (r *userRepository) ReleaseSoraStorageUsageAtomic(ctx context.Context, userID int64, deltaBytes int64) (int64, error) {
    // ...
}
  1. group_repo.go - 合并排序功能:
// 保留官方的排序增强
func groupListOrder(params pagination.PaginationParams) []func(*entsql.Selector) {
    // 官方实现...
}

步骤3.4: Domain层合并

直接复制的官方文件:

openai_messages_dispatch.go

需要手动合并的文件:

  1. constants.go:
// 添加本地Sora平台常量
const (
    PlatformAnthropic   = "anthropic"
    PlatformOpenAI      = "openai"
    PlatformGemini      = "gemini"
    PlatformAntigravity = "antigravity"
    PlatformSora        = "sora"  // 本地新增 (保留)
)

步骤3.5: Middleware层合并

使用本地版本:

# 仅 security_headers.go 有差异
# 本地版本包含 /sora/ 路径支持
cp D:/project/sub2apipro/backend/internal/server/middleware/security_headers.go \
   D:/project/sub2api-merge/backend/internal/server/middleware/

Phase 4: 前端代码合并 (Day 5-6)

步骤4.1: Views层合并

直接复制的官方文件:

# 支付相关页面
views/user/PaymentView.vue
views/user/PaymentQRCodeView.vue
views/user/PaymentResultView.vue
views/user/StripePaymentView.vue
views/user/StripePopupView.vue
views/user/UserOrdersView.vue

# 管理后台页面
views/admin/orders/AdminPaymentDashboardView.vue
views/admin/orders/AdminOrdersView.vue
views/admin/orders/AdminPaymentPlansView.vue
views/admin/ChannelsView.vue
views/auth/OidcCallbackView.vue

需要保留的本地文件:

# Sora相关页面
views/user/SoraView.vue
views/admin/DataManagementView.vue

步骤4.2: Components层合并

直接复制的官方文件 (支付组件):

# 24个支付相关组件
components/payment/*.vue
components/admin/payment/*.vue
components/admin/channel/*.vue
components/admin/TLSFingerprintProfilesModal.vue
components/auth/OidcOAuthSection.vue
components/keys/EndpointPopover.vue

需要保留的本地文件:

# 8个Sora相关组件
components/sora/*.vue

步骤4.3: API和类型合并

直接复制的官方文件:

api/payment.ts
api/admin/payment.ts
api/admin/channels.ts
api/admin/tlsFingerprintProfile.ts

需要保留的本地文件:

api/sora.ts
api/__tests__/sora.spec.ts

需要手动合并的文件:

  1. types/index.ts - 扩展平台类型:
// 合并官方类型 + 本地Sora扩展
export type GroupPlatform = 'anthropic' | 'openai' | 'gemini' | 'antigravity' | 'sora'
export type AccountPlatform = 'anthropic' | 'openai' | 'gemini' | 'antigravity' | 'sora'

// 合并Group类型
export interface Group {
    // === 官方字段 ===
    require_oauth_only?: boolean
    require_privacy_set?: boolean
    messages_dispatch_model_config?: OpenAIMessagesDispatchModelConfig
    
    // === 本地字段 (保留) ===
    sora_image_price_360?: number
    sora_image_price_540?: number
    sora_video_price_per_request?: number
    sora_video_price_per_request_hd?: number
    sora_storage_quota_bytes?: number
}

// 合并User类型
export interface AdminUser {
    // === 官方字段 ===
    // ...
    
    // === 本地字段 (保留) ===
    sora_storage_quota_bytes?: number
    sora_storage_used_bytes?: number
}
  1. stores/app.ts - 合并状态:
export const useAppStore = defineStore('app', {
    state: () => ({
        // === 官方状态 ===
        payment_enabled: false,
        
        // === 本地状态 (保留) ===
        sora_client_enabled: false,
    })
})
  1. router/index.ts - 合并路由:
const routes: RouteRecordRaw[] = [
    // === 官方路由 ===
    { path: '/purchase', name: 'purchase', component: () => import('@/views/user/PaymentView.vue') },
    { path: '/payment/qrcode', name: 'payment-qrcode', component: () => import('@/views/user/PaymentQRCodeView.vue') },
    { path: '/admin/channels', name: 'admin-channels', component: () => import('@/views/admin/ChannelsView.vue') },
    // ... 更多官方路由
    
    // === 本地路由 (保留) ===
    { path: '/sora', name: 'sora', component: () => import('@/views/user/SoraView.vue') },
    { path: '/admin/data-management', name: 'admin-data-management', component: () => import('@/views/admin/DataManagementView.vue') },
]

Phase 5: 数据库迁移 (Day 7)

步骤5.1: 创建迁移脚本

创建新迁移文件: backend/migrations/XXX_merge_migration.sql

-- === 官方新表 ===
CREATE TABLE IF NOT EXISTS payment_orders (...);
CREATE TABLE IF NOT EXISTS payment_provider_instances (...);
CREATE TABLE IF NOT EXISTS payment_audit_logs (...);
CREATE TABLE IF NOT EXISTS subscription_plans (...);
CREATE TABLE IF NOT EXISTS tls_fingerprint_profiles (...);

-- === 官方新字段 ===
ALTER TABLE groups ADD COLUMN IF NOT EXISTS require_oauth_only BOOLEAN DEFAULT false;
ALTER TABLE groups ADD COLUMN IF NOT EXISTS require_privacy_set BOOLEAN DEFAULT false;
ALTER TABLE groups ADD COLUMN IF NOT EXISTS messages_dispatch_model_config JSONB DEFAULT '{}';

ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS channel_id BIGINT;
ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS model_mapping_chain VARCHAR(500);
ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS billing_tier VARCHAR(50);
ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS billing_mode VARCHAR(20);

-- === 本地新字段 (如果不存在) ===
ALTER TABLE users ADD COLUMN IF NOT EXISTS sora_storage_quota_bytes BIGINT DEFAULT 0;
ALTER TABLE users ADD COLUMN IF NOT EXISTS sora_storage_used_bytes BIGINT DEFAULT 0;

ALTER TABLE groups ADD COLUMN IF NOT EXISTS sora_image_price_360 DECIMAL(20,8);
ALTER TABLE groups ADD COLUMN IF NOT EXISTS sora_image_price_540 DECIMAL(20,8);
ALTER TABLE groups ADD COLUMN IF NOT EXISTS sora_video_price_per_request DECIMAL(20,8);
ALTER TABLE groups ADD COLUMN IF NOT EXISTS sora_video_price_per_request_hd DECIMAL(20,8);
ALTER TABLE groups ADD COLUMN IF NOT EXISTS sora_storage_quota_bytes BIGINT DEFAULT 0;

ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS media_type VARCHAR(16);

-- === 本地新表 (如果不存在) ===
CREATE TABLE IF NOT EXISTS sora_accounts (...);
CREATE TABLE IF NOT EXISTS sora_generations (...);

步骤5.2: 执行迁移

# 在测试环境先验证
cd D:/project/sub2api-merge/backend
go run . migrate

# 检查迁移结果
psql -h localhost -U postgres -d sub2api -c "\dt"
psql -h localhost -U postgres -d sub2api -c "\d groups"
psql -h localhost -U postgres -d sub2api -c "\d usage_logs"

Phase 6: 配置合并 (Day 7)

步骤6.1: 后端配置

合并 config.go:

type Config struct {
    // === 官方配置 ===
    PaymentConfig    PaymentConfig
    ChannelConfig    ChannelConfig
    TLSFingerprintConfig TLSFingerprintConfig
    OIDCConnectConfig OIDCConnectConfig
    
    // === 本地配置 (保留) ===
    SoraConfig       SoraConfig
}

步骤6.2: 环境变量

合并 .env.example:

# === 官方配置 ===
PAYMENT_ENABLED=false
PAYMENT_MIN_AMOUNT=1
PAYMENT_MAX_AMOUNT=10000
# ... 更多支付配置

# === 本地配置 (保留) ===
SORA_CLIENT_ENABLED=false
SORA_STORAGE_TYPE=local
# ... 更多Sora配置

Phase 7: 测试验证 (Day 8-10)

步骤7.1: 后端测试

# 运行所有测试
cd D:/project/sub2api-merge/backend
go test ./... -v

# 特别关注
go test ./internal/handler/... -v
go test ./internal/service/... -v
go test ./internal/repository/... -v

步骤7.2: 前端测试

cd D:/project/sub2api-merge/frontend
pnpm install
pnpm typecheck
pnpm lint
pnpm test
pnpm build

步骤7.3: 集成测试清单

测试项 测试内容 预期结果
支付创建 创建支付订单 成功创建状态PENDING
支付回调 模拟支付成功回调 状态变为PAID余额增加
订阅计划 创建/购买订阅 成功创建,用户订阅关联
渠道管理 创建/编辑渠道 成功保存,定价生效
TLS指纹 创建指纹配置 成功保存,账号关联
OIDC登录 OIDC认证流程 成功登录,用户创建
Sora生成 创建Sora任务 任务创建成功(功能禁用提示)
Sora配额 查询配额信息 返回配额数据
API网关 各平台请求转发 正常响应

四、回滚方案

4.1 代码回滚

# 恢复到备份版本
rm -rf D:/project/sub2api-merge
cp -r D:/project/sub2apipro_backup_YYYYMMDD D:/project/sub2api-merge

4.2 数据库回滚

-- 删除新增表
DROP TABLE IF EXISTS payment_orders;
DROP TABLE IF EXISTS payment_provider_instances;
DROP TABLE IF EXISTS payment_audit_logs;
DROP TABLE IF EXISTS subscription_plans;
DROP TABLE IF EXISTS tls_fingerprint_profiles;

-- 删除新增字段
ALTER TABLE groups DROP COLUMN IF EXISTS require_oauth_only;
ALTER TABLE groups DROP COLUMN IF EXISTS require_privacy_set;
ALTER TABLE groups DROP COLUMN IF EXISTS messages_dispatch_model_config;
-- ... 更多字段

五、风险清单与缓解措施

风险 等级 缓解措施
Wire依赖注入冲突 手动合并逐个验证Handler
数据库迁移失败 先在测试环境验证,准备回滚脚本
类型定义不兼容 扩展类型而非替换,保持向后兼容
Sora功能影响其他模块 通过功能开关禁用,确保隔离
前端路由冲突 使用命名空间区分,避免重复
支付功能测试不完整 编写端到端测试,覆盖主要流程

六、合并后检查清单

6.1 功能检查

  • 支付系统功能完整(创建订单、支付、退款)
  • 订阅计划管理正常
  • 渠道管理功能正常
  • TLS指纹配置功能正常
  • OIDC认证功能正常
  • Sora功能开关正常禁用状态
  • Sora配额管理正常
  • 所有API端点正常响应

6.2 代码质量检查

  • 后端测试全部通过
  • 前端类型检查通过
  • 前端lint检查通过
  • 前端构建成功
  • 无console.log残留
  • 无硬编码密钥

6.3 安全检查

  • JWT认证正常
  • API Key认证正常
  • 管理员权限验证正常
  • 支付回调签名验证正常
  • 敏感信息不在日志中泄露

七、时间表

阶段 内容 预计时间
Phase 1 准备阶段 Day 1
Phase 2 Schema层合并 Day 2
Phase 3 后端代码合并 Day 3-4
Phase 4 前端代码合并 Day 5-6
Phase 5 数据库迁移 Day 7
Phase 6 配置合并 Day 7
Phase 7 测试验证 Day 8-10
总计 10天


八、专家审核发现的遗漏项补充

审核日期: 2026-04-14 审核结果: 综合评分 6.7/10 主要问题: Wire合并方案不完整、缺少类型定义、迁移脚本不完整

8.1 必须修复的严重问题

问题1: cmd/server/wire.go 合并方案补充

官方版本新增 (D:\project\sub2api-latest\backend\cmd\server\wire.go):

import (
    "github.com/Wei-Shaw/sub2api/internal/payment"  // 官方独有
)

wire.Build(
    // === 官方新增 Provider ===
    payment.ProvideRegistry,
    payment.ProvideEncryptionKey,
    payment.ProvideDefaultLoadBalancer,
    service.ProvidePaymentConfigService,
    service.ProvidePaymentOrderExpiryService,
)

本地版本 cleanup 函数 (D:\project\sub2apipro\backend\cmd\server\wire.go):

// cleanup 中需要保留
soraMediaCleanup *service.SoraMediaCleanupService

合并后的 cmd/server/wire.go:

import (
    "github.com/Wei-Shaw/sub2api/internal/payment"
    // ... 其他导入
)

wire.Build(
    // === 官方 Provider (必须保留) ===
    payment.ProvideRegistry,
    payment.ProvideEncryptionKey,
    payment.ProvideDefaultLoadBalancer,
    service.ProvidePaymentConfigService,
    service.ProvidePaymentOrderExpiryService,
    
    // === 本地 Provider (保留) ===
    // Sora 相关 Provider...
    
    // ... 其他共同 Provider
)

// cleanup 函数合并
func cleanup(...) {
    // === 官方 cleanup ===
    paymentOrderExpiryService.Stop()
    
    // === 本地 cleanup (保留) ===
    soraMediaCleanup.Stop()
}

问题2: internal/handler/wire.go 结构体定义补充

官方版本 Handlers 结构体:

type Handlers struct {
    Auth           *AuthHandler
    User           *UserHandler
    APIKey         *APIKeyHandler
    Usage          *UsageHandler
    Redeem         *RedeemHandler
    Subscription   *SubscriptionHandler
    Announcement   *AnnouncementHandler
    Admin          *AdminHandlers
    Gateway        *GatewayHandler
    OpenAIGateway  *OpenAIGatewayHandler
    Setting        *SettingHandler
    Totp           *TotpHandler
    Payment        *PaymentHandler        // 官方新增
    PaymentWebhook *PaymentWebhookHandler // 官方新增
}

本地版本 Handlers 结构体:

type Handlers struct {
    // ... 共同字段
    SoraGateway    *SoraGatewayHandler    // 本地独有
    SoraClient     *SoraClientHandler     // 本地独有
}

合并后的 Handlers 结构体:

type Handlers struct {
    // === 共同字段 ===
    Auth           *AuthHandler
    User           *UserHandler
    APIKey         *APIKeyHandler
    Usage          *UsageHandler
    Redeem         *RedeemHandler
    Subscription   *SubscriptionHandler
    Announcement   *AnnouncementHandler
    Admin          *AdminHandlers
    Gateway        *GatewayHandler
    OpenAIGateway  *OpenAIGatewayHandler
    Setting        *SettingHandler
    Totp           *TotpHandler
    
    // === 官方新增 (必须保留) ===
    Payment        *PaymentHandler
    PaymentWebhook *PaymentWebhookHandler
    
    // === 本地新增 (必须保留) ===
    SoraGateway    *SoraGatewayHandler
    SoraClient     *SoraClientHandler
}

合并后的 AdminHandlers 结构体:

type AdminHandlers struct {
    // === 共同字段 ===
    Account        *AccountHandler
    Group          *GroupHandler
    User           *UserHandler
    APIKey         *APIKeyHandler
    Redeem         *RedeemHandler
    Subscription   *SubscriptionHandler
    Usage          *UsageHandler
    Setting        *SettingHandler
    Announcement   *AnnouncementHandler
    Proxy          *ProxyHandler
    Dashboard      *DashboardHandler
    OAuth          *OAuthHandler
    OpenAIOAuth    *OpenAIOAuthHandler
    GeminiOAuth    *GeminiOAuthHandler
    AntigravityOAuth *AntigravityOAuthHandler
    ErrorPassthrough *ErrorPassthroughHandler
    PromoCode      *PromoCodeHandler
    
    // === 官方新增 (必须保留) ===
    TLSFingerprintProfile *TLSFingerprintProfileHandler
    Channel               *ChannelHandler
    Payment               *PaymentHandler
}

问题3: domain/constants.go PlatformSora 常量补充

必须添加到合并版本 (backend/internal/domain/constants.go):

// Platform constants - 合并版本
const (
    PlatformAnthropic   = "anthropic"
    PlatformOpenAI      = "openai"
    PlatformGemini      = "gemini"
    PlatformAntigravity = "antigravity"
    PlatformSora        = "sora"  // ⚠️ 必须添加,否则 endpoint.go 编译失败
)

问题4: OpenAIMessagesDispatchModelConfig 类型定义

官方版本定义 (backend/internal/domain/openai_messages_dispatch.go):

type OpenAIMessagesDispatchModelConfig struct {
    OpusMappedModel    string            `json:"opus_mapped_model,omitempty"`
    SonnetMappedModel  string            `json:"sonnet_mapped_model,omitempty"`
    HaikuMappedModel   string            `json:"haiku_mapped_model,omitempty"`
    ExactModelMappings map[string]string `json:"exact_model_mappings,omitempty"`
}

必须确保合并版本包含此文件,否则 group.gomessages_dispatch_model_config 字段编译失败。

问题5: SoraAccountRepository 接口定义

本地版本定义 (backend/internal/repository/sora_account_repo.go):

type SoraAccountRepository interface {
    GetByAccountID(ctx context.Context, accountID int64) (*SoraAccount, error)
    Upsert(ctx context.Context, accountID int64, accessToken, refreshToken, sessionToken string) error
    UpdateTokens(ctx context.Context, accountID int64, accessToken, refreshToken, sessionToken string) error
    Delete(ctx context.Context, accountID int64) error
}

必须保留此文件,否则 service/wire.goProvideTokenRefreshService 依赖注入失败。


8.2 数据库迁移脚本补充

问题6-10: 迁移脚本完整版本

创建完整迁移文件: backend/migrations/099_merge_migration.sql

-- Migration: 099_merge_migration
-- 合并官方 v0.1.112 与本地版本的数据库迁移
-- 执行前必须先备份数据库

-- ============================================================
-- 第一部分:官方新表(带完整约束)
-- ============================================================

-- payment_orders 表(带外键约束)
CREATE TABLE IF NOT EXISTS payment_orders (
    id BIGSERIAL PRIMARY KEY,
    user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,  -- ⚠️ 添加外键
    user_email VARCHAR(255) NOT NULL DEFAULT '',
    user_name VARCHAR(100) NOT NULL DEFAULT '',
    user_notes TEXT,
    amount DECIMAL(20,2) NOT NULL,
    pay_amount DECIMAL(20,2) NOT NULL,
    fee_rate DECIMAL(10,4) NOT NULL DEFAULT 0,
    recharge_code VARCHAR(64) NOT NULL DEFAULT '',
    out_trade_no VARCHAR(64) NOT NULL DEFAULT '',
    payment_type VARCHAR(30) NOT NULL DEFAULT '',
    payment_trade_no VARCHAR(128) NOT NULL DEFAULT '',
    pay_url TEXT,
    qr_code TEXT,
    qr_code_img TEXT,
    order_type VARCHAR(20) NOT NULL DEFAULT 'balance',
    plan_id BIGINT,
    subscription_group_id BIGINT,
    subscription_days INT,
    provider_instance_id VARCHAR(64),
    status VARCHAR(30) NOT NULL DEFAULT 'PENDING',
    refund_amount DECIMAL(20,2) NOT NULL DEFAULT 0,
    refund_reason TEXT,
    refund_at TIMESTAMPTZ,
    force_refund BOOLEAN NOT NULL DEFAULT FALSE,
    refund_requested_at TIMESTAMPTZ,
    refund_request_reason TEXT,
    refund_requested_by VARCHAR(20),
    expires_at TIMESTAMPTZ NOT NULL,
    paid_at TIMESTAMPTZ,
    completed_at TIMESTAMPTZ,
    failed_at TIMESTAMPTZ,
    failed_reason TEXT,
    client_ip VARCHAR(50) NOT NULL DEFAULT '',
    src_host VARCHAR(255) NOT NULL DEFAULT '',
    src_url TEXT,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_payment_orders_user_id ON payment_orders(user_id);
CREATE INDEX IF NOT EXISTS idx_payment_orders_status ON payment_orders(status);
CREATE INDEX IF NOT EXISTS idx_payment_orders_expires_at ON payment_orders(expires_at);
CREATE INDEX IF NOT EXISTS idx_payment_orders_created_at ON payment_orders(created_at);
CREATE INDEX IF NOT EXISTS idx_payment_orders_paid_at ON payment_orders(paid_at);
CREATE INDEX IF NOT EXISTS idx_payment_orders_order_type ON payment_orders(order_type);

-- subscription_plans 表(带外键约束)
CREATE TABLE IF NOT EXISTS subscription_plans (
    id BIGSERIAL PRIMARY KEY,
    group_id BIGINT NOT NULL REFERENCES groups(id) ON DELETE CASCADE,  -- ⚠️ 添加外键
    name VARCHAR(100) NOT NULL,
    description TEXT NOT NULL DEFAULT '',
    price DECIMAL(20,2) NOT NULL,
    original_price DECIMAL(20,2),
    validity_days INT NOT NULL DEFAULT 30,
    validity_unit VARCHAR(10) NOT NULL DEFAULT 'day',
    features TEXT NOT NULL DEFAULT '',
    product_name VARCHAR(100) NOT NULL DEFAULT '',
    for_sale BOOLEAN NOT NULL DEFAULT TRUE,
    sort_order INT NOT NULL DEFAULT 0,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_subscription_plans_group_id ON subscription_plans(group_id);
CREATE INDEX IF NOT EXISTS idx_subscription_plans_for_sale ON subscription_plans(for_sale);

-- payment_provider_instances 表
CREATE TABLE IF NOT EXISTS payment_provider_instances (
    id BIGSERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL UNIQUE,
    provider_key VARCHAR(50) NOT NULL,
    config TEXT NOT NULL,  -- ⚠️ 注意:应用层需加密存储
    supported_types TEXT NOT NULL DEFAULT '',
    enabled BOOLEAN NOT NULL DEFAULT TRUE,
    refund_enabled BOOLEAN NOT NULL DEFAULT FALSE,
    min_amount DECIMAL(20,2) NOT NULL DEFAULT 1,
    max_amount DECIMAL(20,2) NOT NULL DEFAULT 10000,
    priority INT NOT NULL DEFAULT 0,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- payment_audit_logs 表
CREATE TABLE IF NOT EXISTS payment_audit_logs (
    id BIGSERIAL PRIMARY KEY,
    order_id VARCHAR(64) NOT NULL,
    action VARCHAR(50) NOT NULL,
    old_status VARCHAR(30),
    new_status VARCHAR(30),
    actor_type VARCHAR(20) NOT NULL,
    actor_id BIGINT,
    actor_name VARCHAR(100),
    details TEXT,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_payment_audit_logs_order_id ON payment_audit_logs(order_id);

-- tls_fingerprint_profiles 表
CREATE TABLE IF NOT EXISTS tls_fingerprint_profiles (
    id BIGSERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL UNIQUE,
    description TEXT NOT NULL DEFAULT '',
    client_hello_template JSONB NOT NULL DEFAULT '{}',
    http2_settings JSONB NOT NULL DEFAULT '{}',
    grease_extensions JSONB NOT NULL DEFAULT '{}',
    psk_key_exchange_modes JSONB NOT NULL DEFAULT '{}',
    curves JSONB NOT NULL DEFAULT '{}',
    post_handshake_extensions JSONB NOT NULL DEFAULT '{}',
    sig_algs JSONB NOT NULL DEFAULT '{}',
    supported_versions JSONB NOT NULL DEFAULT '{}',
    min_version INT NOT NULL DEFAULT 771,
    max_version INT NOT NULL DEFAULT 772,
    timeout_ms INT NOT NULL DEFAULT 30000,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- ============================================================
-- 第二部分本地新表完整DDL
-- ============================================================

-- sora_accounts 表 ⚠️ 完整定义
CREATE TABLE IF NOT EXISTS sora_accounts (
    account_id BIGINT PRIMARY KEY REFERENCES accounts(id) ON DELETE CASCADE,
    access_token TEXT NOT NULL,
    refresh_token TEXT NOT NULL,
    session_token TEXT,
    upstream_user_id VARCHAR(128) NOT NULL DEFAULT '',
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- sora_generations 表 ⚠️ 完整定义
CREATE TABLE IF NOT EXISTS sora_generations (
    id BIGSERIAL PRIMARY KEY,
    user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    api_key_id BIGINT,
    group_id BIGINT,
    model VARCHAR(64) NOT NULL,
    prompt TEXT NOT NULL DEFAULT '',
    media_type VARCHAR(16) NOT NULL DEFAULT 'video',
    status VARCHAR(16) NOT NULL DEFAULT 'pending',
    media_url TEXT NOT NULL DEFAULT '',
    media_urls JSONB,
    file_size_bytes BIGINT NOT NULL DEFAULT 0,
    storage_type VARCHAR(16) NOT NULL DEFAULT 'none',
    s3_object_keys JSONB,
    upstream_task_id VARCHAR(128) NOT NULL DEFAULT '',
    error_message TEXT NOT NULL DEFAULT '',
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    completed_at TIMESTAMPTZ,
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_sora_gen_user_created ON sora_generations(user_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_sora_gen_user_status ON sora_generations(user_id, status);
CREATE INDEX IF NOT EXISTS idx_sora_gen_status ON sora_generations(status);

-- ============================================================
-- 第三部分:官方新字段
-- ============================================================

-- groups 表官方新字段
ALTER TABLE groups ADD COLUMN IF NOT EXISTS require_oauth_only BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE groups ADD COLUMN IF NOT EXISTS require_privacy_set BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE groups ADD COLUMN IF NOT EXISTS messages_dispatch_model_config JSONB DEFAULT '{}';

-- usage_logs 表官方新字段
ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS channel_id BIGINT;
ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS model_mapping_chain VARCHAR(500);
ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS billing_tier VARCHAR(50);
ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS billing_mode VARCHAR(20);

-- ⚠️ 添加索引(使用 CONCURRENTLY 避免锁表)
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_usage_logs_channel_id 
    ON usage_logs(channel_id) WHERE channel_id IS NOT NULL;

-- ============================================================
-- 第四部分:本地新字段
-- ============================================================

-- groups 表 Sora 字段
ALTER TABLE groups ADD COLUMN IF NOT EXISTS sora_image_price_360 DECIMAL(20,8);
ALTER TABLE groups ADD COLUMN IF NOT EXISTS sora_image_price_540 DECIMAL(20,8);
ALTER TABLE groups ADD COLUMN IF NOT EXISTS sora_video_price_per_request DECIMAL(20,8);
ALTER TABLE groups ADD COLUMN IF NOT EXISTS sora_video_price_per_request_hd DECIMAL(20,8);
ALTER TABLE groups ADD COLUMN IF NOT EXISTS sora_storage_quota_bytes BIGINT NOT NULL DEFAULT 0;

-- users 表 Sora 字段
ALTER TABLE users ADD COLUMN IF NOT EXISTS sora_storage_quota_bytes BIGINT NOT NULL DEFAULT 0;
ALTER TABLE users ADD COLUMN IF NOT EXISTS sora_storage_used_bytes BIGINT NOT NULL DEFAULT 0;

-- usage_logs 表媒体类型字段
ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS media_type VARCHAR(16);

8.3 完整回滚SQL补充

-- ============================================================
-- 完整回滚脚本: rollback_merge.sql
-- ============================================================

-- Phase 1: 删除外键约束
ALTER TABLE payment_orders DROP CONSTRAINT IF EXISTS fk_payment_orders_user_id;
ALTER TABLE subscription_plans DROP CONSTRAINT IF EXISTS fk_subscription_plans_group_id;

-- Phase 2: 删除新增表(按依赖顺序)
DROP TABLE IF EXISTS sora_generations CASCADE;
DROP TABLE IF EXISTS sora_accounts CASCADE;
DROP TABLE IF EXISTS payment_audit_logs CASCADE;
DROP TABLE IF EXISTS payment_orders CASCADE;
DROP TABLE IF EXISTS subscription_plans CASCADE;
DROP TABLE IF EXISTS payment_provider_instances CASCADE;
DROP TABLE IF EXISTS tls_fingerprint_profiles CASCADE;

-- Phase 3: 删除官方新增字段
ALTER TABLE groups DROP COLUMN IF EXISTS require_oauth_only;
ALTER TABLE groups DROP COLUMN IF EXISTS require_privacy_set;
ALTER TABLE groups DROP COLUMN IF EXISTS messages_dispatch_model_config;

ALTER TABLE usage_logs DROP COLUMN IF EXISTS channel_id;
ALTER TABLE usage_logs DROP COLUMN IF EXISTS model_mapping_chain;
ALTER TABLE usage_logs DROP COLUMN IF EXISTS billing_tier;
ALTER TABLE usage_logs DROP COLUMN IF EXISTS billing_mode;

-- Phase 4: 删除本地新增字段(如果需要完全回滚)
ALTER TABLE groups DROP COLUMN IF EXISTS sora_image_price_360;
ALTER TABLE groups DROP COLUMN IF EXISTS sora_image_price_540;
ALTER TABLE groups DROP COLUMN IF EXISTS sora_video_price_per_request;
ALTER TABLE groups DROP COLUMN IF EXISTS sora_video_price_per_request_hd;
ALTER TABLE groups DROP COLUMN IF EXISTS sora_storage_quota_bytes;

ALTER TABLE users DROP COLUMN IF EXISTS sora_storage_quota_bytes;
ALTER TABLE users DROP COLUMN IF EXISTS sora_storage_used_bytes;

ALTER TABLE usage_logs DROP COLUMN IF EXISTS media_type;

-- Phase 5: 删除新增索引
DROP INDEX IF EXISTS idx_usage_logs_channel_id;
DROP INDEX IF EXISTS idx_sora_gen_user_created;
DROP INDEX IF EXISTS idx_sora_gen_user_status;
DROP INDEX IF EXISTS idx_sora_gen_status;

8.4 前端类型定义补充

补充 OpenAIMessagesDispatchModelConfig 类型 (frontend/src/types/index.ts):

// ⚠️ 必须添加,否则 Group 类型编译失败
export interface OpenAIMessagesDispatchModelConfig {
  opus_mapped_model?: string
  sonnet_mapped_model?: string
  haiku_mapped_model?: string
  exact_model_mappings?: Record<string, string>
}

// 合并后的完整 Group 类型
export interface Group {
  id: number
  name: string
  platform: GroupPlatform
  // ... 其他共同字段
  
  // === 官方字段 ===
  require_oauth_only: boolean
  require_privacy_set: boolean
  messages_dispatch_model_config?: OpenAIMessagesDispatchModelConfig
  default_mapped_model?: string
  
  // === 本地字段 ===
  sora_image_price_360?: number | null
  sora_image_price_540?: number | null
  sora_video_price_per_request?: number | null
  sora_video_price_per_request_hd?: number | null
  sora_storage_quota_bytes: number
}

8.5 安全测试补充清单

测试项 测试内容 预期结果
支付重放攻击 重复发送相同回调 拒绝处理,返回错误
SQL注入测试 输入恶意SQL片段 无注入,正常处理
XSS测试 输入恶意脚本 内容被净化
权限绕过测试 无权限用户访问管理API 返回403
Sora禁用验证 调用Sora API 返回功能禁用提示

九、修订后的时间表

阶段 内容 原计划 修订后
Phase 1 准备阶段 + 备份验证 Day 1 Day 1
Phase 2 Schema层合并 + 类型定义补充 Day 2 Day 2-3
Phase 3 后端代码合并 + Wire完整合并 Day 3-4 Day 4-6
Phase 4 前端代码合并 + 类型补充 Day 5-6 Day 7-8
Phase 5 数据库迁移 + 索引创建 Day 7 Day 9
Phase 6 配置合并 + 安全配置验证 Day 7 Day 9
Phase 7 测试验证 + 安全测试 Day 8-10 Day 10-14
总计 10天 14天

增加时间用于:

  • Wire合并方案的详细验证+2天
  • 安全测试和端到端测试(+2天
  • 迁移脚本的完善和测试验证

十、执行前提条件检查清单

在开始执行合并前,必须满足以下条件:

代码准备

  • 已备份本地版本 (sub2apipro_backup_20260414)
  • 补充 PlatformSora 常量定义
  • 确认 OpenAIMessagesDispatchModelConfig 类型存在
  • 确认 SoraAccountRepository 接口保留
  • 明确 Handlers/AdminHandlers 最终结构

迁移准备

  • 补充完整的 sora_accounts 表 DDL
  • 补充完整的 sora_generations 表 DDL
  • 添加 channel_id 索引脚本
  • 添加外键约束脚本
  • 准备完整回滚脚本

测试准备

  • 编写支付端到端测试用例
  • 编写安全测试用例(重放攻击、注入测试)
  • 编写 Sora 禁用验证测试

备份准备

  • 完整备份数据库
  • 验证备份恢复流程
  • 准备迁移监控脚本

文档版本: v2.0 (专家审核后修订版) 最后更新: 2026-04-14 审核评分: 6.7/10 → 修复后预期 9/10