first commit

This commit is contained in:
toom1996
2026-08-26 10:36:52 +08:00
commit 3cf1368e68

272
README.md Normal file
View File

@ -0,0 +1,272 @@
# Fashion API
秀场 / 品牌档案对外只读 API 服务(Go + Gin + GORM/MySQL)。
本项目是从原 `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 服务
migrate/ 独立结构迁移命令
internal/ 业务代码(不对外暴露)
config/ 配置加载(yml + 环境变量覆盖 + 默认值)
model/ 实体定义(gorm tag + json tag,PasswordHash 永不出参)
database/ MySQL 连接池 / 迁移
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 节。
- **迁移从启动流程剥离**:`auto_migrate` 默认 `false`,改用独立 `cmd/migrate` 命令显式执行,
避免每次启动都对 `brand_runway_images`(89 万行)等大表隐式 `ALTER TABLE`。
- **修复原项目配置隐患**:原 `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` / `DB_AUTO_MIGRATE` | 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+
- MySQL 8.x(本项目对接 WSL 1Panel 本地库:`127.0.0.1:3306`,库名 `db`,账号 `root/root`)
### 构建
```bash
# 两个可执行文件:server(API 服务)与 migrate(结构迁移)
go build -o bin/server ./cmd/server
go build -o bin/migrate ./cmd/migrate
```
### 数据库迁移(可选,首次部署时执行一次)
迁移默认**不**随服务启动执行(`auto_migrate: false`)。需建表/加字段时:
```bash
./bin/migrate # 读取 configs/config.yml
# 或指定配置文件
./bin/migrate -config /path/to/config.yml
```
### 启动服务
```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, source_url, 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 服务 + 优雅关闭
│ └── migrate/main.go # 组合根:独立结构迁移
├── internal/
│ ├── config/ # 配置加载(yml + env + 默认值)
│ ├── model/ # 实体(brand / runway / runway_image / user)
│ ├── database/ # MySQL 连接池 + 迁移
│ ├── 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 注册即可。