refactor(storage): move file management routes from user to admin namespace

- Remove file list, stats, download, and deletion routes from '/api/v1/upload'
- Move these endpoints under '/api/v1/admin/uploads'
- Remove user-specific filtering from files query and statistics to aggregate system-wide uploads by default
- Allow admins to bypass ownership check when downloading private files
- Update backend unit tests, Swagger documentation, and frontend service client and components
This commit is contained in:
ryan
2026-06-13 16:04:21 +08:00
parent dbabe8b8d7
commit 8b19ffed90
13 changed files with 908 additions and 818 deletions
+20 -21
View File
@@ -10,7 +10,6 @@ import (
"strconv"
"strings"
"github.com/Rain-kl/Wavelet/internal/apps/oauth"
"github.com/Rain-kl/Wavelet/internal/db"
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/Rain-kl/Wavelet/internal/util"
@@ -18,40 +17,42 @@ import (
"gorm.io/gorm"
)
type listMyFilesRequest struct {
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 listMyFilesResponse struct {
type listFilesResponse struct {
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
Items []model.Upload `json:"items"`
}
// ListMyFiles 获取当前用户上传的文件列表
// @Summary 获取我的文件列表
// @Description 分页获取当前登录用户上传的文件,支持文件名关键词、业务类型、扩展名过滤
// @Tags upload
// 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 uint64 false "上传用户 ID"
// @Security SessionCookie
// @Success 200 {object} util.ResponseAny{data=listMyFilesResponse} "查询成功"
// @Success 200 {object} util.ResponseAny{data=listFilesResponse} "查询成功"
// @Failure 401 {object} util.ResponseAny "未登录"
// @Router /api/v1/upload/my [get]
func ListMyFiles(c *gin.Context) {
currUser, _ := util.GetFromContext[*model.User](c, oauth.UserObjKey)
// @Failure 403 {object} util.ResponseAny "无管理员权限"
// @Router /api/v1/admin/uploads [get]
func ListFiles(c *gin.Context) {
ctx := c.Request.Context()
var req listMyFilesRequest
var req listFilesRequest
if err := c.ShouldBindQuery(&req); err != nil {
c.JSON(http.StatusOK, util.Err(ErrInvalidParams))
return
@@ -64,8 +65,11 @@ func ListMyFiles(c *gin.Context) {
}
query := db.DB(ctx).Model(&model.Upload{}).
Where("user_id = ? AND status != ?", currUser.ID, model.UploadStatusDeleted)
Where("status != ?", model.UploadStatusDeleted)
if req.UserID != 0 {
query = query.Where("user_id = ?", req.UserID)
}
if req.Keyword != "" {
query = query.Where("LOWER(file_name) LIKE ?", "%"+strings.ToLower(req.Keyword)+"%")
}
@@ -89,7 +93,7 @@ func ListMyFiles(c *gin.Context) {
return
}
c.JSON(http.StatusOK, util.OK(listMyFilesResponse{
c.JSON(http.StatusOK, util.OK(listFilesResponse{
Total: total,
Page: req.Page,
PageSize: req.PageSize,
@@ -100,16 +104,15 @@ func ListMyFiles(c *gin.Context) {
// DeleteFile 软删除文件记录
// @Summary 删除文件
// @Description 将文件状态置为 deleted(软删除),不会立即清理底层存储对象
// @Tags upload
// @Tags admin
// @Produce json
// @Param id path string true "文件 ID"
// @Security SessionCookie
// @Success 200 {object} util.ResponseAny "删除成功"
// @Failure 403 {object} util.ResponseAny "无权操作"
// @Failure 404 {object} util.ResponseAny "文件不存在"
// @Router /api/v1/upload/{id} [delete]
// @Router /api/v1/admin/uploads/{id} [delete]
func DeleteFile(c *gin.Context) {
currUser, _ := util.GetFromContext[*model.User](c, oauth.UserObjKey)
ctx := c.Request.Context()
if storageReadOnly(ctx) {
c.JSON(http.StatusConflict, util.Err(ErrStorageReadOnly))
@@ -131,10 +134,6 @@ func DeleteFile(c *gin.Context) {
c.JSON(http.StatusOK, util.Err(ErrQueryUploadRecordFailed))
return
}
if upload.UserID != currUser.ID && !currUser.IsAdmin {
c.AbortWithStatus(http.StatusForbidden)
return
}
if err := db.DB(ctx).Model(&upload).Update("status", model.UploadStatusDeleted).Error; err != nil {
c.JSON(http.StatusOK, util.Err(ErrDeleteFileFailed))
return