Files
backend_v2/README.md
toom1996 f7ae917603 update
2026-09-17 22:51:00 +08:00

275 lines
13 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
秀场 / 品牌档案对外只读 API 服务(Go + Gin + GORM/PostgreSQL)。
本项目是从原 `admin/backend`(后台管理接口)中**剥离对外只读接口**后独立出的服务,
专门服务于前端项目 `test/test`(`D:\project\test\test`)。它只保留前端实际调用的公开接口,
并按 Gin 推荐的分层结构重写,配置统一收敛到单个 yml 文件。
> 原 `admin/backend` 目录保持不变,本目录是全新独立工程 `module fashionapi`。
---
## 1. 迁移范围
仅迁移 `test/test` 前端真实调用的接口(经源码 `src/lib/api.ts`、`src/lib/auth.ts`、`src/lib/data.ts`、
`src/pages/*.astro` 全量扫描确认):
| 接口 | 方法 | 前端调用位置 | 说明 |
| --- | --- | --- | --- |
| `/api/health` | GET | — | 健康检查 |
| `/api/public/articles` | GET | `data.ts`、`articles.astro`、`index.astro` | 文章列表(分页 + 搜索 + 筛选 + 附图) |
| `/api/public/articles/:id` | GET | `data.ts`、`article.astro` | 文章详情 |
| `/api/public/brands` | GET | `brands.astro`、`data.ts` | 品牌列表(A-Z 索引 + featured 精选 + 搜索) |
| `/api/auth/register` | POST | `auth.ts` | 注册(注册成功直接签发 token) |
| `/api/auth/login` | POST | `auth.ts` | 登录(请求体 `account` 可为邮箱或用户名) |
| `/api/auth/me` | GET | `auth.ts` | 当前用户(需 `Authorization: Bearer <token>`) |
| `/api/auth/logout` | POST | `auth.ts` | 登出(无状态 JWT,服务端直接返回 ok) |
| `/uploads/*` | GET/HEAD | 静态资源 | 上传图片静态文件服务 |
**已确认未使用、未迁移的接口**(原 `admin/backend` 有,但 `test/test` 前端不曾调用):
- `/api/public/brands/letters`(A-Z 字母计数)—— 前端直接走 `/api/public/brands` 列表,未用该聚合端点。
- `/api/public/brands/featured`(独立精选集合端点)—— 前端改用 `/api/public/brands?featured=1` 列表参数,
该参数已在 `/api/public/brands` 中实现,因此无需独立端点。
- `/api/public/colors` —— 原后端**本就没有**此接口;前端 `data.ts` 探测它失败后会从封面图派生占位色,属预期降级,无需实现。
- `/api/runways`、`/api/brands` 等后台管理接口 —— 属 admin 后台,不在前端消费范围内。
---
## 2. 架构选型说明(满足“符合框架推荐写法”要求)
原 `admin/backend` 把所有 handler 平铺在一个 `handlers` 包里,直接读写全局单例 `config.DB`,
路由在 `main.go` 中手工注册。对纯 JSON API 而言存在两个问题:
1. **“MVC” 在 JSON API 下是伪命题**:传统 MVC 的 View 层负责渲染 HTML,而本项目只输出 JSON,
没有模板/视图。若强行套 MVC(`models/`、`views/`、`controllers/`),`views/` 会空置,
controller 又夹带了本应属于 service 的业务逻辑。Gin 官方示例与社区主流实践也不推荐在纯 API 中套 MVC。
2. **全局单例导致不可测**:`config.DB` 包级全局变量使 handler 无法在无真实数据库时单元测试,
且 GORM v2 复用 `*gorm.DB` 会残留 `Statement` 状态(需用 Scope 规避)。
因此本项目采用 **分层架构(Layered / 主动式 Repository)**,这也是 Gin 生态中最贴合“可维护、可测试”目标的写法:
```
cmd/ 组合根(Composition Root):只做依赖装配,不含业务逻辑
server/ 启动 HTTP 服务
dbtool/ 跨环境数据搬运(dump/import)
internal/ 业务代码(不对外暴露)
config/ 配置加载(yml + 环境变量覆盖 + 默认值)
model/ 实体定义(gorm tag + json tag,PasswordHash 永不出参)
database/ PostgreSQL 连接池 / 自动迁移(AutoMigrate)
repository/ 数据访问(接口 + GORM 实现,查询条件用 Scope 安全组合)
service/ 业务逻辑(JWT 签发、密码哈希、参数归一化、聚合)
dto/ 请求/响应结构体与边界常量
handler/ HTTP 层(仅做参数解析与响应封装)
middleware/ 鉴权 / CORS
router/ 路由注册
pkg/ 可复用工具(response / textutil / jwt)
```
**关键改进**
- **依赖注入取代全局单例**:`*gorm.DB`、`*jwt.Manager` 全部通过构造函数显式注入,
消除 `config.DB` 全局变量,handler/service 可单独单测。
- **GORM Scope 规避 Statement 复用**:所有查询条件封装为 `func(*gorm.DB) *gorm.DB` Scope,
避免原项目“复用同一个 `*gorm.DB` 实例导致 WHERE 串味”的隐患。
- **排序字段白名单**:`orderBy` 只允许白名单内的值,杜绝 SQL 注入。
- **配置统一收敛到 yml**:见第 3 节。
- **表结构由 GORM AutoMigrate 托管**:服务启动时幂等创建/更新全部表(替代原 `scripts/sql`、`db/migrations` 下的手写 MySQL 迁移脚本),
跨环境数据搬运经 `cmd/dbtool`(dump/import)。如需避免大表启动期加列,可在部署流程中先行 `dbtool import` 建好结构。
- **修复原项目配置隐患**:原 `admin/backend` 代码只读 `DB_PASS`,而 docker-compose 注入的是
`DB_PASSWORD`,二者不一致;本项目同时兼容 `DB_PASSWORD` 与 `DB_PASS`,`DB_PASSWORD` 优先。
**接口功能一致性**:响应字段、分页元结构(`data/total/current_page/last_page/per_page`)、
默认页大小(文章 12 / 上限 100、品牌 200 / 上限 500)、摘要截断字符(`…` U+2026)、
软删除过滤(`is_deleted = 0`)、登录字段名(`account`)、鉴权中间件行为均与原后端逐一核对一致。
---
## 3. 配置管理(满足“统一使用 yml 文件”要求)
唯一配置源:`configs/config.yml`。加载优先级 **环境变量 > yml > 代码内置默认值**。
- 本地开发:直接改 `configs/config.yml`,无需任何环境变量。
- 容器 / 生产部署:保留 yml 为默认值,用环境变量覆盖敏感项(见 yml 中每项 `env:` 注释)。
**支持的环境变量**
| 变量 | 覆盖项 |
| --- | --- |
| `SERVER_PORT` | server.port |
| `GIN_MODE` | server.mode |
| `DB_HOST` / `DB_PORT` / `DB_USER` / `DB_NAME` | 数据库连接 |
| `DB_PASSWORD`(或 `DB_PASS`) | database.password |
| `DB_LOG_LEVEL` | GORM 日志级别 |
| `JWT_SECRET` / `JWT_EXPIRE_HOURS` | 令牌密钥 / 有效期 |
| `UPLOAD_DIR` / `UPLOAD_URL_PREFIX` | 静态资源目录 / 前缀 |
| `CONFIG_PATH` | 显式指定配置文件路径(优先级高于 `-config` 参数) |
**路径查找顺序**:`-config` 参数 → `CONFIG_PATH` 环境变量 → `./configs/config.yml` → `./config.yml`
→ `../../configs/config.yml`。找不到配置文件不报错,直接使用默认值 + 环境变量,便于纯环境变量部署。
> 生产环境务必通过 `JWT_SECRET` 覆盖默认密钥,并通过 `DB_*` 覆盖数据库口令。
---
## 4. 运行
### 前置要求
- Go 1.26+
- PostgreSQL 16(+ pgvector,见 `scripts/pgvector` 的 Docker 一键起库):`127.0.0.1:5432`,库名 `fashion`,账号 `fashion/fashion_dev_2026`
### 构建
```bash
# 主服务
go build -o bin/server ./cmd/server
# 跨环境数据搬运工具(dump / import)
go build -o bin/dbtool ./cmd/dbtool
```
### 数据库迁移(结构自动创建)
表结构**随服务启动自动执行** GORM AutoMigrate(幂等,仅增量变更,不重建已有表)。
跨环境搬运全库数据(结构 + 数据)用 dbtool:
```bash
go run ./cmd/dbtool dump -out db_dump.json # 导出当前库
go run ./cmd/dbtool import -in db_dump.json # 在目标环境重建并回灌
go run ./cmd/dbtool import -in db_dump.json -data-only # 仅导数据(结构已由 AutoMigrate 建好)
```
### 启动服务
```bash
./bin/server # 读取 configs/config.yml,监听 :8090
./bin/server -config /path/to/config.yml
```
启动后访问 `http://localhost:8090/api/health` 应返回 `{"status":"ok"}`。
### 优雅关闭
服务监听 `SIGINT` / `SIGTERM`,收到信号后等待在途请求完成(最多 `shutdown_timeout` 秒)再退出。
---
## 5. 接口详细
### 5.1 健康检查
`GET /api/health` → `200 {"status":"ok"}`
### 5.2 文章列表
`GET /api/public/articles`
| 参数 | 说明 | 默认 / 上限 |
| --- | --- | --- |
| `page` | 页码 | 1 |
| `size` | 每页条数 | 12 / 100 |
| `keyword` | 标题模糊搜索 | — |
| `brand_id` | 单个品牌 id | — |
| `brand_ids` | 多品牌 id,逗号分隔 | — |
| `collection_type` | 多选:rtw/menswear/couture/resort/pre_fall | — |
| `season` | 多选:spring/fall | — |
| `season_code` | 精确匹配:SS26/FW25… | — |
| `year` | 多选年份 | — |
| `sort` | newest / year_desc / year_asc / image_count | newest |
| `with_images` | 每篇附带前 N 张图(避免逐篇拉详情) | 0 / 12 |
响应:`{ data:[...], total, current_page, last_page, per_page }`。
列表项字段:`id, brand_id, title, summary, cover, brand_name, year, image_count, collection_type, season, season_code, published_at, images`。
### 5.3 文章详情
`GET /api/public/articles/:id` → `200 { data:{...} }` / `404 {"error":"文章不存在"}`。
详情字段:`id, title, summary, description, cover, brand_name, year, image_count, published_at, images[]`。
### 5.4 品牌列表
`GET /api/public/brands`
| 参数 | 说明 | 默认 / 上限 |
| --- | --- | --- |
| `letter` | A-Z 字母索引;`OTHER` 中文分桶;缺省=全部拉丁字母 | — |
| `keyword` | 品牌名 / 展示名模糊搜索 | — |
| `page` / `size` | 分页 | 1 / 200 / 500 |
| `only_with_articles` | 只返回有走秀档案的品牌(默认 1) | 1 |
| `featured` | 传 1 时只返回“代表品牌”精选集合(按指标排名前 N) | 0 |
| `metric` | 精选排名指标:images(默认)/ shows | images |
| `limit` | 精选集合大小 | 200 / 500 |
响应:`{ data:[{id,name,show_name,article_count}], total, current_page, last_page, per_page }`。
`featured=1` 且取不到集合时返回空结果(不会退化成全部品牌,避免前端索引膨胀)。
### 5.5 账号体系
| 接口 | 方法 | 请求 | 成功响应 |
| --- | --- | --- | --- |
| 注册 | `POST /api/auth/register` | `{username,email,password(>=6)}` | `201 {token,user}` |
| 登录 | `POST /api/auth/login` | `{account,password}`(account=邮箱或用户名) | `200 {token,user}` |
| 当前用户 | `GET /api/auth/me` | Header `Authorization: Bearer <token>` | `200 {user}`;无 token → `401` |
| 登出 | `POST /api/auth/logout` | — | `200 {ok:true}` |
`user` 结构:`{id, username, email}`(绝不含密码哈希)。无状态 JWT,token 有效期 7 天。
### 5.6 静态资源
`GET /uploads/*filepath` → 返回 `uploads/` 目录下文件。封面/图片在数据库中以相对路径
(如 `/uploads/2026/08/11/xxx.jpg`)存储,前端拼接前缀即得访问地址。
---
## 6. 目录结构
```
backend/
├── configs/
│ └── config.yml # 唯一配置源
├── cmd/
│ ├── server/main.go # 组合根:启动 HTTP 服务 + 优雅关闭
│ └── dbtool/main.go # 组合根:跨环境数据搬运(dump/import)
├── internal/
│ ├── config/ # 配置加载(yml + env + 默认值)
│ ├── model/ # 实体(brand / runway / runway_image / user)
│ ├── database/ # PostgreSQL 连接池 + 自动迁移
│ ├── repository/ # 数据访问(接口 + GORM 实现)
│ ├── service/ # 业务逻辑
│ ├── dto/ # 请求/响应结构体与边界常量
│ ├── handler/ # HTTP 层
│ ├── middleware/ # 鉴权 / CORS
│ ├── router/ # 路由注册
│ └── pkg/ # response / textutil / jwt 工具
├── uploads/ # 上传图片(运行时生成,gitignore)
├── go.mod / go.sum
├── Dockerfile
├── Makefile
└── README.md
```
---
## 7. 容器化(参考)
`Dockerfile` 为多阶段构建:构建阶段编译 `server` / `migrate`,运行阶段基于 `alpine` 仅携带二进制、
`ca-certificates` 与 `configs/`。容器内需用环境变量覆盖数据库与 JWT 密钥:
```bash
docker build -t fashionapi .
docker run -d -p 8090:8090 \
-e DB_HOST=db -e DB_NAME=fashionadmin \
-e JWT_SECRET=<随机长字符串> \
-v $(pwd)/uploads:/app/uploads \
fashionapi
```
---
## 8. 后续可扩展
- 若需后台管理接口(`/api/runways` 等),可沿用本分层结构在 `internal/` 内新增模块,
并在 `cmd/server` 组合根装配,不影响现有公开接口。
- 若前端真正需要 `/api/public/colors`,在 `repository`/`service`/`handler` 各加一层并在 router 注册即可。