Add 'server/' from commit 'e64421295fff83acbb6d6ab3d3b27f3ef8368f00'
git-subtree-dir: server git-subtree-mainline:c4e617ada7git-subtree-split:e64421295f
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,933 @@
|
||||
# slogan-agent MVP 实现计划
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 实现 slogan-agent 服务端 MVP:登录 → 照片/衣橱/身形管理 → 化身构建(v1 模板匹配)→ 穿搭生成(规则评分 + Agent 规划 + 兜底)→ 效果图按需生成,全链路可运行。
|
||||
|
||||
**Architecture:** Go 单体 + GoFrame v2 + SQLite,严格遵循 video-factory 分层规范(controller → service → dao),包级单例,RouteRegister 反射注册路由,JWT 鉴权,OpenAI 兼容 LLM。规则评分零 LLM 成本,效果图按需生成 + 缓存。
|
||||
|
||||
**Tech Stack:** Go 1.22+ / GoFrame v2.10 / SQLite / JWT / bcrypt / OpenAI 兼容 API / 和风天气 API
|
||||
|
||||
**参考代码(必须阅读后复制模式):**
|
||||
- `/Users/zhangbin/Desktop/d盘/work/video-factory/video-factory/common/`(http.go / auth.go / base_dao.go / cache.go / auth_middleware.go / util.go)
|
||||
- `/Users/zhangbin/Desktop/d盘/work/video-factory/video-factory/shortdrama/agent/chat_model.go`(直接复用整个文件,改包名)
|
||||
- `/Users/zhangbin/Desktop/d盘/work/video-factory/video-factory/shortdrama/`(controller/service/dao/model 全部模式)
|
||||
- `/Users/zhangbin/Desktop/d盘/work/video-factory/video-factory/main.go`(入口模式)
|
||||
|
||||
**数据库:** `slogan.db`(config.yml 配置),表名前缀 `slogan_`。所有 init() 自动建表 + ALTER 迁移兼容。
|
||||
|
||||
**模块路径:** `slogan-agent`(go.mod module name),业务包 `styleagent`。
|
||||
|
||||
---
|
||||
|
||||
## 数据库表总览(Task 2-3 建全)
|
||||
|
||||
| 表 | 关键字段 |
|
||||
|----|---------|
|
||||
| `slogan_user` | id, role(default 'user'), username, phone, password, name, created_at, updated_at |
|
||||
| `slogan_user_photo` | id, user_id, type(1大头照 2全身正面 3全身侧面 4全身背面), url, status, created_at |
|
||||
| `slogan_wardrobe_item` | id, user_id, photo_url, category(上衣/下装/鞋/配饰), season(春/夏/秋/冬/四季), style_tags, color_info, status, created_at |
|
||||
| `slogan_body_measurement` | id, user_id, height, weight, skin_tone(1-5), fit_params(JSON), updated_at |
|
||||
| `slogan_avatar_model` | id, user_id, face_template_id, body_template_id, skin_tone_index, face_texture_url, glb_url, build_status(pending/processing/done/failed), error, params_snapshot(JSON), created_at |
|
||||
| `slogan_hairstyle_asset` | id, name, style_tag, glb_url, thumb_url, applicable_face, sort |
|
||||
| `slogan_outfit_generation_task` | id, user_id, start_date, end_date, location, weather_snapshot(JSON), status(pending/planning/scoring/rendering/done/failed), error, model_name, created_at |
|
||||
| `slogan_outfit_plan` | id, task_id, user_id, date_range, location, source(wardrobe/recommend), score, main_flag(0/1), hairstyle_id, hair_color, weather_ref(JSON), created_at |
|
||||
| `slogan_plan_outfit_item` | id, plan_id, slot(发型/上衣/下装/鞋/配饰), source, wardrobe_item_id, product_name, name, desc |
|
||||
| `slogan_plan_effect_image` | id, plan_id, angle(正面/侧面/背面), url, status, prompt_snapshot, created_at |
|
||||
| `slogan_plan_review` | id, plan_id, user_id, action(fav/unfav), note, created_at |
|
||||
| `slogan_scoring_rule` | id, dimension, rule_type, rules_json, enabled, version |
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 项目骨架(go.mod / config / main.go / common 复制)
|
||||
|
||||
**Files:**
|
||||
- Create: `go.mod`
|
||||
- Create: `config.yml`
|
||||
- Create: `main.go`
|
||||
- Copy: `common/http.go`, `common/auth.go`, `common/base_dao.go`, `common/cache.go`, `common/util.go`, `common/auth_middleware.go`(从 video-factory 复制,改 package 注释即可,无需改逻辑)
|
||||
|
||||
- [ ] **Step 1: 创建 go.mod**
|
||||
|
||||
```bash
|
||||
cd slogan-agent
|
||||
go mod init slogan-agent
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 创建 config.yml**
|
||||
|
||||
```yaml
|
||||
database:
|
||||
default:
|
||||
name: slogan.db
|
||||
type: sqlite
|
||||
debug: false
|
||||
cache:
|
||||
ttl: 60
|
||||
server:
|
||||
address: :3007
|
||||
name: slogan
|
||||
workerId: 1
|
||||
clientMaxBodySize: 209715200
|
||||
requestTimeout: 3000
|
||||
chat:
|
||||
timeout: 300
|
||||
max_retries: 3
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 复制 common 包**
|
||||
|
||||
```bash
|
||||
cp /Users/zhangbin/Desktop/d盘/work/video-factory/video-factory/common/{http.go,auth.go,base_dao.go,cache.go,util.go,auth_middleware.go} common/
|
||||
```
|
||||
|
||||
注意:auth_middleware.go 和 cache.go / util.go 需检查是否有对 video-factory 特定包的 import,如有则调整。auth.go 中 jwtSecret 改为 slogan 自己的密钥。
|
||||
|
||||
- [ ] **Step 4: 创建 main.go**(模式同 video-factory main.go,路由表注册 controller,workspace 鉴权静态服务,端口 3007)
|
||||
|
||||
- [ ] **Step 5: 添加依赖并编译**
|
||||
|
||||
```bash
|
||||
go mod tidy
|
||||
go build ./...
|
||||
```
|
||||
|
||||
Expected: 编译通过(common 包复制可能依赖 gtime/gcache,tidy 解决)。
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add -A && git commit -m "feat: slogan-agent skeleton with common package"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: consts 与全部 entity
|
||||
|
||||
**Files:**
|
||||
- Create: `styleagent/consts/table_name.go`(全部表名常量)
|
||||
- Create: `styleagent/consts/status.go`(任务状态/照片类型/方案来源常量)
|
||||
- Create: `styleagent/model/entity/`(12 个文件:user.go, user_photo.go, wardrobe_item.go, body_measurement.go, avatar_model.go, hairstyle_asset.go, outfit_generation_task.go, outfit_plan.go, plan_outfit_item.go, plan_effect_image.go, plan_review.go, scoring_rule.go)
|
||||
|
||||
- [ ] **Step 1: consts/table_name.go**
|
||||
|
||||
```go
|
||||
package consts
|
||||
|
||||
const (
|
||||
TableNameUser = "slogan_user"
|
||||
TableNameUserPhoto = "slogan_user_photo"
|
||||
TableNameWardrobeItem = "slogan_wardrobe_item"
|
||||
TableNameBodyMeasurement = "slogan_body_measurement"
|
||||
TableNameAvatarModel = "slogan_avatar_model"
|
||||
TableNameHairstyleAsset = "slogan_hairstyle_asset"
|
||||
TableNameOutfitGenTask = "slogan_outfit_generation_task"
|
||||
TableNameOutfitPlan = "slogan_outfit_plan"
|
||||
TableNamePlanOutfitItem = "slogan_plan_outfit_item"
|
||||
TableNamePlanEffectImage = "slogan_plan_effect_image"
|
||||
TableNamePlanReview = "slogan_plan_review"
|
||||
TableNameScoringRule = "slogan_scoring_rule"
|
||||
)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: consts/status.go**
|
||||
|
||||
```go
|
||||
package consts
|
||||
|
||||
// 照片类型
|
||||
const (
|
||||
PhotoTypeHeadshot = 1 // 大头照
|
||||
PhotoTypeFullFront = 2 // 全身正面
|
||||
PhotoTypeFullSide = 3 // 全身侧面
|
||||
PhotoTypeFullBack = 4 // 全身背面
|
||||
)
|
||||
|
||||
// 生成任务状态
|
||||
const (
|
||||
TaskStatusPending = "pending"
|
||||
TaskStatusPlanning = "planning"
|
||||
TaskStatusScoring = "scoring"
|
||||
TaskStatusRendering = "rendering"
|
||||
TaskStatusDone = "done"
|
||||
TaskStatusFailed = "failed"
|
||||
)
|
||||
|
||||
// 方案来源
|
||||
const (
|
||||
PlanSourceWardrobe = "wardrobe"
|
||||
PlanSourceRecommend = "recommend"
|
||||
)
|
||||
|
||||
// 化身构建状态
|
||||
const (
|
||||
AvatarBuildPending = "pending"
|
||||
AvatarBuildProcessing = "processing"
|
||||
AvatarBuildDone = "done"
|
||||
AvatarBuildFailed = "failed"
|
||||
)
|
||||
|
||||
// 评分阈值(可被 scoring_rule 配置覆盖)
|
||||
const DefaultScoreThreshold = 75
|
||||
```
|
||||
|
||||
- [ ] **Step 3: entity 文件**(orm tag 模式同 video-factory entity/user.go;全部含 CreatedAt/UpdatedAt `*gtime.Time`;字段完全对齐 Task 表格总览)
|
||||
|
||||
- [ ] **Step 4: 编译检查** `go build ./...`
|
||||
|
||||
- [ ] **Step 5: Commit** `git add -A && git commit -m "feat: consts and entities"`
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 全部 DAO(init 自动建表)
|
||||
|
||||
**Files:**
|
||||
- Create: `styleagent/dao/user_dao.go`(完整示例,含建表 + CRUD + 缓存)
|
||||
- Create: 其余 11 个 dao 文件(user_photo / wardrobe_item / body_measurement / avatar_model / hairstyle_asset / outfit_generation_task / outfit_plan / plan_outfit_item / plan_effect_image / plan_review / scoring_rule)
|
||||
|
||||
- [ ] **Step 1: user_dao.go**(模式:video-factory dao/user_dao.go)
|
||||
|
||||
```go
|
||||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slogan-agent/common"
|
||||
"slogan-agent/styleagent/consts"
|
||||
"slogan-agent/styleagent/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gcache"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
var User = &userDao{}
|
||||
|
||||
type userDao struct{}
|
||||
|
||||
func init() {
|
||||
ctx := context.Background()
|
||||
_, err := g.DB().Exec(ctx, `CREATE TABLE IF NOT EXISTS `+consts.TableNameUser+` (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
role TEXT NOT NULL DEFAULT 'user',
|
||||
username TEXT NOT NULL DEFAULT '',
|
||||
phone TEXT NOT NULL DEFAULT '',
|
||||
password TEXT NOT NULL DEFAULT '',
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
created_at DATETIME DEFAULT (datetime('now','localtime')),
|
||||
updated_at DATETIME DEFAULT (datetime('now','localtime'))
|
||||
)`)
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "create user table failed: %v", err)
|
||||
}
|
||||
_, _ = g.DB().Exec(ctx, "CREATE UNIQUE INDEX IF NOT EXISTS idx_slogan_user_username ON "+consts.TableNameUser+"(username) WHERE username != ''")
|
||||
_, _ = g.DB().Exec(ctx, "CREATE UNIQUE INDEX IF NOT EXISTS idx_slogan_user_phone ON "+consts.TableNameUser+"(phone) WHERE phone != ''")
|
||||
}
|
||||
|
||||
// 方法:Insert / GetOne / GetByAccount / Update / UpdateFields(复制 video-factory user_dao 对应方法,表名换 consts.TableNameUser)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 其余 11 个 dao**:每个含 init() 建表 + 核心查询方法(按字段):ListByUser(user_photo/wardrobe_item 按 user_id 分页)、GetByUserAndType、GetByUser(avatar/body 单行)、ListByPlan(plan_outfit_item/plan_effect_image)、GetByTask(outfit_plan 列表)、ListAll(hairstyle_asset 按 sort)、GetEnabled(scoring_rule)、UpdateStatus(task 状态流转)
|
||||
|
||||
- [ ] **Step 3: 建表自检**(先写 dao 测试或直接启动临时 main 验证)
|
||||
|
||||
```bash
|
||||
go build ./... && go run main.go 2>&1 | head -5
|
||||
# 验证 slogan.db 生成且无建表错误日志
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Commit** `git add -A && git commit -m "feat: dao layer with auto table creation"`
|
||||
|
||||
---
|
||||
|
||||
### Task 4: 全部 DTO(请求/响应 + g.Meta 路由)
|
||||
|
||||
**Files:**
|
||||
- Create: `styleagent/model/dto/user_dto.go`(LoginReq/LoginRes/ProfileRes)
|
||||
- Create: `styleagent/model/dto/user_photo_dto.go`
|
||||
- Create: `styleagent/model/dto/wardrobe_dto.go`
|
||||
- Create: `styleagent/model/dto/body_measurement_dto.go`
|
||||
- Create: `styleagent/model/dto/avatar_dto.go`
|
||||
- Create: `styleagent/model/dto/hairstyle_dto.go`
|
||||
- Create: `styleagent/model/dto/outfit_dto.go`
|
||||
|
||||
- [ ] **Step 1: 关键 dto 内容**
|
||||
|
||||
```go
|
||||
// user_dto.go
|
||||
type LoginReq struct {
|
||||
g.Meta `path:"/login" method:"post" tags:"用户" summary:"登录"`
|
||||
Account string `v:"required" json:"account"`
|
||||
Password string `v:"required" json:"password"`
|
||||
}
|
||||
type LoginRes struct {
|
||||
Token string `json:"token"`
|
||||
User *LoginUser `json:"user"`
|
||||
}
|
||||
type LoginUser struct {
|
||||
Id int64 `json:"id"`
|
||||
Role string `json:"role"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// user_photo_dto.go
|
||||
type UserPhotoUploadReq struct {
|
||||
g.Meta `path:"/upload" method:"post" tags:"照片" summary:"上传照片"`
|
||||
Type int `v:"required|in:1,2,3,4" json:"type"`
|
||||
// 文件字段:GoFrame 自动绑定 upload 文件(r.GetUploadFile)
|
||||
}
|
||||
type UserPhotoUploadRes struct { Id int64 `json:"id"` }
|
||||
type UserPhotoListReq struct {
|
||||
g.Meta `path:"/list" method:"get" tags:"照片" summary:"照片列表"`
|
||||
Type int `json:"type"` // 可空
|
||||
}
|
||||
type UserPhotoListRes struct {
|
||||
List []*entity.UserPhoto `json:"list"`
|
||||
}
|
||||
type UserPhotoDeleteReq struct {
|
||||
g.Meta `path:"/delete" method:"post" tags:"照片" summary:"删除照片"`
|
||||
Id int64 `v:"required" json:"id"`
|
||||
}
|
||||
|
||||
// wardrobe_dto.go
|
||||
type WardrobeUploadReq struct {
|
||||
g.Meta `path:"/upload" method:"post" tags:"衣橱" summary:"上传服装"`
|
||||
Category string `v:"required" json:"category"`
|
||||
Season string `json:"season"`
|
||||
StyleTags string `json:"style_tags"`
|
||||
ColorInfo string `json:"color_info"`
|
||||
// 文件字段同上
|
||||
}
|
||||
type WardrobeListReq struct {
|
||||
g.Meta `path:"/list" method:"get" tags:"衣橱" summary:"衣橱列表"`
|
||||
Category string `json:"category"`
|
||||
}
|
||||
type WardrobeListRes struct { List []*entity.WardrobeItem `json:"list"` }
|
||||
type WardrobeUpdateReq struct {
|
||||
g.Meta `path:"/update" method:"post" tags:"衣橱" summary:"更新服装"`
|
||||
Id int64 `v:"required" json:"id"`
|
||||
Category string `json:"category"`
|
||||
Season string `json:"season"`
|
||||
StyleTags string `json:"style_tags"`
|
||||
}
|
||||
type WardrobeDeleteReq struct {
|
||||
g.Meta `path:"/delete" method:"post" tags:"衣橱" summary:"删除服装"`
|
||||
Id int64 `v:"required" json:"id"`
|
||||
}
|
||||
|
||||
// body_measurement_dto.go
|
||||
type BodyMeasurementSaveReq struct {
|
||||
g.Meta `path:"/save" method:"post" tags:"身形" summary:"保存身形参数"`
|
||||
Height int `json:"height"`
|
||||
Weight int `json:"weight"`
|
||||
SkinTone int `v:"in:1,2,3,4,5" json:"skin_tone"`
|
||||
FitParams string `json:"fit_params"`
|
||||
}
|
||||
type BodyMeasurementGetRes struct {
|
||||
Height int `json:"height"`
|
||||
Weight int `json:"weight"`
|
||||
SkinTone int `json:"skin_tone"`
|
||||
FitParams string `json:"fit_params"`
|
||||
}
|
||||
|
||||
// avatar_dto.go
|
||||
type AvatarBuildReq struct {
|
||||
g.Meta `path:"/build" method:"post" tags:"化身" summary:"构建化身"`
|
||||
}
|
||||
type AvatarBuildRes struct { TaskId int64 `json:"task_id"` }
|
||||
type AvatarGetRes struct {
|
||||
FaceTemplateId int `json:"face_template_id"`
|
||||
BodyTemplateId int `json:"body_template_id"`
|
||||
SkinToneIndex int `json:"skin_tone_index"`
|
||||
GlbUrl string `json:"glb_url"`
|
||||
BuildStatus string `json:"build_status"`
|
||||
}
|
||||
|
||||
// hairstyle_dto.go
|
||||
type HairstyleListRes struct { List []*entity.HairstyleAsset `json:"list"` }
|
||||
|
||||
// outfit_dto.go
|
||||
type OutfitGenerateReq struct {
|
||||
g.Meta `path:"/generate" method:"post" tags:"穿搭" summary:"生成穿搭方案"`
|
||||
StartDate string `v:"required|date" json:"start_date"`
|
||||
EndDate string `v:"required|date" json:"end_date"`
|
||||
Location string `v:"required" json:"location"`
|
||||
}
|
||||
type OutfitGenerateRes struct { TaskId int64 `json:"task_id"` }
|
||||
type OutfitTaskStatusReq struct {
|
||||
g.Meta `path:"/task/status" method:"get" tags:"穿搭" summary:"任务状态"`
|
||||
TaskId int64 `v:"required" json:"task_id"`
|
||||
}
|
||||
type OutfitTaskStatusRes struct {
|
||||
Status string `json:"status"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
type OutfitPlanListReq struct {
|
||||
g.Meta `path:"/plan/list" method:"get" tags:"穿搭" summary:"方案列表"`
|
||||
}
|
||||
type OutfitPlanListRes struct { List []*entity.OutfitPlan `json:"list"` }
|
||||
type OutfitPlanDetailReq struct {
|
||||
g.Meta `path:"/plan/detail" method:"get" tags:"穿搭" summary:"方案详情"`
|
||||
PlanId int64 `v:"required" json:"plan_id"`
|
||||
}
|
||||
type OutfitPlanDetailRes struct {
|
||||
Plan *entity.OutfitPlan `json:"plan"`
|
||||
Items []*entity.PlanOutfitItem `json:"items"`
|
||||
Images []*entity.PlanEffectImage `json:"images"`
|
||||
Hairstyle *entity.HairstyleAsset `json:"hairstyle,omitempty"`
|
||||
}
|
||||
type OutfitSelectMainReq struct {
|
||||
g.Meta `path:"/plan/select-main" method:"post" tags:"穿搭" summary:"选定主方案"`
|
||||
PlanId int64 `v:"required" json:"plan_id"`
|
||||
}
|
||||
type OutfitReviewReq struct {
|
||||
g.Meta `path:"/plan/review" method:"post" tags:"穿搭" summary:"方案反馈"`
|
||||
PlanId int64 `v:"required" json:"plan_id"`
|
||||
Action string `v:"required|in:fav,unfav" json:"action"`
|
||||
Note string `json:"note"`
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 编译检查** `go build ./...`(entity 引入路径)
|
||||
|
||||
- [ ] **Step 3: Commit** `git add -A && git commit -m "feat: dto layer with route metadata"`
|
||||
|
||||
---
|
||||
|
||||
### Task 5: 用户域(user controller + service)
|
||||
|
||||
**Files:**
|
||||
- Create: `styleagent/controller/user_controller.go`
|
||||
- Create: `styleagent/service/user_service.go`
|
||||
- Test: `styleagent/service/user_service_test.go`
|
||||
|
||||
- [ ] **Step 1: 写失败测试**
|
||||
|
||||
```go
|
||||
// user_service_test.go
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLoginSuccess(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
// 注册新用户
|
||||
userId, err := UserService.Register(ctx, "test_user_1", "password123")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, userId > 0)
|
||||
|
||||
_, token, err := UserService.Login(ctx, "test_user_1", "password123")
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, token)
|
||||
}
|
||||
|
||||
func TestLoginWrongPassword(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
_, _, err := UserService.Login(ctx, "test_user_1", "wrong")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 运行确认失败** `go test ./styleagent/service/ -run TestLogin -v`
|
||||
Expected: FAIL(编译失败/未定义 UserService)
|
||||
|
||||
- [ ] **Step 3: 实现 user_service.go**(复制 video-factory user_service.go 模式 + Register 方法,bcrypt 哈希密码,JWT 7 天;测试需要独立 DB —— 测试用 `test_slogan.db`,在 TestMain 中切换 g.DB 配置)
|
||||
|
||||
- [ ] **Step 4: 实现 user_controller.go**(Login / ChangePassword / Profile 三个方法绑定 dto)
|
||||
|
||||
- [ ] **Step 5: 运行确认通过** `go test ./styleagent/service/ -run TestLogin -v` → PASS
|
||||
|
||||
- [ ] **Step 6: Commit** `git add -A && git commit -m "feat: user domain login/register"`
|
||||
|
||||
---
|
||||
|
||||
### Task 6: 照片/衣橱/身形域(上传 + 列表 + 删除)
|
||||
|
||||
**Files:**
|
||||
- Create: `styleagent/controller/user_photo_controller.go`, `wardrobe_controller.go`, `body_measurement_controller.go`
|
||||
- Create: `styleagent/service/user_photo_service.go`, `wardrobe_service.go`, `body_measurement_service.go`
|
||||
- Create: `styleagent/service/file_storage.go`(文件保存封装:`SaveUploadedFile(file, subDir)` → `workspace/user_{id}/photos/xxx.jpg`,返回相对路径)
|
||||
|
||||
- [ ] **Step 1: file_storage.go**(模式:video-factory character_service 的文件保存逻辑)
|
||||
|
||||
```go
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
)
|
||||
|
||||
// SaveUploadedFile 保存上传文件到 workspace/{subDir},返回 "workspace/{subDir}/{filename}"
|
||||
func SaveUploadedFile(file *ghttp.UploadFile, subDir string) (string, error) {
|
||||
dir := filepath.Join("workspace", subDir)
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
filename := fmt.Sprintf("%d_%s", time.Now().UnixNano(), file.Filename)
|
||||
path := filepath.Join(dir, filename)
|
||||
if err := file.Save(path); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "/" + filepath.ToSlash(filepath.Join("workspace", subDir, filename)), nil
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 写失败测试**(user_photo:上传→列表→删除;wardrobe 同理;body:save→get 往返)
|
||||
|
||||
- [ ] **Step 3: 实现三个 service**:upload 校验(单张 ≤10MB、jpg/png/webp 扩展名校验)→ SaveUploadedFile → dao.Insert;list 按 user_id;delete 校验归属(id + user_id 双条件)后删除文件 + 记录
|
||||
|
||||
- [ ] **Step 4: 实现三个 controller**:Upload 方法用 `r.GetUploadFile("file")` 获取文件(controller 直接拿 request 时用 `*ghttp.Request` 参数)
|
||||
|
||||
```go
|
||||
func (c *userPhoto) Upload(ctx context.Context, req *dto.UserPhotoUploadReq, r *ghttp.Request) (res *dto.UserPhotoUploadRes, err error) {
|
||||
file := r.GetUploadFile("file")
|
||||
userId := common.GetUserId(ctx)
|
||||
url, err := service.UserPhotoService.Upload(ctx, userId, req.Type, file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dto.UserPhotoUploadRes{Id: url.Id}, nil
|
||||
}
|
||||
```
|
||||
|
||||
注意:GetUserId(ctx) 从 auth 中间件注入的 ctx 读取(auth_middleware.go 已有实现,按 video-factory 方式调用)。
|
||||
|
||||
- [ ] **Step 5: 测试通过** `go test ./styleagent/service/ -v`
|
||||
|
||||
- [ ] **Step 6: Commit** `git add -A && git commit -m "feat: photo/wardrobe/body domains"`
|
||||
|
||||
---
|
||||
|
||||
### Task 7: 化身域(v1 模板匹配 + build 任务)
|
||||
|
||||
**Files:**
|
||||
- Create: `styleagent/avatar/template_matcher.go`
|
||||
- Create: `styleagent/avatar/glb_packer.go`
|
||||
- Create: `styleagent/service/avatar_service.go`
|
||||
- Create: `styleagent/controller/avatar_controller.go`
|
||||
- Test: `styleagent/avatar/template_matcher_test.go`
|
||||
|
||||
- [ ] **Step 1: 写失败测试**(template_matcher:给定模拟特征(肤色 1-5 + 身高 cm + 胖瘦 1-5)→ 返回 face_template_id/body_template_id 索引)
|
||||
|
||||
```go
|
||||
package avatar
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestMatchTemplates(t *testing.T) {
|
||||
f := &FaceFeature{SkinTone: 3, HeightCm: 175, Build: 3}
|
||||
faceId, bodyId, skinIdx := MatchTemplates(f)
|
||||
if faceId < 1 || faceId > 20 || bodyId < 1 || bodyId > 6 || skinIdx < 1 || skinIdx > 5 {
|
||||
t.Fatalf("out of range: face=%d body=%d skin=%d", faceId, bodyId, skinIdx)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 确认失败** `go test ./styleagent/avatar/ -v`
|
||||
|
||||
- [ ] **Step 3: 实现 template_matcher.go**
|
||||
|
||||
```go
|
||||
package avatar
|
||||
|
||||
// FaceFeature 从照片+用户填写提取的化身特征(v1 简化:照片仅做肤色采样,其余用户填写/默认)
|
||||
type FaceFeature struct {
|
||||
SkinTone int // 1-5
|
||||
HeightCm int
|
||||
Build int // 1-5 瘦~胖
|
||||
}
|
||||
|
||||
// 预烘焙模板库索引(构建期产物,运行时只读常量)
|
||||
const (
|
||||
FaceTemplateCount = 20
|
||||
BodyTemplateCount = 6
|
||||
SkinToneLevels = 5
|
||||
DefaultFaceTemplate = 5
|
||||
DefaultBodyTemplate = 3
|
||||
)
|
||||
|
||||
// MatchTemplates 特征 → 模板索引(v1 规则映射:肤色→皮肤档,身高+体型→身体模板,脸型由照片后续 AI 提取后替换)
|
||||
func MatchTemplates(f *FaceFeature) (faceId, bodyId, skinIdx int) {
|
||||
if f == nil {
|
||||
return DefaultFaceTemplate, DefaultBodyTemplate, 3
|
||||
}
|
||||
skinIdx = f.SkinTone
|
||||
if skinIdx < 1 { skinIdx = 1 }
|
||||
if skinIdx > SkinToneLevels { skinIdx = SkinToneLevels }
|
||||
// 身体模板:身高 150-190 → 6 档
|
||||
bodyId = (f.HeightCm - 145) / 8
|
||||
if bodyId < 1 { bodyId = 1 }
|
||||
if bodyId > BodyTemplateCount { bodyId = BodyTemplateCount }
|
||||
// v1 脸型固定默认模板(AI 人脸特征提取后替换,见 spec v2)
|
||||
faceId = DefaultFaceTemplate
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: glb_packer.go**(v1:拼 URL —— `/workspace/templates/face_{id}.glb`、`body_{id}.glb`,组合 avatar GLB 记录;真实打包后续)
|
||||
|
||||
- [ ] **Step 5: avatar_service.go**:Build(ctx, userId):检查照片齐备(大头照+至少1张全身)→ 读 body_measurement → MatchTemplates → 插入 avatar_model(build_status=pending)→ 异步 goroutine 执行 processing → done(v1 同步简化:直接 done + glb_url 用 packer 生成的路径);Get(ctx, userId) 返回最新 avatar_model
|
||||
|
||||
- [ ] **Step 6: avatar_controller.go**:Build/Get 绑定 dto;Build 返回 task 语义(v1 直接返回 avatar 记录 id)
|
||||
|
||||
- [ ] **Step 7: 测试通过** + `go build ./...` + **Commit** `git commit -m "feat: avatar domain with template matching"`
|
||||
|
||||
---
|
||||
|
||||
### Task 8: 发型资产列表(静态 seed)
|
||||
|
||||
**Files:**
|
||||
- Create: `styleagent/controller/hairstyle_controller.go`
|
||||
- Create: `styleagent/service/hairstyle_service.go`
|
||||
- Modify: `styleagent/dao/hairstyle_asset_dao.go`(init 时 seed 8 个默认发型)
|
||||
|
||||
- [ ] **Step 1: dao init seed**(插入 8 条:短发/中发/长发/卷发/寸头/马尾/丸子头/波浪卷,style_tag、glb_url=`/workspace/templates/hairstyle_{id}.glb`、sort)
|
||||
|
||||
- [ ] **Step 2: 测试**:List 返回按 sort 排序的 8 条(dao 测试)
|
||||
|
||||
- [ ] **Step 3: service + controller 绑定**,`go build ./...`,**Commit**
|
||||
|
||||
---
|
||||
|
||||
### Task 9: 规则引擎评分(5 维度,零 LLM)
|
||||
|
||||
**Files:**
|
||||
- Create: `styleagent/scoring/rules.go`(ScoreContext + CandidateOutfit)
|
||||
- Create: `styleagent/scoring/weather_rule.go`
|
||||
- Create: `styleagent/scoring/occasion_rule.go`
|
||||
- Create: `styleagent/scoring/color_rule.go`
|
||||
- Create: `styleagent/scoring/completeness_rule.go`
|
||||
- Create: `styleagent/scoring/style_rule.go`
|
||||
- Create: `styleagent/scoring/engine.go`(总分聚合 + 阈值判定)
|
||||
- Test: `styleagent/scoring/engine_test.go`
|
||||
|
||||
- [ ] **Step 1: 写失败测试**(关键边界:冬季温度带外套得分高于无外套;色调和谐组合得分高于冲突组合;缺鞋减分;总分 ≥ 阈值判定通过)
|
||||
|
||||
```go
|
||||
package scoring
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestWinterOuterwearBonus(t *testing.T) {
|
||||
ctx := ScoreContext{
|
||||
TempAvg: 5, // 冬季
|
||||
Occasion: "通勤",
|
||||
Weekday: "workday",
|
||||
Wardrobe: []WardrobeItem{{Category: "上衣", ColorInfo: "#333333"}, {Category: "下装", ColorInfo: "#1a1a1a"}},
|
||||
}
|
||||
withJacket := CandidateOutfit{Items: ctx.Wardrobe, HasOuterwear: true}
|
||||
noJacket := CandidateOutfit{Items: ctx.Wardrobe, HasOuterwear: false}
|
||||
if weatherScore(withJacket) <= weatherScore(noJacket) {
|
||||
t.Fatal("winter should favor outerwear")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 确认失败** `go test ./styleagent/scoring/ -v`
|
||||
|
||||
- [ ] **Step 3: 实现 5 个规则文件**(均为纯函数,输入输出确定):
|
||||
|
||||
```go
|
||||
// rules.go 公共类型
|
||||
type WardrobeItem struct {
|
||||
Category string // 上衣/下装/鞋/配饰
|
||||
Season string // 春/夏/秋/冬/四季
|
||||
ColorInfo string // 如 #RRGGBB 或 "黑/白/红"
|
||||
StyleTags string
|
||||
}
|
||||
type CandidateOutfit struct {
|
||||
Items []WardrobeItem
|
||||
HasOuterwear bool
|
||||
}
|
||||
type ScoreContext struct {
|
||||
TempAvg int // 平均温度℃
|
||||
Season string
|
||||
Occasion string // 通勤/约会/聚会/运动
|
||||
Weekday string // workday/weekend/holiday
|
||||
StyleTags []string // 用户偏好
|
||||
}
|
||||
|
||||
// weather_rule.go 温度档位表
|
||||
func weatherScore(o CandidateOutfit, ctx ScoreContext) int {
|
||||
// 25 分制:温度匹配每件服装 season 加 5 分;<10℃ 无外套扣 10 分;>30℃ 有外套扣 8 分
|
||||
}
|
||||
|
||||
// occasion_rule.go 场合规则表
|
||||
func occasionScore(o CandidateOutfit, ctx ScoreContext) int {
|
||||
// 25 分制:场合→类别规则(约会加分:正装/裙装;运动加分:运动服)基础分 15 + 匹配项各 5
|
||||
}
|
||||
|
||||
// color_rule.go 色相环相似度
|
||||
func colorScore(o CandidateOutfit) int {
|
||||
// 20 分制:同色系 20;邻近色 15;对比色 8;随机冲突 3
|
||||
}
|
||||
|
||||
// completeness_rule.go
|
||||
func completenessScore(o CandidateOutfit) int {
|
||||
// 20 分制:上衣+5 下装+5 鞋+5 配饰+5
|
||||
}
|
||||
|
||||
// style_rule.go 用户偏好
|
||||
func styleScore(o CandidateOutfit, ctx ScoreContext) int {
|
||||
// 10 分制:命中用户 styleTags 每项 +2
|
||||
}
|
||||
|
||||
// engine.go
|
||||
func Score(c *CandidateOutfit, ctx *ScoreContext) int {
|
||||
return weatherScore(*c, *ctx) + occasionScore(*c, *ctx) + colorScore(*c) +
|
||||
completenessScore(*c) + styleScore(*c, *ctx)
|
||||
}
|
||||
func IsPass(score int, threshold int) bool { return score >= threshold }
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 测试通过**(含色相解析测试:`#ff0000` 与 `#ff6666` 同色系;`#ff0000` 与 `#00ff00` 对比色)
|
||||
|
||||
- [ ] **Step 5: Commit** `git add -A && git commit -m "feat: rule-based scoring engine"`
|
||||
|
||||
---
|
||||
|
||||
### Task 10: 天气适配(和风 + 高德 + 缓存)
|
||||
|
||||
**Files:**
|
||||
- Create: `styleagent/weather/qweather.go`
|
||||
- Create: `styleagent/weather/geo.go`
|
||||
- Create: `styleagent/weather/cache.go`
|
||||
- Create: `styleagent/service/weather_service.go`(供 outfit service 调用)
|
||||
- Test: `styleagent/weather/cache_test.go`
|
||||
|
||||
- [ ] **Step 1: 写失败测试**(cache:get→miss→set→hit;TTL 过期)
|
||||
|
||||
- [ ] **Step 2: 实现 cache.go**(内存 map + mutex,key=`{city}:{date}`,TTL 6h)
|
||||
|
||||
- [ ] **Step 3: 实现 qweather.go**:`GetDaily(ctx, cityCode, startDate, endDate) ([]DayWeather, error)`,和风 `v7/weather/7d` 接口,Key 从 `config.yml` 的 `weather.qweather_key` 读取(空则返回 error 提示配置缺失)
|
||||
|
||||
```go
|
||||
type DayWeather struct {
|
||||
Date string `json:"date"`
|
||||
TempMax int `json:"temp_max"`
|
||||
TempMin int `json:"temp_min"`
|
||||
TextDay string `json:"text_day"`
|
||||
}
|
||||
|
||||
// 返回该日期范围内平均温度(用于评分)+ 每日天气
|
||||
func GetDaily(ctx context.Context, cityCode string, startDate, endDate string) (*WeatherResult, error)
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 实现 geo.go**:`GetCityCode(ctx, location) (string, error)` —— 高德地理编码 API,Key 从配置读;失败时降级:直接以 location 为 cityCode 缓存并返回默认天气(config 开启 mock 时)
|
||||
|
||||
- [ ] **Step 5: weather_service.go**:封装 `GetWeather(ctx, location, startDate, endDate)` → 先查缓存 → 未命中调 API → 存缓存;测试用 mock API 响应(httptest server 或注入接口)
|
||||
|
||||
- [ ] **Step 6: 测试通过** + **Commit** `git commit -m "feat: weather adapter with cache"`
|
||||
|
||||
---
|
||||
|
||||
### Task 11: Agent(chat_model 复用 + 方案规划/兜底)
|
||||
|
||||
**Files:**
|
||||
- Copy: `styleagent/agent/chat_model.go`(从 video-factory 复制,改 import 路径)
|
||||
- Copy: `styleagent/agent/types.go`(ChatRequest/ChatMessage/ChatResponse/ToolCall)
|
||||
- Create: `styleagent/agent/outfit_agent.go`(规划 + 兜底两函数)
|
||||
- Create: `styleagent/agent/output.go`(JSON Schema 校验)
|
||||
- Create: `styleagent/agent/agent_config.go`(从 model_config 表读取 LLM 配置,未配置时返回错误)
|
||||
- Test: `styleagent/agent/output_test.go`
|
||||
|
||||
- [ ] **Step 1: 复制 chat_model.go + types.go**,改包路径,`go build ./...` 通过
|
||||
|
||||
- [ ] **Step 2: 写失败测试**(output 解析:合法 JSON 解析为 PlanOutput;缺字段报错;非法 JSON 报错)
|
||||
|
||||
```go
|
||||
// output.go 规划输出结构
|
||||
type PlanOutput struct {
|
||||
Plans []PlanCandidate `json:"plans"`
|
||||
}
|
||||
type PlanCandidate struct {
|
||||
Title string `json:"title"`
|
||||
Hairstyle string `json:"hairstyle"` // 发型名称(匹配资产库)
|
||||
HairColor string `json:"hair_color"` // 如 #A0522D
|
||||
Items []PlanItemOut `json:"items"`
|
||||
}
|
||||
type PlanItemOut struct {
|
||||
Slot string `json:"slot"` // 上衣/下装/鞋/配饰
|
||||
ItemId int64 `json:"item_id,omitempty"` // 衣橱条目(wardrobe 来源)
|
||||
Name string `json:"name"`
|
||||
Desc string `json:"desc"`
|
||||
NewItem bool `json:"new_item"` // 是否为推荐新服装
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 确认失败** `go test ./styleagent/agent/ -v`
|
||||
|
||||
- [ ] **Step 4: 实现 output.go 校验**(json.Unmarshal + 必填字段检查:plans 非空、每套 items 至少 1 件)
|
||||
|
||||
- [ ] **Step 5: 实现 outfit_agent.go**:
|
||||
|
||||
```go
|
||||
// PlanOutfits 规则预筛候选 → LLM 润色规划(1 次调用)
|
||||
func PlanOutfits(ctx context.Context, cfg *ModelConfig, sysPrompt, userInput string, candidates []CandidateData) (*PlanOutput, error)
|
||||
|
||||
// CreateRecommendPlan 兜底创作(全低分时调用,1 次调用)
|
||||
func CreateRecommendPlan(ctx context.Context, cfg *ModelConfig, sysPrompt, userInput string) (*PlanOutput, error)
|
||||
```
|
||||
|
||||
system prompt 要点(写入 agent/prompt.go 常量):角色是穿搭顾问;输出严格 JSON;天气/场合约束注入;仅输出 JSON 无额外文字。
|
||||
|
||||
- [ ] **Step 6: agent_config.go**:从 `model_config` 表(复用 video-factory 结构:system 配置 + 可覆盖)读取 base_url/api_key/model_name,用 gcache 缓存 60s;未配置返回明确错误。
|
||||
|
||||
- [ ] **Step 7: 测试通过**(output 校验单测;agent 调用用 httptest mock OpenAI 端点)+ **Commit**
|
||||
|
||||
---
|
||||
|
||||
### Task 12: 穿搭生成编排(outfit service 核心)
|
||||
|
||||
**Files:**
|
||||
- Create: `styleagent/service/outfit_service.go`(Generate 编排 + 评分 + 兜底 + 落库)
|
||||
- Test: `styleagent/service/outfit_service_test.go`(核心逻辑 mock:weather/agent 注入接口)
|
||||
|
||||
- [ ] **Step 1: 写失败测试**(核心流程:衣橱 3 件 → 规则预筛 3 套 → 评分 → 全低分时触发兜底 → 落库 plan + items;高分时直接落库)
|
||||
|
||||
- [ ] **Step 2: 确认失败**
|
||||
|
||||
- [ ] **Step 3: 实现 outfit_service.go**
|
||||
|
||||
```go
|
||||
type outfitService struct{}
|
||||
var OutfitService = new(outfitService)
|
||||
|
||||
// Generate 创建生成任务并同步执行核心流程(v1 同步;异步任务表见 Task 13)
|
||||
func (s *outfitService) Generate(ctx context.Context, userId int64, req *dto.OutfitGenerateReq) (int64, error) {
|
||||
// 1. 校验衣橱非空(<3 件返回 "衣橱服装不足,请先添加至少 3 件服装")
|
||||
// 2. 天气获取(weather_service)
|
||||
// 3. 规则预筛:衣橱 × 季节温度 × 场合 → 3 套候选(组合算法:按 category 分组随机/轮询组合)
|
||||
// 4. 创建任务记录(planning)→ Agent.PlanOutfits(1 次 LLM)
|
||||
// 5. 规则评分每套 → 任务状态 scoring
|
||||
// 6. 3 套全 < 阈值 → Agent.CreateRecommendPlan(1 次 LLM)→ 新套装标 recommend
|
||||
// 7. 落库 outfit_plan(hairstyle_id 匹配资产库)+ plan_outfit_item(来源标注)
|
||||
// 8. 任务 → done;返回 task_id
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 预筛组合算法**(outfit_combiner.go):按 category 将衣橱分组,按温度过滤 season,生成最多 3 个互不相同组合(确定性:按 id 排序轮询),每个组合带 HasOuterwear 标记
|
||||
|
||||
- [ ] **Step 5: 测试通过** + `go build ./...` + **Commit**
|
||||
|
||||
---
|
||||
|
||||
### Task 13: 穿搭 controller + 异步任务表
|
||||
|
||||
**Files:**
|
||||
- Create: `styleagent/controller/outfit_controller.go`
|
||||
- Modify: `styleagent/service/outfit_service.go`(异步化:Generate 只建任务返回 task_id,worker goroutine 执行;GetTaskStatus / ListPlans / GetPlanDetail / SelectMain / Review)
|
||||
|
||||
- [ ] **Step 1: 异步化改造**:Generate 插入任务(pending)→ 启动 goroutine 执行核心流程(含任务状态流转 pending→planning→scoring→done/failed + error 记录);`startWorker(ctx)` 守护恢复未完成任务(main.go 启动时调用,模式同 video-factory StartVideoPoller)
|
||||
|
||||
- [ ] **Step 2: controller 绑定 6 个 dto 方法**(Generate/TaskStatus/PlanList/PlanDetail/SelectMain/Review)
|
||||
|
||||
- [ ] **Step 3: GetPlanDetail**:查 plan + items + images + hairstyle 资产,组装 OutfitPlanDetailRes
|
||||
|
||||
- [ ] **Step 4: SelectMain**:置 main_flag(事务:同 task 其他 plan 清零)+ 触发效果图任务(Task 14 后接通)
|
||||
|
||||
- [ ] **Step 5: 编译 + 冒烟测试**(TestMain 起 gtest server:登录 → 上传 → generate → 轮询 → detail)**Commit**
|
||||
|
||||
---
|
||||
|
||||
### Task 14: 效果图生成(ImageGenClient 接口 + wanx + mock + 缓存)
|
||||
|
||||
**Files:**
|
||||
- Create: `styleagent/imagegen/client.go`(接口 + Factory)
|
||||
- Create: `styleagent/imagegen/wanx_client.go`
|
||||
- Create: `styleagent/imagegen/mock_client.go`
|
||||
- Create: `styleagent/imagegen/cache.go`
|
||||
- Create: `styleagent/service/effect_image_service.go`(异步任务执行:选主方案后生成 3 视角)
|
||||
- Test: `styleagent/imagegen/cache_test.go` + `mock_client_test.go`
|
||||
|
||||
- [ ] **Step 1: 写失败测试**(cache:plan 内容 hash → 命中/未命中;mock client:调用返回固定 URL)
|
||||
|
||||
- [ ] **Step 2: 实现 client.go**
|
||||
|
||||
```go
|
||||
type ImageGenClient interface {
|
||||
// Generate 生成单张效果图,返回图片 URL
|
||||
Generate(ctx context.Context, req *GenerateReq) (string, error)
|
||||
}
|
||||
type GenerateReq struct {
|
||||
BaseImageURL string // 用户全身照
|
||||
Prompt string // 方案描述
|
||||
Angle string // 正面/侧面/背面
|
||||
Seed int64
|
||||
}
|
||||
func NewClient(supplier string) ImageGenClient // wanx | mock(config 无 key 时强制 mock)
|
||||
```
|
||||
|
||||
- [ ] **Step 3: mock_client.go**:返回 `/workspace/mock/effect_{angle}.png` 占位路径(不真实调用,开发联调用)
|
||||
|
||||
- [ ] **Step 4: wanx_client.go**:通义万相人像写真类 API(`image-sync` 或异步轮询接口),Key/模型从 `imagegen_config` 表读;v1 实现为"调用 + 轮询结果"封装;错误降级 mock
|
||||
|
||||
- [ ] **Step 5: effect_image_service.go**:SelectMain 后 goroutine:按 plan 内容 hash 查缓存 → 未命中调用 ImageGenClient 逐角度生成(3 张)→ 存 plan_effect_image + 任务 rendering→done;每日免费次数校验(user 维度,默认 3 次/天,scoring_rule 表配置)
|
||||
|
||||
- [ ] **Step 6: 测试通过** + **Commit**
|
||||
|
||||
---
|
||||
|
||||
### Task 14.5: 商业化基础(partner_store 列表 + seed)
|
||||
|
||||
**Files:**
|
||||
- Create: `styleagent/model/entity/partner_store.go`(id, name, type(1形象设计 2服装门店), lat, lng, address, commission_policy, status, created_at)
|
||||
- Modify: `styleagent/consts/table_name.go`(+`TableNamePartnerStore = "slogan_partner_store"`)
|
||||
- Create: `styleagent/dao/partner_store_dao.go`(建表 + init seed 4 条示例门店 + ListByType)
|
||||
- Create: `styleagent/model/dto/partner_store_dto.go`(`StoreListReq` path `/list` + `StoreListRes{List []*entity.PartnerStore}`)
|
||||
- Create: `styleagent/service/partner_store_service.go`
|
||||
- Create: `styleagent/controller/partner_store_controller.go`
|
||||
- Modify: `main.go`(注册 `controller.PartnerStore`)
|
||||
|
||||
- [ ] **Step 1: entity + dao**(模式同 Task 3;seed:2 条形象设计 + 2 条服装门店,坐标覆盖城市)
|
||||
|
||||
- [ ] **Step 2: dto + service + controller**(List 支持 `type` 筛选,0 返回全部)
|
||||
|
||||
- [ ] **Step 3: `go build ./...` + 冒烟**(GET /partner-store/list 返回 seed 数据)+ **Commit** `git commit -m "feat: partner store domain"`
|
||||
|
||||
---
|
||||
|
||||
### Task 15: 集成冒烟 + Dockerfile
|
||||
|
||||
**Files:**
|
||||
- Create: `Dockerfile`(复用 video-factory 多阶段构建模式)
|
||||
- Create: `docs/项目文档.md`(服务端文档,模式同 video-factory 项目文档)
|
||||
- Create: `docs/api.json` 导出(启动后 GoFrame OpenAPI)
|
||||
|
||||
- [ ] **Step 1: Dockerfile**(golang:1.22 builder + alpine runtime,复制 video-factory Dockerfile 改端口)
|
||||
|
||||
- [ ] **Step 2: 全链路冒烟**:`go run main.go` → curl 全流程:
|
||||
1. `POST /user/login`(注册后)→ token
|
||||
2. `POST /user-photo/upload`(-F file=@headshot.jpg -F type=1)
|
||||
3. `POST /wardrobe/upload` × 3
|
||||
4. `POST /body-measurement/save`
|
||||
5. `POST /avatar/build` → get
|
||||
6. `POST /outfit/generate` → task status 轮询 → done
|
||||
7. `GET /outfit/plan/list` → detail
|
||||
8. `POST /outfit/plan/select-main` → effect images(mock 路径)
|
||||
9. `GET /hairstyle/list`
|
||||
|
||||
- [ ] **Step 3: 验证响应格式统一** `{"code":0,"message":"OK","data":...}`
|
||||
|
||||
- [ ] **Step 4: Commit** `git commit -m "feat: mvp complete with dockerfile and docs"`
|
||||
|
||||
---
|
||||
|
||||
## Self-Review 备注(执行前已知项)
|
||||
|
||||
- 测试 DB:`styleagent/service` 单测使用独立 sqlite 文件 `test_slogan.db`(TestMain 设置),避免污染开发库
|
||||
- GetUserId(ctx):确认 auth_middleware.go 注入的 key(复制 video-factory 后保持一致)
|
||||
- 上传文件字段名统一 `file`
|
||||
- 天气/LLM/图像 Key 全部从 config.yml / 配置表读取,代码不入 Key
|
||||
Reference in New Issue
Block a user