影视聚合站:统一搜索多个资源站、聚合豆瓣与 TMDB 资料、在线播放、观影记录、弹幕和个性化推荐。
技术栈:Go 1.25 · Gin · Go HTML Template · HTMX · PostgreSQL 15+ · Docker Compose
服务端渲染为主,浏览器端只做增强;Web 与后台任务分两个进程运行,避免抓取和计算拖垮用户请求。
- 公开页面:首页、统一搜索、趋势、发现、影视详情、播放、观看记录、推荐、相似内容、片场、用户公开片单、IPTV 和 TVBox。
- 用户能力:注册、登录、设置、头像、想看/看过、短评、回复、点赞、播放进度同步和月度观影报告。
- 播放能力:HLS/M3U8、FLV、MP4、倍速、全屏、画中画、弹幕、手动换源,以及限定在同一集内的自动故障切换。
- 追剧更新:未完结剧集在详情页和播放页展示下一集播出日期,首页展示在看剧集的今日更新。
- 资料与推荐:豆瓣资料和短评、TMDB 剧照与季集、外部 ID 映射、向量、相似内容、个性化推荐和热门快照。
- 管理与运维:资源站与过滤规则管理、媒体匹配复核、版权/分类管理、反馈处理、数据生命周期操作和指标接口。
这是代码已提供的能力清单,不代表每一项都已完成生产验收。
需要一个可访问的 PostgreSQL,以及名为 postgres_default 的外部 Docker 网络。
cp .env.example .env
# 编辑 .env:至少填 DB_HOST / DB_PASSWORD / DB_NAME / APP_SECRET / SITE_URL
docker compose up -d --build
docker compose ps# 1. 建一个独立数据库(库名不能叫 moovie,会被拒绝启动)
psql -h 127.0.0.1 -U postgres -c 'CREATE DATABASE moovie_new;'
# 2. 配置
cp .env.example .env
# 编辑 .env:DB_* 指向刚建的库,DB_AUTO_MIGRATE=true,JOBS_IN_WEB=true
# 3. 启动(首次会自动建表)
go run ./cmd/web访问 http://localhost:5008,另开终端验证:
curl http://127.0.0.1:5008/health # 只看进程是否活着
curl http://127.0.0.1:5008/ready # 会真正查一次数据库新手最容易踩的坑是 JOBS_IN_WEB:
| 取值 | 含义 | 适用场景 |
|---|---|---|
true |
Web 进程内启动后台任务 Dispatcher,单进程搞定 | 本地开发 |
false |
Web 不跑后台任务,必须另外启动 go run ./cmd/worker |
Compose、生产 |
填了 false 又没起 Worker,页面照样能打开,但豆瓣同步、资料刷新、剧照、向量、热门快照全都不会执行。不要把"页面能打开"当成 Worker 在跑。
完整的环境准备、扩展依赖、端口冲突处理见 开发与发布。
flowchart LR
user["浏览器 / TVBox"]
subgraph web_process["Web 进程 cmd/web"]
mw["中间件<br/>限流 · 超时 · 过载保护"]
svc["Handler → Service"]
end
subgraph worker_process["Worker 进程 cmd/worker"]
disp["Dispatcher<br/>轮询 worker_jobs"]
end
pg[("PostgreSQL")]
ext["资源站 / 豆瓣 / TMDB<br/>Ollama / 弹幕"]
user --> mw --> svc
svc --> pg
svc --> ext
svc -. "写入待办任务" .-> pg
pg -. "取出待办任务" .-> disp
disp --> pg
disp --> ext
两条关键设计:
- 用户请求和后台抓取分进程。 Web 只做"尽快返回页面"这一件事;耗时的资料抓取、向量生成、豆瓣同步先写进
worker_jobs表,由 Worker 慢慢做,结果回写数据库。 - 每类资源都有硬上限。 HTTP 在途请求、重请求、图片代理、数据库连接、外部主机连接都有信号量。超限时主动返回
503 + X-Moovie-Overload,而不是让请求无限堆积直到 OOM。
想改一个页面?按这个顺序找代码:
cmd/web/main.go 找路由
→ internal/<模块>/handler.go 读参数、选响应格式
→ internal/<模块>/service.go 业务规则
→ internal/<模块>/postgres.go SQL
→ web/templates/pages/... 页面显示
→ web/static/js/app.js 浏览器端增强(如果有)
查某个 URL 的入口:
rg -n 'router\.(GET|POST|PUT|PATCH|DELETE)' internal更完整的架构图、请求时序图、启动阶段和分层原则见 架构说明。
cmd/
web/ Web 入口:装配依赖、注册路由、启动和关闭
worker/ Worker 入口:后台任务 Dispatcher
dbmigrate/ 受控执行数据库 migration
burstcheck/ 压测小工具,验证过载保护
internal/
platform/ 配置、数据库、HTTP 中间件、认证、模板渲染、缓存
search/ 资源站搜索、缓存、熔断
mediaidentity/ 统一媒体身份、外部 ID、季集、播放候选
playback/ 详情、播放、候选排序、热门快照
catalog/ 豆瓣/TMDB 资料、剧照、向量、发现页
history/ 播放进度与多设备同步
identity/ 用户、登录态、权限
social/ 片场、短评、点赞、回复
workqueue/ 全站唯一的后台任务队列
admin/ 后台管理
... 其余模块见架构说明
web/
templates/ Go HTML 模板
static/ CSS、JavaScript、图片、PWA
docs/ 架构、流程、数据库和运维文档
每个模块内部统一是 handler.go → service.go → store.go → postgres.go 四层。每个包的第一个文件顶部都有中文包说明,想快速了解一个模块先看它:
head -12 internal/workqueue/dispatcher.go完整目录清单和分层原则见 架构说明。
全部配置项及注释见 .env.example(58 项,每项都有中文说明)。必须自己填的只有这几个:
| 变量 | 说明 |
|---|---|
DB_HOST / DB_PORT / DB_USER / DB_PASSWORD / DB_NAME |
数据库连接。DB_NAME=moovie 会被拒绝,防止误连旧库 |
APP_SECRET |
会话签名密钥,生产要求 ≥32 字节且不能是示例值 |
SITE_URL |
站点绝对地址,用于分享链接和 sitemap;生产必须 HTTPS |
JOBS_IN_WEB |
见上文表格 |
DB_AUTO_MIGRATE |
本地首次设 true 自动建表,之后建议改回 false |
可选增强(不填就自动降级,不影响基础页面):TMDB_API_TOKEN、OLLAMA_HOST、DANMU_API_BASE。
分组说明和生产强制校验规则见 开发与发布。
go build ./...
go vet ./...
go test -p 1 ./...
go test -race -p 1 ./...
-p 1不能省:各测试包共用同一个数据库,并行跑会互相 TRUNCATE 掉数据。本地同时开着go run ./cmd/web也会干扰测试。
数据库结构变更:在 internal/platform/database/migrations/ 追加新编号的 SQL 文件,不要改已有文件(已执行过的版本不会重跑)。
改了代码,页面为什么没变化?
按顺序检查:
- 浏览器访问的端口是不是当前
go run的端口。 - 是否还有旧的后台进程监听同一端口(
lsof -nP -iTCP:5008 -sTCP:LISTEN)。 - 改的是完整页面还是 HTMX partial。
WEB_ROOT是否指向当前仓库的web/。- 浏览器是否缓存了
/static/资源,开发时强制刷新。 - 模板加载错误通常在启动时就会打印,别忽略终端日志。
/health 正常但 /ready 返回 503?
进程活着,但数据库连接或查询没在两秒内成功。检查 PostgreSQL 是否启动、.env 里的主机/端口/密码/库名、连接池是否耗尽、数据库是否被慢查询占满。
注意:/ready 失败不应该触发容器重启,那只会让情况更糟。
搜索页能打开但没有结果?
先区分"本地结果为空"和"上游请求失败"。检查资源站是否启用、来源健康状态、超时日志和熔断冷却。单个来源失败不代表整个搜索模块坏了——降级策略就是这么设计的。
压力一大就看到 503,是 bug 吗?
不是,是主动保护。看响应头 X-Moovie-Overload 判断是全局、重请求还是图片槽位满了。受控 503 比请求无限堆积后容器 OOM 更容易恢复。详见 并发与容量。
没启动 Worker,网站还能用吗?
基础页面和只读请求可以正常使用,但豆瓣同步、资料、短评、剧照、向量和热门快照不会被执行。去数据库看 worker_jobs 的状态,不要把"任务待处理"误判成 Web 请求失败。
为什么有 /play 和 /watch 两个播放入口?
分工不同,不能合并:/play/:source_key/:vod_id 直接播某个资源站的资源,不需要豆瓣关联;/watch/:douban_id 先确定媒体身份再跨站挑最优线路。详见 业务流程。
| 文档 | 内容 |
|---|---|
| 架构说明 | 名词表、架构图、请求时序、启动阶段、目录分层、代码阅读顺序 |
| 业务流程 | 搜索、播放、观看历史、追剧更新、资料与推荐五条链路 |
| 数据库表结构说明 | 30 张表的分层设计、字段含义和核心数据流 |
| 并发与容量 | 过载保护机制、默认边界、容器资源、压测基线 |
| 开发与发布 | 本地环境、配置详解、migration、测试门禁、Compose、上线回滚 |
| 高并发运维手册 | 生产重启诊断、容量评估和处置流程 |
专题设计文档: