- CHANGELOG [Unreleased]: consolidated the duplicated Added/Changed/Fixed groups left by earlier batches into one group each (no entry dropped); added batch C entries — port-based restructure, media/upload domain packages, sweep moved to scanner ticker (B16), router contract test, portsfake unit-test layer. Upload-sweep wording no longer promises the old 24h opportunistic request-path behaviour. - README.md / README_zh.md: new 'Backend structure' section documenting the port/fake layout (cmd/webui composition root, handlers as HTTP-only, internal/ports + portsfake, media/upload/store/scanner/bookfile responsibilities) and the two-tier testing approach (real PG+Redis integration vs fake-injected unit, route table pinned by contract test) Gate: gofmt clean, go vet clean, go test -p 1 all pass (0 skip), scripts/smoke.sh ALL SMOKE TESTS PASSED against a live webui on :18080
6.6 KiB
Book & Comic Library
个人书库/漫画库:Go+Gin 后端(扫描/上传入库、多用户 JWT、阅读进度、磁盘+Redis 缓存)+ Docker Compose 部署。设计见 superpowers/specs/2026-09-04-book-comic-library-design.md;部署规范见 superpowers/specs/2026-09-07-docker-deploy-spec-design.md。英文镜像:README.md。
Deploy mode
- release:
deploy/docker-compose.yml— 多阶段backend|frontend/Dockerfile.prod(target runner)编译产物,不挂源码;infra 端口只在容器网络。 - dev:
deploy/docker-compose.dev.yml— 四服务全容器化,源码挂../backend:/app、../frontend:/app(web 服务带匿名node_modules卷),target: dev镜像;api 跑go mod download && dlv debug ./cmd/webui(热重启不 rebuild,delve :2345);infra 暴露宿主 PG 5432 / Redis 6379;healthcheck 门控。
Volume Mount
- 共享 named volumes:
booklib_postgres_data、booklib_redis_data(两个 compose 同名,dev/release 看到同一份数据;仅down -v清除)。 - 配置/日志绑定挂载:
deploy/nginx/{nginx.conf,conf.d/default.conf}、deploy/redis/redis.conf(:ro)与deploy/logs/nginx—— 全部来自deploy/prepare.sh(模板在各产物同目录的*.tpl),容器里配置不对/缺失 = 忘了重跑它。 - 文件存储:
deploy/api/storage→ 容器/data/books(缓存写/data/books/cache)。
跑起来(生产形态)
cp deploy/.env.example deploy/.env # 填 JWT_SECRET、ADMIN_USER、ADMIN_PASSWORD(≥8 位,低于 8 位 seed 会跳过并 log)
deploy/prepare.sh
docker compose -f deploy/docker-compose.yml up -d --build
bash scripts/smoke.sh && bash scripts/smoke-web.sh
- web:
http://localhost:8080(WEB_PORT可改),API 走 nginx/api/前缀反代到无状态 api 副本(--scale api=N)。 - 原始书放进
deploy/api/storage/(挂到/data/books),scanner 周期入库(默认 60s)。 - nginx access/error 日志:
deploy/logs/nginx/。
开发
deploy/prepare.sh
docker compose -f deploy/docker-compose.dev.yml up -d --build
- 前端: http://localhost:5173(vite,HMR 直接生效;
/api代理到容器内 api)。 - Go 改码后:
docker compose -f deploy/docker-compose.dev.yml restart api(重编译挂载源码,无需 rebuild)。 - 断点调试:delve headless 在
localhost:2345(VSCode launch:{"type":"go","request":"attach","mode":"remote","host":"localhost","port":2345,"substitutePath":[{"from":"${workspaceFolder}/backend","to":"/app"}]};命中断点后用 dlv 命令继续)。 - 直连基础设施跑测试:PG
localhost:5432(lib/lib/lib)、Redislocalhost:6379:
cd backend
export DATABASE_URL='postgres://lib:lib@localhost:5432/lib?sslmode=disable'
export REDIS_URL='redis://localhost:6379'
go vet ./... && gofmt -l .
go test -p 1 -count=1 ./...
-p 1 是必须的:集成测试共用同一个 PG 库,各自 DELETE FROM ... 清表——并行跑会互相删数据导致随机失败。dev 栈起停用 down(不是 rm),否则匿名 node_modules 卷成孤儿。无 PG/Redis 时依赖它们的测试自动 skip;Redis 挂掉不影响功能(全链路降级为 miss/放行,见 spec §9)。
前端门槛:cd frontend && npm run check(tsc + vitest + vite build)。
旧卷迁移(一次性,升级自上一版部署)
# 老 PG 数据 → 新共享卷
docker run --rm -v book-comic-library_pgdata:/from -v booklib_postgres_data:/to alpine cp -a /from/. /to/
# 老 cache 卷是封面/解压派生数据,直接丢弃(自动重建)
docker volume rm book-comic-library_pgdata book-comic-library_cache
书库文件:原宿主 ./library/ 的内容移入 deploy/api/storage/。
可信代理与限流
- nginx 在 compose 网络内,api 的
ClientIP只信TRUSTED_PROXY_CIDRS(逗号分隔 CIDR,默认172.16.0.0/12,即 compose 网段)。外部伪造X-Forwarded-For换不掉限流桶;换部署网络时改这个 env。 - 登录限流 5 次/分钟/IP 按尝试计数,成功登录也计——爆破和正常高频登录同账。
改 schema 前必读
Schema 变更通过 backend/internal/db/migrations/ 中的有序迁移系统执行:
- 新建文件:
NNNN_description.sql(四位序号,小写下划线命名)。 - 已应用的迁移文件不可修改——它们是不可变的。
- 不支持 down 迁移:回滚靠数据库备份,fix-forward。
- 已有数据库在首次启动时自动基线化(0001 标记为已应用,不重跑 DDL)。
- 迁移使用
pg_advisory_lock确保--scale api=N副本串行执行。
每批合入前的本地门禁:go vet ./... && gofmt -l . && go test -p 1 -count=1 ./...(需启动 dev PG+Redis)。
后端结构
后端按消费端接口组织(六边形风格):
cmd/webui—— 二进制入口与装配根:main.go构造具体实现(store.Store、redispkg.R、scanner.Scanner、media.M、upload.U)并交给api.NewRouter,后者只接受 port 接口。cmd/webui/handlers—— HTTP 层:参数绑定、认证/鉴权、错误→状态码映射。没有 SQL,没有压缩包/文件逻辑。internal/ports—— handlers 依赖的小口径接口(UserStore、LibraryStore、BookStore、ProgressStore、BookmarkStore、RateLimiter、Scanner、Media、UploadSessions)与共享 sentinel 错误。接口定义在消费端,实现方来满足它们。internal/ports/portsfake—— 全部 port 的手写内存 fake,错误语义与真实 store 一致(pgx.ErrNoRows、ErrLastAdmin、PgError 23505)。handler 单测无需 PG/Redis。internal/media—— 封面/页抽取、页索引缓存(Redis)、缓存原子写。internal/upload—— 分片上传会话生命周期(init/part/status/complete/sweep)。internal/store—— 所有 SQL,集中一处。internal/scanner—— 书库遍历、ingest(增/改/删一趟完成)、会话清扫搭扫描 ticker 顺风车。internal/bookfile—— 共享文件工具(SafeName、Contains、Hash、FormatFromExt、缓存目录布局)。
测试分两层:集成测试走真实 PG+Redis、过完整 router(handlers/*_test.go 的 setupAPI);单测注入 portsfake、过同一个 router(handlers/*_unit_test.go)。路由表本身由 cmd/webui/api 的 TestRouterContract 钉死。
CI
- 工作流:
.github/workflows/ci.yml(标准 GitHub Actions 语法,兼容 Gitea Actions)。 - Gitea:注册
act_runner实例,在仓库设置中启用 Actions,开箱即用。 - GitHub:开箱即用。
- Runner 注册前,合入前手动执行本地门禁。
PWA
不可变资源(封面/CBZ 页/原文件)SW cache-first,读过的内容离线可翻;登出会清 SW 缓存。