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: "尚未在后台启用", }) 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, 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 }