service.xpcool.com/api/wallpaper/wallpaper.go
夏犀麟 582485ba2b feat(wallpaper): 壁纸模块后端(开源平台插件 + 自建图库 + 前后台接口)
需求:前台壁纸站支持切换到开源壁纸平台,并能展示后台上传的自家图库,
保留原有「浏览器实时生成」能力(扩展而非替换),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 不变)。
2026-09-14 01:39:09 +08:00

237 lines
8.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package wallpaper_v1 壁纸模块接口契约。
//
// 分成两组:
// - open 组(/api/service/open/wallpaper/*):给 xpcool.com 前台调用,免鉴权;
// - admin 组(/api/service/admin/wallpaper/*):后台管理,走 RBAC 权限。
//
// 规范2026-08-27 起):全部 POSTURL 不含参数;入参一律 body。
// 唯一例外是「上传」——文件必须走 multipart其余字段随之走表单而不是 JSON。
package wallpaper
import "github.com/gogf/gf/v2/frame/g"
import "github.com/gogf/gf/v2/net/ghttp"
// ---------------------------------------------------------------------------
// 公共数据结构
// ---------------------------------------------------------------------------
// Item 是壁纸的统一输出条目。开源平台与自建图库填的是同一个结构,
// 前端因此只需要一套渲染逻辑。
type Item struct {
Id string `json:"id"`
Title string `json:"title"`
Width int `json:"width"`
Height int `json:"height"`
Orientation int `json:"orientation"` // 0未知 1横版 2竖版 3方形
ThumbUrl string `json:"thumbUrl"` // 列表网格用480px
PreviewUrl string `json:"previewUrl"` // 全屏展示用(长边 1920
FullUrl string `json:"fullUrl"` // 下载用(原图)
Source string `json:"source"`
SourceName string `json:"sourceName"`
FromOpen bool `json:"fromOpen"`
Author string `json:"author"`
AuthorUrl string `json:"authorUrl"`
PageUrl string `json:"pageUrl"`
License string `json:"license"`
Tags string `json:"tags"`
Category string `json:"category"`
Filesize int64 `json:"filesize"`
CreatedAt string `json:"createdAt"`
}
// SourceInfo 开源平台的可用性描述。
type SourceInfo struct {
Code string `json:"code"`
Name string `json:"name"`
Enabled int `json:"enabled"`
Sort int `json:"sort"`
Configured bool `json:"configured"` // 凭据是否齐备
Available bool `json:"available"` // 启用 且 凭据齐备
Hint string `json:"hint"` // 不可用原因
Remark string `json:"remark"`
HasApiKey bool `json:"hasApiKey"` // 只回传「是否已设置」,不回传明文
}
// ---------------------------------------------------------------------------
// open 组:前台
// ---------------------------------------------------------------------------
// OpenSourceListReq 取全部来源清单(含开源平台 + 自建图库计数)。
type OpenSourceListReq struct {
g.Meta `path:"/wallpaper/sources" method:"post" tags:"Open/Wallpaper" summary:"壁纸来源清单"`
}
type OpenSourceListRes struct {
List []*SourceInfo `json:"list"`
// MineCount 只统计「启用」的自建图片数量,为 0 时前台隐藏「我的图库」入口
MineCount int `json:"mineCount"`
}
// OpenListReq 按来源分页取图。
type OpenListReq struct {
g.Meta `path:"/wallpaper/list" method:"post" tags:"Open/Wallpaper" summary:"壁纸列表(按来源)"`
Source string `json:"source" d:"mine" dc:"来源编码mine=自建图库,其余为平台编码"`
Query string `json:"query" dc:"搜索词(仅部分平台与自建图库支持)"`
Orientation int `json:"orientation" dc:"0不限 1横版 2竖版 3方形"`
Tag string `json:"tag" dc:"标签(仅自建图库)"`
Page int `json:"page" d:"1"`
Size int `json:"size" d:"24"`
}
type OpenListRes struct {
List []*Item `json:"list"`
Total int `json:"total"`
Page int `json:"page"`
Size int `json:"size"`
}
// OpenRandomReq 跨来源随机取一张。
type OpenRandomReq struct {
g.Meta `path:"/wallpaper/random" method:"post" tags:"Open/Wallpaper" summary:"随机取一张壁纸"`
Sources []string `json:"sources" dc:"限定来源;为空则在所有可用来源中随机"`
Orientation int `json:"orientation" dc:"0不限 1横版 2竖版 3方形"`
}
type OpenRandomRes struct {
Item *Item `json:"item"`
}
// OpenTrackDownloadReq 通知平台「这张图被下载了」。
// 目前只有 Unsplash 需要(其 API 许可的硬性要求),其它平台为空操作。
type OpenTrackDownloadReq struct {
g.Meta `path:"/wallpaper/download-track" method:"post" tags:"Open/Wallpaper" summary:"下载回调(平台统计)"`
Source string `json:"source"`
Id string `json:"id"`
}
type OpenTrackDownloadRes struct {
Message string `json:"message"`
}
// ---------------------------------------------------------------------------
// admin 组:自建图库
// ---------------------------------------------------------------------------
// AdminListReq 后台图库列表(含已停用项)。
type AdminListReq struct {
g.Meta `path:"/wallpaper/list" method:"post" tags:"Admin/Wallpaper" summary:"图库列表(含停用)"`
Query string `json:"query"`
Orientation int `json:"orientation"`
Enabled int `json:"enabled" d:"-1" dc:"-1全部 0停用 1启用"`
Page int `json:"page" d:"1"`
Size int `json:"size" d:"24"`
}
type AdminListRes struct {
List []*Item `json:"list"`
Total int `json:"total"`
}
// AdminUploadReq 上传壁纸(文件走 multipart其余字段为表单字段
type AdminUploadReq struct {
g.Meta `path:"/wallpaper/upload" method:"post" mime:"multipart/form-data" tags:"Admin/Wallpaper" summary:"上传壁纸(可多选)"`
Files []*ghttp.UploadFile `json:"files" type:"file" dc:"图片文件,可多选"`
Title string `json:"title" dc:"标题,留空则用文件名"`
Tags string `json:"tags"`
Category string `json:"category"`
Sort int `json:"sort"`
}
// UploadItem 单个文件的处理结果。
// 批量上传时允许部分失败,故逐条返回,而不是整体成功/失败。
type UploadItem struct {
FileName string `json:"fileName"`
Ok bool `json:"ok"`
Duplicated bool `json:"duplicated"` // 内容重复,命中了已有图片(秒传)
Message string `json:"message"`
Item *Item `json:"item"`
}
type AdminUploadRes struct {
List []*UploadItem `json:"list"`
OkCount int `json:"okCount"`
FailCount int `json:"failCount"`
}
// AdminSaveReq 编辑壁纸元数据(不涉及文件)。
type AdminSaveReq struct {
g.Meta `path:"/wallpaper/save" method:"post" tags:"Admin/Wallpaper" summary:"保存壁纸信息"`
Id uint64 `json:"id" v:"required"`
Title string `json:"title"`
Tags string `json:"tags"`
Category string `json:"category"`
Enabled int `json:"enabled"`
Sort int `json:"sort"`
Remark string `json:"remark"`
}
type AdminSaveRes struct {
Message string `json:"message"`
}
// AdminDeleteReq 删除壁纸(同时清理原图与派生图)。
type AdminDeleteReq struct {
g.Meta `path:"/wallpaper/delete" method:"post" tags:"Admin/Wallpaper" summary:"删除壁纸"`
Id uint64 `json:"id" v:"required"`
}
type AdminDeleteRes struct {
Message string `json:"message"`
}
// AdminStatsReq 图库概览。
type AdminStatsReq struct {
g.Meta `path:"/wallpaper/stats" method:"post" tags:"Admin/Wallpaper" summary:"图库统计"`
}
type AdminStatsRes struct {
Total int `json:"total"`
Enabled int `json:"enabled"`
Disabled int `json:"disabled"`
Portrait int `json:"portrait"`
Landscape int `json:"landscape"`
TotalBytes int64 `json:"totalBytes"`
}
// ---------------------------------------------------------------------------
// admin 组:开源平台配置
// ---------------------------------------------------------------------------
// AdminSourceListReq 平台配置列表。
type AdminSourceListReq struct {
g.Meta `path:"/wallpaper/source/list" method:"post" tags:"Admin/Wallpaper" summary:"平台配置列表"`
}
type AdminSourceListRes struct {
List []*SourceInfo `json:"list"`
}
// AdminSourceSaveReq 保存平台配置。
type AdminSourceSaveReq struct {
g.Meta `path:"/wallpaper/source/save" method:"post" tags:"Admin/Wallpaper" summary:"保存平台配置"`
Code string `json:"code" v:"required"`
Name string `json:"name"`
Enabled int `json:"enabled"`
Sort int `json:"sort"`
Remark string `json:"remark"`
// Config 是平台配置的 JSON 字符串。
// 其中 apiKey 留空表示「不修改已保存的 Key」—— 界面不回传密钥明文,
// 用户只改开关时不必重新粘贴。
Config string `json:"config"`
}
type AdminSourceSaveRes struct {
Message string `json:"message"`
}
// AdminSourceTestReq 测试平台连通性。
type AdminSourceTestReq struct {
g.Meta `path:"/wallpaper/source/test" method:"post" tags:"Admin/Wallpaper" summary:"测试平台连通性"`
Code string `json:"code" v:"required"`
}
type AdminSourceTestRes struct {
Ok bool `json:"ok"`
Message string `json:"message"`
}