feat: 书库导入导出/图片变体/书籍搜索/小组件运行时等

新增:
- 书库导入导出(library_import/library_export)及测试
- 图片变体生成(image_variants)与响应式图片(responsiveImage)
- 书籍搜索(bookSearch)+ BookSearch/LibrarySearchGrid 组件
- 小组件运行时(widgetRuntime)与静态检查(widgetLint)
- 上传缓存中间件(upload_cache)与字体 CSS 提取脚本

其它:
- 后端 handlers/services 全量调整
- 前端页面、组件、库函数与配置更新
This commit is contained in:
2026-10-01 03:06:05 +08:00
parent 7fbd6ba699
commit ff2ab286fb
141 changed files with 7527 additions and 1407 deletions

View File

@@ -238,7 +238,7 @@ func (s *UploadService) transcodeImageToWebP(tmp, name, ext string) (string, str
}
// SaveImage 流式保存帖子插图:校验格式/大小/尺寸 → JPEG/PNG 转 WebP → 落盘 → 写 attachments(kind=image)
func (s *UploadService) SaveImage(userID uint, src io.Reader) (*model.Attachment, error) {
func (s *UploadService) SaveImage(userID uint, src io.Reader, source string) (*model.Attachment, error) {
if src == nil {
return nil, errors.New("文件为空")
}
@@ -354,6 +354,7 @@ func (s *UploadService) SaveImage(userID uint, src io.Reader) (*model.Attachment
att := &model.Attachment{
UserID: userID,
Kind: model.AttachmentKindImage,
Source: normalizeImageSource(source),
URL: "/uploads/images/" + storeName,
MIME: storeMime,
Size: int(storeSize),
@@ -585,9 +586,20 @@ func (s *UploadService) CopyBackgroundFromMedia(userID, attachmentID uint) (stri
return s.SaveBackground(f)
}
// normalizeImageSource 把外部传入的图片来源收敛到白名单,未知值回退为 post
func normalizeImageSource(s string) string {
switch s {
case model.AttachmentSourceLibraryCover, model.AttachmentSourceLibraryContent:
return s
default:
return model.AttachmentSourcePost
}
}
// CopyImageFromMedia 把当前用户媒体库里的一张图复制一份新插图(落 uploads/images 并记一条本人附件)
// 用于书籍封面等长期引用场景:与原图解耦,原图删除后副本仍可用
func (s *UploadService) CopyImageFromMedia(userID, attachmentID uint) (*model.Attachment, error) {
// source 决定副本的来源标记(如 library_cover)
func (s *UploadService) CopyImageFromMedia(userID, attachmentID uint, source string) (*model.Attachment, error) {
var att model.Attachment
if err := s.db.Where("id = ? AND user_id = ?", attachmentID, userID).First(&att).Error; err != nil {
return nil, errors.New("图片不存在或不属于你")
@@ -600,7 +612,7 @@ func (s *UploadService) CopyImageFromMedia(userID, attachmentID uint) (*model.At
return nil, e
}
defer r.Close()
return s.SaveImage(userID, r)
return s.SaveImage(userID, r, source)
}
abs, ok := s.safeUploadPath(att.URL)
if !ok {
@@ -611,7 +623,7 @@ func (s *UploadService) CopyImageFromMedia(userID, attachmentID uint) (*model.At
return nil, errors.New("读取图片失败")
}
defer f.Close()
return s.SaveImage(userID, f)
return s.SaveImage(userID, f, source)
}
// BackgroundFileExists 确认 URL 对应文件在 backgrounds 目录内
@@ -667,6 +679,15 @@ func (s *UploadService) ListMedia(userID uint) ([]model.Attachment, error) {
return list, err
}
// FindMediaBySource 按来源查本人附件(存在返回记录,不存在返回 error)
func (s *UploadService) FindMediaBySource(userID, attachmentID uint, source string) (*model.Attachment, error) {
var att model.Attachment
if err := s.db.Where("id = ? AND user_id = ? AND source = ?", attachmentID, userID, source).First(&att).Error; err != nil {
return nil, err
}
return &att, nil
}
// ErrAttachmentInUse 图片正被帖子内容引用,不可物理删除
var ErrAttachmentInUse = errors.New("该图片已被帖子使用,无法删除")