# 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 `) | | `/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 ` | `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 注册即可。