git-subtree-dir: server git-subtree-mainline:c4e617ada7git-subtree-split:e64421295f
147 lines
4.8 KiB
Go
147 lines
4.8 KiB
Go
package service
|
||
|
||
import (
|
||
"context"
|
||
"crypto/md5"
|
||
"encoding/hex"
|
||
"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 := ScoringRuleService.EffectLimit(ctx)
|
||
if limit > 0 {
|
||
used, err := dao.PlanEffectImage.CountByUserToday(ctx, userId)
|
||
if err != nil {
|
||
g.Log().Warningf(ctx, "统计当日效果图数量失败(本次放行): %v", err)
|
||
used = 0
|
||
}
|
||
extra, err := dao.AdRewardLog.CountTodayByType(ctx, userId, consts.AdTypeEffectExtra)
|
||
if err != nil {
|
||
g.Log().Warningf(ctx, "统计广告奖励次数失败(本次放行): %v", err)
|
||
extra = 0
|
||
}
|
||
if used >= limit+extra {
|
||
g.Log().Warningf(ctx, "效果图任务: 用户 %d 当日次数已用尽(%d/%d)", userId, used, limit+extra)
|
||
return
|
||
}
|
||
}
|
||
}
|
||
|
||
if err := dao.OutfitGenTask.UpdateStatus(ctx, plan.TaskId, consts.TaskStatusRendering, ""); err != nil {
|
||
g.Log().Warningf(ctx, "更新任务 %d 为渲染中失败: %v", plan.TaskId, err)
|
||
}
|
||
defer func() {
|
||
if err := dao.OutfitGenTask.UpdateStatus(ctx, plan.TaskId, consts.TaskStatusDone, ""); err != nil {
|
||
g.Log().Warningf(ctx, "更新任务 %d 为完成失败: %v", plan.TaskId, err)
|
||
}
|
||
}()
|
||
|
||
items, err := dao.PlanOutfitItem.ListByPlan(ctx, planId)
|
||
if err != nil {
|
||
g.Log().Warningf(ctx, "读取方案单品失败(效果图描述将缺单品): %v", err)
|
||
}
|
||
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, err := agent.NewClient(g.Cfg().MustGet(ctx, "imagegen.supplier", "wanx").String())
|
||
if err != nil {
|
||
g.Log().Warningf(ctx, "效果图生成不可用: %v", err)
|
||
return
|
||
}
|
||
// 缓存命中记录批量落库(1 条 multi-row SQL),未命中逐个插入并生成
|
||
var cached []*entity.PlanEffectImage
|
||
var pending []*entity.PlanEffectImage
|
||
for _, angle := range effectAngles {
|
||
if url, ok := agent.CacheGet(effectCacheKey(plan, angle)); ok {
|
||
cached = append(cached, &entity.PlanEffectImage{
|
||
PlanId: planId, Angle: angle, Url: url, Status: consts.EffectStatusDone,
|
||
PromptSnapshot: planDesc,
|
||
})
|
||
} else {
|
||
pending = append(pending, &entity.PlanEffectImage{
|
||
PlanId: planId, Angle: angle, Status: consts.EffectStatusRendering,
|
||
PromptSnapshot: planDesc,
|
||
})
|
||
}
|
||
}
|
||
if err := dao.PlanEffectImage.InsertBatch(ctx, cached); err != nil {
|
||
g.Log().Warningf(ctx, "效果图批量落库失败: %v", err)
|
||
}
|
||
for i, rec := range pending {
|
||
recId, err := dao.PlanEffectImage.Insert(ctx, rec)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
url, err := client.Generate(ctx, &agent.GenerateReq{
|
||
BaseImageURL: baseImageURL, Prompt: planDesc, Angle: rec.Angle, Seed: plan.Id*100 + int64(i),
|
||
})
|
||
if err != nil {
|
||
g.Log().Warningf(ctx, "效果图生成失败 plan=%d angle=%s: %v", planId, rec.Angle, err)
|
||
if updErr := dao.PlanEffectImage.UpdateStatus(ctx, recId, consts.EffectStatusFailed, ""); updErr != nil {
|
||
g.Log().Warningf(ctx, "标记效果图失败状态失败: %v", updErr)
|
||
}
|
||
continue
|
||
}
|
||
agent.CacheSet(effectCacheKey(plan, rec.Angle), url)
|
||
if updErr := dao.PlanEffectImage.UpdateStatus(ctx, recId, consts.EffectStatusDone, url); updErr != nil {
|
||
g.Log().Warningf(ctx, "回写效果图完成状态失败: %v", updErr)
|
||
}
|
||
}
|
||
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[:])
|
||
}
|