service.xpcool.com/internal/service/wallpaper/source.go
夏犀麟 d0a084b80e
Some checks failed
Build and Deploy (service.xpcool.com) / build-and-deploy (push) Failing after 28s
feat(wallpaper): 平台配置脱敏回显 + 图库元字段补齐
后台做完界面才发现的两个接口缺口:

1) 平台配置不回显 → 用户只改一个开关,defaultQuery / purity / baseUrl
   会被一起清空(SaveSource 只对 apiKey/apiSecret 做了"留空即保留")。
   现在 Sources() 返回脱敏后的 config(凭据一律置空,是否已设置由
   HasApiKey 表达),前台接口不下发该字段 —— toAPISource 加 withConfig
   开关,公开接口没必要泄露自建反代地址等内部信息。

2) Item 缺 enabled / sort / remark → 后台列表无法显示启用状态,
   编辑抽屉也拿不到当前值。补齐字段并在 library.toItem 中填充,
   开源平台条目保持默认值(这三个字段对前台无意义)。

另抽出 sanitizeSourceConfig 统一处理凭据抹除,避免以后再加配置项时漏抹。
2026-09-14 01:48:23 +08:00

233 lines
7.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
import (
"context"
"fmt"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/errors/gerror"
"service.xpcool.com/internal/dao"
"service.xpcool.com/internal/model/do"
"service.xpcool.com/internal/model/dto"
"service.xpcool.com/internal/model/entity"
)
// Sources 返回所有开源平台的可用性信息。
//
// 合并两个来源:数据库里的配置行 + 代码里注册的适配器。
// 这样「新写了一个适配器但库里还没记录」的平台也会出现在后台列表里,
// 用户可以直接开启,不需要先手工插数据。
func (s *service) Sources(ctx context.Context) ([]dto.WallpaperSourceInfo, error) {
rows, err := dao.WallpaperSource.Ctx(ctx).OrderAsc("sort").OrderAsc("id").All()
if err != nil {
return nil, gerror.Wrap(err, "查询壁纸平台配置失败")
}
// 库里已有的配置,按 code 索引
byCode := make(map[string]entity.WallpaperSource, len(rows))
for _, r := range rows {
var e entity.WallpaperSource
if err = r.Struct(&e); err != nil {
continue
}
byCode[e.Code] = e
}
out := make([]dto.WallpaperSourceInfo, 0, len(providerMap))
seen := make(map[string]bool, len(providerMap))
for _, p := range ListProviders() {
seen[p.Code()] = true
e, ok := byCode[p.Code()]
if !ok {
// 库里没有:给出可用但「未启用」的默认态,提示去后台开启
out = append(out, dto.WallpaperSourceInfo{
Code: p.Code(),
Name: p.Name(),
Enabled: 0,
Configured: !p.RequiresKey(),
Available: false,
Hint: "尚未在后台启用",
Config: sanitizeSourceConfig(dto.WallpaperSourceConfig{}),
})
continue
}
cfg := ParseSourceConfig(e.Config)
configured := !p.RequiresKey() || cfg.ApiKey != ""
info := dto.WallpaperSourceInfo{
Code: p.Code(),
Name: firstNonEmpty(e.Name, p.Name()),
Enabled: e.Enabled,
Sort: e.Sort,
Configured: configured,
Available: e.Enabled == 1 && configured,
Remark: e.Remark,
Config: sanitizeSourceConfig(cfg),
HasApiKey: cfg.ApiKey != "",
}
switch {
case e.Enabled != 1:
info.Hint = "已停用"
case !configured:
info.Hint = "需要配置 API Key"
}
out = append(out, info)
}
// 库里配了但代码里没有适配器的(比如后端降级回滚过)也列出来,避免「看不到所以查不到」
for code, e := range byCode {
if seen[code] {
continue
}
out = append(out, dto.WallpaperSourceInfo{
Code: code,
Name: e.Name,
Enabled: e.Enabled,
Sort: e.Sort,
Available: false,
Hint: "后端未注册该平台的适配器",
Remark: e.Remark,
})
}
return out, nil
}
// AvailableSourceCodes 返回当前可用(已启用且已配置)的平台编码集合。
// 供前台「随机一张」在前端未指定来源时挑一个用。
func (s *service) AvailableSourceCodes(ctx context.Context) ([]string, error) {
infos, err := s.Sources(ctx)
if err != nil {
return nil, err
}
codes := make([]string, 0, len(infos))
for _, i := range infos {
if i.Available {
codes = append(codes, i.Code)
}
}
return codes, nil
}
// LoadSourceConfig 读取某平台的配置。
// 平台不存在时返回错误;平台存在但未启用时也照常返回配置(调用方自行判断)。
func (s *service) LoadSourceConfig(ctx context.Context, code string) (dto.WallpaperSourceConfig, entity.WallpaperSource, error) {
var e entity.WallpaperSource
one, err := dao.WallpaperSource.Ctx(ctx).Where(do.WallpaperSource{Code: code}).One()
if err != nil {
return dto.WallpaperSourceConfig{}, e, gerror.Wrap(err, "查询壁纸平台配置失败")
}
if one.IsEmpty() {
return dto.WallpaperSourceConfig{}, e, gerror.Newf("平台 %s 未配置", code)
}
if err = one.Struct(&e); err != nil {
return dto.WallpaperSourceConfig{}, e, gerror.Wrap(err, "解析壁纸平台配置失败")
}
return ParseSourceConfig(e.Config), e, nil
}
// SaveSource 保存(新增或更新)平台配置。
//
// 关键处理:入参里的 apiKey 若为空,**保留库里已有的值**。
// 后台列表只回传「是否已设置」而不回传明文,用户只想改开关或排序时
// 不必重新粘贴一遍密钥,也不会因为一次误操作把 Key 清掉。
func (s *service) SaveSource(ctx context.Context, in dto.WallpaperSourceSaveInput) error {
if in.Code == "" {
return gerror.New("平台编码不能为空")
}
if _, ok := GetProvider(in.Code); !ok {
return gerror.Newf("未知平台:%s", in.Code)
}
incoming := ParseSourceConfig(in.Config)
old, _, err := s.LoadSourceConfig(ctx, in.Code)
if err == nil && incoming.ApiKey == "" {
incoming.ApiKey = old.ApiKey
}
if err == nil && incoming.ApiSecret == "" {
incoming.ApiSecret = old.ApiSecret
}
encoded, err := gjson.Encode(incoming)
if err != nil {
return gerror.Wrap(err, "平台配置序列化失败")
}
data := do.WallpaperSource{
Code: in.Code,
Name: in.Name,
Enabled: in.Enabled,
Sort: in.Sort,
Config: string(encoded),
Remark: in.Remark,
}
// 先按 code 更新,没有受影响行再插入 —— 避免依赖「主键自增 + 唯一键冲突」的写法。
n, err := dao.WallpaperSource.Ctx(ctx).Where(do.WallpaperSource{Code: in.Code}).Count()
if err != nil {
return gerror.Wrap(err, "检查平台配置是否存在失败")
}
if n > 0 {
if _, err = dao.WallpaperSource.Ctx(ctx).Where(do.WallpaperSource{Code: in.Code}).Data(data).Update(); err != nil {
return gerror.Wrap(err, "更新平台配置失败")
}
return nil
}
if _, err = dao.WallpaperSource.Ctx(ctx).Data(data).Insert(); err != nil {
return gerror.Wrap(err, "新增平台配置失败")
}
return nil
}
// TestSource 试拉一条,用于后台的「测试连通性」按钮。
// 返回一句给人看的结果描述。
func (s *service) TestSource(ctx context.Context, code string) (string, error) {
p, ok := GetProvider(code)
if !ok {
return "", gerror.Newf("未知平台:%s", code)
}
cfg, e, err := s.LoadSourceConfig(ctx, code)
if err != nil {
// 库里没有配置也允许测:用空配置试一次,能通就说明零配置可用
cfg = dto.WallpaperSourceConfig{}
} else if e.Enabled != 1 {
return "", gerror.Newf("平台「%s」当前是停用状态请先启用再测试", firstNonEmpty(e.Name, p.Name()))
}
if p.RequiresKey() && cfg.ApiKey == "" {
return "", gerror.Newf("平台「%s」需要先填写 API Key", p.Name())
}
items, _, err := p.List(ctx, cfg, dto.WallpaperQuery{Page: 1, Size: 1})
if err != nil {
return "", err
}
if len(items) == 0 {
return fmt.Sprintf("接口连通,但未返回任何图片(可能是筛选条件把结果过滤空了):%s", p.Name()), nil
}
return fmt.Sprintf("连通正常,取到示例:%s", firstNonEmpty(items[0].Title, items[0].Id)), nil
}
// ParseSourceConfig 解析 wallpaper_source.config 的 JSON。
//
// **解析失败不报错,回退全默认值** —— 一个平台的脏配置不应该让整个来源列表挂掉,
// 那种「一处配置写错导致整站壁纸不可用」的故障非常难排查。
func ParseSourceConfig(raw string) dto.WallpaperSourceConfig {
var cfg dto.WallpaperSourceConfig
if raw == "" {
return cfg
}
if err := gjson.DecodeTo(raw, &cfg); err != nil {
return dto.WallpaperSourceConfig{}
}
return cfg
}
// sanitizeSourceConfig 生成可安全下发给前端的配置副本:抹掉全部凭据。
//
// 后台需要它来「回显」用户此前填过的 defaultQuery / purity / baseUrl
// 否则用户只想改个开关也会把这些字段一起清空;
// 但凭据明文不能出后端,故这里只清 apiKey / apiSecret
// 是否已设置改由 HasApiKey 布尔表达。
func sanitizeSourceConfig(cfg dto.WallpaperSourceConfig) *dto.WallpaperSourceConfig {
cfg.ApiKey = ""
cfg.ApiSecret = ""
return &cfg
}