Files
backend_v2/README.md
2026-09-20 01:06:13 +08:00

187 lines
9.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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/*`:品牌 / 走秀 / 街拍管理、草稿审核(通过晋升正式表 / 驳回)、入库任务监控与重试、用户管理(提级 VIP)。
- `/admin/internal/ingest`:爬虫上报入口,**HMAC-SHA256 验签 + nonce 防重放**(`INGEST_SECRET`)。
---
## 3. 关键设计
- **双令牌认证**:access token 短命无状态(JWT)+ refresh token 长命落库(可吊销、可踢下线)。refresh 只存 SHA256 哈希。
- **HashID 混淆**:32-bit 平衡 Feistel 网络 + 类型化子密钥,相邻 id 编码无规律,且同数字主键在不同类型下编码完全不同。
- **图片治理**:
- 库里只存对象 key(`runway/<sha1>.jpg`)或本地相对路径,完整 URL 由 `imgurl.Composer` 渲染时拼装(换域名只改配置)。
- key 以内容 sha1 命名 → 重爬天然幂等、不产生孤儿文件。
- 删除图集时按**跨表引用计数**判定孤儿,归零才真删对象;删除动作异步入队,不阻塞请求。
- **图片去重**:`phash`(dHash 向量)近重复标记 + HNSW 索引加速;存储 key 用内容 sha1 寻址(重爬幂等、不产生孤儿文件)。
- **爬虫入库管线**:`ingest_jobs` 队列 + worker(`SELECT ... FOR UPDATE SKIP LOCKED` 多实例安全)异步下载图、补季节码、写草稿表;后台人工审核通过后晋升正式表。失败按指数退避重试,单图失败整任务回滚。
- **图片质量模型**(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"}`。
### 数据库
表结构随服务启动由 GORM AutoMigrate 幂等创建(含 pgvector 扩展、去重索引与语义 embedding 表)。跨环境搬运全库数据用 dbtool:
```bash
go run ./cmd/dbtool dump -out db_dump.json # 导出当前库
go run ./cmd/dbtool import -in db_dump.json # 在目标环境重建并回灌
```
### 优雅关闭
监听 `SIGINT` / `SIGTERM`,等待在途请求完成(最长 `shutdown_timeout` 秒)后退出,并通知入库 worker 停止领取新任务。
---
## 6. 目录结构
```
backend/
├── configs/config.yml # 唯一配置源
├── cmd/
│ ├── server/ # 组合根:三端口 HTTP + 入库 worker
│ ├── dbtool/ # 跨环境数据搬运
│ └── dbdiag/ # 数据库诊断
├── internal/
│ ├── config/ # 配置加载
│ ├── model/ # 实体(brand / runway / street_snap / user / 草稿 / ingest)
│ ├── database/ # 连接池 + AutoMigrate + 去重 schema
│ ├── 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
```