Files
backend_v2/README.md
toom1996 be4db0bf96 fix(publish): 最终审查修复波——迁移索引口径 / 入库计数 / 仓储证据
C1(关键):2026-09-22-03 的街拍实体键索引改用最终表达式
(city, year, COALESCE(title, '')),与体检及 StreetSnapEntityState 口径一致。
此前索引按 (city, year) 比体检更窄,同城同年多专题的合法数据会通过体检、
再由索引抛原生 23505;04 退化为幂等兜底并在注释说明真正常态由 03 承载。
新增 db/migrations/README.md,写清迁移链执行顺序与各步前提/不可逆点。

I1:processRunway 的 image_count 改为按实际落库主图行数(countMainImages),
不再用 len(p.Looks)(空主图 / sha1 重复会被跳过,导致计数偏大且不再自愈)。
补 service 单测与「入库后 image_count == 存活主图行数」的 DB 断言。

I2:三个迁移文件不再把已删除的一次性搬迁脚本写成硬前置,改为写明取回方式
(git show c4bafc5:scripts/migrate_single_table/main.go,并须在旧代码树上运行)。

I3:新增 SetRecordStatus / SetStreetRecordStatus 仓储集成测试:
pending 经仓储通过后在公开视图可见、驳回后不可见、不存在的 id 返回 ErrNotFound。

Minor:修正锁不住口径的走秀 image_count 测试(改为删主图、留细节图);
修正去重注释与事实不符(含 FindNearDuplicateImage 注释);
ListRecords / ListStreetRecords 改用 Scope 杜绝 Count 后复用 *gorm.DB;
dbtool 视图改 CREATE OR REPLACE 并重跑 dump(仍 4 视图、无草稿表);
删除挂在 Popular 上的「IDs 返回…」注释;README 改为单表 + status 现状;
SetRecordStatus 注释写明有意不校验前置状态;规格补两条已知不一致。
2026-09-23 16:03:22 +08:00

192 lines
9.6 KiB
Markdown
Raw Permalink 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/*`:品牌 / 走秀 / 街拍管理、内容审核(单表 + `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/<sha1>.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
```