69 KiB
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~000016 版本化 SQL,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. 配置准备
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中补充:
Tokenhub:
APIKey: your_tokenhub_api_key # 腾讯 Tokenhub 大模型网关 APIKey,用于生成运势与 AI 穿搭推荐
Model: hy4 # 模型代号,留空时默认 hy4
APIKey 留空不会阻断启动,但运势与 AI 推荐功能将不可用(后台任务仅记录错误日志)。
3. 准备数据库
先创建 config.yaml 中 Mysql.DBName 对应的空数据库(迁移脚本只建表,不建库):
CREATE DATABASE IF NOT EXISTS `zt-closet` DEFAULT CHARSET utf8mb4;
再用独立命令执行迁移(服务启动时不会自动迁移):
go run ./cmd/migrate up # 建表到最新版本(幂等,可重复执行)
go run ./cmd/migrate version # 查看当前版本与是否为脏库
其他命令:
go run ./cmd/migrate steps 1 # 向上 1 步(负数向下)
go run ./cmd/migrate down # 全量回滚(按依赖倒序删表,慎用)
go run ./cmd/migrate force 3 # 迁移中断导致 dirty 时,强制修正版本号
迁移脚本位于 pkg/migration,共 17 个版本、22 张表,以 go:embed 打包进二进制;迁移记录保存在 schema_migrations 表。字段与索引以 internal/mysqlmodel/ 的 GORM 模型为准,ER 图见 design/zt-closet-er.drawio。
4. 启动 AI 推理服务(可选,推荐)
衣物上传后的抠图与分类依赖两个 Python 服务,地址与端口在配置的 MattingModel / ClassificationModel 段中约定(默认分别为 8001 / 8000)。
抠图服务(BiRefNet):
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):
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. 编译运行
# 必须在项目根目录执行(配置路径固定为相对路径 configs/config.yaml)
go run .
服务启动后验证健康检查:
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. 业务流程总览
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 条业务主线:
- 注册与额度:微信登录首次自动注册,初始化 Cos 永久免费额度(内置默认 20),AI 推荐 / 搭配分享 / 衣柜分享为每日额度(内置默认 3/10/5,可由
UserDefaultQuota覆盖,允许显式配 0 表示不赠送;未配置或负数回落默认),每日额度不落库、按当天计数每天 0 点重置;并赠送体验会员(vip_type=4,内置默认 3 天,可由UserDefaultQuota.GiftVIPDay覆盖,允许配 0 关闭),同时下发一条系统通知;会员已过期时接口返回的vip_type归一为 1。 - 衣物生命周期:上传(校验 + 扣额)→ 异步 AI 处理(抠图 → 分类)→ 成功入库可被选用;失败自动回退额度;删除软删并回退额度。
- 穿搭与社区:创建穿搭 → 公开(审核自动通过 + 扣分享额度)→ 其他用户点赞/收藏 → 触发通知。
- 衣柜分享:部分衣物或全量快照分享 → 扣额度 → 属主/访客查看(访客访问即记录接收关系)→ 取消或到期懒过期;"分享给我的"列表联表查询接收关系。
- 交易与权益:选购商品 → 下单(15 分钟有效)→ 微信支付回调发放权益 → 超时过期 / 退款回收,发放失败自动补偿。
- AI 推荐:扣 AI 额度 → 结合星座运势与衣橱生成 → 落库"待用户保存" → 保存转正式穿搭(公开仍需扣分享额度)。
- 通知:点赞、支付成功、退款成功等业务动作触发站内通知,未读计数实时维护。
2. 衣物生命周期
衣物图片处理状态机(image_process_status):
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
处理流程:
- 上传校验:扩展名白名单
jpg/jpeg/png+ magic bytes(文件头)双重校验 + 大小 ≤ 20MB,任一项不通过即拒绝上传。 - 同步扣额:先扣减上传额度 1 点,扣减成功后才创建衣物记录(初始状态
3 排队中)并立即返回clothes_uuid;建记录失败自动回退额度。 - 异步 AI 流水线:抠图 → 15 维分类在后台 goroutine 中执行;并发信号量上限 10、单任务超时 3 分钟;抠图/分类调用各带重试(默认 3 次、间隔 1s)。
- 失败处理:任一环节异常即置
5 失败并写入fail_reason(如"图片抠图失败/图片分类失败");失败自动回退上传额度(仅当衣物仍未被删除,防止与删除回退双重回退);分类失败但抠图成功时保留抠好的图,供用户手动补全属性。 - 修正与删除:
update支持 18 个属性字段(分类/风格/颜色/材质/袖型等,无枚举限制,支持自定义分类);删除为软删除,处理状态非失败(失败态已在失败路径回退过)且已扣额度时,在事务内(行锁 → 软删 → 条件扣减 → 写回退流水)回退额度。
3. 穿搭与社区
穿搭公开与审核状态机:
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~10 件衣物(必须属于当前用户)、名称必填;默认
is_public=1私有、audit_status=1待审核、status=1正常。 - 公开:置
is_public=2时审核自动通过(audit_status 1→2)并扣搭配分享额度 1 点;扣额失败则回退为私有。当前无人工驳回流程(audit_status=3不通过为预留值)。 - 可见性:
is_public=2且audit_status=2且status=1三者同时满足才对他人可见;他人仅可点赞/收藏公开可见的穿搭。 - 点赞 / 收藏:先写 MySQL(联合唯一索引 upsert 幂等)再写 Redis 集合;取消时 MySQL 记录状态置 0(不删记录),可再次点赞/收藏;仅"从取消/无记录变为点赞"且非本人时才触发通知。
- 实现备注:当前无独立"社区广场"浏览列表接口(仅"我的穿搭"列表 + 按 UUID 查看公开穿搭)。
4. 衣柜分享
分享状态机(status):
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
处理流程:
- 创建:
share_type=1部分衣物(指定衣物 UUID 列表,逐件校验归属 + 去重)或share_type=2全量快照(当前全部衣物 ID 的引用列表,非属性数据副本)。 - 扣额:扣衣柜分享额度 1 点,明细落库失败自动回退(避免产生空分享);创建即自动审核通过。
- 过期机制:
ExpireTime由创建者指定(0 表示永不);采用懒过期——访问详情时判定过期并更新状态,无定时扫描任务。 - 查看:详情支持属主与访客视角(
is_owner),访客计入访问统计;封禁状态对访问者隐藏(返回"不存在");已取消/已过期返回对应错误。 - 删除衣物影响:不联动清理分享明细,被软删除的衣物在分享详情中自动过滤。
- 接收关系:采用"访问即视为收到"——登录用户(非属主)成功访问他人分享详情时,幂等写入
closet_share_receivers(share_uuid + receiver_user_id联合唯一);一个分享可被多个用户接收,重复访问不重复记录。 - 分享给我的列表:
GET /closet-share/received联表分页返回当前用户收到的分享,额外返回分享者昵称/头像(owner_name/owner_avatar)。 - 实现备注:service 层已支持访客(
userID=0)视角,但当前路由注册在鉴权组内,未登录访客的免登录访问需结合 HTTP 层路由确认。
5. 交易与权益
订单状态机(order_status):
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
处理流程:
- 商品:5 类商品(
1会员包 /2上传包 /3AI 推荐包 /4衣柜分享包 /5搭配分享包),价格单位为分;仅1 上架状态可购买。 - 下单:实际支付金额取
discount_price;下单时把商品权益快照(会员天数/各类额度)冗余进订单,防止商品后续变更导致错发;先落本地待支付订单再调微信 JSAPI,微信下单失败时订单保留并靠超时自动过期。同一用户可并存多个待支付订单(无防重限制)。 - 支付回调:验签 + 解密 → 仅处理
SUCCESS→ 幂等锁 30s → 金额校验(回调金额与订单不一致则拒绝流转、人工介入)→ 待支付 → 已支付 → 发放权益 + 写支付信息 + 发订单通知。竞态兜底:已过期但已付款的订单补发货;已取消但已付款的订单生成待审核退款单,由管理员手动退款。 - 权益发放:会员天数实际发放(叠加规则
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唯一保证不重发。 - 超时过期:后台任务每分钟扫描
待支付且 expire_time < now的订单批量置为已过期(条件更新天然幂等);过期后无权益可回收。 - 退款:所有退款均需管理员操作——用户申请退款仅登记待审核退款单(
0 待处理),运营审核后在微信后台手动退款,退款回调更新状态(0 待处理 → 1 退款中 → 2 成功 / 3 失败);「已取消但已付款」的订单同样生成待审核退款单而非自动退款。退款成功回调将订单置已退款、回收权益(会员天数不回退到过去;赠送额度通过失效凭证回收未用部分,已用不追回;已取消订单权益从未发放,跳过回收)并发通知。 - 发放失败补偿:权益发放失败自动落 pending 任务,后台任务每分钟扫描重试(Redis 锁保证单实例),每轮最多 100 个,重试 3 次仍失败则标记失败 + 告警人工介入;重试前先查发放流水幂等。
- 额度变更统一收口:Cos 永久额度扣减/回退封装在事务内「凭证扣减(先到期先用)+ 免费额度扣减 + 写流水」强一致;每日额度(AI/分享/衣柜分享)由 Redis 原子限流计数(当天 0 点自然过期),MySQL
user_daily_usages兜底统计。
6. AI 推荐
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/>(扣搭配分享额度)"]
处理流程:
- 输入:当前用户处理成功(4) 的全部衣物(分页遍历)+ 由生日推算的星座 + 今日运势(无生日/运势时降级为仅星座或空)。
- 扣额与回退:生成前扣减 AI 额度 1 点;衣橱为空、模型调用失败、无有效单品、落库失败等任一情况均统一回退额度并写失败流水。
- 结果校验:校正模型返回的衣物 ID(过滤幻觉),无有效单品则失败回退。
- 落库:生成穿搭以
source=2 AI、status=3 待用户保存入库(私有、待审核);明细落库失败则删除穿搭 + 回退额度。 - 保存转正:仅
status=3可保存(重复保存幂等),保存仅将状态 3→1,不扣分享额度;后续公开到社区仍走 Public 流程扣搭配分享额度。 - 实现备注:不保存的"待保存"穿搭无自动清理任务;同用户并发请求由 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):
{
"code": 0,
"msg": "成功",
"data": {}
}
- 无业务数据的接口(删除、取消、标记已读等)
data为空对象{}。
失败:
{
"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 刷新
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 重试原请求
- 前端调用
wx.login获取 code,POST 到/user/wxlogin;后端经微信 code2session 换取 openid,首次登录自动注册并初始化免费额度。 - 前端保存双 token;此后所有需鉴权接口携带
Authorization: Bearer <access_token>。 - 收到业务码 11002(HTTP 401)时调用
/user/refresh-token换新(可带上旧 access_token 使其立即吊销),成功后用新 token 重试。
流程 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 + 扣上传额度
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
- 上传接口同步返回(不等待 AI 完成):
clothes_uuid+ 初始状态3(排队中)。 - 后端异步流水线:3 排队 → 1 抠图中(BiRefNet,端口 8001)→ 2 分类中(Fashion-CLIP,端口 8000)→ 4 成功 / 5 失败。
- 前端轮询
GET /clothes/:uuid/status(建议间隔 1~2 秒),直到状态为 4 或 5。 - 成功后调用
GET /clothes/:uuid读取识别结果,可用PUT /clothes/:uuid修正识别值(注意该接口主颜色字段名为color)。 - 失败时根据
fail_reason判断,可重新上传。
流程 3:AI 穿搭推荐与保存
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: 保存成功,转入我的穿搭
- 请求必带
scene_type(1=每日推荐 2=个性化场景),每次调用扣减一次 AI 额度,不足返回 80000 / 90401。 - 推荐结果先落库为"待用户保存"状态(status=3),响应中返回完整推荐内容供预览。
- 用户确认后调用
/ai-recommend/save,结果转为用户正式穿搭;未保存的结果不进入穿搭列表。
流程 4:创建订单到支付完成
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
- 创建订单只传
goods_code,金额由服务端按商品售价计算(单位:分),前端不传金额。 - 前端用响应中的
pay_params调wx.requestPayment;expire_time用于支付倒计时(15 分钟)。 - 支付结果以微信回调为准(公开路由,服务端验签 + 解密);前端支付完成后应通过
GET /order确认order_status=2,不要自行判定支付成功。 - 超时未支付订单由后台任务每分钟扫描并自动置为已过期(4)。
- 退款:
POST /order/refund申请 → 微信异步处理 → 退款回调更新退款状态;可通过订单列表查看refund_time/refund_amount。
流程 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-->>F: 成功(幂等)
F->>B: POST /like {target_uuid, action: 2 取消}
B-->>F: 成功(幂等)
Note over F,B: 收藏同理:POST /collect {target_uuid, action}
- 进入详情页先调
GET /like/:targetUUID获取is_liked/like_count渲染状态。 - 点击切换调用
POST /like(action1=点赞 2=取消),服务端幂等,重复提交安全。 - 收藏同理:
GET /collect查看收藏列表,POST /collect切换收藏状态。
接口调用示例
以下示例中的 token、UUID、时间戳等业务数据均为示意值;实际以接口返回为准。
微信登录:
curl -X POST http://localhost:80/api/v1/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
}
}
}
上传衣物图片(multipart):
curl -X POST http://localhost:80/api/v1/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 http://localhost:80/api/v1/clothes/c8f2e1a0-xxxx-xxxx-xxxx-xxxxxxxxxxxx/status \
-H "Authorization: Bearer <access_token>"
{
"code": 0,
"msg": "成功",
"data": {
"image_process_status": 4,
"fail_reason": ""
}
}
创建订单(微信支付):
curl -X POST http://localhost:80/api/v1/order \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{"goods_code": "<商品编码,如 AI_3>"}'
{
"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 穿搭推荐:
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": "参加朋友的婚礼"}'
{
"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。