// Copyright 2026 Arctel.net // SPDX-License-Identifier: Apache-2.0 package handler import ( "Wavelet/core/contracts" "Wavelet/pkg/ginutil" "Wavelet/pkg/response" "Wavelet/plugins/domain/upload/ingest" "Wavelet/plugins/domain/upload/models" "Wavelet/plugins/domain/upload/repository" "Wavelet/plugins/domain/upload/shared" "errors" "net/http" "strconv" "github.com/gin-gonic/gin" uploadstorage "Wavelet/plugins/domain/upload/storage" ) type listFilesRequest struct { Page int `form:"page"` PageSize int `form:"page_size"` Keyword string `form:"keyword"` Type string `form:"type"` Extension string `form:"extension"` UserID uint64 `form:"user_id"` } type listFilesResponse struct { Total int64 `json:"total"` Page int `json:"page"` PageSize int `json:"page_size"` Items []models.Upload `json:"items"` } // ListFiles 获取系统上传的文件列表 // @Summary 获取文件列表 // @Description 分页获取系统上传的文件列表,支持文件名关键词、业务类型、扩展名、上传用户ID过滤 // @Tags admin // @Produce json // @Param page query int false "页码(默认 1)" // @Param page_size query int false "每页数量(默认 20,最大 100)" // @Param keyword query string false "文件名关键词(模糊匹配)" // @Param type query string false "业务分类过滤" // @Param extension query string false "扩展名过滤" // @Param user_id query int false "上传用户 ID 过滤" // @Security SessionCookie // @Success 200 {object} response.Any{data=listFilesResponse} "查询成功" // @Failure 400 {object} response.Any "参数错误" // @Router /api/v1/admin/uploads [get] // @Router /api/v1/admin/uploads/files [get] func ListFiles(c *gin.Context) { ctx := c.Request.Context() var req listFilesRequest if err := c.ShouldBindQuery(&req); err != nil { response.AbortBadRequest(c, shared.ErrInvalidParams) return } if req.Page <= 0 { req.Page = 1 } if req.PageSize <= 0 || req.PageSize > 100 { req.PageSize = 20 } total, items, err := listUploadFiles(ctx, repository.UploadListFilter{ UserID: req.UserID, Keyword: req.Keyword, Type: req.Type, Extension: req.Extension, Page: req.Page, PageSize: req.PageSize, }) if err != nil { response.AbortBadRequest(c, shared.ErrQueryFileListFailed) return } c.JSON(http.StatusOK, response.OK(listFilesResponse{ Total: total, Page: req.Page, PageSize: req.PageSize, Items: items, })) } // DeleteFile 软删除指定的文件记录 // @Summary 删除文件 // @Description 将指定 ID 的文件状态置为 deleted(软删除) // @Tags admin // @Produce json // @Param id path string true "文件 ID" // @Security SessionCookie // @Success 200 {object} response.Any "删除成功" // @Failure 404 {object} response.Any "文件不存在" // @Router /api/v1/admin/uploads/{id} [delete] // @Router /api/v1/admin/uploads/files/{id} [delete] func DeleteFile(c *gin.Context) { ctx := c.Request.Context() if uploadstorage.ReadOnly(ctx) { response.AbortConflict(c, shared.ErrStorageReadOnly) return } uploadID, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { response.AbortBadRequest(c, shared.ErrInvalidFileID) return } if _, err := softDeleteUpload(ctx, uploadID); err != nil { if isRecordNotFound(err) { response.AbortNotFound(c, shared.ErrFileRecordNotFound) return } response.AbortBadRequest(c, shared.ErrDeleteFileFailed) return } c.JSON(http.StatusOK, response.OKNil()) } // GetDistinctUploadTypes 获取所有已存在的文件业务分类列表 // @Summary 获取业务分类列表 // @Description 查询系统内所有不重复的上传业务分类标识(如 avatar, doc 等) // @Tags admin // @Produce json // @Security SessionCookie // @Success 200 {object} response.Any{data=[]string} "查询成功" // @Router /api/v1/admin/uploads/types [get] func GetDistinctUploadTypes(c *gin.Context) { ctx := c.Request.Context() types, err := listDistinctUploadTypes(ctx) if err != nil { response.AbortBadRequest(c, shared.ErrQueryTypeListFailed) return } c.JSON(http.StatusOK, response.OK(types)) } type listMyFilesRequest struct { Page int `form:"page"` PageSize int `form:"page_size"` Keyword string `form:"keyword"` Type string `form:"type"` Extension string `form:"extension"` } type listMyFilesResponse struct { Total int64 `json:"total"` Page int `json:"page"` PageSize int `json:"page_size"` Items []models.Upload `json:"items"` } // ListMyFiles 获取当前用户上传的文件列表 // @Summary 获取我的文件列表 // @Description 分页获取当前登录用户上传的文件,支持文件名关键词、业务类型、扩展名过滤 // @Tags upload // @Produce json // @Param page query int false "页码(默认 1)" // @Param page_size query int false "每页数量(默认 20,最大 100)" // @Param keyword query string false "文件名关键词(模糊匹配)" // @Param type query string false "业务分类过滤" // @Param extension query string false "扩展名过滤" // @Security SessionCookie // @Success 200 {object} response.Any{data=listMyFilesResponse} "查询成功" // @Failure 401 {object} response.Any "未登录" // @Router /api/v1/upload/my [get] func ListMyFiles(c *gin.Context) { currUser, _ := ginutil.GetFromContext[*contracts.UserDTO](c, contracts.AuthUserObjKey) ctx := c.Request.Context() var req listMyFilesRequest if err := c.ShouldBindQuery(&req); err != nil { response.AbortBadRequest(c, shared.ErrInvalidParams) return } if req.Page <= 0 { req.Page = 1 } if req.PageSize <= 0 || req.PageSize > 100 { req.PageSize = 20 } total, items, err := listMyUploadFiles(ctx, currUser.ID, repository.UploadListFilter{ Keyword: req.Keyword, Type: req.Type, Extension: req.Extension, Page: req.Page, PageSize: req.PageSize, }) if err != nil { response.AbortBadRequest(c, shared.ErrQueryFileListFailed) return } c.JSON(http.StatusOK, response.OK(listMyFilesResponse{ Total: total, Page: req.Page, PageSize: req.PageSize, Items: items, })) } // DeleteMyFile 软删除当前用户本人的文件 // @Summary 删除我的文件 // @Description 将当前用户本人的文件状态置为 deleted(软删除) // @Tags upload // @Produce json // @Param id path string true "文件 ID" // @Security SessionCookie // @Success 200 {object} response.Any "删除成功" // @Failure 403 {object} response.Any "无权操作" // @Failure 404 {object} response.Any "文件不存在" // @Router /api/v1/upload/{id} [delete] func DeleteMyFile(c *gin.Context) { currUser, _ := ginutil.GetFromContext[*contracts.UserDTO](c, contracts.AuthUserObjKey) ctx := c.Request.Context() if uploadstorage.ReadOnly(ctx) { response.AbortConflict(c, shared.ErrStorageReadOnly) return } uploadID, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { response.AbortBadRequest(c, shared.ErrInvalidFileID) return } if _, err := softDeleteOwnedUpload(ctx, currUser.ID, uploadID); err != nil { if isRecordNotFound(err) { response.AbortNotFound(c, shared.ErrFileRecordNotFound) return } if errors.Is(err, ingest.ErrForbidden) { response.AbortForbidden(c, shared.ErrOperationForbidden) return } response.AbortBadRequest(c, shared.ErrDeleteFileFailed) return } c.JSON(http.StatusOK, response.OKNil()) } type updateMyFileRequest struct { FileName string `json:"file_name" binding:"max=255"` AccessMode *int `json:"access_mode" binding:"omitempty,oneof=0 1"` } // UpdateMyFile 更新当前用户本人的文件信息 // @Summary 更新我的文件信息 // @Description 更新当前用户本人的文件名或访问权限模式 (AccessMode) // @Tags upload // @Accept json // @Produce json // @Param id path string true "文件 ID" // @Param request body updateMyFileRequest true "更新字段" // @Security SessionCookie // @Success 200 {object} response.Any{data=models.Upload} "更新成功" // @Failure 403 {object} response.Any "无权操作" // @Failure 404 {object} response.Any "文件不存在" // @Router /api/v1/upload/{id} [put] func UpdateMyFile(c *gin.Context) { currUser, _ := ginutil.GetFromContext[*contracts.UserDTO](c, contracts.AuthUserObjKey) ctx := c.Request.Context() if uploadstorage.ReadOnly(ctx) { response.AbortConflict(c, shared.ErrStorageReadOnly) return } uploadID, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { response.AbortBadRequest(c, shared.ErrInvalidFileID) return } var req updateMyFileRequest if err := c.ShouldBindJSON(&req); err != nil { response.AbortBadRequest(c, shared.ErrInvalidParams) return } updated, err := updateOwnedUpload(ctx, currUser.ID, uploadID, updateMyUploadInput(req)) if err != nil { if isRecordNotFound(err) { response.AbortNotFound(c, shared.ErrFileRecordNotFound) return } if errors.Is(err, ingest.ErrForbidden) { response.AbortForbidden(c, shared.ErrOperationForbidden) return } response.AbortBadRequest(c, shared.ErrUpdateFileFailed) return } c.JSON(http.StatusOK, response.OK(updated)) }