Files

76 KiB
Raw Permalink Blame History

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. 配置准备

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,单一版本(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):

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 表示不赠送)。与会员身份无关,所有用户长期享有 | | NewUserGift | 新用户注册赠送的体验会员及其额度 | GiftVIPDay(体验会员天数,未配置回落 3 天,可配 0 表示不赠送会员)、Cos/AI/OutfitShare/ClosetShare(体验会员额度,未配置回落 0/3/3/3)。额度以体验会员凭证(source=4)发放,有效期为固定值 = 注册时刻 + 体验天数(⚠️ 不跟随 VIP 延长,否则用户后续充值会把体验额度一直「继承」下去);GiftVIPDay 配 0 时额度一律不发放。与 UserDefaultQuota 的免费额度叠加生效 | | AdReward | 广告激励额度 | Enable(是否开启,关闭时接口返回「广告激励未开启」)、Cos/AI/OutfitShare/ClosetShare(各类型固定金额,服务端定价,前端不传金额;可删或置 0 表示不发放该类型) | | Tokenhub | 大模型网关 | APIKey、Model(默认 hy4);模板中缺失,需手动补充(见快速开始) |

未配置(整段缺失或留空)的第三方能力:COS、微信支付与虚拟支付在启动时降级为告警并禁用对应功能;微信登录(WechatMini)未配置时服务可正常启动,但登录接口会返回"未配置"错误。Tokenhub、MattingModel、ClassificationModel 三段为启动强依赖(缺失会导致 panic),请务必配置。

业务流程

本章从业务视角介绍各业务域的闭环流转、状态机与关键业务规则(额度扣减/回退、审核、幂等、补偿等);接口层面的调用顺序与时序图见下文 核心业务调用流程,接口入参/出参细节见 接口调用详解。

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

全系统共 7 条业务主线:

  1. 注册与额度:微信登录首次自动注册,先初始化免费额度(UserDefaultQuota:Cos 永久默认 20,AI 推荐 / 搭配分享 / 衣柜分享为每日默认 3/10/5,允许显式配 0;未配置或负数回落默认,每日额度不落库、按当天计数每天 0 点重置),再赠送体验会员及其额度(NewUserGift:开通 vip_type=4 默认 3 天、允许配 0 关闭,并发放 source=4 体验会员额度凭证,固定有效期为体验期结束、续费不会延长,与免费额度叠加),同时下发一条系统通知;会员已过期时接口返回的 vip_type 归一为 1。
  2. 衣物生命周期:上传(校验 + 扣额)→ 异步 AI 处理(抠图 → 分类)→ 成功入库可被选用;失败自动回退额度;删除软删并回退额度。
  3. 穿搭与社区:创建穿搭 → 公开(审核自动通过 + 扣分享额度)→ 其他用户点赞/收藏 → 触发通知。
  4. 衣柜分享:部分衣物或全量快照分享 → 扣额度 → 属主/访客查看(访客访问即记录接收关系)→ 取消或到期懒过期;"分享给我的"列表联表查询接收关系。
  5. 交易与权益:选购商品 → 下单(15 分钟有效)→ 微信支付回调发放权益 → 超时过期 / 退款回收,发放失败自动补偿。
  6. AI 推荐:异步提交(抢占活跃位 + 扣 AI 额度 + 秒回 task_id)→ 后台生成 → 前端轮询结果 → 落库"待用户保存" → 保存转正式穿搭(公开仍需扣分享额度);超期未保存的由定时任务自动清理。
  7. 通知:点赞、支付成功、退款成功等业务动作触发站内通知,未读计数实时维护。

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

处理流程:

  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. 穿搭与社区

穿搭公开与审核状态机:

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):

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):

stateDiagram-v2
    [*] --> pending : 创建订单(快照权益 + 微信 JSAPI 下单)
    pending --> paid : 支付回调 SUCCESS(验签 + 金额校验 → 发放权益 → 通知)
    pending --> expired : 超时 15 分钟(后台每分钟扫描)
    pending --> cancelled : 用户主动取消(同步关闭微信订单)
    expired --> paid : 过期后付款到账(补发货)
    cancelled --> refunded : 已付款的取消订单生成待审核退款单(管理员手动退款)
    paid --> refunding : 用户申请退款(同事务写待审核退款单)
    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

