Files
slogan/styleagent/service/effect_image_service.go
T
adminandClaude Opus 4.7 ef22f7672a feat: slogan-agent MVP 服务端完整实现
- 用户域:注册/登录(JWT)/修改密码/个人资料
- 照片/衣橱/身形/化身:上传存储 + 3D 化身模板匹配
- 穿搭生成:天气(高德+和风+缓存) → 规则预筛 → LLM 规划(1次调用)
  → 规则评分(5维100分制) → 全低分触发 LLM 兜底创作 → 异步任务状态机
- 效果图:选主方案后异步生成 3 视角(mock/wanx 供应商 + 内容 hash 缓存 + 每日限额)
- 商业化:合作门店列表(seed 4 家)
- 冒烟:全链路端到端验证通过(mock LLM/天气),23 个 API 端点

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-31 12:15:11 +08:00

131 lines
3.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"context"
"crypto/md5"
"encoding/hex"
"encoding/json"
"fmt"
"strings"
"slogan-agent/styleagent/consts"
"slogan-agent/styleagent/dao"
"slogan-agent/styleagent/imagegen"
"slogan-agent/styleagent/model/entity"
"github.com/gogf/gf/v2/frame/g"
)
type effectImageService struct{}
var EffectImageService = new(effectImageService)
var effectAngles = []string{"正面", "侧面", "背面"}
// GenerateForPlan 选定主方案后异步生成 3 视角效果图
func (s *effectImageService) GenerateForPlan(ctx context.Context, planId, userId int64) {
go s.run(ctx, planId, userId)
}
func (s *effectImageService) run(ctx context.Context, planId, userId int64) {
plan, err := dao.OutfitPlan.GetOne(ctx, planId, userId)
if err != nil || plan == nil {
g.Log().Errorf(ctx, "效果图任务: 方案不存在 planId=%d", planId)
return
}
// 每日免费次数校验
limit := dailyEffectLimit(ctx)
if limit > 0 {
used, _ := dao.PlanEffectImage.CountByUserToday(ctx, userId)
if used >= limit {
g.Log().Warningf(ctx, "效果图任务: 用户 %d 当日次数已用尽(%d/%d", userId, used, limit)
return
}
}
_ = dao.OutfitGenTask.UpdateStatus(ctx, plan.TaskId, consts.TaskStatusRendering, "")
defer dao.OutfitGenTask.UpdateStatus(ctx, plan.TaskId, consts.TaskStatusDone, "")
items, _ := dao.PlanOutfitItem.ListByPlan(ctx, planId)
planDesc := planTitleDesc(plan.Title, items)
// 用户全身正面照作 base image
baseImageURL := ""
if photos, err := dao.UserPhoto.ListByUser(ctx, userId, 0); err == nil {
for _, p := range photos {
if p.Type == consts.PhotoTypeFullFront {
baseImageURL = p.Url
break
}
}
}
client := imagegen.NewClient(g.Cfg().MustGet(ctx, "imagegen.supplier", "mock").String())
for i, angle := range effectAngles {
cacheKey := effectCacheKey(plan, angle)
if url, ok := imagegen.CacheGet(cacheKey); ok {
_, _ = dao.PlanEffectImage.Insert(ctx, &entity.PlanEffectImage{
PlanId: planId, Angle: angle, Url: url, Status: consts.EffectStatusDone,
PromptSnapshot: planDesc,
})
continue
}
recId, err := dao.PlanEffectImage.Insert(ctx, &entity.PlanEffectImage{
PlanId: planId, Angle: angle, Status: consts.EffectStatusRendering,
PromptSnapshot: planDesc,
})
if err != nil {
continue
}
url, err := client.Generate(ctx, &imagegen.GenerateReq{
BaseImageURL: baseImageURL, Prompt: planDesc, Angle: angle, Seed: plan.Id*100 + int64(i),
})
if err != nil {
g.Log().Warningf(ctx, "效果图生成失败 plan=%d angle=%s: %v", planId, angle, err)
_ = dao.PlanEffectImage.UpdateStatus(ctx, recId, consts.EffectStatusFailed, "")
continue
}
imagegen.CacheSet(cacheKey, url)
_ = dao.PlanEffectImage.UpdateStatus(ctx, recId, consts.EffectStatusDone, url)
}
g.Log().Infof(ctx, "方案 %d 效果图生成完成", planId)
}
func planTitleDesc(title string, items []*entity.PlanOutfitItem) string {
var sb strings.Builder
sb.WriteString("方案:")
sb.WriteString(title)
sb.WriteString("")
for _, it := range items {
sb.WriteString(it.Slot)
sb.WriteString("")
sb.WriteString(it.Name)
sb.WriteString("")
}
return strings.TrimSuffix(sb.String(), "")
}
func effectCacheKey(plan *entity.OutfitPlan, angle string) string {
sum := md5.Sum([]byte(fmt.Sprintf("%d:%s:%s", plan.Id, plan.Title, angle)))
return "plan:" + hex.EncodeToString(sum[:])
}
func dailyEffectLimit(ctx context.Context) int {
limit := consts.DefaultDailyEffectLimit
rules, err := dao.ScoringRule.ListEnabled(ctx)
if err != nil {
return limit
}
for _, r := range rules {
if r.Dimension == "effect_limit" {
var v struct {
Daily int `json:"daily"`
}
if json.Unmarshal([]byte(r.RulesJson), &v) == nil && v.Daily > 0 {
return v.Daily
}
}
}
return limit
}