134 lines
3.9 KiB
Go
134 lines
3.9 KiB
Go
package service
|
||
|
||
import (
|
||
"context"
|
||
"crypto/md5"
|
||
"encoding/hex"
|
||
"encoding/json"
|
||
"fmt"
|
||
"strings"
|
||
|
||
"slogan-agent/styleagent/agent"
|
||
"slogan-agent/styleagent/consts"
|
||
"slogan-agent/styleagent/dao"
|
||
"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
|
||
}
|
||
// 每日限额:VIP 不限;普通用户 = 基础额度 + 广告激励额外次数
|
||
if !dao.UserMember.IsVip(ctx, userId) {
|
||
limit := dailyEffectLimit(ctx)
|
||
if limit > 0 {
|
||
used, _ := dao.PlanEffectImage.CountByUserToday(ctx, userId)
|
||
extra, _ := dao.AdRewardLog.CountTodayByType(ctx, userId, consts.AdTypeEffectExtra)
|
||
if used >= limit+extra {
|
||
g.Log().Warningf(ctx, "效果图任务: 用户 %d 当日次数已用尽(%d/%d)", userId, used, limit+extra)
|
||
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 := agent.NewClient(g.Cfg().MustGet(ctx, "agent.supplier", "mock").String())
|
||
for i, angle := range effectAngles {
|
||
cacheKey := effectCacheKey(plan, angle)
|
||
if url, ok := agent.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, &agent.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
|
||
}
|
||
agent.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
|
||
}
|