处理流程:

  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 唯一保证不重发。会员包来源(source=1)额度不叠加:月度包/年度包同时持有时同类额度只取 amount 最大的一份(用尽后自动顺延下一份),避免额度翻倍。
  5. 超时过期:后台任务每分钟扫描 待支付且 expire_time < now 的订单批量置为已过期(条件更新天然幂等);过期后无权益可回收。
  6. 退款:所有退款均需管理员操作——用户申请退款受理成功后将订单置为 6 申请退款中 并登记待审核退款单(流水 0 待处理,同一事务 + 订单行锁,重复/并发申请仅受理一次),运营审核后在微信后台手动退款,退款回调更新流水状态(0 待处理 → 2 成功 / 3 失败);退款成功 → 订单 6 → 5,退款失败 → 订单回退 2 已支付(可重新申请)。虚拟支付退款推送(xpay_refund_notify)按官方字段解析:订单号取 MchOrderId、结果取 RetCode(0=成功,失败原因见 RetMsg),关键字段缺失或结果无法判定时应答失败触发微信重试(最多 15 次)而非静默成功;成功后仍反查 query_order 确认真实退款与金额一致。查单兜底任务每 60 秒反查停留超 10 分钟的「申请退款中」订单:微信侧已退款自动置 5 并回收权益,仍未退款则保持状态、超 24 小时告警人工。「已取消但已付款」的订单同样生成待审核退款单而非自动退款,订单保持 3 已取消。退款成功回调将订单置已退款、回收权益(会员天数不回退到过去;赠送额度通过失效凭证回收未用部分,已用不追回;已取消订单权益从未发放,跳过回收)并发通知。
  7. 发放失败补偿:权益发放失败自动落 pending 任务,后台任务每分钟扫描重试(Redis 锁保证单实例),每轮最多 100 个,重试 3 次仍失败则标记失败 + 告警人工介入;重试前先查发放流水幂等。
  8. 额度变更统一收口:Cos 永久额度扣减/回退封装在事务内「凭证扣减(非会员包先到期先用 → 会员包生效份)+ 免费额度扣减 + 写流水」强一致(会员包来源不叠加,未生效份不参与扣减/回退);每日额度(AI/分享/衣柜分享)由 Redis 原子限流计数(当天 0 点自然过期),MySQL user_daily_usages 兜底统计。

6. AI 推荐

flowchart TD
    A["POST /ai-recommend<br/>(异步提交)"] --> B{"抢占用户活跃位<br/>(已有任务则返回原 task_id)"}
    B -- 已有任务 --> X["幂等返回该 task_id<br/>(不重复扣额度)"]
    B -- 抢占成功 --> C["扣 AI 额度 1 点"]
    C --> D["写任务快照 status=1<br/>立即返回 {task_id, status:1}"]
    D --> E["后台 goroutine 生成<br/>(独立 ctx,前端断开不中断)"]
    E --> F["组装输入:处理成功的全部衣物 + 星座 + 今日运势"]
    F --> G["调用大模型生成<br/>(tokenhub)"]
    G --> H{"校验结果<br/>(过滤模型幻觉衣物 ID)"}
    H -- 有效 --> I["落库:source=2 AI、status=3 待用户保存<br/>任务快照置 status=2 + result"]
    H -- 无效 / 任一步失败 --> Y["回退 AI 额度 + 写失败流水<br/>任务快照置 status=3 + error"]
    I --> J["前端轮询 GET /ai-recommend/status<br/>拿到 result"]
    J --> K["用户确认 Save:<br/>status 3→1 转正式穿搭(不扣额)"]
    K --> L["公开到社区仍需 Public<br/>(扣搭配分享额度)"]
    I -. "超过 7 天未保存" .-> M["定时任务自动清理<br/>(软删除穿搭 + 明细)"]

处理流程:

  1. 异步提交:POST /ai-recommend 只做「抢占活跃位 → 扣额度 → 写任务快照」三步,秒级返回 task_id;生成在后台 goroutine 完成,使用独立 context,前端退出、切页面或断网都不会中断。
  2. 提交幂等:同一用户已有任务在执行时直接返回该任务的 task_id,不会重复扣额度;活跃位 TTL 4 分钟,略长于任务超时 3 分钟,避免任务未收尾就被放行重复提交。
  3. 输入:当前用户处理成功(4) 的全部衣物(分页遍历)+ 由生日推算的星座 + 今日运势(无生日/运势时降级为仅星座或空)。
  4. 扣额与回退:提交时扣减 AI 额度 1 点;衣橱为空、模型调用失败、无有效单品、落库失败、任务超时等任一情况均统一回退额度并写失败流水(回退与写流水使用不受任务取消影响的 context,避免断连时收尾失败)。
  5. 结果校验:校正模型返回的衣物 ID(过滤幻觉)、纠正品类、同品类去重(每类最多 1 件)。
  6. 搭配完整性保障:按衣橱实际存在的品类校验——必须含「上装」或「连衣裙」(外套不可孤立)、未选连衣裙时必须有下装、衣橱有鞋必须有鞋、配饰可选、连衣裙与下装互斥;不满足时带纠偏提示重试一次模型(重试前检查剩余时间预算,不足则跳过),仍不满足则由服务端从衣橱按「同风格 → 同主色 → 视觉冲击力接近 → 最近上传」的确定性规则自动补齐(reason 标注"系统按搭配完整性规则补齐"),避免出现"只有外套+下装"这类不完整搭配。
  7. 落库:生成穿搭以 source=2 AI、status=3 待用户保存 入库(私有、待审核);明细落库失败则删除穿搭 + 回退额度。
  8. 结果获取:前端轮询 GET /ai-recommend/status?task_id=...(建议 2 秒一次、上限 90 次);status=2 取 result 渲染,status=3 读 error_msg 提示用户。任务快照在 Redis 保留 15 分钟,过期返回 90402。
  9. 保存转正:仅 status=3 可保存(重复保存幂等),保存仅将状态 3→1,不扣分享额度;后续公开到社区仍走 Public 流程扣搭配分享额度。
  10. 自动清理:未被保存的 status=3 穿搭超过 7 天,由后台定时任务(启动即执行一次,之后每小时一次,单批 500 条)连同明细一起软删除,避免长期堆积;清理后列表与详情立即不可见。

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=已退款 6=申请退款中
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=退款回收 7=到期失效(固定有效期的额度凭证过期,由后台任务每小时补记)
change_num int64 变更数量(正增负减;到期失效为负数)
new_value int64 变更后数值
reason string 变更原因 / 备注
order_no string 关联订单号;购买/退款类为真实订单号,其余为流水 UUID 或凭证 ref_id(用于幂等唯一索引)
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=已退款 6=申请退款中;退款状态 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=已退款 6=申请退款中;不传 = 全部

