feat(*): 创作配置 - 支持 Midjourney、多图保存与自定义公告
Co-authored-by: ddcat666 <26564566+ddcat666@users.noreply.github.com>
This commit is contained in:
1 parent
002a45f0c3
commit
853c7d2ce7
107 files changed
+9261
-408
No files matched your search
@@ -0,0 +1,219 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"infinite-canvas/backend/internal/assets"
|
||||
"infinite-canvas/backend/internal/model"
|
||||
)
|
||||
|
||||
type UpdateAnnouncement struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
CurrentVersion string `json:"currentVersion"`
|
||||
Releases []UpdateRelease `json:"releases"`
|
||||
}
|
||||
|
||||
type UpdateRelease struct {
|
||||
ID string `json:"id"`
|
||||
Version string `json:"version"`
|
||||
Title string `json:"title"`
|
||||
Date string `json:"date"`
|
||||
Blocks []UpdateBlock `json:"blocks"`
|
||||
}
|
||||
|
||||
type UpdateBlock struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text"`
|
||||
ResourceID string `json:"resourceId"`
|
||||
Caption string `json:"caption"`
|
||||
Layout string `json:"layout"`
|
||||
MediaURL string `json:"mediaUrl,omitempty"`
|
||||
}
|
||||
|
||||
func normalizeUpdateAnnouncement(value UpdateAnnouncement) (UpdateAnnouncement, error) {
|
||||
value.CurrentVersion = normalizeAppearanceSingleLine(value.CurrentVersion)
|
||||
if err := validateAppearanceCopy(value.CurrentVersion, "公告当前版本", 40, value.Enabled); err != nil {
|
||||
return value, err
|
||||
}
|
||||
if len(value.Releases) > 30 {
|
||||
return value, BadAuthRequest("更新公告最多 30 个版本")
|
||||
}
|
||||
if value.Enabled && len(value.Releases) == 0 {
|
||||
return value, BadAuthRequest("开启自定义更新公告前请添加版本和内容")
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
versions := map[string]bool{}
|
||||
total := 0
|
||||
currentFound := false
|
||||
for i := range value.Releases {
|
||||
r := &value.Releases[i]
|
||||
r.ID, r.Version = strings.TrimSpace(r.ID), normalizeAppearanceSingleLine(r.Version)
|
||||
r.Title, r.Date = normalizeAppearanceSingleLine(r.Title), strings.TrimSpace(r.Date)
|
||||
if r.ID == "" || len(r.ID) > 80 || seen[r.ID] {
|
||||
return value, BadAuthRequest("公告版本标识无效或重复")
|
||||
}
|
||||
seen[r.ID] = true
|
||||
if err := validateAppearanceCopy(r.Version, "公告版本号", 40, true); err != nil {
|
||||
return value, err
|
||||
}
|
||||
if versions[r.Version] {
|
||||
return value, BadAuthRequest("公告版本号不能重复")
|
||||
}
|
||||
versions[r.Version] = true
|
||||
if r.Version == value.CurrentVersion {
|
||||
currentFound = true
|
||||
}
|
||||
if err := validateAppearanceCopy(r.Title, "公告标题", 100, false); err != nil {
|
||||
return value, err
|
||||
}
|
||||
if r.Date != "" {
|
||||
if _, err := time.Parse("2006-01-02", r.Date); err != nil {
|
||||
return value, BadAuthRequest("公告日期须为 YYYY-MM-DD")
|
||||
}
|
||||
}
|
||||
if len(r.Blocks) == 0 || len(r.Blocks) > 50 {
|
||||
return value, BadAuthRequest("每个公告版本须包含 1 至 50 个内容块")
|
||||
}
|
||||
for j := range r.Blocks {
|
||||
b := &r.Blocks[j]
|
||||
b.ID, b.ResourceID = strings.TrimSpace(b.ID), strings.TrimSpace(b.ResourceID)
|
||||
b.Text, b.Caption, b.MediaURL = normalizeAppearanceCopy(b.Text), normalizeAppearanceSingleLine(b.Caption), ""
|
||||
if b.ID == "" || len(b.ID) > 80 || seen[b.ID] {
|
||||
return value, BadAuthRequest("公告内容块标识无效或重复")
|
||||
}
|
||||
seen[b.ID] = true
|
||||
if b.Layout == "" {
|
||||
b.Layout = "full"
|
||||
}
|
||||
if b.Layout != "full" && b.Layout != "half" {
|
||||
return value, BadAuthRequest("公告布局须为整行或半行")
|
||||
}
|
||||
if err := validateAppearanceCopy(b.Text, "公告正文", 10000, b.Type == "text"); err != nil {
|
||||
return value, err
|
||||
}
|
||||
if err := validateAppearanceCopy(b.Caption, "媒体说明", 200, false); err != nil {
|
||||
return value, err
|
||||
}
|
||||
switch b.Type {
|
||||
case "text":
|
||||
b.ResourceID = ""
|
||||
case "image", "video":
|
||||
if b.ResourceID == "" || len(b.ResourceID) > 80 {
|
||||
return value, BadAuthRequest("请上传公告图片或视频")
|
||||
}
|
||||
b.Text = ""
|
||||
default:
|
||||
return value, BadAuthRequest("公告内容块类型无效")
|
||||
}
|
||||
total += len(b.Text) + len(b.Caption)
|
||||
}
|
||||
}
|
||||
if total > 200000 {
|
||||
return value, BadAuthRequest("公告内容总大小不能超过 200KB")
|
||||
}
|
||||
if value.Enabled && !currentFound {
|
||||
return value, BadAuthRequest("公告当前版本必须对应已添加的版本")
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func updateResourceSlot(kind string) string {
|
||||
if kind == "video" {
|
||||
return AppearanceAssetUpdateVideo
|
||||
}
|
||||
return AppearanceAssetUpdateImage
|
||||
}
|
||||
|
||||
func updateReferenced(value UpdateAnnouncement, id string) bool {
|
||||
for _, release := range value.Releases {
|
||||
for _, block := range release.Blocks {
|
||||
if block.ResourceID == id && (block.Type == "image" || block.Type == "video") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Service) validateUpdateAnnouncementResources(actor *model.User, value, before UpdateAnnouncement) error {
|
||||
for _, release := range value.Releases {
|
||||
for _, block := range release.Blocks {
|
||||
if block.ResourceID == "" {
|
||||
continue
|
||||
}
|
||||
current := ""
|
||||
if updateReferenced(before, block.ResourceID) {
|
||||
current = block.ResourceID
|
||||
}
|
||||
slot := updateResourceSlot(block.Type)
|
||||
if err := s.validateAppearanceResource(actor, slot, block.ResourceID, current); err != nil {
|
||||
return err
|
||||
}
|
||||
resource, err := s.repo.Resource(block.ResourceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
maxBytes, _ := AppearanceAssetMaxBytes(slot)
|
||||
if resource.Size <= 0 || resource.Size > maxBytes {
|
||||
return BadAuthRequest("公告媒体大小超过允许范围")
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func publicUpdateAnnouncement(value UpdateAnnouncement, revision string) UpdateAnnouncement {
|
||||
if !value.Enabled {
|
||||
return UpdateAnnouncement{Releases: []UpdateRelease{}}
|
||||
}
|
||||
result := value
|
||||
result.Releases = append([]UpdateRelease(nil), value.Releases...)
|
||||
for i := range result.Releases {
|
||||
result.Releases[i].Blocks = append([]UpdateBlock(nil), value.Releases[i].Blocks...)
|
||||
for j := range result.Releases[i].Blocks {
|
||||
b := &result.Releases[i].Blocks[j]
|
||||
if b.ResourceID != "" {
|
||||
b.MediaURL = "/api/public/appearance/updates/" + url.PathEscape(b.ResourceID) + "?v=" + url.QueryEscape(revision)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Public delivery is limited to the currently enabled announcement references.
|
||||
// Draft previews require an administrator and never expose unrelated resources.
|
||||
func (s *Service) PrepareUpdateAnnouncementDelivery(actor *model.User, id string, options ResourceAccessOptions, rangeHeader string) (*ResourceDelivery, error) {
|
||||
if actor != nil {
|
||||
if err := s.RequireAdmin(actor); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
_, value, err := s.readAppearance()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
referenced := updateReferenced(value.Updates, id)
|
||||
if actor == nil && (!value.Updates.Enabled || !referenced) {
|
||||
return nil, NotFound("公告媒体未发布")
|
||||
}
|
||||
resource, err := s.repo.Resource(id)
|
||||
if err != nil {
|
||||
return nil, NotFound("公告媒体不存在")
|
||||
}
|
||||
if actor != nil && resource.UserID != actor.ID && !referenced {
|
||||
return nil, Forbidden("不能预览其他管理员的未发布媒体")
|
||||
}
|
||||
if resource.Kind != "image" && resource.Kind != "video" {
|
||||
return nil, BadAuthRequest("公告仅支持图片和视频")
|
||||
}
|
||||
if err := validateAppearanceResourceType(updateResourceSlot(resource.Kind), resource); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if options.Purpose == "" {
|
||||
options.Purpose = assets.PurposeDisplay
|
||||
}
|
||||
return s.prepareResourceDelivery(resource.UserID, resource, options, rangeHeader)
|
||||
}
|
||||
Reference in new issue
Block a user