feat: add ads/sponsor/captcha modules and polish home & chat UI

This commit is contained in:
2026-09-23 04:39:58 +08:00
parent 96314893bb
commit c1f6a6636b
27 changed files with 4278 additions and 121 deletions

View File

@@ -7,6 +7,7 @@ import (
"encoding/hex"
"errors"
"image"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
"io"
@@ -77,6 +78,9 @@ func (s *UploadService) EnsureDir() error {
if err := os.MkdirAll(filepath.Join(s.dir, "backgrounds"), 0o755); err != nil {
return err
}
if err := os.MkdirAll(filepath.Join(s.dir, "ads"), 0o755); err != nil {
return err
}
return os.MkdirAll(filepath.Join(s.dir, "brand"), 0o755)
}
@@ -166,7 +170,10 @@ func detectImageFormat(data []byte) (imageFormat, error) {
if len(data) >= 12 && string(data[0:4]) == "RIFF" && string(data[8:12]) == "WEBP" {
return imageFormat{ext: ".webp", mime: "image/webp"}, nil
}
return imageFormat{}, errors.New("仅支持 JPEG / PNG / WebP")
if len(data) >= 6 && (string(data[0:6]) == "GIF87a" || string(data[0:6]) == "GIF89a") {
return imageFormat{ext: ".gif", mime: "image/gif"}, nil
}
return imageFormat{}, errors.New("仅支持 JPEG / PNG / WebP / GIF")
}
func decodeImageSizeReader(r io.Reader, mime string) (w, h int, err error) {
@@ -403,6 +410,91 @@ func (s *UploadService) SaveBackground(src io.Reader) (string, error) {
return "/uploads/backgrounds/" + filename, nil
}
const AdAssetMaxBytes = 2 << 20 // 收款码 / 广告素材 2MB
// SaveAdAsset 保存广告相关图片(收款码等)到 uploads/ads,保留原格式含 GIF
func (s *UploadService) SaveAdAsset(src io.Reader) (string, error) {
if src == nil {
return "", errors.New("文件为空")
}
tooLarge := func() error { return errors.New("图片不能超过 2MB") }
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, "ads"), 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, "ads", filename)
tmp := fullPath + ".partial"
if s.ops != nil {
release, e := s.ops.BeginTemporary(tmp)
if e != nil {
return "", e
}
defer release()
}
f, err := os.OpenFile(tmp, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
if err != nil {
return "", err
}
remain := AdAssetMaxBytes + 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
}
if int64(n)+writtenRest > AdAssetMaxBytes {
_ = 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 || w > 4096 || h > 4096 {
_ = os.Remove(tmp)
return "", errors.New("图片尺寸无效")
}
if err := os.Rename(tmp, fullPath); err != nil {
_ = os.Remove(tmp)
return "", err
}
return "/uploads/ads/" + filename, nil
}
// CopyBackgroundFromMedia 把当前用户媒体库里的一张图复制进 backgrounds,不写站点设置
func (s *UploadService) CopyBackgroundFromMedia(userID, attachmentID uint) (string, error) {
var att model.Attachment