响应 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 穿搭推荐任务(需鉴权)

AI 推荐为异步接口:提交后立即返回 task_id,需轮询 GET /ai-recommend/status 获取结果。生成在后台执行,前端退出、切页面、断网都不会中断。

请求(json):

参数 类型 必填 说明
scene_type int 是 1=每日推荐 2=个性化场景
scene string 否 用户自定义场景描述(scene_type=2 时使用)

响应 data:

字段 类型 说明
task_id string 任务 ID,用于轮询状态接口
status int 1=生成中

相关错误码:90401 AI 推荐次数已达上限、19002 缓存错误。

GET /ai-recommend/status —— 查询 AI 推荐任务状态(需鉴权)

请求(query):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:

字段 类型 说明
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 推荐失败、90402 AI 推荐任务不存在或已过期。

POST /ai-recommend/save —— 保存 AI 推荐为我的穿搭(需鉴权)

请求(json):

参数 类型 必填 说明
outfit_uuid string 是 AI 推荐结果 UUID(由 GET /ai-recommend/status 的 result.outfit_uuid 返回)

响应: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 重试原请求
  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 处理轮询

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 穿搭推荐与保存

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: 抢占用户活跃位 + 写入任务快照(status=1)
    B->>B: 校验并扣减 AI 额度
    B-->>F: {task_id, status: 1} · 秒级返回

    par 后台生成(独立 ctx,前端退出不中断)
        B->>L: 结合星座运势 + 衣橱衣物生成推荐
        L-->>B: 推荐结果
        B->>B: 落库(status=3 待用户保存)
        B->>R: 任务快照 status=2 + result
    and 前端轮询
        loop 每 2 秒,上限 90 次
            F->>B: GET /ai-recommend/status?task_id
            B-->>F: {status, result?}
        end
    end

    F->>F: 用户预览、确认
    F->>B: POST /ai-recommend/save {outfit_uuid}
    B-->>F: 保存成功,转入我的穿搭
  1. 请求必带 scene_type(1=每日推荐 2=个性化场景),提交时扣减一次 AI 额度,不足返回 90401。
  2. 接口秒级返回 task_id,不等大模型;生成在后台执行,前端退出、切页面或断网都不会中断。
  3. 前端轮询 GET /ai-recommend/status(建议 2 秒一次),直到 status 为 2(成功)或 3(失败);失败时额度已自动回退。
  4. 成功后结果先落库为"待用户保存"状态(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
  1. 创建订单只传 goods_code,金额由服务端按商品售价计算(单位:分),前端不传金额。
  2. 前端用响应中的 pay_params 调 wx.requestPayment;expire_time 用于支付倒计时(15 分钟)。
  3. 支付结果以微信回调为准(公开路由,服务端验签 + 解密);前端支付完成后应通过 GET /order 确认 order_status=2,不要自行判定支付成功。
  4. 超时未支付订单由后台任务每分钟扫描并自动置为已过期(4)。
  5. 退款:POST /order/refund 申请成功后订单置 6 申请退款中(人工审核,非实时到账)→ 运营在微信后台手动退款 → 退款回调将订单置 5 已退款(失败则回退 2 已支付,可重新申请);虚拟支付退款推送字段为 MchOrderId/MchRefundId/RetCode(0=成功),推送丢失时由查单兜底任务自动订正;可通过订单列表查看 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}
  1. 进入详情页先调 GET /like/:targetUUID 获取 is_liked / like_count 渲染状态。
  2. 点击切换调用 POST /like(action 1=点赞 2=取消),服务端幂等,重复提交安全。
  3. 收藏同理: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 秒 重试发放失败的权益任务(恢复僵死任务、幂等防重复发放、超限告警)
虚拟支付退款查单兜底 每 60 秒 反查停留超 10 分钟的「申请退款中」虚拟支付订单:已退款自动置「已退款」并回收权益,仍未退款保持状态、超 24 小时告警人工

部署提示

  • 工作目录:服务启动时必须位于项目根目录(配置路径固定为 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。