优化结构

This commit is contained in:
Yvan
2026-08-21 00:01:48 +08:00
parent e65cf0b1d0
commit 746e33e027
344 changed files with 1021 additions and 578 deletions
+205
View File
@@ -0,0 +1,205 @@
package server
import (
"log/slog"
"mime"
"net/http"
"os"
"path"
"sort"
"strings"
"time"
"kra/app/system/service"
"kra/app/system/transport/handler"
"kra/app/system/transport/httpx"
servermiddleware "kra/app/system/transport/middleware"
"kra/internal/conf"
platformmodule "kra/pkg/module"
"github.com/gin-gonic/gin"
kratoshttp "github.com/go-kratos/kratos/v3/transport/http"
)
func NewGinEngine(runtime *conf.Runtime, access *service.AccessControlService, handlers *handler.Set, auth *service.AuthService, security *service.SecurityService, audit *service.AuditRecorder, logger *slog.Logger, version string) *gin.Engine {
return NewGinEngineWithRuntime(runtime, access, auth, security, audit, logger, version, platformmodule.NewRuntime(NewRoutes(handlers)))
}
func NewGinEngineWithRuntime(runtime *conf.Runtime, access *service.AccessControlService, auth *service.AuthService, security *service.SecurityService, audit *service.AuditRecorder, logger *slog.Logger, version string, routes *platformmodule.Runtime) *gin.Engine {
gin.SetMode(gin.ReleaseMode)
engine := gin.New()
if err := engine.SetTrustedProxies(nil); err != nil && logger != nil {
logger.Error("disable trusted proxies failed", "error", err)
}
engine.Use(servermiddleware.RequestMeta(), servermiddleware.Recovery(logger), servermiddleware.AccessLog(runtime, logger, version), servermiddleware.CORS(runtime), servermiddleware.ErrorAudit(logger), servermiddleware.SecurityRateLimit(security))
prefix := ""
config := runtime.Admin()
if config != nil {
prefix = strings.TrimSuffix(config.RouterPrefix, "/")
}
public := engine.Group(prefix)
public.GET("/health", func(c *gin.Context) { c.JSON(http.StatusOK, "ok") })
private := engine.Group(prefix)
// The reference administration behavior installs operation recording after JWT,
// password and Casbin/data-scope middleware. Keeping the recorder after the
// equivalent Kra guards avoids persisting rejected unauthenticated or
// unauthorized requests as successful business operations.
private.Use(servermiddleware.Auth(auth), servermiddleware.MustChangePassword(), servermiddleware.AccessControl(runtime, access), servermiddleware.OperationAudit(runtime, audit))
if routes != nil {
routes.RegisterRoutes(public, private, engine)
}
registerSwagger(engine, prefix, version, logger)
registerLocalStorage(engine, runtime)
engine.NoRoute(func(c *gin.Context) {
if serveLocalStorage(c, runtime) {
return
}
httpx.Fail(c, "请求的接口不存在")
})
logRegisteredRoutes(engine, logger)
return engine
}
func logRegisteredRoutes(engine *gin.Engine, logger *slog.Logger) {
if logger == nil {
return
}
routes := append([]gin.RouteInfo(nil), engine.Routes()...)
sort.Slice(routes, func(i, j int) bool {
if routes[i].Path == routes[j].Path {
return routes[i].Method < routes[j].Method
}
return routes[i].Path < routes[j].Path
})
systemLogger := logger.With("mod", "system")
for _, route := range routes {
systemLogger.Info("router registered", "method", route.Method, "path", route.Path)
}
systemLogger.Info("router register success", "route_count", len(routes))
}
func NewGinServer(c *conf.Server, engine *gin.Engine) *kratoshttp.Server {
network, address := "tcp", ":8000"
if c != nil && c.Http != nil {
if c.Http.Network != "" {
network = c.Http.Network
}
if c.Http.Addr != "" {
address = c.Http.Addr
}
}
timeout := 10 * time.Minute
if c != nil && c.Http != nil && c.Http.Timeout != nil && c.Http.Timeout.AsDuration() > 0 {
timeout = c.Http.Timeout.AsDuration()
}
server := kratoshttp.NewServer(kratoshttp.Network(network), kratoshttp.Address(address), kratoshttp.Timeout(timeout))
server.HandlePrefix("/", engine)
server.Server.ReadHeaderTimeout = 10 * time.Second
server.Server.ReadTimeout = timeout
server.Server.WriteTimeout = timeout
server.Server.IdleTimeout = 2 * time.Minute
server.Server.MaxHeaderBytes = 1 << 20
return server
}
// serveLocalStorage resolves the local path for every request so a config
// reload takes effect without rebuilding the Gin engine.
func serveLocalStorage(c *gin.Context, runtime *conf.Runtime) bool {
config := runtime.Admin()
if config == nil || config.Local == nil || config.Local.StorePath == "" {
return false
}
prefix := "/" + strings.Trim(config.Local.PathPrefix, "/")
return serveLocalStorageAt(c, runtime, prefix)
}
func registerLocalStorage(engine *gin.Engine, runtime *conf.Runtime) {
config := runtime.Admin()
if config == nil || config.Local == nil || config.Local.StorePath == "" || strings.Trim(config.Local.PathPrefix, "/") == "" {
return
}
if config.Storage != nil && config.Storage.Type != "" && config.Storage.Type != "local" {
return
}
prefix := "/" + strings.Trim(config.Local.PathPrefix, "/")
if localStorageRouteConflicts(engine.Routes(), prefix) {
return
}
handler := func(c *gin.Context) {
if !serveLocalStorageAt(c, runtime, prefix) {
c.Status(http.StatusNotFound)
}
}
engine.GET(prefix+"/*filepath", handler)
engine.HEAD(prefix+"/*filepath", handler)
}
func localStorageRouteConflicts(routes []gin.RouteInfo, prefix string) bool {
staticRoot := strings.Split(strings.TrimPrefix(prefix, "/"), "/")[0]
for _, route := range routes {
if route.Method != http.MethodGet && route.Method != http.MethodHead {
continue
}
routeRoot := strings.Split(strings.TrimPrefix(route.Path, "/"), "/")[0]
if routeRoot == staticRoot {
return true
}
}
return false
}
func serveLocalStorageAt(c *gin.Context, runtime *conf.Runtime, prefix string) bool {
config := runtime.Admin()
if config == nil || config.Local == nil || config.Local.StorePath == "" {
return false
}
if config.Storage != nil && config.Storage.Type != "" && config.Storage.Type != "local" {
return false
}
if currentPrefix := "/" + strings.Trim(config.Local.PathPrefix, "/"); currentPrefix != prefix {
return false
}
if prefix == "/" || (c.Request.URL.Path != prefix && !strings.HasPrefix(c.Request.URL.Path, prefix+"/")) {
return false
}
c.Header("X-Content-Type-Options", "nosniff")
filename := path.Base(c.Request.URL.Path)
if !canServeUploadInline(filename) {
c.Header("Content-Disposition", mime.FormatMediaType("attachment", map[string]string{"filename": filename}))
}
http.StripPrefix(prefix, http.FileServer(filesOnly{FileSystem: http.Dir(config.Local.StorePath)})).ServeHTTP(c.Writer, c.Request)
return true
}
func canServeUploadInline(filename string) bool {
switch strings.ToLower(path.Ext(filename)) {
case ".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".ico", ".avif",
".mp3", ".wav", ".ogg", ".m4a", ".flac", ".aac",
".mp4", ".webm", ".mov", ".avi", ".mkv":
return true
default:
return false
}
}
type filesOnly struct{ http.FileSystem }
func (f filesOnly) Open(name string) (http.File, error) {
file, err := f.FileSystem.Open(name)
if err != nil {
return nil, err
}
info, statErr := file.Stat()
if statErr != nil {
_ = file.Close()
return nil, statErr
}
if info.IsDir() {
_ = file.Close()
return nil, os.ErrPermission
}
return file, nil
}
+402
View File
@@ -0,0 +1,402 @@
package server
import (
"bytes"
"log/slog"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"sort"
"strings"
"testing"
"kra/app/system/transport/handler"
"kra/internal/conf"
"github.com/gin-gonic/gin"
)
func emptyHandlers() *handler.Set {
return &handler.Set{
Authority: &handler.Authority{}, Menu: &handler.Menu{}, API: &handler.API{},
Permission: &handler.Permission{}, Organization: &handler.Organization{},
Announcement: &handler.Announcement{}, Email: &handler.Email{}, Payment: &handler.Payment{}, Task: &handler.Task{},
Media: &handler.Media{}, Audit: &handler.Audit{}, Export: &handler.Export{},
Version: &handler.Version{}, Dictionary: &handler.Dictionary{}, Parameter: &handler.Parameter{},
APIToken: &handler.APIToken{}, SystemConfig: &handler.SystemConfig{}, Public: &handler.Public{},
User: &handler.User{}, Navigation: &handler.Navigation{}, Session: &handler.Session{},
}
}
func TestGinRouteContract(t *testing.T) {
engine := NewGinEngine(conf.NewRuntime(nil, &conf.AdminBackend{}), nil, emptyHandlers(), nil, nil, nil, nil, "test")
routes := engine.Routes()
actual := make([]string, 0, len(routes))
seen := make(map[string]struct{}, len(routes))
for _, route := range routes {
key := route.Method + " " + route.Path
if _, exists := seen[key]; exists {
t.Fatalf("duplicate route %s", key)
}
seen[key] = struct{}{}
actual = append(actual, key)
}
sort.Strings(actual)
if value := strings.Join(actual, "\n"); value != expectedGinRouteContract {
t.Fatalf("route contract changed:\n%s", value)
}
}
func TestGinStartupLogsEveryRegisteredRoute(t *testing.T) {
var output bytes.Buffer
logger := slog.New(slog.NewJSONHandler(&output, nil))
engine := NewGinEngine(conf.NewRuntime(nil, &conf.AdminBackend{}), nil, emptyHandlers(), nil, nil, nil, logger, "test")
text := output.String()
if got, want := strings.Count(text, `"msg":"router registered"`), len(engine.Routes()); got != want {
t.Fatalf("registered route log count = %d, want %d", got, want)
}
if !strings.Contains(text, `"msg":"router register success"`) || !strings.Contains(text, `"route_count":186`) {
t.Fatalf("startup route summary is missing: %s", text)
}
}
func TestSwaggerUsesRegisteredGinRoutes(t *testing.T) {
engine := NewGinEngine(conf.NewRuntime(nil, &conf.AdminBackend{}), nil, emptyHandlers(), nil, nil, nil, nil, "v1.0.0")
response := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "/swagger/doc.json", nil)
engine.ServeHTTP(response, request)
if response.Code != http.StatusOK {
t.Fatalf("swagger document status = %d, body=%s", response.Code, response.Body.String())
}
body := response.Body.String()
for _, expected := range []string{`"swagger":"2.0"`, `"version":"v1.0.0"`, `"/base/login"`, `"/timedTask/triggerTimedTask"`, `"/mediaUpload/{uploadId}"`} {
if !strings.Contains(body, expected) {
t.Fatalf("swagger document missing %s", expected)
}
}
}
func TestSwaggerSupportsRouterPrefix(t *testing.T) {
engine := NewGinEngine(conf.NewRuntime(nil, &conf.AdminBackend{RouterPrefix: "/admin"}), nil, emptyHandlers(), nil, nil, nil, nil, "v1.0.0")
for _, path := range []string{"/admin/swagger/index.html", "/admin/swagger/doc.json"} {
response := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, path, nil)
engine.ServeHTTP(response, request)
if response.Code != http.StatusOK {
t.Fatalf("swagger path %s status = %d, body=%s", path, response.Code, response.Body.String())
}
if strings.HasSuffix(path, "/doc.json") && !strings.Contains(response.Body.String(), `"basePath":"/admin"`) {
t.Fatalf("swagger document does not use router prefix: %s", response.Body.String())
}
}
}
func TestSwaggerUsesRootBasePathWithoutRouterPrefix(t *testing.T) {
engine := NewGinEngine(conf.NewRuntime(nil, &conf.AdminBackend{}), nil, emptyHandlers(), nil, nil, nil, nil, "v1.0.0")
response := httptest.NewRecorder()
engine.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/swagger/doc.json", nil))
if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), `"basePath":"/"`) {
t.Fatalf("swagger basePath mismatch: status=%d body=%s", response.Code, response.Body.String())
}
}
func TestGinDoesNotTrustForwardedIPByDefault(t *testing.T) {
engine := NewGinEngine(conf.NewRuntime(nil, &conf.AdminBackend{}), nil, emptyHandlers(), nil, nil, nil, nil, "test")
engine.GET("/client-ip", func(c *gin.Context) { c.String(http.StatusOK, c.ClientIP()) })
request := httptest.NewRequest(http.MethodGet, "/client-ip", nil)
request.RemoteAddr = "203.0.113.10:4321"
request.Header.Set("X-Forwarded-For", "198.51.100.20")
response := httptest.NewRecorder()
engine.ServeHTTP(response, request)
if response.Code != http.StatusOK || response.Body.String() != "203.0.113.10" {
t.Fatalf("client IP = %q, status=%d", response.Body.String(), response.Code)
}
}
func TestAnnouncementDataSourceRequiresAuthentication(t *testing.T) {
engine := NewGinEngine(conf.NewRuntime(nil, &conf.AdminBackend{}), nil, emptyHandlers(), nil, nil, nil, nil, "test")
response := httptest.NewRecorder()
engine.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/info/getInfoDataSource", nil))
if response.Code != http.StatusUnauthorized {
t.Fatalf("status=%d body=%s", response.Code, response.Body.String())
}
}
func TestLocalStorageResponseHeaders(t *testing.T) {
root := t.TempDir()
for name, body := range map[string]string{"script.html": "<script>alert(1)</script>", "image.png": "png"} {
if err := os.WriteFile(filepath.Join(root, name), []byte(body), 0o600); err != nil {
t.Fatal(err)
}
}
runtime := conf.NewRuntime(nil, &conf.AdminBackend{Local: &conf.AdminBackend_Local{StorePath: root, PathPrefix: "uploads/file"}, Storage: &conf.AdminBackend_Storage{Type: "local"}})
engine := NewGinEngine(runtime, nil, emptyHandlers(), nil, nil, nil, nil, "test")
for _, test := range []struct {
path string
attachment bool
}{
{path: "/uploads/file/script.html", attachment: true},
{path: "/uploads/file/image.png", attachment: false},
} {
response := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, test.path, nil)
engine.ServeHTTP(response, request)
if response.Code != http.StatusOK || response.Header().Get("X-Content-Type-Options") != "nosniff" {
t.Fatalf("unexpected static response for %s: status=%d headers=%v", test.path, response.Code, response.Header())
}
hasAttachment := strings.Contains(response.Header().Get("Content-Disposition"), "attachment")
if hasAttachment != test.attachment {
t.Fatalf("attachment header for %s = %v, want %v", test.path, hasAttachment, test.attachment)
}
}
}
func TestLocalStorageRoutesAreRegistered(t *testing.T) {
runtime := conf.NewRuntime(nil, &conf.AdminBackend{Local: &conf.AdminBackend_Local{StorePath: t.TempDir(), PathPrefix: "uploads/file"}})
engine := NewGinEngine(runtime, nil, emptyHandlers(), nil, nil, nil, nil, "test")
seen := map[string]bool{}
for _, route := range engine.Routes() {
seen[route.Method+" "+route.Path] = true
}
for _, expected := range []string{"GET /uploads/file/*filepath", "HEAD /uploads/file/*filepath"} {
if !seen[expected] {
t.Fatalf("missing registered static route %s", expected)
}
}
response := httptest.NewRecorder()
engine.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/swagger/doc.json", nil))
if strings.Contains(response.Body.String(), "/uploads/file/") {
t.Fatal("swagger document should not expose static file wildcard")
}
}
func TestLocalStorageOldPrefixStopsServingAfterReload(t *testing.T) {
root := t.TempDir()
if err := os.WriteFile(filepath.Join(root, "image.png"), []byte("png"), 0o600); err != nil {
t.Fatal(err)
}
runtime := conf.NewRuntime(nil, &conf.AdminBackend{Local: &conf.AdminBackend_Local{StorePath: root, PathPrefix: "uploads/file"}, Storage: &conf.AdminBackend_Storage{Type: "local"}})
engine := NewGinEngine(runtime, nil, emptyHandlers(), nil, nil, nil, nil, "test")
runtime.Replace(nil, &conf.AdminBackend{Local: &conf.AdminBackend_Local{StorePath: root, PathPrefix: "files"}, Storage: &conf.AdminBackend_Storage{Type: "local"}})
oldResponse := httptest.NewRecorder()
engine.ServeHTTP(oldResponse, httptest.NewRequest(http.MethodGet, "/uploads/file/image.png", nil))
if oldResponse.Code != http.StatusNotFound {
t.Fatalf("old static prefix status = %d, want %d", oldResponse.Code, http.StatusNotFound)
}
newResponse := httptest.NewRecorder()
engine.ServeHTTP(newResponse, httptest.NewRequest(http.MethodGet, "/files/image.png", nil))
if newResponse.Code != http.StatusOK {
t.Fatalf("new static prefix status = %d, want %d", newResponse.Code, http.StatusOK)
}
}
func TestLocalStorageConflictingPrefixFallsBackWithoutStartupPanic(t *testing.T) {
runtime := conf.NewRuntime(nil, &conf.AdminBackend{Local: &conf.AdminBackend_Local{StorePath: t.TempDir(), PathPrefix: "api"}, Storage: &conf.AdminBackend_Storage{Type: "local"}})
engine := NewGinEngine(runtime, nil, emptyHandlers(), nil, nil, nil, nil, "test")
for _, route := range engine.Routes() {
if route.Method == http.MethodGet && route.Path == "/api/*filepath" {
t.Fatal("conflicting static wildcard must not be registered")
}
}
}
func TestLocalStorageRouteIsNotRegisteredForRemoteStorage(t *testing.T) {
runtime := conf.NewRuntime(nil, &conf.AdminBackend{Local: &conf.AdminBackend_Local{StorePath: t.TempDir(), PathPrefix: "uploads/file"}, Storage: &conf.AdminBackend_Storage{Type: "s3"}})
engine := NewGinEngine(runtime, nil, emptyHandlers(), nil, nil, nil, nil, "test")
for _, route := range engine.Routes() {
if strings.HasSuffix(route.Path, "/*filepath") && strings.HasPrefix(route.Path, "/uploads/file/") {
t.Fatalf("remote storage registered local route %s %s", route.Method, route.Path)
}
}
}
const expectedGinRouteContract = `DELETE /api/deleteApisByIds
DELETE /dataAccessLog/deleteDataAccessLogByIds
DELETE /department/deleteDepartment
DELETE /info/deleteInfo
DELETE /info/deleteInfoByIds
DELETE /mediaUpload/:uploadId
DELETE /position/deletePosition
DELETE /sysDictionary/deleteSysDictionary
DELETE /sysDictionaryDetail/deleteSysDictionaryDetail
DELETE /sysError/deleteSysError
DELETE /sysError/deleteSysErrorByIds
DELETE /sysExportTemplate/deleteSysExportTemplate
DELETE /sysExportTemplate/deleteSysExportTemplateByIds
DELETE /sysLoginLog/deleteLoginLog
DELETE /sysLoginLog/deleteLoginLogByIds
DELETE /sysOperationRecord/deleteSysOperationRecord
DELETE /sysOperationRecord/deleteSysOperationRecordByIds
DELETE /sysParams/deleteSysParams
DELETE /sysParams/deleteSysParamsByIds
DELETE /sysVersion/deleteSysVersion
DELETE /sysVersion/deleteSysVersionByIds
DELETE /timedTask/deleteTimedTask
DELETE /user/deleteUser
GET /api/freshCasbin
GET /api/getApiGroups
GET /api/getApiRoles
GET /api/syncApi
GET /attachmentCategory/getCategoryList
GET /authority/getDataScopeDepts
GET /authority/getUsersByAuthority
GET /department/findDepartment
GET /department/getDepartmentUsers
GET /fileUploadAndDownload/findFile
GET /health
GET /info/findInfo
GET /info/getInfoDataSource
GET /info/getInfoList
GET /info/getInfoPublic
GET /logViewer/content
GET /logViewer/dates
GET /logViewer/files
GET /menu/getMenuRoles
GET /payment/configs
GET /payment/orders
GET /position/findPosition
GET /position/getPositionUsers
GET /securityConfig/getSecurityConfig
GET /swagger/*any
GET /sysDictionary/exportSysDictionary
GET /sysDictionary/findSysDictionary
GET /sysDictionary/getSysDictionaryList
GET /sysDictionary/getSysDictionaryListWithDetails
GET /sysDictionaryDetail/findSysDictionaryDetail
GET /sysDictionaryDetail/getDictionaryDetailsByParent
GET /sysDictionaryDetail/getDictionaryPath
GET /sysDictionaryDetail/getDictionaryTreeList
GET /sysDictionaryDetail/getDictionaryTreeListByType
GET /sysDictionaryDetail/getSysDictionaryDetailList
GET /sysError/findSysError
GET /sysError/getSysErrorList
GET /sysExportTemplate/exportExcel
GET /sysExportTemplate/exportExcelByToken
GET /sysExportTemplate/exportTemplate
GET /sysExportTemplate/exportTemplateByToken
GET /sysExportTemplate/findSysExportTemplate
GET /sysExportTemplate/getSysExportTemplateList
GET /sysExportTemplate/previewSQL
GET /sysLoginLog/findLoginLog
GET /sysLoginLog/getLoginLogList
GET /sysOperationRecord/findSysOperationRecord
GET /sysOperationRecord/getSysOperationRecordList
GET /sysParams/findSysParams
GET /sysParams/getSysParam
GET /sysParams/getSysParamsList
GET /sysVersion/downloadVersionJson
GET /sysVersion/findSysVersion
GET /sysVersion/getSysVersionList
GET /timedTask/alertStream
GET /timedTask/getRegisteredMethods
GET /timedTask/getTimedTaskList
GET /timedTask/getTimedTaskLogList
GET /user/getUserInfo
POST /api/createApi
POST /api/deleteApi
POST /api/enterSyncApi
POST /api/getAllApis
POST /api/getApiById
POST /api/getApiList
POST /api/ignoreApi
POST /api/setApiRoles
POST /api/updateApi
POST /attachmentCategory/addCategory
POST /attachmentCategory/deleteCategory
POST /authority/copyAuthority
POST /authority/createAuthority
POST /authority/deleteAuthority
POST /authority/getAuthorityList
POST /authority/setDataScope
POST /authority/setRoleUsers
POST /authorityBtn/canRemoveAuthorityBtn
POST /authorityBtn/getAuthorityBtn
POST /authorityBtn/setAuthorityBtn
POST /base/captcha
POST /base/login
POST /casbin/getPolicyPathByAuthorityId
POST /casbin/updateCasbin
POST /dataAccessLog/getDataAccessLogList
POST /department/createDepartment
POST /department/getDepartmentList
POST /department/setDepartmentUsers
POST /email/emailTest
POST /email/sendEmail
POST /fileUploadAndDownload/deleteFile
POST /fileUploadAndDownload/deleteFiles
POST /fileUploadAndDownload/editFileName
POST /fileUploadAndDownload/getFileList
POST /fileUploadAndDownload/importURL
POST /fileUploadAndDownload/listOssFiles
POST /fileUploadAndDownload/upload
POST /info/createInfo
POST /init/checkdb
POST /init/initdb
POST /jwt/jsonInBlacklist
POST /mediaUpload/chunk
POST /mediaUpload/complete
POST /mediaUpload/init
POST /menu/addBaseMenu
POST /menu/addMenuAuthority
POST /menu/deleteBaseMenu
POST /menu/getBaseMenuById
POST /menu/getBaseMenuTree
POST /menu/getMenu
POST /menu/getMenuAuthority
POST /menu/getMenuList
POST /menu/setMenuRoles
POST /menu/updateBaseMenu
POST /payment/callback/:provider
POST /payment/config
POST /payment/create
POST /payment/order
POST /payment/query
POST /payment/refund
POST /position/createPosition
POST /position/getPositionList
POST /position/setPositionUsers
POST /securityConfig/setSecurityConfig
POST /sysApiToken/createApiToken
POST /sysApiToken/deleteApiToken
POST /sysApiToken/getApiTokenList
POST /sysDictionary/createSysDictionary
POST /sysDictionary/importSysDictionary
POST /sysDictionaryDetail/createSysDictionaryDetail
POST /sysError/createSysError
POST /sysExportTemplate/createSysExportTemplate
POST /sysExportTemplate/importExcel
POST /sysParams/createSysParams
POST /sysVersion/exportVersion
POST /sysVersion/importVersion
POST /system/getServerInfo
POST /system/getSystemConfig
POST /system/reloadSystem
POST /system/setSystemConfig
POST /timedTask/createTimedTask
POST /timedTask/toggleTimedTask
POST /timedTask/triggerTimedTask
POST /user/admin_register
POST /user/changePassword
POST /user/getUserList
POST /user/resetPassword
POST /user/setUserAuthorities
POST /user/setUserAuthority
POST /user/setUserDepartments
POST /user/setUserPositions
PUT /authority/updateAuthority
PUT /department/updateDepartment
PUT /info/updateInfo
PUT /position/updatePosition
PUT /sysDictionary/updateSysDictionary
PUT /sysDictionaryDetail/updateSysDictionaryDetail
PUT /sysError/updateSysError
PUT /sysExportTemplate/updateSysExportTemplate
PUT /sysParams/updateSysParams
PUT /timedTask/updateTimedTask
PUT /user/setSelfInfo
PUT /user/setSelfSetting
PUT /user/setUserInfo`
+170
View File
@@ -0,0 +1,170 @@
package server
import (
"encoding/json"
"log/slog"
"regexp"
"sort"
"strings"
"sync"
"kra/app/system/routeinfo"
"github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
"github.com/swaggo/swag"
)
const swaggerInstanceName = "kra-admin"
var (
swaggerPathParameter = regexp.MustCompile(`:([A-Za-z0-9_]+)`)
swaggerRegistration sync.Once
swaggerDocument runtimeSwaggerDocument
)
type runtimeSwaggerDocument struct {
mu sync.RWMutex
doc string
}
func (d *runtimeSwaggerDocument) ReadDoc() string {
d.mu.RLock()
defer d.mu.RUnlock()
return d.doc
}
func (d *runtimeSwaggerDocument) replace(doc string) {
d.mu.Lock()
d.doc = doc
d.mu.Unlock()
}
func registerSwagger(engine *gin.Engine, prefix, version string, logger *slog.Logger) {
swaggerRegistration.Do(func() { swag.Register(swaggerInstanceName, &swaggerDocument) })
swaggerDocument.replace(buildSwaggerDocument(engine.Routes(), prefix, version))
path := strings.TrimSuffix(prefix, "/") + "/swagger/*any"
engine.GET(path, ginSwagger.WrapHandler(
swaggerFiles.Handler,
ginSwagger.InstanceName(swaggerInstanceName),
ginSwagger.URL("doc.json"),
ginSwagger.PersistAuthorization(true),
))
if logger != nil {
logger.Info("register swagger handler", "mod", "system", "path", path)
}
}
func buildSwaggerDocument(routes []gin.RouteInfo, prefix, version string) string {
basePath := strings.TrimSuffix(prefix, "/")
if basePath == "" {
basePath = "/"
}
sort.Slice(routes, func(i, j int) bool {
if routes[i].Path == routes[j].Path {
return routes[i].Method < routes[j].Method
}
return routes[i].Path < routes[j].Path
})
paths := make(map[string]map[string]any, len(routes))
for _, route := range routes {
if strings.HasSuffix(route.Path, "/*filepath") {
continue
}
method := strings.ToLower(route.Method)
switch method {
case "get", "post", "put", "delete", "patch":
default:
continue
}
apiPath := route.Path
if prefix != "" {
apiPath = strings.TrimPrefix(apiPath, strings.TrimSuffix(prefix, "/"))
}
if apiPath == "" {
apiPath = "/"
}
documentPath := swaggerPathParameter.ReplaceAllString(apiPath, `{$1}`)
group, description := routeinfo.Metadata(route.Method, apiPath)
if description == "" {
description = route.Method + " " + apiPath
}
operation := map[string]any{
"tags": []string{group},
"summary": description,
"operationId": swaggerOperationID(route.Method, apiPath),
"produces": []string{"application/json"},
"responses": map[string]any{
"200": map[string]any{"description": "OK", "schema": map[string]any{"$ref": "#/definitions/Response"}},
},
}
if parameters := swaggerPathParameters(apiPath); len(parameters) > 0 {
operation["parameters"] = parameters
}
if !swaggerPublicPath(apiPath) {
operation["security"] = []map[string][]string{{"ApiKeyAuth": {}}}
}
if paths[documentPath] == nil {
paths[documentPath] = map[string]any{}
}
paths[documentPath][method] = operation
}
document := map[string]any{
"swagger": "2.0",
"info": map[string]any{"title": "Kra Administration API", "version": version},
"basePath": basePath,
"schemes": []string{"http", "https"},
"consumes": []string{"application/json"},
"produces": []string{"application/json"},
"securityDefinitions": map[string]any{
"ApiKeyAuth": map[string]any{"type": "apiKey", "name": "x-token", "in": "header"},
},
"paths": paths,
"definitions": map[string]any{
"Response": map[string]any{
"type": "object",
"properties": map[string]any{
"code": map[string]any{"type": "integer"},
"data": map[string]any{"type": "object"},
"msg": map[string]any{"type": "string"},
},
},
},
}
raw, _ := json.Marshal(document)
return string(raw)
}
func swaggerOperationID(method, path string) string {
value := strings.ToLower(method) + "_" + strings.Trim(path, "/")
value = swaggerPathParameter.ReplaceAllString(value, "$1")
return strings.Map(func(r rune) rune {
if r >= 'a' && r <= 'z' || r >= '0' && r <= '9' || r == '_' {
return r
}
return '_'
}, value)
}
func swaggerPathParameters(path string) []map[string]any {
matches := swaggerPathParameter.FindAllStringSubmatch(path, -1)
parameters := make([]map[string]any, 0, len(matches))
for _, match := range matches {
parameters = append(parameters, map[string]any{"name": match[1], "in": "path", "required": true, "type": "string"})
}
return parameters
}
func swaggerPublicPath(path string) bool {
for _, marker := range []string{
"/health", "/base/login", "/base/captcha", "/init/checkdb", "/init/initdb",
"/api/freshCasbin", "/sysExportTemplate/exportExcelByToken", "/sysExportTemplate/exportTemplateByToken",
"/sysError/createSysError", "/info/getInfoDataSource", "/info/getInfoPublic",
} {
if path == marker {
return true
}
}
return false
}