feat: 交付官方 Docker 运行时,外观改背景图并下线自定义 CSS/JS
站点/后台分轨背景与用户列表排序一并落地;生产 CORS 改走 SITE_URL,健康检查带版本号。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -29,6 +29,10 @@ const (
|
||||
// 帖子插图:允许 JPEG/PNG/WebP,不强制转码
|
||||
ImageMaxBytes = 5 << 20 // 5 MiB
|
||||
ImageMaxDim = 4096
|
||||
|
||||
// 站点背景图(前台/后台墙纸)
|
||||
BackgroundMaxBytes = 8 << 20 // 8 MiB
|
||||
BackgroundMaxDim = 8192
|
||||
)
|
||||
|
||||
// UploadService 附件上传:落盘到 data/uploads,元信息入库 attachments
|
||||
@@ -64,7 +68,10 @@ func (s *UploadService) EnsureDir() error {
|
||||
if err := os.MkdirAll(filepath.Join(s.dir, "avatars"), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.MkdirAll(filepath.Join(s.dir, "images"), 0o755)
|
||||
if err := os.MkdirAll(filepath.Join(s.dir, "images"), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.MkdirAll(filepath.Join(s.dir, "backgrounds"), 0o755)
|
||||
}
|
||||
|
||||
// SaveAvatar 保存裁剪后的 WebP 头像:校验魔数/大小/尺寸 → 落盘 → 写附件记录 → 更新用户头像
|
||||
@@ -267,6 +274,119 @@ func (s *UploadService) SaveImage(userID uint, src io.Reader) (*model.Attachment
|
||||
return att, nil
|
||||
}
|
||||
|
||||
// SaveBackground 保存站点背景图到 uploads/backgrounds,不进用户媒体库
|
||||
func (s *UploadService) SaveBackground(src io.Reader) (string, error) {
|
||||
if src == nil {
|
||||
return "", errors.New("文件为空")
|
||||
}
|
||||
tooLarge := func() error {
|
||||
return errors.New("背景图不能超过 8MB")
|
||||
}
|
||||
|
||||
head := make([]byte, 12)
|
||||
n, err := io.ReadFull(src, head)
|
||||
if err != nil && !errors.Is(err, io.ErrUnexpectedEOF) && !errors.Is(err, io.EOF) {
|
||||
return "", errors.New("读取图片失败")
|
||||
}
|
||||
if n == 0 {
|
||||
return "", errors.New("文件为空")
|
||||
}
|
||||
format, err := detectImageFormat(head[:n])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Join(s.dir, "backgrounds"), 0o755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
nameBytes := make([]byte, 16)
|
||||
if _, err := rand.Read(nameBytes); err != nil {
|
||||
return "", err
|
||||
}
|
||||
filename := hex.EncodeToString(nameBytes) + format.ext
|
||||
fullPath := filepath.Join(s.dir, "backgrounds", filename)
|
||||
tmp := fullPath + ".partial"
|
||||
|
||||
f, err := os.OpenFile(tmp, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
remain := BackgroundMaxBytes + 1 - int64(n)
|
||||
if remain < 0 {
|
||||
_ = f.Close()
|
||||
_ = os.Remove(tmp)
|
||||
return "", tooLarge()
|
||||
}
|
||||
if _, err := f.Write(head[:n]); err != nil {
|
||||
_ = f.Close()
|
||||
_ = os.Remove(tmp)
|
||||
return "", err
|
||||
}
|
||||
writtenRest, copyErr := io.Copy(f, io.LimitReader(src, remain))
|
||||
_ = f.Close()
|
||||
if copyErr != nil {
|
||||
_ = os.Remove(tmp)
|
||||
return "", copyErr
|
||||
}
|
||||
total := int64(n) + writtenRest
|
||||
if total > BackgroundMaxBytes {
|
||||
_ = os.Remove(tmp)
|
||||
return "", tooLarge()
|
||||
}
|
||||
|
||||
rf, err := os.Open(tmp)
|
||||
if err != nil {
|
||||
_ = os.Remove(tmp)
|
||||
return "", err
|
||||
}
|
||||
w, h, err := decodeImageSizeReader(rf, format.mime)
|
||||
_ = rf.Close()
|
||||
if err != nil {
|
||||
_ = os.Remove(tmp)
|
||||
return "", err
|
||||
}
|
||||
if w < 1 || h < 1 {
|
||||
_ = os.Remove(tmp)
|
||||
return "", errors.New("无效的图片尺寸")
|
||||
}
|
||||
if w > BackgroundMaxDim || h > BackgroundMaxDim {
|
||||
_ = os.Remove(tmp)
|
||||
return "", errors.New("背景图边长不能超过 8192px")
|
||||
}
|
||||
if err := os.Rename(tmp, fullPath); err != nil {
|
||||
_ = os.Remove(tmp)
|
||||
return "", err
|
||||
}
|
||||
return "/uploads/backgrounds/" + filename, nil
|
||||
}
|
||||
|
||||
// BackgroundFileExists 确认 URL 对应文件在 backgrounds 目录内
|
||||
func (s *UploadService) BackgroundFileExists(url string) bool {
|
||||
rel := BgRelPath(url)
|
||||
if rel == "" {
|
||||
return false
|
||||
}
|
||||
abs := filepath.Join(s.dir, rel)
|
||||
info, err := os.Stat(abs)
|
||||
return err == nil && !info.IsDir()
|
||||
}
|
||||
|
||||
// RemoveBackgroundIfUnused 旧墙纸不再被前台或后台引用时删除物理文件
|
||||
func (s *UploadService) RemoveBackgroundIfUnused(oldURL, siteURL, adminURL string) {
|
||||
if oldURL == "" || oldURL == siteURL || oldURL == adminURL {
|
||||
return
|
||||
}
|
||||
rel := BgRelPath(oldURL)
|
||||
if rel == "" {
|
||||
return
|
||||
}
|
||||
abs := filepath.Join(s.dir, rel)
|
||||
if err := os.Remove(abs); err != nil && !os.IsNotExist(err) {
|
||||
log.Printf("[upload] 删除背景图失败 path=%s: %v", abs, err)
|
||||
}
|
||||
}
|
||||
|
||||
// UseAvatar 选用一张【本人历史上传】的头像
|
||||
func (s *UploadService) UseAvatar(userID uint, url string) error {
|
||||
url = strings.TrimSpace(url)
|
||||
|
||||
Reference in New Issue
Block a user