Files
slogan/server/styleagent/service/plan_effect_image_service.go
T
2026-08-17 13:19:15 +08:00

175 lines
5.8 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"
"fmt"
"strings"
"slogan-agent/common"
"slogan-agent/styleagent/agent"
"slogan-agent/styleagent/consts"
"slogan-agent/styleagent/dao"
"slogan-agent/styleagent/model/dto"
"slogan-agent/styleagent/model/entity"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
)
type effectImageService struct{}
var EffectImageService = new(effectImageService)
var effectAngles = []string{"正面", "侧面", "背面"}
// GenerateForPlan 选定主方案后异步生成 3 视角效果图
func (s *effectImageService) GenerateForPlan(ctx context.Context, planId, userId int64) {
if err := common.Submit(gctx.New(), "effect", consts.DefaultEffectPoolSize, func(ctx context.Context) {
s.run(ctx, planId, userId)
}); err != nil {
g.Log().Warningf(ctx, "提交效果图任务到协程池失败: %v", err)
}
}
// ListByPlan 方案效果图列表(方案详情组装用)
func (s *effectImageService) ListByPlan(ctx context.Context, planId int64) ([]*dto.PlanEffectImageItem, error) {
list, err := dao.PlanEffectImage.ListByPlan(ctx, planId)
if err != nil {
return nil, err
}
out := make([]*dto.PlanEffectImageItem, 0, len(list))
for _, img := range list {
out = append(out, toPlanEffectImageItem(img))
}
return out, nil
}
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 := PlanOutfitItemService.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 []*dto.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 toPlanEffectImageItem(img *entity.PlanEffectImage) *dto.PlanEffectImageItem {
return &dto.PlanEffectImageItem{
Id: img.Id, PlanId: img.PlanId, Angle: img.Angle, Url: img.Url,
Status: img.Status, PromptSnapshot: img.PromptSnapshot,
CreatedAt: img.CreatedAt, UpdatedAt: img.UpdatedAt,
}
}