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 而言存在两个问题:
- “MVC” 在 JSON API 下是伪命题:传统 MVC 的 View 层负责渲染 HTML,而本项目只输出 JSON,
没有模板/视图。若强行套 MVC(
models/、views/、controllers/),views/会空置, controller 又夹带了本应属于 service 的业务逻辑。Gin 官方示例与社区主流实践也不推荐在纯 API 中套 MVC。 - 全局单例导致不可测:
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.DBScope, 避免原项目“复用同一个*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)
构建
# 两个可执行文件:server(API 服务)与 migrate(结构迁移)
go build -o bin/server ./cmd/server
go build -o bin/migrate ./cmd/migrate
数据库迁移(可选,首次部署时执行一次)
迁移默认不随服务启动执行(auto_migrate: false)。需建表/加字段时:
./bin/migrate # 读取 configs/config.yml
# 或指定配置文件
./bin/migrate -config /path/to/config.yml
启动服务
./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 密钥:
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 注册即可。