【直播切片工作台】第2章:技术选型与分层架构设计

第2章:技术选型与分层架构设计

本章回答「为什么是这些技术」:Go + Gin、GORM + PostgreSQL、zap、原生 HTTP 客户端调外部 AI 服务,以及 handler → service → repository → model 的分层纪律。选型理由将结合仓库实际代码给出,而非泛泛而谈。

流程

技术选型的输入是业务约束:任务以小时计的长耗时外部调用(ASR 轮询、LLM 推理、视频生成)、高并发的 IO 等待而非 CPU 计算、需要多实例水平扩展、团队希望运维面尽量小。处理过程就是逐层匹配:HTTP 层选 Gin(生态成熟、路由性能足够);数据层选 GORM + PostgreSQL(JSONB 能力是硬需求,clips/段落/词级时间戳都是嵌套结构);日志选 zap(结构化 + 低开销);外部服务(豆包 ASR、OpenAI 兼容 LLM、capcut-mate)全部用标准库 net/http 自封装薄客户端,不引 SDK------因为这三家的协议都很简单,SDK 反而带来版本与代理配置负担。

分层的处理规则非常严格:handler 只做「绑定参数 → 调 service → 写 response」,不含业务判断;service 编排 repository 与 Worker,是业务规则的唯一归宿;repository 只见 GORM 与 model;model 不依赖任何上层。跨模块的通用能力下沉到 internal/pkg/,按领域(asr、llm、capcutmate、storage、media)成包,包内自洽、可独立测试。

输出是两条清晰的依赖方向:handler → service → repository → model(业务主链)与 任何人 → pkg(能力下沉)。main.go 是唯一同时看到所有层的地方(组合根)。

实现

各选型在代码中的落点:

  • Gin :路由分组天然支持 /v1/v2 版本化与 JWTAuth 中间件挂载(第 19 章);c.Set/Get 承载认证用户上下文。
  • GORMserializer:json 标签把 []ClipRange[]ASRParagraph 映射到 JSONB 列,读写无需手工序列化;AutoMigrate 承担建表(第 13 章)。
  • PostgreSQLjsonb 列 + 索引。任务与素材的状态列(statusasr_status)都是普通字符串 + B-tree 索引,Worker 扫描 WHERE status='pending' 高效。
  • zap :全局结构化日志,zap.String("job_id", ...) 风格贯穿所有层;lumberjack 负责轮转(第 7 章)。
  • cobra :仅用于 cmd/envinit 命令行(schema/seed/init/reinit/reset-password 五个子命令)。
  • 标准库 http.Client :asr/llm/capcutmate 三个客户端各自持有一个带 Timeout 的 http.Client,超时即配置(LLM 600 秒、ASR 提交 30 秒、capcut-mate 120 秒),差异化的超时策略是自封装的核心收益。

为什么不用 Redis 队列、消息总线、微服务拆分?仓库的答案写在架构里:任务并发数是配置项(默认 3~6 个 Worker/类任务),瓶颈在外部 AI 服务而非本进程;多实例安全靠数据库乐观锁即可(第 15 章);拆微服务会让「一键成片」这类跨三个 Worker 的编排(第 37 章)变复杂。这是一份教科书级的「适度设计」样本。

📌 设计决策

  • LLM 客户端默认指向阿里云 DashScope 的 OpenAI 兼容端点,而非 OpenAI 本体:国内可达性优先,同时保留换模型的能力(改 base_url 即可)。
  • ASR 选豆包 BigModel 录音文件识别:支持 utterances 分句、说话人分离、词级时间戳,是后续切片精度的数据基础。
  • 前端不参与任何长任务执行,全部通过轮询 Task 状态推进 UI,保证刷新/断线后状态可恢复。

代码示例

「外部依赖薄客户端」的典型形态------LLM 包只依赖标准库,配置即结构体:

go 复制代码
// internal/pkg/llm/client.go(节选)
const (
    DefaultBaseURL = "https://dashscope.aliyuncs.com/compatible-mode/v1"
    DefaultModel   = "qwen3.7-plus"
    DefaultTimeout = 600 * time.Second
)

type Config struct {
    APIKey     string
    BaseURL    string
    Model      string
    HTTPClient *http.Client // 可注入,单测替换
    Timeout    time.Duration
}

func NewClient(cfg Config) *Client {
    if cfg.BaseURL == "" { cfg.BaseURL = DefaultBaseURL }
    if cfg.Model == ""   { cfg.Model = DefaultModel }
    if cfg.Timeout <= 0  { cfg.Timeout = DefaultTimeout }
    if cfg.HTTPClient == nil {
        cfg.HTTPClient = &http.Client{Timeout: cfg.Timeout}
    }
    return &Client{cfg: cfg, http: cfg.HTTPClient}
}

分层纪律的一个缩影------service 定义小接口反转依赖,Worker 与单测都面向它:

go 复制代码
// internal/service/ai_slice_worker.go(节选)
// LLMChatClient AI 切片 / ASR 后处理所需的大模型对话接口,便于单测替换。
type LLMChatClient interface {
    Chat(ctx context.Context, messages []llm.ChatMessage) (string, error)
    // ChatStructured 用于需严格 JSON 的场景:temperature=0 + json_object,并关闭思考模式。
    ChatStructured(ctx context.Context, messages []llm.ChatMessage) (string, error)
    // ChatThinking 显式开启思考模式(AI 切片等需更深推理的场景)。
    ChatThinking(ctx context.Context, messages []llm.ChatMessage) (string, error)
}

小结

  • 选型主线:IO 密集 + 长任务 → Go 单进程多 Worker;嵌套数据 → JSONB;外部 AI → 自封装 HTTP 薄客户端。
  • 分层铁律:业务规则只在 service;数据访问只在 repository;pkg 只做能力不下沉业务。
  • 「少一个中间件就少一份运维成本」是本项目反复出现的取舍。

思考题

  1. 自封装 HTTP 客户端何时应该升级为引入官方 SDK(提示:鉴权刷新、流式、分页)?
  2. 若 clips 数据规模膨胀(数万段/项目),JSONB 方案何时需要让位给关联表?

项目信息