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 }