// Package filehdl 是文件上传服务的 HTTP 接入层,分别提供后台 admin 与小程序 ma 两套入口。 package filehdl import ( "net/http" "strconv" "github.com/gin-gonic/gin" "portal-mini-go/internal/middleware" "portal-mini-go/internal/pkg/errors" "portal-mini-go/internal/pkg/response" filesvc "portal-mini-go/internal/service/file" ) // Handler 文件上传请求处理。 type Handler struct { svc *filesvc.Service } func NewHandler(svc *filesvc.Service) *Handler { return &Handler{svc: svc} } // 表单字段名。 const formFieldFile = "file" // Upload 接收 multipart 单文件上传。上传者身份由路由分组决定(admin 或 ma)。 func (h *Handler) Upload(c *gin.Context) { fh, err := c.FormFile(formFieldFile) if err != nil { c.Error(errors.ErrBadRequest) return } f, err := fh.Open() if err != nil { c.Error(errors.Wrap(50000, "open uploaded file", err)) return } defer f.Close() typ, id := resolveUploader(c) if id == 0 { c.Error(errors.ErrUnauthorized) return } res, err := h.svc.Upload(c.Request.Context(), filesvc.UploadInput{ Reader: f, Size: fh.Size, Filename: fh.Filename, UploaderType: typ, UploaderID: id, }) if err != nil { c.Error(err) return } response.OK(c, res) } // Info 返回文件元数据。 func (h *Handler) Info(c *gin.Context) { id, ok := parseID(c) if !ok { return } f, err := h.svc.Get(c.Request.Context(), id) if err != nil { c.Error(err) return } response.OK(c, f) } // List 分页列表查询文件(后台文件管理栏目使用)。支持 query:keyword(原始/落盘文件名)、 // uploader_type、uploader_id、page、size。 func (h *Handler) List(c *gin.Context) { page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) size, _ := strconv.Atoi(c.DefaultQuery("size", "20")) keyword := c.Query("keyword") typ := c.Query("uploader_type") var uid uint64 if v := c.Query("uploader_id"); v != "" { uid, _ = strconv.ParseUint(v, 10, 64) } list, total, err := h.svc.List(c.Request.Context(), filesvc.ListReq{ UploaderType: typ, UploaderID: uid, Keyword: keyword, Page: page, Size: size, }) if err != nil { c.Error(err) return } response.OK(c, gin.H{"list": list, "total": total, "page": page, "size": size}) } // Raw 以流形式返回文件内容,按 MIME 设置 Content-Type,供预览/下载。 func (h *Handler) Raw(c *gin.Context) { id, ok := parseID(c) if !ok { return } rc, f, err := h.svc.Download(c.Request.Context(), id) if err != nil { c.Error(err) return } defer rc.Close() extra := map[string]string{ "Content-Disposition": `inline; filename="` + f.Filename + `"`, } c.DataFromReader(http.StatusOK, f.Size, f.MimeType, rc, extra) } // Delete 删除文件(存储内容 + 元数据)。 func (h *Handler) Delete(c *gin.Context) { id, ok := parseID(c) if !ok { return } if err := h.svc.Delete(c.Request.Context(), id); err != nil { c.Error(err) return } response.OK(c, gin.H{"id": id}) } // resolveUploader 从 context 解析上传者身份:后台走 admin_id,小程序走 user_id。 // 两者均不存在(未认证)返回 ("", 0),由调用方拒绝。 func resolveUploader(c *gin.Context) (string, uint64) { if aid, ok := middleware.AdminIDUint(c); ok && aid != 0 { return "admin", aid } if uid := middleware.UserID(c); uid != 0 { return "ma", uid } return "", 0 } func parseID(c *gin.Context) (uint64, bool) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { c.Error(errors.ErrBadRequest) return 0, false } return id, true } // RegisterAdminUpload 在已鉴权(AdminAuth + PermissionLoader)的后台分组下挂载上传接口。 func RegisterAdminUpload(g *gin.RouterGroup, h *Handler) { rg := g.Group("/files") { rg.GET("", h.List) rg.POST("/upload", h.Upload) rg.GET("/:id", h.Info) rg.GET("/:id/raw", h.Raw) rg.DELETE("/:id", h.Delete) } } // RegisterMiniAppUpload 在已鉴权(MiniAppAuth + MiniUserAuth)的小程序分组下挂载上传接口。 func RegisterMiniAppUpload(g *gin.RouterGroup, h *Handler) { rg := g.Group("/files") { rg.POST("/upload", h.Upload) rg.GET("/:id", h.Info) rg.GET("/:id/raw", h.Raw) rg.DELETE("/:id", h.Delete) } }