feat: rename qiniu to oss, add admin upload proxy with thumbnail, add dev-login

- Rename all QINIU_* config/code/docs to OSS_* to match actual Alibaba Cloud OSS
- Refactor upload module from internal/common/qiniu to internal/common/upload
- Add backend proxy upload endpoint (POST /api/admin/marketing/upload) to avoid CORS
- Auto-generate compressed thumbnail (800px, JPEG 80%) on admin image upload
- Add dev-login endpoint (POST /api/v1/auth/dev-login) for H5 debugging
- Add imageutil package for server-side image resizing

Made-with: Cursor
This commit is contained in:
nepiedg
2026-04-04 02:52:16 +08:00
parent aeaf6a04c2
commit 1eab1b99c1
21 changed files with 1023 additions and 191 deletions
+72
View File
@@ -0,0 +1,72 @@
package imageutil
import (
"bytes"
"fmt"
"image"
"image/jpeg"
"image/png"
"io"
"strings"
"golang.org/x/image/draw"
)
type ResizeOptions struct {
MaxWidth int
Quality int // JPEG quality 1-100
}
func DefaultResizeOptions() ResizeOptions {
return ResizeOptions{MaxWidth: 800, Quality: 80}
}
// GenerateThumbnail reads image data, resizes if wider than MaxWidth, and
// re-encodes as JPEG. Returns the thumbnail bytes and whether the image
// was actually resized (false if already small enough but still re-encoded).
func GenerateThumbnail(data []byte, contentType string, opts ResizeOptions) ([]byte, error) {
if opts.MaxWidth <= 0 {
opts.MaxWidth = 800
}
if opts.Quality <= 0 || opts.Quality > 100 {
opts.Quality = 80
}
src, err := decodeImage(bytes.NewReader(data), contentType)
if err != nil {
return nil, fmt.Errorf("decode image: %w", err)
}
bounds := src.Bounds()
srcW := bounds.Dx()
srcH := bounds.Dy()
dstW := srcW
dstH := srcH
if srcW > opts.MaxWidth {
dstW = opts.MaxWidth
dstH = srcH * opts.MaxWidth / srcW
}
dst := image.NewRGBA(image.Rect(0, 0, dstW, dstH))
draw.BiLinear.Scale(dst, dst.Bounds(), src, bounds, draw.Over, nil)
var buf bytes.Buffer
if err := jpeg.Encode(&buf, dst, &jpeg.Options{Quality: opts.Quality}); err != nil {
return nil, fmt.Errorf("encode jpeg: %w", err)
}
return buf.Bytes(), nil
}
func decodeImage(r io.Reader, contentType string) (image.Image, error) {
ct := strings.ToLower(contentType)
switch {
case strings.Contains(ct, "png"):
return png.Decode(r)
case strings.Contains(ct, "jpeg"), strings.Contains(ct, "jpg"):
return jpeg.Decode(r)
default:
img, _, err := image.Decode(r)
return img, err
}
}