12 KiB
12 KiB
技术设计
「我的形象穿搭」后端实现细节与技术决策。功能与接口清单见 README.md,开发规范见 CLAUDE.md。
1. 总体架构
前后端一体单端口部署(默认 :8080):GoFrame 托管 API + H5 静态产物 + workspace 文件服务。
┌─────────────┐ H5 静态托管 ┌───────────────────────────┐
│ 前端 app-uni │ ───────────────▶│ server (GoFrame :8080) │
└─────────────┘ │ RouteRegister 反射路由 │
│ REST/JSON │ Auth 中间件(JWT) │
└───────────────────────▶│ workspace/* 文件服务 │
└───────────┬───────────────┘
│ 4 组 SQLite(data/)
┌──────────────────┼──────────────────┐
▼ ▼ ▼
default(slogan.db) plan(slogan_plan.db) pay(slogan_pay.db) / cps(slogan_cps.db)
外部依赖(均可配置,未配置时对应功能降级):大模型(LLM)、通义万相(出图)、Tripo(3D)、和风天气 + 高德地理、虎皮棋聚合支付、美团/京东/淘宝联盟。
2. 分层契约
| 层 | 输入 | 输出 | 约束 |
|---|---|---|---|
| controller | *dto.XxxReq(v tag 自动校验) |
*dto.XxxRes, error |
只透传,禁止调 dao、禁止字段搬运 |
| service | 整 *dto.XxxReq 直传 |
*dto.XxxRes, error |
组装/事务/校验/跨表;事务唯一入口 |
| dao | entity/单值 | entity/单值 | 单表 SQL;Record→entity 转换在 dao 内 |
路由由 dto g.Meta 声明,common.RouteRegister 按 controller 结构体名反射为 kebab-case 组前缀统一注册;新增接口 = 写 dto + controller 方法,不手工注册。例外:支付回调 /member/order/notify 需裸文本响应 "success",由 controller 直接写响应体(HTTP 协议职责),经 Auth 白名单放行。
3. 数据库设计(4 库 22 表)
SQLite 无 WAL 并发写,写一律回主 goroutine;跨表数据拆多条单表 SQL + 应用层内存组装(禁 JOIN/子查询,IN 按 ≤100 分批)。金额字段一律整数分 int64。查询走 gdb.CacheOption 缓存(TTL 60s,CacheName 用表名前缀),写操作后按表前缀 ClearCache。
default(用户域)
| 表 | 关键字段 | 说明 |
|---|---|---|
| slogan_user | username/phone/password/role | 账号;username 唯一索引 |
| slogan_user_photo | user_id/type/url/status | type: 1 大头照 2 正面 3 侧面 4 背面;(user_id,type) 唯一,重复上传覆盖 |
| slogan_wardrobe_item | user_id/photo_url/name/category/season/style_tags/color_info | category: 上衣/下装/鞋/配饰 |
| slogan_body_measurement | user_id/height/weight/bust/waist/hip/shoulder/skin_tone/fit_params | 每用户一行 |
| slogan_avatar_model | user_id/glb_url/build_status/params_snapshot | 每用户一行;params_snapshot 存构建参数 JSON;GLB 由前端 three.js 直接加载 |
| slogan_scoring_rule | dimension/rule_type/rules_json/enabled | 评分规则(5 维权重、效果图限次),seed 数据 |
| slogan_partner_store | name/type/lat/lng/address/commission_policy | 合作门店,seed 4 家 |
plan(穿搭域)
| 表 | 关键字段 | 说明 |
|---|---|---|
| slogan_hairstyle_asset | name/style_tag/glb_url/thumb_url/applicable_face | 发型资产,seed 8 款,公开接口 |
| slogan_outfit_generation_task | user_id/start_date/end_date/location/weather_snapshot/status/error/model_name | 状态机:pending→planning→scoring→rendering→done/failed |
| slogan_outfit_plan | task_id/user_id/date_range/title/source/score/main_flag/hairstyle_id/occasion | source: wardrobe/recommend;每任务多方案 |
| slogan_plan_outfit_item | plan_id/slot/source/wardrobe_item_id/product_name/name/desc | slot: top/bottom/shoes/accessory 等 |
| slogan_plan_effect_image | plan_id/angle/url/status/prompt_snapshot | angle: front/side/back |
| slogan_plan_review | plan_id/user_id/action/note | action: fav/unfav |
pay(支付域)
| 表 | 关键字段 | 说明 |
|---|---|---|
| slogan_member_plan | name/price_fen/duration_days/features/status | 套餐配置;金额分 int64 |
| slogan_user_member | user_id/plan_id/expire_at/source | 每用户一行会员态 |
| slogan_payment_order | order_no/user_id/plan_id/amount_fen/channel/status/trade_no/notify_raw | status: pending/paid/failed;order_no 唯一 |
| slogan_pay_notify_log | order_no/body/sign/status | 回调日志(记录类豁免分层,由支付 service 事务内直写) |
| slogan_ad_reward_log | user_id/ad_type/reward_key/status | 广告激励领取流水,限频依据 |
cps(联盟域)
| 表 | 关键字段 | 说明 |
|---|---|---|
| slogan_cps_category | code/name/parent_code/source/source_cat_id/sort | 三源归一分类树 |
| slogan_cps_product | source/outer_id/category_code/name/cover_url/price_fen/shop_name/commission_rate/city/scene_tags/raw/status | (source,outer_id) 唯一;raw 存原始报文 |
| slogan_cps_click_log | user_id/source/outer_id/scene/plan_id/category_code/deeplink/ip | 点击流水 |
| slogan_scene_category_map | scene_type/occasion/source/category_code/priority | 业务场景→联盟分类映射 |
豁免分层规则(与 CLAUDE.md 一致):
- 完全豁免(无任何分层文件):
pay_notify_log(记录类,建表归 payment_order_dao init(),读写由支付 service 事务内直写) - 豁免 controller/dto(保留 entity/dao/service,无独立 HTTP 出入口):
plan_effect_image、plan_outfit_item(方案详情聚合透出)、scoring_rule(纯内部配置)、user_member(member/status 状态聚合透出)——禁止造无路由的空壳 controller/dto 门面 - 其余 17 表五层齐全,每层目录文件数 = 分层表数(21),非表文件不进业务分层目录
4. 核心流程设计
4.1 穿搭生成(slogan_outfit_generation_task 状态机)
POST /outfit/generate
→ 校验(日期不倒挂、衣橱 ≥3 件、task 唯一性)→ 落任务(pending) → 提交协程池
→ planning:和风天气(7天预报) + 高德地理编码(location→adcode) → 规则预筛 3 套候选(衣橱季节/风格匹配)
→ LLM 规划 1 次调用(OpenAI 兼容,temperature 0.8,超时 300s,重试 3 次)
→ scoring:规则引擎 5 维评分(天气 25 / 场合 25 / 色彩 20 / 完整度 20 / 风格 10,阈值 75)
→ 全低分 → LLM 兜底创作 1 次(recommend 来源方案)
→ 落库 outfit_plan + plan_outfit_item → done
- 任务状态经
GET /outfit/task/status轮询(前端 2s × 90);任何失败置 failed + 明确错误文案 - 服务重启时未完成任务标记 failed(
main.goStartWorker 恢复,避免重复消耗 LLM 费用) - 并行调度:任务提交走 common 协程池(大小来自 config
pool节点,consts 提供默认值),禁止裸 go
4.2 效果图(万相异步任务)
POST /outfit/plan/select-main → 置 main_flag → 触发 front/side/back 三张效果图任务(提交协程池)
→ 万相 wan2.7-image-pro 提交异步任务 → 轮询任务状态(5s × 60)→ 下载落盘 workspace/plan_effect/{user}/{plan}/{angle}.png → done
- 内容 hash 缓存 24h(同方案不重复出图)
- 每日限 3 次(
scoring_rule表effect_limit维度),超限返回明确错误;广告激励可补充次数(见 4.5) - prompt 由方案 item 描述组装(含发型/肤色/场景),prompt_snapshot 落库便于回溯
4.3 3D 化身(Tripo 图像转 3D)
POST /avatar/build(用户维度 WithLock 防重复构建)
→ 校验三视角照片齐全(type 2/3/4)+ config 已配 tripo_api_key
→ 落库 build_status=processing → 提交协程池
→ Tripo 图像转 3D(v2.5-20250123,多视角图 → GLB)→ 轮询任务(5s,上限 900s)
→ 下载 GLB 到 workspace/avatar/{user}/avatar.glb → done
- 构建结果经
GET /avatar/get轮询(前端 2s × 30);重启未完成任务标记 failed - 展示端为前端 three.js 渲染(H5):GLTFLoader 加载 GLB,包围盒居中 + 相机自适应,空闲自动旋转,触摸/鼠标拖拽全角度旋转;非 H5 端提示"数字人仅支持 H5 查看"
- GLB 路径:
workspace/avatar/{user}/avatar.glb
4.4 会员支付(虎皮棋聚合支付)
POST /member/order/create → 生成 order_no + 调 xunhu 下单(amount 分→元仅此对接处,协议要求)→ 返回 pay_url
前端打开 pay_url → 轮询 GET /member/order/status(2s × 30)
POST /member/order/notify(公开,裸文本 "success")→ 验签 → 按 order_no 幂等(已 paid 直接 success)
→ 事务:payment_order 置 paid + user_member upsert 会员态 + pay_notify_log 落日志
xunhu_appid/appsecret未配置 → 支付接口返回"支付功能未配置",前端降级隐藏充值入口- 回调重复投递幂等:以 order_no 为唯一键查重,重复直接返回 success
4.5 广告激励(限频)
POST /ad/reward/claim(ad_type: effect_extra / vip_trial)
→ 自然日限频:effect_extra 2 次 / vip_trial 1 次(ad_reward_log 当日计数)
→ 超限返回明确错误;effect_extra 增加当日效果图额度,vip_trial 发放体验会员
4.6 CPS 联盟
- 定时同步:
cps.sync_cron(默认0 4 * * *)拉取美团/京东/淘宝选品池,按 (source,outer_id) upsert,分类归一到 slogan_cps_category - 推荐链路:场景(发型卡/买同款/到店试穿/延伸优惠/找升级款/会员权益)→ scene_category_map → 分类 → 商品池筛选(city/scene_tags)
- 转链:
POST /cps/product/link调联盟转链接口 + cps_click_log 落点击流水(按 (user,outer_id,scene) 去重) - key 全空 → 联盟入口优雅降级隐藏(前端不渲染)
5. 关键技术决策
| 决策 | 方案 | 理由 |
|---|---|---|
| 存储 | SQLite 4 库分域 | 单机部署零运维;分域隔离写锁争用;文件即备份 |
| 金额 | 整数分 int64,common.RoundInt 四舍五入 |
杜绝浮点误差;前端 ÷100 展示 |
| 查询缓存 | gdb.CacheOption(TTL 60s,CacheName=表名前缀+参数),写后 ClearCache(表前缀) |
命中率高、清理精确;防"库里已改、查询旧值" |
| 并发 | 并行点三处落地:common 池封装 → consts 默认大小 → config pool 节点;用户维度互斥用 common.WithLock |
统一入口、可配置、防裸 go 失控 |
| 锁 | common.WithLock[T] 泛型:配置 redis → redis 锁(SET NX EX + token 删除),否则 gcache 内存锁;ErrLockHeld 重试,defer 释放 |
单机/多实例部署形态自适应 |
| 任务恢复 | 启动时未完成任务置 failed | 防重启后重复消耗外部服务费用 |
| 回调幂等 | 业务唯一键先查重再落库 | 支付回调/CPS 同步都可能重复投递 |
| 图片 URL | 相对路径 /workspace/...,前端 resolveUrl() 拼 BASE_URL |
部署地址变化前端一处配置 |
6. 风险与备选
- LLM 输出不稳定:评分引擎兜底 + 全低分走 recommend 创作;prompt 模板在 agent/ 集中维护
- 万相/Tripo 任务长耗时:异步任务 + 轮询;超时置 failed 可重试
- SQLite 并发写锁:写回主 goroutine 串行;分库降低跨域锁争用;必要时可切 WAL 或迁移 PG
- 缓存脏读:写后按表前缀清缓存全覆盖(含 Insert);冒烟回归验证改后读新值