feat: docs api design

This commit is contained in:
neo-f
2023-03-22 22:45:17 +08:00
commit 084d0de8bc
52 changed files with 3420 additions and 0 deletions

106
internal/config/config.go Normal file
View File

@@ -0,0 +1,106 @@
package config
import (
"net/url"
"os"
"path/filepath"
"runtime"
"sync"
"time"
"github.com/go-playground/validator"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/spf13/cast"
"github.com/spf13/viper"
)
var ENVS = [][2]string{
{"本地测试环境", "http://localhost:8080"},
}
type Config struct {
IsLocal bool
Debug bool `mapstructure:"debug"`
HTTPPort int `mapstructure:"http_port" validate:"required"`
PrometheusPort int `mapstructure:"prometheus_port" validate:"required"`
Databases struct {
OSS string `mapstructure:"oss" validate:"required"`
PostgreSQL string `mapstructure:"postgresql" validate:"required"`
Qdrant string `mapstructure:"qdrant" validate:"required"`
} `mapstructure:"databases"`
Sentry struct {
Environment string
DSN string
}
}
type ossConfig struct {
Schema string
Endpoint string
AccessID string
AccessSecret string
Region string
Bucket string
Secure bool
}
func (c *Config) GetOSSConfig() *ossConfig {
oss, err := url.Parse(c.Databases.OSS)
if err != nil {
log.Fatal().Err(err).Msg("parse oss config error")
}
accessSecret, _ := oss.User.Password()
return &ossConfig{
Schema: oss.Scheme,
Endpoint: oss.Host,
AccessID: oss.User.Username(),
AccessSecret: accessSecret,
Region: oss.Query().Get("region"),
Bucket: oss.Query().Get("bucket"),
Secure: cast.ToBool(oss.Query().Get("secure")),
}
}
var (
c *Config
once sync.Once
)
func Get() *Config {
once.Do(setup)
return c
}
func setup() {
_, b, _, _ := runtime.Caller(0)
projectRoot := filepath.Join(filepath.Dir(b), "../..")
viper.SetConfigName("config")
viper.AddConfigPath(projectRoot + "/configs")
viper.AddConfigPath("/etc/octopus/")
viper.AddConfigPath(projectRoot)
err := viper.ReadInConfig()
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
log.Info().Msg("config files not found, ignoring")
} else if err != nil {
log.Warn().Err(err).Msg("read config failed")
}
// unmarshal it
if err := viper.Unmarshal(&c); err != nil {
log.Fatal().Err(err).Msg("unmarshal config failed")
}
if c.Debug {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339})
}
// and validate it
v := validator.New()
if err := v.Struct(c); err != nil {
log.Fatal().Err(err).Msg("validate config failed")
}
log.Info().Msg("load configs success")
}

25
internal/config/const.go Normal file
View File

@@ -0,0 +1,25 @@
package config
const (
URL_HEALTH = "/healthz"
URL_MONITOR = "/sys/monitor"
URL_VERSION = "/sys/version"
URL_OPENAPI = "/openapi.json"
URL_REDOC = "/redoc"
URL_SWAGGER = "/swagger"
URL_GATEWAY_CONFIG = "/v1/global-config/api-gateway"
URL_METRICS = "/metrics"
URL_RAPIDOC = "/rapidoc"
URL_ELEMENTS = "/"
)
const (
LogTagURL = "url"
LogTagMethod = "method"
LogTagHeaders = "headers"
LogTagData = "data"
LogTagAuthorization = "authorization"
LogTagBID = "bid"
LogTagStaffID = "staff_id"
LogTagTraceID = "trace_id"
)

View File

