toom1996 6c31b18e0f update
2026-09-17 00:38:01 +08:00
2026-09-13 21:38:32 +08:00
2026-09-13 00:52:35 +08:00
2026-09-17 00:38:01 +08:00
2026-09-17 00:38:01 +08:00
2026-08-26 10:45:21 +08:00
2026-09-07 00:01:48 +08:00
2026-08-26 10:45:21 +08:00
2026-08-26 10:45:21 +08:00
2026-09-13 00:52:35 +08:00
2026-09-13 00:52:35 +08:00
2026-09-07 00:01:48 +08:00
2026-09-13 00:52:35 +08:00
2026-09-02 21:51:35 +08:00
2026-09-02 21:51:35 +08:00

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)

构建

# 两个可执行文件: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, 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 注册即可。
Description
No description provided
Readme 49 MiB
Languages
Go 99.1%
Batchfile 0.5%
Makefile 0.2%
Dockerfile 0.2%