// Package templates owns user-scoped image-template validation and persistence. package templates import ( "context" "crypto/rand" "encoding/hex" "errors" "fmt" "math" "net/url" "strings" "time" ) var ( ErrInvalidTemplate = errors.New("invalid image template") ErrNotFound = errors.New("image template not found") ) type Settings struct { Engine string `json:"engine,omitempty"` Width int `json:"width,omitempty"` Height int `json:"height,omitempty"` ForceSingle *bool `json:"forceSingle,omitempty"` Scale float64 `json:"scale,omitempty"` Quality string `json:"quality,omitempty"` } type Template struct { ID string `json:"id"` OwnerID string `json:"ownerId"` Name string `json:"name"` Description string `json:"description,omitempty"` Prompt string `json:"prompt"` PreviewImageURL string `json:"previewImageUrl,omitempty"` Settings Settings `json:"settings"` SortOrder int `json:"sortOrder"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } type CreateCommand struct { Name, Description, Prompt, PreviewImageURL string Settings Settings SortOrder int } type UpdateCommand struct { Name, Description, Prompt, PreviewImageURL *string Settings *Settings SortOrder *int } type Patch = UpdateCommand type Catalog interface { ListTemplates(context.Context, string) ([]Template, error) CreateTemplate(context.Context, Template) (Template, error) UpdateTemplate(context.Context, string, string, Patch, time.Time) (Template, bool, error) DeleteTemplate(context.Context, string, string) (Template, bool, error) } type Service struct { catalog Catalog now func() time.Time newID func() string } func NewService(catalog Catalog, now func() time.Time, newID func() string) *Service { if now == nil { now = time.Now } if newID == nil { newID = templateID } return &Service{catalog: catalog, now: now, newID: newID} } func (service *Service) List(ctx context.Context, owner string) ([]Template, error) { if strings.TrimSpace(owner) == "" { return nil, fmt.Errorf("%w: owner is required", ErrInvalidTemplate) } items, err := service.catalog.ListTemplates(ctx, owner) if err != nil { return nil, fmt.Errorf("list image templates: %w", err) } if items == nil { items = []Template{} } return items, nil } func (service *Service) Create(ctx context.Context, owner string, command CreateCommand) (Template, error) { owner = strings.TrimSpace(owner) if owner == "" { return Template{}, fmt.Errorf("%w: owner is required", ErrInvalidTemplate) } name, err := required(command.Name, 80, "模板名称") if err != nil { return Template{}, err } prompt, err := required(command.Prompt, 4000, "预设提示词") if err != nil { return Template{}, err } preview, err := previewURL(command.PreviewImageURL) if err != nil { return Template{}, err } settings := normalizeSettings(command.Settings) now := service.now().UTC() item := Template{ID: service.newID(), OwnerID: owner, Name: name, Description: optional(command.Description, 240), Prompt: prompt, PreviewImageURL: preview, Settings: settings, SortOrder: command.SortOrder, CreatedAt: now, UpdatedAt: now} created, err := service.catalog.CreateTemplate(ctx, item) if err != nil { return Template{}, fmt.Errorf("create image template: %w", err) } return created, nil } func (service *Service) Update(ctx context.Context, owner, id string, command UpdateCommand) (Template, error) { owner, id = strings.TrimSpace(owner), strings.TrimSpace(id) if owner == "" || id == "" { return Template{}, ErrNotFound } patch := Patch{} var err error if command.Name != nil { value, current := required(*command.Name, 80, "模板名称") err = current patch.Name = &value } if err != nil { return Template{}, err } if command.Prompt != nil { value, current := required(*command.Prompt, 4000, "预设提示词") err = current patch.Prompt = &value } if err != nil { return Template{}, err } if command.Description != nil { value := optional(*command.Description, 240) patch.Description = &value } if command.PreviewImageURL != nil { value, current := previewURL(*command.PreviewImageURL) if current != nil { return Template{}, current } patch.PreviewImageURL = &value } if command.Settings != nil { value := normalizeSettings(*command.Settings) patch.Settings = &value } patch.SortOrder = command.SortOrder item, found, err := service.catalog.UpdateTemplate(ctx, owner, id, patch, service.now().UTC()) if err != nil { return Template{}, fmt.Errorf("update image template: %w", err) } if !found { return Template{}, ErrNotFound } return item, nil } func (service *Service) Delete(ctx context.Context, owner, id string) (Template, error) { item, found, err := service.catalog.DeleteTemplate(ctx, strings.TrimSpace(owner), strings.TrimSpace(id)) if err != nil { return Template{}, fmt.Errorf("delete image template: %w", err) } if !found { return Template{}, ErrNotFound } return item, nil } func applyPatch(item *Template, patch Patch) { if patch.Name != nil { item.Name = *patch.Name } if patch.Description != nil { item.Description = *patch.Description } if patch.Prompt != nil { item.Prompt = *patch.Prompt } if patch.PreviewImageURL != nil { item.PreviewImageURL = *patch.PreviewImageURL } if patch.Settings != nil { item.Settings = *patch.Settings } if patch.SortOrder != nil { item.SortOrder = *patch.SortOrder } } func required(value string, limit int, label string) (string, error) { value = optional(value, limit) if value == "" { return "", fmt.Errorf("%w: %s不能为空", ErrInvalidTemplate, label) } return value, nil } func optional(value string, limit int) string { value = strings.TrimSpace(value) if len([]rune(value)) > limit { value = string([]rune(value)[:limit]) } return value } func previewURL(value string) (string, error) { value = optional(value, 1000) if value == "" || strings.HasPrefix(value, "/") { return value, nil } parsed, err := url.Parse(value) if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.Host == "" { return "", fmt.Errorf("%w: 效果预览图地址必须是 http(s) 或站内路径", ErrInvalidTemplate) } return value, nil } func normalizeSettings(value Settings) Settings { if value.Engine != "jimeng" && value.Engine != "evolink" && value.Engine != "bailian" { value.Engine = "" } if value.Width < 1 || value.Height < 1 { value.Width, value.Height = 0, 0 } else { value.Width, value.Height = min(value.Width, 8192), min(value.Height, 8192) } if math.IsNaN(value.Scale) || math.IsInf(value.Scale, 0) { value.Scale = 0 } else if value.Scale != 0 { value.Scale = math.Max(1, math.Min(100, math.Trunc(value.Scale))) } if value.Quality != "low" && value.Quality != "medium" && value.Quality != "high" { value.Quality = "" } return value } func templateID() string { var raw [9]byte _, _ = rand.Read(raw[:]) return "tmpl_" + hex.EncodeToString(raw[:]) }