# Fashion API 秀场 / 街拍 / 品牌档案后端服务(Go + Gin + GORM / PostgreSQL + pgvector)。 一个服务同时承载三块职责,通过**三个物理隔离的端口**对外提供: | 端口 | 用途 | 暴露面 | | --- | --- | --- | | `8090` | 对外公开 API(走秀 / 街拍 / 品牌 / 账号) | 公网(nginx 反代) | | `8091` | SSG 构建期内部接口(全量 / 热门数据) | 仅 `127.0.0.1`(回环)+ 可选 token | | `8092` | 管理后台(后台 UI + 爬虫 ingest 上报) | 内网,独立绑定 / 限流 | --- ## 1. 架构分层 采用 **Layered / 主动式 Repository** 架构,`cmd/` 是唯一依赖装配点(组合根),业务代码全部在 `internal/`,不对外暴露。 ``` cmd/ 组合根:只做依赖装配,不含业务逻辑 server/ 启动 HTTP 服务(三端口)+ 入库 worker + 优雅关闭 dbtool/ 跨环境数据搬运(dump / import) dbdiag/ 数据库诊断 internal/ config/ 配置加载(yml + 环境变量覆盖 + 默认值兜底) model/ 实体定义(gorm tag + json tag,PasswordHash 永不出参) database/ PostgreSQL 连接池 / 自动迁移 / 去重 schema repository/ 数据访问(接口 + GORM 实现,查询条件用 Scope 安全组合) service/ 业务逻辑(JWT 签发、图片治理、去重、审核、入库管线) dto/ 请求 / 响应结构体与边界常量 handler/ HTTP 层(仅做参数解析与响应封装) middleware/ 鉴权 / CORS / 前端签名 / ingest 验签 / SSG token router/ 路由注册(公开 / SSG / 后台三个引擎) pkg/ 可复用工具(hashid / jwt / phash / imgurl / storage / ...) ``` **核心约束**: - **依赖注入取代全局单例**:`*gorm.DB`、`*jwt.Manager` 全部通过构造函数显式注入,handler / service 可单独单测。 - **GORM Scope 规避 Statement 复用**:查询条件封装为 `func(*gorm.DB) *gorm.DB`,避免复用同一实例导致 WHERE 串味。 - **排序字段白名单**:`orderBy` 只允许白名单内的值,杜绝 SQL 注入。 - **对外只暴露编码 ID**:自增主键经 HashID(Feistel 混淆)编码后才出参,防爬虫顺序枚举。 --- ## 2. 接口清单 ### 2.1 对外公开 API(8090,前缀 `/api/v1`) | 接口 | 方法 | 说明 | | --- | --- | --- | | `/api/health` | GET | 健康检查(不随版本演进) | | `/api/v1/public/runway-looks` | GET | 走秀列表(分页 / 筛选 / 排序,每篇附带前 6 张缩略图) | | `/api/v1/public/runway-looks/:id` | GET | 走秀详情(含完整图片集,未登录仅预览前 5 张) | | `/api/v1/public/street-snaps` | GET | 街拍列表 | | `/api/v1/public/street-snaps/:id` | GET | 街拍详情 | | `/api/v1/public/brands` | GET | 品牌列表(A-Z 索引 / 搜索 / featured 精选) | | `/api/v1/auth/login` | POST | 登录(`account` 可为用户名或邮箱,签发双令牌) | | `/api/v1/auth/refresh` | POST | 用 refresh token 换新 access token | | `/api/v1/auth/sessions/current` | DELETE | 单设备登出(吊销当前 refresh) | | `/api/v1/auth/sessions` | DELETE | 全设备登出 / 踢下线 | | `/api/v1/me` | GET | 当前用户(需 Bearer) | | `/api/v1/me/favorites` | GET / POST / DELETE | 收藏列表 / 新增 / 删除 | | `/api/v1/me/favorites/checks` | POST | 批量校验哪些 id 已收藏(与收藏总量解耦) | | `/api/v1/me/history` | GET / POST / DELETE | 浏览历史列表 / 记录 / 清空 | | `/api/v1/me/history/:uid` | DELETE | 删除单条历史 | > 列表接口「首页公开、翻页需登录」:`page=1`(或缺省)无需 token,`page>1` 要求 Bearer,防爬虫全量 dump。详情接口对未登录请求截断到 5 张并打 `preview=true`。 > 旧路径(`/api/v1/auth/me`、`/api/v1/auth/favorites*`、`/api/v1/auth/logout` 等)保留为兼容别名,待旧前端下线后删除。 ### 2.2 SSG 内部接口(8091,前缀 `/api/internal/ssg`) 仅供 astro 构建期调用,回环绑定 + 可选 `SSG_TOKEN` 双重防护: | 接口 | 说明 | | --- | --- | | `/api/internal/ssg/home/runway` | 首页 runway 区块 | | `/api/internal/ssg/brands/popular` | 热门品牌(前 30) | | `/api/internal/ssg/street-snaps/popular` | 热门街拍(按图片数前 20) | ### 2.3 管理后台(8092) - `/admin/login`、`/admin/logout`:后台登录(HttpOnly cookie 会话)。 - `/admin/*`:品牌 / 走秀 / 街拍管理、内容审核(单表 + `status`:通过置 `published`、驳回置 `rejected`)、入库任务监控与重试、用户管理(提级 VIP)。 - `/admin/internal/ingest`:爬虫上报入口,**HMAC-SHA256 验签 + nonce 防重放**(`INGEST_SECRET`)。 --- ## 3. 关键设计 - **双令牌认证**:access token 短命无状态(JWT)+ refresh token 长命落库(可吊销、可踢下线)。refresh 只存 SHA256 哈希。 - **HashID 混淆**:32-bit 平衡 Feistel 网络 + 类型化子密钥,相邻 id 编码无规律,且同数字主键在不同类型下编码完全不同。 - **图片治理**: - 库里只存对象 key(`runway/.jpg`)或本地相对路径,完整 URL 由 `imgurl.Composer` 渲染时拼装(换域名只改配置)。 - key 以内容 sha1 命名 → 重爬天然幂等、不产生孤儿文件。 - 删除图集时按**跨表引用计数**判定孤儿,归零才真删对象;删除动作异步入队,不阻塞请求。 - **图片去重**:`phash`(dHash 向量)近重复标记 + HNSW 索引加速;存储 key 用内容 sha1 寻址(重爬幂等、不产生孤儿文件)。 - **爬虫入库管线**:`ingest_jobs` 队列 + worker(`SELECT ... FOR UPDATE SKIP LOCKED` 多实例安全)异步下载图、补季节码、**直写正式表**(`status=pending`,已无草稿表);后台人工审核只改状态(`published` / `rejected`),不重建图片;公开读走 `public_*` 只读视图,未发布内容结构性不可见。失败按指数退避重试,单图失败整任务回滚。 - **图片质量模型**(2026-09-11 起):所有用户同质量,统一走 CoreIX 公开样式(详情 `high` / 列表 `thumb`),付费墙已取消。 --- ## 4. 配置 唯一配置源:`configs/config.yml`。加载优先级 **环境变量 > yml > 代码内置默认值**。 | 环境变量 | 覆盖项 | | --- | --- | | `SERVER_PORT` / `GIN_MODE` | 公开端口 / 运行模式 | | `SSG_PORT` / `SSG_TOKEN` | SSG 内部端口 / 访问令牌 | | `BACKSTAGE_PORT` | 后台端口 | | `HASHID_SECRET` | 公开 ID 混淆盐值(生产必填) | | `DB_HOST` / `DB_PORT` / `DB_USER` / `DB_PASSWORD` / `DB_NAME` | 数据库连接 | | `JWT_SECRET` / `JWT_EXPIRE_HOURS` / `JWT_REFRESH_EXPIRE_HOURS` | 令牌密钥 / 有效期 | | `CLIENT_SIGN_ENABLED` / `CLIENT_SIGN_SECRET` / `CLIENT_SIGN_TTL` | 公开接口前端签名 | | `UPLOAD_DIR` / `UPLOAD_URL_PREFIX` | 本地静态资源目录 / 前缀 | | `INGEST_SECRET` / `INGEST_TTL` | 爬虫上报 HMAC 密钥 / 时间戳容忍窗口 | | `S4_ENABLED` / `S4_AK` / `S4_SK` / `S4_BUCKET` / `S4_ENDPOINT` / `S4_BASE_URL` / `S4_STYLE_DISPLAY` / `S4_STYLE_THUMB` | 缤纷云 S4 对象存储 | > 生产环境务必通过环境变量覆盖 `JWT_SECRET`、`HASHID_SECRET`、`INGEST_SECRET`、`S4_AK/SK` 与数据库口令,切勿在 yml 中提交真实密钥。 --- ## 5. 运行 ### 前置要求 - Go 1.26+ - PostgreSQL 16(含 pgvector 扩展,见 `scripts/pgvector` 的 Docker 一键起库) ### 构建与启动 ```bash # 主服务 go build -o bin/server ./cmd/server # 跨环境数据搬运工具(dump / import) go build -o bin/dbtool ./cmd/dbtool # 启动(默认读取 configs/config.yml) ./bin/server ./bin/server -config /path/to/config.yml ``` 启动后:公开服务 `http://localhost:8090/api/health` 应返回 `{"status":"ok"}`。 ### 数据库 表结构**不由服务启动自动创建**:全库「结构 + 索引 + 约束 + 数据」统一由 dbtool 导出的**单个纯 SQL 文件**维护(不依赖 pg_dump)。 ```bash # 导出(源机器) go run ./cmd/dbtool dump -out db/backups/db_dump.sql # 导入(目标机器;库须为空,且已启用 pgvector 扩展) psql -U fashion -d fashion -v ON_ERROR_STOP=1 -f db/backups/db_dump.sql ``` 目标库若尚未启用 pgvector,导出时加 `-with-extension`,让文件自带 `CREATE EXTENSION IF NOT EXISTS vector`。 ### 优雅关闭 监听 `SIGINT` / `SIGTERM`,等待在途请求完成(最长 `shutdown_timeout` 秒)后退出,并通知入库 worker 停止领取新任务。 --- ## 6. 目录结构 ``` backend/ ├── configs/config.yml # 唯一配置源 ├── cmd/ │ ├── server/ # 组合根:三端口 HTTP + 入库 worker │ ├── dbtool/ # 全库导出为纯 SQL(结构 + 索引 + 数据) │ └── dbdiag/ # 数据库诊断 ├── internal/ │ ├── config/ # 配置加载 │ ├── model/ # 实体(brand / runway / street_snap / user / ingest) │ ├── database/ # 连接池(结构由 dbtool 导出的 SQL 维护,不做 DDL) │ ├── repository/ # 数据访问接口 + GORM 实现 │ ├── service/ # 业务逻辑 │ ├── dto/ # 请求 / 响应结构体 │ ├── handler/ # HTTP 层 │ ├── middleware/ # 鉴权 / CORS / 签名 / 验签 │ ├── router/ # 公开 / SSG / 后台三引擎路由 │ └── pkg/ # hashid / jwt / phash / imgurl / storage / ... ├── scripts/ # pgvector 起库脚本等 ├── go.mod / go.sum ├── Dockerfile ├── Makefile └── README.md ```