需求:前台壁纸站支持切换到开源壁纸平台,并能展示后台上传的自家图库, 保留原有「浏览器实时生成」能力(扩展而非替换),PC / 移动双端一致。 设计要点(受服务器 5M 出口带宽约束): - 开源平台图片一律走官方 CDN 直链,后端只代理元数据,零带宽消耗。 - 自建图库落盘到 /data/www/wallpaper,由 nginx 的 /wallpaper/ 直出, Go 服务不参与传图;三档尺寸(480 缩略 / 1920 预览 / 原图仅供下载)。 - 内存缓存平台响应(Bing 1h、搜索类 10min),避免撞第三方配额。 内容: - 平台插件:Bing 每日一图、Picsum、Unsplash、Pexels、Wallhaven。 新增平台 = 写一个 provider_xxx.go 并在 allProviders() 加一行, 刻意不用 init() 自注册,避免隐式副作用。所有适配器支持 config.baseUrl 覆盖,用于绕开 DNS 污染(实测 wallhaven.cc 解析到境外无关 IP)。 - 自建图库:上传(MD5 秒传去重 / 尺寸前置校验 / 失败清理孤儿文件)、 元信息编辑、删除、统计;随机取图用「数总数 → 随机偏移」而非 ORDER BY RAND()。 - 接口:open 组 4 个(sources / list / random / download-track), admin 组 8 个(list / upload / save / delete / stats / source.list|save|test)。 全部 POST 且 URL 无参数,符合工作空间接口规范;上传走 multipart 例外。 - 安全:purity 默认锁死 SFW;apiKey 只回传布尔不回传明文; 保存时留空表示保留旧值;open 组错误信息对外脱敏。 - 配置:clientMaxBodySize 提到 64M(gf 默认 8M 会截断 20MB 原图); wallpaper.root / baseUrl 支持环境变量注入并在占位符未替换时兜底。 依赖:golang.org/x/image@v0.23.0(仅用于缩略图缩放,保持 go 1.23.0 不变)。
126 lines
3.9 KiB
Go
126 lines
3.9 KiB
Go
package wallpaper
|
||
|
||
import (
|
||
"context"
|
||
"os"
|
||
"testing"
|
||
"time"
|
||
|
||
"service.xpcool.com/internal/model/dto"
|
||
)
|
||
|
||
// 这一组测试会**真实访问各平台接口**,用于验证 JSON 路径解析与尺寸字段映射。
|
||
//
|
||
// 平台接口的字段名一旦变化,单元测试(打桩)是发现不了的,只有真连一次才知道。
|
||
// 因此保留它们,但默认跳过,只在需要时手动开启:
|
||
//
|
||
// WALLPAPER_LIVE=1 go test ./internal/service/wallpaper/ -run TestLive -v
|
||
//
|
||
// 需要 Key 的平台会自动跳过(除非另外提供了环境变量)。
|
||
func liveEnabled(t *testing.T) {
|
||
t.Helper()
|
||
if os.Getenv("WALLPAPER_LIVE") != "1" {
|
||
t.Skip("跳过联网测试:设置 WALLPAPER_LIVE=1 后运行")
|
||
}
|
||
}
|
||
|
||
// runLive 拉一页并检查基本约束。
|
||
func runLive(t *testing.T, p Provider, cfg dto.WallpaperSourceConfig, q dto.WallpaperQuery) []dto.WallpaperItem {
|
||
t.Helper()
|
||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||
defer cancel()
|
||
|
||
items, _, err := p.List(ctx, cfg, q)
|
||
if err != nil {
|
||
t.Fatalf("%s 拉取失败: %v", p.Code(), err)
|
||
}
|
||
if len(items) == 0 {
|
||
t.Fatalf("%s 未返回任何条目", p.Code())
|
||
}
|
||
for i, it := range items {
|
||
if it.ThumbUrl == "" || it.FullUrl == "" {
|
||
t.Errorf("%s 第 %d 条缺少图片地址: %+v", p.Code(), i, it)
|
||
}
|
||
if it.Width <= 0 || it.Height <= 0 {
|
||
t.Errorf("%s 第 %d 条尺寸异常: %dx%d", p.Code(), i, it.Width, it.Height)
|
||
}
|
||
if it.Orientation != orientationOf(it.Width, it.Height) {
|
||
t.Errorf("%s 第 %d 条朝向与尺寸不一致", p.Code(), i)
|
||
}
|
||
}
|
||
t.Logf("%s 取到 %d 条,示例:%s | 缩略 %s", p.Code(), len(items), items[0].Title, items[0].ThumbUrl)
|
||
return items
|
||
}
|
||
|
||
func TestLiveBing(t *testing.T) {
|
||
liveEnabled(t)
|
||
runLive(t, &BingProvider{}, dto.WallpaperSourceConfig{}, dto.WallpaperQuery{Page: 1, Size: 8})
|
||
}
|
||
|
||
func TestLivePicsum(t *testing.T) {
|
||
liveEnabled(t)
|
||
items := runLive(t, &PicsumProvider{}, dto.WallpaperSourceConfig{}, dto.WallpaperQuery{Page: 1, Size: 6})
|
||
|
||
// 顺带验证朝向过滤确实生效(Picsum 没有服务端筛选,全靠本地过滤)
|
||
portrait := runLive(t, &PicsumProvider{}, dto.WallpaperSourceConfig{},
|
||
dto.WallpaperQuery{Page: 1, Size: 3, Orientation: 2})
|
||
for i, it := range portrait {
|
||
if it.Width >= it.Height {
|
||
t.Errorf("竖版筛选失效:第 %d 条是 %dx%d", i, it.Width, it.Height)
|
||
}
|
||
}
|
||
_ = items
|
||
}
|
||
|
||
func TestLiveUnsplash(t *testing.T) {
|
||
liveEnabled(t)
|
||
key := os.Getenv("UNSPLASH_ACCESS_KEY")
|
||
if key == "" {
|
||
t.Skip("未提供 UNSPLASH_ACCESS_KEY,跳过")
|
||
}
|
||
runLive(t, &UnsplashProvider{}, dto.WallpaperSourceConfig{ApiKey: key},
|
||
dto.WallpaperQuery{Page: 1, Size: 5, Query: "mountain"})
|
||
}
|
||
|
||
func TestLivePexels(t *testing.T) {
|
||
liveEnabled(t)
|
||
key := os.Getenv("PEXELS_API_KEY")
|
||
if key == "" {
|
||
t.Skip("未提供 PEXELS_API_KEY,跳过")
|
||
}
|
||
runLive(t, &PexelsProvider{}, dto.WallpaperSourceConfig{ApiKey: key},
|
||
dto.WallpaperQuery{Page: 1, Size: 5, Query: "mountain"})
|
||
}
|
||
|
||
func TestLiveWallhaven(t *testing.T) {
|
||
liveEnabled(t)
|
||
// Wallhaven 的 SFW 检索无需 Key,故这里不带凭据也应能跑通
|
||
items := runLive(t, &WallhavenProvider{}, dto.WallpaperSourceConfig{},
|
||
dto.WallpaperQuery{Page: 1, Size: 5})
|
||
for i := range items {
|
||
if items[i].FullUrl == "" {
|
||
t.Errorf("第 %d 条缺少原图地址", i)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestLiveBingCopyrightSplit 校验版权串的拆分(纯函数,不联网也跑)。
|
||
func TestLiveBingCopyrightSplit(t *testing.T) {
|
||
cases := []struct {
|
||
in string
|
||
wantTitle string
|
||
wantAuthor string
|
||
}{
|
||
{"冰岛某瀑布 (© 张三/Getty Images)", "冰岛某瀑布", "张三/Getty Images"},
|
||
{"没有括号的版权串", "没有括号的版权串", ""},
|
||
{"", "", ""},
|
||
}
|
||
for _, c := range cases {
|
||
gotTitle, gotAuthor := splitBingCopyright(c.in)
|
||
if gotTitle != c.wantTitle || gotAuthor != c.wantAuthor {
|
||
t.Errorf("splitBingCopyright(%q) = (%q, %q),期望 (%q, %q)",
|
||
c.in, gotTitle, gotAuthor, c.wantTitle, c.wantAuthor)
|
||
}
|
||
}
|
||
}
|