update
This commit is contained in:
+67
-13
@@ -21,7 +21,61 @@
|
|||||||
|
|
||||||
## 3. 接口列表
|
## 3. 接口列表
|
||||||
|
|
||||||
### 3.1 `POST /api/admin/files/upload`(后台上传)
|
### 3.1 `GET /api/admin/files`(后台文件列表)
|
||||||
|
|
||||||
|
分页列出文件,支持按上传者类型/ID 与关键字过滤,供后台「文件管理」栏目使用。
|
||||||
|
|
||||||
|
**请求头**:`Authorization: Bearer <JWT>`。
|
||||||
|
|
||||||
|
**查询参数**
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `page` | int | 否 | `1` | 页码,从 1 开始。 |
|
||||||
|
| `size` | int | 否 | `20` | 每页条数。 |
|
||||||
|
| `keyword` | string | 否 | 空 | 模糊匹配 `origin_name` 或 `filename`。 |
|
||||||
|
| `uploader_type` | string | 否 | 空 | 按上传者类型过滤:`admin` / `ma`;空表示不限。 |
|
||||||
|
| `uploader_id` | uint64 | 否 | `0` | 按上传者主键过滤;`0` 表示不限。 |
|
||||||
|
|
||||||
|
**响应**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"msg": "ok",
|
||||||
|
"data": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"storage_key": "2026/10/27/3f2a...c1.png",
|
||||||
|
"filename": "3f2a...c1.png",
|
||||||
|
"origin_name": "我的图片.png",
|
||||||
|
"mime_type": "image/png",
|
||||||
|
"ext": "png",
|
||||||
|
"size": 12345,
|
||||||
|
"uploader_type": "admin",
|
||||||
|
"uploader_id": 1,
|
||||||
|
"url": "/files/static/2026/10/27/3f2a...c1.png",
|
||||||
|
"created_at": "2026-10-27 22:00:00"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 1,
|
||||||
|
"page": 1,
|
||||||
|
"size": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**错误码**:未认证 `40100`;查询失败 `50000`。
|
||||||
|
|
||||||
|
**curl 示例**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl "http://localhost:8080/api/admin/files?keyword=png&page=1&size=20" \
|
||||||
|
-H "Authorization: Bearer <JWT>"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 `POST /api/admin/files/upload`(后台上传)
|
||||||
|
|
||||||
以 `multipart/form-data` 上传单个文件,表单字段名为 `file`。
|
以 `multipart/form-data` 上传单个文件,表单字段名为 `file`。
|
||||||
|
|
||||||
@@ -60,13 +114,13 @@ curl -X POST http://localhost:8080/api/admin/files/upload \
|
|||||||
-F "file=@/path/to/我的图片.png"
|
-F "file=@/path/to/我的图片.png"
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3.2 `GET /api/admin/files/:id`(后台查询元数据)
|
### 3.3 `GET /api/admin/files/:id`(后台查询元数据)
|
||||||
|
|
||||||
查询指定文件的元数据记录。
|
查询指定文件的元数据记录。
|
||||||
|
|
||||||
**路径参数**:`id`(uint64)。**响应**:返回 `File` 对象(见 §4);不存在返回 `40400`。
|
**路径参数**:`id`(uint64)。**响应**:返回 `File` 对象(见 §4);不存在返回 `40400`。
|
||||||
|
|
||||||
### 3.3 `GET /api/admin/files/:id/raw`(后台获取文件流)
|
### 3.4 `GET /api/admin/files/:id/raw`(后台获取文件流)
|
||||||
|
|
||||||
以流形式返回文件内容,按 `mime_type` 设置 `Content-Type`,`Content-Disposition: inline`,可用于浏览器预览/下载。
|
以流形式返回文件内容,按 `mime_type` 设置 `Content-Type`,`Content-Disposition: inline`,可用于浏览器预览/下载。
|
||||||
|
|
||||||
@@ -74,15 +128,15 @@ curl -X POST http://localhost:8080/api/admin/files/upload \
|
|||||||
|
|
||||||
> 也可直接通过上传返回的 `url` 访问(本地模式由静态目录服务,等效于本接口)。
|
> 也可直接通过上传返回的 `url` 访问(本地模式由静态目录服务,等效于本接口)。
|
||||||
|
|
||||||
### 3.4 `DELETE /api/admin/files/:id`(后台删除)
|
### 3.5 `DELETE /api/admin/files/:id`(后台删除)
|
||||||
|
|
||||||
删除文件:同时清理存储内容与 `files` 表元数据记录。
|
删除文件:同时清理存储内容与 `files` 表元数据记录。
|
||||||
|
|
||||||
**路径参数**:`id`(uint64)。**响应**:`{ "code": 0, "msg": "ok", "data": { "id": 1 } }`;不存在返回 `40400`。
|
**路径参数**:`id`(uint64)。**响应**:`{ "code": 0, "msg": "ok", "data": { "id": 1 } }`;不存在返回 `40400`。
|
||||||
|
|
||||||
### 3.5 `POST /api/ma/files/upload`(小程序上传)
|
### 3.6 `POST /api/ma/files/upload`(小程序上传)
|
||||||
|
|
||||||
与 3.1 一致,但鉴权为小程序登录态。
|
与 3.2 一致,但鉴权为小程序登录态。
|
||||||
|
|
||||||
**请求头**:`X-App-ID: <AppID>` + `Authorization: Bearer <token>`(或 `X-Session-Token`)。
|
**请求头**:`X-App-ID: <AppID>` + `Authorization: Bearer <token>`(或 `X-Session-Token`)。
|
||||||
|
|
||||||
@@ -95,17 +149,17 @@ curl -X POST http://localhost:8080/api/ma/files/upload \
|
|||||||
-F "file=@/path/to/photo.jpg"
|
-F "file=@/path/to/photo.jpg"
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3.6 `GET /api/ma/files/:id`(小程序查询元数据)
|
### 3.7 `GET /api/ma/files/:id`(小程序查询元数据)
|
||||||
|
|
||||||
同 3.2,返回 `File` 对象;不存在 `40400`。
|
同 3.3,返回 `File` 对象;不存在 `40400`。
|
||||||
|
|
||||||
### 3.7 `GET /api/ma/files/:id/raw`(小程序获取文件流)
|
### 3.8 `GET /api/ma/files/:id/raw`(小程序获取文件流)
|
||||||
|
|
||||||
同 3.3,返回文件流;不存在 `40400`。
|
同 3.4,返回文件流;不存在 `40400`。
|
||||||
|
|
||||||
### 3.8 `DELETE /api/ma/files/:id`(小程序删除)
|
### 3.9 `DELETE /api/ma/files/:id`(小程序删除)
|
||||||
|
|
||||||
同 3.4,删除文件内容与元数据;不存在 `40400`。
|
同 3.5,删除文件内容与元数据;不存在 `40400`。
|
||||||
|
|
||||||
## 4. 数据模型
|
## 4. 数据模型
|
||||||
|
|
||||||
@@ -126,7 +180,7 @@ curl -X POST http://localhost:8080/api/ma/files/upload \
|
|||||||
| `CreatedAt` | time | `created_at` | 创建时间(`Y-m-d H:i:s`)。 |
|
| `CreatedAt` | time | `created_at` | 创建时间(`Y-m-d H:i:s`)。 |
|
||||||
|
|
||||||
> `GET .../raw` 直接返回二进制流,不返回 `File` JSON;`GET .../:id` 返回上述 `File` 对象。
|
> `GET .../raw` 直接返回二进制流,不返回 `File` JSON;`GET .../:id` 返回上述 `File` 对象。
|
||||||
> 上传成功响应(§3.1)仅返回摘要字段:`id` / `url` / `filename` / `origin_name` / `mime_type` / `size`。
|
> 上传成功响应(§3.2)仅返回摘要字段:`id` / `url` / `filename` / `origin_name` / `mime_type` / `size`。
|
||||||
|
|
||||||
## 5. 错误码
|
## 5. 错误码
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,32 @@ func (h *Handler) Info(c *gin.Context) {
|
|||||||
response.OK(c, f)
|
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,供预览/下载。
|
// Raw 以流形式返回文件内容,按 MIME 设置 Content-Type,供预览/下载。
|
||||||
func (h *Handler) Raw(c *gin.Context) {
|
func (h *Handler) Raw(c *gin.Context) {
|
||||||
id, ok := parseID(c)
|
id, ok := parseID(c)
|
||||||
@@ -127,6 +153,7 @@ func parseID(c *gin.Context) (uint64, bool) {
|
|||||||
func RegisterAdminUpload(g *gin.RouterGroup, h *Handler) {
|
func RegisterAdminUpload(g *gin.RouterGroup, h *Handler) {
|
||||||
rg := g.Group("/files")
|
rg := g.Group("/files")
|
||||||
{
|
{
|
||||||
|
rg.GET("", h.List)
|
||||||
rg.POST("/upload", h.Upload)
|
rg.POST("/upload", h.Upload)
|
||||||
rg.GET("/:id", h.Info)
|
rg.GET("/:id", h.Info)
|
||||||
rg.GET("/:id/raw", h.Raw)
|
rg.GET("/:id/raw", h.Raw)
|
||||||
|
|||||||
@@ -49,3 +49,37 @@ func (r *Repository) Delete(ctx context.Context, id uint64) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListFilter 文件列表过滤条件。
|
||||||
|
type ListFilter struct {
|
||||||
|
UploaderType string // 上传者类型(admin/ma),为空表示不限
|
||||||
|
UploaderID uint64 // 上传者主键,0 表示不限
|
||||||
|
Keyword string // 模糊匹配 origin_name 或 filename,为空表示不限
|
||||||
|
Page int // 页码,从 1 开始
|
||||||
|
Size int // 每页条数
|
||||||
|
}
|
||||||
|
|
||||||
|
// List 分页查询文件,所有条件均为可选。返回列表、总数与错误。
|
||||||
|
func (r *Repository) List(ctx context.Context, f ListFilter) ([]model.File, int64, error) {
|
||||||
|
q := r.db.WithContext(ctx).Model(&model.File{})
|
||||||
|
if f.UploaderType != "" {
|
||||||
|
q = q.Where("uploader_type = ?", f.UploaderType)
|
||||||
|
}
|
||||||
|
if f.UploaderID != 0 {
|
||||||
|
q = q.Where("uploader_id = ?", f.UploaderID)
|
||||||
|
}
|
||||||
|
if f.Keyword != "" {
|
||||||
|
kw := "%" + f.Keyword + "%"
|
||||||
|
q = q.Where("origin_name LIKE ? OR filename LIKE ?", kw, kw)
|
||||||
|
}
|
||||||
|
|
||||||
|
var total int64
|
||||||
|
if err := q.Count(&total).Error; err != nil {
|
||||||
|
return nil, 0, errors.Wrap(50000, "count files", err)
|
||||||
|
}
|
||||||
|
var list []model.File
|
||||||
|
if err := q.Order("id DESC").Offset((f.Page - 1) * f.Size).Limit(f.Size).Find(&list).Error; err != nil {
|
||||||
|
return nil, 0, errors.Wrap(50000, "list files", err)
|
||||||
|
}
|
||||||
|
return list, total, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
"portal-mini-go/internal/pkg/errors"
|
"portal-mini-go/internal/pkg/errors"
|
||||||
"portal-mini-go/internal/pkg/storage"
|
"portal-mini-go/internal/pkg/storage"
|
||||||
"portal-mini-go/internal/pkg/types"
|
"portal-mini-go/internal/pkg/types"
|
||||||
|
"portal-mini-go/internal/repository/file"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 文件相关业务错误(业务码沿用现有 40xxx/41xxx/50xxx 段)。
|
// 文件相关业务错误(业务码沿用现有 40xxx/41xxx/50xxx 段)。
|
||||||
@@ -31,6 +32,7 @@ type FileRepository interface {
|
|||||||
Create(ctx context.Context, f *model.File) error
|
Create(ctx context.Context, f *model.File) error
|
||||||
GetByID(ctx context.Context, id uint64) (*model.File, error)
|
GetByID(ctx context.Context, id uint64) (*model.File, error)
|
||||||
Delete(ctx context.Context, id uint64) error
|
Delete(ctx context.Context, id uint64) error
|
||||||
|
List(ctx context.Context, filter file.ListFilter) ([]model.File, int64, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Service 文件上传业务服务。
|
// Service 文件上传业务服务。
|
||||||
@@ -114,6 +116,33 @@ func (s *Service) Get(ctx context.Context, id uint64) (*model.File, error) {
|
|||||||
return s.repo.GetByID(ctx, id)
|
return s.repo.GetByID(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListReq 文件列表查询条件。
|
||||||
|
type ListReq struct {
|
||||||
|
UploaderType string // 上传者类型(admin/ma),空表示不限
|
||||||
|
UploaderID uint64 // 上传者主键,0 表示不限
|
||||||
|
Keyword string // 模糊匹配 origin_name 或 filename
|
||||||
|
Page int // 页码,从 1 开始
|
||||||
|
Size int // 每页条数
|
||||||
|
}
|
||||||
|
|
||||||
|
// List 分页查询文件,缺省第 1 页、每页 20 条。
|
||||||
|
func (s *Service) List(ctx context.Context, req ListReq) ([]model.File, int64, error) {
|
||||||
|
page, size := req.Page, req.Size
|
||||||
|
if page <= 0 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if size <= 0 {
|
||||||
|
size = 20
|
||||||
|
}
|
||||||
|
return s.repo.List(ctx, file.ListFilter{
|
||||||
|
UploaderType: req.UploaderType,
|
||||||
|
UploaderID: req.UploaderID,
|
||||||
|
Keyword: req.Keyword,
|
||||||
|
Page: page,
|
||||||
|
Size: size,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Download 返回文件读取流与元数据;调用方负责关闭流。
|
// Download 返回文件读取流与元数据;调用方负责关闭流。
|
||||||
func (s *Service) Download(ctx context.Context, id uint64) (io.ReadCloser, *model.File, error) {
|
func (s *Service) Download(ctx context.Context, id uint64) (io.ReadCloser, *model.File, error) {
|
||||||
f, err := s.repo.GetByID(ctx, id)
|
f, err := s.repo.GetByID(ctx, id)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"portal-mini-go/internal/model"
|
"portal-mini-go/internal/model"
|
||||||
"portal-mini-go/internal/pkg/config"
|
"portal-mini-go/internal/pkg/config"
|
||||||
"portal-mini-go/internal/pkg/storage"
|
"portal-mini-go/internal/pkg/storage"
|
||||||
|
"portal-mini-go/internal/repository/file"
|
||||||
filesvc "portal-mini-go/internal/service/file"
|
filesvc "portal-mini-go/internal/service/file"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -56,6 +57,10 @@ type fakeRepo struct {
|
|||||||
createErr error
|
createErr error
|
||||||
delErr error
|
delErr error
|
||||||
deletedID uint64
|
deletedID uint64
|
||||||
|
listed []model.File
|
||||||
|
listTotal int64
|
||||||
|
listErr error
|
||||||
|
listFilter file.ListFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *fakeRepo) Create(_ context.Context, f *model.File) error { r.created = f; return r.createErr }
|
func (r *fakeRepo) Create(_ context.Context, f *model.File) error { r.created = f; return r.createErr }
|
||||||
@@ -66,6 +71,13 @@ func (r *fakeRepo) GetByID(_ context.Context, id uint64) (*model.File, error) {
|
|||||||
return r.byID, nil
|
return r.byID, nil
|
||||||
}
|
}
|
||||||
func (r *fakeRepo) Delete(_ context.Context, id uint64) error { r.deletedID = id; return r.delErr }
|
func (r *fakeRepo) Delete(_ context.Context, id uint64) error { r.deletedID = id; return r.delErr }
|
||||||
|
func (r *fakeRepo) List(_ context.Context, f file.ListFilter) ([]model.File, int64, error) {
|
||||||
|
r.listFilter = f
|
||||||
|
if r.listErr != nil {
|
||||||
|
return nil, 0, r.listErr
|
||||||
|
}
|
||||||
|
return r.listed, r.listTotal, nil
|
||||||
|
}
|
||||||
|
|
||||||
// 编译期接口断言。
|
// 编译期接口断言。
|
||||||
var (
|
var (
|
||||||
@@ -192,3 +204,27 @@ func TestDelete(t *testing.T) {
|
|||||||
require.Equal(t, uint64(9), repo.deletedID)
|
require.Equal(t, uint64(9), repo.deletedID)
|
||||||
require.Empty(t, store.data)
|
require.Empty(t, store.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestList(t *testing.T) {
|
||||||
|
want := []model.File{{ID: 1, OriginName: "a.png"}, {ID: 2, OriginName: "b.jpg"}}
|
||||||
|
repo := &fakeRepo{listed: want, listTotal: 2}
|
||||||
|
svc := newSvc(config.UploadConfig{}, newFakeStorage(), repo)
|
||||||
|
|
||||||
|
got, total, err := svc.List(context.Background(), filesvc.ListReq{Keyword: "png", Page: 2, Size: 15})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, int64(2), total)
|
||||||
|
require.Equal(t, want, got)
|
||||||
|
require.Equal(t, "png", repo.listFilter.Keyword)
|
||||||
|
require.Equal(t, 2, repo.listFilter.Page)
|
||||||
|
require.Equal(t, 15, repo.listFilter.Size)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestList_Defaults(t *testing.T) {
|
||||||
|
repo := &fakeRepo{listed: nil, listTotal: 0}
|
||||||
|
svc := newSvc(config.UploadConfig{}, newFakeStorage(), repo)
|
||||||
|
|
||||||
|
_, _, err := svc.List(context.Background(), filesvc.ListReq{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 1, repo.listFilter.Page, "缺省页码应为 1")
|
||||||
|
require.Equal(t, 20, repo.listFilter.Size, "缺省每页应为 20")
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user