feat: 站点媒体库与移动端底部导航,服务测试补强

- 新增媒体库:media_library/media_thumbs 服务(WebP 缩略图)、管理端 media 页面
- 移动端底部导航 MobileTabBar 替换 MobilePostBar
- 旧数据导入增强与测试、路由与登录会话/板块侧边栏测试补强
- site-doc 组件精简(移除 Breadcrumb),文档新增迁移公告说明
This commit is contained in:
2026-09-27 02:35:38 +08:00
parent 734afbc39f
commit 1d862a0dd3
61 changed files with 4757 additions and 1129 deletions

View File

@@ -0,0 +1,172 @@
package service
import (
"bytes"
"image"
"image/color"
"image/gif"
"image/jpeg"
"image/png"
"os"
"path/filepath"
"strings"
"testing"
"github.com/freefire/jiang13-bbs/model"
webpenc "github.com/gen2brain/webp"
"github.com/glebarez/sqlite"
xwebp "golang.org/x/image/webp"
"gorm.io/gorm"
)
func newUploadTestService(t *testing.T) (*UploadService, string) {
t.Helper()
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
if err != nil {
t.Fatalf("open sqlite: %v", err)
}
if err := db.AutoMigrate(&model.Attachment{}); err != nil {
t.Fatalf("migrate: %v", err)
}
dir := t.TempDir()
s := NewUploadService(db, dir)
if err := s.EnsureDir(); err != nil {
t.Fatalf("ensure dir: %v", err)
}
return s, dir
}
func storedUploadPath(t *testing.T, dir, url string) []byte {
t.Helper()
rel := strings.TrimPrefix(url, "/uploads/")
data, err := os.ReadFile(filepath.Join(dir, filepath.FromSlash(rel)))
if err != nil {
t.Fatalf("read stored file %s: %v", url, err)
}
return data
}
func TestSaveImageTranscodesJPEGToWebP(t *testing.T) {
s, dir := newUploadTestService(t)
src := image.NewRGBA(image.Rect(0, 0, 48, 36))
for y := 0; y < 36; y++ {
for x := 0; x < 48; x++ {
src.Set(x, y, color.RGBA{uint8(x * 5), uint8(y * 7), 128, 255})
}
}
var in bytes.Buffer
if err := jpeg.Encode(&in, src, nil); err != nil {
t.Fatalf("encode jpeg: %v", err)
}
att, err := s.SaveImage(1, bytes.NewReader(in.Bytes()))
if err != nil {
t.Fatalf("SaveImage: %v", err)
}
if !strings.HasSuffix(att.URL, ".webp") || att.MIME != "image/webp" {
t.Fatalf("want webp attachment, got url=%s mime=%s", att.URL, att.MIME)
}
if att.Width != 48 || att.Height != 36 {
t.Fatalf("dims changed: %dx%d", att.Width, att.Height)
}
img, err := xwebp.Decode(bytes.NewReader(storedUploadPath(t, dir, att.URL)))
if err != nil {
t.Fatalf("decode stored webp: %v", err)
}
if got := img.Bounds(); got.Dx() != 48 || got.Dy() != 36 {
t.Fatalf("stored webp dims: %v", got)
}
}
func TestSaveImageTranscodesPNGLosslessKeepsAlpha(t *testing.T) {
s, dir := newUploadTestService(t)
src := image.NewNRGBA(image.Rect(0, 0, 24, 24))
for y := 0; y < 24; y++ {
for x := 0; x < 24; x++ {
if y < 12 {
src.SetNRGBA(x, y, color.NRGBA{0, 0, 0, 0}) // 上半透明
} else {
src.SetNRGBA(x, y, color.NRGBA{12, 34, 56, 255})
}
}
}
var in bytes.Buffer
if err := png.Encode(&in, src); err != nil {
t.Fatalf("encode png: %v", err)
}
att, err := s.SaveImage(1, bytes.NewReader(in.Bytes()))
if err != nil {
t.Fatalf("SaveImage: %v", err)
}
if !strings.HasSuffix(att.URL, ".webp") || att.MIME != "image/webp" {
t.Fatalf("want webp attachment, got url=%s mime=%s", att.URL, att.MIME)
}
img, err := xwebp.Decode(bytes.NewReader(storedUploadPath(t, dir, att.URL)))
if err != nil {
t.Fatalf("decode stored webp: %v", err)
}
if _, _, _, a := img.At(0, 0).RGBA(); a != 0 {
t.Fatalf("alpha lost at transparent pixel: %d", a)
}
r, g, b, _ := img.At(5, 20).RGBA()
if r>>8 != 12 || g>>8 != 34 || b>>8 != 56 {
t.Fatalf("opaque color changed: %d,%d,%d", r>>8, g>>8, b>>8)
}
}
func TestSaveImageKeepsGIFAsIs(t *testing.T) {
s, dir := newUploadTestService(t)
src := image.NewRGBA(image.Rect(0, 0, 16, 16))
for y := 0; y < 16; y++ {
for x := 0; x < 16; x++ {
src.Set(x, y, color.RGBA{uint8(x * 16), uint8(y * 16), 0, 255})
}
}
var in bytes.Buffer
if err := gif.Encode(&in, src, nil); err != nil {
t.Fatalf("encode gif: %v", err)
}
att, err := s.SaveImage(1, bytes.NewReader(in.Bytes()))
if err != nil {
t.Fatalf("SaveImage: %v", err)
}
if !strings.HasSuffix(att.URL, ".gif") || att.MIME != "image/gif" {
t.Fatalf("gif should be kept as-is, got url=%s mime=%s", att.URL, att.MIME)
}
data := storedUploadPath(t, dir, att.URL)
magic := string(data[:6])
if magic != "GIF89a" && magic != "GIF87a" {
t.Fatalf("stored gif magic invalid: %q", magic)
}
}
func TestSaveImageKeepsWebPAsIs(t *testing.T) {
s, dir := newUploadTestService(t)
src := image.NewRGBA(image.Rect(0, 0, 20, 10))
var in bytes.Buffer
if err := webpenc.Encode(&in, src, webpenc.Options{Quality: ImageWebPQuality}); err != nil {
t.Fatalf("encode webp: %v", err)
}
att, err := s.SaveImage(1, bytes.NewReader(in.Bytes()))
if err != nil {
t.Fatalf("SaveImage: %v", err)
}
if !strings.HasSuffix(att.URL, ".webp") || att.MIME != "image/webp" {
t.Fatalf("want webp attachment, got url=%s mime=%s", att.URL, att.MIME)
}
if !bytes.Equal(storedUploadPath(t, dir, att.URL), in.Bytes()) {
t.Fatal("webp input should be stored byte-for-byte")
}
if att.Size != in.Len() {
t.Fatalf("size mismatch: att=%d input=%d", att.Size, in.Len())
}
}