refactor: Gitea 式目录改组,移除本分支 SPA 与杂项产物

将 model/service/handler/middleware 迁至 models/services/routers/api/modules/auth,并删除 frontend、embed_static、scripts 及误入库缓存/二进制。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-29 03:54:56 +08:00
parent 1414c71dec
commit 9fe299a45f
449 changed files with 0 additions and 52779 deletions

152
services/image_webp.go Normal file
View File

@@ -0,0 +1,152 @@
package service
import (
"bytes"
"errors"
"fmt"
"image"
"image/gif"
"io"
"mime/multipart"
"path/filepath"
"strings"
"github.com/KarpelesLab/gowebp"
// 注册解码器
_ "image/jpeg"
_ "image/png"
_ "golang.org/x/image/webp"
)
// 图片展示方案(上传时同时保留原图与 WebP按此决定返回给前端的 URL
const (
ImageDeliveryWebP = "webp" // 使用 WebP默认省流量
ImageDeliveryOriginal = "original" // 使用原图
)
const (
// UploadWebPQuality 上传衍生 WebP 有损质量0100
UploadWebPQuality float32 = 82
// UploadWebPMethod 编码档位3 速度与体积较均衡
UploadWebPMethod = 3
// ThumbWebPQuality 帖子预览图质量
ThumbWebPQuality float32 = 80
)
// preparedUpload 原图 + 可选 WebP 衍生
type preparedUpload struct {
OrigExt string // 含点,如 .jpg
OrigContentType string
OrigData []byte
WebPData []byte // 空表示无衍生(动图 GIF或原图已是 WebP
}
// prepareUploadImage 始终保留原图字节;静态图额外生成 WebP 衍生
func prepareUploadImage(file *multipart.FileHeader) (*preparedUpload, error) {
ext := strings.ToLower(filepath.Ext(file.Filename))
if !allowedImageExt[ext] {
return nil, errors.New("仅支持 jpg/png/gif/webp 格式")
}
if ext == ".jpeg" {
ext = ".jpg"
}
src, err := file.Open()
if err != nil {
return nil, err
}
defer src.Close()
raw, err := io.ReadAll(src)
if err != nil {
return nil, err
}
if len(raw) == 0 {
return nil, errors.New("空图片文件")
}
out := &preparedUpload{
OrigExt: ext,
OrigContentType: imageContentType(ext),
OrigData: raw,
}
// 动图 GIF只保留原文件
if ext == ".gif" && gifFrameCount(raw) > 1 {
return out, nil
}
// 上传已是 WebP原图即 WebP不再重复衍生
if ext == ".webp" {
return out, nil
}
img, _, err := image.Decode(bytes.NewReader(raw))
if err != nil {
return nil, fmt.Errorf("解码图片失败: %w", err)
}
webpBytes, err := encodeWebPBytes(img, UploadWebPQuality, UploadWebPMethod)
if err != nil {
return nil, fmt.Errorf("转换 WebP 失败: %w", err)
}
out.WebPData = webpBytes
return out, nil
}
func gifFrameCount(raw []byte) int {
g, err := gif.DecodeAll(bytes.NewReader(raw))
if err != nil || g == nil {
return 0
}
return len(g.Image)
}
func encodeWebPBytes(img image.Image, quality float32, method int) ([]byte, error) {
var buf bytes.Buffer
if err := gowebp.Encode(&buf, img, &gowebp.Options{
Lossy: true,
Quality: quality,
Method: method,
}); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func normalizeImageDelivery(raw string) string {
if strings.ToLower(strings.TrimSpace(raw)) == ImageDeliveryOriginal {
return ImageDeliveryOriginal
}
return ImageDeliveryWebP
}
func imageContentType(ext string) string {
switch strings.ToLower(ext) {
case ".jpg", ".jpeg":
return "image/jpeg"
case ".png":
return "image/png"
case ".gif":
return "image/gif"
case ".webp":
return "image/webp"
default:
return "application/octet-stream"
}
}
// siblingUploadExts 同主文件名可能存在的伴生扩展名(删除时一并清理)
func siblingUploadExts(ext string) []string {
ext = strings.ToLower(ext)
all := []string{".jpg", ".jpeg", ".png", ".gif", ".webp"}
out := make([]string, 0, len(all))
for _, e := range all {
if e == ext || (ext == ".jpg" && e == ".jpeg") || (ext == ".jpeg" && e == ".jpg") {
continue
}
out = append(out, e)
}
return out
}