2613 lines
109 KiB
Markdown
2613 lines
109 KiB
Markdown
# 智能衣橱(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`。
|
||
|