第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承载认证用户上下文。 - GORM :
serializer:json标签把[]ClipRange、[]ASRParagraph映射到 JSONB 列,读写无需手工序列化;AutoMigrate承担建表(第 13 章)。 - PostgreSQL :
jsonb列 + 索引。任务与素材的状态列(status、asr_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 只做能力不下沉业务。
- 「少一个中间件就少一份运维成本」是本项目反复出现的取舍。
思考题
- 自封装 HTTP 客户端何时应该升级为引入官方 SDK(提示:鉴权刷新、流式、分页)?
- 若 clips 数据规模膨胀(数万段/项目),JSONB 方案何时需要让位给关联表?
项目信息
- GitHub仓库:github.com/Chyona/live-mixer
- 项目案例:gogoshine.com