feat: docs api design
This commit is contained in:
106
internal/config/config.go
Normal file
106
internal/config/config.go
Normal 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
25
internal/config/const.go
Normal 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"
|
||||
)
|
||||
77
internal/dal/gorm_logger.go
Normal file
77
internal/dal/gorm_logger.go
Normal 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,
|
||||
}
|
||||
18
internal/dal/migrations/migrations.go
Normal file
18
internal/dal/migrations/migrations.go
Normal 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")
|
||||
}
|
||||
}
|
||||
10
internal/dal/model/base.go
Normal file
10
internal/dal/model/base.go
Normal 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:更新时间"`
|
||||
}
|
||||
57
internal/dal/model/docs.go
Normal file
57
internal/dal/model/docs.go
Normal 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
50
internal/dal/oss.go
Normal 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
75
internal/dal/postgres.go
Normal 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
23
internal/dal/repo/docs.go
Normal 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
23
internal/router/base.go
Normal 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
82
internal/router/debug.go
Normal 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(¶ms); 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
81
internal/router/doc.go
Normal 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
106
internal/schema/base.go
Normal 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
106
internal/schema/docs.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user