mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-09 09:06:36 +08:00
feat(storage): implement user-group level file management dashboard and APIs
- Expose user-scoped CRUD APIs under `/api/v1/upload` (my files query, stats, rename, delete) - Update backend handlers and routers with ownership validation checks - Create a dedicated frontend personal file manager card-list and upload button under `/files` - Add comprehensive backend test coverage and update API docs
This commit is contained in:
+225
@@ -3944,6 +3944,195 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/upload/my": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"SessionCookie": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "分页获取当前登录用户上传的文件,支持文件名关键词、业务类型、扩展名过滤",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"upload"
|
||||||
|
],
|
||||||
|
"summary": "获取我的文件列表",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "页码(默认 1)",
|
||||||
|
"name": "page",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "每页数量(默认 20,最大 100)",
|
||||||
|
"name": "page_size",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "文件名关键词(模糊匹配)",
|
||||||
|
"name": "keyword",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "业务分类过滤",
|
||||||
|
"name": "type",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "扩展名过滤",
|
||||||
|
"name": "extension",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "查询成功",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/util.ResponseAny"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/definitions/upload.listMyFilesResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "未登录",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/util.ResponseAny"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/upload/{id}": {
|
||||||
|
"put": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"SessionCookie": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "更新当前用户本人的文件名或访问权限模式 (AccessMode)",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"upload"
|
||||||
|
],
|
||||||
|
"summary": "更新我的文件信息",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "文件 ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "更新字段",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/upload.updateMyFileRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "更新成功",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/util.ResponseAny"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/definitions/model.Upload"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "无权操作",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/util.ResponseAny"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "文件不存在",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/util.ResponseAny"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"SessionCookie": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "将当前用户本人的文件状态置为 deleted(软删除)",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"upload"
|
||||||
|
],
|
||||||
|
"summary": "删除我的文件",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "文件 ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "删除成功",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/util.ResponseAny"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "无权操作",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/util.ResponseAny"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "文件不存在",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/util.ResponseAny"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/user-info": {
|
"/api/v1/user-info": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -6022,6 +6211,26 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"upload.listMyFilesResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"items": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/model.Upload"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"page": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"page_size": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"upload.trendItem": {
|
"upload.trendItem": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -6036,6 +6245,22 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"upload.updateMyFileRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"access_mode": {
|
||||||
|
"type": "integer",
|
||||||
|
"enum": [
|
||||||
|
0,
|
||||||
|
1
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"file_name": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 255
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"user.changePasswordRequest": {
|
"user.changePasswordRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -3937,6 +3937,195 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/upload/my": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"SessionCookie": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "分页获取当前登录用户上传的文件,支持文件名关键词、业务类型、扩展名过滤",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"upload"
|
||||||
|
],
|
||||||
|
"summary": "获取我的文件列表",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "页码(默认 1)",
|
||||||
|
"name": "page",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "每页数量(默认 20,最大 100)",
|
||||||
|
"name": "page_size",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "文件名关键词(模糊匹配)",
|
||||||
|
"name": "keyword",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "业务分类过滤",
|
||||||
|
"name": "type",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "扩展名过滤",
|
||||||
|
"name": "extension",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "查询成功",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/util.ResponseAny"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/definitions/upload.listMyFilesResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "未登录",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/util.ResponseAny"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/upload/{id}": {
|
||||||
|
"put": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"SessionCookie": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "更新当前用户本人的文件名或访问权限模式 (AccessMode)",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"upload"
|
||||||
|
],
|
||||||
|
"summary": "更新我的文件信息",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "文件 ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "更新字段",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/upload.updateMyFileRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "更新成功",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/util.ResponseAny"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/definitions/model.Upload"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "无权操作",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/util.ResponseAny"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "文件不存在",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/util.ResponseAny"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"SessionCookie": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "将当前用户本人的文件状态置为 deleted(软删除)",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"upload"
|
||||||
|
],
|
||||||
|
"summary": "删除我的文件",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "文件 ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "删除成功",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/util.ResponseAny"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "无权操作",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/util.ResponseAny"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "文件不存在",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/util.ResponseAny"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/user-info": {
|
"/api/v1/user-info": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -6015,6 +6204,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"upload.listMyFilesResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"items": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/model.Upload"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"page": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"page_size": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"upload.trendItem": {
|
"upload.trendItem": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -6029,6 +6238,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"upload.updateMyFileRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"access_mode": {
|
||||||
|
"type": "integer",
|
||||||
|
"enum": [
|
||||||
|
0,
|
||||||
|
1
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"file_name": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 255
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"user.changePasswordRequest": {
|
"user.changePasswordRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -938,6 +938,19 @@ definitions:
|
|||||||
total:
|
total:
|
||||||
type: integer
|
type: integer
|
||||||
type: object
|
type: object
|
||||||
|
upload.listMyFilesResponse:
|
||||||
|
properties:
|
||||||
|
items:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/model.Upload'
|
||||||
|
type: array
|
||||||
|
page:
|
||||||
|
type: integer
|
||||||
|
page_size:
|
||||||
|
type: integer
|
||||||
|
total:
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
upload.trendItem:
|
upload.trendItem:
|
||||||
properties:
|
properties:
|
||||||
count:
|
count:
|
||||||
@@ -947,6 +960,17 @@ definitions:
|
|||||||
size:
|
size:
|
||||||
type: integer
|
type: integer
|
||||||
type: object
|
type: object
|
||||||
|
upload.updateMyFileRequest:
|
||||||
|
properties:
|
||||||
|
access_mode:
|
||||||
|
enum:
|
||||||
|
- 0
|
||||||
|
- 1
|
||||||
|
type: integer
|
||||||
|
file_name:
|
||||||
|
maxLength: 255
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
user.changePasswordRequest:
|
user.changePasswordRequest:
|
||||||
properties:
|
properties:
|
||||||
new_password:
|
new_password:
|
||||||
@@ -3470,6 +3494,121 @@ paths:
|
|||||||
summary: 上传文件
|
summary: 上传文件
|
||||||
tags:
|
tags:
|
||||||
- upload
|
- upload
|
||||||
|
/api/v1/upload/{id}:
|
||||||
|
delete:
|
||||||
|
description: 将当前用户本人的文件状态置为 deleted(软删除)
|
||||||
|
parameters:
|
||||||
|
- description: 文件 ID
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: 删除成功
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/util.ResponseAny'
|
||||||
|
"403":
|
||||||
|
description: 无权操作
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/util.ResponseAny'
|
||||||
|
"404":
|
||||||
|
description: 文件不存在
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/util.ResponseAny'
|
||||||
|
security:
|
||||||
|
- SessionCookie: []
|
||||||
|
summary: 删除我的文件
|
||||||
|
tags:
|
||||||
|
- upload
|
||||||
|
put:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: 更新当前用户本人的文件名或访问权限模式 (AccessMode)
|
||||||
|
parameters:
|
||||||
|
- description: 文件 ID
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- description: 更新字段
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/upload.updateMyFileRequest'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: 更新成功
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/util.ResponseAny'
|
||||||
|
- properties:
|
||||||
|
data:
|
||||||
|
$ref: '#/definitions/model.Upload'
|
||||||
|
type: object
|
||||||
|
"403":
|
||||||
|
description: 无权操作
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/util.ResponseAny'
|
||||||
|
"404":
|
||||||
|
description: 文件不存在
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/util.ResponseAny'
|
||||||
|
security:
|
||||||
|
- SessionCookie: []
|
||||||
|
summary: 更新我的文件信息
|
||||||
|
tags:
|
||||||
|
- upload
|
||||||
|
/api/v1/upload/my:
|
||||||
|
get:
|
||||||
|
description: 分页获取当前登录用户上传的文件,支持文件名关键词、业务类型、扩展名过滤
|
||||||
|
parameters:
|
||||||
|
- description: 页码(默认 1)
|
||||||
|
in: query
|
||||||
|
name: page
|
||||||
|
type: integer
|
||||||
|
- description: 每页数量(默认 20,最大 100)
|
||||||
|
in: query
|
||||||
|
name: page_size
|
||||||
|
type: integer
|
||||||
|
- description: 文件名关键词(模糊匹配)
|
||||||
|
in: query
|
||||||
|
name: keyword
|
||||||
|
type: string
|
||||||
|
- description: 业务分类过滤
|
||||||
|
in: query
|
||||||
|
name: type
|
||||||
|
type: string
|
||||||
|
- description: 扩展名过滤
|
||||||
|
in: query
|
||||||
|
name: extension
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: 查询成功
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/util.ResponseAny'
|
||||||
|
- properties:
|
||||||
|
data:
|
||||||
|
$ref: '#/definitions/upload.listMyFilesResponse'
|
||||||
|
type: object
|
||||||
|
"401":
|
||||||
|
description: 未登录
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/util.ResponseAny'
|
||||||
|
security:
|
||||||
|
- SessionCookie: []
|
||||||
|
summary: 获取我的文件列表
|
||||||
|
tags:
|
||||||
|
- upload
|
||||||
/api/v1/user-info:
|
/api/v1/user-info:
|
||||||
get:
|
get:
|
||||||
description: 返回当前登录用户的基本信息及余额数据,需要登录。包括用户 ID、用户名、信任等级、各类余额信息等。
|
description: 返回当前登录用户的基本信息及余额数据,需要登录。包括用户 ID、用户名、信任等级、各类余额信息等。
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import {UserFilesMain} from "@/components/common/user/files"
|
||||||
|
|
||||||
|
export default function UserFilesPage() {
|
||||||
|
return <UserFilesMain />
|
||||||
|
}
|
||||||
@@ -0,0 +1,367 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import {useQuery, useMutation, useQueryClient} from "@tanstack/react-query"
|
||||||
|
import {
|
||||||
|
Upload,
|
||||||
|
Trash2,
|
||||||
|
Download,
|
||||||
|
Search,
|
||||||
|
FileText,
|
||||||
|
FileImage,
|
||||||
|
FileVideo,
|
||||||
|
FileAudio,
|
||||||
|
FileArchive,
|
||||||
|
Loader2,
|
||||||
|
Globe,
|
||||||
|
Lock,
|
||||||
|
Plus,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight
|
||||||
|
} from "lucide-react"
|
||||||
|
import {toast} from "sonner"
|
||||||
|
|
||||||
|
import {Button} from "@/components/ui/button"
|
||||||
|
import {Input} from "@/components/ui/input"
|
||||||
|
import {Badge} from "@/components/ui/badge"
|
||||||
|
import {Card, CardContent} from "@/components/ui/card"
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
} from "@/components/ui/alert-dialog"
|
||||||
|
|
||||||
|
import {formatFileSize, getFileUrl, UploadService} from "@/lib/services/upload/upload.service"
|
||||||
|
import type {Upload as UploadRecord} from "@/lib/services/upload/types"
|
||||||
|
|
||||||
|
/* ─── 工具函数 ─────────────────────────────────────────── */
|
||||||
|
|
||||||
|
function getFileIcon(mimeType: string, className = "size-10") {
|
||||||
|
if (mimeType.startsWith("image/")) return <FileImage className={`${className} text-blue-400`} />
|
||||||
|
if (mimeType.startsWith("video/")) return <FileVideo className={`${className} text-purple-400`} />
|
||||||
|
if (mimeType.startsWith("audio/")) return <FileAudio className={`${className} text-green-400`} />
|
||||||
|
if (mimeType.includes("zip") || mimeType.includes("tar") || mimeType.includes("gzip"))
|
||||||
|
return <FileArchive className={`${className} text-amber-400`} />
|
||||||
|
return <FileText className={`${className} text-slate-400`} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(dateStr: string) {
|
||||||
|
return new Date(dateStr).toLocaleString("zh-CN", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UserFileManager() {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const [keyword, setKeyword] = React.useState("")
|
||||||
|
const [debouncedKeyword, setDebouncedKeyword] = React.useState("")
|
||||||
|
const [page, setPage] = React.useState(1)
|
||||||
|
const [uploading, setUploading] = React.useState(false)
|
||||||
|
const [deleteTarget, setDeleteTarget] = React.useState<UploadRecord | null>(null)
|
||||||
|
const [uploadAccessMode, setUploadAccessMode] = React.useState<number>(0) // 默认私有
|
||||||
|
const fileInputRef = React.useRef<HTMLInputElement>(null)
|
||||||
|
const pageSize = 12 // 每页 12 个卡片更均衡(3x4 或 4x3 布局)
|
||||||
|
|
||||||
|
// 搜索防抖
|
||||||
|
React.useEffect(() => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setDebouncedKeyword(keyword)
|
||||||
|
setPage(1)
|
||||||
|
}, 400)
|
||||||
|
return () => clearTimeout(timer)
|
||||||
|
}, [keyword])
|
||||||
|
|
||||||
|
// 我的文件列表查询
|
||||||
|
const { data, isPending } = useQuery({
|
||||||
|
queryKey: ["user-files", page, pageSize, debouncedKeyword],
|
||||||
|
queryFn: () => UploadService.listMyUploads(page, pageSize, debouncedKeyword || undefined),
|
||||||
|
})
|
||||||
|
|
||||||
|
const files = data?.items ?? []
|
||||||
|
const total = data?.total ?? 0
|
||||||
|
const totalPages = Math.ceil(total / pageSize)
|
||||||
|
|
||||||
|
// 上传文件 Mutation
|
||||||
|
const uploadMutation = useMutation({
|
||||||
|
mutationFn: async (file: File) => {
|
||||||
|
return UploadService.uploadFile(file, "generic", undefined, uploadAccessMode)
|
||||||
|
},
|
||||||
|
onMutate: () => {
|
||||||
|
setUploading(true)
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("文件上传成功")
|
||||||
|
void queryClient.invalidateQueries({ queryKey: ["user-files"] })
|
||||||
|
},
|
||||||
|
onError: (err: Error) => {
|
||||||
|
toast.error(err.message || "上传失败")
|
||||||
|
},
|
||||||
|
onSettled: () => {
|
||||||
|
setUploading(false)
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.value = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 删除文件 Mutation
|
||||||
|
const deleteMutation = useMutation({
|
||||||
|
mutationFn: (id: string) => UploadService.deleteMyFile(id),
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("文件已成功删除")
|
||||||
|
void queryClient.invalidateQueries({ queryKey: ["user-files"] })
|
||||||
|
setDeleteTarget(null)
|
||||||
|
},
|
||||||
|
onError: (err: Error) => {
|
||||||
|
toast.error(err.message || "删除失败")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0]
|
||||||
|
if (file) {
|
||||||
|
uploadMutation.mutate(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const triggerUploadClick = () => {
|
||||||
|
fileInputRef.current?.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDownload = (file: UploadRecord) => {
|
||||||
|
const url = UploadService.getDownloadUrl(file.id)
|
||||||
|
const a = document.createElement("a")
|
||||||
|
a.href = url
|
||||||
|
a.download = file.file_name
|
||||||
|
a.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex w-full flex-col gap-6">
|
||||||
|
{/* 操作栏:搜索、权限选择、上传按钮 */}
|
||||||
|
<div className="flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-4">
|
||||||
|
<div className="relative flex-1 max-w-md">
|
||||||
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
placeholder="搜索文件名..."
|
||||||
|
value={keyword}
|
||||||
|
onChange={(e) => setKeyword(e.target.value)}
|
||||||
|
className="pl-9 h-9"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3 self-end sm:self-auto">
|
||||||
|
{/* 上传权限设置 */}
|
||||||
|
<div className="flex items-center gap-1.5 border rounded-lg p-1 bg-muted/40 h-9 text-xs">
|
||||||
|
<Button
|
||||||
|
variant={uploadAccessMode === 0 ? "secondary" : "ghost"}
|
||||||
|
size="sm"
|
||||||
|
className="h-7 px-2.5 text-xs font-normal"
|
||||||
|
onClick={() => setUploadAccessMode(0)}
|
||||||
|
>
|
||||||
|
<Lock className="mr-1 size-3 text-amber-500" />
|
||||||
|
私有
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={uploadAccessMode === 1 ? "secondary" : "ghost"}
|
||||||
|
size="sm"
|
||||||
|
className="h-7 px-2.5 text-xs font-normal"
|
||||||
|
onClick={() => setUploadAccessMode(1)}
|
||||||
|
>
|
||||||
|
<Globe className="mr-1 size-3 text-sky-500" />
|
||||||
|
公开
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={fileInputRef}
|
||||||
|
onChange={handleFileChange}
|
||||||
|
className="hidden"
|
||||||
|
disabled={uploading}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={triggerUploadClick}
|
||||||
|
disabled={uploading}
|
||||||
|
className="h-9 gap-1.5 text-xs font-medium"
|
||||||
|
>
|
||||||
|
{uploading ? (
|
||||||
|
<Loader2 className="size-3.5 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Plus className="size-3.5" />
|
||||||
|
)}
|
||||||
|
{uploading ? "正在上传..." : "上传文件"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 文件展示区 */}
|
||||||
|
{isPending ? (
|
||||||
|
<div className="flex items-center justify-center py-24">
|
||||||
|
<Loader2 className="size-8 animate-spin text-primary" />
|
||||||
|
</div>
|
||||||
|
) : files.length === 0 ? (
|
||||||
|
<Card className="border border-dashed py-16 flex flex-col items-center justify-center text-center">
|
||||||
|
<CardContent className="flex flex-col items-center gap-4">
|
||||||
|
<div className="p-4 bg-muted rounded-full text-muted-foreground/60">
|
||||||
|
<Upload className="size-10" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<h3 className="font-semibold text-sm">还没有上传文件</h3>
|
||||||
|
<p className="text-xs text-muted-foreground max-w-xs">
|
||||||
|
{debouncedKeyword ? "未搜索到匹配的文件" : "上传您的第一个文件,支持图片、视频、文档和压缩包等格式。"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{!debouncedKeyword && (
|
||||||
|
<Button onClick={triggerUploadClick} disabled={uploading} size="sm" className="mt-2 text-xs">
|
||||||
|
选择文件上传
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||||
|
{files.map((file) => (
|
||||||
|
<Card
|
||||||
|
key={file.id}
|
||||||
|
className="group border border-border/40 hover:border-border/80 bg-card/60 hover:bg-card/95 transition-all duration-300 shadow-xs hover:shadow-md overflow-hidden relative flex flex-col h-full"
|
||||||
|
>
|
||||||
|
{/* 文件预览/图标区 */}
|
||||||
|
<div className="h-36 bg-muted/30 border-b border-dashed relative flex items-center justify-center overflow-hidden p-2 group-hover:bg-muted/10 transition-colors">
|
||||||
|
{file.mime_type.startsWith("image/") ? (
|
||||||
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
|
<img
|
||||||
|
src={getFileUrl(file.id, "medium") ?? undefined}
|
||||||
|
alt={file.file_name}
|
||||||
|
className="max-h-full max-w-full object-contain rounded-md shadow-xs transition-transform duration-350 group-hover:scale-103"
|
||||||
|
onError={(e) => {
|
||||||
|
;(e.currentTarget as HTMLImageElement).style.display = "none"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
getFileIcon(file.mime_type)
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 访问权限 Badge */}
|
||||||
|
<div className="absolute top-2 right-2">
|
||||||
|
{file.access_mode === 0 ? (
|
||||||
|
<Badge variant="secondary" className="bg-amber-500/10 text-amber-600 dark:text-amber-400 text-[10px] gap-1 px-1.5 py-0 rounded-md border border-amber-500/20 font-normal">
|
||||||
|
<Lock className="size-2.5" />
|
||||||
|
私有
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge variant="secondary" className="bg-sky-500/10 text-sky-600 dark:text-sky-400 text-[10px] gap-1 px-1.5 py-0 rounded-md border border-sky-500/20 font-normal">
|
||||||
|
<Globe className="size-2.5" />
|
||||||
|
公开
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 文件详情区 */}
|
||||||
|
<CardContent className="p-4 flex-1 flex flex-col justify-between gap-3">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<h4 className="font-semibold text-xs text-foreground truncate select-all" title={file.file_name}>
|
||||||
|
{file.file_name}
|
||||||
|
</h4>
|
||||||
|
<div className="flex items-center justify-between text-[10px] text-muted-foreground font-mono">
|
||||||
|
<span>{formatFileSize(file.file_size)}</span>
|
||||||
|
<span className="truncate max-w-[120px]" title={file.mime_type}>{file.mime_type}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between pt-1 border-t border-dashed">
|
||||||
|
<span className="text-[9px] text-muted-foreground/80">{formatDate(file.created_at)}</span>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
className="size-7 rounded-md hover:bg-muted text-muted-foreground hover:text-foreground"
|
||||||
|
title="下载文件"
|
||||||
|
onClick={() => handleDownload(file)}
|
||||||
|
>
|
||||||
|
<Download className="size-3.5" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
className="size-7 rounded-md hover:bg-destructive/10 text-muted-foreground hover:text-destructive"
|
||||||
|
title="删除文件"
|
||||||
|
onClick={() => setDeleteTarget(file)}
|
||||||
|
>
|
||||||
|
<Trash2 className="size-3.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 分页 */}
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<div className="flex items-center justify-center gap-2 pt-4">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="border-dashed text-xs h-8 px-3"
|
||||||
|
disabled={page <= 1}
|
||||||
|
onClick={() => setPage((p) => p - 1)}
|
||||||
|
>
|
||||||
|
<ChevronLeft className="mr-1 size-3.5" />
|
||||||
|
上一页
|
||||||
|
</Button>
|
||||||
|
<span className="text-xs text-muted-foreground font-medium px-2">
|
||||||
|
{page} / {totalPages}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="border-dashed text-xs h-8 px-3"
|
||||||
|
disabled={page >= totalPages}
|
||||||
|
onClick={() => setPage((p) => p + 1)}
|
||||||
|
>
|
||||||
|
下一页
|
||||||
|
<ChevronRight className="ml-1 size-3.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 删除确认 Dialog */}
|
||||||
|
<AlertDialog open={!!deleteTarget} onOpenChange={(open) => !open && setDeleteTarget(null)}>
|
||||||
|
<AlertDialogContent suppressHydrationWarning>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>确认删除文件</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
确定要删除文件{" "}
|
||||||
|
<span className="font-semibold text-foreground">「{deleteTarget?.file_name}」</span>{" "}
|
||||||
|
吗?删除后此文件将无法恢复。
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel disabled={deleteMutation.isPending}>取消</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={() => deleteTarget && deleteMutation.mutate(deleteTarget.id)}
|
||||||
|
disabled={deleteMutation.isPending}
|
||||||
|
className="bg-destructive hover:bg-destructive/90 text-destructive-foreground"
|
||||||
|
>
|
||||||
|
{deleteMutation.isPending && <Loader2 className="mr-1.5 size-3.5 animate-spin" />}
|
||||||
|
确认删除
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import {motion} from "motion/react"
|
||||||
|
|
||||||
|
import {UserFileManager} from "./file-manager"
|
||||||
|
|
||||||
|
export function UserFilesMain() {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 15 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.35, ease: "easeOut" }}
|
||||||
|
className="flex w-full flex-col gap-6 py-6"
|
||||||
|
>
|
||||||
|
{/* 顶部标题区 */}
|
||||||
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 border-b pb-5">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-xl font-bold tracking-tight bg-gradient-to-r from-foreground via-foreground/90 to-muted-foreground bg-clip-text text-transparent">
|
||||||
|
我的文件
|
||||||
|
</h1>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
管理您上传的个人文件,支持上传、下载、搜索与删除操作
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UserFileManager />
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -68,6 +68,7 @@ import {usePublicConfig} from "@/hooks/use-public-config"
|
|||||||
const data = {
|
const data = {
|
||||||
navMain: [
|
navMain: [
|
||||||
{ title: "首页", url: "/home", icon: Home },
|
{ title: "首页", url: "/home", icon: Home },
|
||||||
|
{ title: "我的文件", url: "/files", icon: FolderOpen },
|
||||||
],
|
],
|
||||||
admin: [
|
admin: [
|
||||||
{ title: "用户管理", url: "/admin/users", icon: UserRound },
|
{ title: "用户管理", url: "/admin/users", icon: UserRound },
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export interface Upload {
|
|||||||
storage_driver: string
|
storage_driver: string
|
||||||
type: string
|
type: string
|
||||||
status: string
|
status: string
|
||||||
|
access_mode: number
|
||||||
metadata: UploadMetadata
|
metadata: UploadMetadata
|
||||||
created_at: string
|
created_at: string
|
||||||
updated_at: string
|
updated_at: string
|
||||||
|
|||||||
@@ -136,4 +136,54 @@ export class UploadService extends BaseService {
|
|||||||
const result = await this.uploadFile(file, type, undefined, accessMode)
|
const result = await this.uploadFile(file, type, undefined, accessMode)
|
||||||
return { id: result.id }
|
return { id: result.id }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取我的文件列表
|
||||||
|
*/
|
||||||
|
static async listMyUploads(
|
||||||
|
page = 1,
|
||||||
|
pageSize = 20,
|
||||||
|
keyword?: string,
|
||||||
|
type?: string,
|
||||||
|
extension?: string
|
||||||
|
): Promise<ListUploadsResponse> {
|
||||||
|
const params: Record<string, string | number> = { page, page_size: pageSize }
|
||||||
|
if (keyword) params.keyword = keyword
|
||||||
|
if (type) params.type = type
|
||||||
|
if (extension) params.extension = extension
|
||||||
|
const response = await apiClient.get<{ data: ListUploadsResponse }>('/api/v1/upload/my', {
|
||||||
|
params
|
||||||
|
} as InternalAxiosRequestConfig)
|
||||||
|
return response.data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除我的文件
|
||||||
|
*/
|
||||||
|
static async deleteMyFile(id: string): Promise<void> {
|
||||||
|
const response = await apiClient.delete<{ data: void }>(`/api/v1/upload/${id}`)
|
||||||
|
return response.data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新我的文件
|
||||||
|
*/
|
||||||
|
static async updateMyFile(id: string, fileName: string, accessMode?: number): Promise<Upload> {
|
||||||
|
const response = await apiClient.put<{ data: Upload }>(`/api/v1/upload/${id}`, {
|
||||||
|
file_name: fileName,
|
||||||
|
access_mode: accessMode
|
||||||
|
})
|
||||||
|
return response.data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 我的文件批量 ZIP 打包下载
|
||||||
|
*/
|
||||||
|
static async batchDownloadMyFiles(ids: string[]): Promise<Blob> {
|
||||||
|
const response = await apiClient.post<Blob>('/api/v1/upload/download/batch', { ids }, {
|
||||||
|
responseType: 'blob',
|
||||||
|
} as InternalAxiosRequestConfig)
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Rain-kl/Wavelet/internal/apps/oauth"
|
||||||
"github.com/Rain-kl/Wavelet/internal/db"
|
"github.com/Rain-kl/Wavelet/internal/db"
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
"github.com/Rain-kl/Wavelet/internal/util"
|
"github.com/Rain-kl/Wavelet/internal/util"
|
||||||
@@ -164,3 +165,200 @@ func GetDistinctUploadTypes(c *gin.Context) {
|
|||||||
sort.Strings(dbTypes)
|
sort.Strings(dbTypes)
|
||||||
c.JSON(http.StatusOK, util.OK(dbTypes))
|
c.JSON(http.StatusOK, util.OK(dbTypes))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 []model.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} util.ResponseAny{data=listMyFilesResponse} "查询成功"
|
||||||
|
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||||
|
// @Router /api/v1/upload/my [get]
|
||||||
|
func ListMyFiles(c *gin.Context) {
|
||||||
|
currUser, _ := util.GetFromContext[*model.User](c, oauth.UserObjKey)
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
var req listMyFilesRequest
|
||||||
|
if err := c.ShouldBindQuery(&req); err != nil {
|
||||||
|
c.JSON(http.StatusOK, util.Err(ErrInvalidParams))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.Page <= 0 {
|
||||||
|
req.Page = 1
|
||||||
|
}
|
||||||
|
if req.PageSize <= 0 || req.PageSize > 100 {
|
||||||
|
req.PageSize = 20
|
||||||
|
}
|
||||||
|
|
||||||
|
query := db.DB(ctx).Model(&model.Upload{}).
|
||||||
|
Where("user_id = ? AND status != ?", currUser.ID, model.UploadStatusDeleted)
|
||||||
|
|
||||||
|
if req.Keyword != "" {
|
||||||
|
query = query.Where("LOWER(file_name) LIKE ?", "%"+strings.ToLower(req.Keyword)+"%")
|
||||||
|
}
|
||||||
|
if req.Type != "" {
|
||||||
|
query = query.Where("type = ?", req.Type)
|
||||||
|
}
|
||||||
|
if req.Extension != "" {
|
||||||
|
query = query.Where("extension = ?", strings.ToLower(req.Extension))
|
||||||
|
}
|
||||||
|
|
||||||
|
var total int64
|
||||||
|
if err := query.Count(&total).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, util.Err(ErrQueryFileCountFailed))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var items []model.Upload
|
||||||
|
offset := (req.Page - 1) * req.PageSize
|
||||||
|
if err := query.Order("created_at DESC").Offset(offset).Limit(req.PageSize).Find(&items).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, util.Err(ErrQueryFileListFailed))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, util.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} util.ResponseAny "删除成功"
|
||||||
|
// @Failure 403 {object} util.ResponseAny "无权操作"
|
||||||
|
// @Failure 404 {object} util.ResponseAny "文件不存在"
|
||||||
|
// @Router /api/v1/upload/{id} [delete]
|
||||||
|
func DeleteMyFile(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))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadID, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, util.Err(ErrInvalidFileID))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var upload model.Upload
|
||||||
|
if err := db.DB(ctx).Where("id = ? AND status != ?", uploadID, model.UploadStatusDeleted).First(&upload).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
c.AbortWithStatus(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, util.Err(ErrQueryUploadRecordFailed))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if upload.UserID != currUser.ID {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, util.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} util.ResponseAny{data=model.Upload} "更新成功"
|
||||||
|
// @Failure 403 {object} util.ResponseAny "无权操作"
|
||||||
|
// @Failure 404 {object} util.ResponseAny "文件不存在"
|
||||||
|
// @Router /api/v1/upload/{id} [put]
|
||||||
|
func UpdateMyFile(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))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadID, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, util.Err(ErrInvalidFileID))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req updateMyFileRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusOK, util.Err(ErrInvalidParams))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var upload model.Upload
|
||||||
|
if err := db.DB(ctx).Where("id = ? AND status != ?", uploadID, model.UploadStatusDeleted).First(&upload).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
c.AbortWithStatus(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, util.Err(ErrQueryUploadRecordFailed))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if upload.UserID != currUser.ID {
|
||||||
|
c.AbortWithStatus(http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updates := make(map[string]any)
|
||||||
|
if req.FileName != "" {
|
||||||
|
updates["file_name"] = req.FileName
|
||||||
|
}
|
||||||
|
if req.AccessMode != nil {
|
||||||
|
updates["access_mode"] = *req.AccessMode
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(updates) > 0 {
|
||||||
|
if err := db.DB(ctx).Model(&upload).Updates(updates).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, util.Err("更新文件记录失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, util.OK(upload))
|
||||||
|
}
|
||||||
|
|||||||
@@ -48,6 +48,11 @@ func setupTestRouter(authUser *model.User) *gin.Engine {
|
|||||||
uploadGroup.Use(authMiddleware)
|
uploadGroup.Use(authMiddleware)
|
||||||
{
|
{
|
||||||
uploadGroup.POST("", UploadFile)
|
uploadGroup.POST("", UploadFile)
|
||||||
|
uploadGroup.GET("/my", ListMyFiles)
|
||||||
|
uploadGroup.DELETE("/:id", DeleteMyFile)
|
||||||
|
uploadGroup.PUT("/:id", UpdateMyFile)
|
||||||
|
uploadGroup.GET("/download/:id", DownloadFile)
|
||||||
|
uploadGroup.POST("/download/batch", BatchDownloadFiles)
|
||||||
}
|
}
|
||||||
|
|
||||||
adminGroup := r.Group("/api/v1/admin/uploads")
|
adminGroup := r.Group("/api/v1/admin/uploads")
|
||||||
@@ -855,3 +860,134 @@ func TestGetFileStats(t *testing.T) {
|
|||||||
t.Errorf("expected 1 document category, got %d", categoryMap["文档"])
|
t.Errorf("expected 1 document category, got %d", categoryMap["文档"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUserUploadManagement(t *testing.T) {
|
||||||
|
dbConn, _, cleanup := testhelper.SetupTestEnvironment(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
user1 := &model.User{ID: 1001, Username: "user1"}
|
||||||
|
user2 := &model.User{ID: 1002, Username: "user2"}
|
||||||
|
|
||||||
|
_ = dbConn.Create(user1)
|
||||||
|
_ = dbConn.Create(user2)
|
||||||
|
|
||||||
|
router1 := setupTestRouter(user1)
|
||||||
|
router2 := setupTestRouter(user2)
|
||||||
|
|
||||||
|
// Seed upload records
|
||||||
|
upload1 := model.Upload{
|
||||||
|
ID: 4001,
|
||||||
|
UserID: 1001,
|
||||||
|
FileName: "user1-file.txt",
|
||||||
|
FilePath: "uploads/user1-file.txt",
|
||||||
|
FileSize: 100,
|
||||||
|
MimeType: "text/plain",
|
||||||
|
Extension: "txt",
|
||||||
|
StorageDriver: "local",
|
||||||
|
Status: model.UploadStatusUsed,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
upload2 := model.Upload{
|
||||||
|
ID: 4002,
|
||||||
|
UserID: 1002,
|
||||||
|
FileName: "user2-file.png",
|
||||||
|
FilePath: "uploads/user2-file.png",
|
||||||
|
FileSize: 200,
|
||||||
|
MimeType: "image/png",
|
||||||
|
Extension: "png",
|
||||||
|
StorageDriver: "local",
|
||||||
|
Status: model.UploadStatusUsed,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = dbConn.Create(&upload1)
|
||||||
|
_ = dbConn.Create(&upload2)
|
||||||
|
|
||||||
|
t.Run("ListMyFiles only returns own files", func(t *testing.T) {
|
||||||
|
req, _ := http.NewRequest("GET", "/api/v1/upload/my", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
router1.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
var resp struct {
|
||||||
|
ErrorMsg string `json:"error_msg"`
|
||||||
|
Data listMyFilesResponse `json:"data"`
|
||||||
|
}
|
||||||
|
_ = json.Unmarshal(w.Body.Bytes(), &resp)
|
||||||
|
|
||||||
|
if resp.ErrorMsg != "" {
|
||||||
|
t.Fatalf("ListMyFiles error: %s", resp.ErrorMsg)
|
||||||
|
}
|
||||||
|
if resp.Data.Total != 1 {
|
||||||
|
t.Errorf("expected 1 file for user1, got %d", resp.Data.Total)
|
||||||
|
}
|
||||||
|
if len(resp.Data.Items) != 1 || resp.Data.Items[0].ID != 4001 {
|
||||||
|
t.Errorf("expected file 4001, got items: %+v", resp.Data.Items)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("UpdateMyFile updates file name and access mode successfully", func(t *testing.T) {
|
||||||
|
newMode := 1
|
||||||
|
reqBody, _ := json.Marshal(updateMyFileRequest{
|
||||||
|
FileName: "renamed.txt",
|
||||||
|
AccessMode: &newMode,
|
||||||
|
})
|
||||||
|
req, _ := http.NewRequest("PUT", "/api/v1/upload/4001", bytes.NewReader(reqBody))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
router1.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected status 200, got %d. Body: %s", w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
var updated model.Upload
|
||||||
|
dbConn.First(&updated, 4001)
|
||||||
|
if updated.FileName != "renamed.txt" {
|
||||||
|
t.Errorf("expected file name renamed.txt, got %s", updated.FileName)
|
||||||
|
}
|
||||||
|
if updated.AccessMode != 1 {
|
||||||
|
t.Errorf("expected access mode 1, got %d", updated.AccessMode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("UpdateMyFile blocks non-owners", func(t *testing.T) {
|
||||||
|
reqBody, _ := json.Marshal(updateMyFileRequest{
|
||||||
|
FileName: "hack.txt",
|
||||||
|
})
|
||||||
|
req, _ := http.NewRequest("PUT", "/api/v1/upload/4001", bytes.NewReader(reqBody))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
router2.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Code != http.StatusForbidden {
|
||||||
|
t.Errorf("expected status 403, got %d", w.Code)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("DeleteMyFile blocks non-owners", func(t *testing.T) {
|
||||||
|
req, _ := http.NewRequest("DELETE", "/api/v1/upload/4001", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
router2.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Code != http.StatusForbidden {
|
||||||
|
t.Errorf("expected status 403, got %d", w.Code)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("DeleteMyFile deletes file successfully", func(t *testing.T) {
|
||||||
|
req, _ := http.NewRequest("DELETE", "/api/v1/upload/4001", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
router1.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected status 200, got %d", w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
var deleted model.Upload
|
||||||
|
dbConn.First(&deleted, 4001)
|
||||||
|
if deleted.Status != model.UploadStatusDeleted {
|
||||||
|
t.Errorf("expected status deleted, got %s", deleted.Status)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,15 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
catImage = "图片"
|
||||||
|
catVideo = "视频"
|
||||||
|
catAudio = "音频"
|
||||||
|
catDocument = "文档"
|
||||||
|
catArchive = "压缩包"
|
||||||
|
catOther = "其他"
|
||||||
|
)
|
||||||
|
|
||||||
type trendItem struct {
|
type trendItem struct {
|
||||||
Date string `json:"date"`
|
Date string `json:"date"`
|
||||||
Count int64 `json:"count"`
|
Count int64 `json:"count"`
|
||||||
@@ -110,7 +119,7 @@ func GetFileStats(c *gin.Context) {
|
|||||||
|
|
||||||
catCount := make(map[string]int64)
|
catCount := make(map[string]int64)
|
||||||
catSize := make(map[string]int64)
|
catSize := make(map[string]int64)
|
||||||
categoriesList := []string{"图片", "视频", "音频", "文档", "压缩包", "其他"}
|
categoriesList := []string{catImage, catVideo, catAudio, catDocument, catArchive, catOther}
|
||||||
for _, cat := range categoriesList {
|
for _, cat := range categoriesList {
|
||||||
catCount[cat] = 0
|
catCount[cat] = 0
|
||||||
catSize[cat] = 0
|
catSize[cat] = 0
|
||||||
@@ -190,21 +199,21 @@ func getFileCategory(mimeType, ext string) string {
|
|||||||
ext = strings.ToLower(ext)
|
ext = strings.ToLower(ext)
|
||||||
|
|
||||||
if strings.HasPrefix(mimeType, "image/") || isImageExtension(ext) {
|
if strings.HasPrefix(mimeType, "image/") || isImageExtension(ext) {
|
||||||
return "图片"
|
return catImage
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(mimeType, "video/") {
|
if strings.HasPrefix(mimeType, "video/") {
|
||||||
return "视频"
|
return catVideo
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(mimeType, "audio/") {
|
if strings.HasPrefix(mimeType, "audio/") {
|
||||||
return "音频"
|
return catAudio
|
||||||
}
|
}
|
||||||
if isArchiveExtension(ext) || strings.Contains(mimeType, "zip") || strings.Contains(mimeType, "tar") || strings.Contains(mimeType, "gzip") {
|
if isArchiveExtension(ext) || strings.Contains(mimeType, "zip") || strings.Contains(mimeType, "tar") || strings.Contains(mimeType, "gzip") {
|
||||||
return "压缩包"
|
return catArchive
|
||||||
}
|
}
|
||||||
if isDocumentExtension(ext) || strings.HasPrefix(mimeType, "text/") || mimeType == "application/pdf" {
|
if isDocumentExtension(ext) || strings.HasPrefix(mimeType, "text/") || mimeType == "application/pdf" {
|
||||||
return "文档"
|
return catDocument
|
||||||
}
|
}
|
||||||
return "其他"
|
return catOther
|
||||||
}
|
}
|
||||||
|
|
||||||
func isArchiveExtension(ext string) bool {
|
func isArchiveExtension(ext string) bool {
|
||||||
|
|||||||
+27
-14
@@ -210,11 +210,7 @@ func registerRoutes(r *gin.Engine) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Upload
|
// Upload
|
||||||
uploadRouter := apiV1Router.Group("/upload")
|
registerUploadRoutes(apiV1Router)
|
||||||
uploadRouter.Use(oauth.LoginRequired())
|
|
||||||
{
|
|
||||||
uploadRouter.POST("", upload.UploadFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Config (public)
|
// Config (public)
|
||||||
configRouter := apiV1Router.Group("/config")
|
configRouter := apiV1Router.Group("/config")
|
||||||
@@ -277,15 +273,7 @@ func registerRoutes(r *gin.Engine) {
|
|||||||
adminRouter.DELETE("/users/:id", admin_user.DeleteUser)
|
adminRouter.DELETE("/users/:id", admin_user.DeleteUser)
|
||||||
|
|
||||||
// Uploads
|
// Uploads
|
||||||
adminUploadsRouter := adminRouter.Group("/uploads")
|
registerAdminUploadRoutes(adminRouter)
|
||||||
{
|
|
||||||
adminUploadsRouter.GET("", upload.ListFiles)
|
|
||||||
adminUploadsRouter.GET("/stats", upload.GetFileStats)
|
|
||||||
adminUploadsRouter.DELETE("/:id", upload.DeleteFile)
|
|
||||||
adminUploadsRouter.GET("/download/:id", upload.DownloadFile)
|
|
||||||
adminUploadsRouter.POST("/download/batch", upload.BatchDownloadFiles)
|
|
||||||
adminUploadsRouter.GET("/types", upload.GetDistinctUploadTypes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// System Config
|
// System Config
|
||||||
adminRouter.POST("/system-configs", system_config.CreateSystemConfig)
|
adminRouter.POST("/system-configs", system_config.CreateSystemConfig)
|
||||||
@@ -325,3 +313,28 @@ func registerRoutes(r *gin.Engine) {
|
|||||||
// 注册前端静态路由(当启用 embed_frontend 编译标签时)
|
// 注册前端静态路由(当启用 embed_frontend 编译标签时)
|
||||||
registerFrontend(r)
|
registerFrontend(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func registerUploadRoutes(apiV1Router *gin.RouterGroup) {
|
||||||
|
uploadRouter := apiV1Router.Group("/upload")
|
||||||
|
uploadRouter.Use(oauth.LoginRequired())
|
||||||
|
{
|
||||||
|
uploadRouter.POST("", upload.UploadFile)
|
||||||
|
uploadRouter.GET("/my", upload.ListMyFiles)
|
||||||
|
uploadRouter.DELETE("/:id", upload.DeleteMyFile)
|
||||||
|
uploadRouter.PUT("/:id", upload.UpdateMyFile)
|
||||||
|
uploadRouter.GET("/download/:id", upload.DownloadFile)
|
||||||
|
uploadRouter.POST("/download/batch", upload.BatchDownloadFiles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerAdminUploadRoutes(adminRouter *gin.RouterGroup) {
|
||||||
|
adminUploadsRouter := adminRouter.Group("/uploads")
|
||||||
|
{
|
||||||
|
adminUploadsRouter.GET("", upload.ListFiles)
|
||||||
|
adminUploadsRouter.GET("/stats", upload.GetFileStats)
|
||||||
|
adminUploadsRouter.DELETE("/:id", upload.DeleteFile)
|
||||||
|
adminUploadsRouter.GET("/download/:id", upload.DownloadFile)
|
||||||
|
adminUploadsRouter.POST("/download/batch", upload.BatchDownloadFiles)
|
||||||
|
adminUploadsRouter.GET("/types", upload.GetDistinctUploadTypes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user