Files
zt-closet-backend/README.md
T
2026-09-15 16:02:45 +08:00

1572 lines
70 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# zt-closet-backend
智能衣橱微信小程序后端服务。基于 Go + Gin 构建,提供衣物管理、AI 衣物识别(分类/抠图)、穿搭与 AI 推荐、每日星座运势、社区互动(分享/收藏/点赞/通知)、额度体系、商品与微信支付订单等完整能力,并内置 OpenTelemetry 全链路可观测性与结构化日志。
## 核心功能
- **用户体系**:微信小程序登录(code2session,首次自动注册并初始化免费额度)、JWT 双 token(access/refresh)、个人资料维护。
- **衣物管理**:图片上传(扩展名 + magic bytes 双重校验)→ 异步 AI 流水线(抠图 → 15 维分类)→ 衣物 CRUD 与处理状态查询。
- **穿搭与 AI 推荐**:自建穿搭(含社区公开与审核)、基于星座运势 + 衣橱衣物的 AI 穿搭推荐(大模型生成,落库为"待用户保存")。
- **运势**:每日 0 点定时生成十二星座运势,Redis 分布式锁保证多实例下仅执行一次,查询时支持按需兜底生成。
- **社区互动**:衣柜分享(部分衣物 / 全量快照)、穿搭点赞与收藏(Redis 集合 + MySQL 双写)、站内通知(未读计数)。
- **额度体系**:四类额度(上传 / AI 推荐 / 穿搭分享 / 衣柜分享)的扣减、回退、发放、回收;上传为**永久额度**,AI 推荐 / 穿搭分享 / 衣柜分享为**每日额度**(每天 0 点重置);购买赠送额度以**凭证**(`quota_grants`)发放——会员包随 VIP 有效期、加量包独立有效期(`goods.valid_days`)、广告激励当天有效;Cos 消费先扣即将过期凭证再扣免费额度,全部走事务 + 幂等流水。
- **商品与订单**:商品(会员包 / 上传包 / AI 推荐包 / 分享包)、微信支付 V3 下单、支付与退款回调(验签 + 解密)、订单 15 分钟超时自动过期、权益发放失败自动补偿。
- **可观测性**:OpenTelemetry Trace / Metric / Log 三信号 OTLP gRPC 上报;结构化 JSON 日志注入 `trace_id` / `span_id`。
## 技术栈
| 分类 | 技术 |
| --- | --- |
| 语言 | Go 1.27 |
| Web 框架 | Gin v1.12 |
| ORM / 数据库 | GORM + MySQL(含 soft_delete 软删除插件) |
| 缓存 / 锁 | go-redis v9(会话存储、缓存、分布式锁、计数) |
| 配置 | spf13/viper(仅读 YAML 文件) |
| 鉴权 | golang-jwt/v5(HS256 双 token) |
| 对象存储 | 腾讯云 COS SDK v5(含 CDN 签名 URL) |
| 微信生态 | 小程序 code2session 登录、微信支付 APIv3(wechatpay-go 官方 SDK)、小程序虚拟支付(XPay) |
| 可观测性 | OpenTelemetry v1.46(otelgin、otelslog、OTLP gRPC 导出) |
| 日志 | log/slog + lumberjack(JSON、按大小轮转压缩) |
| AI 推理服务 | Python FastAPI:Fashion-CLIP 衣物分类、BiRefNet 抠图 |
## 目录结构
```
zt-closet-backend/
├── main.go # 入口:初始化配置/日志/DB/Redis/COS/支付,启动后台任务与 HTTP 服务
├── cmd/
│ └── migrate/ # 独立数据库迁移命令入口(up/down/steps/version/force)
├── configs/
│ ├── config.example.yaml # 配置模板(含注释说明)
│ └── config.yaml # 实际配置(已被 .gitignore 忽略,需自行复制创建)
├── global/ # 全局单例:配置指针、DB/Redis/COS 引擎、OTel Tracer/Meter
├── internal/
│ ├── dto/ # 请求/响应 DTO 与分页结构
│ ├── middleware/ # RequestID / OTel / 请求日志(脱敏) / CORS / JWT 鉴权
│ ├── mysqldao/ # MySQL 数据访问层(tracing span 命名 mysqldao.Xxx)
│ ├── mysqlmodel/ # GORM 数据表模型(18 张表)
│ ├── redisdao/ # Redis 数据访问层(tracing span 命名 redisdao.Xxx)
│ ├── redismodel/ # Redis 引擎初始化与 key 常量
│ ├── routers/ # 路由注册(router.go)与各 handler(api/v1)
│ └── service/ # 业务逻辑层(16 个 service)
├── pkg/
│ ├── app/ # HTTP 统一响应封装 {code, msg, data}
│ ├── classification/ # 衣物分类服务客户端
│ ├── cos/ # 腾讯云 COS 客户端
│ ├── errcode/ # 统一业务错误码(按业务域分段)
│ ├── lzcwyjwt/ # JWT 生成/解析
│ ├── matting/ # 抠图服务客户端
│ ├── migration/ # 数据库迁移:embed 迁移脚本 + Up/Down/Steps/Version/Force
│ │ # (目录下只存放单一 000001_init,up 建全量表/down 删全量表)
│ ├── setting/ # viper 配置读取与配置结构体
│ ├── tokenhub/ # 腾讯 Tokenhub 大模型网关客户端(运势/AI 推荐生成 + 调度)
│ ├── tracing/ # OpenTelemetry 初始化与日志桥接
│ ├── upload/ # 文件上传校验与存储
│ └── wechat/ # 微信登录与微信支付
├── thirdparty/ # Python AI 推理服务
│ ├── classification_server/ # Fashion-CLIP 衣物分类(端口 8000)
│ └── matting_server/ # BiRefNet 抠图(端口 8001)
├── design/ # 数据库 ER 图(drawio)
├── sql/ # 占位目录(建表 SQL 已统一收归 pkg/migration)
├── scripts/ # 占位目录(暂无部署脚本)
└── storage/ # 本地存储(日志、上传临时文件,已被 gitignore)
```
> 说明:`sql/`、`scripts/`、`docs/` 目前为空占位目录;建表与索引由 `pkg/migration` 中的版本化迁移脚本维护,表模型见 `internal/mysqlmodel/`,ER 图见 `design/zt-closet-er.drawio`。
## 快速开始
### 1. 环境依赖
- Go 1.27+
- MySQL 5.7+ / 8.0
- Redis 5+
- Python 3.9+(运行 AI 推理服务时需要;若仅做基础调试可先跳过)
### 2. 配置准备
```bash
cp configs/config.example.yaml configs/config.yaml
```
编辑 `configs/config.yaml`,至少修改以下内容:
- `Mysql`:数据库连接信息
- `Redis`:Redis 连接信息
- `JWT.SecretKey`:**必填**,须为强随机字符串,留空会导致启动失败
- `Server.HttpPort`:服务监听端口(模板默认为 80)
- `Cos` / `WechatMini` / `WechatPay` / `WechatVirtualPay`:按需填写;未配置的第三方能力(COS、微信支付、虚拟支付)启动时仅告警、不阻断服务
> **重要**:`config.example.yaml` 中不包含 `Tokenhub` 段,但服务启动时会强制读取该段(缺失将导致启动 panic)。请手动在 `config.yaml` 中补充:
```yaml
Tokenhub:
APIKey: your_tokenhub_api_key # 腾讯 Tokenhub 大模型网关 APIKey,用于生成运势与 AI 穿搭推荐
Model: hy4 # 模型代号,留空时默认 hy4
```
`APIKey` 留空不会阻断启动,但运势与 AI 推荐功能将不可用(后台任务仅记录错误日志)。
### 3. 准备数据库
先创建 `config.yaml` 中 `Mysql.DBName` 对应的**空数据库**(迁移脚本只建表,不建库):
```sql
CREATE DATABASE IF NOT EXISTS `zt-closet` DEFAULT CHARSET utf8mb4;
```
再用独立命令执行迁移(服务启动时**不会**自动迁移):
```bash
go run ./cmd/migrate up # 建表到最新版本(幂等,可重复执行)
go run ./cmd/migrate version # 查看当前版本与是否为脏库
```
其他命令:
```bash
go run ./cmd/migrate steps 1 # 向上 1 步(负数向下)
go run ./cmd/migrate down # 全量回滚(按依赖倒序删表,慎用)
go run ./cmd/migrate force 3 # 迁移中断导致 dirty 时,强制修正版本号
```
迁移脚本位于 `pkg/migration`,**单一版本**(`000001_init`)包含全部 **27 张业务表**,以 `go:embed` 打包进二进制;迁移记录保存在 `schema_migrations` 表。
> 维护约定:项目当前**无线上数据**,因此不使用增量迁移(ALTER/UPDATE)。结构变更直接修改 `000001_init.up.sql` 里对应表的 `CREATE TABLE` 语句,并同步 `000001_init.down.sql`。
字段与索引以 `internal/mysqlmodel/` 的 GORM 模型为准,ER 图见 `design/zt-closet-er.drawio`。
### 4. 启动 AI 推理服务(可选,推荐)
衣物上传后的抠图与分类依赖两个 Python 服务,地址与端口在配置的 `MattingModel` / `ClassificationModel` 段中约定(默认分别为 `8001` / `8000`)。
**抠图服务(BiRefNet):**
```bash
cd thirdparty/matting_server
pip install -r requirements.txt
python main.py # 监听 0.0.0.0:8001
```
- 模型使用 `ZhengPeng7/BiRefNet_lite`(small 变体),本地缓存优先、缺失时自动下载;可用环境变量 `BIREFNET_REPO_ID` 覆盖模型来源。
- 默认从 `https://hf-mirror.com`(国内镜像)下载权重。
**分类服务(Fashion-CLIP):**
```bash
cd thirdparty/classification_server
pip install -r requirements.txt
python main.py # 监听 0.0.0.0:8000
```
- 模型使用 `patrickjohncyh/fashion-clip`,一次前向识别衣物 15 个维度(大类/子类别/风格/主副颜色/厚度/版型/材质/花纹/袖型/衣长/领型/正式程度/场合/视觉冲击力)。
- 首次运行需下载模型权重,同样走 hf-mirror 镜像。
> 建议在带 GPU 的环境中部署两个推理服务;CPU 环境亦可运行(抠图服务自动退回 fp32)。
### 5. 编译运行
```bash
# 必须在项目根目录执行(配置路径固定为相对路径 configs/config.yaml)
go run .
```
服务启动后验证健康检查:
```bash
curl http://localhost:<HttpPort>/ping
# {"message":"pong"}
```
## 配置说明
配置文件为 `configs/config.yaml`(YAML),**仅从文件读取,不支持环境变量覆盖**。各配置段说明如下:
| 配置段 | 说明 | 关键字段 |
| --- | --- | --- |
| `Server` | HTTP 服务 | `Domain`(CORS 允许域名,`*` 或不填允许所有来源)、`RunMode`(debug/release)、`HttpPort`、`ReadTimeout`/`WriteTimeout`(秒) |
| `Log` | 日志 | `LogSavePath`、`LogFileName`、`MaxSize`/`MaxBackups`/`MaxAge`(MB/份数/天数)、`LogLevel`(Debug/Info/Warn/Error) |
| `Mysql` | MySQL 连接池 | 用户名/密码/Host/Port/DBName 及连接池参数 |
| `Redis` | Redis 连接 | Host/Port/Password/DB 及连接池、重试、超时参数 |
| `Otel` | OpenTelemetry | `Enabled`、`ServiceName`、`ExporterEndpoint`(OTLP gRPC 端点)、`Insecure`、`SamplerRatio`(0~1) |
| `JWT` | 鉴权 | `SecretKey`(**必填**,空则启动失败)、`Issuer`、`AccessTokenExpire`(默认 7200s)、`RefreshTokenExpire`(默认 86400s) |
| `Cos` | 腾讯云 COS | SecretID/SecretKey、上传/下载域名、CDN 下载鉴权密钥 |
| `Upload` | 上传限制 | `TmpSavePath`、`MaxSizeMB`(默认 20)、`AllowedExtensions` 扩展名白名单 |
| `MattingModel` | 抠图服务地址 | `Host`/`Port`(默认 8001)、`MaxRetry`(默认 3) |
| `ClassificationModel` | 分类服务地址 | `Host`/`Port`(默认 8000)、`MaxRetry`(默认 3) |
| `WechatMini` | 微信小程序 | `AppID`/`AppSecret`(用于 code2session 登录) |
| `WechatPay` | 微信支付 V3 | `Enable`(是否启用,未开通置 false 不加载)/`AppID`/`MchID`/`APIv3Key`(**必须 32 字节**)/`SerialNo`/`PrivateKeyPath`/`NotifyURL`/`RefundNotifyURL` |
| `WechatVirtualPay` | 小程序虚拟支付(XPay) | `Enable`(是否启用)/`OfferID`/`EnvAppKey`(现网 AppKey)/`SandboxAppKey`(沙箱 AppKey)/`Token`(消息推送验签);与普通微信支付独立商户号、独立结算,虚拟商品(会员/额度包)须走此通道。**环境自动选择**:只填 `SandboxAppKey` 走沙箱测试(不真实扣款),只填 `EnvAppKey` 或两把都填走现网(现网优先) |
> **虚拟支付道具同步由管理后台负责,服务端不会自动同步**:商品发布为虚拟支付道具(上传/发布、改价)由管理后台完成,并将微信侧返回的 `product_id` 与 `product_status` 回填到 `goods` 表后,虚拟商品才可下单。**运营改价后必须通过管理后台同步道具价格**,否则微信侧道具价格与本地不一致:微信扣款多于订单金额时发货金额校验会拒绝(用户付款拿不到权益),少于订单金额时产生差价资损。
| `UserDefaultQuota` | 新用户免费额度与赠送会员 | `Cos`(**永久**额度,未配置回落 20)、`AI`/`OutfitShare`/`ClosetShare`(**每日**额度,未配置回落 3/10/5,每天 0 点重置;可显式配 **0** 表示不赠送)、`GiftVIPDay`(注册赠送体验会员天数,未配置回落 3 天,可配 **0** 关闭;负数回落默认) |
| `AdReward` | 广告激励额度 | `Enable`(是否开启,关闭时接口返回「广告激励未开启」)、`Cos`/`AI`/`OutfitShare`/`ClosetShare`(各类型**固定金额**,服务端定价,前端不传金额;可删或置 0 表示不发放该类型) |
| `Tokenhub` | 大模型网关 | `APIKey`、`Model`(默认 hy4);**模板中缺失,需手动补充(见快速开始)** |
> 未配置(整段缺失或留空)的第三方能力:COS、微信支付与虚拟支付在启动时降级为告警并禁用对应功能;微信登录(`WechatMini`)未配置时服务可正常启动,但登录接口会返回"未配置"错误。`Tokenhub`、`MattingModel`、`ClassificationModel` 三段为启动强依赖(缺失会导致 panic),请务必配置。
## 业务流程
> 本章从**业务视角**介绍各业务域的闭环流转、状态机与关键业务规则(额度扣减/回退、审核、幂等、补偿等);接口层面的调用顺序与时序图见下文 [核心业务调用流程](#核心业务调用流程),接口入参/出参细节见 [接口调用详解](#接口调用详解)。
### 1. 业务流程总览
```mermaid
flowchart TD
A["微信登录<br/>新用户自动注册"] --> B["初始化免费额度<br/>默认 上传 20 / AI 3 / 搭配分享 10 / 衣柜分享 5<br/>(可配置,允许为 0)"]
B --> 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
```
全系统共 7 条业务主线:
1. **注册与额度**:微信登录首次自动注册,初始化 Cos **永久**免费额度(内置默认 20),AI 推荐 / 搭配分享 / 衣柜分享为**每日**额度(内置默认 3/10/5,可由 `UserDefaultQuota` 覆盖,**允许显式配 0** 表示不赠送;未配置或负数回落默认),每日额度不落库、按当天计数每天 0 点重置;并赠送体验会员(`vip_type=4`,内置默认 3 天,可由 `UserDefaultQuota.GiftVIPDay` 覆盖,**允许配 0 关闭**),同时下发一条系统通知;会员已过期时接口返回的 `vip_type` 归一为 1。
2. **衣物生命周期**:上传(校验 + 扣额)→ 异步 AI 处理(抠图 → 分类)→ 成功入库可被选用;失败自动回退额度;删除软删并回退额度。
3. **穿搭与社区**:创建穿搭 → 公开(审核自动通过 + 扣分享额度)→ 其他用户点赞/收藏 → 触发通知。
4. **衣柜分享**:部分衣物或全量快照分享 → 扣额度 → 属主/访客查看(访客访问即记录接收关系)→ 取消或到期懒过期;"分享给我的"列表联表查询接收关系。
5. **交易与权益**:选购商品 → 下单(15 分钟有效)→ 微信支付回调发放权益 → 超时过期 / 退款回收,发放失败自动补偿。
6. **AI 推荐**:扣 AI 额度 → 结合星座运势与衣橱生成 → 落库"待用户保存" → 保存转正式穿搭(公开仍需扣分享额度)。
7. **通知**:点赞、支付成功、退款成功等业务动作触发站内通知,未读计数实时维护。
### 2. 衣物生命周期
衣物图片处理状态机(`image_process_status`):
```mermaid
stateDiagram-v2
[*] --> queued : 上传成功(同步扣上传额度 1 点)
queued --> matting : 异步任务开始
matting --> classifying : 抠图完成
classifying --> success : 分类完成
classifying --> failed : 分类失败
matting --> failed : 抠图失败
queued --> failed : 保存/上传异常
success --> deleted : 用户删除(回退额度)
failed --> deleted : 用户删除(不回退,失败时已回退)
state "3 排队中" as queued
state "1 抠图中" as matting
state "2 分类中" as classifying
state "4 成功" as success
state "5 失败" as failed
```
处理流程:
1. **上传校验**:扩展名白名单 `jpg/jpeg/png` + magic bytes(文件头)双重校验 + 大小 ≤ 20MB,任一项不通过即拒绝上传。
2. **同步扣额**:先扣减上传额度 1 点,扣减成功后才创建衣物记录(初始状态 `3 排队中`)并立即返回 `clothes_uuid`;建记录失败自动回退额度。
3. **异步 AI 流水线**:抠图 → 15 维分类在后台 goroutine 中执行;并发信号量上限 10、单任务超时 3 分钟;抠图/分类调用各带重试(默认 3 次、间隔 1s)。
4. **失败处理**:任一环节异常即置 `5 失败` 并写入 `fail_reason`(如"图片抠图失败/图片分类失败");**失败自动回退上传额度**(仅当衣物仍未被删除,防止与删除回退双重回退);分类失败但抠图成功时保留抠好的图,供用户手动补全属性。
5. **修正与删除**:`update` 支持 18 个属性字段(分类/风格/颜色/材质/袖型等,无枚举限制,支持自定义分类);删除为软删除,**处理状态非失败**(失败态已在失败路径回退过)且已扣额度时,在事务内(行锁 → 软删 → 条件扣减 → 写回退流水)回退额度。
### 3. 穿搭与社区
穿搭公开与审核状态机:
```mermaid
stateDiagram-v2
[*] --> created : 创建穿搭(1~10 件本人衣物)
created --> private : 默认私有 is_public=1
private --> public : 公开 is_public=2(审核自动通过 + 扣搭配分享额度 1 点)
public --> private : 取消公开(不返还已扣额度)
public --> interacted : 其他用户点赞 / 收藏(触发点赞通知)
private --> [*] : 修改(明细先删后建)/ 删除(软删 + 明细物理删)
public --> [*] : 修改(明细先删后建)/ 删除(软删 + 明细物理删)
state "创建(私有/待审核)" as created
state "1 私有" as private
state "2 公开(审核通过)" as public
state "社区互动" as interacted
```
处理流程:
1. **创建**:1~10 件衣物(必须属于当前用户)、名称必填;默认 `is_public=1` 私有、`audit_status=1` 待审核、`status=1` 正常。
2. **公开**:置 `is_public=2` 时审核**自动通过**(`audit_status 1→2`)并扣搭配分享额度 1 点;扣额失败则回退为私有。当前无人工驳回流程(`audit_status=3` 不通过为预留值)。
3. **可见性**:`is_public=2` 且 `audit_status=2` 且 `status=1` 三者同时满足才对他人可见;他人仅可点赞/收藏公开可见的穿搭。
4. **点赞 / 收藏**:先写 MySQL(联合唯一索引 upsert 幂等)再写 Redis 集合;取消时 MySQL 记录状态置 0(不删记录),可再次点赞/收藏;仅"从取消/无记录变为点赞"且非本人时才触发通知。
5. **实现备注**:当前无独立"社区广场"浏览列表接口(仅"我的穿搭"列表 + 按 UUID 查看公开穿搭)。
### 4. 衣柜分享
分享状态机(`status`):
```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
```
处理流程:
1. **创建**:`share_type=1` 部分衣物(指定衣物 UUID 列表,逐件校验归属 + 去重)或 `share_type=2` 全量快照(当前全部衣物 ID 的引用列表,非属性数据副本)。
2. **扣额**:扣衣柜分享额度 1 点,明细落库失败自动回退(避免产生空分享);创建即自动审核通过。
3. **过期机制**:`ExpireTime` 由创建者指定(0 表示永不);采用**懒过期**——访问详情时判定过期并更新状态,无定时扫描任务。
4. **查看**:详情支持属主与访客视角(`is_owner`),访客计入访问统计;封禁状态对访问者隐藏(返回"不存在");已取消/已过期返回对应错误。
5. **删除衣物影响**:不联动清理分享明细,被软删除的衣物在分享详情中自动过滤。
6. **接收关系**:采用"访问即视为收到"——登录用户(非属主)成功访问他人分享详情时,幂等写入 `closet_share_receivers`(`share_uuid + receiver_user_id` 联合唯一);一个分享可被多个用户接收,重复访问不重复记录。
7. **分享给我的列表**:`GET /closet-share/received` 联表分页返回当前用户收到的分享,额外返回分享者昵称/头像(`owner_name` / `owner_avatar`)。
8. **实现备注**:service 层已支持访客(`userID=0`)视角,但当前路由注册在鉴权组内,未登录访客的免登录访问需结合 HTTP 层路由确认。
### 5. 交易与权益
订单状态机(`order_status`):
```mermaid
stateDiagram-v2
[*] --> pending : 创建订单(快照权益 + 微信 JSAPI 下单)
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
```
处理流程:
1. **商品**:5 类商品(`1` 会员包 / `2` 上传包 / `3` AI 推荐包 / `4` 衣柜分享包 / `5` 搭配分享包),价格单位为**分**;仅 `1 上架` 状态可购买。
2. **下单**:实际支付金额取 `discount_price`;下单时把商品权益快照(会员天数/各类额度)冗余进订单,防止商品后续变更导致错发;先落本地待支付订单再调微信 JSAPI,微信下单失败时订单保留并靠超时自动过期。同一用户可并存多个待支付订单(无防重限制)。
3. **支付回调**:验签 + 解密 → 仅处理 `SUCCESS` → 幂等锁 30s → **金额校验**(回调金额与订单不一致则拒绝流转、人工介入)→ 待支付 → 已支付 → 发放权益 + 写支付信息 + 发订单通知。竞态兜底:已过期但已付款的订单补发货;已取消但已付款的订单生成**待审核退款单**,由管理员手动退款。
4. **权益发放**:会员天数实际发放(叠加规则 `vip_expire = max(vip_expire, now) + 天数×86400`,续费顺延,会员类型按**续费后最终到期时长**推导——剩余 ≥365 天为年度会员、否则月度会员,避免年度会员续月包被降级);Cos / AI / 分享额度改为写入**额度凭证** `quota_grants`——会员包 `expire_time=0`(跟随 VIP)、加量包按 `goods.valid_days` 独立到期,幂等由 `(ref_id, quota_type)` 唯一索引保证;补偿任务 `order_no + benefit_type` 唯一保证不重发。
5. **超时过期**:后台任务**每分钟**扫描 `待支付且 expire_time < now` 的订单批量置为已过期(条件更新天然幂等);过期后无权益可回收。
6. **退款**:**所有退款均需管理员操作**——用户申请退款仅登记待审核退款单(`0 待处理`),运营审核后在微信后台手动退款,退款回调更新状态(`0 待处理 → 1 退款中 → 2 成功 / 3 失败`);「已取消但已付款」的订单同样生成待审核退款单而非自动退款。退款成功回调将订单置已退款、**回收权益**(会员天数不回退到过去;赠送额度通过失效凭证回收未用部分,已用不追回;已取消订单权益从未发放,跳过回收)并发通知。
7. **发放失败补偿**:权益发放失败自动落 pending 任务,后台任务**每分钟**扫描重试(Redis 锁保证单实例),每轮最多 100 个,重试 3 次仍失败则标记失败 + 告警人工介入;重试前先查发放流水幂等。
8. **额度变更统一收口**:Cos 永久额度扣减/回退封装在事务内「凭证扣减(先到期先用)+ 免费额度扣减 + 写流水」强一致;每日额度(AI/分享/衣柜分享)由 Redis 原子限流计数(当天 0 点自然过期),MySQL `user_daily_usages` 兜底统计。
### 6. AI 推荐
```mermaid
flowchart TD
A["发起推荐请求"] --> B{"Redis 锁 180s<br/>(防并发重复生成)"}
B -- 抢锁失败 --> X["返回「请求过于频繁」"]
B -- 成功 --> C["扣 AI 额度 1 点<br/>(生成前权威扣减)"]
C --> D["组装输入:<br/>处理成功的全部衣物 + 星座 + 今日运势"]
D --> E["调用大模型生成<br/>(tokenhub)"]
E --> F{"校验结果<br/>(过滤模型幻觉衣物 ID)"}
F -- 有效 --> G["落库:source=2 AI、status=3 待用户保存"]
F -- 无效 / 任一步失败 --> Y["回退 AI 额度 + 写失败流水"]
G --> H["用户确认 Save:<br/>status 3→1 转正式穿搭(不扣额)"]
H --> I["公开到社区仍需 Public<br/>(扣搭配分享额度)"]
```
处理流程:
1. **输入**:当前用户**处理成功(4)** 的全部衣物(分页遍历)+ 由生日推算的星座 + 今日运势(无生日/运势时降级为仅星座或空)。
2. **扣额与回退**:**生成前**扣减 AI 额度 1 点;衣橱为空、模型调用失败、无有效单品、落库失败等任一情况均**统一回退额度**并写失败流水。
3. **结果校验**:校正模型返回的衣物 ID(过滤幻觉),无有效单品则失败回退。
4. **落库**:生成穿搭以 `source=2 AI`、`status=3 待用户保存` 入库(私有、待审核);明细落库失败则删除穿搭 + 回退额度。
5. **保存转正**:仅 `status=3` 可保存(重复保存幂等),保存仅将状态 3→1,**不扣分享额度**;后续公开到社区仍走 Public 流程扣搭配分享额度。
6. **实现备注**:不保存的"待保存"穿搭无自动清理任务;同用户并发请求由 Redis 锁(180s)拦截。
### 7. 通知触发规则
| 触发动作 | 通知类型 | 发送者 | 接收者 | 触发条件与防重 |
| --- | --- | --- | --- | --- |
| 点赞 | `1` 点赞 | 点赞者 | 穿搭作者 | 仅当"从取消/无记录变为点赞"且非本人点赞时触发 |
| 支付成功 | `2` 订单 | 系统(sender=0) | 买家 | 支付回调发放权益成功后触发;取消/过期不发 |
| 退款成功 | `2` 订单 | 系统(sender=0) | 买家 | 退款回调回收权益后触发 |
| 系统通知 | `3` 系统 | 预留 | — | 常量已定义,当前代码无发送点(需运营/后台触发) |
- **发送实现**:写入 MySQL 通知表(`is_read=1` 未读)+ Redis 未读计数自增,best-effort(失败仅告警,不影响主业务)。
- **未读/已读**:`is_read` 取值 `1 未读 / 2 已读`;标记已读为**单条接口**(条件更新"仅未读→已读"防并发重复扣减,随后 Redis 计数自减),无"全部已读"接口。
- **未读计数**:Redis 优先,Redis 为 0/异常时回落 MySQL 统计并回填缓存。
> 以上 7 条业务主线的**接口调用顺序**(时序图)见下文 [核心业务调用流程](#核心业务调用流程)。
## API 概览
- 统一前缀:`/api/v1`
- 统一响应结构:成功 `{"code":0,"msg":"成功","data":{...}}`;失败 `{"code":<业务错误码>,"msg":<文案>,"details":[...]}`。HTTP 状态码大多为 200(按业务 code 处理),鉴权类错误返回 401/403,限流 429,系统错误 500。
- 鉴权方式:`Authorization: Bearer <access_token>`(登录/刷新接口获取)。
### 通用调用约定
#### 鉴权
- 需登录接口统一携带请求头 `Authorization: Bearer <access_token>`;access_token 由登录接口返回,默认有效期 7200 秒(`JWT.AccessTokenExpire`)。
- access_token 过期(业务码 `11002`,HTTP 401)时,前端调用 `POST /user/refresh-token` 换新,成功后携带新 access_token 重试原请求。
- 鉴权中间件除校验 JWT 外,还会校验 Redis 会话状态与用户状态(必须为 `status=1` 正常,否则拒绝);Redis 故障时降级为仅校验 JWT。
#### 统一响应结构
成功(HTTP 200):
```json
{
"code": 0,
"msg": "成功",
"data": {}
}
```
- 无业务数据的接口(删除、取消、标记已读等)`data` 为空对象 `{}`。
失败:
```json
{
"code": 10000,
"msg": "请求参数错误",
"details": ["field: 错误说明"]
}
```
- `details` 仅在参数校验类错误时返回错误明细数组,非校验类错误无此字段。
- **判断业务结果一律以 `code` 为准**,不能仅凭 HTTP 状态码。
#### HTTP 状态码与业务码映射
| HTTP 状态码 | 业务 code | 场景 |
| --- | --- | --- |
| 200 | 0 及其余全部业务码 | 成功与绝大多数业务错误(按 code 区分) |
| 401 | 11000 / 11001 / 11002 / 11004 | 未授权、Token 无效 / 已过期 / 缺失 |
| 403 | 12000 | 越权访问他人资源 |
| 429 | 10003 | 请求过于频繁(限流) |
| 500 | 19000–19003 / 19999 / 39001 / 39002 | 系统 / 数据库 / 缓存 / 下游 AI 服务错误 |
#### 微信支付回调专用响应
`/order/pay/notify`、`/order/refund/notify` 是微信服务端回调,不走统一响应包装,按微信 V3 规范返回:
- 受理成功:HTTP 200,`{"code":"SUCCESS","message":"成功"}`
- 受理失败:HTTP 500,`{"code":"FAIL","message":"<失败原因>"}`
#### 分页约定
- 所有列表接口使用 GET query 参数:`page`(可选,≥1)、`page_size`(可选,1~50)。
- 列表响应统一包含:`total`(总条数,int64)、`page`(当前页)、`page_size`(每页条数)、`list`(数据数组)。
#### 金额单位
- 所有金额字段(`price`、`discount_price`、`unit_price`、`pay_amount`、`refund_amount`)单位均为**分**(int64),前端展示需除以 100。
#### 幂等与动作切换
- 点赞 / 收藏 / 取消分享等写操作幂等,重复提交不会产生重复数据。
- 点赞与收藏通过 `action` 字段切换:`1=执行(点赞/收藏)`、`2=取消`。
- 删除衣物回退上传额度、权益发放等均由服务端事务 + 幂等流水保证一致,前端无需补偿处理。
### 健康检查
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| GET | `/ping` | 存活探针,返回 `{"message":"pong"}` |
### 公开路由(无需登录)
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| 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 | `/user/info` | 查询个人信息 |
| PUT | `/user/profile` | 更新个人资料 |
| GET | `/quota/info` | 额度概览(四类额度余量) |
| GET | `/quota/flow` | 额度流水 |
| GET | `/horoscope/today` | 今日星座运势(按生日推算星座) |
**衣物 / 穿搭**
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| POST | `/clothes/upload` | 上传衣物图片(触发 AI 抠图/分类流水线) |
| GET | `/clothes` | 衣物列表 |
| GET | `/clothes/:uuid` | 衣物详情 |
| GET | `/clothes/:uuid/status` | 衣物 AI 处理状态 |
| PUT | `/clothes/:uuid` | 更新衣物信息 |
| DELETE | `/clothes/:uuid` | 删除衣物(返还上传额度) |
| POST | `/outfit` | 创建穿搭 |
| GET | `/outfit` | 穿搭列表 |
| GET | `/outfit/:uuid` | 穿搭详情 |
| PUT | `/outfit/:uuid` | 更新穿搭 |
| DELETE | `/outfit/:uuid` | 删除穿搭 |
| PUT | `/outfit/:uuid/public` | 公开穿搭(进入社区,需审核) |
**分享 / 社区 / 通知**
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| POST | `/closet-share` | 创建衣柜分享 |
| GET | `/closet-share` | 我的衣柜分享列表 |
| GET | `/closet-share/received` | 分享给我的列表 |
| GET | `/closet-share/:uuid` | 分享详情 |
| POST | `/closet-share/:uuid/cancel` | 取消分享 |
| POST | `/collect` | 收藏/取消收藏(由请求 Action 决定) |
| GET | `/collect` | 收藏列表 |
| GET | `/collect/:targetUUID` | 查询目标收藏状态与数量 |
| POST | `/like` | 点赞/取消点赞(由请求 Action 决定) |
| GET | `/like/:targetUUID` | 查询目标点赞情况 |
| GET | `/notification` | 通知列表 |
| GET | `/notification/unread` | 未读通知数 |
| GET | `/notification/:uuid` | 通知详情 |
| POST | `/notification/:uuid/read` | 标记已读 |
**订单 / AI 推荐**
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| POST | `/order` | 创建订单(微信 JSAPI 支付) |
| GET | `/order` | 订单列表 |
| POST | `/order/:orderNo/cancel` | 取消订单 |
| POST | `/order/refund` | 申请退款 |
| POST | `/ad-reward/grant` | 广告激励额度发放(当天有效,幂等;由 `AdReward` 配置开关控制,金额服务端定价) |
| POST | `/ai-recommend` | 生成 AI 穿搭推荐(扣减 AI 额度) |
| POST | `/ai-recommend/save` | 保存 AI 推荐结果为用户穿搭 |
### 接口调用详解
> 本节为路由总表的字段级补充。请求参数位置标注:`json` = JSON 请求体、`query` = URL 查询参数、`path` = 路径参数、`form` = multipart 文件。路径中的 `:xxx` 为占位参数名,调用时替换为真实值。字段类型按 JSON 语义标注(string / int / int64 / bool / object / array)。
#### 公共数据结构
以下对象在多处接口中出现,统一在此定义,后文不再重复。
**用户对象(user_info)**
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| username | string | 昵称 |
| avatar | string | 头像地址 |
| sex | int | 1=男 2=女 |
| birth_day | int64 | 生日(秒级时间戳) |
| 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 到期时间 |
**衣物对象(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 | 处理后图片地址 |
**穿搭对象(OutfitItem)**
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| outfit_uuid | string | 穿搭 UUID |
| outfit_name | string | 穿搭名称 |
| outfit_desc | string | 穿搭描述 |
| scene | string | 适合场景 |
| source | int | 1=用户创建 2=AI 推荐 |
| is_public | int | 1=不公开 2=公开 |
| audit_status | int | 1=待审核 2=审核通过 3=审核不通过 |
| items | array | 衣物列表,元素:`{"clothes_uuid": string}` |
**商品对象(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 | 会员天数 |
| upload_quota | int64 | 上传额度 |
| closet_share_quota | int64 | 衣柜分享额度 |
| outfit_share_quota | int64 | 搭配分享额度 |
| description | string | 商品描述 |
**订单对象(OrderItem)**
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| order_no | string | 订单号 |
| goods_name | string | 商品名称 |
| unit_price | int64 | 单价(分) |
| pay_amount | int64 | 实付金额(分) |
| order_status | int | 1=待支付 2=已支付 3=已取消 4=已过期 5=已退款 |
| member_days | int64 | 会员天数 |
| expire_time | int64 | 支付截止时间戳(秒) |
| pay_time | int64 | 支付时间戳(秒) |
| refund_time | int64 | 退款时间戳(秒) |
| refund_amount | int64 | 退款金额(分) |
| created_at | int64 | 创建时间戳(秒) |
**通知对象(NotificationItem)**
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| notification_uuid | string | 消息 UUID |
| msg_type | int | 1=点赞通知 2=订单通知 3=系统通知 |
| title | string | 标题 |
| content | string | 内容 |
| is_read | int | 1=未读 2=已读 |
| created_at | int64 | 创建时间戳(秒) |
**额度流水对象(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=退款回收 |
| change_num | int64 | 变更数量(正增负减) |
| new_value | int64 | 变更后数值 |
| reason | string | 变更原因 / 备注 |
| order_no | string | 关联订单号(购买类流水) |
| created_at | int64 | 创建时间戳(秒) |
#### 1. 用户模块
**POST `/user/wxlogin` —— 微信登录(公开)**
请求(json):
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| code | string | 是 | 微信登录 code(`wx.login` 获取),首次登录自动注册并初始化免费额度 |
响应 `data`:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| access_token | string | 访问令牌,后续请求放入 `Authorization: Bearer` 头 |
| refresh_token | string | 刷新令牌,用于 access 过期后续期 |
| expire_time | int64 | access_token 过期时间(秒) |
| user_info | object | 用户对象(见公共结构) |
相关错误码:21002 微信登录失败、21003 Code 无效。
**POST `/user/refresh-token` —— 刷新令牌(公开)**
请求(json):
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| refresh_token | string | 是 | 有效的 refresh_token |
| access_token | string | 是 | 旧 access_token,必须与 refresh_token 一起传入;服务端校验通过后立即吊销旧 access + 旧 refresh,缺少返回 10000 |
响应 `data`:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| access_token | string | 新访问令牌 |
| refresh_token | string | 新刷新令牌(旋转更新) |
| expire_time | int64 | 新 access_token 过期时间(秒) |
**GET `/user/info` —— 查询当前用户信息(需鉴权)**
无请求参数。响应 `data` = 用户对象(见公共结构)。
**PUT `/user/profile` —— 更新个人资料(需鉴权)**
请求(json):
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| username | string | 否 | 昵称,最长 32 |
| avatar | string | 否 | 头像地址,最长 500 |
| sex | int | 否 | 0=未知 1=男 2=女 |
| birth_day | int64 | 否 | 生日(秒级时间戳) |
响应:`data` 为空对象 `{}`。
**POST `/user/avatar/upload` —— 上传头像(需鉴权)**
multipart 上传,字段名 `file`;仅支持图片(jpeg/png/gif/webp),复用全站图片扩展名白名单与大小限制。上传成功后写入 COS(key 规则 `{用户UUID}/{用户ID}/avatar/{随机名}.{扩展名}`)并将头像 key 存入 `users.avatar`,**不消耗** COS 上传额度。响应 `data`:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| avatar | string | 头像可访问 URL |
#### 2. 额度模块
**GET `/quota/info` —— 额度概览(需鉴权)**
无请求参数。响应 `data`:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| quotas | array | 额度数组,元素见下表 |
数组元素:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| quota_type | int | 1=上传 2=AI 3=社区分享 4=衣柜分享 |
| total | int64 | 总配额 |
| used | int64 | 已使用 |
| remain | int64 | 剩余配额 |
**GET `/quota/flow` —— 额度流水(需鉴权)**
请求(query):
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| page / page_size | int | 否 | 分页(见通用约定) |
| quota_type | int | 否 | 1=上传 2=AI 3=社区分享 4=衣柜分享;不传 = 全部 |
响应 `data.list` 元素 = 额度流水对象(见公共结构)。
#### 3. 运势模块
**GET `/horoscope/today` —— 今日星座运势(需鉴权)**
无请求参数(星座按用户生日自动推算)。响应 `data`:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| stat_date | int64 | 日期(格式 YYYYMMDD) |
| 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 | 今日幸运数字 |
#### 4. 衣物模块
图片处理状态枚举(`image_process_status`,全模块通用):
| 值 | 含义 |
| --- | --- |
| 1 | 抠图中 |
| 2 | 分类中 |
| 3 | 排队中 |
| 4 | 成功 |
| 5 | 失败(配合 `fail_reason` 查看原因) |
**POST `/clothes/upload` —— 上传衣物图片(需鉴权)**
请求(form,multipart/form-data):
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| file | file | 是 | 衣物图片文件(**字段名固定为 `file`**,无其他表单字段) |
服务端校验:扩展名白名单(配置 `Upload.AllowedExtensions`)+ 大小上限(`Upload.MaxSizeMB`,默认 20MB)+ magic bytes 真图校验;上传即扣减一次上传额度。
响应 `data`:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| clothes_uuid | string | 衣物 UUID,后续查询/更新/删除均用此值 |
| image_process_status | int | 处理状态(见枚举表),初始为 3=排队中 |
相关错误码:30000 文件为空、30001 类型不支持、30002 超限、39000 保存失败、80000 额度不足。
**GET `/clothes` —— 衣物列表(需鉴权)**
请求(query):
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| page / page_size | int | 否 | 分页 |
| category_main | string | 否 | 按大类筛选 |
| category_sub | string | 否 | 按小类筛选 |
响应 `data`:`total` / `page` / `page_size` + `list`(衣物对象数组,见公共结构)。
**GET `/clothes/:uuid` —— 衣物详情(需鉴权)**
| 参数 | 位置 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| uuid | path | string | 是 | 衣物 UUID |
响应 `data` = 衣物对象(见公共结构)。
相关错误码:50000 衣物不存在、50005 无权操作该衣物。
**GET `/clothes/:uuid/status` —— 查询图片处理状态(需鉴权)**
| 参数 | 位置 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| uuid | path | string | 是 | 衣物 UUID |
响应 `data`:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| image_process_status | int | 处理状态(见枚举表) |
| fail_reason | string | 失败原因(状态为 5 时有效) |
**PUT `/clothes/:uuid` —— 更新衣物信息(需鉴权)**
| 参数 | 位置 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| uuid | path | string | 是 | 衣物 UUID |
请求(json,全部可选,传什么改什么):
| 参数 | 类型 | 说明 |
| --- | --- | --- |
| category_main | string | 大类,最长 64 |
| category_sub | string | 小类,最长 64 |
| style | string | 风格,最长 64 |
| color | string | 主颜色,最长 64(**注意:请求字段名为 `color`,非 `color_main`**) |
| 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 | 正式程度(1-5),最长 16 |
| occasions | string | 适用场合,最长 255 |
| visual_impact | string | 视觉冲击力(1-5),最长 16 |
| name | string | 名称,最长 64 |
| remark | string | 备注,最长 500 |
响应:`data` 为空对象 `{}`。
**DELETE `/clothes/:uuid` —— 删除衣物(需鉴权)**
| 参数 | 位置 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| uuid | path | string | 是 | 衣物 UUID |
响应:`data` 为空对象 `{}`。删除成功自动回退上传额度。
相关错误码:50000 衣物不存在、50001 已被搭配使用无法删除。
#### 5. 穿搭模块
穿搭枚举汇总:来源 `source` 1=用户创建 2=AI 推荐;公开 `is_public` 1=不公开 2=公开;审核 `audit_status` 1=待审核 2=通过 3=不通过。
**POST `/outfit` —— 创建穿搭(需鉴权)**
请求(json):
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| outfit_name | string | 是 | 穿搭名称,最长 32 |
| outfit_desc | string | 否 | 穿搭描述,最长 500 |
| scene | string | 否 | 适合场景,最长 100 |
| source | int | 否 | 1=用户创建 2=AI 推荐 |
| items | array | 是 | 衣物列表,1~10 项,元素:`{"clothes_uuid": string}` |
响应 `data` = 穿搭对象(见公共结构)。
**GET `/outfit` —— 我的穿搭列表(需鉴权)**
请求(query):
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| page / page_size | int | 否 | 分页 |
| is_public | int | 否 | 1=不公开 2=公开 |
| source | int | 否 | 1=用户创建 2=AI 推荐 |
响应 `data.list` 元素 = 穿搭对象(见公共结构)。
**GET `/outfit/:uuid` —— 穿搭详情(需鉴权)**
| 参数 | 位置 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| uuid | path | string | 是 | 穿搭 UUID |
响应 `data` = 穿搭对象(见公共结构)。
相关错误码:40000 穿搭不存在、40004 无权操作该穿搭。
**PUT `/outfit/:uuid` —— 更新穿搭(需鉴权)**
| 参数 | 位置 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| uuid | path | string | 是 | 穿搭 UUID |
请求(json):
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| outfit_name | string | 是 | 穿搭名称,最长 32 |
| outfit_desc | string | 否 | 穿搭描述,最长 500 |
| scene | string | 否 | 适合场景,最长 100 |
| items | array | 是 | 衣物列表,1~10 项,元素:`{"clothes_uuid": string}` |
响应:`data` 为空对象 `{}`。
**DELETE `/outfit/:uuid` —— 删除穿搭(需鉴权)**
| 参数 | 位置 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| uuid | path | string | 是 | 穿搭 UUID |
响应:`data` 为空对象 `{}`。
**PUT `/outfit/:uuid/public` —— 设置公开状态(需鉴权)**
| 参数 | 位置 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| uuid | path | string | 是 | 穿搭 UUID |
请求(json):
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| is_public | int | 是 | 1=不公开 2=公开(公开进入社区,需审核) |
响应:`data` 为空对象 `{}`。
#### 6. 衣柜分享模块
分享状态枚举:`status` 1=正常 2=已取消 3=已过期 4=已封禁。
**POST `/closet-share` —— 创建衣柜分享(需鉴权)**
请求(json):
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| share_type | int | 是 | 1=部分衣物分享 2=全量快照分享 |
| clothes_uuids | array | 部分分享必填 | 衣物 UUID 数组(share_type=1 时必填) |
| expire_time | int64 | 否 | 过期时间戳(秒) |
响应 `data`:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| share_uuid | string | 分享 UUID,供他人访问分享详情 |
**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 | 过期时间戳(秒) |
| status | int | 1=正常 2=已取消 3=已过期 4=已封禁 |
**GET `/closet-share/:uuid` —— 分享详情(需鉴权,访客视角)**
| 参数 | 位置 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| uuid | path | 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 | 是否本人分享 |
相关错误码:90100 分享不存在、90101 分享已过期、90102 分享已取消。
**POST `/closet-share/:uuid/cancel` —— 取消分享(需鉴权)**
| 参数 | 位置 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| uuid | path | string | 是 | 分享 UUID |
无请求体,响应为操作结果(空对象)。
#### 7. 点赞 / 收藏模块
动作枚举:`action` 1=执行(点赞/收藏)2=取消,重复调用幂等。
**POST `/collect` —— 收藏 / 取消收藏(需鉴权)**
请求(json):
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| target_uuid | string | 是 | 目标(社区穿搭)UUID |
| action | int | 是 | 1=收藏 2=取消收藏 |
响应:`data` 为空对象 `{}`。
相关错误码:90200 目标内容不存在、90202 收藏操作失败。
**GET `/collect` —— 我的收藏列表(需鉴权)**
请求(query):仅 `page` / `page_size` 分页参数。
响应 `data.list` 元素 = 穿搭对象(见公共结构)。
**POST `/like` —— 点赞 / 取消点赞(需鉴权)**
请求(json):
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| target_uuid | string | 是 | 目标穿搭 UUID |
| action | int | 是 | 1=点赞 2=取消点赞 |
响应:`data` 为空对象 `{}`。
相关错误码:90201 点赞操作失败。
**GET `/like/:targetUUID` —— 查询点赞状态与数量(需鉴权)**
| 参数 | 位置 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| targetUUID | path | string | 是 | 目标穿搭 UUID(**参数名为 `targetUUID`,非 `uuid`**) |
响应 `data`:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| is_liked | bool | 当前用户是否已点赞 |
| like_count | int64 | 点赞总数 |
#### 8. 通知模块
**GET `/notification` —— 通知列表(需鉴权)**
请求(query):
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| page / page_size | int | 否 | 分页 |
| is_read | int | 否 | 1=未读 2=已读;不传 = 全部(**注意:不是布尔值**) |
响应 `data.list` 元素 = 通知对象(见公共结构)。
**GET `/notification/unread` —— 未读通知数(需鉴权)**
无请求参数。响应 `data`:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| unread_count | int64 | 未读通知数 |
**GET `/notification/:uuid` —— 通知详情(需鉴权)**
| 参数 | 位置 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| uuid | path | string | 是 | 通知 UUID |
响应 `data` = 通知对象(见公共结构)。
相关错误码:90000 通知不存在。
**POST `/notification/:uuid/read` —— 标记已读(需鉴权)**
| 参数 | 位置 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| uuid | path | string | 是 | 通知 UUID |
无请求体,响应为操作结果(空对象)。
#### 9. 商品模块(公开)
商品类型枚举:`goods_type` 1=会员包 2=上传包 3=AI 推荐包 4=衣柜分享包 5=搭配分享包。
**GET `/goods` —— 商品列表(公开)**
请求(query):
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| page / page_size | int | 否 | 分页 |
| goods_type | int | 否 | 商品类型(见枚举),不传 = 全部 |
响应 `data.list` 元素 = 商品对象(见公共结构,仅返回上架商品)。
**GET `/goods/:code` —— 按编码查询商品(公开)**
| 参数 | 位置 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| code | path | string | 是 | 商品编码(goods_code) |
响应 `data` = 商品对象(见公共结构)。
相关错误码:60000 商品不存在、60001 商品已下架。
#### 10. 订单模块
订单状态枚举:`order_status` 1=待支付 2=已支付 3=已取消 4=已过期 5=已退款;退款状态 `status` 0=待处理 1=退款中 2=退款成功 3=退款失败。
**POST `/order` —— 创建订单并发起微信支付(需鉴权)**
请求(json):
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| goods_code | string | 是 | 商品编码(**唯一请求字段**,金额由服务端按商品售价计算,客户端不传金额) |
响应 `data`:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| order_no | string | 订单号 |
| pay_amount | int64 | 应付金额(分) |
| expire_time | int64 | 支付截止时间戳(秒),前端倒计时用(15 分钟) |
| pay_params | object | 小程序拉起支付参数(微信原生命名,注意大小写):`timeStamp`、`nonceStr`、`package`、`signType`、`paySign` |
相关错误码:60000 商品不存在、60001 已下架、79000 创建订单失败。
**GET `/order` —— 我的订单列表(需鉴权)**
请求(query):
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| page / page_size | int | 否 | 分页 |
| status | int | 否 | 1=待支付 2=已支付 3=已取消 4=已过期 5=已退款;不传 = 全部 |
响应 `data.list` 元素 = 订单对象(见公共结构)。
**POST `/order/:orderNo/cancel` —— 取消订单(需鉴权)**
| 参数 | 位置 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| orderNo | path | string | 是 | 订单号(**参数名为 `orderNo`,非 `uuid`**) |
无请求体,响应为操作结果(空对象)。仅待支付订单可取消。
**POST `/order/refund` —— 申请退款(需鉴权)**
请求(json):
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| order_no | string | 是 | 订单号 |
| reason | string | 是 | 退款原因,最长 200 |
响应 `data`:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| refund_no | string | 退款单号 |
| refund_amount | int64 | 退款金额(分) |
| status | int | 0=待处理 1=退款中 2=退款成功 3=退款失败 |
| apply_time | int64 | 申请时间戳(秒) |
相关错误码:71000 退款记录不存在、71001 当前状态不允许退款、71002 退款金额不合法、71003 退款申请失败。
**POST `/order/pay/notify` —— 微信支付回调(公开,微信服务端调用)**
请求体为微信支付回调原始报文(服务端验签 + 解密)。响应遵循微信规范(见通用调用约定),前端**不要**手动调用。
**POST `/order/refund/notify` —— 微信退款回调(公开,微信服务端调用)**
请求体为微信退款回调原始报文。响应遵循微信规范(见通用调用约定),前端**不要**手动调用。
#### 11. AI 推荐模块
**POST `/ai-recommend` —— 发起 AI 穿搭推荐(需鉴权)**
请求(json):
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| scene_type | int | 是 | 1=每日推荐 2=个性化场景 |
| scene | string | 否 | 用户自定义场景描述(scene_type=2 时使用) |
响应 `data`:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| outfit_uuid | string | 生成结果 UUID |
| outfit_name | string | 穿搭名称 |
| outfit_desc | string | 穿搭描述 |
| scene | string | 场景 |
| source | int | 1=用户创建 2=AI 推荐 |
| status | int | 1=正常 2=封禁 3=待用户保存 |
| outfit_item | array | 衣物列表,元素:`{"clothes_uuid": string}` |
相关错误码:90400 AI 推荐失败、90401 AI 推荐次数已达上限、80000 额度不足。
**POST `/ai-recommend/save` —— 保存 AI 推荐为我的穿搭(需鉴权)**
请求(json):
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| outfit_uuid | string | 是 | AI 推荐结果 UUID(由 `/ai-recommend` 返回) |
响应:`data` 为空对象 `{}`。
### 错误码
业务错误码按业务域分段(`pkg/errcode`),常见分段如下:
| 段位 | 业务域 |
| --- | --- |
| 0 | 成功 |
| 10000–10999 | 通用/参数(10000 参数错误、10001 不存在、10003 限流) |
| 11000–11999 | 鉴权(11000 未授权、11001 Token 鉴权失败、11002 Token 已过期、11004 Token 缺失) |
| 12000–12999 | 权限(12000 无权限) |
| 19000–19999 | 系统错误(19000 服务器错误、19001 数据库、19002 缓存) |
| 20000–29999 | 用户(21000 用户名或密码错误、21002 微信登录失败、22000 用户不存在) |
| 30000–39999 | 上传(30000 空文件、30001 扩展名不允许、30002 超限、39001 抠图失败、39002 分类失败) |
| 40000–49999 | 穿搭 |
| 50000–59999 | 衣物 |
| 60000–69999 | 商品 |
| 70000–79999 | 订单/退款(70000 订单不存在、70004 订单已过期) |
| 80000–89999 | 额度/VIP(80000 额度不足、80004 广告激励未开启) |
| 90000–99999 | 通知/分享/社区/星座/AI 推荐(90000 通知不存在、90400 AI 推荐失败、90401 AI 推荐次数已达上限) |
### 核心业务调用流程
> 以下为前后端联调时的关键调用时序(业务视角的状态机与业务规则见上文 [业务流程](#业务流程))。参与方:小程序前端、后端服务、微信服务端、AI 推理服务与大模型网关。
#### 流程 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: 首次登录:注册用户 + 初始化免费额度
B-->>F: {access_token, refresh_token, expire_time, user_info}
Note over F,B: access_token 过期(业务码 11002 / HTTP 401)
F->>B: POST /user/refresh-token {refresh_token, access_token?}
B-->>F: 新 {access_token, refresh_token, expire_time}
F->>B: 携带新 token 重试原请求
```
1. 前端调用 `wx.login` 获取 code,POST 到 `/user/wxlogin`;后端经微信 code2session 换取 openid,首次登录自动注册并初始化免费额度。
2. 前端保存双 token;此后所有需鉴权接口携带 `Authorization: Bearer <access_token>`。
3. 收到业务码 11002(HTTP 401)时调用 `/user/refresh-token` 换新(可带上旧 access_token 使其立即吊销),成功后用新 token 重试。
#### 流程 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 + 扣上传额度
B-->>F: {clothes_uuid, image_process_status: 3 排队中}
Note over B,M: 异步:排队(3) → 抠图(1) → 分类(2) → 成功(4)/失败(5)
B->>M: 抠图请求(失败重试 ≤ MaxRetry)
B->>C: 15 维分类请求(失败重试 ≤ MaxRetry)
loop 前端轮询(建议 1~2 秒间隔)
F->>B: GET /clothes/:uuid/status
B-->>F: {image_process_status, fail_reason}
end
alt 状态为 4 成功
F->>B: GET /clothes/:uuid 读取识别结果
else 状态为 5 失败
Note over F: 查看 fail_reason,可重新上传
end
```
1. 上传接口**同步返回**(不等待 AI 完成):`clothes_uuid` + 初始状态 `3`(排队中)。
2. 后端异步流水线:3 排队 → 1 抠图中(BiRefNet,端口 8001)→ 2 分类中(Fashion-CLIP,端口 8000)→ 4 成功 / 5 失败。
3. 前端轮询 `GET /clothes/:uuid/status`(建议间隔 1~2 秒),直到状态为 4 或 5。
4. 成功后调用 `GET /clothes/:uuid` 读取识别结果,可用 `PUT /clothes/:uuid` 修正识别值(注意该接口主颜色字段名为 `color`)。
5. 失败时根据 `fail_reason` 判断,可重新上传。
#### 流程 3:AI 穿搭推荐与保存
```mermaid
sequenceDiagram
participant F as 小程序前端
participant B as 后端服务
participant L as 大模型网关(Tokenhub)
F->>B: POST /ai-recommend {scene_type, scene?}
B->>B: 校验并扣减 AI 额度
B->>L: 结合星座运势 + 衣橱衣物生成推荐
L-->>B: 推荐结果
B->>B: 落库(status=3 待用户保存)
B-->>F: {outfit_uuid, outfit_name, outfit_item...}
F->>F: 用户预览、确认
F->>B: POST /ai-recommend/save {outfit_uuid}
B-->>F: 保存成功,转入我的穿搭
```
1. 请求必带 `scene_type`(1=每日推荐 2=个性化场景),每次调用扣减一次 AI 额度,不足返回 80000 / 90401。
2. 推荐结果先落库为"待用户保存"状态(status=3),响应中返回完整推荐内容供预览。
3. 用户确认后调用 `/ai-recommend/save`,结果转为用户正式穿搭;未保存的结果不进入穿搭列表。
#### 流程 4:创建订单到支付完成
```mermaid
sequenceDiagram
participant F as 小程序前端
participant B as 后端服务
participant W as 微信支付
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: 订单置为已支付,发放权益(会员/额度)
F->>B: GET /order 确认支付结果
B-->>F: {order_status: 2 已支付}
opt 超时未支付
B->>B: 后台任务每分钟扫描,超 15 分钟置为已过期
end
```
1. 创建订单只传 `goods_code`,金额由服务端按商品售价计算(单位:分),前端不传金额。
2. 前端用响应中的 `pay_params` 调 `wx.requestPayment`;`expire_time` 用于支付倒计时(15 分钟)。
3. 支付结果以微信回调为准(公开路由,服务端验签 + 解密);前端支付完成后应通过 `GET /order` 确认 `order_status=2`,不要自行判定支付成功。
4. 超时未支付订单由后台任务每分钟扫描并自动置为已过期(4)。
5. 退款:`POST /order/refund` 申请 → 微信异步处理 → 退款回调更新退款状态;可通过订单列表查看 `refund_time` / `refund_amount`。
#### 流程 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-->>F: 成功(幂等)
F->>B: POST /like {target_uuid, action: 2 取消}
B-->>F: 成功(幂等)
Note over F,B: 收藏同理:POST /collect {target_uuid, action}
```
1. 进入详情页先调 `GET /like/:targetUUID` 获取 `is_liked` / `like_count` 渲染状态。
2. 点击切换调用 `POST /like`(`action` 1=点赞 2=取消),服务端幂等,重复提交安全。
3. 收藏同理:`GET /collect` 查看收藏列表,`POST /collect` 切换收藏状态。
### 接口调用示例
> 以下示例中的 token、UUID、时间戳等业务数据均为示意值;实际以接口返回为准。
**微信登录:**
```bash
curl -X POST http://localhost:80/api/v1/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
}
}
}
```
**上传衣物图片(multipart):**
```bash
curl -X POST http://localhost:80/api/v1/clothes/upload \
-H "Authorization: Bearer <access_token>" \
-F "file=@/path/to/t-shirt.png"
```
```json
{
"code": 0,
"msg": "成功",
"data": {
"clothes_uuid": "c8f2e1a0-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"image_process_status": 3
}
}
```
**轮询处理状态:**
```bash
curl http://localhost:80/api/v1/clothes/c8f2e1a0-xxxx-xxxx-xxxx-xxxxxxxxxxxx/status \
-H "Authorization: Bearer <access_token>"
```
```json
{
"code": 0,
"msg": "成功",
"data": {
"image_process_status": 4,
"fail_reason": ""
}
}
```
**创建订单(微信支付):**
```bash
curl -X POST http://localhost:80/api/v1/order \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{"goods_code": "<商品编码,如 AI_3>"}'
```
```json
{
"code": 0,
"msg": "成功",
"data": {
"order_no": "20260907000123",
"pay_amount": 990,
"expire_time": 1756743300,
"pay_params": {
"timeStamp": "1756742400",
"nonceStr": "1a2b3c4d5e6f",
"package": "prepay_id=wx09153000123456",
"signType": "RSA",
"paySign": "xxxxxxxxxxxxxxxx"
}
}
}
```
**AI 穿搭推荐:**
```bash
curl -X POST http://localhost:80/api/v1/ai-recommend \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{"scene_type": 2, "scene": "参加朋友的婚礼"}'
```
```json
{
"code": 0,
"msg": "成功",
"data": {
"outfit_uuid": "of-xxxxxxxxxxxx",
"outfit_name": "婚礼宾客穿搭",
"outfit_desc": "简约大方,正式又不失亲和力",
"scene": "参加朋友的婚礼",
"source": 2,
"status": 3,
"outfit_item": [
{ "clothes_uuid": "c8f2e1a0-xxxx-xxxx-xxxx-xxxxxxxxxxxx" }
]
}
}
```
## 可观测性与后台任务
### 可观测性
- **Trace/Metric/Log** 三信号通过 OTLP gRPC 上报(`Otel.Enabled=true` 时);初始化失败自动降级为 no-op,不阻断启动。
- 请求链路:每请求生成 `X-Request-ID`,Gin 中间件自动开启 server span,日志中注入 `trace_id`/`span_id`。
- 结构化日志(JSON)同时输出到文件(lumberjack 滚动压缩)与 stdout;请求日志对 query 中的 `token`/`code`/`secret`/`password` 等参数自动脱敏。
### 后台任务
| 任务 | 周期 | 说明 |
| --- | --- | --- |
| 每日运势生成 | 每日 0 点 + 启动兜底 | 大模型生成十二星座运势;Redis 分布式锁保证多实例仅执行一次;当日不足 12 条时按需兜底生成 |
| 订单超时过期 | 每 60 秒 | 将超过 15 分钟未支付的订单批量置为"已过期" |
| 权益发放补偿 | 每 60 秒 | 重试发放失败的权益任务(恢复僵死任务、幂等防重复发放、超限告警) |
## 部署提示
- **工作目录**:服务启动时必须位于项目根目录(配置路径固定为 `configs/config.yaml`),容器化部署时注意挂载配置文件与日志目录。
- **多实例**:支持水平扩展;写操作幂等保护与关键后台任务(运势生成、权益补偿)均通过 Redis 分布式锁协调。
- **推理服务**:`thirdparty/` 下的两个 Python 服务建议独立部署(GPU 优先),并通过 `MattingModel` / `ClassificationModel` 配置段指向其地址。
- **可观测性后端**:按需配置 `Otel.ExporterEndpoint` 指向 OTLP Collector;无需可观测性时设置 `Enabled: false` 即可零开销关闭。
## 安全注意事项
- `JWT.SecretKey` 必须设置为强随机值,代码会强制校验,为空直接启动失败;请勿使用默认占位值。
- `WechatPay.APIv3Key` 必须为 32 字节随机密钥;微信支付/退款回调依赖官方 SDK 验签与解密,勿绕过。
- 所有密钥仅存放于 `configs/config.yaml`(已被 `.gitignore` 忽略),严禁提交到版本库或写入数据库。
- 后端对 AI 大模型输入(衣物信息、用户数据)做了清洗与截断,防 prompt 注入;上传文件除扩展名外还校验 magic bytes,防伪造类型。
- 生产环境请将 `Server.RunMode` 设为 `release`,并通过反向代理对外提供 HTTPS。