116 KiB
智能衣橱(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. 五分钟上手
- 2. 通用调用约定
- 3. 接口总表(48 个)
- 4. 公共数据结构
- 5. 接口详解
- 6. 业务逻辑与状态机
- 7. 核心调用时序图
- 8. 错误码全表
- 9. 前端联调 FAQ 与检查清单
- 10. 接口调用示例(curl)
1. 五分钟上手
标准联调路径:
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 节):
- 判断成败只看 body 里的
code,绝大多数业务错误返回 HTTP 200(包括21001 账号已被禁用)。 - 刷新 Token 必须同时传
refresh_token和access_token,否则报10000;成功后旧的两个 token 立即失效,必须整体替换。 - 衣物上传是异步的,返回只代表"已排队",必须轮询状态接口。
- 更新衣物的主颜色请求字段是
color,响应里才是color_main。 - 金额单位都是分、时间都是秒,展示前要换算。
- AI 推荐是异步的:
POST /ai-recommend只返回task_id,必须轮询GET /ai-recommend/status才能拿到结果;生成后不保存不退额度。退款是人工审核,不是实时到账。
2. 通用调用约定
2.1 鉴权与 Token
请求头格式
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。拦截器里必须单独判断并跳转封禁提示页。
刷新流程(前端必须实现)
任意接口返回 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)
{
"code": 0,
"msg": "成功",
"data": { }
}
code恒为0,msg恒为"成功"。data永不为null:无业务数据(删除、取消、标记已读等)时是空对象{}。- 成功响应没有
details字段。
失败
{
"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再交给 JSDate(因为 JS 用毫秒)。
2.6 幂等与重试
以下接口天然幂等,可安全重试(重复提交不会产生重复数据或重复扣费):
- 点赞 / 取消点赞
POST /like - 收藏 / 取消收藏
POST /collect - 穿搭公开
PUT /outfit/:uuid/public(重复公开不会重复扣额度;但取消公开后再次公开会再扣 1 次,见 6.6) - 标记已读
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 已退款(申请后为 6 申请退款中,继续轮询;回到 2 表示退款失败/被驳回) |
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可用于排障,但未加入 CORSExpose-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 |
查询目标点赞状态与数量 |
通知(5)
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /notification |
通知列表 |
| GET | /notification/unread |
未读通知数 |
| GET | /notification/:uuid |
通知详情 |
| POST | /notification/:uuid/read |
标记单条已读 |
| POST | /notification/read-all |
全部标记已读(前端进入通知页时调用) |
订单(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=已退款 6=申请退款中 |
| 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 — 微信登录(公开)
业务逻辑
-
前端
wx.login()获取code,传给后端;后端用code2session换openid。 -
首次登录自动注册:昵称默认"微信用户",
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)。 - 免费额度(
-
并发首次登录不会注册重复账号(依赖
wx_open_id唯一索引)。 -
账号被禁用/注销(
status != 1)时返回21001,HTTP 仍是 200。 -
额度初始化失败不影响登录(仅后端告警)。
请求(json)
| 参数 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|
| code | string | ✅ | required | wx.login 获取的临时登录凭证,一次性 |
响应 data
| 字段 | 类型 | 说明 |
|---|---|---|
| access_token | string | 访问令牌,放入 Authorization: Bearer |
| refresh_token | string | 刷新令牌,用于续期 |
| expire_time | int64 | access_token 过期时间戳(秒) |
| user_info | object | 用户对象 |
错误码: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 = 用户对象。
新注册用户
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 天体验期后再购买会员,体验额度照常失效,只剩免费额度 + 新购会员包额度。 ⚠️ 会员包来源(source=1)的额度不叠加:月度包与年度包同时持有时,同类额度只取 amount 最大的一份计入(月/年额度相同时即等价于只按一份计算),避免额度翻倍;该份用尽后自动顺延到下一份有效会员包凭证,已购额度不浪费。加量包(source=2)、广告激励(source=3)、体验额度(source=4)仍按原逻辑叠加求和。 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 | 否 | 见分页约定 | 分页 |
| quota_type | int | 否 | oneof=1 2 3 4 |
额度类型筛选;不传 = 全部 |
响应 data.list[] = 额度流水对象
错误码: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 — 上传衣物图片(鉴权)
业务逻辑
- 同步校验:文件非空 → 扩展名白名单(默认
jpg/jpeg/png)→ 大小 ≤ 20MB(配置Upload.MaxSizeMB)→ magic bytes 文件头真图校验(防伪造扩展名)。 - 同步扣 1 个上传额度(
quota_type=1),额度不足直接80000,不会创建记录。 - 创建衣物记录,
image_process_status = 3(排队中),立即返回。 - 后端异步流水线(与请求无关,最多并发 10,单任务超时 3 分钟):
3 排队中→1 抠图中(BiRefNet 抠图,失败重试 3 次,间隔 1s)→ 上传 COS →2 分类中(Fashion-CLIP 识别 15 个属性,失败重试 3 次)→4 成功(回填属性 + 抠图 URL)。 - 任一环节失败 → 置
5 失败并写fail_reason("图片抠图失败"/"图片保存失败"/"图片上传失败"/"图片分类失败"/"内部错误"),并自动回退 1 个上传额度。 - 分类失败但抠图成功时仍会保留抠好的图(
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(衣物对象 数组)
列表包含处理中的衣物(状态 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 = 衣物对象
业务逻辑(可见性)
- 属主:始终可见。
- 非属主:两个只读入口,二选一携带即可,都不携带则
50005。share_uuid:该分享正常、穿搭未被删除 / 封禁、且该衣物属于该分享的穿搭明细 → 返回只读衣物信息。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 节枚举表 |
| 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 = 穿搭对象
错误码: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[] = 穿搭对象
⚠️ 该接口不会过滤
status=3(AI 待保存)的穿搭,但响应中已带上status字段。前端应据此把 AI 未保存的穿搭归到「AI 推荐」入口,不要混入「我的搭配」。这类穿搭超过 7 天未保存会被后端自动清理(软删除),详见 穿搭对象。
错误码: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 = 穿搭对象
错误码: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 穿搭推荐任务(鉴权)
业务逻辑
- 提交幂等:同一用户已有任务在生成中时,直接返回该任务的
task_id(不会重复扣额度),前端继续轮询原任务即可。 - 提交时先扣 1 个 AI 额度(
quota_type=2);额度不足返回90401AI 推荐次数已达上限(注意不是80000)。 - 接口秒级返回(实测 ~30ms),不等大模型。
- 生成在后台执行,输入为:当前用户**处理成功(状态 4)**的全部衣物 + 生日推算的星座 + 今日运势(星座/运势缺失会降级,不影响生成)。
- 搭配完整性保障(按衣橱实际存在的品类判定):结果必须包含「上装」或「连衣裙」二者之一(外套是外层,不可孤立出现);未选「连衣裙」且衣橱有「下装」时必须含「下装」;衣橱有「鞋」时必须含「鞋」;「配饰」为可选;「连衣裙」与「下装」互斥。不满足时后端会带纠偏提示自动重试一次模型,仍不满足则由服务端从衣橱确定性补齐缺失品类(补齐单品的
reason会标注"系统按搭配完整性规则补齐")。因此正常结果不会出现"只有外套+下装"这类穿不出去的搭配;同一品类最多 1 件。 - 失败时额度自动回退,失败原因写入任务状态(
error_code/error_msg),详见下方状态接口:90400+衣橱暂无可用衣物90400+大模型调用失败90400+未生成有效搭配(两次模型调用均未返回衣橱内的有效单品)90400+生成任务异常终止(超过 3 分钟未完成)
- ⚠️ 生成成功但未调用 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 — 创建衣柜分享(鉴权)
业务逻辑
expire_time:0表示永不过期;非 0 时必须晚于当前时间(否则10000+details:["过期时间必须晚于当前时间"])。share_type=1:必须传非空clothes_uuids(否则10000+details:["部分分享需选择衣物"]),且衣物必须全部属于本人。share_type=2:自动快照当前全部衣物(后端分页遍历,无需传列表)。- 先扣 1 个衣柜分享额度(
quota_type=4),不足 →80000。 - 创建分享(
status=1正常)→ 写入明细;明细写入失败会回退额度并返回19001(避免产生空分享)。 - 创建即自动审核通过。
请求(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 | 衣物对象 数组 |
| 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 — 创建穿搭分享(鉴权)
业务逻辑
- 仅能分享本人的穿搭(他人 →
40004)。 - 无需先公开到社区:仅自己可见的穿搭同样可分享(分享与公开解耦);穿搭已被删除 →
40000,已被封禁 →40004。 - 额度:
is_public=1(仅自己可见)→ 扣 1 次衣柜分享额度(quota_type=4,与衣柜分享共用池),额度不足 →80000且不创建分享;is_public=2(已公开)→ 不扣。扣额成功后分享落库失败会自动回退 1 次额度。 - 创建成功后返回
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 | 穿搭对象,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[] = 穿搭对象
错误码: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[] = 通知对象
错误码: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 = 通知对象
错误码:10000 参数错误 · 90000 通知不存在 · 12000 无权限访问
POST /notification/:uuid/read — 标记已读(鉴权)
业务逻辑
- 条件更新(仅"未读→已读"),防并发重复扣减未读数;已读时直接返回当前未读数,幂等。
- ⚠️ 本接口只影响该条;需要全部已读请用
POST /notification/read-all(见下节),无需逐条调用。
请求(path)
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| uuid | string | ✅ | 通知 UUID |
响应 data = {}
错误码:10000 参数错误 · 90000 通知不存在 · 19001 数据库错误
POST /notification/read-all — 全部标记已读(鉴权)
业务逻辑
- 将当前用户全部未读通知一次性置为已读(
is_read1 → 2);条件更新(仅未读→已读),幂等,重复调用不影响结果。 - ⚠️ 无请求体、无 query 参数,作用于当前登录用户的全部通知,不受列表页
msg_type/is_read筛选影响。 - 以 MySQL 为权威重新统计未读数并回填 Redis 缓存后返回,前端可直接用返回值刷新红点,无需再调
GET /notification/unread。 - 前端约定:
pages/notification在列表渲染完成后调用(先拉列表保留本次进入的未读标记,再清零服务端状态与红点),实现「点开通知页即全部已读」。
响应 data
| 字段 | 类型 | 说明 |
|---|---|---|
| unread_count | int64 | 标记后的未读通知数(正常为 0) |
错误码:19001 数据库错误 · 19002 缓存错误
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[] = 商品对象(仅上架商品)
错误码:10000 参数错误 · 19001 数据库错误
GET /goods/:code — 按编码查询商品(公开)
请求(path)
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| code | string | ✅ | 商品编码 goods_code(不是数字 ID) |
响应 data = 商品对象
错误码:10000 参数错误 · 60000 商品不存在 · 60001 商品已下架
5.12 订单与退款模块
order_status 枚举:1=待支付 2=已支付 3=已取消 4=已过期 5=已退款 6=申请退款中(已受理退款申请,待运营审核并手动发起退款)
退款 status 枚举:0=待处理 1=退款中 2=退款成功 3=退款失败
POST /order — 创建订单并发起微信支付(鉴权)
业务逻辑
- 只用
goods_code下单,金额由服务端按商品售价discount_price计算,前端不要传金额。 - 商品必须存在且上架(否则
60000/60001)。 - 用户必须有
wx_open_id(即必须通过微信登录),否则79000+details:["缺少微信 openid"]。 - 支付有效期 15 分钟,
expire_time用于倒计时。 - 下单时把商品权益(会员天数 / 各类额度)快照进订单,后续商品改价不影响已下单权益。
- 先落本地待支付订单,再调微信 JSAPI。微信下单失败返回
79000,但本地订单保留为待支付,15 分钟后自动过期。 - 同一用户可并存多个待支付订单(无防重限制),前端应避免重复点击。
请求(json)
| 参数 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|
| goods_code | string | ✅ | required | 商品编码(唯一请求字段) |
响应 data
| 字段 | 类型 | 说明 |
|---|---|---|
| order_no | string | 订单号(后续取消/退款/查询用) |
| pay_amount | int64 | 应付金额(分) |
| expire_time | int64 | 支付截止时间戳(秒),倒计时用(15 分钟) |
| pay_params | object | 微信支付参数,见此,整体透传给 wx.requestPayment |
错误码:10000 参数错误 · 60000 商品不存在 · 60001 商品已下架 · 79000 创建订单失败(含 缺少微信 openid)· 19001 数据库错误
POST /order/virtual — 创建虚拟支付订单(鉴权)
小程序虚拟商品(会员 / 额度包)必须走虚拟支付(微信 2026-04-01 起全终端强制)。本接口本地落单(pay_channel=2)后返回前端调起 wx.requestVirtualPayment 所需的签名参数,不调用微信 JSAPI 下单。
业务逻辑
- 服务端校验虚拟支付已开通、商品上架且已发布为虚拟支付道具(
product_id非空、product_status=2)。 - 下单时把商品权益快照进订单;
outTradeNo复用本地订单号。 - 服务端用
offerId+ 现网appKey生成signData/paySig,用登录缓存的session_key生成signature,原样返回前端。 - 支付结果不由前端回调决定:前端
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 | 用户态签名(服务端生成) |
前端调用契约(示例)
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 6 |
订单状态筛选(6=申请退款中);不传 = 全部 |
响应 data.list[] = 订单对象
错误码: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已支付订单可申请,其它状态(含申请中的6)→71001。 - ✅ 受理成功后订单立即置
6申请退款中:订单状态流转与退款流水(status=0待处理)在同一事务内完成;重复/并发申请只有一次受理成功,其余返回71001。前端刷新订单列表即可看到"退款审核中"。 - ⚠️ 系统不会自动调微信退款:接口只是登记退款申请(流水
status=0待处理),需运营人工审核并手动发起退款——普通支付订单在微信支付商户后台退款,虚拟支付订单(pay_channel=2)请在虚拟支付后台(Midas)发起退款,退款结果由推送回调同步。 - 退款结果由回调驱动(
pay_channel=1走微信支付 V3 退款通知;pay_channel=2走虚拟支付退款推送xpay_refund_notify,以RetCode=0判定成功):成功 → 订单5已退款(回收权益 + 发退款通知);失败 → 退款流水置3失败、订单回退2已支付,用户可重新申请。 - 前端应提示"退款审核中"并轮询订单状态:
6继续轮询、5为终态、回到2表示退款失败/被驳回(可再次申请);不要承诺实时到账。 - 同一订单已有退款记录且状态不是
3(失败)→71001;上次失败可重新申请(复用记录、换新退款单号)。 - 退款成功后,会员天数与已发放额度会被回收(已消耗部分不回收),并发退款通知。
- 🛟 兜底:虚拟支付退款结果除推送外还有查单兜底任务(每 60 秒扫描停留超 10 分钟的「申请退款中」订单反查
query_order)——微信侧已退款会自动订正为5并回收权益;微信侧仍未退款则保持6,超过 24 小时输出告警交人工。推送丢失也不会导致订单永久卡单。 - ⚠️ 运营线下拒绝(未发起微信退款)不会产生回调,订单会停留在
6申请退款中:兜底任务会持续核对并对超期订单告警,需运营在后台发起退款,或由研发人工订正。
请求(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 秒)→ 金额校验(与订单金额不一致则拒绝流转、人工介入)→ 流转订单状态 → 发放/回收权益 → 发站内通知。
- 竞态兜底:已过期但已付款 → 补发货;已取消但已付款 → 生成待审核退款单(订单保持
3已取消,其权益从未发放),由管理员手动退款。
POST /xpay/notify — 虚拟支付消息推送(公开)
🚫 这是微信服务端推送地址,前端绝对不要调用。
- 事件类型(
Event字段):xpay_goods_deliver_notify道具发货、xpay_refund_notify退款完成、xpay_subscribe_ios_refund_query_notifyiOS 退款问询(需 3 秒内应答);代币支付/投诉/风控等其余事件仅记录不处理。 - 应答按虚拟支付规范:
{"ErrCode":0,"ErrMsg":"success"}表示处理完成;{"ErrCode":1}表示处理失败,微信会按 2/4/8/16… 秒重试,最多 15 次。服务端对「关键字段缺失 / 订单查不到 / 退款结果无法判定」一律应答失败以触发重试,不再静默应答成功。 - 退款推送
xpay_refund_notify的官方字段(易错点,勿按普通支付回调的字段理解):
| 字段 | 说明 |
|---|---|
| MchOrderId | 原支付单商户单号(= 本地 order_no);该推送没有 OutTradeNo |
| MchRefundId | 商户退款单号 |
| WxRefundId | 微信退款单号 |
| RefundFee | 退款金额(分) |
| RetCode | 退款结果:0=成功,非 0=失败;该推送没有 RefundStatus |
| RetMsg | 退款失败原因 |
- 处理链路:定位订单 → 幂等锁 →
RetCode=0时反查query_order确认真实退款且金额一致(推送无签名,防伪造回收权益)→ 订单6 → 5+ 回收权益 + 发通知 + 退款流水回写微信退款单号与原始报文;RetCode≠0时订单回退2 已支付+ 退款流水置失败(原因取RetMsg)。
6. 业务逻辑与状态机
6.1 七条业务主线(总览)
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
- 注册与额度:微信登录首次自动注册,同步初始化 4 类免费额度(内置默认 上传 20 / AI 3 / 搭配分享 10 / 衣柜分享 5,可由
UserDefaultQuota配置,允许为 0)。 - 衣物生命周期:上传(校验 + 扣额)→ 异步 AI 处理(抠图 → 分类)→ 成功入库可被选用 → 删除软删并回退额度。
- 穿搭与社区:创建穿搭 → 公开(自动审核通过 + 扣额度)→ 他人点赞触发通知。
- 衣柜分享:部分衣物或全量快照 → 扣额度 → 属主/访客查看 → 取消或惰性过期。
- 交易与权益:选购商品 → 下单(15 分钟有效)→ 微信支付回调发放权益 → 超时过期 / 退款回收。
- AI 推荐:扣 AI 额度 → 结合星座运势与衣橱生成 → 落库"待用户保存" → 保存转正式穿搭。
- 通知:点赞、支付成功、退款成功触发站内通知,未读计数实时维护(需前端轮询)。
6.2 衣物 AI 处理状态机
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 穿搭状态机(公开 / 审核 / 保存)
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 衣柜分享状态机
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 订单状态机
stateDiagram-v2
[*] --> pending : 创建订单(快照权益 + 微信 JSAPI 下单,15 分钟有效)
pending --> paid : 支付回调 SUCCESS(验签 + 金额校验 → 发放权益 → 通知)
pending --> expired : 超时 15 分钟(后台每分钟扫描)
pending --> cancelled : 用户主动取消(同步关闭微信订单)
expired --> paid : 过期后付款到账(补发货)
cancelled --> refunded : 已取消但已付款 → 生成待审核退款单,管理员手动退款
paid --> refunding : 用户申请退款(POST /order/refund,同事务写退款流水)
refunding --> refunded : 退款成功回调(回收权益 → 通知)
refunding --> paid : 退款失败/被驳回(流水置失败,可重新申请)
paid --> refunded : 退款成功回调(回收权益 → 通知)
state "1 待支付" as pending
state "2 已支付" as paid
state "3 已取消" as cancelled
state "4 已过期" as expired
state "5 已退款" as refunded
state "6 申请退款中" as refunding
关键规则
- 金额校验:回调金额与订单
pay_amount不一致 → 拒绝流转、不发货、转人工。 - 权益发放:会员天数按快照实际发放,会员类型按续费后最终到期时长推导(剩余 ≥365 天为年度,否则月度,避免年度会员续月包被降级);上传 / AI / 衣柜分享 / 搭配分享额度改为写入额度凭证
quota_grants(会员包expire_time=0跟随 VIP、加量包按goods.valid_days独立到期)。会员包来源额度不叠加:月度包/年度包同时持有时同类额度只取 amount 最大的一份,该份用尽后自动顺延下一份。 - 幂等 + 补偿:
benefit_grant_tasks唯一索引 + "发放中"状态防并发;失败自动落任务,每分钟重试,重试 3 次仍失败则标记失败并告警人工介入。 - 退款回收:会员天数按快照回退(不回退到过去);赠送额度通过失效凭证回收未用部分,已消耗部分不追回。
- 退款状态流转:
POST /order/refund受理后订单2 → 6(申请退款中,同一事务写入退款流水,行锁防重复受理);退款成功回调6 → 5并回收权益;微信退款失败回调6 → 2且流水置失败,用户可重新申请。运营线下直接拒绝不会产生回调,订单会停留在6,需人工介入。
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 刷新
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 处理轮询
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 穿搭推荐与保存
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 下单到支付完成
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 点赞 / 收藏切换
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)。申请成功后订单为 6 申请退款中,前端提示"退款审核中"并轮询订单状态:5 已退款为终态;若回到 2 已支付,说明退款失败/被驳回,可引导用户重新申请。
Q:虚拟支付退款推送丢了 / 解析失败,会不会一直卡在「申请退款中」?
A:不会。退款推送处理对无法判定的报文会应答失败触发微信重试(间隔 2/4/8/16… 最多 15 次);同时后台查单兜底任务每 60 秒反查停留超 10 分钟的「申请退款中」订单——微信侧已退款会自动订正为 5 已退款 并回收权益;若微信侧确实未退款则保持 6,超过 24 小时输出告警交人工介入。
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 次),展示处理中/失败态
- 支付后轮询订单状态,不依赖支付组件回调
- 退款提示"人工审核中"(申请后订单状态
6,轮询到5为终态,回到2表示失败可重申请) - 公开穿搭前提示将消耗 1 次搭配分享额度
- AI 推荐前提示将消耗 1 次 AI 额度,且不保存不退
- 点赞/收藏按钮做乐观更新 + 失败回滚(接口幂等,可重试)
- 通知红点通过轮询
GET /notification/unread刷新(无推送) - 不可逆操作(删除衣物/穿搭、取消订单/分享)加二次确认
- 列表分页
page_size不超过 50 - 排障时请求头自带
X-Request-ID(跨域读不到响应头)
10. 接口调用示例(curl)
示例中的 token、UUID、订单号均为示意值;
BASE=http://<host>/api/v1。
微信登录
curl -X POST $BASE/user/wxlogin \
-H "Content-Type: application/json" \
-d '{"code": "0b3abcxxxxxxxx"}'
{
"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
curl -X POST $BASE/user/refresh-token \
-H "Content-Type: application/json" \
-d '{"refresh_token":"<refresh_token>","access_token":"<旧 access_token>"}'
上传衣物(multipart)
curl -X POST $BASE/clothes/upload \
-H "Authorization: Bearer <access_token>" \
-F "file=@/path/to/t-shirt.png"
{
"code": 0,
"msg": "成功",
"data": {
"clothes_uuid": "c8f2e1a0-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"image_process_status": 3
}
}
轮询处理状态
curl "$BASE/clothes/c8f2e1a0-xxxx-xxxx-xxxx-xxxxxxxxxxxx/status" \
-H "Authorization: Bearer <access_token>"
{ "code": 0, "msg": "成功", "data": { "image_process_status": 4, "fail_reason": "" } }
修正衣物属性(注意 color)
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"}'
额度概览
curl $BASE/quota/info -H "Authorization: Bearer <access_token>"
{
"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 穿搭推荐与保存
curl -X POST $BASE/ai-recommend \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{"scene_type": 2, "scene": "参加朋友的婚礼"}'
{
"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" }]
}
}
curl -X POST $BASE/ai-recommend/save \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{"outfit_uuid":"of-xxxxxxxxxxxx"}'
创建订单(微信支付)
curl -X POST $BASE/order \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{"goods_code": "AI_3"}'
{
"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"
}
}
}
小程序侧:
wx.requestPayment({
...pay_params, // timeStamp / nonceStr / package / signType / paySign 原样透传
success: () => { /* 仅表示拉起成功,仍需轮询 GET /order 确认 order_status=2 */ },
fail: () => { /* 用户取消或失败 */ }
})
点赞 / 取消点赞
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}}
创建衣柜分享
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}'
错误响应示例(额度不足)
{ "code": 80000, "msg": "额度不足" }
错误响应示例(参数校验)
{
"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。