@@ -0,0 +1,77 @@
package dal
import (
"context"
"errors"
"time"
"github.com/rs/zerolog/log"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/gorm/utils"
)
type GormLogger struct {
SourceField string
SlowThreshold time.Duration
SkipErrRecordNotFound bool
}
// Error implements logger.Interface
func (l *GormLogger) Error(ctx context.Context, s string, args ...interface{}) {
log.Ctx(ctx).Error().Msgf(s, args...)
}
// Warn implements logger.Interface
func (*GormLogger) Warn(ctx context.Context, s string, args ...interface{}) {
log.Ctx(ctx).Warn().Msgf(s, args...)
}
// Info implements logger.Interface
func (*GormLogger) Info(ctx context.Context, s string, args ...interface{}) {
log.Ctx(ctx).Info().Msgf(s, args...)
}
// LogMode implements logger.Interface
func (l *GormLogger) LogMode(logger.LogLevel) logger.Interface {
return l
}
// Trace implements logger.Interface
func (l *GormLogger) Trace(
ctx context.Context,
begin time.Time,
fc func() (string, int64),
err error,
) {
elapsed := time.Since(begin)
sql, rows := fc()
log := log.Ctx(ctx).
With().
Str("sql", sql).
Str("elapsed", elapsed.String()).
Int64("rows", rows).
Logger()
if l.SourceField != "" {
log = log.With().Str(l.SourceField, utils.FileWithLineNum()).Logger()
}
if err != nil && (!errors.Is(err, gorm.ErrRecordNotFound) || !l.SkipErrRecordNotFound) {
log.Error().Err(err).Msg("[GORM] query error")
return
}
if l.SlowThreshold != 0 && elapsed > l.SlowThreshold {
log.Warn().Msg("[GORM] slow query")
return
}
log.Debug().Msg("[GORM] query")
}
var _ logger.Interface = &GormLogger{}
var DefaultGormLogger = &GormLogger{
SlowThreshold: time.Second * 3,
SourceField: "source",
SkipErrRecordNotFound: true,
}

View File

@@ -0,0 +1,18 @@
package migrations
import (
"octopus/internal/dal/model"
"github.com/rs/zerolog/log"
"gorm.io/gorm"
)
func Migrate(db *gorm.DB) {
if err := db.AutoMigrate(
new(model.Doc),
new(model.DocFolder),
); err != nil {
log.Fatal().Err(err).Msg("failed to migrate database")
}
}

View File

@@ -0,0 +1,10 @@
package model
import "time"
type Base struct {
OrgID string `gorm:"column:org_id;type:varchar;primaryKey;comment:组织ID"` // 组织ID
ID string `gorm:"column:id;type:varchar;primaryKey;comment:ID"` // ID
CreatedAt time.Time `gorm:"column:created_at;comment:创建时间"`
UpdatedAt time.Time `gorm:"column:updated_at;comment:更新时间"`
}

View File

@@ -0,0 +1,57 @@
package model
import (
"context"
"net/url"
"octopus/internal/config"
"octopus/internal/dal"
"time"
"github.com/rs/xid"
"gorm.io/gorm"
)
const TableNameDocFolder = "doc_folders"
const TableNameDoc = "docs"
type DocFolder struct {
Base
Name string `gorm:"column:name;type:varchar;not null"` // 文件夹名称
IsDeletable bool `gorm:"column:is_deletable;type:boolean;not null;default:true"` // 是否允许被删除
IsEditable bool `gorm:"column:is_editable;type:boolean;not null;default:true"` // 是否允许被修改
Path string `gorm:"column:path;type:varchar;index:idx_path"` // 路径索引
CreatedBy string `gorm:"column:created_by;type:varchar;not null"` // 创建人
}
func (*DocFolder) TableName() string {
return TableNameDocFolder
}
func (df *DocFolder) BeforeCreate(*gorm.DB) error {
df.ID = xid.New().String()
return nil
}
type Doc struct {
Base
Name string `gorm:"column:name;type:varchar;not null"` // 文件夹名称
IsDeletable bool `gorm:"column:is_deletable;type:boolean;not null;default:true"` // 是否允许被删除
IsEditable bool `gorm:"column:is_editable;type:boolean;not null;default:true"` // 是否允许编辑名称
FolderID string `gorm:"column:folder_id;type:varchar;index:idx_folder_id"` // 文件夹ID
OSSObjectID string `gorm:"column:oss_object_id;type:varchar"` // OSS Object ID
CreatedBy string `gorm:"column:created_by;type:varchar;not null"` // 创建人
Folder *DocFolder `gorm:"foreignKey:FolderID;references:ID"`
}
func (df *Doc) PresignedURL(ctx context.Context) (*url.URL, error) {
bucket := config.Get().GetOSSConfig().Bucket
return dal.GetMinio().PresignedGetObject(ctx, bucket, df.OSSObjectID, time.Hour, nil)
}
func (*Doc) TableName() string {
return TableNameDocFolder
}
func (df *Doc) BeforeCreate(*gorm.DB) error {
df.ID = xid.New().String()
return nil
}

