Files
zt-closet-backend/接口文档.md
T
zhengchiliu c2519c4b6d feat(clothes): 支持通过社区公开穿搭只读查看衣物
新增 outfit_uuid 查询参数,非属主可通过社区公开穿搭查看衣物详情。
同时调整点赞通知逻辑,仅在首次点赞时发送通知。
2026-09-16 02:28:59 +08:00

2613 lines
109 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 智能衣橱(zt-closet)后端接口文档 · 前端版
> 面向**前端/小程序工程师**的联调文档。全文基于后端源码(`internal/routers`、`internal/dto`、`internal/service`、`pkg/errcode`)逐字段核对生成,包含接口入参出参、业务逻辑与状态机、调用时序、错误码全表。
>
> - 服务:Go + Gin,微信小程序后端
> - 统一前缀:`/api/v1`(下文中路径如 `/user/wxlogin` 均表示 `/api/v1/user/wxlogin`)
> - 健康检查:`GET /ping`(无 `/api/v1` 前缀)
> - 文档版本:v1.1 · 生成日期:2026-09-09
---
## 目录
- [1. 五分钟上手](#1-五分钟上手)
- [2. 通用调用约定](#2-通用调用约定)
- [2.1 鉴权与 Token](#21-鉴权与-token)
- [2.2 统一响应结构](#22-统一响应结构)
- [2.3 HTTP 状态码与业务码](#23-http-状态码与业务码)
- [2.4 分页约定](#24-分页约定)
- [2.5 单位约定](#25-单位约定)
- [2.6 幂等与重试](#26-幂等与重试)
- [2.7 需要轮询的场景](#27-需要轮询的场景)
- [2.8 CORS 与链路追踪](#28-cors-与链路追踪)
- [3. 接口总表(48 个)](#3-接口总表48-个)
- [4. 公共数据结构](#4-公共数据结构)
- [5. 接口详解](#5-接口详解)
- [5.1 用户模块](#51-用户模块)
- [5.2 额度模块](#52-额度模块)
- [5.3 运势模块](#53-运势模块)
- [5.4 衣物模块](#54-衣物模块)
- [5.5 穿搭模块](#55-穿搭模块)
- [5.6 AI 推荐模块](#56-ai-推荐模块)
- [5.7 衣柜分享模块](#57-衣柜分享模块)
- [5.8 穿搭分享模块](#58-穿搭分享模块)
- [5.9 收藏与点赞模块](#59-收藏与点赞模块)
- [5.10 通知模块](#510-通知模块)
- [5.11 商品模块](#511-商品模块)
- [5.12 订单与退款模块](#512-订单与退款模块)
- [6. 业务逻辑与状态机](#6-业务逻辑与状态机)
- [7. 核心调用时序图](#7-核心调用时序图)
- [8. 错误码全表](#8-错误码全表)
- [9. 前端联调 FAQ 与检查清单](#9-前端联调-faq-与检查清单)
- [10. 接口调用示例(curl)](#10-接口调用示例curl)
---
## 1. 五分钟上手
标准联调路径:
```text
1. wx.login() 拿到 code
└─> POST /user/wxlogin -> access_token / refresh_token / user_info
2. 所有需登录接口带 Header:
Authorization: Bearer <access_token>
3. 上传衣物
POST /clothes/upload (multipart, 字段名 file)
└─> { clothes_uuid, image_process_status: 3 }
└─> 轮询 GET /clothes/:uuid/status 直到 4(成功)或 5(失败)
4. 做穿搭
POST /outfit 创建 -> PUT /outfit/:uuid/public 公开(扣额度,进入社区)
或 POST /ai-recommend AI 推荐 -> POST /ai-recommend/save 保存
5. 额度不够
GET /goods 商品列表 -> POST /order 下单 -> wx.requestPayment(pay_params)
-> 轮询 GET /order 直到 order_status = 2(已支付)
```
**最容易踩的 6 个坑(详见 [第 9 节](#9-前端联调-faq-与检查清单)):**
1. **判断成败只看 body 里的 `code`**,绝大多数业务错误返回 HTTP 200(包括 `21001 账号已被禁用`)。
2. **刷新 Token 必须同时传 `refresh_token` 和 `access_token`**,否则报 `10000`;成功后旧的两个 token 立即失效,必须整体替换。
3. **衣物上传是异步的**,返回只代表"已排队",必须轮询状态接口。
4. **更新衣物的主颜色请求字段是 `color`**,响应里才是 `color_main`。
5. **金额单位都是分、时间都是秒**,展示前要换算。
6. **AI 推荐是异步的**:`POST /ai-recommend` 只返回 `task_id`,必须轮询 `GET /ai-recommend/status` 才能拿到结果;生成后不保存不退额度。退款是人工审核,不是实时到账。
---
## 2. 通用调用约定
### 2.1 鉴权与 Token
**请求头格式**
```http
Authorization: Bearer <access_token>
```
- 必须恰好是 `Bearer` + 空格 + token(大小写敏感),其它写法一律 `11001`。
- access_token 默认有效期 **7200 秒**(配置 `JWT.AccessTokenExpire`),refresh_token 默认 **86400 秒**。
- 登录响应里的 `expire_time` 是 access_token 的过期时间戳(秒),前端可据此提前刷新。
**服务端校验顺序(失败即中断)**
| 顺序 | 校验 | 返回 | HTTP |
| --- | --- | --- | --- |
| 1 | 无 `Authorization` 头 | `11004` Token 缺失 | 401 |
| 2 | 格式不是 `Bearer xxx` | `11001` + `details:["invalid Authorization format"]` | 401 |
| 3 | access_token 已过期 | `11002` Token 已过期 | 401 |
| 4 | 拿 refresh_token 当 access 用 | `11001` + `details:["token type mismatch"]` | 401 |
| 5 | Redis 会话不存在(已登出/被顶替) | `11001` + `details:["token session invalid"]` | 401 |
| 6 | 会话 userID 与 token 不一致 | `11001` + `details:["token user mismatch"]` | 401 |
| 7 | 用户不存在 | `11001` + `details:["用户不存在"]` | 401 |
| 8 | 用户 `status != 1`(禁用/注销) | `21001` 账号已被禁用 | **200** |
> ⚠️ 第 8 步是 **HTTP 200 + code 21001**,不是 401。拦截器里必须单独判断并跳转封禁提示页。
**刷新流程(前端必须实现)**
```text
任意接口返回 code = 11002 / 11001(HTTP 401)
└─> POST /user/refresh-token { refresh_token, access_token } // 两个都要传
├─ 成功 -> 用返回的两个新 token **整体替换**本地存储,再重试原请求
└─ 失败(11001/11002)-> 清空本地登录态,重新走 wx.login
```
- 刷新成功后,服务端会**立即吊销旧的 access_token 与 refresh_token**,前端不能沿用任何旧值。
- refresh_token 也是**一次性旋转**的:每次刷新都会返回新的 refresh_token。
**公开接口(无需 Token)**:`/ping`、`/user/wxlogin`、`/user/refresh-token`、`/goods`、`/goods/:code`、`/order/pay/notify`、`/order/refund/notify`。
---
### 2.2 统一响应结构
**成功**(HTTP 200)
```json
{
"code": 0,
"msg": "成功",
"data": { }
}
```
- `code` 恒为 `0`,`msg` 恒为 `"成功"`。
- **`data` 永不为 `null`**:无业务数据(删除、取消、标记已读等)时是空对象 `{}`。
- 成功响应**没有 `details` 字段**。
**失败**
```json
{
"code": 80000,
"msg": "额度不足",
"details": ["额度不足,请先购买"]
}
```
- 失败响应**没有 `data` 字段**,前端不要读取 `data`。
- `details` 是**可选**的字符串数组,仅在有补充信息时出现(参数校验错误、业务提示等)。典型内容:
`"请先完善生日信息"`、`"过期时间必须晚于当前时间"`、`"部分分享需选择衣物"`、`"衣橱暂无可用衣物"`、`"未生成有效搭配"`、`"缺少旧 access token"`、`"缺少微信 openid"` 等。
- **`details` 可直接用于 toast 提示用户**,但建议仅对 `10000 参数错误` 与业务提示类展示,系统错误统一展示兜底文案。
---
### 2.3 HTTP 状态码与业务码
**绝大多数业务错误返回 HTTP 200**,只有下表这些会返回非 200:
| HTTP | 业务 code | 场景 |
| --- | --- | --- |
| 200 | `0` 成功,**以及除下表外的所有业务码** | 成功与绝大多数业务错误 |
| **401** | `11000` 未授权、`11001` Token 鉴权失败、`11002` Token 已过期、`11004` Token 缺失 | 鉴权失败 → 走刷新或重新登录 |
| **403** | `12000` 无权限访问 | 越权操作他人资源 |
| **429** | `10003` 请求过于频繁 | 限流(如 AI 推荐并发锁),稍后重试 |
| **500** | `19000` 服务内部错误、`19001` 数据库错误、`19002` 缓存错误、`19003` 下游服务调用失败、`19999` 未知错误、`39001` 抠图失败、`39002` 分类失败 | 服务端异常 → 提示"服务繁忙" |
> **前端拦截器建议**:先取 body 的 `code` 判断业务结果;`code` 缺失(网络层失败)时才回落到 HTTP 状态码。
---
### 2.4 分页约定
所有列表接口统一使用 **GET query 参数**:
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| page | int | 否 | 页码,≥1,默认 **1** |
| page_size | int | 否 | 每页条数,1~50,默认 **10**;超过 50 服务端自动收敛为 50 |
列表响应统一结构:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| total | int64 | 总条数 |
| page | int | 当前页码 |
| page_size | int | 每页条数(服务端实际生效值) |
| list | array | 数据数组;无数据时为空数组 `[]` |
---
### 2.5 单位约定
| 类别 | 单位 | 涉及字段 |
| --- | --- | --- |
| **金额** | **分**(int64) | `price`、`discount_price`、`unit_price`、`pay_amount`、`refund_amount` |
| **时间** | **秒级 Unix 时间戳**(int64) | `expire_time`、`pay_time`、`refund_time`、`created_at`、`birth_day`、`vip_expire`、`ban_start`、`ban_expire`、`apply_time` |
| 日期 | `YYYYMMDD` 整数 | 运势 `stat_date` |
> 展示金额需 `÷ 100`;展示时间需 `× 1000` 再交给 JS `Date`(因为 JS 用毫秒)。
---
### 2.6 幂等与重试
以下接口**天然幂等,可安全重试**(重复提交不会产生重复数据或重复扣费):
- 点赞 / 取消点赞 `POST /like`
- 收藏 / 取消收藏 `POST /collect`
- 穿搭公开 `PUT /outfit/:uuid/public`(重复公开**不会**重复扣额度;但**取消公开后再次公开会再扣 1 次**,见 [6.6](#66-额度规则全景))
- 标记已读 `POST /notification/:uuid/read`
- 保存 AI 推荐 `POST /ai-recommend/save`
- 提交 AI 推荐 `POST /ai-recommend`(任务生成中重复提交会返回**同一个 `task_id`**,不会重复扣额度)
- 取消订单 `POST /order/:orderNo/cancel`
- 取消分享 `POST /closet-share/:uuid/cancel`
**不建议重试**:`POST /clothes/upload`(每次都会扣 1 个上传额度)、`POST /order`(会创建新订单)。
---
### 2.7 需要轮询的场景
后端**没有 WebSocket / 消息推送**,以下场景前端需主动轮询:
| 场景 | 轮询接口 | 终态 | 建议间隔 |
| --- | --- | --- | --- |
| 衣物 AI 处理 | `GET /clothes/:uuid/status` | `4` 成功 / `5` 失败(`1/2/3` 继续轮询) | 1~2 秒,建议上限 60 次 |
| AI 推荐生成 | `GET /ai-recommend/status` | `2` 成功 / `3` 失败(`1` 继续轮询) | 2 秒,建议上限 90 次 |
| 支付结果 | `GET /order`(按 `status` 或 `order_no` 过滤) | `2` 已支付 / `3` 已取消 / `4` 已过期 | 支付后 1 秒起,最多 15 分钟 |
| 退款进度 | `GET /order` | `5` 已退款 | 30 秒(人工审核,可能很久) |
| 今日运势未生成 | `GET /horoscope/today` | 返回数据(首次可能 `90300`) | 3~5 秒,最多 3 次 |
| 未读通知数 | `GET /notification/unread` | 无终态,按需刷新 | 进入页面时 / 30~60 秒 |
---
### 2.8 CORS 与链路追踪
- 允许的 Method:`GET, POST, PUT, PATCH, DELETE, OPTIONS`;允许的请求头:`Origin, Content-Type, Authorization`。
- 响应头 `X-Request-ID` 可用于排障,但**未加入 CORS `Expose-Headers`,跨域场景下 JS 读不到**;需要排障时请在请求侧自带 `X-Request-ID` 头,服务端会透传。
- 后端已开启 OpenTelemetry 全链路追踪,反馈问题时提供 `X-Request-ID` 可快速定位。
---
## 3. 接口总表(49 个)
### 健康检查(1)
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| GET | `/ping` | 存活探针,返回 `{"message":"pong"}`,**不在 `/api/v1` 下** |
### 公开路由(6,无需登录)
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| POST | `/user/wxlogin` | 微信登录(code 换 openid,首次自动注册) |
| POST | `/user/refresh-token` | 刷新 access_token |
| GET | `/goods` | 商品列表(仅上架) |
| GET | `/goods/:code` | 按商品编码查询 |
| POST | `/order/pay/notify` | 🚫 **微信支付回调,服务端专用,前端禁止调用** |
| POST | `/order/refund/notify` | 🚫 **微信退款回调,服务端专用,前端禁止调用** |
| GET/POST | `/xpay/notify` | 🚫 **虚拟支付消息推送(GET 为 URL 验证 / POST 为发货·退款·iOS 退款问询回调),服务端专用,前端禁止调用** |
### 鉴权路由(36,需 `Authorization: Bearer`)
**用户 / 额度 / 运势(5)**
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| GET | `/user/info` | 当前用户信息 |
| PUT | `/user/profile` | 更新个人资料 |
| GET | `/quota/info` | 额度概览(4 类额度余量) |
| GET | `/quota/flow` | 额度流水 |
| GET | `/horoscope/today` | 今日星座运势(按生日推算星座) |
**衣物(6)**
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| POST | `/clothes/upload` | 上传衣物图片(扣上传额度,触发异步 AI 流水线) |
| GET | `/clothes` | 我的衣物列表 |
| GET | `/clothes/:uuid` | 衣物详情 |
| GET | `/clothes/:uuid/status` | 衣物 AI 处理状态(轮询用) |
| PUT | `/clothes/:uuid` | 更新衣物属性 |
| DELETE | `/clothes/:uuid` | 删除衣物(满足条件时回退上传额度) |
**穿搭(7)**
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| POST | `/outfit` | 创建穿搭 |
| GET | `/outfit` | 我的穿搭列表 |
| GET | `/outfit/community` | 社区穿搭列表(其他用户公开的穿搭) |
| GET | `/outfit/:uuid` | 穿搭详情 |
| PUT | `/outfit/:uuid` | 更新穿搭 |
| DELETE | `/outfit/:uuid` | 删除穿搭 |
| PUT | `/outfit/:uuid/public` | 公开 / 取消公开(公开扣搭配分享额度) |
**AI 推荐(2)**
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| POST | `/ai-recommend` | 生成 AI 穿搭推荐(扣 AI 额度) |
| POST | `/ai-recommend/save` | 保存 AI 推荐为我的穿搭 |
**衣柜分享(5)**
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| POST | `/closet-share` | 创建衣柜分享(扣衣柜分享额度) |
| GET | `/closet-share` | 我的分享列表 |
| GET | `/closet-share/received` | 分享给我的列表 |
| GET | `/closet-share/:uuid` | 分享详情(属主 / 访客视角) |
| POST | `/closet-share/:uuid/cancel` | 取消分享 |
**穿搭分享(5)**
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| POST | `/outfit-share` | 创建穿搭分享(无需公开到社区;私密穿搭扣 1 次衣柜分享额度) |
| GET | `/outfit-share` | 我的穿搭分享列表 |
| GET | `/outfit-share/received` | 分享给我的穿搭分享 |
| GET | `/outfit-share/:uuid` | 穿搭分享详情(属主 / 访客视角) |
| POST | `/outfit-share/:uuid/cancel` | 取消穿搭分享 |
**收藏 / 点赞(5)**
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| POST | `/collect` | 收藏 / 取消收藏 |
| GET | `/collect` | 我的收藏列表 |
| GET | `/collect/:targetUUID` | 查询目标收藏状态与数量 |
| POST | `/like` | 点赞 / 取消点赞 |
| GET | `/like/:targetUUID` | 查询目标点赞状态与数量 |
**通知(4)**
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| GET | `/notification` | 通知列表 |
| GET | `/notification/unread` | 未读通知数 |
| GET | `/notification/:uuid` | 通知详情 |
| POST | `/notification/:uuid/read` | 标记单条已读 |
**订单(4)**
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| POST | `/order` | 创建订单(微信 JSAPI 支付) |
| POST | `/order/virtual` | 创建虚拟支付订单(小程序虚拟支付,iOS/Android 全终端) |
| GET | `/order` | 我的订单列表 |
| POST | `/order/:orderNo/cancel` | 取消订单 |
| POST | `/order/refund` | 申请退款 |
> **说明**:社区广场列表接口为 `GET /outfit/community`(分页返回其他用户已公开且审核通过的穿搭,含作者信息与点赞/收藏计数)。
---
## 4. 公共数据结构
以下对象在多个接口中复用,后文接口章节直接引用,不再重复列出字段。
### 4.1 用户对象 `user_info`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| username | string | 昵称(新用户默认"微信用户") |
| avatar | string | 头像地址 |
| sex | int | 性别:`1`=男 `2`=女(`0` 或未设置时不展示) |
| birth_day | int64 | 生日(秒级时间戳);**为 0 表示未设置**,运势接口会要求先完善 |
| phone | string | 手机号 |
| is_new_user | bool | 是否本次新注册用户(用于引导页) |
| status | int | `1`=正常 `2`=临时禁用 `3`=永久禁用 `4`=已注销 |
| ban_reason | string | 封禁原因 |
| ban_start | int64 | 封禁开始时间(秒) |
| ban_expire | int64 | 封禁到期时间(秒) |
| vip_type | int | `1`=非会员 `2`=月度会员 `3`=年度会员 `4`=体验会员(新用户注册赠送) |
| vip_expire | int64 | VIP 到期时间(秒) |
### 4.2 衣物对象 `ClothesItem`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| clothes_uuid | string | 衣物 UUID(后续查询/更新/删除均用此值) |
| category_main | string | 大类:上装 / 下装 / 连衣裙 / 外套 / 鞋 / 配饰 |
| category_sub | string | 小类:T恤 / 牛仔裤 / 运动鞋 / 发带 / 戒指 等 |
| style | string | 风格 |
| color_main | string | 主颜色 |
| color_sub | string | 副颜色 |
| thickness | string | 厚度 |
| version | string | 版型:宽松 / 修身 / 阔腿 / 紧身 / 常规合身 |
| material | string | 材质:牛仔布 / 针织 / 皮质 / 纯棉 / 羊毛 等 |
| pattern | string | 花纹 |
| sleeve_type | string | 袖型 |
| clothes_type | string | 衣长:短款 / 常规 / 中长 / 长款 |
| neckline_type | string | 领型 |
| formality | string | 正式程度 `1-5` |
| occasions | string | 适用场合 |
| visual_impact | string | 视觉冲击力 `1-5` |
| name | string | 名称 |
| remark | string | 备注 |
| processed_image | string | 抠图后的图片完整 URL(可直接展示) |
| image_process_status | int | 处理状态:`1`=抠图中 `2`=分类中 `3`=排队中 `4`=成功 `5`=失败 |
| fail_reason | string | 处理失败原因;**仅属主可见**,非属主(穿搭分享 / 衣柜分享只读)场景为空串 |
> 衣物**列表与详情**响应均带 `image_process_status`:前端应据此区分「处理中(1/2/3)」与「处理失败(5)」,不要用属性/图片是否为空来推断。`fail_reason` 仅在属主场景返回。
### 4.3 穿搭对象 `OutfitItem`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| outfit_uuid | string | 穿搭 UUID |
| outfit_name | string | 穿搭名称(≤32) |
| outfit_desc | string | 穿搭描述(≤500) |
| scene | string | 适合场景(≤100) |
| source | int | 来源:`1`=用户创建 `2`=AI 推荐 |
| is_public | int | 公开:`1`=不公开 `2`=公开 |
| audit_status | int | 审核:`1`=待审核 `2`=审核通过 `3`=审核不通过 |
| status | int | 状态:`1`=正常 `2`=封禁 `3`=**待用户保存**(AI 推荐生成后未保存) |
| items | array | 衣物列表,元素 `{"clothes_uuid": string}` |
> **关于 `status=3`(AI 待保存)**:AI 推荐生成的穿搭会以 `status=3` 出现在 `GET /outfit` 列表中,前端应据此把它归到「AI 推荐」入口,不要混入「我的搭配」。
>
> 这类穿搭**超过 7 天未被用户保存,会由后端定时任务自动清理**(软删除,列表与详情立即不可见)。用户超期后再调 `POST /ai-recommend/save` 会返回 `40000` 穿搭不存在,属预期行为。
### 4.4 商品对象 `GoodsItem`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| id | int64 | 商品 ID |
| goods_code | string | 商品编码(**下单时用这个**) |
| goods_name | string | 商品名称 |
| goods_type | int | `1`=会员包 `2`=上传包 `3`=AI 推荐包 `4`=衣柜分享包 `5`=搭配分享包 |
| price | int64 | 原价(**分**) |
| discount_price | int64 | 售价(**分**,实际支付金额) |
| member_days | int64 | 赠送会员天数;`>=365` 发放年度会员,否则月度会员 |
| upload_quota | int64 | 赠送上传额度(对应后端 `cos_quota`) |
| closet_share_quota | int64 | 赠送衣柜分享额度 |
| outfit_share_quota | int64 | 赠送搭配分享额度 |
| description | string | 商品描述 |
> 商品对象**不含** AI 推荐额度展示字段,但 `goods_type=3`(AI 推荐包)会发放 AI 额度。
### 4.5 订单对象 `OrderItem`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| order_no | string | 订单号 |
| goods_name | string | 商品名称 |
| unit_price | int64 | 单价(**分**) |
| pay_amount | int64 | 实付金额(**分**) |
| pay_channel | int | 支付渠道 `1`=微信支付(JSAPI) `2`=虚拟支付 |
| order_status | int | `1`=待支付 `2`=已支付 `3`=已取消 `4`=已过期 `5`=已退款 |
| member_days | int64 | 订单快照:会员天数 |
| expire_time | int64 | 支付截止时间(秒),前端倒计时用(15 分钟) |
| pay_time | int64 | 支付时间(秒),未支付为 0 |
| refund_time | int64 | 退款时间(秒),未退款为 0 |
| refund_amount | int64 | 退款金额(**分**) |
| created_at | int64 | 创建时间(秒) |
### 4.6 通知对象 `NotificationItem`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| notification_uuid | string | 通知 UUID |
| msg_type | int | `1`=点赞通知 `2`=订单通知 `3`=系统通知 |
| title | string | 标题 |
| content | string | 内容 |
| is_read | int | ⚠️ `1`=**未读** `2`=**已读**(不是布尔值,也不是 0/1) |
| created_at | int64 | 创建时间(秒) |
### 4.7 额度流水对象 `QuotaFlowItem`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| quota_flow_uuid | string | 流水 UUID |
| quota_type | int | `1`=上传 `2`=AI `3`=社区分享 `4`=衣柜分享 |
| change_target | int | `1`=配额调整 `2`=使用 |
| change_type | int | `1`=购买获赠 `2`=活动赠送 `3`=上传消耗 `4`=删除回退 `5`=人工调整 `6`=退款回收 `7`=**到期失效**(固定有效期的额度凭证过期,由后台任务每小时补记) |
| change_num | int64 | 变更数量(**正数为增加,负数为减少**;到期失效为负数) |
| new_value | int64 | 变更后的数值 |
| reason | string | 变更原因 / 备注 |
| order_no | string | 关联订单号:购买/退款类为真实订单号;赠送/消耗/回退/到期类由后端填充流水 UUID 或凭证 `ref_id`,用于满足唯一索引幂等 |
| created_at | int64 | 创建时间(秒) |
### 4.8 微信支付参数 `pay_params`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| timeStamp | string | ⚠️ 注意大小写(微信原生命名),直接透传给 `wx.requestPayment` |
| nonceStr | string | 随机字符串 |
| package | string | 形如 `prepay_id=wx...` |
| signType | string | 签名方式,如 `RSA` |
| paySign | string | 签名 |
> `pay_params` 是微信原样返回的字段,**整体透传给 `wx.requestPayment` 即可,不要改名或转换大小写**。
---
## 5. 接口详解
> 参数位置标注:`json` = JSON 请求体、`query` = URL 查询参数、`path` = 路径参数、`form` = multipart 表单。
> 路径中的 `:xxx` 是占位参数名,调用时替换为真实值(注意 `targetUUID`、`orderNo` 的驼峰写法)。
---
### 5.1 用户模块
#### POST `/user/wxlogin` — 微信登录(公开)
**业务逻辑**
1. 前端 `wx.login()` 获取 `code`,传给后端;后端用 `code2session` 换 `openid`。
2. **首次登录自动注册**:昵称默认"微信用户",`status=1` 正常,返回 `is_new_user=true`,随后依次赠送两样东西:
- **免费额度**(`UserDefaultQuota` 段,与会员身份无关、所有用户长期享有):内置默认上传 20 / AI 推荐 3 / 搭配分享 10 / 衣柜分享 5,允许显式配置为 0;
- **体验会员及其额度**(`NewUserGift` 段):开通 `vip_type=4` 体验会员(内置默认 3 天,配 0 表示不赠送会员),并按该段配置发放**体验会员额度凭证**(`source=4`,如右上表;有效期是**固定值** = 注册时刻 + 体验天数,**不随后续续费延长**)。内置默认上传 0 / AI 3 / 搭配分享 3 / 衣柜分享 3。这部分额度与上面的免费额度**叠加生效**。
两段配置都支持显式配 0 表示不赠送;`GiftVIPDay` 配 0 时,体验会员额度一律不发放。赠送成功后下发一条系统通知「欢迎礼包已到账」。因此新用户返回的 `vip_type` 通常为 `4`(配置为 0 时为 `1`)。
3. 并发首次登录不会注册重复账号(依赖 `wx_open_id` 唯一索引)。
4. 账号被禁用/注销(`status != 1`)时返回 `21001`,HTTP 仍是 200。
5. 额度初始化失败**不影响登录**(仅后端告警)。
**请求**(json)
| 参数 | 类型 | 必填 | 约束 | 说明 |
| --- | --- | --- | --- | --- |
| code | string | ✅ | required | `wx.login` 获取的临时登录凭证,一次性 |
**响应** `data`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| access_token | string | 访问令牌,放入 `Authorization: Bearer` |
| refresh_token | string | 刷新令牌,用于续期 |
| expire_time | int64 | access_token 过期时间戳(秒) |
| user_info | object | [用户对象](#41-用户对象-user_info) |
**错误码**:`10000` 参数错误 · `21002` 微信登录失败 · `21003` 微信 Code 无效 · `21001` 账号已被禁用(HTTP 200)· `29000` 用户创建失败 · `19002` 缓存错误
---
#### POST `/user/refresh-token` — 刷新 Token(公开)
**业务逻辑**
- ⚠️ **`refresh_token` 与 `access_token` 两个字段都必须传**,缺少 access_token 会返回 `10000` + `details:["缺少旧 access token"]`。
- 刷新成功后,服务端**立即吊销**旧的 access_token 与 refresh_token,并返回一对全新的 token —— 前端必须**整体替换**本地存储。
- refresh_token 旋转更新,不可重复使用同一返回值多次刷新。
**请求**(json)
| 参数 | 类型 | 必填 | 约束 | 说明 |
| --- | --- | --- | --- | --- |
| refresh_token | string | ✅ | required | 当前有效的 refresh_token |
| access_token | string | ✅ | 业务必填 | 当前(可能已过期)的 access_token,用于刷新后立即吊销 |
**响应** `data`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| access_token | string | 新访问令牌 |
| refresh_token | string | 新刷新令牌 |
| expire_time | int64 | 新 access_token 过期时间戳(秒) |
**错误码**:`10000` + `缺少旧 access token` · `11001` Token 鉴权失败(无效/已被吊销)· `11002` Token 已过期 · `19002` 缓存错误
---
#### GET `/user/info` — 当前用户信息(鉴权)
无请求参数。
**响应** `data` = [用户对象](#41-用户对象-user_info)。
> 新注册用户 `birth_day = 0`,此时调用运势接口会提示"请先完善生日信息",建议在首页做一次引导。
**错误码**:`11001`/`11002`/`11004` 鉴权 · `22000` 用户不存在 · `29003` 用户查询失败
---
#### PUT `/user/profile` — 更新个人资料(鉴权)
**业务逻辑**
- **只更新非空字段**(传什么改什么),全空则直接返回成功且不写库。
- 昵称按字符数(汉字算 1)限制 ≤ 32,超长返回 `10000` + `details:["昵称长度不能超过 32 个字符"]`。
- `birth_day` 不能晚于当前时间。
- 更新后自动失效缓存,下次 `GET /user/info` 即为最新值。
**请求**(json,全部可选)
| 参数 | 类型 | 必填 | 约束 | 说明 |
| --- | --- | --- | --- | --- |
| username | string | 否 | `max=32` | 昵称 |
| avatar | string | 否 | `max=500` | 头像地址(建议先自行上传到 COS 再传 URL) |
| sex | int | 否 | `oneof=0 1 2` | `0`=未知(忽略) `1`=男 `2`=女 |
| birth_day | int64 | 否 | — | 生日(秒级时间戳),**决定星座与运势** |
**响应** `data` = `{}`
**错误码**:`10000` 参数错误(含 `昵称长度不能超过 32 个字符`、`头像地址过长`、`性别参数不合法`、`生日不能晚于当前时间`)· `29001` 用户更新失败
---
#### POST `/user/avatar/upload` — 上传头像(鉴权)
**业务逻辑**
- multipart 上传,字段名 `file`;仅支持图片(jpeg/png/gif/webp),复用全站图片扩展名白名单与大小限制(`MaxSizeMB`)。
- 上传成功后写入 COS(key 规则 `{用户UUID}/{用户ID}/avatar/{随机名}.{扩展名}`),并将 COS key 存入 `users.avatar`。
- 头像上传**不消耗** COS 上传额度。
- 更新后自动失效缓存,`GET /user/info` 返回的头像字段为可直接访问的 URL。
**请求**:`multipart/form-data`,字段名 `file`。
**响应** `data`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| avatar | string | 头像可访问 URL |
**错误码**:`11001`/`11002`/`11004` 鉴权 · `30000` 上传文件为空 · `30001` 不支持的文件类型 · `30002` 文件大小超过限制 · `39000` 文件保存失败 · `22000` 用户不存在 · `29001` 用户更新失败
---
### 5.2 额度模块
全站共 **4 类额度**:**上传额度为永久额度(不每天重置)**;**AI 推荐 / 社区分享 / 衣柜分享为每日额度(每天 0 点重置)**。
额度有两个来源,**叠加生效**:
**① 免费额度** —— 由服务端配置 `UserDefaultQuota` 控制,所有用户长期享有、与会员身份无关(下表「新用户默认」为内置兜底值,**允许配置为 0**,表示不赠送该类免费额度):
| quota_type | 名称 | 类型 | 新用户默认 | 消耗场景 |
| --- | --- | --- | --- | --- |
| 1 | 上传额度 | 永久 | 20 | 上传一件衣物扣 1 |
| 2 | AI 推荐额度 | 每日 | 3 | 生成一次 AI 穿搭推荐扣 1 |
| 3 | 社区分享额度(搭配分享) | 每日 | 10 | 穿搭首次公开扣 1 |
| 4 | 衣柜分享额度 | 每日 | 5 | 创建一次衣柜分享扣 1 |
**② 额度凭证**(`quota_grants`)—— 有有效期的赠送额度,按来源分三类:
| source | 来源 | 有效期 |
| --- | --- | --- |
| 1 | 会员包(购买) | `expire_time=0`,**跟随 VIP**:VIP 到期自动失效,续期自动延长 |
| 2 | 加量包(购买) | 独立有效期 `goods.valid_days`,到期失效 |
| 3 | 广告激励 | 当天 23:59:59 到期(金额由 `AdReward` 配置固定,开关控制) |
| 4 | 体验会员赠送(新用户注册) | **固定有效期** = 注册时刻 + 体验天数,**不随后续续费延长** |
> 新用户注册时,`NewUserGift` 段配置的体验会员额度以 **source=4** 凭证发放;`GiftVIPDay` 配 0(不赠送会员)时这部分额度一律不发放。
> ⚠️ 体验额度**不会**被后续充值「继承」:用户 3 天体验期后再购买会员,体验额度照常失效,只剩免费额度 + 新购会员包额度。
> Cos 消费先扣「即将过期凭证」再扣免费额度;每日额度总额 = 免费配置 + 有效凭证赠送额。
#### GET `/quota/info` — 额度概览(鉴权)
无请求参数。
**响应** `data`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| quotas | array | 额度数组,元素见下表 |
`quotas[]`:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| quota_type | int | `1`=上传 `2`=AI `3`=社区分享 `4`=衣柜分享 |
| total | int64 | 总额度(上传=永久总额度;AI/社区/衣柜=每日总额度) |
| used | int64 | 已使用(上传=永久累计已用;AI/社区/衣柜=当日已用) |
| remain | int64 | 剩余(`max(total-used, 0)`,不会为负;每日额度次日 0 点重置) |
**错误码**:`19001` 数据库错误 · `89000` 额度更新失败
---
#### GET `/quota/flow` — 额度流水(鉴权)
**请求**(query)
| 参数 | 类型 | 必填 | 约束 | 说明 |
| --- | --- | --- | --- | --- |
| page / page_size | int | 否 | 见[分页约定](#24-分页约定) | 分页 |
| quota_type | int | 否 | `oneof=1 2 3 4` | 额度类型筛选;不传 = 全部 |
**响应** `data.list[]` = [额度流水对象](#47-额度流水对象-quotaflowitem)
**错误码**:`10000` 参数错误(quota_type 非 1~4)· `19001` 数据库错误
---
#### POST `/ad-reward/grant` — 广告激励额度发放(鉴权)
看广告后发放单次激励额度,**当天有效**(当天 23:59:59 到期),通过 `ref_id` 幂等防重复领取。
**业务逻辑**:广告激励由服务端 `AdReward` 配置开关控制——**未开启时返回 `80004 广告激励未开启`**;发放金额由**服务端配置固定值**(`cos`/`ai`/`outfit_share`/`closet_share` 各类型分别配置),前端**不传金额**,只传类型与广告记录 ID。
**请求**(JSON body)
| 参数 | 类型 | 必填 | 约束 | 说明 |
| --- | --- | --- | --- | --- |
| reward_type | string | ✅ | `oneof=cos ai outfit_share closet_share` | 激励类型,映射到额度类型 |
| ref_id | string | ✅ | — | 广告记录ID(幂等键,同 ref_id+类型 重复发放返回 `80002`) |
**响应** `data`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| ref_id | string | 广告记录ID |
| quota_type | int | `1`=上传 `2`=AI `3`=社区分享 `4`=衣柜分享 |
| amount | int64 | 发放数量(由服务端配置决定) |
**错误码**:`80002` 广告激励已领取 · `80003` 无效的激励类型 · `80004` 广告激励未开启 · `10000` 参数错误 · `19001` 数据库错误
---
### 5.3 运势模块
#### GET `/horoscope/today` — 今日星座运势(鉴权)
**业务逻辑**
- 星座由用户 `birth_day` **自动推算**,请求不需要传星座。
- **未设置生日** → `10000` + `details:["请先完善生日信息"]`,前端应引导跳转资料页。
- 读取顺序:Redis 缓存(有效期至次日 0 点)→ MySQL → **都没有时异步触发生成,并当次返回 `90300`**。此时前端应提示"运势生成中,请稍后重试",3~5 秒后重试即可。
- 十二星座运势由后端每日 0 点定时任务批量生成(Redis 分布式锁保证多实例只生成一次)。
- 运势是 AI 推荐的输入之一,缺失不影响 AI 推荐(会降级为仅星座或空)。
**请求**:无参数。
**响应** `data`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| stat_date | int64 | 日期,格式 `YYYYMMDD`(如 `20260907`) |
| zodiac | string | 星座中文名(摩羯座 / 水瓶座 / … / 射手座) |
| overall_score | int | 综合运势 `1-5` |
| love_score | int | 爱情运势 `1-5` |
| career_score | int | 事业运势 `1-5` |
| wealth_score | int | 财运运势 `1-5` |
| daily_text | string | 今日运势文案 |
| lucky_color | string | 今日幸运色 |
| lucky_number | string | 今日幸运数字 |
**错误码**:`10000` + `请先完善生日信息` · `90300` 星座运势不存在(生成中,请重试)· `19001` 数据库错误
---
### 5.4 衣物模块
#### 图片处理状态枚举 `image_process_status`(全模块通用)
| 值 | 含义 | 前端处理 |
| --- | --- | --- |
| 1 | 抠图中 | 继续轮询 |
| 2 | 分类中 | 继续轮询 |
| 3 | 排队中 | 继续轮询 |
| 4 | **成功** | 终态:可读取识别结果、可用于 AI 推荐 |
| 5 | **失败** | 终态:查看 `fail_reason`,可重新上传 |
---
#### POST `/clothes/upload` — 上传衣物图片(鉴权)
**业务逻辑**
1. **同步校验**:文件非空 → 扩展名白名单(默认 `jpg/jpeg/png`)→ 大小 ≤ 20MB(配置 `Upload.MaxSizeMB`)→ **magic bytes 文件头真图校验**(防伪造扩展名)。
2. **同步扣 1 个上传额度**(`quota_type=1`),额度不足直接 `80000`,不会创建记录。
3. 创建衣物记录,`image_process_status = 3`(排队中),**立即返回**。
4. **后端异步流水线**(与请求无关,最多并发 10,单任务超时 3 分钟):
`3 排队中` → `1 抠图中`(BiRefNet 抠图,失败重试 3 次,间隔 1s)→ 上传 COS → `2 分类中`(Fashion-CLIP 识别 15 个属性,失败重试 3 次)→ `4 成功`(回填属性 + 抠图 URL)。
5. **任一环节失败** → 置 `5 失败` 并写 `fail_reason`(`"图片抠图失败"` / `"图片保存失败"` / `"图片上传失败"` / `"图片分类失败"` / `"内部错误"`),**并自动回退 1 个上传额度**。
6. **分类失败但抠图成功时**仍会保留抠好的图(`processed_image`),用户可用 `PUT /clothes/:uuid` 手动补全属性;但状态仍是 `5`。
> **前端必做**:拿到 `clothes_uuid` 后**轮询** `GET /clothes/:uuid/status` 直到 `4` 或 `5`。**状态 1/2/3 时不要展示"完成"**。
**请求**(form,`multipart/form-data`)
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| file | file | ✅ | 衣物图片,**字段名固定为 `file`**,无其他表单字段 |
**响应** `data`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| clothes_uuid | string | 衣物 UUID,后续查询/更新/删除/轮询都用它 |
| image_process_status | int | 初始恒为 `3`(排队中) |
**错误码**:`30000` 上传文件为空 · `30001` 不支持的文件类型(含 magic bytes 校验失败)· `30002` 文件大小超过限制 · `39000` 文件保存失败 · `50002` 创建衣物失败(额度已自动回退)· `80000` 额度不足
---
#### GET `/clothes` — 我的衣物列表(鉴权)
**请求**(query)
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| page / page_size | int | 否 | 分页 |
| category_main | string | 否 | 按大类筛选(上装 / 下装 / 连衣裙 / 外套 / 鞋 / 配饰) |
| category_sub | string | 否 | 按小类筛选 |
**响应** `data`:`total` / `page` / `page_size` + `list`([衣物对象](#42-衣物对象-clothesitem) 数组)
> 列表**包含**处理中的衣物(状态 1/2/3),其属性字段为空字符串。前端应识别并展示"处理中"占位,或按 `image_process_status` 过滤(注意:列表响应不含该字段,如需判断请结合上传后的本地状态或详情接口)。
**错误码**:`10000` 参数错误 · `19001` 数据库错误
---
#### GET `/clothes/:uuid` — 衣物详情(鉴权)
**请求**(path + query)
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| uuid | string | ✅ | 衣物 UUID |
| share_uuid | string | 否 | 穿搭分享 UUID:非属主通过穿搭分享只读查看该分享穿搭内的衣物时必传 |
| outfit_uuid | string | 否 | 穿搭 UUID:非属主通过**社区公开穿搭**只读查看该穿搭内的衣物时必传 |
**响应** `data` = [衣物对象](#42-衣物对象-clothesitem)
**业务逻辑(可见性)**
- 属主:始终可见。
- 非属主:两个只读入口,**二选一携带即可**,都不携带则 `50005`。
1. `share_uuid`:该分享正常、穿搭未被删除 / 封禁、且该衣物属于该分享的穿搭明细 → 返回**只读**衣物信息。
2. `outfit_uuid`:该穿搭**公开可见**(`is_public=2` 且 `audit_status=2` 且 `status=1`,与 `GET /outfit/:uuid` 非属主口径一致)且该衣物属于该穿搭明细 → 返回**只读**衣物信息。
- 只读场景下响应不含失败原因(`fail_reason` 恒为空);通过 `outfit_uuid`(社区公开穿搭)进入时,属主**私有备注 `remark` 也不下发**,避免暴露给陌生人(`share_uuid` 好友分享场景保持原样)。
**错误码**:`10000` 参数错误(uuid 为空)· `50000` 衣物不存在 · `50005` 无权操作该衣物(含未携带凭证 / 穿搭不可见 / 衣物不属于该穿搭)· `90103` 穿搭分享不存在(含穿搭已删除 / 已封禁)· `90104` 穿搭分享已取消
---
#### GET `/clothes/:uuid/status` — 查询 AI 处理状态(鉴权,轮询用)
**请求**(path)
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| uuid | string | ✅ | 衣物 UUID |
**响应** `data`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| image_process_status | int | 处理状态,见 [5.4 节枚举表](#54-衣物模块) |
| fail_reason | string | 失败原因(状态为 `5` 时有效,其余为空) |
**错误码**:`10000` 参数错误 · `50000` 衣物不存在 · `50005` 无权操作该衣物
---
#### PUT `/clothes/:uuid` — 更新衣物属性(鉴权)
**业务逻辑**
- 用于**修正 AI 识别结果**或用户手动补全(尤其分类失败但抠图成功时)。
- **只更新非空字段**,全空返回成功但不写库。
- 字段**无枚举限制**,允许自定义值(如自定义分类)。
- ⚠️ **主颜色的请求字段是 `color`,不是 `color_main`**(响应里才是 `color_main`),这是最容易写错的地方。
- 更新不会改变 `image_process_status`(识别失败仍是 `5`,但属性已可用)。
**请求**(path)
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| uuid | string | ✅ | 衣物 UUID(路径参数优先,body 中的 `clothes_uuid` 会被覆盖) |
**请求体**(json,全部可选,传什么改什么)
| 参数 | 类型 | 约束 | 说明 |
| --- | --- | --- | --- |
| category_main | string | ≤64 | 大类 |
| category_sub | string | ≤64 | 小类 |
| style | string | ≤64 | 风格 |
| **color** | string | ≤64 | ⚠️ **主颜色(请求字段名是 `color`)** |
| color_sub | string | ≤64 | 副颜色 |
| thickness | string | ≤64 | 厚度 |
| version | string | ≤64 | 版型 |
| material | string | ≤64 | 材质 |
| pattern | string | ≤64 | 花纹 |
| sleeve_type | string | ≤64 | 袖型 |
| clothes_type | string | ≤64 | 衣长类型 |
| neckline_type | string | ≤64 | 领型 |
| formality | string | ≤16 | 正式程度 1-5 |
| occasions | string | ≤255 | 适用场合 |
| visual_impact | string | ≤16 | 视觉冲击力 1-5 |
| name | string | ≤64 | 名称 |
| remark | string | ≤500 | 备注 |
**响应** `data` = `{}`
**错误码**:`10000` 参数错误 · `50000` 衣物不存在 · `50004` 更新衣物失败 · `50005` 无权操作该衣物
---
#### DELETE `/clothes/:uuid` — 删除衣物(鉴权)
**业务逻辑**
- **软删除**,**无恢复接口**,视为不可逆操作,前端需二次确认。
- **额度回退规则**(重要):
- 衣物状态为 `1/2/3/4`(处理中或成功)且已扣额度 → 事务内**回退 1 个上传额度**;
- 衣物状态为 `5`(处理失败)→ **不回退**(失败时已回退过,防止双退)。
- **已被穿搭引用的衣物无法删除** → `50001` 衣服已被搭配使用,无法删除(需先从穿搭中移除)。
- 删除后,已创建的衣柜分享里该衣物会自动过滤掉(不会崩)。
**请求**(path)
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| uuid | string | ✅ | 衣物 UUID |
**响应** `data` = `{}`
**错误码**:`10000` 参数错误 · `50000` 衣物不存在 · `50001` 衣服已被搭配使用,无法删除 · `50003` 删除衣物失败 · `50005` 无权操作该衣物
---
### 5.5 穿搭模块
**枚举速查**:`source` `1`=用户创建 `2`=AI 推荐 · `is_public` `1`=不公开 `2`=公开 · `audit_status` `1`=待审核 `2`=审核通过 `3`=审核不通过 · `status` `1`=正常 `2`=封禁 `3`=待用户保存(AI 推荐生成后未保存)
#### POST `/outfit` — 创建穿搭(鉴权)
**业务逻辑**
- 1~10 件衣物,**必须全部属于当前用户**(非本人 → `50005`,不存在 → `50000`),同一穿搭内重复衣物会自动去重。
- 创建后默认 `is_public=1` 私有、`audit_status=1` 待审核、`status=1` 正常。
- `source` 传非 1/2 的值时服务端兜底为 `1`(用户创建)。
- **创建不扣任何额度**,公开时才扣。
**请求**(json)
| 参数 | 类型 | 必填 | 约束 | 说明 |
| --- | --- | --- | --- | --- |
| outfit_name | string | ✅ | required, ≤32 | 穿搭名称 |
| outfit_desc | string | 否 | ≤500 | 穿搭描述 |
| scene | string | 否 | ≤100 | 适合场景 |
| source | int | 否 | `oneof=1 2` | `1`=用户创建 `2`=AI 推荐 |
| items | array | ✅ | `min=1, max=10` | 衣物列表,元素 `{"clothes_uuid": string}` |
**响应** `data` = [穿搭对象](#43-穿搭对象-outfititem)
**错误码**:`10000` 参数错误(名称超长 / items 为空或超过 10 件)· `40001` 创建穿搭失败 · `50000` 衣物不存在 · `50005` 无权操作该衣物
---
#### GET `/outfit` — 我的穿搭列表(鉴权)
**请求**(query)
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| page / page_size | int | 否 | 分页 |
| is_public | int | 否 | `1`=不公开 `2`=公开;不传 = 全部 |
| source | int | 否 | `1`=用户创建 `2`=AI 推荐;不传 = 全部 |
**响应** `data.list[]` = [穿搭对象](#43-穿搭对象-outfititem)
> ⚠️ 该接口**不会过滤 `status=3`(AI 待保存)的穿搭**,但响应中已带上 `status` 字段。前端应据此把 AI 未保存的穿搭归到「AI 推荐」入口,不要混入「我的搭配」。
>
> 这类穿搭超过 7 天未保存会被后端自动清理(软删除),详见 [穿搭对象](#43-穿搭对象-outfititem)。
**错误码**:`10000` 参数错误 · `19001` 数据库错误
---
#### GET `/outfit/community` — 社区穿搭列表(鉴权)
**业务逻辑**
- 返回**其他用户**已公开的穿搭:`is_public=2` + `audit_status=2` + `status=1`(与详情页他人可见性口径一致),并**排除本人**的穿搭(自己的公开穿搭在"我的穿搭"中查看)。
- 列表按发布时间倒序(`id DESC`)。
- 每项聚合作者信息与互动计数:`owner_name` / `owner_avatar`(无头像为空串)、`like_count` / `collect_count`(仅统计有效点赞/收藏)。
**请求**(query)
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| page / page_size | int | 否 | 分页,page_size 上限 50 |
**响应** `data`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| total | int64 | 总条数 |
| page | int | 当前页 |
| page_size | int | 每页条数 |
| list | array | 社区穿搭数组,见下表 |
`list[]`:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| outfit_uuid | string | 穿搭 UUID |
| outfit_name | string | 穿搭名称 |
| outfit_desc | string | 穿搭描述 |
| scene | string | 适合场景 |
| source | int | `1`=用户创建 `2`=AI 推荐 |
| created_at | int64 | 发布时间(秒级时间戳) |
| items | array | 衣物明细 `[{clothes_uuid, processed_image}]`,`processed_image` 为可访问 URL |
| owner_name | string | 作者昵称 |
| owner_avatar | string | 作者头像 URL(无则空串) |
| like_count | int64 | 点赞数 |
| collect_count | int64 | 收藏数 |
**错误码**:`10000` 参数错误 · `19001` 数据库错误
---
#### GET `/outfit/:uuid` — 穿搭详情(鉴权)
**请求**(path)
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| uuid | string | ✅ | 穿搭 UUID |
**业务逻辑(可见性)**
- 本人穿搭:始终可见。
- 他人穿搭:必须同时满足 `is_public=2` + `audit_status=2` + `status=1`,否则返回 `40004`。
- 通过好友分享链接查看穿搭请使用 `GET /outfit-share/:uuid`(不要求穿搭已公开)。
**响应** `data` = [穿搭对象](#43-穿搭对象-outfititem)
**错误码**:`10000` 参数错误 · `40000` 穿搭不存在 · `40004` 无权操作该穿搭(他人未公开/未过审)
---
#### PUT `/outfit/:uuid` — 更新穿搭(鉴权)
**业务逻辑**
- 只更新 `outfit_name` / `outfit_desc` / `scene` 与明细。
- **明细采用"先删后建"**(事务内),中途失败不会留下空明细。
- `outfit_uuid` 以路径参数为准(body 中传了也会被覆盖)。
- 更新**不影响**公开状态与审核状态(已公开的仍是公开,不重复扣额度)。
**请求**(path)
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| uuid | string | ✅ | 穿搭 UUID |
**请求体**(json)
| 参数 | 类型 | 必填 | 约束 | 说明 |
| --- | --- | --- | --- | --- |
| outfit_name | string | ✅ | required, ≤32 | 穿搭名称 |
| outfit_desc | string | 否 | ≤500 | 穿搭描述 |
| scene | string | 否 | ≤100 | 适合场景 |
| items | array | ✅ | `min=1, max=10` | 衣物列表,元素 `{"clothes_uuid": string}` |
**响应** `data` = `{}`
**错误码**:`10000` 参数错误 · `40000` 穿搭不存在 · `40003` 更新穿搭失败 · `40004` 无权操作该穿搭 · `50000` 衣物不存在
---
#### DELETE `/outfit/:uuid` — 删除穿搭(鉴权)
**业务逻辑**
- 软删穿搭 + **物理删除**明细,**无恢复接口**,需二次确认。
- **不返还任何额度**(已扣的搭配分享额度不退)。
- 删除后,他人收藏列表与点赞记录不会自动清理。
**请求**(path)
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| uuid | string | ✅ | 穿搭 UUID |
**响应** `data` = `{}`
**错误码**:`10000` 参数错误 · `40000` 穿搭不存在 · `40002` 删除穿搭失败 · `40004` 无权操作该穿搭
---
#### PUT `/outfit/:uuid/public` — 设置公开状态(鉴权)
**业务逻辑(额度相关,重点)**
- **公开(`is_public=2`)**:条件更新 `is_public 1→2` 且 `audit_status → 2`(**自动审核通过,无需等待人工**)→ 成功后**扣 1 个社区分享额度**(`quota_type=3`)。
- **额度不足 → 回滚为私有**并返回 `80000`,穿搭不会公开。
- 重复公开(已是公开态)→ **幂等成功,不重复扣额度**。
- ⚠️ **取消公开后再次公开,会再扣 1 个额度**(因为 `is_public` 已回到 1,首次公开条件重新成立)。
- **取消公开(`is_public=1`)**:直接置为私有,**不返还已扣额度**。
- 公开成功后,其他用户可通过 `POST /like`、`POST /collect` 按 UUID 互动。
**请求**(path)
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| uuid | string | ✅ | 穿搭 UUID |
**请求体**(json)
| 参数 | 类型 | 必填 | 约束 | 说明 |
| --- | --- | --- | --- | --- |
| is_public | int | ✅ | required, `oneof=1 2` | `1`=不公开 `2`=公开 |
**响应** `data` = `{}`
**错误码**:`10000` 参数错误 · `40000` 穿搭不存在 · `40003` 更新穿搭失败 · `40004` 无权操作该穿搭 · `80000` 额度不足(已自动回滚为私有)
---
### 5.6 AI 推荐模块
> AI 推荐是**异步接口**。`POST /ai-recommend` 只提交任务并立即返回 `task_id`,真正的生成在后台执行;前端用 `GET /ai-recommend/status` 轮询结果。
>
> 这样设计的好处:接口不再长时间挂起,**前端退出、切页面、断网都不会中断生成** —— 结果仍会落库为"待保存"穿搭,用户重新进入也能看到。
#### POST `/ai-recommend` — 提交 AI 穿搭推荐任务(鉴权)
**业务逻辑**
1. **提交幂等**:同一用户已有任务在生成中时,直接返回该任务的 `task_id`(**不会重复扣额度**),前端继续轮询原任务即可。
2. **提交时先扣 1 个 AI 额度**(`quota_type=2`);额度不足返回 **`90401` AI 推荐次数已达上限**(注意不是 `80000`)。
3. 接口**秒级返回**(实测 ~30ms),不等大模型。
4. 生成在后台执行,输入为:当前用户**处理成功(状态 4)**的全部衣物 + 生日推算的星座 + 今日运势(星座/运势缺失会降级,不影响生成)。
5. 失败时**额度自动回退**,失败原因写入任务状态(`error_code` / `error_msg`),详见下方状态接口:
- `90400` + `衣橱暂无可用衣物`
- `90400` + `大模型调用失败`
- `90400` + `未生成有效搭配`
- `90400` + `生成任务异常终止`(超过 3 分钟未完成)
6. ⚠️ **生成成功但未调用 save,不退还 AI 额度**(额度在提交时已消耗)。
**请求**(json)
| 参数 | 类型 | 必填 | 约束 | 说明 |
| --- | --- | --- | --- | --- |
| scene_type | int | ✅ | required, `oneof=1 2` | `1`=每日推荐 `2`=个性化场景 |
| scene | string | 否 | — | 用户自定义场景描述(`scene_type=2` 时使用,如"参加朋友的婚礼") |
**响应** `data`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| task_id | string | 任务 ID,用于轮询 `GET /ai-recommend/status` |
| status | int | 任务状态,提交后恒为 `1`(生成中) |
**错误码**:`10000` 参数错误 · `90401` AI 推荐次数已达上限(AI 额度不足)· `10003` 请求过于频繁(429,活跃位刚过期的极端情况,重试即可)· `19002` 缓存错误
---
#### GET `/ai-recommend/status` — 查询 AI 推荐任务状态(鉴权)
**业务逻辑**
- 任务快照保存在 Redis,**有效期 15 分钟**;任务不存在、已过期或不属于当前用户,统一返回 `90402`(不暴露他人任务是否存在)。
- 任务状态 `status`:
| 值 | 含义 | 前端处理 |
| --- | --- | --- |
| 1 | 生成中 | 继续轮询,建议 2 秒一次、上限 90 次(3 分钟) |
| 2 | **成功** | 终态:读取 `result` 渲染推荐结果 |
| 3 | **失败** | 终态:读 `error_code` / `error_msg` 提示用户(**额度已自动回退**) |
**请求**(query)
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| task_id | string | ✅ | `POST /ai-recommend` 返回的 `task_id` |
**响应** `data`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| task_id | string | 任务 ID |
| status | int | 任务状态:`1` 生成中 / `2` 成功 / `3` 失败 |
| result | object | **仅 `status=2` 时存在**,结构见下表 |
| error_code | int | **仅 `status=3` 时存在**,业务错误码(通常 `90400`) |
| error_msg | string | **仅 `status=3` 时存在**,失败原因文案 |
`result` 对象(`status=2`):
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| outfit_uuid | string | 生成结果 UUID(**保存时要用**) |
| outfit_name | string | 穿搭名称 |
| outfit_desc | string | 穿搭描述 |
| scene | string | 场景 |
| source | int | `2`=AI 推荐 |
| status | int | ⚠️ 穿搭状态 `3`=待用户保存(**与顶层的任务状态 `status` 同名但含义不同**) |
| outfit_item | array | ⚠️ 注意字段名是 `outfit_item`(单数 item),元素 `{"clothes_uuid": string}` |
**错误码**:`10000` 参数错误 · `90402` 任务不存在或已过期 · `19002` 缓存错误
---
#### POST `/ai-recommend/save` — 保存 AI 推荐为我的穿搭(鉴权)
**业务逻辑**
- 仅 `status=3`(待用户保存)的穿搭可保存 → 置为 `status=1`。
- 已是 `status=1` → **幂等成功**(重复保存安全)。
- 其它状态(如已封禁)→ `40003`。
- **保存不扣任何额度**;后续若要公开到社区,仍需调 `PUT /outfit/:uuid/public`,届时扣 1 个搭配分享额度。
**请求**(json)
| 参数 | 类型 | 必填 | 约束 | 说明 |
| --- | --- | --- | --- | --- |
| outfit_uuid | string | ✅ | required | 由 `GET /ai-recommend/status` 返回的 `result.outfit_uuid` |
**响应** `data` = `{}`
**错误码**:`10000` 参数错误 · `40000` 穿搭不存在 · `40003` 更新穿搭失败(状态不允许保存)· `40004` 无权操作该穿搭
---
### 5.7 衣柜分享模块
**状态枚举** `status`:`1`=正常 `2`=已取消 `3`=已过期 `4`=已封禁
**分享类型** `share_type`:`1`=部分衣物分享 `2`=全量快照分享
#### POST `/closet-share` — 创建衣柜分享(鉴权)
**业务逻辑**
1. `expire_time`:`0` 表示**永不过期**;非 0 时必须晚于当前时间(否则 `10000` + `details:["过期时间必须晚于当前时间"]`)。
2. `share_type=1`:必须传非空 `clothes_uuids`(否则 `10000` + `details:["部分分享需选择衣物"]`),且衣物必须全部属于本人。
`share_type=2`:自动快照当前全部衣物(后端分页遍历,无需传列表)。
3. **先扣 1 个衣柜分享额度**(`quota_type=4`),不足 → `80000`。
4. 创建分享(`status=1` 正常)→ 写入明细;**明细写入失败会回退额度并返回 `19001`**(避免产生空分享)。
5. 创建即自动审核通过。
**请求**(json)
| 参数 | 类型 | 必填 | 约束 | 说明 |
| --- | --- | --- | --- | --- |
| share_type | int | ✅ | required, `oneof=1 2` | `1`=部分衣物 `2`=全量快照 |
| clothes_uuids | array | 条件必填 | — | 衣物 UUID 数组;`share_type=1` 时必填且非空 |
| expire_time | int64 | 否 | — | 过期时间戳(秒);`0` 或省略 = 永不过期 |
**响应** `data`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| share_uuid | string | 分享 UUID,供他人访问分享详情 |
**错误码**:`10000` 参数错误(含 `部分分享需选择衣物`、`过期时间必须晚于当前时间`)· `80000` 额度不足 · `19001` 数据库错误(额度已回退)· `50000` 衣物不存在 · `50005` 无权操作该衣物
---
#### GET `/closet-share` — 我的分享列表(鉴权)
**请求**(query)
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| page / page_size | int | 否 | 分页 |
| status | int | 否 | `1`=正常 `2`=已取消 `3`=已过期 `4`=已封禁;不传 = 全部 |
**响应** `data.list[]`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| share_uuid | string | 分享 UUID |
| share_type | int | `1`=部分衣物 `2`=全量快照 |
| clothes_count | int | 衣物数量 |
| expire_time | int64 | 过期时间戳(秒),`0`=永不过期 |
| status | int | `1`=正常 `2`=已取消 `3`=已过期 `4`=已封禁 |
| owner_name | string | 分享者昵称(本接口暂不返回,恒为空) |
| owner_avatar | string | 分享者头像(本接口暂不返回,恒为空) |
| created_at | int64 | 分享创建时间(秒,前端合并排序用) |
> ⚠️ **过期是"惰性"的**:`expire_time` 到了但没人访问时,列表里 `status` 可能仍是 `1`,直到有人访问详情才变成 `3`。**前端应同时判断 `status` 与 `expire_time`** 来展示"已过期"。
**错误码**:`10000` 参数错误 · `19001` 数据库错误
---
#### GET `/closet-share/received` — 分享给我的列表(鉴权)
**业务逻辑**
- "访问即视为收到":登录用户(非分享属主)成功访问他人分享详情时,自动记录接收关系;重复访问不重复记录,属主访问不记录。
- 列表按接收记录倒序分页返回,支持按 `status` 筛选。
**请求**(query)
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| page / page_size | int | 否 | 分页 |
| status | int | 否 | `1`=正常 `2`=已取消 `3`=已过期 `4`=已封禁;不传 = 全部 |
**响应** `data.list[]`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| share_uuid | string | 分享 UUID |
| share_type | int | `1`=部分衣物 `2`=全量快照 |
| clothes_count | int | 衣物数量 |
| expire_time | int64 | 过期时间戳(秒),`0`=永不过期 |
| status | int | `1`=正常 `2`=已取消 `3`=已过期 `4`=已封禁 |
| owner_name | string | 分享者昵称 |
| owner_avatar | string | 分享者头像 |
> ⚠️ 过期与状态判定同"我的分享列表",前端应同时判断 `status` 与 `expire_time`。
**错误码**:`10000` 参数错误 · `19001` 数据库错误
---
#### GET `/closet-share/:uuid` — 分享详情(鉴权,属主 / 访客视角)
**业务逻辑**
- 属主与访客都可访问(需已登录,`is_owner` 区分视角)。
- **访客访问会 +1 访问计数**,属主自己访问不计数。
- 状态处理:`2` 已取消 → `90102`;`4` 已封禁 → `90100`(**故意伪装成"不存在"**);`3` 已过期 → `90101`;`expire_time` 已过 → 惰性置为 `3` 并返回 `90101`。
- 已被软删除的衣物会自动从分享中过滤。
**请求**(path)
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| uuid | string | ✅ | 分享 UUID |
**响应** `data`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| share_uuid | string | 分享 UUID |
| owner_name | string | 分享者昵称 |
| owner_avatar | string | 分享者头像 |
| clothes | array | [衣物对象](#42-衣物对象-clothesitem) 数组 |
| expire_time | int64 | 过期时间戳(秒) |
| status | int | `1`=正常 `2`=已取消 `3`=已过期 `4`=已封禁 |
| is_owner | bool | 是否本人分享(`true` 时隐藏"收藏/点赞"入口) |
**错误码**:`10000` 参数错误 · `90100` 分享不存在(含已封禁)· `90101` 分享已过期 · `90102` 分享已取消
---
#### POST `/closet-share/:uuid/cancel` — 取消分享(鉴权)
**业务逻辑**
- **仅属主可操作**,他人 → `12000`(HTTP 403)。
- 置 `status=2`,**不可逆,且不返还衣柜分享额度**,前端需二次确认。
**请求**(path)
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| uuid | string | ✅ | 分享 UUID |
**响应** `data` = `{}`
**错误码**:`10000` 参数错误 · `12000` 无权限访问(403)· `90100` 分享不存在 · `19001` 数据库错误
---
### 5.8 穿搭分享模块
**状态枚举** `status`:`1`=正常 `2`=用户取消
> 穿搭分享用于把「自己的穿搭」以链接形式分享给好友,**与「公开到社区」完全解耦**:仅自己可见的穿搭同样可以分享,好友凭 `share_uuid` 只读查看整套穿搭与单品图。
>
> **额度**:分享**仅自己可见**的穿搭扣 1 次**衣柜分享额度**(`quota_type=4`,与衣柜分享共用同一个额度池);分享**已公开**的穿搭**不扣**(公开到社区时已扣过社区分享额度)。额度不足 → `80000` 且不创建分享。
>
> 分享一旦发出即独立于穿搭的公开状态——取消公开不影响已发出的分享,仅「取消分享」「删除穿搭」「封禁穿搭」才使分享失效。
#### POST `/outfit-share` — 创建穿搭分享(鉴权)
**业务逻辑**
1. 仅能分享**本人**的穿搭(他人 → `40004`)。
2. **无需先公开到社区**:仅自己可见的穿搭同样可分享(分享与公开解耦);穿搭已被删除 → `40000`,已被封禁 → `40004`。
3. **额度**:`is_public=1`(仅自己可见)→ 扣 1 次衣柜分享额度(`quota_type=4`,与衣柜分享共用池),额度不足 → `80000` 且**不创建分享**;`is_public=2`(已公开)→ **不扣**。扣额成功后分享落库失败会**自动回退** 1 次额度。
4. 创建成功后返回 `share_uuid`,分享链接携带该值。
**请求**(json)
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| outfit_uuid | string | ✅ | 穿搭 UUID |
**响应** `data`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| share_uuid | string | 分享 UUID,供他人访问分享详情 |
**错误码**:`10000` 参数错误 · `40000` 穿搭不存在 · `40004` 无权操作该穿搭 · `80000` 额度不足 · `19001` 数据库错误
---
#### GET `/outfit-share` — 我的穿搭分享列表(鉴权)
**请求**(query)
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| page | int | 否 | 页码,默认 1 |
| page_size | int | 否 | 每页数量,默认 10,最大 50 |
| status | int | 否 | `1`=正常 `2`=已取消;不传 = 全部 |
**响应** `data.list[]`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| share_uuid | string | 分享 UUID |
| outfit_uuid | string | 穿搭 UUID |
| outfit_name | string | 穿搭名称 |
| cover_image | string | 封面(首件衣物抠图 URL,无则空) |
| items_count | int | 包含衣物数量 |
| status | int | `1`=正常 `2`=已取消 |
| owner_name | string | 分享者昵称(本接口不返回,恒为空) |
| owner_avatar | string | 分享者头像(本接口不返回,恒为空) |
| created_at | int64 | 分享创建时间(秒) |
**错误码**:`10000` 参数错误 · `19001` 数据库错误
---
#### GET `/outfit-share/received` — 分享给我的穿搭分享(鉴权)
**业务逻辑**
- "访问即视为收到":登录用户(非分享属主)成功访问他人穿搭分享详情时,自动记录接收关系;重复访问不重复记录,属主访问不记录。
- 列表按接收记录倒序分页返回,支持按 `status` 筛选;**仅排除已删除 / 已封禁的穿搭**(穿搭是否公开不影响,分享与公开解耦)。
- 字段同「我的穿搭分享列表」,并额外返回 `owner_name` / `owner_avatar`。
**错误码**:`10000` 参数错误 · `19001` 数据库错误
---
#### GET `/outfit-share/:uuid` — 穿搭分享详情(鉴权,属主 / 访客视角)
**业务逻辑**
- 属主与访客都可访问(需已登录,`is_owner` 区分视角)。
- **访客访问成功即记录接收关系**;属主访问不记录。
- 分享已取消 → `90104`;分享不存在或对应穿搭已删除 / 已封禁 → `90103`(穿搭取消公开**不影响**已发出的分享)。
**请求**(path)
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| uuid | string | ✅ | 分享 UUID |
**响应** `data`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| share_uuid | string | 分享 UUID |
| owner_name | string | 分享者昵称 |
| owner_avatar | string | 分享者头像 |
| status | int | `1`=正常 `2`=已取消 |
| is_owner | bool | 是否本人分享 |
| outfit | object | [穿搭对象](#43-穿搭对象-outfititem),`items[]` 含 `processed_image`(可直接展示) |
**错误码**:`10000` 参数错误 · `90103` 穿搭分享不存在(含穿搭已删除 / 已封禁)· `90104` 穿搭分享已取消
---
#### POST `/outfit-share/:uuid/cancel` — 取消穿搭分享(鉴权)
**业务逻辑**:**仅属主可操作**,他人 → `12000`(HTTP 403);置 `status=2`,不可逆。
**请求**(path)
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| uuid | string | ✅ | 分享 UUID |
**响应** `data` = `{}`
**错误码**:`10000` 参数错误 · `12000` 无权限访问(403)· `90103` 穿搭分享不存在 · `19001` 数据库错误
---
### 5.9 收藏与点赞模块
**动作枚举** `action`:`1`=执行(点赞/收藏)`2`=取消。两个接口都**幂等**,重复提交无副作用。
#### POST `/collect` — 收藏 / 取消收藏(鉴权)
**业务逻辑**
- 目标为社区穿搭 UUID。目标不存在 → `90200`;**非本人且未公开可见** → `40004`。
- 取消时仅当当前为"已收藏"才更新为已取消;**记录不会物理删除**,可再次收藏。
- **收藏不触发任何通知**。
**请求**(json)
| 参数 | 类型 | 必填 | 约束 | 说明 |
| --- | --- | --- | --- | --- |
| target_uuid | string | ✅ | required | 目标穿搭 UUID |
| action | int | ✅ | required, `oneof=1 2` | `1`=收藏 `2`=取消收藏 |
**响应** `data` = `{}`
**错误码**:`10000` 参数错误 · `90200` 目标内容不存在 · `90202` 收藏操作失败 · `40004` 无权操作该穿搭 · `19001` 数据库错误
---
#### GET `/collect` — 我的收藏列表(鉴权)
**请求**(query):仅 `page` / `page_size`。
**响应** `data.list[]` = [穿搭对象](#43-穿搭对象-outfititem)
**错误码**:`10000` 参数错误 · `19001` 数据库错误
---
#### GET `/collect/:targetUUID` — 查询收藏状态与数量(鉴权)
进入穿搭详情时用于初始化收藏按钮状态。
**请求**(path)
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| **targetUUID** | string | ✅ | ⚠️ 路径参数名是 `targetUUID`(驼峰),不是 `uuid` |
**响应** `data`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| is_collected | bool | 当前用户是否已收藏 |
| collect_count | int64 | 该穿搭收藏总数 |
**错误码**:`10000` 参数错误 · `90200` 目标内容不存在 · `40004` 无权操作该穿搭 · `19001` 数据库错误
---
#### POST `/like` — 点赞 / 取消点赞(鉴权)
**业务逻辑**
- 目标为社区穿搭 UUID。目标不存在 → `90200`;**非本人且未公开可见** → `40004`。
- **通知触发条件**:仅当状态从"无记录或已取消"变为"已点赞"、且**不是给自己点赞**时,才向穿搭作者发一条 `msg_type=1` 通知(标题"收到新的点赞",内容"你的穿搭「{名称}」收到了一个赞")。**重复点赞不会重复通知**。
**请求**(json)
| 参数 | 类型 | 必填 | 约束 | 说明 |
| --- | --- | --- | --- | --- |
| target_uuid | string | ✅ | required | 目标穿搭 UUID |
| action | int | ✅ | required, `oneof=1 2` | `1`=点赞 `2`=取消点赞 |
**响应** `data` = `{}`
**错误码**:`10000` 参数错误 · `90200` 目标内容不存在 · `90201` 点赞操作失败 · `40004` 无权操作该穿搭 · `19001` 数据库错误
---
#### GET `/like/:targetUUID` — 查询点赞状态与数量(鉴权)
**请求**(path)
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| **targetUUID** | string | ✅ | ⚠️ 路径参数名是 `targetUUID`(驼峰),不是 `uuid` |
**响应** `data`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| is_liked | bool | 当前用户是否已点赞 |
| like_count | int64 | 该穿搭点赞总数 |
**错误码**:`10000` 参数错误 · `19001` 数据库错误
---
### 5.10 通知模块
**`is_read` 取值**:⚠️ `1`=**未读** `2`=**已读**(非常规的 0/1,也不是布尔值)
**`msg_type` 取值**:`1`=点赞通知 `2`=订单通知 `3`=系统通知(当前代码无发送点,预留)
**通知触发点(仅 3 处)**
| 触发动作 | msg_type | 标题 | 内容 | 接收者 |
| --- | --- | --- | --- | --- |
| 他人点赞我的公开穿搭 | `1` | 收到新的点赞 | 你的穿搭「{OutfitName}」收到了一个赞 | 穿搭作者(非本人点赞才触发) |
| 支付成功 | `2` | 订单支付成功 | 你购买的「{GoodsName}」已到账 | 买家(sender=0 系统) |
| 退款成功 | `2` | 退款成功 | 订单「{GoodsName}」已退款 | 买家(sender=0 系统) |
> 通知**无推送能力**(无 WebSocket/订阅消息),前端需**轮询** `GET /notification/unread` 刷新红点。
#### GET `/notification` — 通知列表(鉴权)
**请求**(query)
| 参数 | 类型 | 必填 | 约束 | 说明 |
| --- | --- | --- | --- | --- |
| page / page_size | int | 否 | — | 分页 |
| is_read | int | 否 | `oneof=1 2` | ⚠️ `1`=未读 `2`=已读;不传 = 全部(**不是布尔值**) |
| msg_type | int | 否 | `oneof=1 2 3` | `1`=点赞 `2`=订单 `3`=系统;不传 = 全部(可与 `is_read` 组合筛选) |
**响应** `data.list[]` = [通知对象](#46-通知对象-notificationitem)
**错误码**:`10000` 参数错误 · `19001` 数据库错误
---
#### GET `/notification/unread` — 未读通知数(鉴权)
**业务逻辑**:优先读 Redis 计数;Redis 为 0 或异常时回落 MySQL 统计并回填缓存。计数为 best-effort,可能短暂偏小,**展示红点时以列表接口为准更稳妥**。
**响应** `data`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| unread_count | int64 | 未读通知数 |
**错误码**:`19001` 数据库错误 · `19002` 缓存错误
---
#### GET `/notification/:uuid` — 通知详情(鉴权)
**请求**(path)
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| uuid | string | ✅ | 通知 UUID |
**响应** `data` = [通知对象](#46-通知对象-notificationitem)
**错误码**:`10000` 参数错误 · `90000` 通知不存在 · `12000` 无权限访问
---
#### POST `/notification/:uuid/read` — 标记已读(鉴权)
**业务逻辑**
- **条件更新**(仅"未读→已读"),防并发重复扣减未读数;已读时直接返回当前未读数,幂等。
- ⚠️ **没有"全部已读"接口**,需要全标已读时前端需逐条调用。
**请求**(path)
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| uuid | string | ✅ | 通知 UUID |
**响应** `data` = `{}`
**错误码**:`10000` 参数错误 · `90000` 通知不存在 · `19001` 数据库错误
---
### 5.11 商品模块(公开,无需登录)
**`goods_type` 枚举**:`1`=会员包 `2`=上传包 `3`=AI 推荐包 `4`=衣柜分享包 `5`=搭配分享包
**`status`**:`1`=上架 `2`=下架(列表只返回上架商品)
#### GET `/goods` — 商品列表(公开)
**请求**(query)
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| page / page_size | int | 否 | 分页 |
| goods_type | int | 否 | 商品类型筛选;不传或 `0` = 全部 |
**响应** `data.list[]` = [商品对象](#44-商品对象-goodsitem)(**仅上架商品**)
**错误码**:`10000` 参数错误 · `19001` 数据库错误
---
#### GET `/goods/:code` — 按编码查询商品(公开)
**请求**(path)
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| code | string | ✅ | 商品编码 `goods_code`(**不是数字 ID**) |
**响应** `data` = [商品对象](#44-商品对象-goodsitem)
**错误码**:`10000` 参数错误 · `60000` 商品不存在 · `60001` 商品已下架
---
### 5.12 订单与退款模块
**`order_status` 枚举**:`1`=待支付 `2`=已支付 `3`=已取消 `4`=已过期 `5`=已退款
**退款 `status` 枚举**:`0`=待处理 `1`=退款中 `2`=退款成功 `3`=退款失败
#### POST `/order` — 创建订单并发起微信支付(鉴权)
**业务逻辑**
1. 只用 `goods_code` 下单,**金额由服务端按商品售价 `discount_price` 计算**,前端**不要传金额**。
2. 商品必须存在且上架(否则 `60000` / `60001`)。
3. 用户必须有 `wx_open_id`(即必须通过微信登录),否则 `79000` + `details:["缺少微信 openid"]`。
4. **支付有效期 15 分钟**,`expire_time` 用于倒计时。
5. 下单时把商品权益(会员天数 / 各类额度)**快照进订单**,后续商品改价不影响已下单权益。
6. **先落本地待支付订单,再调微信 JSAPI**。微信下单失败返回 `79000`,但**本地订单保留为待支付**,15 分钟后自动过期。
7. 同一用户可并存多个待支付订单(**无防重限制**),前端应避免重复点击。
**请求**(json)
| 参数 | 类型 | 必填 | 约束 | 说明 |
| --- | --- | --- | --- | --- |
| goods_code | string | ✅ | required | 商品编码(**唯一请求字段**) |
**响应** `data`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| order_no | string | 订单号(后续取消/退款/查询用) |
| pay_amount | int64 | 应付金额(**分**) |
| expire_time | int64 | 支付截止时间戳(秒),倒计时用(15 分钟) |
| pay_params | object | 微信支付参数,[见此](#48-微信支付参数-pay_params),**整体透传给 `wx.requestPayment`** |
**错误码**:`10000` 参数错误 · `60000` 商品不存在 · `60001` 商品已下架 · `79000` 创建订单失败(含 `缺少微信 openid`)· `19001` 数据库错误
---
#### POST `/order/virtual` — 创建虚拟支付订单(鉴权)
小程序虚拟商品(会员 / 额度包)**必须走虚拟支付**(微信 2026-04-01 起全终端强制)。本接口本地落单(`pay_channel=2`)后返回前端调起 `wx.requestVirtualPayment` 所需的签名参数,**不调用微信 JSAPI 下单**。
**业务逻辑**
1. 服务端校验虚拟支付已开通、商品上架且已发布为虚拟支付道具(`product_id` 非空、`product_status=2`)。
2. 下单时把商品权益快照进订单;`outTradeNo` 复用本地订单号。
3. 服务端用 `offerId` + 现网 `appKey` 生成 `signData` / `paySig`,用登录缓存的 `session_key` 生成 `signature`,原样返回前端。
4. 支付结果**不由前端回调决定**:前端 `success` 可能丢失,最终以服务端收到的「道具发货推送」为准。
**请求**(json)
| 参数 | 类型 | 必填 | 约束 | 说明 |
| --- | --- | --- | --- | --- |
| goods_code | string | ✅ | required | 商品编码 |
| platform | string | ✅ | `oneof=android ios harmony windows` | 支付平台,前端按 `wx.getSystemInfoSync().platform` 上报 |
**响应** `data`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| order_no | string | 订单号(即 `outTradeNo`) |
| pay_amount | int64 | 实付金额(**分**) |
| expire_time | int64 | 支付截止时间戳(秒),15 分钟 |
| sign_data | string | 参与签名的 JSON 字符串,**原样传给 `wx.requestVirtualPayment`,勿 JSON.parse/stringify** |
| pay_sig | string | 支付签名(服务端生成) |
| signature | string | 用户态签名(服务端生成) |
**前端调用契约**(示例)
```js
const res = await post('/order/virtual', { goods_code, platform: wx.getSystemInfoSync().platform })
wx.requestVirtualPayment({
signData: res.data.sign_data, // 原样透传,勿重新序列化
paySig: res.data.pay_sig,
signature: res.data.signature,
success: () => { /* 仅表示拉起成功,仍需轮询 GET /order 确认 order_status=2 */ },
fail: (err) => { /* requestVirtualPayment:fail cancel = 用户取消 */ },
})
```
**错误码**:`10000` 参数错误 · `60000` 商品不存在 · `60001` 商品已下架 · `70005` 虚拟支付未开通 · `70006` 商品尚未发布为虚拟支付道具 · `70007` 虚拟支付订单不支持取消 · `70008` iOS 虚拟支付最低 1 元 · `79003` 创建虚拟支付订单失败 · `19001` 数据库错误
> iOS 虚拟支付限制:最低 1 元(低于 100 分返回 `70008`)、iOS 15+、微信 8.0.68+、不支持沙箱;结算由 Apple(45~60 天)。iOS 订单退款用户走 App Store 申请,服务端会收到退款问询推送并应答。
>
> **运营约定**:虚拟支付道具的发布与改价由**管理后台**负责(服务端**不会自动同步**道具)。商品发布为道具后回填 `product_id` / `product_status=2` 才可下单;**运营改价后必须通过管理后台同步微信侧道具价格**,否则微信侧道具价格与本地不一致:微信扣款多于订单金额时发货金额校验会拒绝(用户付款拿不到权益),少于订单金额时产生差价资损。
---
#### GET `/order` — 我的订单列表(鉴权)
**请求**(query)
| 参数 | 类型 | 必填 | 约束 | 说明 |
| --- | --- | --- | --- | --- |
| page / page_size | int | 否 | — | 分页 |
| status | int | 否 | `oneof=1 2 3 4 5` | 订单状态筛选;不传 = 全部 |
**响应** `data.list[]` = [订单对象](#45-订单对象-orderitem)
**错误码**:`10000` 参数错误 · `19001` 数据库错误
---
#### POST `/order/:orderNo/cancel` — 取消订单(鉴权)
**业务逻辑**
- **虚拟支付订单(`pay_channel=2`)不可取消** → `70007`(虚拟支付无服务端关单 API,取消后微信侧单仍可支付,会造成付款不到账)。
- **仅 `1` 待支付**订单可取消;已支付 → `70002`;其它状态 → `70001`。
- 条件更新 `1→3`,失败 → `70001`。
- 本地取消后后端尽力调用微信关单(失败仅告警,不影响结果)。
- **不可撤销**,但可重新下单。
**请求**(path)
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| **orderNo** | string | ✅ | ⚠️ 路径参数名是 `orderNo`(驼峰),不是 `uuid` |
**响应** `data` = `{}`
**错误码**:`10000` 参数错误 · `70000` 订单不存在 · `70001` 订单状态不正确 · `70002` 订单已支付 · `79001` 取消订单失败 · `12000` 无权限访问
---
#### POST `/order/refund` — 申请退款(鉴权)
**业务逻辑(重要:人工审核模式)**
- **iOS 虚拟支付订单不可主动退款** → `71004`,请引导用户走 App Store 申请退款(Apple 通过退款问询推送回收权益)。
- **仅 `2` 已支付**订单可申请,其它状态 → `71001`。
- ⚠️ **系统不会自动调微信退款**:接口只是登记退款申请(`status=0` 待处理),**需运营人工审核并手动发起退款**——普通支付订单在微信支付商户后台退款,**虚拟支付订单(`pay_channel=2`)请在虚拟支付后台(Midas)发起退款**,退款结果由推送回调同步。
- 前端应提示"**退款审核中**",并允许轮询订单状态直到 `5` 已退款;**不要承诺实时到账**。
- 同一订单已有退款记录且状态不是 `3`(失败)→ `71001`;上次失败可重新申请(复用记录、换新退款单号)。
- 退款成功后,订单置 `5`,**会员天数与已发放额度会被回收**(已消耗部分不回收),并发退款通知。
**请求**(json)
| 参数 | 类型 | 必填 | 约束 | 说明 |
| --- | --- | --- | --- | --- |
| order_no | string | ✅ | required | 订单号 |
| reason | string | ✅ | required, ≤200 | 退款原因 |
**响应** `data`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| refund_no | string | 退款单号 |
| refund_amount | int64 | 退款金额(**分**) |
| status | int | `0`=待处理 `1`=退款中 `2`=退款成功 `3`=退款失败 |
| apply_time | int64 | 申请时间(秒) |
**错误码**:`10000` 参数错误 · `70000` 订单不存在 · `71000` 退款记录不存在 · `71001` 当前状态不允许退款 · `71002` 退款金额不合法 · `71003` 退款申请失败 · `71004` iOS 订单请通过 App Store 申请退款
---
#### POST `/order/pay/notify` · POST `/order/refund/notify` — 微信回调(公开)
🚫 **这是微信服务端回调地址,前端绝对不要调用。**
- 不走统一 `{code,msg,data}` 响应包装,按微信 V3 规范返回:成功 HTTP 200 `{"code":"SUCCESS","message":"成功"}`;失败 HTTP 500 `{"code":"FAIL","message":"<原因>"}`。
- 服务端处理:验签 + 解密 → 幂等锁(30 秒)→ **金额校验**(与订单金额不一致则拒绝流转、人工介入)→ 流转订单状态 → 发放/回收权益 → 发站内通知。
- 竞态兜底:已过期但已付款 → 补发货;已取消但已付款 → **自动发起微信退款**。
---
## 6. 业务逻辑与状态机
### 6.1 七条业务主线(总览)
```mermaid
flowchart TD
A["微信登录<br/>新用户自动注册"] --> B["初始化 4 类免费额度<br/>默认 上传 20 / AI 3 / 搭配分享 10 / 衣柜分享 5<br/>(可配置,允许为 0)"]
B --> B2["赠送体验会员 + 会员额度<br/>默认 3 天;AI 3 / 分享 3 / 衣柜 3<br/>(体验会员凭证 source=4,固定到期)"]
B2 --> C["上传衣物<br/>校验 → 扣上传额度 → 异步 AI 抠图 + 分类"]
C --> D["衣橱管理<br/>查看 / 修正属性 / 删除回退额度"]
D --> E["创建穿搭"]
D --> F["AI 穿搭推荐<br/>扣 AI 额度 → 大模型生成 → 待用户保存"]
D --> G["衣柜分享<br/>部分衣物 / 全量快照"]
E --> H["公开到社区<br/>自动审核通过 → 扣搭配分享额度"]
H --> I["社区互动<br/>点赞 → 触发站内通知"]
F --> E
B --> J{"额度不足?"}
J -- "是" --> K["选购商品 → 微信支付 → 权益到账"]
K --> B
```
1. **注册与额度**:微信登录首次自动注册,同步初始化 4 类免费额度(内置默认 上传 20 / AI 3 / 搭配分享 10 / 衣柜分享 5,可由 `UserDefaultQuota` 配置,允许为 0)。
2. **衣物生命周期**:上传(校验 + 扣额)→ 异步 AI 处理(抠图 → 分类)→ 成功入库可被选用 → 删除软删并回退额度。
3. **穿搭与社区**:创建穿搭 → 公开(自动审核通过 + 扣额度)→ 他人点赞触发通知。
4. **衣柜分享**:部分衣物或全量快照 → 扣额度 → 属主/访客查看 → 取消或惰性过期。
5. **交易与权益**:选购商品 → 下单(15 分钟有效)→ 微信支付回调发放权益 → 超时过期 / 退款回收。
6. **AI 推荐**:扣 AI 额度 → 结合星座运势与衣橱生成 → 落库"待用户保存" → 保存转正式穿搭。
7. **通知**:点赞、支付成功、退款成功触发站内通知,未读计数实时维护(需前端轮询)。
---
### 6.2 衣物 AI 处理状态机
```mermaid
stateDiagram-v2
[*] --> queued : 上传成功(同步扣上传额度 1 点)
queued --> matting : 异步任务开始
matting --> classifying : 抠图完成(上传 COS)
classifying --> success : 分类完成,回填 15 维属性
classifying --> failed : 分类失败(保留抠图结果)
matting --> failed : 抠图失败
queued --> failed : 保存/上传 COS 异常
success --> [*] : 可被穿搭/AI 推荐选用
failed --> [*] : 用户可修正属性或删除(额度已自动回退)
state "1 抠图中" as matting
state "2 分类中" as classifying
state "3 排队中" as queued
state "4 成功" as success
state "5 失败(写 fail_reason)" as failed
```
**关键规则**
- 上传接口**同步返回**(不等待 AI),返回即 `3 排队中`。
- 异步流水线并发上限 **10**,单任务超时 **3 分钟**;抠图/分类各带重试(默认 3 次,间隔 1 秒)。
- 失败时 `fail_reason` 取值:`图片抠图失败` / `图片保存失败` / `图片上传失败` / `图片分类失败` / `内部错误`。
- **失败自动回退上传额度**,三重防重防护:状态已是 5 不再退、衣物已删除不再退、删除接口用行锁 + 状态判断。
- 只有 `4 成功` 的衣物才会被 AI 推荐采用。
---
### 6.3 穿搭状态机(公开 / 审核 / 保存)
```mermaid
stateDiagram-v2
[*] --> created : 创建穿搭(1~10 件本人衣物)
created --> private : 默认 is_public=1, audit_status=1, status=1
private --> public : 公开(审核自动通过 + 扣搭配分享额度 1 点)
public --> private : 取消公开(不返还额度,再次公开会再扣)
public --> interacted : 他人点赞(触发通知)/ 收藏(不通知)
private --> [*] : 修改(明细先删后建)/ 删除(软删 + 明细物理删)
public --> [*] : 修改 / 删除(均不返还额度)
state "1 私有(待审核)" as private
state "2 公开(审核通过)" as public
state "社区互动" as interacted
```
**AI 推荐穿搭的额外状态**:`status=3` 待用户保存 → 调 `/ai-recommend/save` → `status=1` 正常。
**可见性判定**:他人可见需同时满足 `is_public=2` 且 `audit_status=2` 且 `status=1`,否则 `40004`。
---
### 6.4 衣柜分享状态机
```mermaid
stateDiagram-v2
[*] --> normal : 创建分享(扣衣柜分享额度 1 点 + 自动审核通过)
normal --> cancelled : 用户取消(仅属主,不返还额度,不可逆)
normal --> expired : 惰性过期(访问详情时判定 now > ExpireTime)
normal --> banned : 封禁(预留状态,对访问者隐藏为"不存在")
state "1 正常" as normal
state "2 已取消" as cancelled
state "3 已过期" as expired
state "4 已封禁" as banned
```
**关键规则**
- `expire_time = 0` 表示永不过期。
- **惰性过期**:到时间但无人访问时列表 `status` 仍为 `1`,前端必须同时判断 `status` 与 `expire_time`。
- 访客访问 +1 访问计数,属主访问不计数。
- 删除衣物**不会**清理分享明细,被软删的衣物在分享详情中自动过滤。
---
### 6.5 订单状态机
```mermaid
stateDiagram-v2
[*] --> pending : 创建订单(快照权益 + 微信 JSAPI 下单,15 分钟有效)
pending --> paid : 支付回调 SUCCESS(验签 + 金额校验 → 发放权益 → 通知)
pending --> expired : 超时 15 分钟(后台每分钟扫描)
pending --> cancelled : 用户主动取消(同步关闭微信订单)
expired --> paid : 过期后付款到账(补发货)
cancelled --> refunded : 已取消但已付款 → 生成待审核退款单,管理员手动退款
paid --> refunded : 退款成功回调(回收权益 → 通知)
state "1 待支付" as pending
state "2 已支付" as paid
state "3 已取消" as cancelled
state "4 已过期" as expired
state "5 已退款" as refunded
```
**关键规则**
- **金额校验**:回调金额与订单 `pay_amount` 不一致 → 拒绝流转、不发货、转人工。
- **权益发放**:会员天数按快照实际发放,会员类型按**续费后最终到期时长**推导(剩余 ≥365 天为年度,否则月度,避免年度会员续月包被降级);上传 / AI / 衣柜分享 / 搭配分享额度改为写入**额度凭证** `quota_grants`(会员包 `expire_time=0` 跟随 VIP、加量包按 `goods.valid_days` 独立到期)。
- **幂等 + 补偿**:`benefit_grant_tasks` 唯一索引 + "发放中"状态防并发;失败自动落任务,**每分钟重试**,重试 3 次仍失败则标记失败并告警人工介入。
- **退款回收**:会员天数按快照回退(不回退到过去);赠送额度通过**失效凭证**回收未用部分,**已消耗部分不追回**。
---
### 6.6 额度规则全景
**扣减 / 回退 / 发放 / 回收全部走事务 + 幂等流水(唯一索引 `(order_no, quota_type, change_type)` 兜底),前端无需做补偿。**
| 场景 | 额度处理 | 前端提示 |
| --- | --- | --- |
| 上传衣物(建记录成功) | **扣 1 个上传额度** | — |
| 上传衣物但建记录失败 | **自动回退** | 重试即可,不会丢额度 |
| 衣物抠图/分类/保存失败 | **自动回退**(状态非 5 且未删除) | 提示识别失败,可重传 |
| 删除"处理中/成功"的衣物 | **回退 1 个上传额度** | 可提示"已返还 1 次上传额度" |
| 删除"处理失败"的衣物 | **不回退**(失败时已退过) | 不要提示返还 |
| 穿搭**首次**公开 | **扣 1 个搭配分享额度** | 公开前展示额度消耗确认 |
| 穿搭重复公开 | **不重复扣**(幂等) | — |
| **取消公开后再次公开** | **再扣 1 个额度** | ⚠️ 重点提示用户 |
| 穿搭取消公开 | **不返还** | — |
| 穿搭删除 | **不返还** | — |
| 分享「仅自己可见」的穿搭给好友 | **扣 1 个衣柜分享额度**(与衣柜分享共用池) | 额度不足时引导购买 |
| 分享「已公开」的穿搭给好友 | **不扣**(公开到社区时已扣社区分享额度) | — |
| 私密穿搭分享但分享记录落库失败 | **自动回退** | 重试即可 |
| AI 推荐生成(成功) | **扣 1 个 AI 额度** | 生成前展示剩余次数 |
| AI 推荐失败(衣橱为空/模型失败/无有效单品/落库失败) | **自动回退** | 可安全重试 |
| **AI 推荐成功但未保存** | **不返还** | ⚠️ 重点提示用户及时保存 |
| 创建衣柜分享 | **扣 1 个衣柜分享额度** | — |
| 创建衣柜分享但明细写入失败 | **自动回退** | 重试即可 |
| 取消衣柜分享 | **不返还** | 二次确认 |
| 新用户注册 | **发放**:免费额度 + 体验会员额度(`NewUserGift`,`source=4` 凭证,固定有效期=体验期结束) | 「欢迎礼包已到账」 |
| 固定有效期额度过期 | **自动失效**:到期即不再计入有效额度;**后续充值续费不会延长它**(跟随 VIP 的凭证则不受影响,续期自动延长) | 无感知,剩余额度同步下降 |
| 额度过期记账 | 后台任务**每小时**为已过期的固定有效期凭证补记 `change_type=7`(到期失效)流水,变更量为负数(幂等,重复执行不会重复记账) | 额度明细里能看到 `-N 到期失效`,账目闭环 |
| 购买商品支付成功 | **发放**(按订单快照) | 展示"已到账" |
| 退款成功 | **回收**(`total >= used`,已用部分不退) | 提示额度已回收 |
**额度不足时**:前端跳商品列表引导购买。注意 AI 额度不足返回的是 `90401`(不是 `80000`)。私密穿搭分享额度不足返回 `80000`,此时**不要**回退为普通穿搭详情链接(私密穿搭对他人不可见,对方打开会是无权限的空页),应提示用户补充衣柜分享额度。
---
### 6.7 通知触发规则
| 触发动作 | msg_type | 发送者 | 接收者 | 触发条件与防重 |
| --- | --- | --- | --- | --- |
| 点赞 | `1` | 点赞者 | 穿搭作者 | 仅"从取消/无记录 变为 已点赞"且**非本人**时触发;重复点赞不重复通知 |
| 支付成功 | `2` | 系统(sender=0) | 买家 | 权益发放成功后触发;取消/过期不发 |
| 退款成功 | `2` | 系统(sender=0) | 买家 | 权益回收后触发 |
| 系统通知 | `3` | 预留 | — | 常量已定义,当前代码无发送点 |
- 通知写入 MySQL + Redis 未读计数自增,best-effort(失败仅告警,不影响主业务)。
- **无推送能力,前端需轮询** `GET /notification/unread`。
---
## 7. 核心调用时序图
### 7.1 登录与 Token 刷新
```mermaid
sequenceDiagram
participant F as 小程序前端
participant B as 后端服务
participant W as 微信服务端
F->>B: POST /user/wxlogin {code}
B->>W: code2session(code)
W-->>B: openid / session_key
B->>B: 首次登录:注册用户 + 初始化 4 类免费额度 + 赠送体验会员及会员额度
B-->>F: {access_token, refresh_token, expire_time, user_info}
Note over F,B: 后续请求携带 Authorization: Bearer <access_token>
F->>B: 任意鉴权接口
B-->>F: code 11002 / HTTP 401(Token 已过期)
F->>B: POST /user/refresh-token {refresh_token, access_token}
B-->>F: 全新 {access_token, refresh_token, expire_time}
Note over F: 必须整体替换本地 token(旧的已立即失效)
F->>B: 携带新 token 重试原请求
```
### 7.2 衣物上传与 AI 处理轮询
```mermaid
sequenceDiagram
participant F as 小程序前端
participant B as 后端服务
participant M as 抠图服务(8001)
participant C as 分类服务(8000)
F->>B: POST /clothes/upload (multipart, 字段名 file)
B->>B: 校验扩展名/大小/magic bytes + 扣 1 个上传额度
B-->>F: {clothes_uuid, image_process_status: 3}
Note over B,C: 异步:3 排队 → 1 抠图 → 2 分类 → 4 成功 / 5 失败
B->>M: 抠图请求(失败重试 ≤3 次,间隔 1s)
B->>C: 15 维分类请求(失败重试 ≤3 次)
loop 前端轮询(建议 1~2 秒)
F->>B: GET /clothes/:uuid/status
B-->>F: {image_process_status, fail_reason}
end
alt 状态 = 4 成功
F->>B: GET /clothes/:uuid 读取识别结果
opt 识别不准
F->>B: PUT /clothes/:uuid {color: "...", ...}
end
else 状态 = 5 失败
Note over F: 展示 fail_reason,可重新上传(额度已自动回退)
end
```
### 7.3 AI 穿搭推荐与保存
```mermaid
sequenceDiagram
participant F as 小程序前端
participant B as 后端服务
participant R as Redis
participant L as 大模型网关(Tokenhub)
F->>B: POST /ai-recommend {scene_type, scene?}
B->>R: 抢占用户活跃位(已有任务则返回原 task_id,幂等)
B->>B: 扣 1 个 AI 额度(不足 → 90401)
B->>R: 写入任务快照(status=1 生成中)
B-->>F: {task_id, status: 1} · 秒级返回,不等大模型
par 后台生成(独立 ctx,前端退出也不中断)
B->>L: 结合星座运势 + 衣橱(状态 4)衣物生成
L-->>B: 推荐结果
B->>B: 校验结果(过滤模型幻觉衣物 ID)
B->>B: 落库 source=2, status=3 待用户保存
B->>R: 任务快照 status=2 + result
and 前端轮询
loop 每 2 秒,上限 90 次
F->>B: GET /ai-recommend/status?task_id=...
B->>R: 读任务快照
B-->>F: {task_id, status, result?}
end
end
alt 生成失败
B->>B: 回退 AI 额度 + 写失败流水
B->>R: 任务快照 status=3 + error_code/error_msg
end
F->>F: 用户预览、确认
F->>B: POST /ai-recommend/save {outfit_uuid}
B-->>F: 保存成功(status 3 → 1,不扣额度)
opt 公开到社区
F->>B: PUT /outfit/:uuid/public {is_public: 2}
B->>B: 自动审核通过 + 扣 1 个搭配分享额度
end
```
### 7.4 下单到支付完成
```mermaid
sequenceDiagram
participant F as 小程序前端
participant B as 后端服务
participant W as 微信支付
F->>B: GET /goods 选品
F->>B: POST /order {goods_code}
B->>B: 按售价计算金额 + 快照权益,创建订单(15 分钟有效)
B->>W: 微信 JSAPI 下单
B-->>F: {order_no, pay_amount, expire_time, pay_params}
F->>W: wx.requestPayment(pay_params)
W-->>B: POST /order/pay/notify(验签 + 解密)
B->>B: 金额校验 → 订单 1→2 → 发放权益 → 发站内通知
loop 前端轮询(1 秒起)
F->>B: GET /order
B-->>F: order_status
end
Note over F: 以服务端 order_status=2 为准,<br/>不要只信支付组件回调
opt 超时未支付
B->>B: 后台任务每分钟扫描,超 15 分钟置为 4 已过期
end
```
### 7.5 点赞 / 收藏切换
```mermaid
sequenceDiagram
participant F as 小程序前端
participant B as 后端服务
F->>B: GET /like/:targetUUID 获取初始状态
B-->>F: {is_liked, like_count}
F->>B: POST /like {target_uuid, action: 1}
B->>B: 状态 无/已取消 → 已点赞,且非本人 → 向作者发通知
B-->>F: 成功(幂等)
F->>B: POST /like {target_uuid, action: 2}
B-->>F: 成功(幂等,记录保留可再次点赞)
Note over F,B: 收藏同理:POST /collect {target_uuid, action},但收藏不触发通知
```
---
## 8. 错误码全表
> **前端统一以响应体 `code` 为准**(HTTP 状态码仅供参考)。全量 83 个错误码如下。
### 8.1 通用 / 鉴权 / 权限 / 系统(16)
| code | 含义 | HTTP | 前端建议 |
| --- | --- | --- | --- |
| 0 | 成功 | 200 | — |
| 10000 | 请求参数错误 | 200 | 展示 `details[0]` |
| 10001 | 资源不存在 | 200 | 通用兜底 |
| 10002 | 请求方法不允许 | 200 | 检查 Method |
| 10003 | 请求过于频繁 | **429** | 稍后重试(AI 推荐 180s 并发锁等) |
| 11000 | 未授权 | **401** | 跳登录 |
| 11001 | Token 鉴权失败 | **401** | 尝试刷新,失败则重新登录 |
| 11002 | Token 已过期 | **401** | 刷新 Token 后重试 |
| 11003 | Token 生成失败 | 200 | 重新登录 |
| 11004 | Token 缺失 | **401** | 跳登录 |
| 12000 | 无权限访问 | **403** | 提示无权操作 |
| 19000 | 服务内部错误 | **500** | "服务繁忙,请稍后重试" |
| 19001 | 数据库错误 | **500** | 同上 |
| 19002 | 缓存错误 | **500** | 同上 |
| 19003 | 下游服务调用失败 | **500** | 同上(AI 服务不可用) |
| 19999 | 未知错误 | **500** | 同上 |
### 8.2 用户(13)
| code | 含义 | HTTP |
| --- | --- | --- |
| 20000 | 用户参数错误 | 200 |
| 21000 | 用户名或密码错误 | 200 |
| **21001** | **账号已被禁用** | **200** ⚠️ 非 401,需单独处理 |
| 21002 | 微信登录失败 | 200 |
| 21003 | 微信 Code 无效 | 200 |
| 21004 | 注册暂未开放 | 200 |
| 22000 | 用户不存在 | 200 |
| 23000 | 用户已存在 | 200 |
| 23001 | 邮箱已被注册 | 200 |
| 29000 | 用户创建失败 | 200 |
| 29001 | 用户更新失败 | 200 |
| 29002 | 用户删除失败 | 200 |
| 29003 | 用户查询失败 | 200 |
### 8.3 上传(10)
| code | 含义 | HTTP |
| --- | --- | --- |
| 30000 | 上传文件为空 | 200 |
| 30001 | 不支持的文件类型 | 200 |
| 30002 | 文件大小超过限制 | 200 |
| 30004 | 文件不存在 | 200 |
| 30005 | 文件不是公开的 | 200 |
| 30006 | 权限不足 | 200 |
| 30007 | 文件处理中 | 200 |
| 39000 | 文件保存失败 | 200 |
| 39001 | 图片抠图处理失败 | **500** |
| 39002 | 图片分类处理失败 | **500** |
### 8.4 穿搭(5)
| code | 含义 | HTTP |
| --- | --- | --- |
| 40000 | 穿搭不存在 | 200 |
| 40001 | 创建穿搭失败 | 200 |
| 40002 | 删除穿搭失败 | 200 |
| 40003 | 更新穿搭失败 | 200 |
| 40004 | 无权操作该穿搭 | 200 |
### 8.5 衣物(6)
| code | 含义 | HTTP |
| --- | --- | --- |
| 50000 | 衣物不存在 | 200 |
| 50001 | 衣服已被搭配使用,无法删除 | 200 |
| 50002 | 创建衣物失败 | 200 |
| 50003 | 删除衣物失败 | 200 |
| 50004 | 更新衣物失败 | 200 |
| 50005 | 无权操作该衣物 | 200 |
### 8.6 商品(5)
| code | 含义 | HTTP |
| --- | --- | --- |
| 60000 | 商品不存在 | 200 |
| 60001 | 商品已下架 | 200 |
| 69000 | 创建商品失败 | 200 |
| 69001 | 更新商品失败 | 200 |
| 69002 | 删除商品失败 | 200 |
### 8.7 订单 / 退款(12)
| code | 含义 | HTTP |
| --- | --- | --- |
| 70000 | 订单不存在 | 200 |
| 70001 | 订单状态不正确 | 200 |
| 70002 | 订单已支付 | 200 |
| 70003 | 订单已取消 | 200 |
| 70004 | 订单已过期 | 200 |
| 70005 | 虚拟支付未开通 | 200 |
| 70006 | 商品尚未发布为虚拟支付道具 | 200 |
| 70007 | 虚拟支付订单不支持取消 | 200 |
| 70008 | iOS 虚拟支付最低 1 元 | 200 |
| 79000 | 创建订单失败 | 200 |
| 79001 | 取消订单失败 | 200 |
| 79002 | 更新订单失败 | 200 |
| 79003 | 创建虚拟支付订单失败 | 200 |
| 71000 | 退款记录不存在 | 200 |
| 71001 | 当前状态不允许退款 | 200 |
| 71002 | 退款金额不合法 | 200 |
| 71003 | 退款申请失败 | 200 |
| 71004 | iOS 订单请通过 App Store 申请退款 | 200 |
### 8.8 额度 / VIP(9)
| code | 含义 | HTTP |
| --- | --- | --- |
| **80000** | **额度不足** | 200 |
| 80001 | 额度记录不存在 | 200 |
| 80002 | 广告激励已领取 | 200 |
| 80003 | 无效的激励类型 | 200 |
| 80004 | 广告激励未开启 | 200 |
| 89000 | 额度更新失败 | 200 |
| 81000 | 会员记录不存在 | 200 |
| 81001 | 会员已过期 | 200 |
| 81002 | 会员开通失败 | 200 |
### 8.9 通知 / 分享 / 社区 / 星座 / AI(10)
| code | 含义 | HTTP |
| --- | --- | --- |
| 90000 | 通知不存在 | 200 |
| 90100 | 分享不存在 | 200 |
| 90101 | 分享已过期 | 200 |
| 90102 | 分享已取消 | 200 |
| 90103 | 穿搭分享不存在 | 200 |
| 90104 | 穿搭分享已取消 | 200 |
| 90200 | 目标内容不存在 | 200 |
| 90201 | 点赞操作失败 | 200 |
| 90202 | 收藏操作失败 | 200 |
| 90300 | 星座运势不存在(生成中,稍后重试) | 200 |
| 90400 | AI 推荐失败 | 200 |
| **90401** | **AI 推荐次数已达上限**(AI 额度不足) | 200 |
| 90402 | AI 推荐任务不存在或已过期(轮询用) | 200 |
---
## 9. 前端联调 FAQ 与检查清单
### 9.1 高频问题
**Q:接口返回 HTTP 200,但业务是失败的?**
A:正常。除 401/403/429/500 外,所有业务错误都返回 HTTP 200。**必须解析 `code`**。
**Q:`21001 账号已被禁用` 为什么不是 401?**
A:它在鉴权中间件最后一步(查用户状态)返回,HTTP 是 200。拦截器里需单独判断 `code === 21001` 并跳封禁页。
**Q:刷新 Token 一直报 `10000 缺少旧 access token`?**
A:`/user/refresh-token` **必须同时传 `refresh_token` 和 `access_token`**,只传前者会失败。
**Q:上传成功但衣物没有属性?**
A:上传是异步的。用返回的 `clothes_uuid` 轮询 `GET /clothes/:uuid/status`,直到 `4`(成功)再拉详情。
**Q:更新衣物颜色没生效?**
A:主颜色的**请求字段名是 `color`**(响应才是 `color_main`)。写成 `color_main` 会被忽略(因为只更新非空字段,且不报错)。
**Q:AI 推荐报 `10003 请求过于频繁`?**
A:同一用户 180 秒内只能有一个生成任务(Redis 锁)。前端应禁用按钮并提示"生成中",生成完成后再放开。
**Q:AI 推荐扣了额度但用户没保存,能退吗?**
A:不能。额度在**生成时**已消耗,只有生成失败才回退。前端应在生成前明确提示消耗。
**Q:支付组件回调 success,但订单还是待支付?**
A:以服务端为准。微信回调是异步的,**轮询 `GET /order` 直到 `order_status=2`**。
**Q:退款申请后一直没到账?**
A:退款是**人工审核模式**,接口只登记申请(`status=0`)。前端提示"退款审核中",并允许轮询订单状态到 `5`。
**Q:衣柜分享列表里显示"正常",但对方打不开?**
A:过期是**惰性**的。前端需同时判断 `status` 与 `expire_time`。
**Q:`GET /outfit` 里混进了 AI 推荐的穿搭?**
A:列表**不过滤 `status=3`(待用户保存)**。前端用 `source` 区分来源,AI 未保存的单独入口展示。
**Q:如何浏览"社区广场"的公开穿搭?**
A:调用 `GET /outfit/community` 分页获取其他用户已公开(公开 + 审核通过 + 正常)的穿搭列表,含作者信息与点赞/收藏计数;点击卡片进入 `GET /outfit/:uuid` 详情(他人视角)互动。
### 9.2 联调检查清单
- [ ] 统一封装请求:自动带 `Authorization: Bearer`,统一解析 `code`
- [ ] 实现 401 自动刷新 Token(同时传两个 token),失败则清登录态
- [ ] 单独处理 `code === 21001`(账号禁用,HTTP 200)
- [ ] 金额展示 `÷100`,时间展示 `×1000`
- [ ] 上传后轮询状态(1~2 秒,最多 60 次),展示处理中/失败态
- [ ] 支付后轮询订单状态,不依赖支付组件回调
- [ ] 退款提示"人工审核中"
- [ ] 公开穿搭前提示将消耗 1 次搭配分享额度
- [ ] AI 推荐前提示将消耗 1 次 AI 额度,且**不保存不退**
- [ ] 点赞/收藏按钮做乐观更新 + 失败回滚(接口幂等,可重试)
- [ ] 通知红点通过轮询 `GET /notification/unread` 刷新(无推送)
- [ ] 不可逆操作(删除衣物/穿搭、取消订单/分享)加二次确认
- [ ] 列表分页 `page_size` 不超过 50
- [ ] 排障时请求头自带 `X-Request-ID`(跨域读不到响应头)
---
## 10. 接口调用示例(curl)
> 示例中的 token、UUID、订单号均为示意值;`BASE` = `http://<host>/api/v1`。
### 微信登录
```bash
curl -X POST $BASE/user/wxlogin \
-H "Content-Type: application/json" \
-d '{"code": "0b3abcxxxxxxxx"}'
```
```json
{
"code": 0,
"msg": "成功",
"data": {
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"refresh_token": "eyJhbGciOiJIUzI1NiIs...",
"expire_time": 1756742400,
"user_info": {
"username": "微信用户",
"avatar": "",
"sex": 0,
"birth_day": 0,
"phone": "",
"is_new_user": true,
"status": 1,
"ban_reason": "",
"ban_start": 0,
"ban_expire": 0,
"vip_type": 4,
"vip_expire": 1744761600
}
}
}
```
### 刷新 Token
```bash
curl -X POST $BASE/user/refresh-token \
-H "Content-Type: application/json" \
-d '{"refresh_token":"<refresh_token>","access_token":"<旧 access_token>"}'
```
### 上传衣物(multipart)
```bash
curl -X POST $BASE/clothes/upload \
-H "Authorization: Bearer <access_token>" \
-F "file=@/path/to/t-shirt.png"
```
```json
{
"code": 0,
"msg": "成功",
"data": {
"clothes_uuid": "c8f2e1a0-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"image_process_status": 3
}
}
```
### 轮询处理状态
```bash
curl "$BASE/clothes/c8f2e1a0-xxxx-xxxx-xxxx-xxxxxxxxxxxx/status" \
-H "Authorization: Bearer <access_token>"
```
```json
{ "code": 0, "msg": "成功", "data": { "image_process_status": 4, "fail_reason": "" } }
```
### 修正衣物属性(注意 `color`)
```bash
curl -X PUT $BASE/clothes/c8f2e1a0-xxxx-xxxx-xxxx-xxxxxxxxxxxx \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{"color":"白色","category_main":"上装","name":"我的白T"}'
```
### 额度概览
```bash
curl $BASE/quota/info -H "Authorization: Bearer <access_token>"
```
```json
{
"code": 0,
"msg": "成功",
"data": {
"quotas": [
{ "quota_type": 1, "total": 20, "used": 1, "remain": 19 },
{ "quota_type": 2, "total": 3, "used": 0, "remain": 3 },
{ "quota_type": 3, "total": 10, "used": 0, "remain": 10 },
{ "quota_type": 4, "total": 5, "used": 0, "remain": 5 }
]
}
}
```
### AI 穿搭推荐与保存
```bash
curl -X POST $BASE/ai-recommend \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{"scene_type": 2, "scene": "参加朋友的婚礼"}'
```
```json
{
"code": 0,
"msg": "成功",
"data": {
"outfit_uuid": "of-xxxxxxxxxxxx",
"outfit_name": "婚礼宾客穿搭",
"outfit_desc": "简约大方,正式又不失亲和力",
"scene": "参加朋友的婚礼",
"source": 2,
"status": 3,
"outfit_item": [{ "clothes_uuid": "c8f2e1a0-xxxx-xxxx-xxxx-xxxxxxxxxxxx" }]
}
}
```
```bash
curl -X POST $BASE/ai-recommend/save \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{"outfit_uuid":"of-xxxxxxxxxxxx"}'
```
### 创建订单(微信支付)
```bash
curl -X POST $BASE/order \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{"goods_code": "AI_3"}'
```
```json
{
"code": 0,
"msg": "成功",
"data": {
"order_no": "OD20260907000123",
"pay_amount": 990,
"expire_time": 1756743300,
"pay_params": {
"timeStamp": "1756742400",
"nonceStr": "1a2b3c4d5e6f",
"package": "prepay_id=wx09153000123456",
"signType": "RSA",
"paySign": "xxxxxxxxxxxxxxxx"
}
}
}
```
小程序侧:
```javascript
wx.requestPayment({
...pay_params, // timeStamp / nonceStr / package / signType / paySign 原样透传
success: () => { /* 仅表示拉起成功,仍需轮询 GET /order 确认 order_status=2 */ },
fail: () => { /* 用户取消或失败 */ }
})
```
### 点赞 / 取消点赞
```bash
curl -X POST $BASE/like \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{"target_uuid":"of-xxxxxxxxxxxx","action":1}'
curl "$BASE/like/of-xxxxxxxxxxxx" -H "Authorization: Bearer <access_token>"
# {"code":0,"msg":"成功","data":{"is_liked":true,"like_count":12}}
```
### 创建衣柜分享
```bash
curl -X POST $BASE/closet-share \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{"share_type":1,"clothes_uuids":["c8f2e1a0-xxxx"],"expire_time":1756828800}'
```
### 错误响应示例(额度不足)
```json
{ "code": 80000, "msg": "额度不足" }
```
### 错误响应示例(参数校验)
```json
{
"code": 10000,
"msg": "请求参数错误",
"details": ["Key: 'OutfitCreateRequest.OutfitName' Error:Field validation for 'OutfitName' failed on the 'required' tag"]
}
```
---
> **文档维护说明**:本文档由后端源码(`internal/routers/router.go`、`internal/dto/`、`internal/service/`、`pkg/errcode/`)逐字段核对生成。接口变更时请同步更新本文件;工程视角的部署/配置说明见 `README.md`。