# 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:/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["微信登录
新用户自动注册"] --> B["初始化免费额度
默认 上传 20 / AI 3 / 搭配分享 10 / 衣柜分享 5
(可配置,允许为 0)"] B --> C["上传衣物
校验 → 扣上传额度 → 异步 AI 抠图 + 分类"] C --> D["衣橱管理
查看 / 修正属性 / 删除回退额度"] D --> E["创建穿搭"] D --> F["AI 穿搭推荐
扣 AI 额度 → 大模型生成 → 待用户保存"] D --> G["衣柜分享
部分衣物 / 全量快照"] E --> H["公开到社区
自动审核通过 → 扣搭配分享额度"] H --> I["社区互动
点赞 / 收藏 / 站内通知"] 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 额度 + 秒回 `task_id`)→ 后台生成 → 前端轮询结果 → 落库"待用户保存" → 保存转正式穿搭(公开仍需扣分享额度);超期未保存的由定时任务自动清理。 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["POST /ai-recommend
(异步提交)"] --> B{"抢占用户活跃位
(已有任务则返回原 task_id)"} B -- 已有任务 --> X["幂等返回该 task_id
(不重复扣额度)"] B -- 抢占成功 --> C["扣 AI 额度 1 点"] C --> D["写任务快照 status=1
立即返回 {task_id, status:1}"] D --> E["后台 goroutine 生成
(独立 ctx,前端断开不中断)"] E --> F["组装输入:处理成功的全部衣物 + 星座 + 今日运势"] F --> G["调用大模型生成
(tokenhub)"] G --> H{"校验结果
(过滤模型幻觉衣物 ID)"} H -- 有效 --> I["落库:source=2 AI、status=3 待用户保存
任务快照置 status=2 + result"] H -- 无效 / 任一步失败 --> Y["回退 AI 额度 + 写失败流水
任务快照置 status=3 + error"] I --> J["前端轮询 GET /ai-recommend/status
拿到 result"] J --> K["用户确认 Save:
status 3→1 转正式穿搭(不扣额)"] K --> L["公开到社区仍需 Public
(扣搭配分享额度)"] I -. "超过 7 天未保存" .-> M["定时任务自动清理
(软删除穿搭 + 明细)"] ``` 处理流程: 1. **异步提交**:`POST /ai-recommend` 只做「抢占活跃位 → 扣额度 → 写任务快照」三步,**秒级返回 `task_id`**;生成在后台 goroutine 完成,使用独立 context,前端退出、切页面或断网都不会中断。 2. **提交幂等**:同一用户已有任务在执行时直接返回该任务的 `task_id`,不会重复扣额度;活跃位 TTL 4 分钟,略长于任务超时 3 分钟,避免任务未收尾就被放行重复提交。 3. **输入**:当前用户**处理成功(4)** 的全部衣物(分页遍历)+ 由生日推算的星座 + 今日运势(无生日/运势时降级为仅星座或空)。 4. **扣额与回退**:**提交时**扣减 AI 额度 1 点;衣橱为空、模型调用失败、无有效单品、落库失败、任务超时等任一情况均**统一回退额度**并写失败流水(回退与写流水使用不受任务取消影响的 context,避免断连时收尾失败)。 5. **结果校验**:校正模型返回的衣物 ID(过滤幻觉),无有效单品则失败回退。 6. **落库**:生成穿搭以 `source=2 AI`、`status=3 待用户保存` 入库(私有、待审核);明细落库失败则删除穿搭 + 回退额度。 7. **结果获取**:前端轮询 `GET /ai-recommend/status?task_id=...`(建议 2 秒一次、上限 90 次);`status=2` 取 `result` 渲染,`status=3` 读 `error_msg` 提示用户。任务快照在 Redis 保留 15 分钟,过期返回 `90402`。 8. **保存转正**:仅 `status=3` 可保存(重复保存幂等),保存仅将状态 3→1,**不扣分享额度**;后续公开到社区仍走 Public 流程扣搭配分享额度。 9. **自动清理**:未被保存的 `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 `(登录/刷新接口获取)。 ### 通用调用约定 #### 鉴权 - 需登录接口统一携带请求头 `Authorization: Bearer `;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 穿搭推荐任务(需鉴权)** > 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 刷新 ```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 `。 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 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:创建订单到支付完成 ```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 " \ -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 " ``` ```json { "code": 0, "msg": "成功", "data": { "image_process_status": 4, "fail_reason": "" } } ``` **创建订单(微信支付):** ```bash curl -X POST http://localhost:80/api/v1/order \ -H "Authorization: Bearer " \ -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 " \ -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。