# 智能衣橱(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 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 ``` - 必须恰好是 `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:非属主通过穿搭分享只读查看该分享穿搭内的衣物时必传 | **响应** `data` = [衣物对象](#42-衣物对象-clothesitem) **业务逻辑(可见性)** - 属主:始终可见。 - 非属主:**仅当携带有效 `share_uuid`**(该分享正常、穿搭未被删除 / 封禁、且该衣物属于该分享的穿搭明细)时返回**只读**衣物信息;否则 `50005`。 **错误码**:`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["微信登录
新用户自动注册"] --> B["初始化 4 类免费额度
默认 上传 20 / AI 3 / 搭配分享 10 / 衣柜分享 5
(可配置,允许为 0)"] B --> B2["赠送体验会员 + 会员额度
默认 3 天;AI 3 / 分享 3 / 衣柜 3
(体验会员凭证 source=4,固定到期)"] B2 --> C["上传衣物
校验 → 扣上传额度 → 异步 AI 抠图 + 分类"] C --> D["衣橱管理
查看 / 修正属性 / 删除回退额度"] D --> E["创建穿搭"] D --> F["AI 穿搭推荐
扣 AI 额度 → 大模型生成 → 待用户保存"] D --> G["衣柜分享
部分衣物 / 全量快照"] E --> H["公开到社区
自动审核通过 → 扣搭配分享额度"] H --> I["社区互动
点赞 → 触发站内通知"] 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 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 为准,
不要只信支付组件回调 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:///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":"","access_token":"<旧 access_token>"}' ``` ### 上传衣物(multipart) ```bash curl -X POST $BASE/clothes/upload \ -H "Authorization: Bearer " \ -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 " ``` ```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 " \ -H "Content-Type: application/json" \ -d '{"color":"白色","category_main":"上装","name":"我的白T"}' ``` ### 额度概览 ```bash curl $BASE/quota/info -H "Authorization: Bearer " ``` ```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 " \ -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 " \ -H "Content-Type: application/json" \ -d '{"outfit_uuid":"of-xxxxxxxxxxxx"}' ``` ### 创建订单(微信支付) ```bash curl -X POST $BASE/order \ -H "Authorization: Bearer " \ -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 " \ -H "Content-Type: application/json" \ -d '{"target_uuid":"of-xxxxxxxxxxxx","action":1}' curl "$BASE/like/of-xxxxxxxxxxxx" -H "Authorization: Bearer " # {"code":0,"msg":"成功","data":{"is_liked":true,"like_count":12}} ``` ### 创建衣柜分享 ```bash curl -X POST $BASE/closet-share \ -H "Authorization: Bearer " \ -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`。