50
internal/dal/oss.go Normal file
View File

@@ -0,0 +1,50 @@
package dal
import (
"net/url"
"octopus/internal/config"
"sync"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
"github.com/rs/zerolog/log"
// pb "github.com/qdrant/go-client/qdrant"
)
var (
ossOnce sync.Once
ossInstance *minio.Client
)
func GetMinio() *minio.Client {
ossOnce.Do(initMinio)
return ossInstance
}
func initMinio() {
log.Info().Msg("loading minio configs")
ossConfig, err := url.Parse(config.Get().Databases.OSS)
if err != nil {
log.Fatal().Err(err).Msg("parse oss config error")
}
accessSecret, _ := ossConfig.User.Password()
// defaultConfig := &model.StorageConfig{
// Schema: ossConfig.Scheme,
// Endpoint: ossConfig.Host,
// AccessID: ossConfig.User.Username(),
// AccessSecret: accessSecret,
// Bucket: ossConfig.Query().Get("bucket"),
// Region: ossConfig.Query().Get("region"),
// Secure: cast.ToBool(ossConfig.Query().Get("secure")),
// }
minioClient, err := minio.New(ossConfig.Host, &minio.Options{
Creds: credentials.NewStaticV4(ossConfig.User.Username(), accessSecret, ""),
Secure: false,
})
if err != nil {
log.Fatal().Msgf("minio client init error: %v", err)
}
ossInstance = minioClient
}

75
internal/dal/postgres.go Normal file
View File

