package service import ( "context" "encoding/json" "errors" "strings" "backend/internal/model" "backend/internal/repo" ) type UserGenerationService struct { v1 *V1Service events *repo.EventRepository users *repo.UserRepository models *repo.ModelRepository } func NewUserGenerationService(v1 *V1Service, events *repo.EventRepository, users *repo.UserRepository, models *repo.ModelRepository) *UserGenerationService { return &UserGenerationService{ v1: v1, events: events, users: users, models: models, } } type UserGenerateRequest struct { Model string Prompt string Ratio string Resolution string Duration string ReferenceImages []string } func (s *UserGenerationService) Generate(ctx context.Context, user *model.User, in UserGenerateRequest) (map[string]any, error) { if user == nil || strings.TrimSpace(user.ID) == "" { return nil, errors.New("未登录或会话已过期") } pending, err := s.events.PendingByUser(ctx, user.ID, "user") if err != nil { return nil, err } if pending != nil { return nil, errors.New("已有正在生成的任务,请稍候") } modelItem, err := s.models.Get(ctx, strings.TrimSpace(in.Model)) if err != nil { return nil, ErrUnknownModel } principal := &APIPrincipal{ User: user, TokenType: "session", } switch modelItem.Type { case "video": resp, err := s.v1.prepareSessionVideo(ctx, principal, V1VideoRequest{ Model: in.Model, Prompt: in.Prompt, Duration: in.Duration, AspectRatio: in.Ratio, Resolution: in.Resolution, ReferenceImages: in.ReferenceImages, }) if err != nil { return nil, err } return resp, nil default: resp, err := s.v1.prepareSessionImage(ctx, principal, V1ImageRequest{ Model: in.Model, Prompt: in.Prompt, AspectRatio: in.Ratio, Resolution: in.Resolution, ReferenceImages: in.ReferenceImages, }) if err != nil { return nil, err } return resp, nil } } func (s *UserGenerationService) AdminTest(ctx context.Context, user *model.User, in UserGenerateRequest) (map[string]any, error) { if user == nil || strings.TrimSpace(user.ID) == "" { return nil, errors.New("未登录或会话已过期") } modelItem, err := s.models.Get(ctx, strings.TrimSpace(in.Model)) if err != nil { return nil, ErrUnknownModel } principal := &APIPrincipal{ User: user, TokenType: "session", } switch modelItem.Type { case "video": return s.v1.prepareAdminTestVideo(ctx, principal, V1VideoRequest{ Model: in.Model, Prompt: in.Prompt, Duration: in.Duration, AspectRatio: in.Ratio, Resolution: in.Resolution, ReferenceImages: in.ReferenceImages, }) default: return s.v1.prepareAdminTestImage(ctx, principal, V1ImageRequest{ Model: in.Model, Prompt: in.Prompt, AspectRatio: in.Ratio, Resolution: in.Resolution, ReferenceImages: in.ReferenceImages, }) } } func (s *UserGenerationService) MyJobs(ctx context.Context, user *model.User, source string) (map[string]any, error) { if user == nil || strings.TrimSpace(user.ID) == "" { return map[string]any{"pending": nil, "latest": nil}, nil } // source scopes the lookup: "user" = 画图台(默认),"admin" = 后台测试模型。 // Both are this caller's own events; the admin-test poll uses "admin" so a // gateway-timed-out (524) test can still recover its result. if source != "admin" { source = "user" } pending, err := s.events.PendingByUser(ctx, user.ID, source) if err != nil { return nil, err } latest, err := s.events.LatestByUser(ctx, user.ID, source) if err != nil { return nil, err } return map[string]any{ "pending": shapeJobEvent(pending), "latest": shapeJobEvent(latest), }, nil } func shapeJobEvent(item *model.EventLog) map[string]any { if item == nil { return nil } status := item.Status url := "" if strings.TrimSpace(item.File) != "" { url = "/images/" + strings.ReplaceAll(strings.TrimSpace(item.File), "\\", "/") } return map[string]any{ "id": item.ID, "kind": item.Kind, "model": item.Model, "prompt": item.Prompt, "ratio": item.Ratio, "resolution": item.Resolution, "duration": item.Duration, "status": status, "file": emptyOrNil(item.File), "url": emptyOrNil(url), "reference_urls": referenceURLs(item.RefFiles), "elapsed_ms": item.ElapsedMS, "error": emptyOrNil(item.Error), "charged": item.Cost, "cost": item.Cost, "ts": item.TS.Unix(), } } // referenceURLs turns the stored relative reference paths into /images URLs so // the playground can re-display the uploaded reference image(s) after a reload. func referenceURLs(raw []byte) []string { if len(raw) == 0 { return []string{} } var paths []string if err := json.Unmarshal(raw, &paths); err != nil { return []string{} } out := make([]string, 0, len(paths)) for _, p := range paths { p = strings.ReplaceAll(strings.TrimSpace(p), "\\", "/") if p != "" { out = append(out, "/images/"+p) } } return out } func emptyOrNil(v string) any { if strings.TrimSpace(v) == "" { return nil } return v }