Files
ttstd-infinite-canvas/backend/internal/canvas/user_asset_document.go

210 lines
6.1 KiB
Go

package canvas
import (
"bytes"
"encoding/json"
"infinite-canvas/backend/internal/kernel"
"math"
"strings"
)
var userAssetKinds = map[string]struct{}{
"text": {},
"image": {},
"video": {},
"audio": {},
"model": {},
"entity": {},
}
func validateUserAssetDocument(raw json.RawMessage) error {
var object map[string]json.RawMessage
if err := json.Unmarshal(raw, &object); err != nil {
return kernel.BadAuthRequest("素材数据格式错误")
}
kind, err := requiredJSONStringField(object, "kind")
if err != nil {
return err
}
kind = strings.TrimSpace(kind)
if _, ok := userAssetKinds[kind]; !ok {
return kernel.BadAuthRequest("不支持的素材类型")
}
if _, err := requiredJSONStringField(object, "title"); err != nil {
return err
}
if _, err := requiredJSONStringField(object, "coverUrl"); err != nil {
return err
}
if err := validateUserAssetTags(object); err != nil {
return err
}
data, err := requiredJSONObjectField(object, "data")
if err != nil {
return err
}
return validateUserAssetData(kind, data)
}
func validateUserAssetTags(object map[string]json.RawMessage) error {
raw, ok := object["tags"]
if !ok {
return kernel.BadAuthRequest("素材缺少 tags 字段")
}
trimmed := bytes.TrimSpace(raw)
if len(trimmed) == 0 || trimmed[0] != '[' {
return kernel.BadAuthRequest("素材 tags 必须是字符串数组")
}
var tags []string
if err := json.Unmarshal(raw, &tags); err != nil {
return kernel.BadAuthRequest("素材 tags 必须是字符串数组")
}
return nil
}
func validateUserAssetData(kind string, data map[string]json.RawMessage) error {
switch kind {
case "text":
_, err := requiredJSONStringField(data, "content")
return err
case "image":
if err := requireMediaLocator(data, "dataUrl", "图片"); err != nil {
return err
}
if _, err := requiredJSONNumberField(data, "width"); err != nil {
return err
}
if _, err := requiredJSONNumberField(data, "height"); err != nil {
return err
}
if _, err := requiredJSONNumberField(data, "bytes"); err != nil {
return err
}
return requireNonEmptyJSONString(data, "mimeType")
case "video":
if err := requireMediaLocator(data, "url", "视频"); err != nil {
return err
}
if _, err := requiredJSONNumberField(data, "width"); err != nil {
return err
}
if _, err := requiredJSONNumberField(data, "height"); err != nil {
return err
}
if _, err := requiredJSONNumberField(data, "bytes"); err != nil {
return err
}
return requireNonEmptyJSONString(data, "mimeType")
case "audio":
if err := requireMediaLocator(data, "url", "音频"); err != nil {
return err
}
if _, err := requiredJSONNumberField(data, "bytes"); err != nil {
return err
}
return requireNonEmptyJSONString(data, "mimeType")
case "model":
if err := requireMediaLocator(data, "url", "模型"); err != nil {
return err
}
if _, err := requiredJSONNumberField(data, "bytes"); err != nil {
return err
}
if err := requireNonEmptyJSONString(data, "mimeType"); err != nil {
return err
}
return requireNonEmptyJSONString(data, "fileName")
case "entity":
return requireJSONObjectFieldPresent(data, "definition")
default:
return kernel.BadAuthRequest("不支持的素材类型")
}
}
func requireMediaLocator(data map[string]json.RawMessage, primaryKey string, label string) error {
primary, err := optionalJSONStringField(data, primaryKey)
if err != nil {
return err
}
storageKey, err := optionalJSONStringField(data, "storageKey")
if err != nil {
return err
}
if strings.TrimSpace(primary) == "" && strings.TrimSpace(storageKey) == "" {
return kernel.BadAuthRequest(label + "素材缺少 " + primaryKey + " 或 storageKey")
}
return nil
}
func requiredJSONStringField(object map[string]json.RawMessage, key string) (string, error) {
raw, ok := object[key]
if !ok {
return "", kernel.BadAuthRequest("素材缺少 " + key + " 字段")
}
var value string
if err := json.Unmarshal(raw, &value); err != nil {
return "", kernel.BadAuthRequest("素材字段 " + key + " 必须是字符串")
}
return value, nil
}
func optionalJSONStringField(object map[string]json.RawMessage, key string) (string, error) {
raw, ok := object[key]
if !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return "", nil
}
var value string
if err := json.Unmarshal(raw, &value); err != nil {
return "", kernel.BadAuthRequest("素材字段 " + key + " 必须是字符串")
}
return value, nil
}
func requireNonEmptyJSONString(object map[string]json.RawMessage, key string) error {
value, err := requiredJSONStringField(object, key)
if err != nil {
return err
}
if strings.TrimSpace(value) == "" {
return kernel.BadAuthRequest("素材字段 " + key + " 不能为空")
}
return nil
}
func requiredJSONNumberField(object map[string]json.RawMessage, key string) (float64, error) {
raw, ok := object[key]
if !ok {
return 0, kernel.BadAuthRequest("素材缺少 " + key + " 字段")
}
// json.Unmarshal(null, &float64) succeeds without assigning a value.
// Reject null explicitly so the persisted JSON obeys the client contract.
if bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return 0, kernel.BadAuthRequest("素材字段 " + key + " 必须是数字")
}
var value float64
if err := json.Unmarshal(raw, &value); err != nil {
return 0, kernel.BadAuthRequest("素材字段 " + key + " 必须是数字")
}
if math.IsNaN(value) || math.IsInf(value, 0) || value < 0 {
return 0, kernel.BadAuthRequest("素材字段 " + key + " 必须是非负数字")
}
return value, nil
}
func requiredJSONObjectField(object map[string]json.RawMessage, key string) (map[string]json.RawMessage, error) {
raw, ok := object[key]
if !ok {
return nil, kernel.BadAuthRequest("素材缺少 " + key + " 字段")
}
var nested map[string]json.RawMessage
if err := json.Unmarshal(raw, &nested); err != nil || nested == nil {
return nil, kernel.BadAuthRequest("素材字段 " + key + " 必须是对象")
}
return nested, nil
}
func requireJSONObjectFieldPresent(object map[string]json.RawMessage, key string) error {
_, err := requiredJSONObjectField(object, key)
return err
}