commit 3cf1368e68ee82a629f630e15418642468407418 Author: toom1996 <23cm.cn@gmail.com> Date: Wed Aug 26 10:36:52 2026 +0800 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..7e49a94 --- /dev/null +++ b/README.md @@ -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 `) | +| `/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 注册即可。