service.xpcool.com/internal/controller/wallpaper/admin.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

204 lines
6.6 KiB
Go

package wallpaper
import (
"context"
"io"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
wallpaperv1 "service.xpcool.com/api/wallpaper"
"service.xpcool.com/internal/model/dto"
wallpapersvc "service.xpcool.com/internal/service/wallpaper"
)
// Controller 后台管理接口(走 RBAC 权限)。
type Controller struct{}
// New 构造后台控制器。
func New() *Controller { return &Controller{} }
// ---------------------------------------------------------------------------
// 自建图库
// ---------------------------------------------------------------------------
// List 图库列表(含已停用项,便于重新启用)。
func (c *Controller) List(ctx context.Context, req *wallpaperv1.AdminListReq) (res *wallpaperv1.AdminListRes, err error) {
q := dto.WallpaperQuery{
Query: req.Query,
Orientation: req.Orientation,
Page: req.Page,
Size: req.Size,
}
items, total, err := wallpapersvc.Wallpaper().LibraryAdminList(ctx, q, req.Enabled)
if err != nil {
return nil, err
}
return &wallpaperv1.AdminListRes{List: toAPIItems(items), Total: total}, nil
}
// Upload 批量上传壁纸。
//
// 刻意「逐文件返回结果」而不是整体成功/失败:一次传 20 张,其中 1 张格式不对时,
// 用户希望的是「19 张成功、1 张告诉我为什么失败」,而不是全部回滚重来。
func (c *Controller) Upload(ctx context.Context, req *wallpaperv1.AdminUploadReq) (res *wallpaperv1.AdminUploadRes, err error) {
if len(req.Files) == 0 {
return nil, gerror.New("没有收到文件")
}
out := make([]*wallpaperv1.UploadItem, 0, len(req.Files))
okCount, failCount := 0, 0
for _, f := range req.Files {
row := &wallpaperv1.UploadItem{FileName: f.Filename}
data, readErr := readUpload(f)
if readErr != nil {
row.Message = readErr.Error()
failCount++
out = append(out, row)
continue
}
item, duplicated, upErr := wallpapersvc.Wallpaper().Upload(ctx, f.Filename, data, dto.WallpaperSaveInput{
Title: req.Title,
Tags: req.Tags,
Category: req.Category,
Sort: req.Sort,
Enabled: 1,
})
if upErr != nil {
row.Message = upErr.Error()
failCount++
out = append(out, row)
continue
}
row.Ok = true
row.Duplicated = duplicated
row.Item = toAPIItem(item)
if duplicated {
row.Message = "内容重复,已指向图库中已有的图片"
} else {
row.Message = "上传成功"
}
okCount++
out = append(out, row)
}
return &wallpaperv1.AdminUploadRes{List: out, OkCount: okCount, FailCount: failCount}, nil
}
// readUpload 把一个上传文件读成字节切片。
//
// 先看 Size 再读:超限的文件直接拒绝,避免把一个 200MB 的文件读进内存才发现。
func readUpload(f *ghttp.UploadFile) ([]byte, error) {
if f == nil {
return nil, gerror.New("文件为空")
}
if f.Size <= 0 {
return nil, gerror.New("文件为空")
}
if f.Size > wallpapersvc.MaxUploadBytes {
return nil, gerror.Newf("文件超过 %d MB 上限", wallpapersvc.MaxUploadBytes>>20)
}
rc, err := f.Open()
if err != nil {
return nil, gerror.New("读取上传文件失败")
}
defer func() { _ = rc.Close() }()
data, err := io.ReadAll(io.LimitReader(rc, wallpapersvc.MaxUploadBytes+1))
if err != nil {
return nil, gerror.New("读取上传文件失败")
}
if int64(len(data)) > wallpapersvc.MaxUploadBytes {
return nil, gerror.Newf("文件超过 %d MB 上限", wallpapersvc.MaxUploadBytes>>20)
}
return data, nil
}
// Save 保存壁纸元数据。
func (c *Controller) Save(ctx context.Context, req *wallpaperv1.AdminSaveReq) (res *wallpaperv1.AdminSaveRes, err error) {
err = wallpapersvc.Wallpaper().SaveMeta(ctx, dto.WallpaperSaveInput{
Id: req.Id,
Title: req.Title,
Tags: req.Tags,
Category: req.Category,
Enabled: req.Enabled,
Sort: req.Sort,
Remark: req.Remark,
})
if err != nil {
return nil, err
}
return &wallpaperv1.AdminSaveRes{Message: "保存成功"}, nil
}
// Delete 删除壁纸。
func (c *Controller) Delete(ctx context.Context, req *wallpaperv1.AdminDeleteReq) (res *wallpaperv1.AdminDeleteRes, err error) {
if err = wallpapersvc.Wallpaper().Delete(ctx, req.Id); err != nil {
return nil, err
}
return &wallpaperv1.AdminDeleteRes{Message: "删除成功"}, nil
}
// Stats 图库概览。
func (c *Controller) Stats(ctx context.Context, req *wallpaperv1.AdminStatsReq) (res *wallpaperv1.AdminStatsRes, err error) {
st, err := wallpapersvc.Wallpaper().Stat(ctx)
if err != nil {
return nil, err
}
return &wallpaperv1.AdminStatsRes{
Total: st.Total,
Enabled: st.Enabled,
Disabled: st.Disabled,
Portrait: st.Portrait,
Landscape: st.Landscape,
TotalBytes: st.TotalBytes,
}, nil
}
// ---------------------------------------------------------------------------
// 开源平台配置
// ---------------------------------------------------------------------------
// SourceList 平台配置列表。
func (c *Controller) SourceList(ctx context.Context, req *wallpaperv1.AdminSourceListReq) (res *wallpaperv1.AdminSourceListRes, err error) {
list, err := wallpapersvc.Wallpaper().Sources(ctx)
if err != nil {
return nil, err
}
out := make([]*wallpaperv1.SourceInfo, 0, len(list))
for i := range list {
// 后台带配置:编辑界面需要回显 defaultQuery / purity / baseUrl 等,
// 否则用户只改开关也会把这些字段一并清空。
out = append(out, toAPISource(&list[i], true))
}
return &wallpaperv1.AdminSourceListRes{List: out}, nil
}
// SourceSave 保存平台配置。
func (c *Controller) SourceSave(ctx context.Context, req *wallpaperv1.AdminSourceSaveReq) (res *wallpaperv1.AdminSourceSaveRes, err error) {
err = wallpapersvc.Wallpaper().SaveSource(ctx, dto.WallpaperSourceSaveInput{
Code: req.Code,
Name: req.Name,
Enabled: req.Enabled,
Sort: req.Sort,
Remark: req.Remark,
Config: req.Config,
})
if err != nil {
return nil, err
}
return &wallpaperv1.AdminSourceSaveRes{Message: "保存成功"}, nil
}
// SourceTest 测试平台连通性。
func (c *Controller) SourceTest(ctx context.Context, req *wallpaperv1.AdminSourceTestReq) (res *wallpaperv1.AdminSourceTestRes, err error) {
msg, err := wallpapersvc.Wallpaper().TestSource(ctx, req.Code)
if err != nil {
// 测试失败是「预期内的结果」而不是接口异常,故把原因作为数据返回,
// 让前端能直接在界面上展示,而不是弹一个通用错误。
g.Log().Infof(ctx, "平台连通性测试未通过 code=%s: %v", req.Code, err)
return &wallpaperv1.AdminSourceTestRes{Ok: false, Message: err.Error()}, nil
}
return &wallpaperv1.AdminSourceTestRes{Ok: true, Message: msg}, nil
}