@@ -0,0 +1,75 @@
package dal
import (
slog "log"
"os"
"sync"
"time"
"octopus/internal/config"
"github.com/rs/zerolog/log"
"gorm.io/driver/postgres"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
var (
sqlOnce sync.Once
sqlInstance *gorm.DB
)
func GetpostgresTest() *gorm.DB {
l := logger.Default
l = l.LogMode(logger.Info)
db, _ := gorm.Open(sqlite.Open("file::memory:?parseTime=True&loc=Local"), &gorm.Config{Logger: l})
return db
}
func GetPostgres() *gorm.DB {
sqlOnce.Do(initPostgres)
return sqlInstance
}
func initPostgres() {
log.Info().Msg("loading postgres configs")
dsn := config.Get().Databases.PostgreSQL
db, err := gorm.Open(
postgres.Open(dsn),
&gorm.Config{
DisableForeignKeyConstraintWhenMigrating: true,
Logger: logger.New(
slog.New(os.Stdout, "\r\n", slog.LstdFlags), logger.Config{
SlowThreshold: 200 * time.Millisecond,
LogLevel: logger.Info,
IgnoreRecordNotFoundError: false,
Colorful: true,
},
),
},
)
if err != nil {
log.Fatal().Str("dsn", dsn).Err(err).Msg("connected to postgres failed")
}
sqlDB, err := db.DB()
if err != nil {
log.Fatal().Str("dsn", dsn).Err(err).Msg("connected to postgres failed")
}
// SetMaxIdleConns sets the maximum number of connections in the idle connection pool.
sqlDB.SetMaxIdleConns(50)
// SetMaxOpenConns sets the maximum number of open connections to the database.
sqlDB.SetMaxOpenConns(50)
// SetConnMaxLifetime sets the maximum amount of time a connection may be reused.
sqlDB.SetConnMaxLifetime(time.Minute * 2)
if err != nil {
log.Fatal().Str("dsn", dsn).Err(err).Msg("connected to postgres failed")
}
if config.Get().Debug {
db = db.Debug()
}
sqlInstance = db
log.Info().Msg("connected to postgres")
}

23
internal/dal/repo/docs.go Normal file
View File

@@ -0,0 +1,23 @@
package repo
import (
"octopus/internal/dal/model"
"gorm.io/gorm"
)
type DocFolderRepo struct {
db *gorm.DB
}
func NewDocFolderRepo(db *gorm.DB) *DocFolderRepo {
return &DocFolderRepo{db}
}
type DocFolderRepoInterface interface {
CreateFolder(folder *model.DocFolder) error
}
// func (r *DocFolderRepo) CreateFolder(folder *model.DocFolder) error {
// return r.db.Create(folder).Error
// }

23
internal/router/base.go Normal file
View File

@@ -0,0 +1,23 @@
package router
import (
"github.com/casdoor/casdoor-go-sdk/casdoorsdk"
"github.com/gofiber/fiber/v2"
"github.com/rs/zerolog/log"
)
var userClaimsKey = struct{}{}
func JWTRequired(c *fiber.Ctx) error {
jwt := c.Get("Authorization")
if jwt == "" {
return c.Status(401).JSON(fiber.Map{"msg": "Unauthorized"})
}
claims, err := casdoorsdk.ParseJwtToken(jwt)
if err != nil {
log.Ctx(c.UserContext()).Error().Err(err).Msg("Unauthorized user")
return c.Status(401).JSON(fiber.Map{"msg": "Unauthorized"})
}
c.Locals(userClaimsKey, claims)
return nil
}

82
internal/router/debug.go Normal file
View File

@@ -0,0 +1,82 @@
package router
import (
"context"
"fmt"
"octopus/statics"
"sort"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/basicauth"
"github.com/neo-f/soda"
)
func RegisterDebuggerRouter(app *soda.Soda) {
app.App.Get("/debugger", basicAuth, func(c *fiber.Ctx) error {
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
return c.SendString(statics.DebuggerHTML)
})
app.App.Get("/debugger/functions", basicAuth, func(c *fiber.Ctx) error {
sort.Slice(debugFunctions, func(i, j int) bool {
return debugFunctions[i].Title < debugFunctions[j].Title
})
return c.JSON(debugFunctions)
})
app.App.Post("/debugger/execute", basicAuth, func(c *fiber.Ctx) error {
var params struct {
Func string `json:"func"`
Params map[string]string `json:"params"`
}
if err := c.BodyParser(&params); err != nil {
return err
}
var fn *DebugFunction
for _, f := range debugFunctions {
if f.Name == params.Func {
fn = &f
break
}
}
if fn == nil {
return fmt.Errorf("function not found")
}
resp, err := fn.Do(c.UserContext(), params.Params)
if err != nil {
return err
}
return c.JSON(resp)
})
}
var basicAuth = basicauth.New(basicauth.Config{
Users: map[string]string{"neo": "whosyourdaddy"},
})
type DebugFunctionParameter struct {
Name string `json:"name"`
Description string `json:"description"`
Required bool `json:"required"`
}
type DebugFunction struct {
Name string `json:"name"`
Title string `json:"title"`
Description string `json:"description"`
Parameters []DebugFunctionParameter `json:"parameters"`
Do func(context.Context, map[string]string) (interface{}, error) `json:"-"`
}
var debugFunctions = []DebugFunction{
{
Name: "echo",
Title: "demo debugger",
Description: "echo the parameters",
Parameters: []DebugFunctionParameter{
{Name: "param-a", Description: "param-a", Required: true},
{Name: "param-b", Description: "param-b", Required: true},
},
Do: func(ctx context.Context, params map[string]string) (interface{}, error) {
return params, nil
},
},
}

81
internal/router/doc.go Normal file
View File

@@ -0,0 +1,81 @@
package router
import (
"octopus/internal/schema"
"github.com/neo-f/soda"
)
func RegisterDocRouter(app *soda.Soda) {
app.Get("/docs", nil).
AddTags("文档管理").
SetSummary("获取文档列表").
AddJWTSecurity(JWTRequired).
SetParameters(schema.ListDocQuery{}).
AddJSONResponse(200, schema.DocList{}).OK()
app.Post("/docs", nil).
AddTags("文档管理").
SetSummary("新建文档").
AddJWTSecurity(JWTRequired).
SetJSONRequestBody(schema.CreateDoc{}).
AddJSONResponse(200, schema.Doc{}).OK()
app.Put("/docs/:id", nil).
AddTags("文档管理").
SetSummary("更新文档").
AddJWTSecurity(JWTRequired).
SetParameters(schema.DocID{}).
SetJSONRequestBody(schema.ListDocQuery{}).
AddJSONResponse(200, schema.Doc{}).OK()
app.Delete("/docs/:id", nil).
AddTags("文档管理").
SetSummary("获取文档列表").
AddJWTSecurity(JWTRequired).
SetParameters(schema.DocID{}).
AddJSONResponse(200, nil).OK()
app.Post("/docs/batch/delete", nil).
AddTags("文档管理").
SetSummary("批量-文件删除").
AddJWTSecurity(JWTRequired).
SetJSONRequestBody(schema.DocsBatchDelete{}).
AddJSONResponse(200, schema.DocsBatchResults{}).OK()
app.Post("/docs/batch/update", nil).
AddTags("文档管理").
SetSummary("批量-文件删除").
AddJWTSecurity(JWTRequired).
SetJSONRequestBody(schema.DocsBatchUpdate{}).
AddJSONResponse(200, schema.DocsBatchResults{}).OK()
app.Get("/doc-folders", nil).
AddTags("文档管理").
SetSummary("获取文件夹树").
AddJWTSecurity(JWTRequired).
SetParameters(schema.GetDocFolderTree{}).
AddJSONResponse(200, schema.DocFolderWithChildren{}).OK()
app.Post("/doc-folders", nil).
AddTags("文档管理").
SetSummary("新建文件夹").
AddJWTSecurity(JWTRequired).
SetJSONRequestBody(schema.CreateDocFolder{}).
AddJSONResponse(200, schema.DocFolder{}).OK()
app.Put("/doc-folders/:id", nil).
AddTags("文档管理").
SetSummary("更新文件夹").
AddJWTSecurity(JWTRequired).
SetParameters(schema.DocFolderID{}).
SetJSONRequestBody(schema.UpdateDocFolder{}).
AddJSONResponse(200, schema.DocFolder{}).OK()
app.Delete("/doc-folders/:id", nil).
AddTags("文档管理").
SetSummary("删除文件夹").
AddJWTSecurity(JWTRequired).
SetParameters(schema.DocFolderID{}).
AddJSONResponse(200, nil).OK()
}

106
internal/schema/base.go Normal file
View File

@@ -0,0 +1,106 @@
package schema
const (
maxPageSize = 1000
defaultPageSize = 10
)
type PageableQuery struct {
Size *int `query:"size" oai:"description=返回数据数量;default=10;maximum=1000"`
Offset *int `query:"offset" oai:"description=数据偏移量;default=0"`
}
func (p *PageableQuery) GetLimit() int {
if p.Size == nil {
return defaultPageSize
}
if *p.Size >= maxPageSize {
return maxPageSize
}
return *p.Size
}
func (p *PageableQuery) GetOffset() int {
if p.Offset == nil {
return 0
}
return *p.Offset
}
type SortField struct {
Field string
Asc bool
}
type SortableQuery struct {
SortBy *[]string `query:"sort_by" oai:"description=排序字段, 如: +id,-created_at,test 表示依次按照id正序,created_at倒序,test正序"`
}
func (s *SortableQuery) GetOrderField() []SortField {
if s.SortBy == nil {
return nil
}
fields := make([]SortField, 0, len(*s.SortBy))
for _, v := range *s.SortBy {
if v == "" {
continue
}
switch v[0] {
case '+':
fields = append(fields, SortField{Field: v[1:], Asc: true})
case '-':
fields = append(fields, SortField{Field: v[1:], Asc: false})
default:
fields = append(fields, SortField{Field: v, Asc: true})
}
}
return fields
}
type SortableBody struct {
SortBy *[]string `json:"sort_by" oai:"description=排序字段, 如: id desc/asc"`
}
func (s *SortableBody) GetOrderField() []SortField {
if s.SortBy == nil {
return nil
}
fields := make([]SortField, 0, len(*s.SortBy))
for _, v := range *s.SortBy {
if v == "" {
continue
}
switch v[0] {
case '+':
fields = append(fields, SortField{Field: v[1:], Asc: true})
case '-':
fields = append(fields, SortField{Field: v[1:], Asc: false})
default:
fields = append(fields, SortField{Field: v, Asc: true})
}
}
return fields
}
type PageableBody struct {
Size *int `json:"size" oai:"description=返回数据数量;default=10;maximum=1000"`
Offset *int `json:"offset" oai:"description=数据偏移量;default=0"`
}
func (p *PageableBody) GetLimit() int {
if p.Size == nil {
return defaultPageSize
}
if *p.Size >= maxPageSize {
return maxPageSize
}
return *p.Size
}
func (p *PageableBody) GetOffset() int {
if p.Offset == nil {
return 0
}
return *p.Offset
}

106
internal/schema/docs.go Normal file
View File

@@ -0,0 +1,106 @@
package schema
import (
"octopus/pkg/tools"
"time"
"github.com/gofiber/fiber/v2"
)
type DocFolder struct {
ID string `json:"id" oai:"description=文件夹ID"`
IsDeletable bool `json:"is_deletable" oai:"description=文件夹是否允许被删除"`
IsEditable bool `json:"is_editable" oai:"description=文件夹是否允许被修改"`
CreatedAt time.Time `json:"created_at" oai:"description=创建时间"`
UpdatedAt time.Time `json:"updated_at" oai:"description=更新时间"`
}
type DocFolderID struct {
ID string `path:"id" oai:"description=文件夹ID"`
}
type CreateDocFolder struct {
Name string `json:"name" validate:"required" oai:"description=文件夹名称"`
ParentID string `json:"parent_id" oai:"description=父文件夹ID,空表示在根路径创建"`
}
type UpdateDocFolder struct {
Name *string `json:"name" oai:"description=修改文件夹名称"`
ParentID *string `json:"parent_id" oai:"description=修改文件夹父级ID"`
}
type GetDocFolderTree struct {
ParentID *string `query:"parent_id" oai:"description=父文件夹ID,空表示获取根路径"`
}
type DocFolderWithChildren struct {
ID string `json:"id" oai:"description=文件夹ID**根文件夹无ID**"`
IsDeletable bool `json:"is_deletable" oai:"description=文件夹是否允许被删除"`
IsEditable bool `json:"is_editable" oai:"description=文件夹是否允许被修改"`
CreatedAt time.Time `json:"created_at" oai:"description=创建时间"`
UpdatedAt time.Time `json:"updated_at" oai:"description=更新时间"`
Children []*DocFolderWithChildren `json:"children" oai:"description=子文件夹"`
}
type Doc struct {
ID string `json:"id" oai:"description=文档ID"`
Folder DocFolder `json:"folder" oai:"description=归属文件夹信息"`
PresignedURL string `json:"presigned_url" oai:"description=文档预签名下载URL(临时下载URL)"`
IsDeletable bool `json:"is_deletable" oai:"description=文件夹是否允许被删除"`
IsEditable bool `json:"is_editable" oai:"description=文件夹是否允许被修改"`
UploadedAt time.Time `json:"uploaded_at" oai:"description=上传时间"`
CreatedAt time.Time `json:"created_at" oai:"description=创建时间"`
UpdatedAt time.Time `json:"updated_at" oai:"description=更新时间"`
}
type DocList struct {
Items []*Doc `json:"items" oai:"description=文档列表"`
Total int64 `json:"total" oai:"description=文档总数"`
}
type DocID struct {
ID string `path:"id" oai:"description=文档ID"`
}
type CreateDoc struct {
Name string `json:"name" validate:"required" oai:"description=文档名称"`
FolderID string `json:"folder_id" validate:"required" oai:"description=归属文件夹ID"`
}
type UpdateDoc struct {
Name *string `json:"name" oai:"description=更新文档名称"`
FolderID *string `json:"folder_id" oai:"description=更新归属文件夹ID"`
}
type ListDocQuery struct {
PageableQuery
SortableQuery
FolderIDs *[]string `query:"folder_ids" validate:"required" oai:"description=归属文件夹ID"`
}
type DocsBatchDelete struct {
IDs []string `query:"ids" validate:"required" oai:"description=批量选择文件ID列表"`
FolderID string ` validate:"required" oai:"description=更新归属文件夹ID" json:"folder_id"`
}
type DocsBatchUpdate struct {
IDs []string `query:"ids" validate:"required" oai:"description=批量选择文件ID列表"`
}
type DocActionResult struct {
ID string `json:"id" oai:"description=文档ID"`
Success bool `json:"success" oai:"description=操作是否成功"`
Message *string `json:"message,omitempty" oai:"description=操作失败信息"`
}
type DocsBatchResults []DocActionResult
func (q *ListDocQuery) Validate() error {
sortable := tools.NewSet("uploaded_at")
for _, s := range q.GetOrderField() {
if !sortable.Has(s.Field) {
return fiber.NewError(fiber.StatusBadRequest, "unsupported order field")
}
}
return nil
}