优化结构

This commit is contained in:
Yvan
2026-08-20 23:02:06 +08:00
parent 63dbb4cfbc
commit e65cf0b1d0
338 changed files with 622 additions and 481 deletions
-219
View File
@@ -1,219 +0,0 @@
package server
import (
"log/slog"
"mime"
"net/http"
"os"
"path"
"sort"
"strings"
"time"
"kra/internal/conf"
"kra/internal/server/handler"
"kra/internal/server/httpx"
servermiddleware "kra/internal/server/middleware"
serverrouter "kra/internal/server/router"
"kra/internal/service"
"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 {
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") })
serverrouter.RegisterPublic(public, engine, handlers.Public)
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))
serverrouter.RegisterUser(private, handlers.User)
serverrouter.RegisterNavigation(private, handlers.Navigation)
serverrouter.RegisterSession(private, handlers.Session)
serverrouter.RegisterAuthority(private, handlers.Authority)
serverrouter.RegisterMenu(private, handlers.Menu)
serverrouter.RegisterAPI(private, public, engine, handlers.API)
serverrouter.RegisterPermission(private, handlers.Permission)
serverrouter.RegisterOrganization(private, handlers.Organization)
serverrouter.RegisterDictionary(private, handlers.Dictionary)
serverrouter.RegisterParameter(private, handlers.Parameter)
serverrouter.RegisterAPIToken(private, handlers.APIToken)
serverrouter.RegisterSystemConfig(private, handlers.SystemConfig)
serverrouter.RegisterVersion(private, handlers.Version)
serverrouter.RegisterExport(private, public, handlers.Export)
serverrouter.RegisterAudit(private, public, handlers.Audit)
serverrouter.RegisterTask(private, handlers.Task)
serverrouter.RegisterMedia(private, handlers.Media)
serverrouter.RegisterAnnouncement(private, public, handlers.Announcement)
serverrouter.RegisterEmail(private, handlers.Email)
serverrouter.RegisterPayment(private, public, handlers.Payment)
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
@@ -1,402 +0,0 @@
package server
import (
"bytes"
"log/slog"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"sort"
"strings"
"testing"
"kra/internal/conf"
"kra/internal/server/handler"
"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`
-110
View File
@@ -1,110 +0,0 @@
package handler
import (
"time"
"kra/internal/server/httpx"
"kra/internal/service"
"kra/internal/service/dto"
"github.com/gin-gonic/gin"
)
type Announcement struct{ service *service.AnnouncementService }
func NewAnnouncement(service *service.AnnouncementService) *Announcement {
return &Announcement{service: service}
}
func announcementInput(req dto.AnnouncementRequest) service.AnnouncementInput {
return service.AnnouncementInput{ID: req.ID, Title: req.Title, Content: req.Content, UserID: req.UserID, Attachments: req.Attachments}
}
func (h *Announcement) Create(c *gin.Context) {
var req dto.AnnouncementRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if err := h.service.Create(c.Request.Context(), announcementInput(req)); err != nil {
httpx.Fail(c, "创建失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "创建成功")
}
func (h *Announcement) Delete(c *gin.Context) {
if err := h.service.Delete(c.Request.Context(), c.Query("ID")); err != nil {
httpx.Fail(c, "删除失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
}
func (h *Announcement) DeleteByIDs(c *gin.Context) {
if err := h.service.DeleteByIDs(c.Request.Context(), c.QueryArray("IDs[]")); err != nil {
httpx.Fail(c, "批量删除失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "批量删除成功")
}
func (h *Announcement) Update(c *gin.Context) {
var req dto.AnnouncementRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if err := h.service.Update(c.Request.Context(), announcementInput(req)); err != nil {
httpx.Fail(c, "更新失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "更新成功")
}
func (h *Announcement) Find(c *gin.Context) {
item, err := h.service.Find(c.Request.Context(), c.Query("ID"))
if err != nil {
httpx.Fail(c, "查询失败")
return
}
httpx.OKWithData(c, item)
}
func (h *Announcement) List(c *gin.Context) {
var req dto.AnnouncementSearchRequest
if err := c.ShouldBindQuery(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
items, total, err := h.service.List(c.Request.Context(), req.Page, req.PageSize, req.StartCreatedAt, req.EndCreatedAt)
if err != nil {
httpx.Fail(c, "获取失败")
return
}
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功")
}
func (h *Announcement) DataSource(c *gin.Context) {
users, err := h.service.UserOptions(c.Request.Context())
if err != nil {
httpx.Fail(c, "查询失败")
return
}
httpx.OKWithData(c, gin.H{"userID": users})
}
func (h *Announcement) Public(c *gin.Context) {
httpx.Write(c, httpx.CodeSuccess, gin.H{"info": "不需要鉴权的公告接口信息"}, "获取成功")
}
func parseTime(value string) (*time.Time, error) {
if value == "" {
return nil, nil
}
parsed, err := time.Parse(time.RFC3339, value)
if err != nil {
return nil, err
}
return &parsed, nil
}
-256
View File
@@ -1,256 +0,0 @@
package handler
import (
"kra/internal/server/httpx"
"kra/internal/service"
"kra/internal/service/dto"
"github.com/gin-gonic/gin"
)
type API struct{ service *service.APIService }
func NewAPI(service *service.APIService) *API { return &API{service: service} }
func (h *API) List(c *gin.Context) {
var req dto.APIListRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if req.Page == 0 {
httpx.Fail(c, "Page值不能为空")
return
}
if req.PageSize == 0 {
httpx.Fail(c, "PageSize值不能为空")
return
}
items, total, err := h.service.ListAPI(c.Request.Context(), &req)
if err != nil {
httpx.Fail(c, "获取失败")
return
}
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功")
}
func (h *API) All(c *gin.Context) {
items, err := h.service.AllAPI(c.Request.Context(), true)
if err != nil {
httpx.Fail(c, "获取失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{"apis": items}, "获取成功")
}
func (h *API) Create(c *gin.Context) {
var req dto.APIRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if message := validateAPIRequest(&req); message != "" {
httpx.Fail(c, message)
return
}
created, err := h.service.CreateAPIRequest(c.Request.Context(), &req)
if err != nil {
httpx.Fail(c, "创建失败")
return
}
httpx.Write(c, httpx.CodeSuccess, created, "创建成功")
}
func (h *API) Update(c *gin.Context) {
var req dto.APIRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if message := validateAPIRequest(&req); message != "" {
httpx.Fail(c, message)
return
}
if err := h.service.UpdateAPIRequest(c.Request.Context(), &req); err != nil {
httpx.Fail(c, "修改失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "修改成功")
}
func (h *API) Delete(c *gin.Context) {
var req dto.DeleteAPIRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if req.ID == 0 {
httpx.Fail(c, "ID值不能为空")
return
}
if err := h.service.DeleteAPI(c.Request.Context(), req.ID); err != nil {
httpx.Fail(c, "删除失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
}
func (h *API) DeleteByIDs(c *gin.Context) {
var req dto.DeleteAPIsRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if err := h.service.DeleteAPIs(c.Request.Context(), req.IDs); err != nil {
httpx.Fail(c, "删除失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
}
func (h *API) Find(c *gin.Context) {
var req dto.GetAPIRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if req.ID == 0 {
httpx.Fail(c, "ID值不能为空")
return
}
item, err := h.service.FindAPIResponse(c.Request.Context(), req.ID)
if err != nil {
httpx.Fail(c, "获取失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{"api": item}, "获取成功")
}
func (h *API) Groups(c *gin.Context) {
groups, groupAPIMap, err := h.service.Groups(c.Request.Context())
if err != nil {
httpx.Fail(c, "获取失败")
return
}
httpx.OKWithData(c, gin.H{"groups": groups, "apiGroupMap": groupAPIMap})
}
func (h *API) Roles(c *gin.Context) {
path, method := c.Query("path"), c.Query("method")
if path == "" || method == "" {
httpx.Fail(c, "API路径和请求方法不能为空")
return
}
ids, err := h.service.APIRoleIDs(c.Request.Context(), path, method)
if err != nil {
httpx.Fail(c, "获取失败"+err.Error())
return
}
if ids == nil {
ids = []uint{}
}
httpx.Write(c, httpx.CodeSuccess, ids, "获取成功")
}
func (h *API) SetRoles(c *gin.Context) {
var req dto.SetAPIRolesRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if req.Path == "" || req.Method == "" {
httpx.Fail(c, "API路径和请求方法不能为空")
return
}
if err := h.service.SetAPIRoles(c.Request.Context(), req.Path, req.Method, req.AuthorityIDs); err != nil {
httpx.Fail(c, "设置失败"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "设置成功")
}
func (h *API) Sync(engine *gin.Engine) gin.HandlerFunc {
return func(c *gin.Context) {
routes := engine.Routes()
values := make([]dto.APIRequest, 0, len(routes))
for _, route := range routes {
values = append(values, dto.APIRequest{Path: route.Path, Method: route.Method})
}
result, err := h.service.SyncAPIResponses(c.Request.Context(), values)
if err != nil {
httpx.Fail(c, "同步失败")
return
}
httpx.OKWithData(c, result)
}
}
func (h *API) Ignore(c *gin.Context) {
var req dto.IgnoreAPIRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if err := h.service.SetAPIIgnored(c.Request.Context(), req.Path, req.Method, req.Flag); err != nil {
httpx.Fail(c, "忽略失败")
return
}
httpx.OK(c)
}
func (h *API) ApplySync(c *gin.Context) {
var req dto.ApplyAPISyncRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if err := h.service.ApplyAPISyncRequest(c.Request.Context(), &req); err != nil {
httpx.Fail(c, "忽略失败")
return
}
httpx.OK(c)
}
func (h *API) FreshCasbin(c *gin.Context) {
if err := h.service.FreshCasbin(c.Request.Context()); err != nil {
httpx.Fail(c, "刷新失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "刷新成功")
}
func (h *API) SetPolicyPaths(c *gin.Context) {
var req dto.SetPolicyPathsRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if req.AuthorityID == 0 {
httpx.Fail(c, "AuthorityId值不能为空")
return
}
if err := h.service.SetPolicyPathsRequest(c.Request.Context(), &req); err != nil {
httpx.Fail(c, "更新失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "更新成功")
}
func (h *API) PolicyPaths(c *gin.Context) {
var req dto.GetPolicyPathsRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if req.AuthorityID == 0 {
httpx.Fail(c, "AuthorityId值不能为空")
return
}
paths, err := h.service.PolicyPathResponses(c.Request.Context(), req.AuthorityID)
if err != nil {
httpx.Fail(c, "获取失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{"paths": paths}, "获取成功")
}
func validateAPIRequest(req *dto.APIRequest) string {
switch {
case req.Path == "":
return "Path值不能为空"
case req.Description == "":
return "Description值不能为空"
case req.APIGroup == "":
return "ApiGroup值不能为空"
case req.Method == "":
return "Method值不能为空"
default:
return ""
}
}
-58
View File
@@ -1,58 +0,0 @@
package handler
import (
"kra/internal/server/httpx"
"kra/internal/service"
"kra/internal/service/dto"
"github.com/gin-gonic/gin"
)
type APIToken struct {
service *service.TokenService
}
func NewAPIToken(service *service.TokenService) *APIToken {
return &APIToken{service: service}
}
func (h *APIToken) Create(c *gin.Context) {
var req dto.CreateAPITokenRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
token, err := h.service.CreateAPIToken(c.Request.Context(), req.UserID, req.AuthorityID, req.Days, req.Remark)
if err != nil {
httpx.Fail(c, "签发失败: "+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{"token": token}, "签发成功")
}
func (h *APIToken) List(c *gin.Context) {
var req dto.APITokenListRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
items, total, err := h.service.APITokens(c.Request.Context(), req.Page, req.PageSize, req.UserID, req.Status)
if err != nil {
httpx.Fail(c, "获取失败")
return
}
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功")
}
func (h *APIToken) Delete(c *gin.Context) {
var req dto.IDRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if err := h.service.DisableAPIToken(c.Request.Context(), req.ID); err != nil {
httpx.Fail(c, "作废失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "作废成功")
}
-304
View File
@@ -1,304 +0,0 @@
package handler
import (
"errors"
"log/slog"
"strconv"
"time"
"kra/internal/biz"
"kra/internal/server/httpx"
"kra/internal/service"
"kra/internal/service/dto"
"github.com/gin-gonic/gin"
)
type Audit struct {
service *service.AuditService
recorder *service.AuditRecorder
logs *service.LogViewerService
logger *slog.Logger
}
func NewAudit(service *service.AuditService, recorder *service.AuditRecorder, logs *service.LogViewerService, logger *slog.Logger) *Audit {
return &Audit{service: service, recorder: recorder, logs: logs, logger: logger}
}
func (h *Audit) Operations(c *gin.Context) {
var req dto.OperationRecordSearchRequest
if err := c.ShouldBindQuery(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
items, total, err := h.service.OperationsFilter(c.Request.Context(), req.Page, req.PageSize, req.Path, req.Method, req.Status)
if err != nil {
httpx.Fail(c, "获取失败")
return
}
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功")
}
func (h *Audit) Operation(c *gin.Context) {
var req dto.AuditIDQuery
if err := c.ShouldBindQuery(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if req.ID == 0 {
httpx.Fail(c, "ID值不能为空")
return
}
item, err := h.service.Operation(c.Request.Context(), req.ID)
if err != nil {
httpx.Fail(c, "查询失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{"reSysOperationRecord": item}, "查询成功")
}
func (h *Audit) DeleteOperation(c *gin.Context) {
var req dto.IDRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if err := h.service.DeleteOperations(c.Request.Context(), []int{int(req.ID)}); err != nil {
httpx.Fail(c, "删除失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
}
func (h *Audit) DeleteOperations(c *gin.Context) {
var req dto.AuditIDsRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if err := h.service.DeleteOperations(c.Request.Context(), req.IDs); err != nil {
httpx.Fail(c, "批量删除失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "批量删除成功")
}
func (h *Audit) Logins(c *gin.Context) {
var req dto.LoginLogSearchRequest
if err := c.ShouldBindQuery(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
items, total, err := h.service.LoginsFilter(c.Request.Context(), req.Page, req.PageSize, req.Username, req.Status)
if err != nil {
httpx.Fail(c, "获取失败")
return
}
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功")
}
func (h *Audit) Login(c *gin.Context) {
var req dto.AuditIDQuery
if err := c.ShouldBindQuery(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
item, err := h.service.Login(c.Request.Context(), req.ID)
if err != nil {
httpx.Fail(c, "查询失败")
return
}
httpx.Write(c, httpx.CodeSuccess, item, "查询成功")
}
func (h *Audit) DeleteLogin(c *gin.Context) {
var req dto.IDRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if err := h.service.DeleteLogins(c.Request.Context(), []int{int(req.ID)}); err != nil {
httpx.Fail(c, "删除失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
}
func (h *Audit) DeleteLogins(c *gin.Context) {
var req dto.AuditIDsRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if err := h.service.DeleteLogins(c.Request.Context(), req.IDs); err != nil {
httpx.Fail(c, "批量删除失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "批量删除成功")
}
func (h *Audit) DataAccess(c *gin.Context) {
var req dto.DataAccessListRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
items, total, err := h.service.DataAccessRequest(c.Request.Context(), &req)
if err != nil {
httpx.Fail(c, "获取失败:"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功")
}
func (h *Audit) DeleteDataAccess(c *gin.Context) {
var req dto.IDsRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if err := h.service.DeleteDataAccess(c.Request.Context(), req.IDs); err != nil {
httpx.Fail(c, "删除失败:"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
}
func (h *Audit) LogDates(c *gin.Context) {
month := c.Query("month")
// Month uses `binding:"required"`; an omitted or empty query is a
// generic parameter error, not the service's format-specific error.
if month == "" {
httpx.Fail(c, "参数错误")
return
}
data, err := h.logs.LogDates(c.Request.Context(), month)
if err != nil {
failLogViewer(c, err, h.logger)
return
}
httpx.Write(c, httpx.CodeSuccess, data, "获取成功")
}
func (h *Audit) LogFiles(c *gin.Context) {
date := c.Query("date")
if date == "" {
httpx.Fail(c, "参数错误")
return
}
data, err := h.logs.LogFiles(c.Request.Context(), date)
if err != nil {
failLogViewer(c, err, h.logger)
return
}
httpx.Write(c, httpx.CodeSuccess, data, "获取成功")
}
func (h *Audit) LogContent(c *gin.Context) {
date, path := c.Query("date"), c.Query("path")
if date == "" || path == "" {
httpx.Fail(c, "参数错误")
return
}
var cursor *int64
if raw := c.Query("cursor"); raw != "" {
value, err := strconv.ParseInt(raw, 10, 64)
if err != nil {
httpx.Fail(c, "参数错误")
return
}
cursor = &value
}
// Preserve negative cursors for the usecase to classify with the compatible
// "日志文件路径不合法" error rather than rejecting them as binding errors.
data, err := h.logs.LogContent(c.Request.Context(), date, path, cursor)
if err != nil {
failLogViewer(c, err, h.logger)
return
}
httpx.Write(c, httpx.CodeSuccess, data, "获取成功")
}
func failLogViewer(c *gin.Context, err error, logger *slog.Logger) {
if logger != nil {
logger.ErrorContext(c.Request.Context(), "日志查看失败", "mod", "log-viewer", "error", err)
}
message := "读取日志失败"
switch {
case errors.Is(err, biz.ErrInvalidLogMonth):
message = "日志月份格式不正确"
case errors.Is(err, biz.ErrInvalidLogDate):
message = "日志日期格式不正确"
case errors.Is(err, biz.ErrInvalidLogPath):
message = "日志文件路径不合法"
case errors.Is(err, biz.ErrLogFileNotFound):
message = "日志文件不存在"
case errors.Is(err, biz.ErrLogFileUnreadable):
message = "日志文件不可读取"
case errors.Is(err, biz.ErrLogRootUnavailable):
message = "日志目录不可读取"
}
httpx.Fail(c, message)
}
func (h *Audit) DeleteError(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
if err := h.service.DeleteErrors(c.Request.Context(), []uint{uint(id)}); err != nil {
httpx.Fail(c, "删除失败:"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
}
func (h *Audit) DeleteErrors(c *gin.Context) {
if err := h.service.DeleteErrors(c.Request.Context(), IDsFromQuery(c)); err != nil {
httpx.Fail(c, "批量删除失败:"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "批量删除成功")
}
func (h *Audit) UpdateError(c *gin.Context) {
var req dto.ErrorRecordMutationRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if err := h.service.UpdateErrorRequest(c.Request.Context(), &req); err != nil {
httpx.Fail(c, "更新失败:"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "更新成功")
}
func (h *Audit) Error(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
item, err := h.service.Error(c.Request.Context(), uint(id))
if err != nil {
httpx.Fail(c, "查询失败:"+err.Error())
return
}
httpx.OKWithData(c, item)
}
func (h *Audit) Errors(c *gin.Context) {
p, size, err := page(c)
if err != nil {
httpx.Fail(c, err.Error())
return
}
createdAtRange := make([]time.Time, 0, 2)
for _, raw := range c.QueryArray("createdAtRange[]") {
value, parseErr := time.Parse(time.RFC3339, raw)
if parseErr != nil {
httpx.Fail(c, parseErr.Error())
return
}
createdAtRange = append(createdAtRange, value)
}
items, total, err := h.service.ErrorsFilter(c.Request.Context(), p, size, c.Query("form"), c.Query("info"), createdAtRange)
if err != nil {
httpx.Fail(c, "获取失败:"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: p, PageSize: size}, "获取成功")
}
func (h *Audit) CreateError(c *gin.Context) {
var req dto.ErrorRecordMutationRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if err := h.recorder.CreateErrorMutationRequest(c.Request.Context(), &req); err != nil {
httpx.Fail(c, "创建失败:"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "创建成功")
}
-169
View File
@@ -1,169 +0,0 @@
package handler
import (
"kra/internal/server/httpx"
"kra/internal/service"
"kra/internal/service/dto"
"github.com/gin-gonic/gin"
)
type Authority struct{ service *service.AuthorityService }
func NewAuthority(service *service.AuthorityService) *Authority { return &Authority{service: service} }
func (h *Authority) List(c *gin.Context) {
items, err := h.service.Authorities(c.Request.Context())
if err != nil {
httpx.Fail(c, "获取失败"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, items, "获取成功")
}
func (h *Authority) Create(c *gin.Context) {
var req dto.AuthorityRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if req.AuthorityID == 0 {
httpx.Fail(c, "AuthorityId值不能为空")
return
}
if req.AuthorityName == "" {
httpx.Fail(c, "AuthorityName值不能为空")
return
}
value, err := h.service.CreateAuthorityRequest(c.Request.Context(), &req)
if err != nil {
httpx.Fail(c, "创建失败"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{"authority": value}, "创建成功")
}
func (h *Authority) Copy(c *gin.Context) {
var req dto.CopyAuthorityRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if req.OldAuthorityID == 0 {
httpx.Fail(c, "OldAuthorityId值不能为空")
return
}
if req.Authority.AuthorityID == 0 {
httpx.Fail(c, "AuthorityId值不能为空")
return
}
if req.Authority.AuthorityName == "" {
httpx.Fail(c, "AuthorityName值不能为空")
return
}
value, err := h.service.CopyAuthorityRequest(c.Request.Context(), &req)
if err != nil {
httpx.Fail(c, "拷贝失败"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{"authority": value}, "拷贝成功")
}
func (h *Authority) Update(c *gin.Context) {
var req dto.AuthorityRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if req.AuthorityID == 0 {
httpx.Fail(c, "AuthorityId值不能为空")
return
}
if req.AuthorityName == "" {
httpx.Fail(c, "AuthorityName值不能为空")
return
}
value, err := h.service.UpdateAuthorityRequest(c.Request.Context(), &req)
if err != nil {
httpx.Fail(c, "更新失败"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{"authority": value}, "更新成功")
}
func (h *Authority) Delete(c *gin.Context) {
var req dto.DeleteAuthorityRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if req.AuthorityID == 0 {
httpx.Fail(c, "AuthorityId值不能为空")
return
}
if err := h.service.DeleteAuthority(c.Request.Context(), req.AuthorityID); err != nil {
httpx.Fail(c, "删除失败"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
}
func (h *Authority) SetUsers(c *gin.Context) {
var req dto.SetRoleUsersRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if req.AuthorityID == 0 {
httpx.Fail(c, "角色ID不能为空")
return
}
if err := h.service.SetAuthorityUsers(c.Request.Context(), req.AuthorityID, req.UserIDs); err != nil {
httpx.Fail(c, "设置失败"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "设置成功")
}
func (h *Authority) Users(c *gin.Context) {
var req dto.SetRoleUsersRequest
if err := c.ShouldBindQuery(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
ids, err := h.service.AuthorityUserIDs(c.Request.Context(), req.AuthorityID)
if err != nil {
httpx.Fail(c, "获取失败"+err.Error())
return
}
if ids == nil {
ids = []uint{}
}
httpx.Write(c, httpx.CodeSuccess, ids, "获取成功")
}
func (h *Authority) SetDataScope(c *gin.Context) {
var req dto.SetDataScopeRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if req.AuthorityID == 0 {
httpx.Fail(c, "角色ID不能为空")
return
}
if err := h.service.SetDataScope(c.Request.Context(), req.AuthorityID, req.DataScope, req.DepartmentIDs); err != nil {
httpx.Fail(c, "设置失败"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "设置成功")
}
func (h *Authority) DataScopeDepartments(c *gin.Context) {
var req dto.SetDataScopeRequest
if err := c.ShouldBindQuery(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
ids, err := h.service.DataScopeDepartmentIDs(c.Request.Context(), req.AuthorityID)
if err != nil {
httpx.Fail(c, "获取失败"+err.Error())
return
}
if ids == nil {
ids = []uint{}
}
httpx.Write(c, httpx.CodeSuccess, ids, "获取成功")
}
-266
View File
@@ -1,266 +0,0 @@
package handler
import (
"strconv"
"kra/internal/server/httpx"
"kra/internal/service"
"kra/internal/service/dto"
"github.com/gin-gonic/gin"
)
type Dictionary struct{ service *service.DictionaryService }
func NewDictionary(service *service.DictionaryService) *Dictionary {
return &Dictionary{service: service}
}
func (h *Dictionary) Create(c *gin.Context) {
var req dto.DictionaryRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
created, err := h.service.CreateDictionaryRequest(c.Request.Context(), &req)
if err != nil {
httpx.Fail(c, "创建失败")
return
}
httpx.Write(c, httpx.CodeSuccess, created, "创建成功")
}
func (h *Dictionary) Update(c *gin.Context) {
var req dto.DictionaryRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if err := h.service.UpdateDictionaryRequest(c.Request.Context(), &req); err != nil {
httpx.Fail(c, "更新失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "更新成功")
}
func (h *Dictionary) Delete(c *gin.Context) {
var req dto.DictionaryRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if err := h.service.DeleteDictionary(c.Request.Context(), req.ID); err != nil {
httpx.Fail(c, "删除失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
}
func (h *Dictionary) Find(c *gin.Context) {
var req dto.DictionaryRequest
if err := c.ShouldBindQuery(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
item, err := h.service.Dictionary(c.Request.Context(), req.ID, req.Type, req.Status, true)
if err != nil {
httpx.Fail(c, "字典未创建或未开启")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{"resysDictionary": item}, "查询成功")
}
func (h *Dictionary) List(details bool) gin.HandlerFunc {
return func(c *gin.Context) {
items, _, err := h.service.Dictionaries(c.Request.Context(), 0, 0, c.Query("name"), "", details)
if err != nil {
httpx.Fail(c, "获取失败")
return
}
httpx.Write(c, httpx.CodeSuccess, items, "获取成功")
}
}
func (h *Dictionary) Export(c *gin.Context) {
var req dto.DictionaryRequest
if err := c.ShouldBindQuery(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if req.ID == 0 {
httpx.Fail(c, "字典ID不能为空")
return
}
item, err := h.service.ExportDictionary(c.Request.Context(), req.ID)
if err != nil {
httpx.Fail(c, "导出失败")
return
}
httpx.Write(c, httpx.CodeSuccess, item, "导出成功")
}
func (h *Dictionary) Import(c *gin.Context) {
var req dto.ImportDictionaryRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if err := h.service.ImportDictionaryJSON(c.Request.Context(), req.JSON); err != nil {
httpx.Fail(c, "导入失败: "+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "导入成功")
}
func (h *Dictionary) CreateDetail(c *gin.Context) {
var req dto.DictionaryDetailRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if err := h.service.CreateDictionaryDetailRequest(c.Request.Context(), &req); err != nil {
httpx.Fail(c, "创建失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "创建成功")
}
func (h *Dictionary) UpdateDetail(c *gin.Context) {
var req dto.DictionaryDetailRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if err := h.service.UpdateDictionaryDetailRequest(c.Request.Context(), &req); err != nil {
httpx.Fail(c, "更新失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "更新成功")
}
func (h *Dictionary) DeleteDetail(c *gin.Context) {
var req dto.DictionaryDetailRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if err := h.service.DeleteDictionaryDetail(c.Request.Context(), req.ID); err != nil {
httpx.Fail(c, "删除失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
}
func (h *Dictionary) FindDetail(c *gin.Context) {
var req dto.DictionaryDetailRequest
if err := c.ShouldBindQuery(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if req.ID == 0 {
httpx.Fail(c, "ID值不能为空")
return
}
item, err := h.service.DictionaryDetail(c.Request.Context(), req.ID)
if err != nil {
httpx.Fail(c, "查询失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{"reSysDictionaryDetail": item}, "查询成功")
}
func (h *Dictionary) Details(c *gin.Context) {
p, size, err := page(c)
if err != nil {
httpx.Fail(c, err.Error())
return
}
var dictionaryID uint64
if raw, exists := c.GetQuery("sysDictionaryID"); exists && raw != "" {
var parseErr error
dictionaryID, parseErr = strconv.ParseUint(raw, 10, 0)
if parseErr != nil {
httpx.Fail(c, parseErr.Error())
return
}
}
filter := dto.DictionaryDetailListRequest{DictionaryID: uint(dictionaryID), Label: c.Query("label"), Value: c.Query("value")}
if raw, exists := c.GetQuery("status"); exists {
value, parseErr := strconv.ParseBool(raw)
if parseErr != nil {
httpx.Fail(c, parseErr.Error())
return
}
filter.Status = &value
}
if raw, exists := c.GetQuery("parentID"); exists {
value, parseErr := strconv.ParseUint(raw, 10, 64)
if parseErr != nil {
httpx.Fail(c, parseErr.Error())
return
}
parentID := uint(value)
filter.ParentID = &parentID
}
if raw, exists := c.GetQuery("level"); exists {
value, parseErr := strconv.Atoi(raw)
if parseErr != nil {
httpx.Fail(c, parseErr.Error())
return
}
filter.Level = &value
}
items, total, err := h.service.DictionaryDetailsRequest(c.Request.Context(), p, size, filter)
if err != nil {
httpx.Fail(c, "获取失败")
return
}
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: p, PageSize: size}, "获取成功")
}
func (h *Dictionary) Tree(byType bool) gin.HandlerFunc {
return func(c *gin.Context) {
rawID := c.Query("sysDictionaryID")
id, parseErr := strconv.ParseUint(rawID, 10, 32)
typ := ""
if byType {
typ = c.Query("type")
if typ == "" {
httpx.Fail(c, "字典类型不能为空")
return
}
} else if rawID == "" {
httpx.Fail(c, "字典ID不能为空")
return
} else if parseErr != nil {
httpx.Fail(c, "字典ID格式错误")
return
}
items, err := h.service.DictionaryTree(c.Request.Context(), uint(id), typ)
if err != nil {
httpx.Fail(c, "获取失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{"list": items}, "获取成功")
}
}
func (h *Dictionary) DetailsByParent(c *gin.Context) {
var req dto.DictionaryDetailsByParentRequest
if err := c.ShouldBindQuery(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
items, err := h.service.DictionaryDetailsByParent(c.Request.Context(), req.DictionaryID, req.ParentID, req.IncludeChildren)
if err != nil {
httpx.Fail(c, "获取失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{"list": items}, "获取成功")
}
func (h *Dictionary) Path(c *gin.Context) {
rawID := c.Query("id")
if rawID == "" {
httpx.Fail(c, "字典详情ID不能为空")
return
}
id, parseErr := strconv.ParseUint(rawID, 10, 32)
if parseErr != nil {
httpx.Fail(c, "字典详情ID格式错误")
return
}
path, err := h.service.DictionaryPath(c.Request.Context(), uint(id))
if err != nil {
httpx.Fail(c, "获取失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{"path": path}, "获取成功")
}
-34
View File
@@ -1,34 +0,0 @@
package handler
import (
"kra/internal/server/httpx"
"kra/internal/service"
"kra/internal/service/dto"
"github.com/gin-gonic/gin"
)
type Email struct{ service *service.EmailService }
func NewEmail(service *service.EmailService) *Email { return &Email{service: service} }
func (h *Email) Test(c *gin.Context) {
if err := h.service.Test(c.Request.Context()); err != nil {
httpx.Fail(c, "发送失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "发送成功")
}
func (h *Email) Send(c *gin.Context) {
var req dto.SendEmailRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if err := h.service.Send(c.Request.Context(), req.To, req.Subject, req.Body); err != nil {
httpx.Fail(c, "发送失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "发送成功")
}
-244
View File
@@ -1,244 +0,0 @@
package handler
import (
"errors"
"net/http"
"net/url"
"strings"
"kra/internal/server/httpx"
"kra/internal/service"
"kra/internal/service/dto"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type Export struct {
service *service.ExportService
}
func NewExport(service *service.ExportService) *Export {
return &Export{service: service}
}
func exportParams(values url.Values) (map[string]string, error) {
out := map[string]string{}
nested, err := url.ParseQuery(values.Get("params"))
if err != nil {
return nil, err
}
for key, items := range nested {
if len(items) > 0 {
out[key] = items[0]
}
}
return out, nil
}
func exportIssueParams(values url.Values, blank bool) (map[string]string, error) {
params, err := exportParams(values)
if err != nil && blank {
return map[string]string{}, nil
}
return params, err
}
func (h *Export) Create(c *gin.Context) {
var req dto.ExportTemplateRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if req.Name == "" {
httpx.Fail(c, "Name值不能为空")
return
}
if err := h.service.CreateRequest(c.Request.Context(), &req); err != nil {
httpx.Fail(c, "创建失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "创建成功")
}
func (h *Export) Update(c *gin.Context) {
var req dto.ExportTemplateRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if req.Name == "" {
httpx.Fail(c, "Name值不能为空")
return
}
if err := h.service.UpdateRequest(c.Request.Context(), &req); err != nil {
httpx.Fail(c, "更新失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "更新成功")
}
func (h *Export) Delete(c *gin.Context) {
var req dto.IDRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
// Deleting an empty model is rejected by GORM's global-update guard in the
// reference endpoint. Preserve the same externally visible failure.
if req.ID == 0 {
httpx.Fail(c, "删除失败")
return
}
if err := h.service.Delete(c.Request.Context(), []uint{req.ID}); err != nil {
httpx.Fail(c, "删除失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
}
func (h *Export) DeleteMany(c *gin.Context) {
var req dto.IDsRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if err := h.service.Delete(c.Request.Context(), req.IDs); err != nil {
httpx.Fail(c, "批量删除失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "批量删除成功")
}
func (h *Export) Find(c *gin.Context) {
var req dto.ExportTemplateRequest
if err := c.ShouldBindQuery(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
item, err := h.service.Template(c.Request.Context(), req.ID, "")
if err != nil {
httpx.Fail(c, "查询失败")
return
}
httpx.OKWithData(c, gin.H{"resysExportTemplate": item})
}
func (h *Export) List(c *gin.Context) {
p, size, err := page(c)
if err != nil {
httpx.Fail(c, err.Error())
return
}
start, startErr := parseTime(c.Query("startCreatedAt"))
end, endErr := parseTime(c.Query("endCreatedAt"))
if startErr != nil || endErr != nil {
httpx.Fail(c, "参数错误")
return
}
items, total, err := h.service.TemplatesFilter(c.Request.Context(), p, size, c.Query("name"), c.Query("tableName"), c.Query("templateID"), start, end)
if err != nil {
httpx.Fail(c, "获取失败")
return
}
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: p, PageSize: size}, "获取成功")
}
func (h *Export) Preview(c *gin.Context) {
templateID := c.Query("templateID")
if templateID == "" {
httpx.Fail(c, "模板ID不能为空")
return
}
params, _ := exportParams(c.Request.URL.Query())
sql, err := h.service.Preview(c.Request.Context(), templateID, params)
if err != nil {
httpx.Fail(c, "获取失败")
return
}
httpx.OKWithData(c, gin.H{"sql": sql})
}
func (h *Export) Issue(blank bool) gin.HandlerFunc {
return func(c *gin.Context) {
templateID := c.Query("templateID")
if templateID == "" {
httpx.Fail(c, "模板ID不能为空")
return
}
params, err := exportIssueParams(c.Request.URL.Query(), blank)
if err != nil {
httpx.Fail(c, "解析 params 参数失败")
return
}
token, err := h.service.IssueToken(c.Request.Context(), templateID, params, blank)
if err != nil {
httpx.Fail(c, "导出令牌创建失败")
return
}
path := "/sysExportTemplate/exportExcelByToken?token=" + token
if blank {
path = "/sysExportTemplate/exportTemplateByToken?token=" + token
}
httpx.OKWithData(c, path)
}
}
func (h *Export) Import(c *gin.Context) {
templateID := c.Query("templateID")
if templateID == "" {
httpx.Fail(c, "模板ID不能为空")
return
}
file, err := c.FormFile("file")
if err != nil {
httpx.Fail(c, "文件获取失败")
return
}
opened, err := file.Open()
if err != nil {
httpx.Fail(c, "读取文件失败")
return
}
defer opened.Close()
if err = h.service.Import(c.Request.Context(), templateID, opened); err != nil {
httpx.Fail(c, err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "导入成功")
}
func (h *Export) Download(expectBlank bool) gin.HandlerFunc {
return func(c *gin.Context) {
token := c.Query("token")
if token == "" {
httpx.Fail(c, "导出token不能为空")
return
}
value, err := h.service.ConsumeToken(c.Request.Context(), token, expectBlank)
if err != nil {
switch {
case errors.Is(err, service.ErrExportTokenMalformed):
httpx.Fail(c, "解析导出参数失败")
case errors.Is(err, service.ErrExportTokenType):
httpx.Fail(c, "token类型错误")
default:
httpx.Fail(c, "导出token无效或已过期")
}
return
}
var data []byte
var name string
if expectBlank {
data, name, err = h.service.ExportBlankTemplate(c.Request.Context(), value.TemplateID)
} else {
data, name, err = h.service.Export(c.Request.Context(), value.TemplateID, value.Params)
}
if err != nil {
httpx.Fail(c, "获取失败")
return
}
if expectBlank {
name = strings.TrimSuffix(name, "_template.xlsx") + "模板.xlsx"
} else {
name = strings.TrimSuffix(name, ".xlsx") + strings.ReplaceAll(uuid.NewString(), "-", "")[:6] + ".xlsx"
}
c.Header("Content-Disposition", "attachment; filename="+name)
c.Header("success", "true")
c.Data(http.StatusOK, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", data)
}
}
-40
View File
@@ -1,40 +0,0 @@
package handler
import (
"net/url"
"testing"
)
func TestExportParamsRejectsMalformedNestedQuery(t *testing.T) {
_, err := exportParams(url.Values{"params": {"name=%zz"}})
if err == nil {
t.Fatal("exportParams accepted malformed nested query")
}
}
func TestExportParamsUsesFirstValue(t *testing.T) {
got, err := exportParams(url.Values{"params": {"name=first&name=second"}})
if err != nil {
t.Fatalf("exportParams returned error: %v", err)
}
if got["name"] != "first" {
t.Fatalf("exportParams name = %q, want first", got["name"])
}
}
func TestExportIssueParamsKeepsBlankTemplateCompatible(t *testing.T) {
got, err := exportIssueParams(url.Values{"params": {"name=%zz"}}, true)
if err != nil {
t.Fatalf("blank template params returned error: %v", err)
}
if len(got) != 0 {
t.Fatalf("blank template params = %#v, want empty", got)
}
}
func TestExportIssueParamsRejectsMalformedExcelParams(t *testing.T) {
_, err := exportIssueParams(url.Values{"params": {"name=%zz"}}, false)
if err == nil {
t.Fatal("Excel export accepted malformed nested query")
}
}
-292
View File
@@ -1,292 +0,0 @@
package handler
import (
"errors"
"net/http"
"strconv"
"kra/internal/server/httpx"
servermiddleware "kra/internal/server/middleware"
"kra/internal/service"
"kra/internal/service/dto"
"github.com/gin-gonic/gin"
)
type Media struct{ service *service.MediaService }
func NewMedia(service *service.MediaService) *Media { return &Media{service: service} }
func (h *Media) limitMultipartBody(c *gin.Context) int64 {
limit := h.service.MediaConfig().EffectiveMaxFileSize()
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, limit+(1<<20))
return limit
}
func rejectMediaTooLarge(c *gin.Context, err error, message string) bool {
var tooLarge *http.MaxBytesError
if !errors.As(err, &tooLarge) {
return false
}
c.AbortWithStatusJSON(http.StatusRequestEntityTooLarge, httpx.Response{Code: httpx.CodeError, Data: gin.H{}, Msg: message})
return true
}
func (h *Media) Upload(c *gin.Context) {
claims := servermiddleware.Claims(c)
if claims == nil {
httpx.Fail(c, "接收文件失败")
return
}
limit := h.limitMultipartBody(c)
header, err := c.FormFile("file")
if err != nil {
if rejectMediaTooLarge(c, err, "文件超过大小上限") {
return
}
httpx.Fail(c, "接收文件失败")
return
}
if header.Size > limit {
c.AbortWithStatusJSON(http.StatusRequestEntityTooLarge, httpx.Response{Code: httpx.CodeError, Data: gin.H{}, Msg: "文件超过大小上限"})
return
}
opened, err := header.Open()
if err != nil {
// Let the upload service surface this as the generic upload
// failure; keep the endpoint message consistent.
httpx.Fail(c, "上传文件失败")
return
}
defer opened.Close()
category, _ := strconv.Atoi(c.DefaultPostForm("classId", "0"))
save := c.DefaultQuery("noSave", "0") == "0"
item, err := h.service.Upload(c.Request.Context(), claims.ID, header.Filename, header.Header.Get("Content-Type"), category, opened, save)
if err != nil {
httpx.Fail(c, "上传文件失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{"file": item}, "上传成功")
}
func (h *Media) List(c *gin.Context) {
var req dto.MediaListRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
items, total, err := h.service.MediaListRequest(c.Request.Context(), &req)
if err != nil {
httpx.Fail(c, "获取失败")
return
}
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功")
}
func (h *Media) Delete(c *gin.Context) {
var req dto.IDRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if err := h.service.Delete(c.Request.Context(), req.ID); err != nil {
httpx.Fail(c, "删除失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
}
func (h *Media) DeleteMany(c *gin.Context) {
var req dto.MediaBatchDeleteRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
failed := []uint{}
for _, id := range req.IDs {
if h.service.Delete(c.Request.Context(), id) != nil {
failed = append(failed, id)
}
}
if len(failed) > 0 {
httpx.Write(c, httpx.CodeSuccess, gin.H{"failedIds": failed}, "部分文件删除失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "批量删除成功")
}
func (h *Media) Find(c *gin.Context) {
id, parseErr := strconv.ParseUint(c.Query("id"), 10, 64)
if parseErr != nil {
httpx.Fail(c, "文件ID非法")
return
}
item, err := h.service.Media(c.Request.Context(), uint(id))
if err != nil {
httpx.Fail(c, "查询失败")
return
}
httpx.OKWithData(c, item)
}
func (h *Media) Rename(c *gin.Context) {
var req dto.RenameMediaRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if err := h.service.Rename(c.Request.Context(), req.ID, req.Name); err != nil {
httpx.Fail(c, "编辑失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "编辑成功")
}
func (h *Media) ImportURLs(c *gin.Context) {
var req []dto.ImportMediaRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if err := h.service.ImportURLRequests(c.Request.Context(), req); err != nil {
httpx.Fail(c, "导入URL失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "导入URL成功")
}
func (h *Media) Storage(c *gin.Context) {
var req dto.StorageListRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
items, next, more, err := h.service.Storage(c.Request.Context(), req.Prefix, req.Cursor, req.Limit)
if err != nil {
httpx.Fail(c, "列举存储桶文件失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{"list": items, "nextCursor": next, "hasMore": more}, "获取成功")
}
func (h *Media) Categories(c *gin.Context) {
items, err := h.service.Categories(c.Request.Context())
if err != nil {
httpx.Fail(c, "获取分类列表失败")
return
}
httpx.OKWithData(c, items)
}
func (h *Media) SaveCategory(c *gin.Context) {
var req dto.CategoryRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, "参数错误")
return
}
if err := h.service.SaveCategoryRequest(c.Request.Context(), &req); err != nil {
httpx.Fail(c, "创建/更新失败:"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "创建/更新成功")
}
func (h *Media) DeleteCategory(c *gin.Context) {
var req dto.IDRequest
if c.ShouldBindJSON(&req) != nil || req.ID == 0 {
httpx.Fail(c, "参数错误")
return
}
if err := h.service.DeleteCategory(c.Request.Context(), req.ID); err != nil {
httpx.Fail(c, "删除失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
}
func (h *Media) InitUpload(c *gin.Context) {
claims := servermiddleware.Claims(c)
var req dto.InitUploadRequest
if claims == nil {
httpx.Fail(c, "参数错误")
return
}
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if config := h.service.MediaConfig(); config.MaxFileSize > 0 && req.FileSize > config.MaxFileSize {
httpx.Fail(c, "文件超过大小上限")
return
}
result, err := h.service.InitUpload(c.Request.Context(), claims.ID, req.FileName, req.FileHash, req.FileSize, req.ChunkSize, req.ChunkTotal)
if err != nil {
httpx.Fail(c, err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, result, "成功")
}
func (h *Media) SaveChunk(c *gin.Context) {
claims := servermiddleware.Claims(c)
if claims == nil {
httpx.Fail(c, "接收分片失败")
return
}
limit := h.limitMultipartBody(c)
header, err := c.FormFile("chunk")
if err != nil {
if rejectMediaTooLarge(c, err, "分片超过大小上限") {
return
}
httpx.Fail(c, "接收分片失败")
return
}
if header.Size > limit {
c.AbortWithStatusJSON(http.StatusRequestEntityTooLarge, httpx.Response{Code: httpx.CodeError, Data: gin.H{}, Msg: "分片超过大小上限"})
return
}
uploadID, parseErr := strconv.ParseUint(c.PostForm("uploadId"), 10, 64)
if parseErr != nil || uploadID == 0 {
httpx.Fail(c, "上传会话 ID 非法")
return
}
index, parseErr := strconv.Atoi(c.PostForm("chunkIndex"))
if parseErr != nil || index < 0 {
httpx.Fail(c, "分片序号非法")
return
}
opened, err := header.Open()
if err != nil {
httpx.Fail(c, "分片读取失败")
return
}
defer opened.Close()
if err = h.service.SaveChunk(c.Request.Context(), claims.ID, uint(uploadID), index, c.PostForm("chunkHash"), opened); err != nil {
httpx.Fail(c, err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "ok")
}
func (h *Media) CompleteUpload(c *gin.Context) {
claims := servermiddleware.Claims(c)
var req dto.CompleteUploadRequest
if claims == nil {
httpx.Fail(c, "参数错误")
return
}
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
item, err := h.service.CompleteUpload(c.Request.Context(), claims.ID, req.UploadID)
if err != nil {
httpx.Fail(c, err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{"media": item}, "成功")
}
func (h *Media) CancelUpload(c *gin.Context) {
claims := servermiddleware.Claims(c)
id, _ := strconv.ParseUint(c.Param("uploadId"), 10, 64)
if claims == nil {
httpx.Fail(c, "未登录")
return
}
if err := h.service.CancelUpload(c.Request.Context(), claims.ID, uint(id)); err != nil {
httpx.Fail(c, err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "已取消")
}
-213
View File
@@ -1,213 +0,0 @@
package handler
import (
"kra/internal/server/httpx"
"kra/internal/service"
"kra/internal/service/dto"
"github.com/gin-gonic/gin"
)
type Menu struct{ service *service.MenuService }
func NewMenu(service *service.MenuService) *Menu { return &Menu{service: service} }
func (h *Menu) List(c *gin.Context) {
items, err := h.service.Tree(c.Request.Context())
if err != nil {
httpx.Fail(c, "获取失败")
return
}
httpx.Write(c, httpx.CodeSuccess, items, "获取成功")
}
func (h *Menu) Tree(c *gin.Context) {
items, err := h.service.Tree(c.Request.Context())
if err != nil {
httpx.Fail(c, "获取失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{"menus": items}, "获取成功")
}
func (h *Menu) Create(c *gin.Context) {
var req dto.MenuRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if req.Path == "" {
httpx.Fail(c, "Path值不能为空")
return
}
if req.Name == "" {
httpx.Fail(c, "Name值不能为空")
return
}
if req.Component == "" {
httpx.Fail(c, "Component值不能为空")
return
}
if req.Sort < 0 {
httpx.Fail(c, "Sort长度或值不在合法范围,ge=0")
return
}
if req.Meta.Title == "" {
httpx.Fail(c, "Title值不能为空")
return
}
if err := h.service.Create(c.Request.Context(), &req); err != nil {
httpx.Fail(c, "添加失败:"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "添加成功")
}
func (h *Menu) Update(c *gin.Context) {
var req dto.MenuRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if req.Path == "" {
httpx.Fail(c, "Path值不能为空")
return
}
if req.Name == "" {
httpx.Fail(c, "Name值不能为空")
return
}
if req.Component == "" {
httpx.Fail(c, "Component值不能为空")
return
}
if req.Sort < 0 {
httpx.Fail(c, "Sort长度或值不在合法范围,ge=0")
return
}
if req.Meta.Title == "" {
httpx.Fail(c, "Title值不能为空")
return
}
if err := h.service.Update(c.Request.Context(), &req); err != nil {
httpx.Fail(c, "更新失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "更新成功")
}
func (h *Menu) Delete(c *gin.Context) {
var req dto.DeleteMenuRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if req.ID == 0 {
httpx.Fail(c, "ID值不能为空")
return
}
if err := h.service.Delete(c.Request.Context(), uint(req.ID)); err != nil {
httpx.Fail(c, "删除失败:"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
}
func (h *Menu) Find(c *gin.Context) {
var req dto.GetMenuRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if req.ID == 0 {
httpx.Fail(c, "ID值不能为空")
return
}
item, err := h.service.Find(c.Request.Context(), uint(req.ID))
if err != nil {
httpx.Fail(c, "获取失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{"menu": item}, "获取成功")
}
func (h *Menu) SetAuthorityMenus(c *gin.Context) {
var req dto.SetAuthorityMenusRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if req.AuthorityID == 0 {
httpx.Fail(c, "AuthorityId值不能为空")
return
}
if err := h.service.SetAuthorityMenus(c.Request.Context(), &req); err != nil {
httpx.Fail(c, "添加失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "添加成功")
}
func (h *Menu) AuthorityMenus(c *gin.Context) {
var req dto.GetAuthorityMenusRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if req.AuthorityID == 0 {
httpx.Fail(c, "AuthorityId值不能为空")
return
}
menus, err := h.service.AuthorityMenus(c.Request.Context(), req.AuthorityID)
if err != nil {
httpx.Write(c, httpx.CodeError, gin.H{"menus": menus}, "获取失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{"menus": menus}, "获取成功")
}
func (h *Menu) RoleIDs(c *gin.Context) {
var req dto.SetMenuRolesRequest
if err := c.ShouldBindQuery(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if req.MenuID == 0 {
httpx.Fail(c, "菜单ID不能为空")
return
}
ids, err := h.service.RoleIDs(c.Request.Context(), req.MenuID)
if err != nil {
httpx.Fail(c, "获取失败"+err.Error())
return
}
if ids == nil {
ids = []uint{}
}
defaultIDs, err := h.service.DefaultRouterRoleIDs(c.Request.Context(), req.MenuID)
if err != nil {
httpx.Fail(c, "获取失败"+err.Error())
return
}
if defaultIDs == nil {
defaultIDs = []uint{}
}
httpx.Write(c, httpx.CodeSuccess, gin.H{"authorityIds": ids, "defaultRouterAuthorityIds": defaultIDs}, "获取成功")
}
func (h *Menu) SetRoles(c *gin.Context) {
var req dto.SetMenuRolesRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if req.MenuID == 0 {
httpx.Fail(c, "菜单ID不能为空")
return
}
if err := h.service.SetRoles(c.Request.Context(), &req); err != nil {
httpx.Fail(c, "设置失败"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "设置成功")
}
-26
View File
@@ -1,26 +0,0 @@
package handler
import (
"kra/internal/server/httpx"
"kra/internal/server/middleware"
"kra/internal/service"
"github.com/gin-gonic/gin"
)
type Navigation struct{ service *service.UserService }
func NewNavigation(service *service.UserService) *Navigation { return &Navigation{service: service} }
func (h *Navigation) Menu(c *gin.Context) {
claims := middleware.Claims(c)
if claims == nil {
httpx.NoAuth(c, "未登录或非法访问")
return
}
menus, err := h.service.Menus(c.Request.Context(), claims.AuthorityID)
if err != nil {
httpx.Fail(c, "获取失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{"menus": menus}, "获取成功")
}
-253
View File
@@ -1,253 +0,0 @@
package handler
import (
"kra/internal/server/httpx"
"kra/internal/service"
"kra/internal/service/dto"
"github.com/gin-gonic/gin"
)
type Organization struct {
departments *service.DepartmentService
positions *service.PositionService
}
func NewOrganization(departments *service.DepartmentService, positions *service.PositionService) *Organization {
return &Organization{departments: departments, positions: positions}
}
func (h *Organization) ListDepartments(c *gin.Context) {
var req dto.DepartmentListRequest
_ = c.ShouldBindJSON(&req)
items, err := h.departments.Departments(c.Request.Context(), req.Name)
if err != nil {
httpx.Fail(c, "获取失败:"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, items, "获取成功")
}
func (h *Organization) CreateDepartment(c *gin.Context) {
var req dto.DepartmentRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if req.Name == "" {
httpx.Fail(c, "部门名称不能为空")
return
}
if err := h.departments.Create(c.Request.Context(), &req); err != nil {
httpx.Fail(c, "创建失败:"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "创建成功")
}
func (h *Organization) UpdateDepartment(c *gin.Context) {
var req dto.DepartmentRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if req.ID == 0 {
httpx.Fail(c, "部门ID不能为空")
return
}
if err := h.departments.Update(c.Request.Context(), &req); err != nil {
httpx.Fail(c, "更新失败:"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "更新成功")
}
func (h *Organization) DeleteDepartment(c *gin.Context) {
var req dto.DeleteDepartmentRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if err := h.departments.Delete(c.Request.Context(), req.ID); err != nil {
httpx.Fail(c, "删除失败:"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
}
func (h *Organization) FindDepartment(c *gin.Context) {
var req dto.DeleteDepartmentRequest
if err := c.ShouldBindQuery(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
item, err := h.departments.Department(c.Request.Context(), req.ID)
if err != nil {
httpx.Fail(c, "获取失败:"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, item, "获取成功")
}
func (h *Organization) DepartmentUsers(c *gin.Context) {
var req dto.SetDepartmentUsersRequest
if err := c.ShouldBindQuery(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
ids, err := h.departments.UserIDs(c.Request.Context(), req.DepartmentID)
if err != nil {
httpx.Fail(c, "获取失败:"+err.Error())
return
}
if ids == nil {
ids = []uint{}
}
httpx.Write(c, httpx.CodeSuccess, ids, "获取成功")
}
func (h *Organization) SetDepartmentUsers(c *gin.Context) {
var req dto.SetDepartmentUsersRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if req.DepartmentID == 0 {
httpx.Fail(c, "部门ID不能为空")
return
}
if err := h.departments.SetUsers(c.Request.Context(), &req); err != nil {
httpx.Fail(c, "设置失败:"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "设置成功")
}
func (h *Organization) SetUserDepartments(c *gin.Context) {
var req dto.SetUserDepartmentsRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if req.ID == 0 {
httpx.Fail(c, "用户ID不能为空")
return
}
if err := h.departments.SetUserDepartments(c.Request.Context(), &req); err != nil {
httpx.Fail(c, "设置失败:"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "设置成功")
}
func (h *Organization) ListPositions(c *gin.Context) {
var req dto.PositionListRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
items, total, err := h.positions.Positions(c.Request.Context(), &req)
if err != nil {
httpx.Fail(c, "获取失败:"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功")
}
func (h *Organization) CreatePosition(c *gin.Context) {
var req dto.PositionRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if req.Name == "" {
httpx.Fail(c, "岗位名称不能为空")
return
}
if err := h.positions.Create(c.Request.Context(), &req); err != nil {
httpx.Fail(c, "创建失败:"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "创建成功")
}
func (h *Organization) UpdatePosition(c *gin.Context) {
var req dto.PositionRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if req.ID == 0 {
httpx.Fail(c, "岗位ID不能为空")
return
}
if err := h.positions.Update(c.Request.Context(), &req); err != nil {
httpx.Fail(c, "更新失败:"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "更新成功")
}
func (h *Organization) DeletePosition(c *gin.Context) {
var req dto.DeletePositionRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if err := h.positions.Delete(c.Request.Context(), req.ID); err != nil {
httpx.Fail(c, "删除失败:"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
}
func (h *Organization) FindPosition(c *gin.Context) {
var req dto.DeletePositionRequest
if err := c.ShouldBindQuery(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
item, err := h.positions.Position(c.Request.Context(), req.ID)
if err != nil {
httpx.Fail(c, "获取失败:"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, item, "获取成功")
}
func (h *Organization) PositionUsers(c *gin.Context) {
var req dto.SetPositionUsersRequest
if err := c.ShouldBindQuery(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
ids, err := h.positions.UserIDs(c.Request.Context(), req.PositionID)
if err != nil {
httpx.Fail(c, "获取失败:"+err.Error())
return
}
if ids == nil {
ids = []uint{}
}
httpx.Write(c, httpx.CodeSuccess, ids, "获取成功")
}
func (h *Organization) SetPositionUsers(c *gin.Context) {
var req dto.SetPositionUsersRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if req.PositionID == 0 {
httpx.Fail(c, "岗位ID不能为空")
return
}
if err := h.positions.SetUsers(c.Request.Context(), &req); err != nil {
httpx.Fail(c, "设置失败:"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "设置成功")
}
func (h *Organization) SetUserPositions(c *gin.Context) {
var req dto.SetUserPositionsRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if req.ID == 0 {
httpx.Fail(c, "用户ID不能为空")
return
}
if err := h.positions.SetUserPositions(c.Request.Context(), &req); err != nil {
httpx.Fail(c, "设置失败:"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "设置成功")
}
-92
View File
@@ -1,92 +0,0 @@
package handler
import (
"kra/internal/server/httpx"
"kra/internal/service"
"kra/internal/service/dto"
"github.com/gin-gonic/gin"
)
type Parameter struct {
service *service.ParameterService
}
func NewParameter(service *service.ParameterService) *Parameter {
return &Parameter{service: service}
}
func (h *Parameter) Create(c *gin.Context) {
var req dto.SystemParameterRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if err := h.service.CreateParameterRequest(c.Request.Context(), &req); err != nil {
httpx.Fail(c, "创建失败:"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "创建成功")
}
func (h *Parameter) Update(c *gin.Context) {
var req dto.SystemParameterRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if err := h.service.UpdateParameterRequest(c.Request.Context(), &req); err != nil {
httpx.Fail(c, "更新失败:"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "更新成功")
}
func (h *Parameter) Delete(c *gin.Context) {
if err := h.service.DeleteParameters(c.Request.Context(), []string{c.Query("ID")}); err != nil {
httpx.Fail(c, "删除失败:"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
}
func (h *Parameter) DeleteMany(c *gin.Context) {
ids := c.QueryArray("IDs[]")
if err := h.service.DeleteParameters(c.Request.Context(), ids); err != nil {
httpx.Fail(c, "批量删除失败:"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "批量删除成功")
}
func (h *Parameter) Find(c *gin.Context) {
item, err := h.service.ParameterByID(c.Request.Context(), c.Query("ID"))
if err != nil {
httpx.Fail(c, "查询失败:"+err.Error())
return
}
httpx.OKWithData(c, item)
}
func (h *Parameter) Get(c *gin.Context) {
item, err := h.service.ParameterByKey(c.Request.Context(), c.Query("key"))
if err != nil {
httpx.Fail(c, "获取失败:"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, item, "获取成功")
}
func (h *Parameter) List(c *gin.Context) {
var req dto.SystemParameterSearchRequest
if err := c.ShouldBindQuery(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
items, total, err := h.service.ParametersFilter(c.Request.Context(), req.Page, req.PageSize, req.Name, req.Key, req.StartCreatedAt, req.EndCreatedAt)
if err != nil {
httpx.Fail(c, "获取失败:"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功")
}
-139
View File
@@ -1,139 +0,0 @@
package handler
import (
"io"
"net/http"
"kra/internal/server/httpx"
"kra/internal/service"
"kra/internal/service/dto"
"github.com/gin-gonic/gin"
)
type Payment struct{ service *service.PaymentService }
func NewPayment(service *service.PaymentService) *Payment { return &Payment{service: service} }
func (h *Payment) Configs(c *gin.Context) {
v, err := h.service.Configs(c.Request.Context())
if err != nil {
httpx.Fail(c, err.Error())
return
}
httpx.OKWithData(c, v)
}
func (h *Payment) SaveConfig(c *gin.Context) {
var req dto.PaymentConfigRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if err := h.service.SaveConfig(c.Request.Context(), &req); err != nil {
httpx.Fail(c, err.Error())
return
}
httpx.OK(c)
}
func (h *Payment) Order(c *gin.Context) {
var req dto.PaymentQueryRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
order, err := h.service.Order(c.Request.Context(), req.Provider, req.TradeNo)
if err != nil {
httpx.Fail(c, err.Error())
return
}
httpx.OKWithData(c, order)
}
func (h *Payment) Orders(c *gin.Context) {
var req dto.PaymentOrderListRequest
if err := c.ShouldBindQuery(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
items, total, page, pageSize, err := h.service.Orders(c.Request.Context(), &req)
if err != nil {
httpx.Fail(c, err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: page, PageSize: pageSize}, "获取成功")
}
func (h *Payment) Create(c *gin.Context) {
var req dto.PaymentRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
req.ClientIP = c.ClientIP()
result, err := h.service.Create(c.Request.Context(), &req)
if err != nil {
httpx.Fail(c, err.Error())
return
}
httpx.OKWithData(c, result)
}
func (h *Payment) Query(c *gin.Context) {
var req dto.PaymentQueryRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
result, err := h.service.Query(c.Request.Context(), &req)
if err != nil {
httpx.Fail(c, err.Error())
return
}
httpx.OKWithData(c, result)
}
func (h *Payment) Refund(c *gin.Context) {
var req dto.PaymentRefundRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
result, err := h.service.Refund(c.Request.Context(), &req)
if err != nil {
httpx.Fail(c, err.Error())
return
}
httpx.OKWithData(c, result)
}
func (h *Payment) Callback(c *gin.Context) {
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 8<<20)
raw, err := io.ReadAll(c.Request.Body)
if err != nil {
writePaymentCallbackAck(c, service.DefaultPaymentCallbackAck(c.Param("provider"), false))
return
}
headers := map[string]string{}
for k, v := range c.Request.Header {
if len(v) > 0 {
headers[k] = v[0]
}
}
query := map[string]string{}
for k, v := range c.Request.URL.Query() {
if len(v) > 0 {
query[k] = v[0]
}
}
ack, err := h.service.Callback(c.Request.Context(), &dto.PaymentCallbackRequest{Provider: c.Param("provider"), Headers: headers, Body: raw, Query: query})
writePaymentCallbackAck(c, ack)
_ = err
}
func writePaymentCallbackAck(c *gin.Context, ack dto.PaymentCallbackAck) {
if ack.StatusCode == 0 {
ack.StatusCode = http.StatusInternalServerError
}
if ack.ContentType != "" {
c.Header("Content-Type", ack.ContentType)
}
c.Status(ack.StatusCode)
if len(ack.Body) > 0 {
_, _ = c.Writer.Write(ack.Body)
}
}
-29
View File
@@ -1,29 +0,0 @@
package handler
import (
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"kra/internal/service/dto"
)
func TestWritePaymentCallbackAckDoesNotExposeError(t *testing.T) {
gin.SetMode(gin.TestMode)
recorder := httptest.NewRecorder()
context, _ := gin.CreateTestContext(recorder)
writePaymentCallbackAck(context, dto.PaymentCallbackAck{
StatusCode: 500,
ContentType: "text/plain; charset=utf-8",
Body: []byte("failure"),
})
if recorder.Code != 500 {
t.Fatalf("status = %d, want 500", recorder.Code)
}
if recorder.Body.String() != "failure" {
t.Fatalf("body = %q, want fixed failure ack", recorder.Body.String())
}
if recorder.Body.String() == "internal order resolver details" {
t.Fatal("internal error was exposed in callback response")
}
}
-55
View File
@@ -1,55 +0,0 @@
package handler
import (
"kra/internal/server/httpx"
"kra/internal/service"
"kra/internal/service/dto"
"github.com/gin-gonic/gin"
)
type Permission struct{ service *service.PermissionService }
func NewPermission(service *service.PermissionService) *Permission {
return &Permission{service: service}
}
func (h *Permission) Buttons(c *gin.Context) {
var req dto.GetAuthorityButtonsRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
selected, err := h.service.SelectedButtons(c.Request.Context(), req.AuthorityID, req.MenuID)
if err != nil {
httpx.Fail(c, "查询失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{"selected": selected}, "查询成功")
}
func (h *Permission) SetButtons(c *gin.Context) {
var req dto.SetAuthorityButtonsRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if err := h.service.SetSelectedButtons(c.Request.Context(), req.AuthorityID, req.MenuID, req.Selected); err != nil {
httpx.Fail(c, "分配失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "分配成功")
}
func (h *Permission) CanRemoveButton(c *gin.Context) {
allowed, err := h.service.CanRemoveButton(c.Request.Context(), c.Query("id"))
if err != nil {
httpx.Fail(c, err.Error())
return
}
if !allowed {
httpx.Fail(c, "此按钮正在被使用无法删除")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
}
-169
View File
@@ -1,169 +0,0 @@
package handler
import (
"context"
"errors"
"strconv"
"strings"
"time"
"kra/internal/biz"
"kra/internal/server/httpx"
"kra/internal/service"
"kra/internal/service/dto"
"github.com/gin-gonic/gin"
"github.com/mojocn/base64Captcha"
)
type Public struct {
auth *service.AuthService
system *service.SystemConfigService
settings *service.SecurityService
store *captchaStore
}
func NewPublic(auth *service.AuthService, system *service.SystemConfigService, settings *service.SecurityService) *Public {
return &Public{auth: auth, system: system, settings: settings, store: &captchaStore{service: settings}}
}
func (h *Public) captchaConfig() (int, int, int) {
config := h.settings.CaptchaSettings()
return config.KeyLong, config.ImageWidth, config.ImageHeight
}
func (h *Public) Captcha(c *gin.Context) {
keyLong, width, height := h.captchaConfig()
security, err := h.settings.CurrentSecurity(c.Request.Context())
if err != nil || security == nil {
httpx.Fail(c, "安全服务暂不可用")
return
}
openCaptcha := true
if security != nil {
keyLong, width, height = security.KeyLong, security.ImgWidth, security.ImgHeight
ttl := time.Duration(security.CaptchaTimeout) * time.Second
failures, counterErr := h.settings.EnsureLoginIPCounter(c.Request.Context(), c.ClientIP(), ttl)
if counterErr != nil {
httpx.Fail(c, "安全服务暂不可用")
return
}
openCaptcha = security.CaptchaOpen == 0 || failures > security.CaptchaOpen
}
driver := base64Captcha.NewDriverDigit(height, width, keyLong, 0.7, 80)
id, picture, _, err := base64Captcha.NewCaptcha(driver, h.store).Generate()
if err != nil {
httpx.Fail(c, "验证码获取失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{"captchaId": id, "picPath": picture, "captchaLength": keyLong, "openCaptcha": openCaptcha}, "验证码获取成功")
}
func (h *Public) Login(c *gin.Context) {
var req dto.LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if req.Username == "" {
httpx.Fail(c, "Username值不能为空")
return
}
if req.Password == "" {
httpx.Fail(c, "Password值不能为空")
return
}
result, err := h.auth.Login(c.Request.Context(), &req, c.ClientIP(), c.Request.UserAgent())
if err != nil {
switch {
case errors.Is(err, biz.ErrAccountLocked):
var locked *biz.AccountLockedError
errors.As(err, &locked)
minutes := 0
if locked != nil {
minutes = locked.Minutes
}
httpx.Fail(c, "账号已锁定,请 "+strconv.Itoa(minutes)+" 分钟后再试")
case errors.Is(err, biz.ErrCaptchaInvalid):
httpx.Fail(c, "验证码错误")
case errors.Is(err, biz.ErrUserDisabled):
httpx.Fail(c, "用户被禁止登录")
case errors.Is(err, biz.ErrTokenIssue):
httpx.Fail(c, "获取token失败")
case errors.Is(err, biz.ErrLoginState):
httpx.Fail(c, "设置登录状态失败")
case errors.Is(err, biz.ErrTokenRevoke):
httpx.Fail(c, "jwt作废失败")
default:
httpx.Fail(c, "用户名不存在或者密码错误")
}
return
}
maxAge := int(time.UnixMilli(result.ExpiresAt).Unix() - time.Now().Unix())
httpx.SetTokenCookie(c, result.Token, maxAge)
httpx.Write(c, httpx.CodeSuccess, result, "登录成功")
}
func (h *Public) CheckDatabase(c *gin.Context) {
initialized, err := h.system.IsInitialized(c.Request.Context())
if err != nil {
httpx.Fail(c, "数据库状态检查失败")
return
}
message := "数据库无需初始化"
if !initialized {
message = "前往初始化数据库"
}
httpx.Write(c, httpx.CodeSuccess, gin.H{"needInit": !initialized}, message)
}
func (h *Public) InitializeDatabase(engine *gin.Engine) gin.HandlerFunc {
return func(c *gin.Context) {
initialized, err := h.system.IsInitialized(c.Request.Context())
if err != nil {
httpx.Fail(c, "数据库状态检查失败")
return
}
if initialized {
httpx.Fail(c, "已存在数据库配置")
return
}
var input dto.DatabaseInitRequest
if c.ShouldBindJSON(&input) != nil {
httpx.Fail(c, "参数校验不通过")
return
}
routes := engine.Routes()
values := make([]dto.Route, 0, len(routes))
for _, route := range routes {
values = append(values, dto.Route{Path: route.Path, Method: route.Method})
}
if err := h.system.InitializeRoutes(c.Request.Context(), &input, values); err != nil {
httpx.Fail(c, "自动创建数据库失败,请查看后台日志,检查后在进行初始化")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "自动创建数据库成功")
}
}
type captchaStore struct {
service *service.SecurityService
}
func (s *captchaStore) Set(id, value string) error {
expiration := s.service.CaptchaSettings().StoreExpiration
return s.service.SetCaptcha(context.Background(), id, value, expiration)
}
func (s *captchaStore) Get(id string, clear bool) string {
value, ok, err := s.service.GetCaptcha(context.Background(), id)
if err != nil || !ok {
return ""
}
if clear {
_ = s.service.DeleteCaptcha(context.Background(), id)
}
return value
}
func (s *captchaStore) Verify(id, answer string, clear bool) bool {
return id != "" && answer != "" && strings.EqualFold(s.Get(id, clear), answer)
}
-38
View File
@@ -1,38 +0,0 @@
package handler
import (
"strconv"
"github.com/gin-gonic/gin"
)
func page(c *gin.Context) (int, int, error) {
var value, size int
if raw, exists := c.GetQuery("page"); exists && raw != "" {
parsed, err := strconv.Atoi(raw)
if err != nil {
return 0, 0, err
}
value = parsed
}
if raw, exists := c.GetQuery("pageSize"); exists && raw != "" {
parsed, err := strconv.Atoi(raw)
if err != nil {
return 0, 0, err
}
size = parsed
}
return value, size, nil
}
func IDsFromQuery(c *gin.Context) []uint {
values := c.QueryArray("IDs[]")
ids := make([]uint, 0, len(values))
for _, value := range values {
id, _ := strconv.ParseUint(value, 10, 64)
if id > 0 {
ids = append(ids, uint(id))
}
}
return ids
}
-24
View File
@@ -1,24 +0,0 @@
package handler
import (
"kra/internal/server/httpx"
"kra/internal/service"
"github.com/gin-gonic/gin"
)
type Session struct{ tokens *service.TokenService }
func NewSession(tokens *service.TokenService) *Session { return &Session{tokens: tokens} }
func (h *Session) Logout(c *gin.Context) {
token := c.GetHeader("x-token")
if token == "" {
token, _ = c.Cookie("x-token")
}
if err := h.tokens.BlacklistToken(c.Request.Context(), token); err != nil {
httpx.Fail(c, "jwt作废失败")
return
}
httpx.SetTokenCookie(c, "", -1)
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "jwt作废成功")
}
-29
View File
@@ -1,29 +0,0 @@
package handler
type Set struct {
Authority *Authority
Menu *Menu
API *API
Permission *Permission
Organization *Organization
Announcement *Announcement
Email *Email
Payment *Payment
Task *Task
Media *Media
Audit *Audit
Export *Export
Version *Version
Dictionary *Dictionary
Parameter *Parameter
APIToken *APIToken
SystemConfig *SystemConfig
Public *Public
User *User
Navigation *Navigation
Session *Session
}
func NewSet(authority *Authority, menu *Menu, api *API, permission *Permission, organization *Organization, announcement *Announcement, email *Email, payment *Payment, task *Task, media *Media, audit *Audit, export *Export, version *Version, dictionary *Dictionary, parameter *Parameter, apiToken *APIToken, systemConfig *SystemConfig, public *Public, user *User, navigation *Navigation, session *Session) *Set {
return &Set{Authority: authority, Menu: menu, API: api, Permission: permission, Organization: organization, Announcement: announcement, Email: email, Payment: payment, Task: task, Media: media, Audit: audit, Export: export, Version: version, Dictionary: dictionary, Parameter: parameter, APIToken: apiToken, SystemConfig: systemConfig, Public: public, User: user, Navigation: navigation, Session: session}
}
-80
View File
@@ -1,80 +0,0 @@
package handler
import (
"kra/internal/server/httpx"
"kra/internal/service"
"kra/internal/service/dto"
"github.com/gin-gonic/gin"
)
type SystemConfig struct {
system *service.SystemConfigService
settings *service.SecurityService
}
func NewSystemConfig(system *service.SystemConfigService, settings *service.SecurityService) *SystemConfig {
return &SystemConfig{system: system, settings: settings}
}
func (h *SystemConfig) GetSecurity(c *gin.Context) {
value, err := h.settings.Security(c.Request.Context())
if err != nil {
httpx.Fail(c, "获取安全配置失败")
return
}
httpx.Write(c, httpx.CodeSuccess, value, "获取成功")
}
func (h *SystemConfig) SetSecurity(c *gin.Context) {
var req dto.SecurityConfigRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
result, err := h.settings.SaveSecurityRequest(c.Request.Context(), &req)
if err != nil {
httpx.Fail(c, "设置安全配置失败")
return
}
httpx.Write(c, httpx.CodeSuccess, result, "设置成功")
}
func (h *SystemConfig) Get(c *gin.Context) {
value, err := h.system.SystemConfig()
if err != nil {
httpx.Fail(c, "获取失败")
return
}
httpx.Write(c, httpx.CodeSuccess, value, "获取成功")
}
func (h *SystemConfig) Set(c *gin.Context) {
var req dto.SetSystemConfigRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if err := h.system.SaveSystemConfig(c.Request.Context(), &req); err != nil {
httpx.Fail(c, "设置失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "设置成功")
}
func (h *SystemConfig) Reload(c *gin.Context) {
if err := h.system.ReloadConfig(c.Request.Context()); err != nil {
httpx.Fail(c, "重载系统失败:"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "重载系统成功")
}
func (h *SystemConfig) ServerInfo(c *gin.Context) {
server, err := h.system.ServerInfo()
if err != nil {
httpx.Fail(c, "获取失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{"server": server}, "获取成功")
}
-162
View File
@@ -1,162 +0,0 @@
package handler
import (
"fmt"
"net/http"
"time"
"kra/internal/server/httpx"
"kra/internal/server/middleware"
"kra/internal/service"
"kra/internal/service/dto"
"github.com/gin-gonic/gin"
)
type Task struct {
service *service.TaskService
}
func NewTask(service *service.TaskService) *Task {
return &Task{service: service}
}
func (h *Task) Create(c *gin.Context) {
var req dto.TaskRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
_, err := h.service.CreateRequest(c.Request.Context(), &req)
if err != nil {
httpx.Fail(c, "创建失败: "+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "创建成功")
}
func (h *Task) Update(c *gin.Context) {
var req dto.TaskRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if err := h.service.UpdateRequest(c.Request.Context(), &req); err != nil {
httpx.Fail(c, "更新失败: "+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "更新成功")
}
func (h *Task) Delete(c *gin.Context) {
var req dto.DeleteTaskRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if err := h.service.Delete(c.Request.Context(), req.ID); err != nil {
httpx.Fail(c, "删除失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
}
func (h *Task) Toggle(c *gin.Context) {
var req dto.ToggleTaskRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if err := h.service.Toggle(c.Request.Context(), req.ID, req.Enabled); err != nil {
httpx.Fail(c, "操作失败: "+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "操作成功")
}
func (h *Task) Trigger(c *gin.Context) {
var req dto.DeleteTaskRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if err := h.service.Trigger(c.Request.Context(), req.ID); err != nil {
httpx.Fail(c, "触发失败: "+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "已触发, 执行结果见执行日志")
}
func (h *Task) List(c *gin.Context) {
var req dto.TaskListRequest
if err := c.ShouldBindQuery(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
items, total, err := h.service.ListRequest(c.Request.Context(), req.Page, req.PageSize, req.Name, req.ExecutorType, req.Enabled)
if err != nil {
httpx.Fail(c, "获取失败")
return
}
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功")
}
func (h *Task) Logs(c *gin.Context) {
var req dto.TaskLogListRequest
if err := c.ShouldBindQuery(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
items, total, err := h.service.Logs(c.Request.Context(), req.Page, req.PageSize, req.TaskID, req.Status)
if err != nil {
httpx.Fail(c, "获取失败")
return
}
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功")
}
func (h *Task) Methods(c *gin.Context) {
httpx.Write(c, httpx.CodeSuccess, gin.H{"methods": h.service.RegisteredMethods()}, "获取成功")
}
func (h *Task) AlertStream(c *gin.Context) {
claims := middleware.Claims(c)
if claims == nil || claims.ID == 0 {
httpx.Fail(c, "未获取到用户身份")
return
}
c.Header("Content-Type", "text/event-stream; charset=utf-8")
c.Header("Cache-Control", "no-cache, no-transform")
c.Header("Connection", "keep-alive")
c.Header("X-Accel-Buffering", "no")
flusher, ok := c.Writer.(http.Flusher)
if !ok {
httpx.Fail(c, "SSE 不受支持")
return
}
// Remove the normal server deadline for long-lived SSE connections.
controller := http.NewResponseController(c.Writer)
_ = controller.SetWriteDeadline(time.Time{})
_ = controller.SetReadDeadline(time.Time{})
events := h.service.Subscribe(claims.ID)
defer h.service.Unsubscribe(claims.ID, events)
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
_, _ = fmt.Fprint(c.Writer, ": connected\n\n")
flusher.Flush()
for {
select {
case event, open := <-events:
if !open {
return
}
_, _ = fmt.Fprintf(c.Writer, "event: timedTask:alert\ndata: %s\n\n", event)
flusher.Flush()
case <-ticker.C:
_, _ = fmt.Fprint(c.Writer, ": ping\n\n")
flusher.Flush()
case <-c.Request.Context().Done():
return
}
}
}
-257
View File
@@ -1,257 +0,0 @@
package handler
import (
"strconv"
"time"
"kra/internal/server/httpx"
"kra/internal/server/middleware"
"kra/internal/service"
"kra/internal/service/dto"
"github.com/gin-gonic/gin"
)
type User struct {
service *service.UserService
auth *service.AuthService
}
func NewUser(user *service.UserService, auth *service.AuthService) *User {
return &User{service: user, auth: auth}
}
func (h *User) List(c *gin.Context) {
var req dto.UserListRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if req.Page == 0 {
httpx.Fail(c, "Page值不能为空")
return
}
if req.PageSize == 0 {
httpx.Fail(c, "PageSize值不能为空")
return
}
items, total, err := h.service.ListUsersRequest(c.Request.Context(), &req)
if err != nil {
httpx.Fail(c, "获取失败")
return
}
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功")
}
func (h *User) Create(c *gin.Context) {
var req dto.UserRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if req.Username == "" {
httpx.Fail(c, "Username值不能为空")
return
}
if req.Password == "" {
httpx.Fail(c, "Password值不能为空")
return
}
if req.NickName == "" {
httpx.Fail(c, "NickName值不能为空")
return
}
if req.AuthorityID == 0 {
httpx.Fail(c, "AuthorityId值不能为空")
return
}
result, err := h.service.CreateUserRequest(c.Request.Context(), &req)
if err != nil {
if service.IsPasswordPolicyError(err) {
httpx.Fail(c, err.Error())
return
}
if result == nil {
result = &dto.UserResponse{}
}
httpx.Write(c, httpx.CodeError, gin.H{"user": result}, "注册失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{"user": result}, "注册成功")
}
func (h *User) Update(c *gin.Context) {
var req dto.UserRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if req.ID == 0 {
httpx.Fail(c, "ID值不能为空")
return
}
if err := h.service.UpdateUserRequest(c.Request.Context(), &req); err != nil {
httpx.Fail(c, "修改失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "设置成功")
}
func (h *User) UpdateSelf(c *gin.Context) {
claims := middleware.Claims(c)
if claims == nil {
httpx.NoAuth(c, "未登录或非法访问")
return
}
var req dto.SelfUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if err := h.service.UpdateSelfUserRequest(c.Request.Context(), claims.ID, &req); err != nil {
httpx.Fail(c, "修改失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "设置成功")
}
func (h *User) Delete(c *gin.Context) {
var req dto.IDRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if req.ID == 0 {
httpx.Fail(c, "ID值不能为空")
return
}
claims := middleware.Claims(c)
if claims != nil && claims.ID == req.ID {
httpx.Fail(c, "删除失败, 无法删除自己。")
return
}
if err := h.service.DeleteUser(c.Request.Context(), req.ID); err != nil {
httpx.Fail(c, "删除失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
}
func (h *User) ResetPassword(c *gin.Context) {
var req dto.ResetPasswordRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if err := h.service.ResetPassword(c.Request.Context(), req.ID, req.Password); err != nil {
if service.IsPasswordPolicyError(err) {
httpx.Fail(c, err.Error())
return
}
httpx.Fail(c, "重置失败"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "重置成功")
}
func (h *User) ChangePassword(c *gin.Context) {
claims := middleware.Claims(c)
var req dto.ChangePasswordRequest
if claims == nil {
httpx.NoAuth(c, "未登录或非法访问")
return
}
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if req.Password == "" {
httpx.Fail(c, "Password值不能为空")
return
}
if req.NewPassword == "" {
httpx.Fail(c, "NewPassword值不能为空")
return
}
if err := h.service.ChangePassword(c.Request.Context(), claims.ID, req.Password, req.NewPassword); err != nil {
if service.IsPasswordPolicyError(err) {
httpx.Fail(c, err.Error())
return
}
httpx.Fail(c, "修改失败,原密码与当前账户不符")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "修改成功")
}
func (h *User) SetSelfSetting(c *gin.Context) {
claims := middleware.Claims(c)
var setting map[string]any
if claims == nil {
httpx.NoAuth(c, "未登录或非法访问")
return
}
if err := c.ShouldBindJSON(&setting); err != nil {
httpx.Fail(c, err.Error())
return
}
if err := h.service.SetUserSetting(c.Request.Context(), claims.ID, setting); err != nil {
httpx.Fail(c, "设置失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "设置成功")
}
func (h *User) SetAuthorities(c *gin.Context) {
var req dto.UserAuthoritiesRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if err := h.service.SetUserAuthorities(c.Request.Context(), req.ID, req.AuthorityIDs); err != nil {
httpx.Fail(c, "修改失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "修改成功")
}
func (h *User) SwitchAuthority(c *gin.Context) {
claims := middleware.Claims(c)
var req dto.SwitchAuthorityRequest
if claims == nil {
httpx.NoAuth(c, "未登录或非法访问")
return
}
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if req.AuthorityID == 0 {
httpx.Fail(c, "AuthorityId值不能为空")
return
}
login, err := h.auth.SwitchAuthority(c.Request.Context(), claims, req.AuthorityID)
if err != nil {
httpx.Fail(c, err.Error())
return
}
c.Header("new-token", login.Token)
c.Header("new-expires-at", strconv.FormatInt(login.ExpiresAt/1000, 10))
maxAge := int(time.UnixMilli(login.ExpiresAt).Unix() - time.Now().Unix())
httpx.SetTokenCookie(c, login.Token, maxAge)
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "修改成功")
}
func (h *User) Get(c *gin.Context) {
claims := middleware.Claims(c)
if claims == nil {
httpx.NoAuth(c, "未登录或非法访问")
return
}
result, err := h.service.UserByUUID(c.Request.Context(), claims.UUID)
if err != nil {
httpx.Fail(c, "获取失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{"userInfo": result}, "获取成功")
}
-152
View File
@@ -1,152 +0,0 @@
package handler
import (
"errors"
"fmt"
"net/http"
"strconv"
"time"
"kra/internal/biz"
"kra/internal/server/httpx"
"kra/internal/service"
"kra/internal/service/dto"
"github.com/gin-gonic/gin"
)
type Version struct{ service *service.VersionService }
func NewVersion(service *service.VersionService) *Version { return &Version{service: service} }
func versionStage(err error) (biz.VersionStage, bool) {
var stageErr *biz.VersionStageError
if !errors.As(err, &stageErr) {
return "", false
}
return stageErr.Stage, true
}
func (h *Version) Delete(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
if err := h.service.DeleteVersions(c.Request.Context(), []uint{uint(id)}); err != nil {
httpx.Fail(c, "删除失败:"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
}
func (h *Version) DeleteMany(c *gin.Context) {
raw := c.QueryArray("IDs[]")
ids := make([]uint, 0, len(raw))
for _, value := range raw {
id, _ := strconv.ParseUint(value, 10, 64)
ids = append(ids, uint(id))
}
if err := h.service.DeleteVersions(c.Request.Context(), ids); err != nil {
httpx.Fail(c, "批量删除失败:"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "批量删除成功")
}
func (h *Version) Find(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
item, err := h.service.Version(c.Request.Context(), uint(id))
if err != nil {
httpx.Fail(c, "查询失败:"+err.Error())
return
}
httpx.OKWithData(c, item)
}
func (h *Version) List(c *gin.Context) {
var req dto.VersionSearchRequest
if err := c.ShouldBindQuery(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
name, code := "", ""
if req.VersionName != nil {
name = *req.VersionName
}
if req.VersionCode != nil {
code = *req.VersionCode
}
createdAtRange := make([]*time.Time, 0, len(req.CreatedAtRange))
for i := range req.CreatedAtRange {
createdAtRange = append(createdAtRange, &req.CreatedAtRange[i])
}
items, total, err := h.service.Versions(c.Request.Context(), req.Page, req.PageSize, name, code, createdAtRange)
if err != nil {
httpx.Fail(c, "获取失败:"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功")
}
func (h *Version) Export(c *gin.Context) {
var req dto.ExportVersionRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if err := h.service.Export(c.Request.Context(), req.VersionName, req.VersionCode, req.Description, req.MenuIDs, req.APIIDs, req.DictIDs); err != nil {
switch stage, _ := versionStage(err); stage {
case biz.VersionStageMenus:
httpx.Fail(c, "获取菜单数据失败:"+err.Error())
case biz.VersionStageAPIs:
httpx.Fail(c, "获取API数据失败:"+err.Error())
case biz.VersionStageDictionaries:
httpx.Fail(c, "获取字典数据失败:"+err.Error())
case biz.VersionStageJSON:
httpx.Fail(c, "JSON序列化失败:"+err.Error())
case biz.VersionStageSave:
httpx.Fail(c, "保存版本记录失败:"+err.Error())
default:
httpx.Fail(c, "创建发版失败:"+err.Error())
}
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "创建发版成功")
}
func (h *Version) Download(c *gin.Context) {
rawID := c.Query("ID")
if rawID == "" {
httpx.Fail(c, "版本ID不能为空")
return
}
id, _ := strconv.ParseUint(rawID, 10, 64)
raw, code, err := h.service.VersionData(c.Request.Context(), uint(id))
if err != nil {
httpx.Fail(c, "获取版本记录失败:"+err.Error())
return
}
name := fmt.Sprintf("version_%s_%s.json", code, time.Now().Format("20060102150405"))
c.Header("Content-Type", "application/json")
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", name))
c.Header("Content-Length", strconv.Itoa(len(raw)))
c.Data(http.StatusOK, "application/json", []byte(raw))
}
func (h *Version) Import(c *gin.Context) {
var req dto.ImportVersionRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, "解析JSON数据失败:"+err.Error())
return
}
if err := h.service.ImportRequest(c.Request.Context(), &req); err != nil {
if errors.Is(err, biz.ErrInvalidVersion) {
httpx.Fail(c, "版本信息格式错误")
return
}
switch stage, _ := versionStage(err); stage {
case biz.VersionStageMenus:
httpx.Fail(c, "导入菜单失败: "+err.Error())
case biz.VersionStageAPIs:
httpx.Fail(c, "导入API失败: "+err.Error())
case biz.VersionStageDictionaries:
httpx.Fail(c, "导入字典失败: "+err.Error())
default:
httpx.Fail(c, "导入失败:"+err.Error())
}
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "导入成功")
}
-27
View File
@@ -1,27 +0,0 @@
package httpx
import (
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
)
func requestUsesHTTPS(request *http.Request) bool {
if request.TLS != nil {
return true
}
forwarded := strings.SplitN(request.Header.Get("X-Forwarded-Proto"), ",", 2)[0]
return strings.EqualFold(strings.TrimSpace(forwarded), "https")
}
// SetTokenCookie also marks cookies Secure behind TLS or a TLS-terminating
// reverse proxy.
func SetTokenCookie(c *gin.Context, value string, maxAge int) {
cookie := &http.Cookie{Name: "x-token", Value: value, Path: "/", MaxAge: maxAge, Secure: requestUsesHTTPS(c.Request), HttpOnly: true, SameSite: http.SameSiteStrictMode}
if maxAge < 0 {
cookie.Expires = time.Unix(1, 0)
}
http.SetCookie(c.Writer, cookie)
}
-40
View File
@@ -1,40 +0,0 @@
package httpx
import (
"net/http"
"github.com/gin-gonic/gin"
)
const (
CodeSuccess = 0
CodeError = 7
CodePasswordChangeRequired = 10001
)
type Response struct {
Code int `json:"code"`
Data any `json:"data"`
Msg string `json:"msg"`
}
type PageResult struct {
List any `json:"list"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"pageSize"`
}
func Write(c *gin.Context, code int, data any, message string) {
c.JSON(http.StatusOK, Response{Code: code, Data: data, Msg: message})
}
func OK(c *gin.Context) { Write(c, CodeSuccess, gin.H{}, "操作成功") }
func OKWithData(c *gin.Context, data any) { Write(c, CodeSuccess, data, "成功") }
func Fail(c *gin.Context, message string) { Write(c, CodeError, gin.H{}, message) }
func NoAuth(c *gin.Context, message string) {
c.AbortWithStatusJSON(http.StatusUnauthorized, Response{Code: CodeError, Data: nil, Msg: message})
}
-53
View File
@@ -1,53 +0,0 @@
package middleware
import (
"context"
"kra/internal/biz"
"kra/internal/conf"
"kra/internal/server/httpx"
"kra/internal/service"
"github.com/gin-gonic/gin"
)
type accessController interface {
Authorize(context.Context, uint, string, string) (bool, error)
ContextWithDataScope(context.Context, uint, uint) (context.Context, error)
}
func AccessControl(runtime *conf.Runtime, access accessController) gin.HandlerFunc {
return func(c *gin.Context) {
claims := Claims(c)
if claims == nil {
httpx.NoAuth(c, "未登录或非法访问")
return
}
path := c.Request.URL.Path
policyPath := path
if config := runtime.Admin(); config != nil {
policyPath = service.NormalizeRoutePath(policyPath, config.RouterPrefix)
}
allowed, err := access.Authorize(c.Request.Context(), claims.AuthorityID, policyPath, c.Request.Method)
if err != nil || !allowed {
httpx.Write(c, httpx.CodeError, gin.H{}, "权限不足")
c.Abort()
return
}
requestContext, err := access.ContextWithDataScope(c.Request.Context(), claims.AuthorityID, claims.ID)
if err != nil {
httpx.Write(c, httpx.CodeError, gin.H{}, "数据权限解析失败")
c.Abort()
return
}
if scope, ok := biz.DataScopeFromContext(requestContext); ok {
requestID, _ := c.Get("request_id")
scope.RequestID, _ = requestID.(string)
scope.Method, scope.Path = c.Request.Method, path
requestContext = biz.NewDataScopeContext(requestContext, scope)
}
requestContext = biz.NewActorContext(requestContext, biz.Actor{UserID: claims.ID, AuthorityID: claims.AuthorityID})
c.Request = c.Request.WithContext(requestContext)
c.Next()
}
}
-142
View File
@@ -1,142 +0,0 @@
package middleware
import (
"bytes"
"errors"
"io"
"log/slog"
"net/http"
"strings"
"time"
"kra/internal/biz"
"kra/internal/conf"
"kra/internal/server/httpx"
"github.com/gin-gonic/gin"
)
const defaultRequestBodyLimit int64 = 8 << 20
// AccessLog is the single global request/response capture point, matching
// the reference middleware ordering and making every HTTP request observable.
func AccessLog(runtime *conf.Runtime, logger *slog.Logger, version string) gin.HandlerFunc {
return func(c *gin.Context) {
started := time.Now()
var requestBody []byte
multipart := strings.Contains(c.GetHeader("Content-Type"), "multipart/form-data")
mediaUpload := multipart && isMediaUploadRoute(c.FullPath())
config := runtime.Admin()
bodyLimit := defaultRequestBodyLimit
if mediaUpload {
bodyLimit = biz.DefaultMaxMediaFileSize + (1 << 20)
if config != nil && config.Media != nil && config.Media.MaxFileSize > 0 {
bodyLimit = config.Media.MaxFileSize + (1 << 20)
}
}
bytesIn := c.Request.ContentLength
maxBytes := 1 << 20
writer := &captureWriter{ResponseWriter: c.Writer, maxBytes: maxBytes}
c.Writer = writer
c.Header("X-Kra-Version", version)
requestReadFailed := c.Request.ContentLength > bodyLimit
if requestReadFailed {
c.AbortWithStatusJSON(http.StatusRequestEntityTooLarge, gin.H{"code": httpx.CodeError, "msg": "请求体超过大小上限"})
} else if c.Request.Body != nil && !mediaUpload {
limited := http.MaxBytesReader(c.Writer, c.Request.Body, bodyLimit)
var err error
requestBody, err = io.ReadAll(limited)
if err != nil {
requestReadFailed = true
var tooLarge *http.MaxBytesError
if errors.As(err, &tooLarge) {
c.AbortWithStatusJSON(http.StatusRequestEntityTooLarge, gin.H{"code": httpx.CodeError, "msg": "请求体超过大小上限"})
} else {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"code": httpx.CodeError, "msg": "请求体读取失败"})
}
} else {
c.Request.Body = io.NopCloser(bytes.NewReader(requestBody))
bytesIn = int64(len(requestBody))
}
} else if c.Request.Body != nil {
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, bodyLimit)
}
if bytesIn < 0 {
bytesIn = 0
}
logLimit := 1024
if config != nil && config.Zap != nil && config.Zap.AccessLogMaxBytes > 0 {
logLimit = int(config.Zap.AccessLogMaxBytes)
}
requestText := ""
if multipart {
requestText = "[文件]"
} else {
requestText = redactJSON(requestBody, c.GetHeader("Content-Type"), logLimit)
}
c.Set(ctxReqBodyKey, requestText)
c.Set(ctxRespBufferKey, &writer.body)
if !requestReadFailed {
c.Next()
}
if logger == nil {
return
}
responseText := redactJSON(writer.body.Bytes(), c.Writer.Header().Get("Content-Type"), logLimit)
userID, authorityID := uint(0), uint(0)
if claims := Claims(c); claims != nil {
userID, authorityID = claims.ID, claims.AuthorityID
}
route := c.FullPath()
if route == "" {
route = "unmatched"
}
bytesOut := int64(c.Writer.Size())
if bytesOut < 0 {
bytesOut = 0
}
privateErrors := strings.TrimRight(c.Errors.ByType(gin.ErrorTypePrivate).String(), "\n")
attributes := []any{
"mod", "http", "ip", c.ClientIP(), "method", c.Request.Method, "http_path", c.Request.URL.Path, "http_route", route,
"http_status", c.Writer.Status(), "latency_ms", time.Since(started).Milliseconds(),
"request_id", stringValueFromContext(c, "request_id"), "trace_id", stringValueFromContext(c, "trace_id"),
"bytes_in", bytesIn, "bytes_out", bytesOut, "user_id", userID, "authority_id", authorityID,
"error", c.Writer.Status() >= http.StatusInternalServerError || privateErrors != "", "ua", c.Request.UserAgent(), "req_query", c.Request.URL.RawQuery}
if config != nil && config.Zap != nil && config.Zap.AccessReqHeaders {
attributes = append(attributes, "req_headers", redactHeaders(c.Request.Header))
}
if config != nil && config.Zap != nil && config.Zap.AccessReqBody {
attributes = append(attributes, "req_body", requestText)
}
if config != nil && config.Zap != nil && config.Zap.AccessRespData {
attributes = append(attributes, "resp_data", responseText)
}
if privateErrors != "" {
attributes = append(attributes, "error_msg", privateErrors)
}
logger.InfoContext(c.Request.Context(), "请求完成", attributes...)
}
}
func isMediaUploadRoute(route string) bool {
return strings.HasSuffix(route, "/fileUploadAndDownload/upload") ||
strings.HasSuffix(route, "/mediaUpload/chunk")
}
func redactHeaders(headers map[string][]string) map[string]string {
out := make(map[string]string, len(headers))
for key, values := range headers {
lower := strings.ToLower(key)
if lower == "authorization" || lower == "cookie" || lower == "set-cookie" || lower == "x-token" {
out[key] = "***"
} else {
out[key] = strings.Join(values, ",")
}
}
return out
}
func stringValueFromContext(c *gin.Context, key string) string {
value, _ := c.Get(key)
return stringValue(value)
}
@@ -1,65 +0,0 @@
package middleware
import (
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"kra/internal/conf"
"github.com/gin-gonic/gin"
)
func TestAccessLogRejectsOversizedRequestBody(t *testing.T) {
gin.SetMode(gin.TestMode)
engine := gin.New()
called := false
engine.Use(AccessLog(conf.NewRuntime(nil, &conf.AdminBackend{}), nil, "test"))
engine.POST("/payload", func(c *gin.Context) { called = true })
request := httptest.NewRequest(http.MethodPost, "/payload", strings.NewReader(strings.Repeat("a", int(defaultRequestBodyLimit+1))))
request.Header.Set("Content-Type", "application/json")
response := httptest.NewRecorder()
engine.ServeHTTP(response, request)
if response.Code != http.StatusRequestEntityTooLarge || called {
t.Fatalf("status=%d called=%v body=%s", response.Code, called, response.Body.String())
}
}
func TestAccessLogRejectsOversizedMultipartOnOrdinaryRoute(t *testing.T) {
gin.SetMode(gin.TestMode)
engine := gin.New()
called := false
engine.Use(AccessLog(conf.NewRuntime(nil, &conf.AdminBackend{}), nil, "test"))
engine.POST("/login", func(c *gin.Context) { called = true })
request := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(strings.Repeat("a", int(defaultRequestBodyLimit+1))))
request.ContentLength = -1
request.Header.Set("Content-Type", "multipart/form-data; boundary=test")
response := httptest.NewRecorder()
engine.ServeHTTP(response, request)
if response.Code != http.StatusRequestEntityTooLarge || called {
t.Fatalf("status=%d called=%v body=%s", response.Code, called, response.Body.String())
}
}
func TestAccessLogAllowsMediaLimitOnlyOnUploadRoute(t *testing.T) {
gin.SetMode(gin.TestMode)
engine := gin.New()
called := false
engine.Use(AccessLog(conf.NewRuntime(nil, &conf.AdminBackend{}), nil, "test"))
engine.POST("/api/fileUploadAndDownload/upload", func(c *gin.Context) {
called = true
_, _ = io.Copy(io.Discard, c.Request.Body)
})
request := httptest.NewRequest(http.MethodPost, "/api/fileUploadAndDownload/upload", strings.NewReader(strings.Repeat("a", int(defaultRequestBodyLimit+1))))
request.Header.Set("Content-Type", "multipart/form-data; boundary=test")
response := httptest.NewRecorder()
engine.ServeHTTP(response, request)
if response.Code != http.StatusOK || !called {
t.Fatalf("status=%d called=%v body=%s", response.Code, called, response.Body.String())
}
}
-51
View File
@@ -1,51 +0,0 @@
package middleware
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
"kra/internal/biz"
"kra/internal/conf"
"github.com/gin-gonic/gin"
)
type accessControllerStub struct {
scopeErr error
}
func (*accessControllerStub) Authorize(context.Context, uint, string, string) (bool, error) {
return true, nil
}
func (s *accessControllerStub) ContextWithDataScope(ctx context.Context, authorityID, userID uint) (context.Context, error) {
if s.scopeErr != nil {
return ctx, s.scopeErr
}
return biz.NewDataScopeContext(ctx, biz.DataScope{UserID: userID, AuthorityID: authorityID, Scope: 1, All: true}), nil
}
func TestAccessControlFailsClosedWhenDataScopeResolutionFails(t *testing.T) {
gin.SetMode(gin.TestMode)
engine := gin.New()
engine.Use(func(c *gin.Context) {
c.Set(claimsKey, &biz.AuthClaims{ID: 7, AuthorityID: 888})
c.Next()
})
engine.Use(AccessControl(conf.NewRuntime(nil, &conf.AdminBackend{}), &accessControllerStub{scopeErr: errors.New("database unavailable")}))
called := false
engine.GET("/protected", func(c *gin.Context) { called = true })
response := httptest.NewRecorder()
engine.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/protected", nil))
if called {
t.Fatal("protected handler ran after data-scope resolution failed")
}
if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), "数据权限解析失败") {
t.Fatalf("status=%d body=%s", response.Code, response.Body.String())
}
}
-198
View File
@@ -1,198 +0,0 @@
package middleware
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"kra/internal/conf"
"kra/internal/service"
"kra/internal/service/dto"
"github.com/gin-gonic/gin"
)
const ctxOperationAuditPersistFailedKey = "operation_audit_persist_failed"
func OperationAudit(runtime *conf.Runtime, service *service.AuditRecorder) gin.HandlerFunc {
return func(c *gin.Context) {
path := c.Request.URL.Path
if !recordsOperation(c.Request.Method, path) {
c.Next()
return
}
var requestBody []byte
maxBytes := 1024
if config := runtime.Admin(); config != nil && config.Zap != nil && config.Zap.AccessLogMaxBytes > 0 {
maxBytes = int(config.Zap.AccessLogMaxBytes)
}
if c.Request.Method == http.MethodGet {
requestBody = operationQueryBody(c.Request.URL.RawQuery)
} else if value, ok := c.Get(ctxReqBodyKey); ok {
requestBody = []byte(stringValue(value))
} else if c.Request.Body != nil {
// Fallback for tests or custom middleware chains that omit AccessLog.
if strings.Contains(c.GetHeader("Content-Type"), "multipart/form-data") {
requestBody = []byte("[文件]")
} else {
limited := http.MaxBytesReader(c.Writer, c.Request.Body, defaultRequestBodyLimit)
var err error
requestBody, err = io.ReadAll(limited)
if err != nil {
var tooLarge *http.MaxBytesError
if errors.As(err, &tooLarge) {
c.AbortWithStatus(http.StatusRequestEntityTooLarge)
} else {
c.AbortWithStatus(http.StatusBadRequest)
}
return
}
c.Request.Body = io.NopCloser(bytes.NewReader(requestBody))
}
}
// The reference middleware captures up to the global 1 MiB response
// safety limit and only applies the configured operation-log limit when a
// download response is recorded. Using maxBytes here would silently
// truncate ordinary JSON responses before that decision is possible.
started := time.Now()
c.Next()
userID := uint(0)
if claims := Claims(c); claims != nil {
userID = claims.ID
} else if value, err := strconv.ParseUint(c.GetHeader("x-user-id"), 10, 64); err == nil {
userID = uint(value)
}
requestID, _ := c.Get("request_id")
status := c.Writer.Status()
responseBody := ""
if value, ok := c.Get(ctxRespBufferKey); ok {
if body, bok := value.(*bytes.Buffer); bok {
responseBody = body.String()
}
}
if isDownloadResponse(c) && len(responseBody) > maxBytes {
responseBody = "[超出记录长度]"
}
errorMessage := c.Errors.ByType(gin.ErrorTypePrivate).String()
if err := service.RecordOperationRequest(c.Request.Context(), &dto.OperationRecordRequest{IP: c.ClientIP(), Method: c.Request.Method, Path: path, Status: status, LatencyMS: time.Since(started).Milliseconds(), Agent: c.Request.UserAgent(), ErrorMessage: errorMessage, Body: operationRequestBody(requestBody, c.GetHeader("Content-Type"), maxBytes), Response: responseBody, UserID: userID, RequestID: stringValue(requestID), TraceID: stringValueFromContext(c, "trace_id"), DeviceID: c.GetHeader("X-Device-Id")}); err != nil {
// Preserve the business response, but expose audit persistence failures
// to the global access/error logging pipeline.
c.Set(ctxOperationAuditPersistFailedKey, true)
_ = c.Error(fmt.Errorf("operation audit persist: %w", err))
}
}
}
func operationQueryBody(raw string) []byte {
query, _ := url.QueryUnescape(raw)
values := make(map[string]string)
for _, item := range strings.Split(query, "&") {
parts := strings.Split(item, "=")
if len(parts) == 2 {
values[parts[0]] = parts[1]
}
}
body, _ := json.Marshal(&values)
return body
}
func operationRequestBody(raw []byte, contentType string, limit int) string {
if strings.Contains(contentType, "multipart/form-data") {
return "[文件]"
}
text := string(raw)
if strings.Contains(strings.ToLower(contentType), "json") && text != "" {
var value any
if json.Unmarshal(raw, &value) == nil {
maskOperationBody(value)
if encoded, err := json.Marshal(value); err == nil {
text = string(encoded)
}
}
}
if len(text) > limit {
return "[超出记录长度]"
}
return text
}
func maskOperationBody(value any) {
switch current := value.(type) {
case map[string]any:
for key, item := range current {
normalized := strings.ToLower(strings.ReplaceAll(strings.ReplaceAll(key, "_", ""), "-", ""))
if normalized == "password" || normalized == "newpassword" || normalized == "oldpassword" || normalized == "confirmpassword" || normalized == "passwd" || normalized == "pwd" || normalized == "token" || normalized == "accesstoken" || normalized == "refreshtoken" || normalized == "secret" || normalized == "clientsecret" || normalized == "apikey" || normalized == "privatekey" || normalized == "idcard" {
current[key] = "***"
continue
}
maskOperationBody(item)
}
case []any:
for _, item := range current {
maskOperationBody(item)
}
}
}
func isDownloadResponse(c *gin.Context) bool {
header := c.Writer.Header()
return strings.Contains(header.Get("Pragma"), "public") ||
strings.Contains(header.Get("Expires"), "0") ||
strings.Contains(header.Get("Cache-Control"), "must-revalidate, post-check=0, pre-check=0") ||
strings.Contains(header.Get("Content-Type"), "application/force-download") ||
strings.Contains(header.Get("Content-Type"), "application/octet-stream") ||
strings.Contains(header.Get("Content-Type"), "application/vnd.ms-excel") ||
strings.Contains(header.Get("Content-Type"), "application/download") ||
strings.Contains(header.Get("Content-Disposition"), "attachment") ||
strings.Contains(header.Get("Content-Transfer-Encoding"), "binary")
}
// recordsOperation mirrors the routes on which operation records are enabled.
// Matching by suffix keeps the behavior stable when router-prefix is configured.
func recordsOperation(method, path string) bool {
_, ok := operationRoutes[method+" "+routeSuffix(path)]
return ok
}
func routeSuffix(path string) string {
for _, marker := range []string{"/user/", "/api/", "/casbin/", "/authority/", "/menu/", "/department/", "/position/", "/sysDictionary/", "/sysDictionaryDetail/", "/sysParams/", "/securityConfig/", "/system/", "/sysApiToken/", "/sysVersion/", "/sysExportTemplate/", "/sysError/", "/sysLoginLog/", "/sysOperationRecord/", "/dataAccessLog/", "/timedTask/", "/info/", "/email/"} {
if index := strings.Index(path, marker); index >= 0 {
return path[index:]
}
}
return path
}
var operationRoutes = func() map[string]struct{} {
values := []string{
"POST /user/admin_register", "POST /user/changePassword", "POST /user/setUserAuthority", "DELETE /user/deleteUser", "PUT /user/setUserInfo", "PUT /user/setSelfInfo", "POST /user/setUserAuthorities", "POST /user/setUserDepartments", "POST /user/setUserPositions", "POST /user/resetPassword", "PUT /user/setSelfSetting",
"GET /api/getApiGroups", "GET /api/syncApi", "POST /api/ignoreApi", "POST /api/enterSyncApi", "POST /api/createApi", "POST /api/deleteApi", "POST /api/getApiById", "POST /api/updateApi", "DELETE /api/deleteApisByIds", "POST /api/setApiRoles", "POST /casbin/updateCasbin",
"POST /authority/createAuthority", "POST /authority/deleteAuthority", "PUT /authority/updateAuthority", "POST /authority/copyAuthority", "POST /authority/setDataScope", "POST /authority/setRoleUsers",
"POST /menu/addBaseMenu", "POST /menu/addMenuAuthority", "POST /menu/deleteBaseMenu", "POST /menu/updateBaseMenu", "POST /menu/setMenuRoles",
"POST /department/createDepartment", "PUT /department/updateDepartment", "DELETE /department/deleteDepartment", "POST /department/setDepartmentUsers",
"POST /position/createPosition", "PUT /position/updatePosition", "DELETE /position/deletePosition", "POST /position/setPositionUsers",
"POST /sysDictionary/createSysDictionary", "DELETE /sysDictionary/deleteSysDictionary", "PUT /sysDictionary/updateSysDictionary", "POST /sysDictionary/importSysDictionary", "GET /sysDictionary/exportSysDictionary",
"POST /sysDictionaryDetail/createSysDictionaryDetail", "DELETE /sysDictionaryDetail/deleteSysDictionaryDetail", "PUT /sysDictionaryDetail/updateSysDictionaryDetail",
"POST /sysParams/createSysParams", "DELETE /sysParams/deleteSysParams", "DELETE /sysParams/deleteSysParamsByIds", "PUT /sysParams/updateSysParams",
"POST /securityConfig/setSecurityConfig", "POST /system/setSystemConfig", "POST /system/reloadSystem",
"POST /sysApiToken/createApiToken", "POST /sysApiToken/getApiTokenList", "POST /sysApiToken/deleteApiToken",
"DELETE /sysVersion/deleteSysVersion", "DELETE /sysVersion/deleteSysVersionByIds", "POST /sysVersion/exportVersion", "POST /sysVersion/importVersion",
"POST /sysExportTemplate/createSysExportTemplate", "DELETE /sysExportTemplate/deleteSysExportTemplate", "DELETE /sysExportTemplate/deleteSysExportTemplateByIds", "PUT /sysExportTemplate/updateSysExportTemplate", "POST /sysExportTemplate/importExcel",
"DELETE /sysError/deleteSysError", "DELETE /sysError/deleteSysErrorByIds", "PUT /sysError/updateSysError",
"DELETE /sysLoginLog/deleteLoginLog", "DELETE /sysLoginLog/deleteLoginLogByIds", "DELETE /dataAccessLog/deleteDataAccessLogByIds",
"POST /timedTask/createTimedTask", "PUT /timedTask/updateTimedTask", "DELETE /timedTask/deleteTimedTask", "POST /timedTask/toggleTimedTask", "POST /timedTask/triggerTimedTask",
"POST /info/createInfo", "DELETE /info/deleteInfo", "DELETE /info/deleteInfoByIds", "PUT /info/updateInfo", "POST /email/emailTest", "POST /email/sendEmail",
}
out := make(map[string]struct{}, len(values))
for _, value := range values {
out[value] = struct{}{}
}
return out
}()
-83
View File
@@ -1,83 +0,0 @@
package middleware
import (
"errors"
"net/http"
"strconv"
"strings"
"kra/internal/biz"
"kra/internal/server/httpx"
"kra/internal/service"
"github.com/gin-gonic/gin"
"golang.org/x/sync/singleflight"
)
const claimsKey = "admin_claims"
var refreshTokens singleflight.Group
func Auth(auth *service.AuthService) gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("x-token")
if token == "" {
token, _ = c.Cookie("x-token")
}
if token == "" {
httpx.NoAuth(c, "未登录或非法访问,请登录")
return
}
value, err, _ := refreshTokens.Do(token, func() (any, error) {
return auth.AuthenticateToken(c.Request.Context(), token)
})
if err != nil {
message := "无法处理此token"
switch {
case errors.Is(err, biz.ErrTokenExpired):
message = "登录已过期,请重新登录"
case errors.Is(err, biz.ErrTokenMalformed):
message = "这不是一个token"
case errors.Is(err, biz.ErrTokenSignatureInvalid):
message = "无效签名"
case errors.Is(err, biz.ErrTokenNotValidYet):
message = "token尚未激活"
case errors.Is(err, biz.ErrTokenDisabled):
message = "您的帐户异地登陆或令牌失效"
}
httpx.SetTokenCookie(c, "", -1)
httpx.NoAuth(c, message)
return
}
authentication := value.(*biz.TokenAuthentication)
if authentication.Refreshed != nil {
c.Header("new-token", authentication.Refreshed.Value)
c.Header("new-expires-at", strconv.FormatInt(authentication.Refreshed.ExpiresAt.Unix(), 10))
httpx.SetTokenCookie(c, authentication.Refreshed.Value, int(authentication.Refreshed.TTL.Seconds()))
}
c.Set(claimsKey, authentication.Claims)
c.Next()
}
}
func Claims(c *gin.Context) *biz.AuthClaims {
value, _ := c.Get(claimsKey)
claims, _ := value.(*biz.AuthClaims)
return claims
}
func MustChangePassword() gin.HandlerFunc {
return func(c *gin.Context) {
claims := Claims(c)
if claims == nil || !claims.MustChangePwd {
c.Next()
return
}
path := strings.TrimSuffix(c.Request.URL.Path, "/")
if strings.HasSuffix(path, "/user/changePassword") || strings.HasSuffix(path, "/user/getUserInfo") || strings.HasSuffix(path, "/jwt/jsonInBlacklist") {
c.Next()
return
}
c.AbortWithStatusJSON(http.StatusConflict, httpx.Response{Code: httpx.CodePasswordChangeRequired, Data: gin.H{"needChangePassword": true}, Msg: "密码已过期,请先修改密码"})
}
}
-74
View File
@@ -1,74 +0,0 @@
package middleware
import (
"bytes"
"encoding/json"
"strings"
"github.com/gin-gonic/gin"
)
// The global access logger is the single request/response capture point.
// OperationAudit consumes these values after the handler returns, avoiding a
// second body read/writer wrapper.
const (
ctxReqBodyKey = "kra_req_body"
ctxRespBufferKey = "kra_resp_buffer"
)
type captureWriter struct {
gin.ResponseWriter
body bytes.Buffer
maxBytes int
}
func (w *captureWriter) Write(data []byte) (int, error) {
limit := w.maxBytes
if limit <= 0 {
limit = 1024
}
if w.body.Len() < limit {
remaining := limit - w.body.Len()
if len(data) > remaining {
_, _ = w.body.Write(data[:remaining])
} else {
_, _ = w.body.Write(data)
}
}
return w.ResponseWriter.Write(data)
}
func redactJSON(raw []byte, contentType string, limit int) string {
if len(raw) == 0 {
return ""
}
if limit <= 0 {
limit = 1024
}
text := string(raw)
if !strings.Contains(strings.ToLower(contentType), "json") {
if len(text) > limit {
return "[超出记录长度]"
}
return text
}
var value any
if json.Unmarshal(raw, &value) != nil {
if len(text) > limit {
return "[超出记录长度]"
}
return text
}
maskOperationBody(value)
encoded, _ := json.Marshal(value)
if len(encoded) > limit {
return "[超出记录长度]"
}
return string(encoded)
}
func stringValue(value any) string { text, _ := value.(string); return text }
func isBootstrapPath(path string) bool {
return strings.HasSuffix(path, "/health") || strings.Contains(path, "/init/")
}
-65
View File
@@ -1,65 +0,0 @@
package middleware
import (
"net/http"
"strings"
"kra/internal/conf"
"github.com/gin-gonic/gin"
)
const (
defaultCORSHeaders = "Content-Type,AccessToken,X-CSRF-Token, Authorization, Token,X-Token,X-User-Id"
defaultCORSMethods = "POST, GET, OPTIONS,DELETE,PUT"
defaultCORSExpose = "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type, New-Token, New-Expires-At"
)
// CORS applies the current administration CORS rules on every request so a
// configuration reload takes effect without rebuilding the Gin engine.
func CORS(runtime *conf.Runtime) gin.HandlerFunc {
return func(c *gin.Context) {
config := runtime.Admin()
if config == nil || config.Cors == nil {
c.Next()
return
}
mode := strings.TrimSpace(config.Cors.Mode)
origin := c.GetHeader("Origin")
corsHandled := false
if mode == "allow-all" {
setCORSHeaders(c, origin, defaultCORSHeaders, defaultCORSMethods, defaultCORSExpose, true)
corsHandled = true
} else if rule := matchingCORSRule(config.Cors.Whitelist, origin); rule != nil {
setCORSHeaders(c, rule.AllowOrigin, rule.AllowHeaders, rule.AllowMethods, rule.ExposeHeaders, rule.AllowCredentials)
corsHandled = true
} else if mode == "strict-whitelist" && !(c.Request.Method == http.MethodGet && c.Request.URL.Path == "/health") {
c.AbortWithStatus(http.StatusForbidden)
return
}
if corsHandled && c.Request.Method == http.MethodOptions {
c.AbortWithStatus(http.StatusNoContent)
return
}
c.Next()
}
}
func matchingCORSRule(rules []*conf.AdminBackend_CORSRule, origin string) *conf.AdminBackend_CORSRule {
for _, rule := range rules {
if rule != nil && origin == rule.AllowOrigin {
return rule
}
}
return nil
}
func setCORSHeaders(c *gin.Context, origin, headers, methods, expose string, credentials bool) {
c.Header("Access-Control-Allow-Origin", origin)
c.Header("Access-Control-Allow-Headers", headers)
c.Header("Access-Control-Allow-Methods", methods)
c.Header("Access-Control-Expose-Headers", expose)
if credentials {
c.Header("Access-Control-Allow-Credentials", "true")
}
}
-39
View File
@@ -1,39 +0,0 @@
package middleware
import (
"net/http"
"net/http/httptest"
"testing"
"kra/internal/conf"
"github.com/gin-gonic/gin"
)
func runCORSTest(t *testing.T, admin *conf.AdminBackend, method, origin string) *httptest.ResponseRecorder {
t.Helper()
engine := gin.New()
engine.Use(CORS(conf.NewRuntime(nil, admin)))
engine.Any("/test", func(c *gin.Context) { c.Status(http.StatusOK) })
request := httptest.NewRequest(method, "/test", nil)
if origin != "" {
request.Header.Set("Origin", origin)
}
response := httptest.NewRecorder()
engine.ServeHTTP(response, request)
return response
}
func TestCORSDoesNotConsumeUnmatchedWhitelistPreflight(t *testing.T) {
response := runCORSTest(t, &conf.AdminBackend{Cors: &conf.AdminBackend_CORS{Mode: "whitelist"}}, http.MethodOptions, "https://unknown.example")
if response.Code != http.StatusOK {
t.Fatalf("unmatched whitelist preflight status = %d, want %d", response.Code, http.StatusOK)
}
}
func TestCORSConsumesMatchedWhitelistPreflight(t *testing.T) {
response := runCORSTest(t, &conf.AdminBackend{Cors: &conf.AdminBackend_CORS{Mode: "whitelist", Whitelist: []*conf.AdminBackend_CORSRule{{AllowOrigin: "https://admin.example"}}}}, http.MethodOptions, "https://admin.example")
if response.Code != http.StatusNoContent {
t.Fatalf("matched whitelist preflight status = %d, want %d", response.Code, http.StatusNoContent)
}
}
-78
View File
@@ -1,78 +0,0 @@
package middleware
import (
"bytes"
"encoding/json"
"log/slog"
"strings"
"kra/internal/server/httpx"
"github.com/gin-gonic/gin"
)
// ErrorAudit emits a structured Error-level log for unexpected HTTP failures.
// The logging core is the single persistence path for sys_error, matching the
// reference behavior and avoiding duplicate rows for HTTP failures.
func ErrorAudit(logger *slog.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
privateErrors := strings.TrimSpace(c.Errors.ByType(gin.ErrorTypePrivate).String())
auditPersistFailed, _ := c.Get(ctxOperationAuditPersistFailedKey)
// sysError writes must never audit themselves. Log-viewer failures are
// already recorded by the handler with the underlying filesystem error;
// emitting again from the response envelope would duplicate both the
// classified error file and the sys_error row.
if auditPersistFailed != true && (strings.Contains(c.Request.URL.Path, "/sysError/") || strings.Contains(c.Request.URL.Path, "/logViewer/") || c.Writer.Status() >= 300 && c.Writer.Status() < 500) {
return
}
var response httpx.Response
var body []byte
if value, ok := c.Get(ctxRespBufferKey); ok {
if buffer, valid := value.(*bytes.Buffer); valid {
body = buffer.Bytes()
}
}
if json.Unmarshal(body, &response) != nil && privateErrors == "" {
return
}
if response.Code == httpx.CodeSuccess && privateErrors == "" {
return
}
if privateErrors == "" && expectedClientFailure(response.Msg) {
return
}
errorMessage := response.Msg
if privateErrors != "" {
errorMessage = privateErrors
}
requestID, _ := c.Get("request_id")
if logger != nil {
logger.ErrorContext(c.Request.Context(), "请求处理失败", "mod", failureLogModule(c.Request.URL.Path), "path", c.Request.URL.Path, "method", c.Request.Method, "status", c.Writer.Status(), "error", errorMessage, "request_id", stringValue(requestID), "trace_id", stringValueFromContext(c, "trace_id"))
}
}
}
func failureLogModule(path string) string {
for _, marker := range []string{"/fileUploadAndDownload/", "/mediaUpload/", "/attachmentCategory/"} {
if strings.Contains(path, marker) {
return "upload"
}
}
if strings.Contains(path, "/timedTask/") {
return "timedTask"
}
if strings.Contains(path, "/logViewer/") {
return "log-viewer"
}
return "biz"
}
func expectedClientFailure(message string) bool {
for _, value := range []string{"参数错误", "请输入用户名和密码", "验证码错误", "用户名不存在或者密码错误", "用户被禁止登录", "账号已锁定", "权限不足", "密码已过期", "未登录", "token", "令牌失效"} {
if strings.Contains(message, value) {
return true
}
}
return false
}
@@ -1,111 +0,0 @@
package middleware
import (
"bytes"
"errors"
"log/slog"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/gin-gonic/gin"
)
func runErrorAudit(t *testing.T, path, response string, logger *slog.Logger) {
t.Helper()
gin.SetMode(gin.TestMode)
engine := gin.New()
engine.Use(ErrorAudit(logger))
engine.GET(path, func(c *gin.Context) {
c.Set(ctxRespBufferKey, bytes.NewBufferString(response))
c.Set("request_id", "request-1")
c.Set("trace_id", "trace-1")
c.Status(http.StatusOK)
})
request := httptest.NewRequest(http.MethodGet, path, nil)
responseRecorder := httptest.NewRecorder()
engine.ServeHTTP(responseRecorder, request)
}
func TestErrorAuditEmitsUnexpectedBusinessFailure(t *testing.T) {
var output bytes.Buffer
logger := slog.New(slog.NewJSONHandler(&output, nil))
runErrorAudit(t, "/test", `{"code":7,"data":{},"msg":"数据库写入失败"}`, logger)
if text := output.String(); !strings.Contains(text, `"mod":"biz"`) || !strings.Contains(text, `"error":"数据库写入失败"`) {
t.Fatalf("unexpected error log: %s", text)
}
}
func TestErrorAuditUsesFeatureModule(t *testing.T) {
var output bytes.Buffer
logger := slog.New(slog.NewJSONHandler(&output, nil))
runErrorAudit(t, "/timedTask/triggerTimedTask", `{"code":7,"data":{},"msg":"任务执行失败"}`, logger)
if text := output.String(); !strings.Contains(text, `"mod":"timedTask"`) {
t.Fatalf("unexpected feature log module: %s", text)
}
}
func TestErrorAuditSkipsExpectedClientFailure(t *testing.T) {
var output bytes.Buffer
logger := slog.New(slog.NewJSONHandler(&output, nil))
runErrorAudit(t, "/test", `{"code":7,"data":{},"msg":"参数错误"}`, logger)
if output.Len() != 0 {
t.Fatalf("expected no error log, got %s", output.String())
}
}
func TestErrorAuditAllowsNilLogger(t *testing.T) {
runErrorAudit(t, "/test", `{"code":7,"data":{},"msg":"数据库写入失败"}`, nil)
}
func TestErrorAuditSkipsLogViewerFailureAlreadyLoggedByHandler(t *testing.T) {
var output bytes.Buffer
logger := slog.New(slog.NewJSONHandler(&output, nil))
runErrorAudit(t, "/logViewer/content", `{"code":7,"data":{},"msg":"日志文件不可读取"}`, logger)
if output.Len() != 0 {
t.Fatalf("log viewer failure must not be emitted twice, got %s", output.String())
}
}
func TestErrorAuditEmitsPrivateMiddlewareErrorOnSuccessResponse(t *testing.T) {
var output bytes.Buffer
logger := slog.New(slog.NewJSONHandler(&output, nil))
gin.SetMode(gin.TestMode)
engine := gin.New()
engine.Use(ErrorAudit(logger))
engine.GET("/test", func(c *gin.Context) {
c.Error(errors.New("operation audit persist: database unavailable"))
c.JSON(http.StatusOK, gin.H{"code": 0, "data": gin.H{}, "msg": "成功"})
})
response := httptest.NewRecorder()
engine.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/test", nil))
if !strings.Contains(output.String(), "operation audit persist: database unavailable") {
t.Fatalf("private middleware error was not logged: %s", output.String())
}
}
func TestErrorAuditEmitsOperationAuditFailureForSysErrorRoute(t *testing.T) {
var output bytes.Buffer
logger := slog.New(slog.NewJSONHandler(&output, nil))
gin.SetMode(gin.TestMode)
engine := gin.New()
engine.Use(ErrorAudit(logger))
engine.PUT("/sysError/updateSysError", func(c *gin.Context) {
c.Set(ctxOperationAuditPersistFailedKey, true)
c.Error(errors.New("operation audit persist: database unavailable"))
c.JSON(http.StatusOK, gin.H{"code": 0, "data": gin.H{}, "msg": "成功"})
})
response := httptest.NewRecorder()
engine.ServeHTTP(response, httptest.NewRequest(http.MethodPut, "/sysError/updateSysError", nil))
if !strings.Contains(output.String(), "operation audit persist: database unavailable") {
t.Fatalf("sysError operation audit failure was not logged: %s", output.String())
}
}
-47
View File
@@ -1,47 +0,0 @@
package middleware
import (
"net/http"
"strings"
"time"
"kra/internal/server/httpx"
"kra/internal/service"
"github.com/gin-gonic/gin"
)
func SecurityRateLimit(settings *service.SecurityService) gin.HandlerFunc {
return func(c *gin.Context) {
path := strings.TrimSuffix(c.Request.URL.Path, "/")
if !strings.HasSuffix(path, "/base/login") && !strings.HasSuffix(path, "/base/captcha") {
c.Next()
return
}
config, err := settings.CurrentSecurity(c.Request.Context())
if err != nil || config == nil {
c.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{"code": httpx.CodeError, "msg": "安全服务暂不可用"})
return
}
if !config.LimitEnable {
c.Next()
return
}
window := config.LimitWindow
if window < 1 {
window = 60
}
key := "KRA_SecLimit" + c.ClientIP() + c.FullPath()
count, cacheErr := settings.IncrementRateLimit(c.Request.Context(), key, time.Duration(window)*time.Second)
if cacheErr != nil {
c.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{"code": httpx.CodeError, "msg": "安全服务暂不可用"})
return
}
if int(count) > config.LimitCount {
c.JSON(200, gin.H{"code": httpx.CodeError, "msg": "请求太过频繁,请稍后再试"})
c.Abort()
return
}
c.Next()
}
}
@@ -1,70 +0,0 @@
package middleware
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"kra/internal/biz"
"kra/internal/service"
"github.com/gin-gonic/gin"
)
type rateLimitSecurityRepo struct{}
func (rateLimitSecurityRepo) SecurityConfig(context.Context) (*biz.SecurityConfig, error) {
return &biz.SecurityConfig{LimitEnable: true, LimitWindow: 60, LimitCount: 1}, nil
}
func (rateLimitSecurityRepo) SaveSecurityConfig(context.Context, *biz.SecurityConfig) error {
return nil
}
func (rateLimitSecurityRepo) BackfillPasswordUpdatedAt(context.Context, time.Time) error {
return nil
}
type rateLimitCache struct{ err error }
func (rateLimitCache) Get(context.Context, string) (string, bool, error) { return "", false, nil }
func (rateLimitCache) Set(context.Context, string, string, time.Duration) error { return nil }
func (rateLimitCache) Delete(context.Context, string) error { return nil }
func (c rateLimitCache) Increment(context.Context, string, time.Duration) (int64, error) {
return 2, c.err
}
func TestSecurityRateLimitMatchesResponseContract(t *testing.T) {
gin.SetMode(gin.TestMode)
settings := service.NewSecurityService(biz.NewSecurityUsecase(rateLimitSecurityRepo{}, rateLimitCache{}, nil, nil))
engine := gin.New()
engine.Use(SecurityRateLimit(settings))
engine.POST("/base/login", func(c *gin.Context) { c.Status(http.StatusNoContent) })
response := httptest.NewRecorder()
engine.ServeHTTP(response, httptest.NewRequest(http.MethodPost, "/base/login", nil))
if response.Code != http.StatusOK {
t.Fatalf("status = %d, body=%s", response.Code, response.Body.String())
}
body := response.Body.String()
if !strings.Contains(body, `"code":7`) || !strings.Contains(body, `"msg":"请求太过频繁,请稍后再试"`) || strings.Contains(body, `"data"`) {
t.Fatalf("unexpected rate-limit response: %s", body)
}
}
func TestSecurityRateLimitFailsClosedWhenCacheIsUnavailable(t *testing.T) {
gin.SetMode(gin.TestMode)
settings := service.NewSecurityService(biz.NewSecurityUsecase(rateLimitSecurityRepo{}, rateLimitCache{err: errors.New("cache unavailable")}, nil, nil))
engine := gin.New()
called := false
engine.Use(SecurityRateLimit(settings))
engine.POST("/base/login", func(c *gin.Context) { called = true })
response := httptest.NewRecorder()
engine.ServeHTTP(response, httptest.NewRequest(http.MethodPost, "/base/login", nil))
if response.Code != http.StatusServiceUnavailable || called {
t.Fatalf("status=%d called=%v body=%s", response.Code, called, response.Body.String())
}
}
-44
View File
@@ -1,44 +0,0 @@
package middleware
import (
"log/slog"
"net"
"net/http"
"net/http/httputil"
"os"
"runtime/debug"
"strings"
"github.com/gin-gonic/gin"
)
func Recovery(logger *slog.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
panicValue := recover()
if panicValue == nil {
return
}
brokenPipe := false
if networkError, ok := panicValue.(*net.OpError); ok {
if syscallError, ok := networkError.Err.(*os.SyscallError); ok {
message := strings.ToLower(syscallError.Error())
brokenPipe = strings.Contains(message, "broken pipe") || strings.Contains(message, "connection reset by peer")
}
}
request, _ := httputil.DumpRequest(c.Request, false)
if logger != nil {
logger.ErrorContext(c.Request.Context(), "recovery from panic", "mod", "http", "error", panicValue, "request", string(request), "stack", string(debug.Stack()))
}
if brokenPipe {
if err, ok := panicValue.(error); ok {
_ = c.Error(err)
}
c.Abort()
return
}
c.AbortWithStatus(http.StatusInternalServerError)
}()
c.Next()
}
}
-102
View File
@@ -1,102 +0,0 @@
package middleware
import (
"crypto/rand"
"encoding/hex"
"strings"
"kra/internal/logging"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
func randomHex(bytes int) string {
value := make([]byte, bytes)
_, _ = rand.Read(value)
return hex.EncodeToString(value)
}
func RequestMeta() gin.HandlerFunc {
return func(c *gin.Context) {
requestID := c.GetHeader("X-Request-Id")
if !saneHeaderID(requestID) {
requestID = uuid.NewString()
}
traceID, parentSpanID := "", ""
if upstreamTraceID, upstreamSpanID, ok := parseTraceparent(c.GetHeader("traceparent")); ok {
traceID, parentSpanID = upstreamTraceID, upstreamSpanID
} else if candidate := c.GetHeader("X-Trace-Id"); saneHeaderID(candidate) {
traceID = candidate
}
if traceID == "" {
traceID = randomHex(16)
}
spanID := randomHex(8)
c.Header("X-Request-Id", requestID)
c.Header("X-Trace-Id", traceID)
if validTraceID(traceID) {
c.Header("traceparent", "00-"+traceID+"-"+spanID+"-01")
}
c.Set("request_id", requestID)
c.Set("trace_id", traceID)
c.Set("span_id", spanID)
c.Set("parent_span_id", parentSpanID)
c.Request = c.Request.WithContext(logging.WithContextFields(c.Request.Context(), &logging.ContextFields{
RequestID: requestID, TraceID: traceID, SpanID: spanID, ParentSpanID: parentSpanID,
DeviceID: c.GetHeader("X-Device-Id"), ClientIP: c.ClientIP(),
HTTPMethod: c.Request.Method, HTTPPath: c.Request.URL.Path,
}))
c.Next()
}
}
func parseTraceparent(value string) (traceID, parentSpanID string, ok bool) {
parts := strings.Split(value, "-")
if len(parts) < 4 {
return "", "", false
}
version, traceID, parentSpanID, flags := parts[0], parts[1], parts[2], parts[3]
if !lowerHex(version, 2) || version == "ff" || version == "00" && len(parts) != 4 ||
!validTraceID(traceID) || !lowerHex(parentSpanID, 16) || allZero(parentSpanID) || !lowerHex(flags, 2) {
return "", "", false
}
return traceID, parentSpanID, true
}
func saneHeaderID(value string) bool {
if value == "" || len(value) > 64 {
return false
}
for index := 0; index < len(value); index++ {
if value[index] <= 0x20 || value[index] > 0x7e {
return false
}
}
return true
}
func validTraceID(value string) bool { return lowerHex(value, 32) && !allZero(value) }
func lowerHex(value string, length int) bool {
if len(value) != length {
return false
}
for index := 0; index < len(value); index++ {
if current := value[index]; current < '0' || current > '9' {
if current < 'a' || current > 'f' {
return false
}
}
}
return true
}
func allZero(value string) bool {
for index := 0; index < len(value); index++ {
if value[index] != '0' {
return false
}
}
return true
}
-20
View File
@@ -1,20 +0,0 @@
package router
import (
"kra/internal/server/handler"
"github.com/gin-gonic/gin"
)
func RegisterAnnouncement(private, public *gin.RouterGroup, h *handler.Announcement) {
privateInfo := private.Group("/info")
privateInfo.POST("/createInfo", h.Create)
privateInfo.DELETE("/deleteInfo", h.Delete)
privateInfo.DELETE("/deleteInfoByIds", h.DeleteByIDs)
privateInfo.PUT("/updateInfo", h.Update)
privateInfo.GET("/findInfo", h.Find)
privateInfo.GET("/getInfoList", h.List)
privateInfo.GET("/getInfoDataSource", h.DataSource)
publicInfo := public.Group("/info")
publicInfo.GET("/getInfoPublic", h.Public)
}
-28
View File
@@ -1,28 +0,0 @@
package router
import (
"kra/internal/server/handler"
"github.com/gin-gonic/gin"
)
func RegisterAPI(group, public *gin.RouterGroup, engine *gin.Engine, h *handler.API) {
api := group.Group("/api")
api.POST("/getApiList", h.List)
api.POST("/getAllApis", h.All)
api.POST("/createApi", h.Create)
api.POST("/updateApi", h.Update)
api.POST("/deleteApi", h.Delete)
api.DELETE("/deleteApisByIds", h.DeleteByIDs)
api.POST("/getApiById", h.Find)
api.GET("/getApiGroups", h.Groups)
api.GET("/getApiRoles", h.Roles)
api.POST("/setApiRoles", h.SetRoles)
api.GET("/syncApi", h.Sync(engine))
api.POST("/ignoreApi", h.Ignore)
api.POST("/enterSyncApi", h.ApplySync)
public.Group("/api").GET("/freshCasbin", h.FreshCasbin)
casbin := group.Group("/casbin")
casbin.POST("/updateCasbin", h.SetPolicyPaths)
casbin.POST("/getPolicyPathByAuthorityId", h.PolicyPaths)
}
-14
View File
@@ -1,14 +0,0 @@
package router
import (
"kra/internal/server/handler"
"github.com/gin-gonic/gin"
)
func RegisterAPIToken(group *gin.RouterGroup, handler *handler.APIToken) {
router := group.Group("/sysApiToken")
router.POST("/createApiToken", handler.Create)
router.POST("/getApiTokenList", handler.List)
router.POST("/deleteApiToken", handler.Delete)
}
-34
View File
@@ -1,34 +0,0 @@
package router
import (
"kra/internal/server/handler"
"github.com/gin-gonic/gin"
)
func RegisterAudit(group, public *gin.RouterGroup, h *handler.Audit) {
operations := group.Group("/sysOperationRecord")
operations.GET("/getSysOperationRecordList", h.Operations)
operations.GET("/findSysOperationRecord", h.Operation)
operations.DELETE("/deleteSysOperationRecord", h.DeleteOperation)
operations.DELETE("/deleteSysOperationRecordByIds", h.DeleteOperations)
logins := group.Group("/sysLoginLog")
logins.GET("/getLoginLogList", h.Logins)
logins.GET("/findLoginLog", h.Login)
logins.DELETE("/deleteLoginLog", h.DeleteLogin)
logins.DELETE("/deleteLoginLogByIds", h.DeleteLogins)
access := group.Group("/dataAccessLog")
access.POST("/getDataAccessLogList", h.DataAccess)
access.DELETE("/deleteDataAccessLogByIds", h.DeleteDataAccess)
viewer := group.Group("/logViewer")
viewer.GET("/dates", h.LogDates)
viewer.GET("/files", h.LogFiles)
viewer.GET("/content", h.LogContent)
errors := group.Group("/sysError")
errors.DELETE("/deleteSysError", h.DeleteError)
errors.DELETE("/deleteSysErrorByIds", h.DeleteErrors)
errors.PUT("/updateSysError", h.UpdateError)
errors.GET("/findSysError", h.Error)
errors.GET("/getSysErrorList", h.Errors)
public.Group("/sysError").POST("/createSysError", h.CreateError)
}
-20
View File
@@ -1,20 +0,0 @@
package router
import (
"kra/internal/server/handler"
"github.com/gin-gonic/gin"
)
func RegisterAuthority(group *gin.RouterGroup, h *handler.Authority) {
router := group.Group("/authority")
router.POST("/getAuthorityList", h.List)
router.POST("/createAuthority", h.Create)
router.POST("/copyAuthority", h.Copy)
router.PUT("/updateAuthority", h.Update)
router.POST("/deleteAuthority", h.Delete)
router.POST("/setRoleUsers", h.SetUsers)
router.GET("/getUsersByAuthority", h.Users)
router.POST("/setDataScope", h.SetDataScope)
router.GET("/getDataScopeDepts", h.DataScopeDepartments)
}
-29
View File
@@ -1,29 +0,0 @@
package router
import (
"kra/internal/server/handler"
"github.com/gin-gonic/gin"
)
func RegisterDictionary(group *gin.RouterGroup, h *handler.Dictionary) {
dictionaries := group.Group("/sysDictionary")
dictionaries.POST("/createSysDictionary", h.Create)
dictionaries.PUT("/updateSysDictionary", h.Update)
dictionaries.DELETE("/deleteSysDictionary", h.Delete)
dictionaries.GET("/findSysDictionary", h.Find)
dictionaries.GET("/getSysDictionaryList", h.List(false))
dictionaries.GET("/getSysDictionaryListWithDetails", h.List(true))
dictionaries.GET("/exportSysDictionary", h.Export)
dictionaries.POST("/importSysDictionary", h.Import)
details := group.Group("/sysDictionaryDetail")
details.POST("/createSysDictionaryDetail", h.CreateDetail)
details.PUT("/updateSysDictionaryDetail", h.UpdateDetail)
details.DELETE("/deleteSysDictionaryDetail", h.DeleteDetail)
details.GET("/findSysDictionaryDetail", h.FindDetail)
details.GET("/getSysDictionaryDetailList", h.Details)
details.GET("/getDictionaryTreeList", h.Tree(false))
details.GET("/getDictionaryTreeListByType", h.Tree(true))
details.GET("/getDictionaryDetailsByParent", h.DetailsByParent)
details.GET("/getDictionaryPath", h.Path)
}
-13
View File
@@ -1,13 +0,0 @@
package router
import (
"kra/internal/server/handler"
"github.com/gin-gonic/gin"
)
func RegisterEmail(group *gin.RouterGroup, h *handler.Email) {
email := group.Group("/email")
email.POST("/emailTest", h.Test)
email.POST("/sendEmail", h.Send)
}
-24
View File
@@ -1,24 +0,0 @@
package router
import (
"kra/internal/server/handler"
"github.com/gin-gonic/gin"
)
func RegisterExport(group, public *gin.RouterGroup, h *handler.Export) {
router := group.Group("/sysExportTemplate")
router.POST("/createSysExportTemplate", h.Create)
router.PUT("/updateSysExportTemplate", h.Update)
router.DELETE("/deleteSysExportTemplate", h.Delete)
router.DELETE("/deleteSysExportTemplateByIds", h.DeleteMany)
router.GET("/findSysExportTemplate", h.Find)
router.GET("/getSysExportTemplateList", h.List)
router.GET("/previewSQL", h.Preview)
router.GET("/exportExcel", h.Issue(false))
router.GET("/exportTemplate", h.Issue(true))
router.POST("/importExcel", h.Import)
pub := public.Group("/sysExportTemplate")
pub.GET("/exportExcelByToken", h.Download(false))
pub.GET("/exportTemplateByToken", h.Download(true))
}
-28
View File
@@ -1,28 +0,0 @@
package router
import (
"kra/internal/server/handler"
"github.com/gin-gonic/gin"
)
func RegisterMedia(group *gin.RouterGroup, h *handler.Media) {
files := group.Group("/fileUploadAndDownload")
files.POST("/upload", h.Upload)
files.POST("/getFileList", h.List)
files.POST("/deleteFile", h.Delete)
files.POST("/deleteFiles", h.DeleteMany)
files.GET("/findFile", h.Find)
files.POST("/editFileName", h.Rename)
files.POST("/importURL", h.ImportURLs)
files.POST("/listOssFiles", h.Storage)
categories := group.Group("/attachmentCategory")
categories.GET("/getCategoryList", h.Categories)
categories.POST("/addCategory", h.SaveCategory)
categories.POST("/deleteCategory", h.DeleteCategory)
upload := group.Group("/mediaUpload")
upload.POST("/init", h.InitUpload)
upload.POST("/chunk", h.SaveChunk)
upload.POST("/complete", h.CompleteUpload)
upload.DELETE("/:uploadId", h.CancelUpload)
}
-21
View File
@@ -1,21 +0,0 @@
package router
import (
"kra/internal/server/handler"
"github.com/gin-gonic/gin"
)
func RegisterMenu(group *gin.RouterGroup, h *handler.Menu) {
router := group.Group("/menu")
router.POST("/getMenuList", h.List)
router.POST("/getBaseMenuTree", h.Tree)
router.POST("/addBaseMenu", h.Create)
router.POST("/updateBaseMenu", h.Update)
router.POST("/deleteBaseMenu", h.Delete)
router.POST("/getBaseMenuById", h.Find)
router.POST("/addMenuAuthority", h.SetAuthorityMenus)
router.POST("/getMenuAuthority", h.AuthorityMenus)
router.GET("/getMenuRoles", h.RoleIDs)
router.POST("/setMenuRoles", h.SetRoles)
}
-29
View File
@@ -1,29 +0,0 @@
package router
import (
"kra/internal/server/handler"
"github.com/gin-gonic/gin"
)
func RegisterOrganization(group *gin.RouterGroup, h *handler.Organization) {
departments := group.Group("/department")
departments.POST("/getDepartmentList", h.ListDepartments)
departments.POST("/createDepartment", h.CreateDepartment)
departments.PUT("/updateDepartment", h.UpdateDepartment)
departments.DELETE("/deleteDepartment", h.DeleteDepartment)
departments.GET("/findDepartment", h.FindDepartment)
departments.GET("/getDepartmentUsers", h.DepartmentUsers)
departments.POST("/setDepartmentUsers", h.SetDepartmentUsers)
group.POST("/user/setUserDepartments", h.SetUserDepartments)
positions := group.Group("/position")
positions.POST("/getPositionList", h.ListPositions)
positions.POST("/createPosition", h.CreatePosition)
positions.PUT("/updatePosition", h.UpdatePosition)
positions.DELETE("/deletePosition", h.DeletePosition)
positions.GET("/findPosition", h.FindPosition)
positions.GET("/getPositionUsers", h.PositionUsers)
positions.POST("/setPositionUsers", h.SetPositionUsers)
group.POST("/user/setUserPositions", h.SetUserPositions)
}
-18
View File
@@ -1,18 +0,0 @@
package router
import (
"kra/internal/server/handler"
"github.com/gin-gonic/gin"
)
func RegisterParameter(group *gin.RouterGroup, handler *handler.Parameter) {
router := group.Group("/sysParams")
router.POST("/createSysParams", handler.Create)
router.PUT("/updateSysParams", handler.Update)
router.DELETE("/deleteSysParams", handler.Delete)
router.DELETE("/deleteSysParamsByIds", handler.DeleteMany)
router.GET("/findSysParams", handler.Find)
router.GET("/getSysParam", handler.Get)
router.GET("/getSysParamsList", handler.List)
}
-18
View File
@@ -1,18 +0,0 @@
package router
import (
"github.com/gin-gonic/gin"
"kra/internal/server/handler"
)
func RegisterPayment(group, public *gin.RouterGroup, h *handler.Payment) {
payment := group.Group("/payment")
payment.GET("/configs", h.Configs)
payment.POST("/config", h.SaveConfig)
payment.GET("/orders", h.Orders)
payment.POST("/order", h.Order)
payment.POST("/create", h.Create)
payment.POST("/query", h.Query)
payment.POST("/refund", h.Refund)
public.POST("/payment/callback/:provider", h.Callback)
}
-14
View File
@@ -1,14 +0,0 @@
package router
import (
"kra/internal/server/handler"
"github.com/gin-gonic/gin"
)
func RegisterPermission(group *gin.RouterGroup, h *handler.Permission) {
buttons := group.Group("/authorityBtn")
buttons.POST("/getAuthorityBtn", h.Buttons)
buttons.POST("/setAuthorityBtn", h.SetButtons)
buttons.POST("/canRemoveAuthorityBtn", h.CanRemoveButton)
}
-16
View File
@@ -1,16 +0,0 @@
package router
import (
"kra/internal/server/handler"
"github.com/gin-gonic/gin"
)
func RegisterPublic(group *gin.RouterGroup, engine *gin.Engine, handler *handler.Public) {
base := group.Group("/base")
base.POST("/captcha", handler.Captcha)
base.POST("/login", handler.Login)
init := group.Group("/init")
init.POST("/checkdb", handler.CheckDatabase)
init.POST("/initdb", handler.InitializeDatabase(engine))
}
-19
View File
@@ -1,19 +0,0 @@
package router
import (
"kra/internal/server/handler"
"github.com/gin-gonic/gin"
)
func RegisterSystemConfig(group *gin.RouterGroup, handler *handler.SystemConfig) {
security := group.Group("/securityConfig")
security.GET("/getSecurityConfig", handler.GetSecurity)
security.POST("/setSecurityConfig", handler.SetSecurity)
system := group.Group("/system")
system.POST("/getSystemConfig", handler.Get)
system.POST("/setSystemConfig", handler.Set)
system.POST("/reloadSystem", handler.Reload)
system.POST("/getServerInfo", handler.ServerInfo)
}
-20
View File
@@ -1,20 +0,0 @@
package router
import (
"kra/internal/server/handler"
"github.com/gin-gonic/gin"
)
func RegisterTask(group *gin.RouterGroup, h *handler.Task) {
router := group.Group("/timedTask")
router.POST("/createTimedTask", h.Create)
router.PUT("/updateTimedTask", h.Update)
router.DELETE("/deleteTimedTask", h.Delete)
router.POST("/toggleTimedTask", h.Toggle)
router.POST("/triggerTimedTask", h.Trigger)
router.GET("/getTimedTaskList", h.List)
router.GET("/getTimedTaskLogList", h.Logs)
router.GET("/getRegisteredMethods", h.Methods)
router.GET("/alertStream", h.AlertStream)
}
-30
View File
@@ -1,30 +0,0 @@
package router
import (
"kra/internal/server/handler"
"github.com/gin-gonic/gin"
)
func RegisterUser(group *gin.RouterGroup, handler *handler.User) {
user := group.Group("/user")
user.POST("/getUserList", handler.List)
user.POST("/admin_register", handler.Create)
user.PUT("/setUserInfo", handler.Update)
user.PUT("/setSelfInfo", handler.UpdateSelf)
user.DELETE("/deleteUser", handler.Delete)
user.POST("/resetPassword", handler.ResetPassword)
user.POST("/changePassword", handler.ChangePassword)
user.PUT("/setSelfSetting", handler.SetSelfSetting)
user.POST("/setUserAuthorities", handler.SetAuthorities)
user.POST("/setUserAuthority", handler.SwitchAuthority)
user.GET("/getUserInfo", handler.Get)
}
func RegisterNavigation(group *gin.RouterGroup, handler *handler.Navigation) {
group.Group("/menu").POST("/getMenu", handler.Menu)
}
func RegisterSession(group *gin.RouterGroup, handler *handler.Session) {
group.Group("/jwt").POST("/jsonInBlacklist", handler.Logout)
}
-18
View File
@@ -1,18 +0,0 @@
package router
import (
"kra/internal/server/handler"
"github.com/gin-gonic/gin"
)
func RegisterVersion(group *gin.RouterGroup, h *handler.Version) {
router := group.Group("/sysVersion")
router.DELETE("/deleteSysVersion", h.Delete)
router.DELETE("/deleteSysVersionByIds", h.DeleteMany)
router.GET("/findSysVersion", h.Find)
router.GET("/getSysVersionList", h.List)
router.POST("/exportVersion", h.Export)
router.GET("/downloadVersionJson", h.Download)
router.POST("/importVersion", h.Import)
}
-10
View File
@@ -1,10 +0,0 @@
package server
import (
"kra/internal/server/handler"
"github.com/google/wire"
)
// ProviderSet is server providers.
var ProviderSet = wire.NewSet(NewGinEngine, NewGinServer, handler.NewAuthority, handler.NewMenu, handler.NewAPI, handler.NewPermission, handler.NewOrganization, handler.NewAnnouncement, handler.NewEmail, handler.NewPayment, handler.NewTask, handler.NewMedia, handler.NewAudit, handler.NewExport, handler.NewVersion, handler.NewDictionary, handler.NewParameter, handler.NewAPIToken, handler.NewSystemConfig, handler.NewPublic, handler.NewUser, handler.NewNavigation, handler.NewSession, handler.NewSet)
-170
View File
@@ -1,170 +0,0 @@
package server
import (
"encoding/json"
"log/slog"
"regexp"
"sort"
"strings"
"sync"
"kra/internal/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
}