Files
NianAIGC/backend/internal/templates/service.go

251 lines
7.1 KiB
Go

// 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[:])
}