需求:前台壁纸站支持切换到开源壁纸平台,并能展示后台上传的自家图库, 保留原有「浏览器实时生成」能力(扩展而非替换),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 不变)。
124 lines
4.1 KiB
Go
124 lines
4.1 KiB
Go
package wallpaper
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"time"
|
||
|
||
"github.com/gogf/gf/v2/encoding/gjson"
|
||
|
||
"service.xpcool.com/internal/model/dto"
|
||
)
|
||
|
||
// PicsumProvider Lorem Picsum 随机摄影图库。
|
||
//
|
||
// 特点:无需 API Key、量极大、支持按 id 取任意尺寸。
|
||
// 图片本身来自 Unsplash,故作者信息与许可沿用 Unsplash。
|
||
//
|
||
// 局限:接口不支持关键词搜索、也不支持朝向筛选,两者都只能取回后在本地过滤。
|
||
// 为了过滤后仍有足够条目,这里会一次多取一些(接口上限 100 条)。
|
||
type PicsumProvider struct{}
|
||
|
||
const picsumBase = "https://picsum.photos"
|
||
|
||
func (p *PicsumProvider) Code() string { return "picsum" }
|
||
func (p *PicsumProvider) Name() string { return "Lorem Picsum" }
|
||
func (p *PicsumProvider) RequiresKey() bool { return false }
|
||
|
||
func (p *PicsumProvider) List(ctx context.Context, cfg dto.WallpaperSourceConfig, q dto.WallpaperQuery) ([]dto.WallpaperItem, int, error) {
|
||
page, size := normalizePage(q.Page, q.Size)
|
||
|
||
key := cacheKey(p.Code(), q)
|
||
if v, err := openCache.Get(ctx, key); err == nil && !v.IsNil() {
|
||
if cached, ok := v.Val().([]dto.WallpaperItem); ok {
|
||
return cached, len(cached), nil
|
||
}
|
||
}
|
||
|
||
// 需要本地过滤(搜索词 / 朝向)时必须多取:该接口的列表是按 id 顺序返回的,
|
||
// 前几页几乎全是横图,只多取三四倍的话筛竖版经常一条都不剩(实测踩过)。
|
||
// 故只要带过滤条件就直接取满接口上限 100 条。
|
||
fetch := size
|
||
if q.Query != "" || q.Orientation != 0 {
|
||
fetch = 100
|
||
}
|
||
|
||
base := baseOf(cfg.BaseUrl, picsumBase)
|
||
url := fmt.Sprintf("%s/v2/list?page=%d&limit=%d", base, page, fetch)
|
||
j, err := httpGetJSON(ctx, url, nil, 12*time.Second)
|
||
if err != nil {
|
||
return nil, 0, fmt.Errorf("获取 Picsum 列表失败: %w", err)
|
||
}
|
||
// 该接口的响应体是**根数组**(没有外层对象),故用 Array() 逐项包装成 gjson。
|
||
// 注意 gjson.Json 自身没有 IsArray(),判类型要走 Var().IsSlice()。
|
||
if !j.Var().IsSlice() {
|
||
return nil, 0, fmt.Errorf("Picsum 响应格式异常:期望数组")
|
||
}
|
||
|
||
out := make([]dto.WallpaperItem, 0, size)
|
||
for _, raw := range j.Array() {
|
||
item := gjson.New(raw)
|
||
id := item.Get("id").String()
|
||
if id == "" {
|
||
continue
|
||
}
|
||
w := item.Get("width").Int()
|
||
h := item.Get("height").Int()
|
||
if !matchOrientation(w, h, q.Orientation) {
|
||
continue
|
||
}
|
||
author := item.Get("author").String()
|
||
// Picsum 不提供标题,用「作者 (#id)」拼一个可辨识的标题
|
||
title := author
|
||
if title == "" {
|
||
title = "Picsum #" + id
|
||
} else {
|
||
title = fmt.Sprintf("%s (#%s)", author, id)
|
||
}
|
||
// 关键词只能本地匹配(作者名 / 标题),该平台没有搜索能力
|
||
if q.Query != "" && !containsFold(author, q.Query) && !containsFold(title, q.Query) {
|
||
continue
|
||
}
|
||
if len(out) >= size {
|
||
break
|
||
}
|
||
out = append(out, dto.WallpaperItem{
|
||
Id: id,
|
||
Title: title,
|
||
Width: w,
|
||
Height: h,
|
||
Orientation: orientationOf(w, h),
|
||
// 按原图比例缩出目标宽度,避免被接口裁剪改变构图
|
||
ThumbUrl: picsumScaleURL(base, id, 480, w, h),
|
||
PreviewUrl: picsumScaleURL(base, id, 1600, w, h),
|
||
FullUrl: item.Get("download_url").String(),
|
||
Source: p.Code(),
|
||
SourceName: p.Name(),
|
||
FromOpen: true,
|
||
Author: author,
|
||
AuthorUrl: item.Get("url").String(),
|
||
PageUrl: item.Get("url").String(),
|
||
License: "Unsplash License(来自 Lorem Picsum)",
|
||
})
|
||
}
|
||
|
||
_ = openCache.Set(ctx, key, out, cacheTTLSearch)
|
||
return out, len(out), nil
|
||
}
|
||
|
||
// picsumScaleURL 生成 Picsum 的等比缩放地址。
|
||
//
|
||
// Picsum 的 /id/{id}/{w}/{h} 会把图**裁剪**到给定尺寸,
|
||
// 直接传 16:9 会切掉竖图的两头,所以这里按原图比例算出高度 —— 纯缩放、不裁剪。
|
||
func picsumScaleURL(base, id string, targetW, w, h int) string {
|
||
if w <= 0 || h <= 0 {
|
||
return fmt.Sprintf("%s/id/%s/%d/%d", base, id, targetW, targetW*9/16)
|
||
}
|
||
if targetW >= w {
|
||
// 原图比目标还小就别放大了,直接用原尺寸
|
||
return fmt.Sprintf("%s/id/%s/%d/%d", base, id, w, h)
|
||
}
|
||
targetH := h * targetW / w
|
||
return fmt.Sprintf("%s/id/%s/%d/%d", base, id, targetW, targetH)
|
||
}
|