From f63acae3530aa0ecde23d0ca108c8ddf6a09788e Mon Sep 17 00:00:00 2001
From: yvan <8574526@qq.com>
Date: Fri, 14 Aug 2026 16:34:17 +0800
Subject: [PATCH] fix
---
cmd/kratos-admin/main.go | 23 +-
cmd/kratos-admin/wire_gen.go | 4 +-
configs/config.yaml | 37 +-
go.mod | 7 +
go.sum | 15 +
internal/biz/api_token.go | 3 -
internal/biz/data_scope.go | 7 +
internal/biz/email.go | 17 +-
internal/biz/media.go | 8 +-
internal/biz/security.go | 3 -
internal/conf/conf.pb.go | 914 +++++++++++++++++++--
internal/conf/conf.proto | 74 +-
internal/data/api.go | 4 +-
internal/data/api_token.go | 2 +-
internal/data/authority.go | 81 +-
internal/data/cache.go | 2 +-
internal/data/config_store.go | 12 +
internal/data/data.go | 37 +-
internal/data/data_scope.go | 230 ++++++
internal/data/database.go | 59 +-
internal/data/dictionary.go | 2 +-
internal/data/email.go | 9 +-
internal/data/export.go | 2 +-
internal/data/migrations.go | 71 +-
internal/data/mongo.go | 63 ++
internal/data/parameter.go | 2 +-
internal/data/runtime_clients.go | 72 +-
internal/data/system.go | 4 +-
internal/data/system_init.go | 18 +-
internal/server/gin.go | 4 +-
internal/server/handler/media.go | 6 +-
internal/server/handler/public.go | 8 +-
internal/server/middleware/access.go | 13 +-
internal/server/middleware/access_log.go | 86 ++
internal/server/middleware/audit.go | 70 +-
internal/server/middleware/auth.go | 56 +-
internal/server/middleware/capture.go | 22 +-
internal/server/middleware/email.go | 48 --
internal/server/middleware/error_audit.go | 41 +
internal/server/middleware/rate_limit.go | 4 +-
internal/server/middleware/recovery.go | 51 ++
internal/server/middleware/request.go | 32 +-
internal/service/access.go | 24 +-
internal/service/api.go | 10 +
internal/service/api_metadata.go | 191 +++++
internal/service/authentication.go | 7 +-
internal/service/dto/system_config.go | 45 +-
internal/service/system_config.go | 62 +-
internal/service/system_init.go | 14 +-
pkg/adminauth/token.go | 26 +-
pkg/logging/daily.go | 37 +-
pkg/logging/zap.go | 107 ++-
web/src/view/systemTools/system/system.vue | 92 ++-
53 files changed, 2488 insertions(+), 350 deletions(-)
create mode 100644 internal/data/data_scope.go
create mode 100644 internal/data/mongo.go
create mode 100644 internal/server/middleware/access_log.go
delete mode 100644 internal/server/middleware/email.go
create mode 100644 internal/server/middleware/error_audit.go
create mode 100644 internal/server/middleware/recovery.go
create mode 100644 internal/service/api_metadata.go
diff --git a/cmd/kratos-admin/main.go b/cmd/kratos-admin/main.go
index 7403078..d9eb32a 100644
--- a/cmd/kratos-admin/main.go
+++ b/cmd/kratos-admin/main.go
@@ -51,13 +51,6 @@ func newApp(logger *slog.Logger, hs *kratoshttp.Server, scheduler *worker.TaskSc
func main() {
flag.Parse()
- logger, closeLogger := logging.NewZapLogger("logs", "application.log",
- slog.String("service.id", id),
- slog.String("service.name", Name),
- slog.String("service.version", Version),
- )
- defer closeLogger()
- log.SetDefault(logger)
c := config.New(
config.WithSource(
file.NewSource(flagconf),
@@ -73,6 +66,22 @@ func main() {
if err := c.Scan(&bc); err != nil {
panic(err)
}
+ logOptions := logging.Options{Level: "info", Format: "json", EncodeLevel: "LowercaseLevelEncoder", LogInConsole: true, ShowLine: true, RetentionDay: 7}
+ logRoot := "logs"
+ if bc.Admin != nil && bc.Admin.Zap != nil {
+ zapConfig := bc.Admin.Zap
+ logOptions = logging.Options{Level: zapConfig.Level, Format: zapConfig.Format, EncodeLevel: zapConfig.EncodeLevel, Prefix: zapConfig.Prefix, StacktraceKey: zapConfig.StacktraceKey, LogInConsole: zapConfig.LogInConsole, ShowLine: zapConfig.ShowLine, RetentionDay: int(zapConfig.RetentionDay), FileOnlyModules: zapConfig.FileOnlyModules}
+ if zapConfig.Director != "" {
+ logRoot = zapConfig.Director
+ }
+ }
+ loggerAttrs := []any{slog.String("service.id", id), slog.String("service.name", Name), slog.String("service.version", Version)}
+ if bc.Admin != nil && bc.Admin.App != nil {
+ loggerAttrs = append(loggerAttrs, slog.String("node", bc.Admin.App.Node), slog.String("app_id", bc.Admin.App.AppId), slog.String("env", bc.Admin.App.Env))
+ }
+ logger, closeLogger := logging.NewZapLogger(logRoot, "application.log", logOptions, loggerAttrs...)
+ defer closeLogger()
+ log.SetDefault(logger)
if bc.Admin != nil {
bc.Admin.ConfigPath = flagconf
if info, err := os.Stat(flagconf); err == nil && info.IsDir() {
diff --git a/cmd/kratos-admin/wire_gen.go b/cmd/kratos-admin/wire_gen.go
index 9c8a256..1b86afd 100644
--- a/cmd/kratos-admin/wire_gen.go
+++ b/cmd/kratos-admin/wire_gen.go
@@ -45,7 +45,7 @@ func wireApp(confServer *conf.Server, confData *conf.Data, adminBackend *conf.Ad
systemService := service.NewSystemService(systemUsecase, runtime, settingsService)
accessRepo := data.NewAccessRepo(dataData)
accessUsecase := biz.NewAccessUsecase(accessRepo)
- accessService := service.NewAccessService(accessUsecase)
+ accessService := service.NewAccessService(accessUsecase, runtime)
authority := handler.NewAuthority(accessService)
menuRepo := data.NewMenuRepo(dataData)
menuUsecase := biz.NewMenuUsecase(menuRepo)
@@ -94,7 +94,7 @@ func wireApp(confServer *conf.Server, confData *conf.Data, adminBackend *conf.Ad
user := handler.NewUser(systemService)
navigation := handler.NewNavigation(systemService)
session := handler.NewSession(settingsService)
- httpServer := server.NewGinServer(confServer, runtime, systemService, accessService, authority, menu, api, permission, organization, announcement, email, task, media, audit, export, version, dictionary, parameter, apiToken, systemConfig, public, user, navigation, session, settingsService, auditService, emailService, logger)
+ httpServer := server.NewGinServer(confServer, runtime, systemService, accessService, authority, menu, api, permission, organization, announcement, email, task, media, audit, export, version, dictionary, parameter, apiToken, systemConfig, public, user, navigation, session, settingsService, auditService, logger)
app := newApp(logger, httpServer, taskScheduler)
return app, func() {
cleanup()
diff --git a/configs/config.yaml b/configs/config.yaml
index 75c0806..b2e81f7 100644
--- a/configs/config.yaml
+++ b/configs/config.yaml
@@ -9,14 +9,26 @@ data:
host: 127.0.0.1
port: "3306"
user: root
- password: root
+ password: "12345678"
name: test
config: timeout=5s&parseTime=True&loc=Local&charset=utf8mb4
+ log_mode: info
+ max_idle_conns: 10
+ max_open_conns: 100
+ conn_max_lifetime: 3600
redis:
+ name: default
addr: 127.0.0.1:6379
+ password: ""
+ db: 0
+ use_cluster: false
+ cluster_addrs: []
read_timeout: 0.2s
write_timeout: 0.2s
database_list: []
+ redis_list: []
+ mongo:
+ hosts: []
admin:
router_prefix: ""
jwt:
@@ -40,6 +52,29 @@ admin:
use_multipoint: false
use_strict_auth: false
disable_auto_migrate: false
+ use_mongo: false
+ zap:
+ level: info
+ prefix: "[kra] "
+ format: json
+ director: logs
+ encode_level: LowercaseLevelEncoder
+ stacktrace_key: stacktrace
+ show_line: true
+ log_in_console: true
+ retention_day: 7
+ access_req_body: true
+ access_resp_data: true
+ access_req_headers: false
+ access_log_max_bytes: 32768
+ file_only_modules: []
+ cors:
+ mode: whitelist
+ whitelist: []
+ app:
+ node: ""
+ app_id: kra
+ env: development
disk_list:
- mount_point: /
storage:
diff --git a/go.mod b/go.mod
index a282256..553b8a2 100644
--- a/go.mod
+++ b/go.mod
@@ -29,6 +29,7 @@ require (
github.com/tencentyun/cos-go-sdk-v5 v0.7.60
github.com/xuri/excelize/v2 v2.9.0
go.einride.tech/aip v0.86.3
+ go.mongodb.org/mongo-driver v1.17.2
go.uber.org/automaxprocs v1.6.0
go.uber.org/zap v1.27.0
go.uber.org/zap/exp v0.3.0
@@ -92,6 +93,7 @@ require (
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
+ github.com/golang/snappy v0.0.4 // indirect
github.com/google/go-querystring v1.0.0 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
@@ -114,6 +116,7 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
+ github.com/montanaflynn/stats v0.7.1 // indirect
github.com/mozillazg/go-httpheader v0.2.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect
@@ -129,8 +132,12 @@ require (
github.com/tklauser/numcpus v0.10.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
+ github.com/xdg-go/pbkdf2 v1.0.0 // indirect
+ github.com/xdg-go/scram v1.1.2 // indirect
+ github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d // indirect
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 // indirect
+ github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel v1.44.0 // indirect
diff --git a/go.sum b/go.sum
index 9c1cb74..783c262 100644
--- a/go.sum
+++ b/go.sum
@@ -166,6 +166,8 @@ github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF0
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
+github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
@@ -262,6 +264,8 @@ github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwd
github.com/mojocn/base64Captcha v1.3.8 h1:rrN9BhCwXKS8ht1e21kvR3iTaMgf4qPC9sRoV52bqEg=
github.com/mojocn/base64Captcha v1.3.8/go.mod h1:QFZy927L8HVP3+VV5z2b1EAEiv1KxVJKZbAucVgLUy4=
github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
+github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
+github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/mozillazg/go-httpheader v0.2.1 h1:geV7TrjbL8KXSyvghnFm+NyTux/hxwueTSrwhe88TQQ=
github.com/mozillazg/go-httpheader v0.2.1/go.mod h1:jJ8xECTlalr6ValeXYdOF8fFUISeBAdw6E61aqQma60=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
@@ -337,17 +341,27 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
+github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
+github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
+github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
+github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
+github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
+github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7psK/lVsjIS2otl+1WyRyY=
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
github.com/xuri/excelize/v2 v2.9.0 h1:1tgOaEq92IOEumR1/JfYS/eR0KHOCsRv/rYXXh6YJQE=
github.com/xuri/excelize/v2 v2.9.0/go.mod h1:uqey4QBZ9gdMeWApPLdhm9x+9o2lq4iVmjiLfBS5hdE=
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A=
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
+github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
+github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.einride.tech/aip v0.86.3 h1:jg80Ec4XBPYg1i7avzrl3MJol/dUwmMMLHtcmEMyxgM=
go.einride.tech/aip v0.86.3/go.mod h1:dZuN/0sXeoscfWqsW8QLcLrGZdvsCC1B2R2CZ4kHmao=
+go.mongodb.org/mongo-driver v1.17.2 h1:gvZyk8352qSfzyZ2UMWcpDpMSGEr1eqE4T793SqyhzM=
+go.mongodb.org/mongo-driver v1.17.2/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
@@ -470,6 +484,7 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
diff --git a/internal/biz/api_token.go b/internal/biz/api_token.go
index 0925377..b71870b 100644
--- a/internal/biz/api_token.go
+++ b/internal/biz/api_token.go
@@ -41,9 +41,6 @@ func (uc *SettingsUsecase) PrepareAPIToken(ctx context.Context, userID, authorit
if days == -1 {
duration = 100 * 365 * 24 * time.Hour
}
- if duration <= 0 {
- return nil, 0, errors.New("有效天数必须大于0或为-1")
- }
return user, duration, nil
}
diff --git a/internal/biz/data_scope.go b/internal/biz/data_scope.go
index 26b9455..5f803eb 100644
--- a/internal/biz/data_scope.go
+++ b/internal/biz/data_scope.go
@@ -4,8 +4,15 @@ import "context"
type DataScope struct {
All bool
+ Scope int
+ UserID uint
+ AuthorityID uint
+ PrimaryDeptID uint
OwnerUserID uint
DepartmentIDs []uint
+ RequestID string
+ Method string
+ Path string
}
type dataScopeKey struct{}
diff --git a/internal/biz/email.go b/internal/biz/email.go
index 523de5c..5d394cd 100644
--- a/internal/biz/email.go
+++ b/internal/biz/email.go
@@ -16,23 +16,8 @@ type EmailUsecase struct{ repo EmailRepo }
func NewEmailUsecase(repo EmailRepo) *EmailUsecase { return &EmailUsecase{repo: repo} }
-func splitRecipients(value string) []string {
- parts := strings.Split(value, ",")
- result := make([]string, 0, len(parts))
- for _, part := range parts {
- if recipient := strings.TrimSpace(part); recipient != "" {
- result = append(result, recipient)
- }
- }
- return result
-}
-
func (uc *EmailUsecase) Send(ctx context.Context, to, subject, body string) error {
- recipients := splitRecipients(to)
- if len(recipients) == 0 || subject == "" {
- return errors.New("收件人和邮件标题不能为空")
- }
- return uc.repo.Send(ctx, recipients, subject, body)
+ return uc.repo.Send(ctx, strings.Split(to, ","), subject, body)
}
func (uc *EmailUsecase) Test(ctx context.Context) error {
diff --git a/internal/biz/media.go b/internal/biz/media.go
index 64430fe..922be93 100644
--- a/internal/biz/media.go
+++ b/internal/biz/media.go
@@ -1,11 +1,13 @@
package biz
import (
+ "bufio"
"context"
"crypto/md5"
"encoding/hex"
"errors"
"io"
+ "net/http"
"path/filepath"
"strings"
"time"
@@ -50,9 +52,13 @@ func (uc *MediaUsecase) Upload(ctx context.Context, userID uint, name, mime stri
return nil, err
}
ext := strings.ToLower(filepath.Ext(name))
+ buffered := bufio.NewReader(reader)
+ if header, _ := buffered.Peek(512); len(header) > 0 {
+ mime = http.DetectContentType(header)
+ }
key := time.Now().Format("20060102") + "/" + uuid.NewString() + ext
hash := md5.New()
- stored, err := uc.files.Put(ctx, key, io.TeeReader(reader, hash))
+ stored, err := uc.files.Put(ctx, key, io.TeeReader(buffered, hash))
if err != nil {
return nil, err
}
diff --git a/internal/biz/security.go b/internal/biz/security.go
index 5fe8b17..ee5bce0 100644
--- a/internal/biz/security.go
+++ b/internal/biz/security.go
@@ -34,9 +34,6 @@ type SecurityRepo interface {
}
func (uc *SettingsUsecase) UpdateSecurity(ctx context.Context, value *SecurityConfig) error {
- if value.KeyLong < 1 || value.ImgWidth < 1 || value.ImgHeight < 1 || value.PwdMinLength < 1 || value.LimitWindow < 1 || value.LimitCount < 1 || value.LockThreshold < 1 || value.LockDuration < 1 || value.PwdExpireDays < 1 {
- return errors.New("安全配置数值必须大于0")
- }
return uc.SaveSecurityConfig(ctx, value)
}
diff --git a/internal/conf/conf.pb.go b/internal/conf/conf.pb.go
index 1e50a77..259a486 100644
--- a/internal/conf/conf.pb.go
+++ b/internal/conf/conf.pb.go
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
-// protoc-gen-go v1.36.12
-// protoc v7.35.1
+// protoc-gen-go v1.36.11
+// protoc (unknown)
// source: conf/conf.proto
package conf
@@ -131,6 +131,8 @@ type Data struct {
Database *Data_Database `protobuf:"bytes,1,opt,name=database,proto3" json:"database,omitempty"`
Redis *Data_Redis `protobuf:"bytes,2,opt,name=redis,proto3" json:"redis,omitempty"`
DatabaseList []*Data_Database `protobuf:"bytes,3,rep,name=database_list,json=databaseList,proto3" json:"database_list,omitempty"`
+ RedisList []*Data_Redis `protobuf:"bytes,4,rep,name=redis_list,json=redisList,proto3" json:"redis_list,omitempty"`
+ Mongo *Data_Mongo `protobuf:"bytes,5,opt,name=mongo,proto3" json:"mongo,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -186,6 +188,20 @@ func (x *Data) GetDatabaseList() []*Data_Database {
return nil
}
+func (x *Data) GetRedisList() []*Data_Redis {
+ if x != nil {
+ return x.RedisList
+ }
+ return nil
+}
+
+func (x *Data) GetMongo() *Data_Mongo {
+ if x != nil {
+ return x.Mongo
+ }
+ return nil
+}
+
// AdminBackend contains settings for the administration HTTP transport.
type AdminBackend struct {
state protoimpl.MessageState `protogen:"open.v1"`
@@ -200,6 +216,9 @@ type AdminBackend struct {
Media *AdminBackend_Media `protobuf:"bytes,8,opt,name=media,proto3" json:"media,omitempty"`
DiskList []*AdminBackend_Disk `protobuf:"bytes,9,rep,name=disk_list,json=diskList,proto3" json:"disk_list,omitempty"`
System *AdminBackend_System `protobuf:"bytes,10,opt,name=system,proto3" json:"system,omitempty"`
+ Zap *AdminBackend_Zap `protobuf:"bytes,11,opt,name=zap,proto3" json:"zap,omitempty"`
+ Cors *AdminBackend_CORS `protobuf:"bytes,12,opt,name=cors,proto3" json:"cors,omitempty"`
+ App *AdminBackend_App `protobuf:"bytes,13,opt,name=app,proto3" json:"app,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -304,6 +323,27 @@ func (x *AdminBackend) GetSystem() *AdminBackend_System {
return nil
}
+func (x *AdminBackend) GetZap() *AdminBackend_Zap {
+ if x != nil {
+ return x.Zap
+ }
+ return nil
+}
+
+func (x *AdminBackend) GetCors() *AdminBackend_CORS {
+ if x != nil {
+ return x.Cors
+ }
+ return nil
+}
+
+func (x *AdminBackend) GetApp() *AdminBackend_App {
+ if x != nil {
+ return x.App
+ }
+ return nil
+}
+
type Server_HTTP struct {
state protoimpl.MessageState `protogen:"open.v1"`
Network string `protobuf:"bytes,1,opt,name=network,proto3" json:"network,omitempty"`
@@ -365,20 +405,27 @@ func (x *Server_HTTP) GetTimeout() *durationpb.Duration {
}
type Data_Database struct {
- state protoimpl.MessageState `protogen:"open.v1"`
- Driver string `protobuf:"bytes,1,opt,name=driver,proto3" json:"driver,omitempty"`
- Source string `protobuf:"bytes,2,opt,name=source,proto3" json:"source,omitempty"`
- Host string `protobuf:"bytes,3,opt,name=host,proto3" json:"host,omitempty"`
- Port string `protobuf:"bytes,4,opt,name=port,proto3" json:"port,omitempty"`
- User string `protobuf:"bytes,5,opt,name=user,proto3" json:"user,omitempty"`
- Password string `protobuf:"bytes,6,opt,name=password,proto3" json:"password,omitempty"`
- Name string `protobuf:"bytes,7,opt,name=name,proto3" json:"name,omitempty"`
- Config string `protobuf:"bytes,8,opt,name=config,proto3" json:"config,omitempty"`
- Path string `protobuf:"bytes,9,opt,name=path,proto3" json:"path,omitempty"`
- AliasName string `protobuf:"bytes,10,opt,name=alias_name,json=aliasName,proto3" json:"alias_name,omitempty"`
- Disable bool `protobuf:"varint,11,opt,name=disable,proto3" json:"disable,omitempty"`
- unknownFields protoimpl.UnknownFields
- sizeCache protoimpl.SizeCache
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Driver string `protobuf:"bytes,1,opt,name=driver,proto3" json:"driver,omitempty"`
+ Source string `protobuf:"bytes,2,opt,name=source,proto3" json:"source,omitempty"`
+ Host string `protobuf:"bytes,3,opt,name=host,proto3" json:"host,omitempty"`
+ Port string `protobuf:"bytes,4,opt,name=port,proto3" json:"port,omitempty"`
+ User string `protobuf:"bytes,5,opt,name=user,proto3" json:"user,omitempty"`
+ Password string `protobuf:"bytes,6,opt,name=password,proto3" json:"password,omitempty"`
+ Name string `protobuf:"bytes,7,opt,name=name,proto3" json:"name,omitempty"`
+ Config string `protobuf:"bytes,8,opt,name=config,proto3" json:"config,omitempty"`
+ Path string `protobuf:"bytes,9,opt,name=path,proto3" json:"path,omitempty"`
+ AliasName string `protobuf:"bytes,10,opt,name=alias_name,json=aliasName,proto3" json:"alias_name,omitempty"`
+ Disable bool `protobuf:"varint,11,opt,name=disable,proto3" json:"disable,omitempty"`
+ Prefix string `protobuf:"bytes,12,opt,name=prefix,proto3" json:"prefix,omitempty"`
+ Engine string `protobuf:"bytes,13,opt,name=engine,proto3" json:"engine,omitempty"`
+ LogMode string `protobuf:"bytes,14,opt,name=log_mode,json=logMode,proto3" json:"log_mode,omitempty"`
+ MaxIdleConns int32 `protobuf:"varint,15,opt,name=max_idle_conns,json=maxIdleConns,proto3" json:"max_idle_conns,omitempty"`
+ MaxOpenConns int32 `protobuf:"varint,16,opt,name=max_open_conns,json=maxOpenConns,proto3" json:"max_open_conns,omitempty"`
+ ConnMaxLifetime int32 `protobuf:"varint,17,opt,name=conn_max_lifetime,json=connMaxLifetime,proto3" json:"conn_max_lifetime,omitempty"`
+ Singular bool `protobuf:"varint,18,opt,name=singular,proto3" json:"singular,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
}
func (x *Data_Database) Reset() {
@@ -488,12 +535,66 @@ func (x *Data_Database) GetDisable() bool {
return false
}
+func (x *Data_Database) GetPrefix() string {
+ if x != nil {
+ return x.Prefix
+ }
+ return ""
+}
+
+func (x *Data_Database) GetEngine() string {
+ if x != nil {
+ return x.Engine
+ }
+ return ""
+}
+
+func (x *Data_Database) GetLogMode() string {
+ if x != nil {
+ return x.LogMode
+ }
+ return ""
+}
+
+func (x *Data_Database) GetMaxIdleConns() int32 {
+ if x != nil {
+ return x.MaxIdleConns
+ }
+ return 0
+}
+
+func (x *Data_Database) GetMaxOpenConns() int32 {
+ if x != nil {
+ return x.MaxOpenConns
+ }
+ return 0
+}
+
+func (x *Data_Database) GetConnMaxLifetime() int32 {
+ if x != nil {
+ return x.ConnMaxLifetime
+ }
+ return 0
+}
+
+func (x *Data_Database) GetSingular() bool {
+ if x != nil {
+ return x.Singular
+ }
+ return false
+}
+
type Data_Redis struct {
state protoimpl.MessageState `protogen:"open.v1"`
Network string `protobuf:"bytes,1,opt,name=network,proto3" json:"network,omitempty"`
Addr string `protobuf:"bytes,2,opt,name=addr,proto3" json:"addr,omitempty"`
ReadTimeout *durationpb.Duration `protobuf:"bytes,3,opt,name=read_timeout,json=readTimeout,proto3" json:"read_timeout,omitempty"`
WriteTimeout *durationpb.Duration `protobuf:"bytes,4,opt,name=write_timeout,json=writeTimeout,proto3" json:"write_timeout,omitempty"`
+ Name string `protobuf:"bytes,5,opt,name=name,proto3" json:"name,omitempty"`
+ Password string `protobuf:"bytes,6,opt,name=password,proto3" json:"password,omitempty"`
+ Db int32 `protobuf:"varint,7,opt,name=db,proto3" json:"db,omitempty"`
+ UseCluster bool `protobuf:"varint,8,opt,name=use_cluster,json=useCluster,proto3" json:"use_cluster,omitempty"`
+ ClusterAddrs []string `protobuf:"bytes,9,rep,name=cluster_addrs,json=clusterAddrs,proto3" json:"cluster_addrs,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -556,6 +657,225 @@ func (x *Data_Redis) GetWriteTimeout() *durationpb.Duration {
return nil
}
+func (x *Data_Redis) GetName() string {
+ if x != nil {
+ return x.Name
+ }
+ return ""
+}
+
+func (x *Data_Redis) GetPassword() string {
+ if x != nil {
+ return x.Password
+ }
+ return ""
+}
+
+func (x *Data_Redis) GetDb() int32 {
+ if x != nil {
+ return x.Db
+ }
+ return 0
+}
+
+func (x *Data_Redis) GetUseCluster() bool {
+ if x != nil {
+ return x.UseCluster
+ }
+ return false
+}
+
+func (x *Data_Redis) GetClusterAddrs() []string {
+ if x != nil {
+ return x.ClusterAddrs
+ }
+ return nil
+}
+
+type Data_MongoHost struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Host string `protobuf:"bytes,1,opt,name=host,proto3" json:"host,omitempty"`
+ Port string `protobuf:"bytes,2,opt,name=port,proto3" json:"port,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *Data_MongoHost) Reset() {
+ *x = Data_MongoHost{}
+ mi := &file_conf_conf_proto_msgTypes[7]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *Data_MongoHost) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Data_MongoHost) ProtoMessage() {}
+
+func (x *Data_MongoHost) ProtoReflect() protoreflect.Message {
+ mi := &file_conf_conf_proto_msgTypes[7]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use Data_MongoHost.ProtoReflect.Descriptor instead.
+func (*Data_MongoHost) Descriptor() ([]byte, []int) {
+ return file_conf_conf_proto_rawDescGZIP(), []int{2, 2}
+}
+
+func (x *Data_MongoHost) GetHost() string {
+ if x != nil {
+ return x.Host
+ }
+ return ""
+}
+
+func (x *Data_MongoHost) GetPort() string {
+ if x != nil {
+ return x.Port
+ }
+ return ""
+}
+
+type Data_Mongo struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Coll string `protobuf:"bytes,1,opt,name=coll,proto3" json:"coll,omitempty"`
+ Options string `protobuf:"bytes,2,opt,name=options,proto3" json:"options,omitempty"`
+ Database string `protobuf:"bytes,3,opt,name=database,proto3" json:"database,omitempty"`
+ Username string `protobuf:"bytes,4,opt,name=username,proto3" json:"username,omitempty"`
+ Password string `protobuf:"bytes,5,opt,name=password,proto3" json:"password,omitempty"`
+ AuthSource string `protobuf:"bytes,6,opt,name=auth_source,json=authSource,proto3" json:"auth_source,omitempty"`
+ MinPoolSize uint64 `protobuf:"varint,7,opt,name=min_pool_size,json=minPoolSize,proto3" json:"min_pool_size,omitempty"`
+ MaxPoolSize uint64 `protobuf:"varint,8,opt,name=max_pool_size,json=maxPoolSize,proto3" json:"max_pool_size,omitempty"`
+ SocketTimeoutMs int64 `protobuf:"varint,9,opt,name=socket_timeout_ms,json=socketTimeoutMs,proto3" json:"socket_timeout_ms,omitempty"`
+ ConnectTimeoutMs int64 `protobuf:"varint,10,opt,name=connect_timeout_ms,json=connectTimeoutMs,proto3" json:"connect_timeout_ms,omitempty"`
+ IsZap bool `protobuf:"varint,11,opt,name=is_zap,json=isZap,proto3" json:"is_zap,omitempty"`
+ Hosts []*Data_MongoHost `protobuf:"bytes,12,rep,name=hosts,proto3" json:"hosts,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *Data_Mongo) Reset() {
+ *x = Data_Mongo{}
+ mi := &file_conf_conf_proto_msgTypes[8]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *Data_Mongo) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Data_Mongo) ProtoMessage() {}
+
+func (x *Data_Mongo) ProtoReflect() protoreflect.Message {
+ mi := &file_conf_conf_proto_msgTypes[8]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use Data_Mongo.ProtoReflect.Descriptor instead.
+func (*Data_Mongo) Descriptor() ([]byte, []int) {
+ return file_conf_conf_proto_rawDescGZIP(), []int{2, 3}
+}
+
+func (x *Data_Mongo) GetColl() string {
+ if x != nil {
+ return x.Coll
+ }
+ return ""
+}
+
+func (x *Data_Mongo) GetOptions() string {
+ if x != nil {
+ return x.Options
+ }
+ return ""
+}
+
+func (x *Data_Mongo) GetDatabase() string {
+ if x != nil {
+ return x.Database
+ }
+ return ""
+}
+
+func (x *Data_Mongo) GetUsername() string {
+ if x != nil {
+ return x.Username
+ }
+ return ""
+}
+
+func (x *Data_Mongo) GetPassword() string {
+ if x != nil {
+ return x.Password
+ }
+ return ""
+}
+
+func (x *Data_Mongo) GetAuthSource() string {
+ if x != nil {
+ return x.AuthSource
+ }
+ return ""
+}
+
+func (x *Data_Mongo) GetMinPoolSize() uint64 {
+ if x != nil {
+ return x.MinPoolSize
+ }
+ return 0
+}
+
+func (x *Data_Mongo) GetMaxPoolSize() uint64 {
+ if x != nil {
+ return x.MaxPoolSize
+ }
+ return 0
+}
+
+func (x *Data_Mongo) GetSocketTimeoutMs() int64 {
+ if x != nil {
+ return x.SocketTimeoutMs
+ }
+ return 0
+}
+
+func (x *Data_Mongo) GetConnectTimeoutMs() int64 {
+ if x != nil {
+ return x.ConnectTimeoutMs
+ }
+ return 0
+}
+
+func (x *Data_Mongo) GetIsZap() bool {
+ if x != nil {
+ return x.IsZap
+ }
+ return false
+}
+
+func (x *Data_Mongo) GetHosts() []*Data_MongoHost {
+ if x != nil {
+ return x.Hosts
+ }
+ return nil
+}
+
type AdminBackend_JWT struct {
state protoimpl.MessageState `protogen:"open.v1"`
SigningKey string `protobuf:"bytes,1,opt,name=signing_key,json=signingKey,proto3" json:"signing_key,omitempty"`
@@ -568,7 +888,7 @@ type AdminBackend_JWT struct {
func (x *AdminBackend_JWT) Reset() {
*x = AdminBackend_JWT{}
- mi := &file_conf_conf_proto_msgTypes[7]
+ mi := &file_conf_conf_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -580,7 +900,7 @@ func (x *AdminBackend_JWT) String() string {
func (*AdminBackend_JWT) ProtoMessage() {}
func (x *AdminBackend_JWT) ProtoReflect() protoreflect.Message {
- mi := &file_conf_conf_proto_msgTypes[7]
+ mi := &file_conf_conf_proto_msgTypes[9]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -636,7 +956,7 @@ type AdminBackend_Captcha struct {
func (x *AdminBackend_Captcha) Reset() {
*x = AdminBackend_Captcha{}
- mi := &file_conf_conf_proto_msgTypes[8]
+ mi := &file_conf_conf_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -648,7 +968,7 @@ func (x *AdminBackend_Captcha) String() string {
func (*AdminBackend_Captcha) ProtoMessage() {}
func (x *AdminBackend_Captcha) ProtoReflect() protoreflect.Message {
- mi := &file_conf_conf_proto_msgTypes[8]
+ mi := &file_conf_conf_proto_msgTypes[10]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -702,7 +1022,7 @@ type AdminBackend_Local struct {
func (x *AdminBackend_Local) Reset() {
*x = AdminBackend_Local{}
- mi := &file_conf_conf_proto_msgTypes[9]
+ mi := &file_conf_conf_proto_msgTypes[11]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -714,7 +1034,7 @@ func (x *AdminBackend_Local) String() string {
func (*AdminBackend_Local) ProtoMessage() {}
func (x *AdminBackend_Local) ProtoReflect() protoreflect.Message {
- mi := &file_conf_conf_proto_msgTypes[9]
+ mi := &file_conf_conf_proto_msgTypes[11]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -760,7 +1080,7 @@ type AdminBackend_Email struct {
func (x *AdminBackend_Email) Reset() {
*x = AdminBackend_Email{}
- mi := &file_conf_conf_proto_msgTypes[10]
+ mi := &file_conf_conf_proto_msgTypes[12]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -772,7 +1092,7 @@ func (x *AdminBackend_Email) String() string {
func (*AdminBackend_Email) ProtoMessage() {}
func (x *AdminBackend_Email) ProtoReflect() protoreflect.Message {
- mi := &file_conf_conf_proto_msgTypes[10]
+ mi := &file_conf_conf_proto_msgTypes[12]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -853,7 +1173,7 @@ type AdminBackend_Media struct {
func (x *AdminBackend_Media) Reset() {
*x = AdminBackend_Media{}
- mi := &file_conf_conf_proto_msgTypes[11]
+ mi := &file_conf_conf_proto_msgTypes[13]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -865,7 +1185,7 @@ func (x *AdminBackend_Media) String() string {
func (*AdminBackend_Media) ProtoMessage() {}
func (x *AdminBackend_Media) ProtoReflect() protoreflect.Message {
- mi := &file_conf_conf_proto_msgTypes[11]
+ mi := &file_conf_conf_proto_msgTypes[13]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -897,7 +1217,7 @@ type AdminBackend_Disk struct {
func (x *AdminBackend_Disk) Reset() {
*x = AdminBackend_Disk{}
- mi := &file_conf_conf_proto_msgTypes[12]
+ mi := &file_conf_conf_proto_msgTypes[14]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -909,7 +1229,7 @@ func (x *AdminBackend_Disk) String() string {
func (*AdminBackend_Disk) ProtoMessage() {}
func (x *AdminBackend_Disk) ProtoReflect() protoreflect.Message {
- mi := &file_conf_conf_proto_msgTypes[12]
+ mi := &file_conf_conf_proto_msgTypes[14]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -938,13 +1258,14 @@ type AdminBackend_System struct {
UseMultipoint bool `protobuf:"varint,2,opt,name=use_multipoint,json=useMultipoint,proto3" json:"use_multipoint,omitempty"`
UseStrictAuth bool `protobuf:"varint,3,opt,name=use_strict_auth,json=useStrictAuth,proto3" json:"use_strict_auth,omitempty"`
DisableAutoMigrate bool `protobuf:"varint,4,opt,name=disable_auto_migrate,json=disableAutoMigrate,proto3" json:"disable_auto_migrate,omitempty"`
+ UseMongo bool `protobuf:"varint,5,opt,name=use_mongo,json=useMongo,proto3" json:"use_mongo,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *AdminBackend_System) Reset() {
*x = AdminBackend_System{}
- mi := &file_conf_conf_proto_msgTypes[13]
+ mi := &file_conf_conf_proto_msgTypes[15]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -956,7 +1277,7 @@ func (x *AdminBackend_System) String() string {
func (*AdminBackend_System) ProtoMessage() {}
func (x *AdminBackend_System) ProtoReflect() protoreflect.Message {
- mi := &file_conf_conf_proto_msgTypes[13]
+ mi := &file_conf_conf_proto_msgTypes[15]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1000,6 +1321,349 @@ func (x *AdminBackend_System) GetDisableAutoMigrate() bool {
return false
}
+func (x *AdminBackend_System) GetUseMongo() bool {
+ if x != nil {
+ return x.UseMongo
+ }
+ return false
+}
+
+type AdminBackend_Zap struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Level string `protobuf:"bytes,1,opt,name=level,proto3" json:"level,omitempty"`
+ Prefix string `protobuf:"bytes,2,opt,name=prefix,proto3" json:"prefix,omitempty"`
+ Format string `protobuf:"bytes,3,opt,name=format,proto3" json:"format,omitempty"`
+ Director string `protobuf:"bytes,4,opt,name=director,proto3" json:"director,omitempty"`
+ EncodeLevel string `protobuf:"bytes,5,opt,name=encode_level,json=encodeLevel,proto3" json:"encode_level,omitempty"`
+ StacktraceKey string `protobuf:"bytes,6,opt,name=stacktrace_key,json=stacktraceKey,proto3" json:"stacktrace_key,omitempty"`
+ ShowLine bool `protobuf:"varint,7,opt,name=show_line,json=showLine,proto3" json:"show_line,omitempty"`
+ LogInConsole bool `protobuf:"varint,8,opt,name=log_in_console,json=logInConsole,proto3" json:"log_in_console,omitempty"`
+ RetentionDay int32 `protobuf:"varint,9,opt,name=retention_day,json=retentionDay,proto3" json:"retention_day,omitempty"`
+ AccessReqBody bool `protobuf:"varint,10,opt,name=access_req_body,json=accessReqBody,proto3" json:"access_req_body,omitempty"`
+ AccessRespData bool `protobuf:"varint,11,opt,name=access_resp_data,json=accessRespData,proto3" json:"access_resp_data,omitempty"`
+ AccessReqHeaders bool `protobuf:"varint,12,opt,name=access_req_headers,json=accessReqHeaders,proto3" json:"access_req_headers,omitempty"`
+ AccessLogMaxBytes int32 `protobuf:"varint,13,opt,name=access_log_max_bytes,json=accessLogMaxBytes,proto3" json:"access_log_max_bytes,omitempty"`
+ FileOnlyModules []string `protobuf:"bytes,14,rep,name=file_only_modules,json=fileOnlyModules,proto3" json:"file_only_modules,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *AdminBackend_Zap) Reset() {
+ *x = AdminBackend_Zap{}
+ mi := &file_conf_conf_proto_msgTypes[16]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *AdminBackend_Zap) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*AdminBackend_Zap) ProtoMessage() {}
+
+func (x *AdminBackend_Zap) ProtoReflect() protoreflect.Message {
+ mi := &file_conf_conf_proto_msgTypes[16]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use AdminBackend_Zap.ProtoReflect.Descriptor instead.
+func (*AdminBackend_Zap) Descriptor() ([]byte, []int) {
+ return file_conf_conf_proto_rawDescGZIP(), []int{3, 7}
+}
+
+func (x *AdminBackend_Zap) GetLevel() string {
+ if x != nil {
+ return x.Level
+ }
+ return ""
+}
+
+func (x *AdminBackend_Zap) GetPrefix() string {
+ if x != nil {
+ return x.Prefix
+ }
+ return ""
+}
+
+func (x *AdminBackend_Zap) GetFormat() string {
+ if x != nil {
+ return x.Format
+ }
+ return ""
+}
+
+func (x *AdminBackend_Zap) GetDirector() string {
+ if x != nil {
+ return x.Director
+ }
+ return ""
+}
+
+func (x *AdminBackend_Zap) GetEncodeLevel() string {
+ if x != nil {
+ return x.EncodeLevel
+ }
+ return ""
+}
+
+func (x *AdminBackend_Zap) GetStacktraceKey() string {
+ if x != nil {
+ return x.StacktraceKey
+ }
+ return ""
+}
+
+func (x *AdminBackend_Zap) GetShowLine() bool {
+ if x != nil {
+ return x.ShowLine
+ }
+ return false
+}
+
+func (x *AdminBackend_Zap) GetLogInConsole() bool {
+ if x != nil {
+ return x.LogInConsole
+ }
+ return false
+}
+
+func (x *AdminBackend_Zap) GetRetentionDay() int32 {
+ if x != nil {
+ return x.RetentionDay
+ }
+ return 0
+}
+
+func (x *AdminBackend_Zap) GetAccessReqBody() bool {
+ if x != nil {
+ return x.AccessReqBody
+ }
+ return false
+}
+
+func (x *AdminBackend_Zap) GetAccessRespData() bool {
+ if x != nil {
+ return x.AccessRespData
+ }
+ return false
+}
+
+func (x *AdminBackend_Zap) GetAccessReqHeaders() bool {
+ if x != nil {
+ return x.AccessReqHeaders
+ }
+ return false
+}
+
+func (x *AdminBackend_Zap) GetAccessLogMaxBytes() int32 {
+ if x != nil {
+ return x.AccessLogMaxBytes
+ }
+ return 0
+}
+
+func (x *AdminBackend_Zap) GetFileOnlyModules() []string {
+ if x != nil {
+ return x.FileOnlyModules
+ }
+ return nil
+}
+
+type AdminBackend_CORS struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Mode string `protobuf:"bytes,1,opt,name=mode,proto3" json:"mode,omitempty"`
+ Whitelist []*AdminBackend_CORSRule `protobuf:"bytes,2,rep,name=whitelist,proto3" json:"whitelist,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *AdminBackend_CORS) Reset() {
+ *x = AdminBackend_CORS{}
+ mi := &file_conf_conf_proto_msgTypes[17]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *AdminBackend_CORS) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*AdminBackend_CORS) ProtoMessage() {}
+
+func (x *AdminBackend_CORS) ProtoReflect() protoreflect.Message {
+ mi := &file_conf_conf_proto_msgTypes[17]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use AdminBackend_CORS.ProtoReflect.Descriptor instead.
+func (*AdminBackend_CORS) Descriptor() ([]byte, []int) {
+ return file_conf_conf_proto_rawDescGZIP(), []int{3, 8}
+}
+
+func (x *AdminBackend_CORS) GetMode() string {
+ if x != nil {
+ return x.Mode
+ }
+ return ""
+}
+
+func (x *AdminBackend_CORS) GetWhitelist() []*AdminBackend_CORSRule {
+ if x != nil {
+ return x.Whitelist
+ }
+ return nil
+}
+
+type AdminBackend_CORSRule struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ AllowOrigin string `protobuf:"bytes,1,opt,name=allow_origin,json=allowOrigin,proto3" json:"allow_origin,omitempty"`
+ AllowMethods string `protobuf:"bytes,2,opt,name=allow_methods,json=allowMethods,proto3" json:"allow_methods,omitempty"`
+ AllowHeaders string `protobuf:"bytes,3,opt,name=allow_headers,json=allowHeaders,proto3" json:"allow_headers,omitempty"`
+ ExposeHeaders string `protobuf:"bytes,4,opt,name=expose_headers,json=exposeHeaders,proto3" json:"expose_headers,omitempty"`
+ AllowCredentials bool `protobuf:"varint,5,opt,name=allow_credentials,json=allowCredentials,proto3" json:"allow_credentials,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *AdminBackend_CORSRule) Reset() {
+ *x = AdminBackend_CORSRule{}
+ mi := &file_conf_conf_proto_msgTypes[18]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *AdminBackend_CORSRule) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*AdminBackend_CORSRule) ProtoMessage() {}
+
+func (x *AdminBackend_CORSRule) ProtoReflect() protoreflect.Message {
+ mi := &file_conf_conf_proto_msgTypes[18]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use AdminBackend_CORSRule.ProtoReflect.Descriptor instead.
+func (*AdminBackend_CORSRule) Descriptor() ([]byte, []int) {
+ return file_conf_conf_proto_rawDescGZIP(), []int{3, 9}
+}
+
+func (x *AdminBackend_CORSRule) GetAllowOrigin() string {
+ if x != nil {
+ return x.AllowOrigin
+ }
+ return ""
+}
+
+func (x *AdminBackend_CORSRule) GetAllowMethods() string {
+ if x != nil {
+ return x.AllowMethods
+ }
+ return ""
+}
+
+func (x *AdminBackend_CORSRule) GetAllowHeaders() string {
+ if x != nil {
+ return x.AllowHeaders
+ }
+ return ""
+}
+
+func (x *AdminBackend_CORSRule) GetExposeHeaders() string {
+ if x != nil {
+ return x.ExposeHeaders
+ }
+ return ""
+}
+
+func (x *AdminBackend_CORSRule) GetAllowCredentials() bool {
+ if x != nil {
+ return x.AllowCredentials
+ }
+ return false
+}
+
+type AdminBackend_App struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Node string `protobuf:"bytes,1,opt,name=node,proto3" json:"node,omitempty"`
+ AppId string `protobuf:"bytes,2,opt,name=app_id,json=appId,proto3" json:"app_id,omitempty"`
+ Env string `protobuf:"bytes,3,opt,name=env,proto3" json:"env,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *AdminBackend_App) Reset() {
+ *x = AdminBackend_App{}
+ mi := &file_conf_conf_proto_msgTypes[19]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *AdminBackend_App) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*AdminBackend_App) ProtoMessage() {}
+
+func (x *AdminBackend_App) ProtoReflect() protoreflect.Message {
+ mi := &file_conf_conf_proto_msgTypes[19]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use AdminBackend_App.ProtoReflect.Descriptor instead.
+func (*AdminBackend_App) Descriptor() ([]byte, []int) {
+ return file_conf_conf_proto_rawDescGZIP(), []int{3, 10}
+}
+
+func (x *AdminBackend_App) GetNode() string {
+ if x != nil {
+ return x.Node
+ }
+ return ""
+}
+
+func (x *AdminBackend_App) GetAppId() string {
+ if x != nil {
+ return x.AppId
+ }
+ return ""
+}
+
+func (x *AdminBackend_App) GetEnv() string {
+ if x != nil {
+ return x.Env
+ }
+ return ""
+}
+
type AdminBackend_Storage struct {
state protoimpl.MessageState `protogen:"open.v1"`
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"`
@@ -1016,7 +1680,7 @@ type AdminBackend_Storage struct {
func (x *AdminBackend_Storage) Reset() {
*x = AdminBackend_Storage{}
- mi := &file_conf_conf_proto_msgTypes[14]
+ mi := &file_conf_conf_proto_msgTypes[20]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1028,7 +1692,7 @@ func (x *AdminBackend_Storage) String() string {
func (*AdminBackend_Storage) ProtoMessage() {}
func (x *AdminBackend_Storage) ProtoReflect() protoreflect.Message {
- mi := &file_conf_conf_proto_msgTypes[14]
+ mi := &file_conf_conf_proto_msgTypes[20]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1041,7 +1705,7 @@ func (x *AdminBackend_Storage) ProtoReflect() protoreflect.Message {
// Deprecated: Use AdminBackend_Storage.ProtoReflect.Descriptor instead.
func (*AdminBackend_Storage) Descriptor() ([]byte, []int) {
- return file_conf_conf_proto_rawDescGZIP(), []int{3, 7}
+ return file_conf_conf_proto_rawDescGZIP(), []int{3, 11}
}
func (x *AdminBackend_Storage) GetType() string {
@@ -1115,7 +1779,7 @@ type AdminBackend_Qiniu struct {
func (x *AdminBackend_Qiniu) Reset() {
*x = AdminBackend_Qiniu{}
- mi := &file_conf_conf_proto_msgTypes[15]
+ mi := &file_conf_conf_proto_msgTypes[21]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1127,7 +1791,7 @@ func (x *AdminBackend_Qiniu) String() string {
func (*AdminBackend_Qiniu) ProtoMessage() {}
func (x *AdminBackend_Qiniu) ProtoReflect() protoreflect.Message {
- mi := &file_conf_conf_proto_msgTypes[15]
+ mi := &file_conf_conf_proto_msgTypes[21]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1140,7 +1804,7 @@ func (x *AdminBackend_Qiniu) ProtoReflect() protoreflect.Message {
// Deprecated: Use AdminBackend_Qiniu.ProtoReflect.Descriptor instead.
func (*AdminBackend_Qiniu) Descriptor() ([]byte, []int) {
- return file_conf_conf_proto_rawDescGZIP(), []int{3, 8}
+ return file_conf_conf_proto_rawDescGZIP(), []int{3, 12}
}
func (x *AdminBackend_Qiniu) GetZone() string {
@@ -1212,7 +1876,7 @@ type AdminBackend_ObjectStore struct {
func (x *AdminBackend_ObjectStore) Reset() {
*x = AdminBackend_ObjectStore{}
- mi := &file_conf_conf_proto_msgTypes[16]
+ mi := &file_conf_conf_proto_msgTypes[22]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1224,7 +1888,7 @@ func (x *AdminBackend_ObjectStore) String() string {
func (*AdminBackend_ObjectStore) ProtoMessage() {}
func (x *AdminBackend_ObjectStore) ProtoReflect() protoreflect.Message {
- mi := &file_conf_conf_proto_msgTypes[16]
+ mi := &file_conf_conf_proto_msgTypes[22]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1237,7 +1901,7 @@ func (x *AdminBackend_ObjectStore) ProtoReflect() protoreflect.Message {
// Deprecated: Use AdminBackend_ObjectStore.ProtoReflect.Descriptor instead.
func (*AdminBackend_ObjectStore) Descriptor() ([]byte, []int) {
- return file_conf_conf_proto_rawDescGZIP(), []int{3, 9}
+ return file_conf_conf_proto_rawDescGZIP(), []int{3, 13}
}
func (x *AdminBackend_ObjectStore) GetEndpoint() string {
@@ -1325,11 +1989,14 @@ const file_conf_conf_proto_rawDesc = "" +
"\x04HTTP\x12\x18\n" +
"\anetwork\x18\x01 \x01(\tR\anetwork\x12\x12\n" +
"\x04addr\x18\x02 \x01(\tR\x04addr\x123\n" +
- "\atimeout\x18\x03 \x01(\v2\x19.google.protobuf.DurationR\atimeout\"\xef\x04\n" +
+ "\atimeout\x18\x03 \x01(\v2\x19.google.protobuf.DurationR\atimeout\"\x86\f\n" +
"\x04Data\x125\n" +
"\bdatabase\x18\x01 \x01(\v2\x19.kratos.api.Data.DatabaseR\bdatabase\x12,\n" +
"\x05redis\x18\x02 \x01(\v2\x16.kratos.api.Data.RedisR\x05redis\x12>\n" +
- "\rdatabase_list\x18\x03 \x03(\v2\x19.kratos.api.Data.DatabaseR\fdatabaseList\x1a\x8b\x02\n" +
+ "\rdatabase_list\x18\x03 \x03(\v2\x19.kratos.api.Data.DatabaseR\fdatabaseList\x125\n" +
+ "\n" +
+ "redis_list\x18\x04 \x03(\v2\x16.kratos.api.Data.RedisR\tredisList\x12,\n" +
+ "\x05mongo\x18\x05 \x01(\v2\x16.kratos.api.Data.MongoR\x05mongo\x1a\xea\x03\n" +
"\bDatabase\x12\x16\n" +
"\x06driver\x18\x01 \x01(\tR\x06driver\x12\x16\n" +
"\x06source\x18\x02 \x01(\tR\x06source\x12\x12\n" +
@@ -1343,12 +2010,43 @@ const file_conf_conf_proto_rawDesc = "" +
"\n" +
"alias_name\x18\n" +
" \x01(\tR\taliasName\x12\x18\n" +
- "\adisable\x18\v \x01(\bR\adisable\x1a\xb3\x01\n" +
+ "\adisable\x18\v \x01(\bR\adisable\x12\x16\n" +
+ "\x06prefix\x18\f \x01(\tR\x06prefix\x12\x16\n" +
+ "\x06engine\x18\r \x01(\tR\x06engine\x12\x19\n" +
+ "\blog_mode\x18\x0e \x01(\tR\alogMode\x12$\n" +
+ "\x0emax_idle_conns\x18\x0f \x01(\x05R\fmaxIdleConns\x12$\n" +
+ "\x0emax_open_conns\x18\x10 \x01(\x05R\fmaxOpenConns\x12*\n" +
+ "\x11conn_max_lifetime\x18\x11 \x01(\x05R\x0fconnMaxLifetime\x12\x1a\n" +
+ "\bsingular\x18\x12 \x01(\bR\bsingular\x1a\xb9\x02\n" +
"\x05Redis\x12\x18\n" +
"\anetwork\x18\x01 \x01(\tR\anetwork\x12\x12\n" +
"\x04addr\x18\x02 \x01(\tR\x04addr\x12<\n" +
"\fread_timeout\x18\x03 \x01(\v2\x19.google.protobuf.DurationR\vreadTimeout\x12>\n" +
- "\rwrite_timeout\x18\x04 \x01(\v2\x19.google.protobuf.DurationR\fwriteTimeout\"\xf8\x12\n" +
+ "\rwrite_timeout\x18\x04 \x01(\v2\x19.google.protobuf.DurationR\fwriteTimeout\x12\x12\n" +
+ "\x04name\x18\x05 \x01(\tR\x04name\x12\x1a\n" +
+ "\bpassword\x18\x06 \x01(\tR\bpassword\x12\x0e\n" +
+ "\x02db\x18\a \x01(\x05R\x02db\x12\x1f\n" +
+ "\vuse_cluster\x18\b \x01(\bR\n" +
+ "useCluster\x12#\n" +
+ "\rcluster_addrs\x18\t \x03(\tR\fclusterAddrs\x1a3\n" +
+ "\tMongoHost\x12\x12\n" +
+ "\x04host\x18\x01 \x01(\tR\x04host\x12\x12\n" +
+ "\x04port\x18\x02 \x01(\tR\x04port\x1a\x95\x03\n" +
+ "\x05Mongo\x12\x12\n" +
+ "\x04coll\x18\x01 \x01(\tR\x04coll\x12\x18\n" +
+ "\aoptions\x18\x02 \x01(\tR\aoptions\x12\x1a\n" +
+ "\bdatabase\x18\x03 \x01(\tR\bdatabase\x12\x1a\n" +
+ "\busername\x18\x04 \x01(\tR\busername\x12\x1a\n" +
+ "\bpassword\x18\x05 \x01(\tR\bpassword\x12\x1f\n" +
+ "\vauth_source\x18\x06 \x01(\tR\n" +
+ "authSource\x12\"\n" +
+ "\rmin_pool_size\x18\a \x01(\x04R\vminPoolSize\x12\"\n" +
+ "\rmax_pool_size\x18\b \x01(\x04R\vmaxPoolSize\x12*\n" +
+ "\x11socket_timeout_ms\x18\t \x01(\x03R\x0fsocketTimeoutMs\x12,\n" +
+ "\x12connect_timeout_ms\x18\n" +
+ " \x01(\x03R\x10connectTimeoutMs\x12\x15\n" +
+ "\x06is_zap\x18\v \x01(\bR\x05isZap\x120\n" +
+ "\x05hosts\x18\f \x03(\v2\x1a.kratos.api.Data.MongoHostR\x05hosts\"\x90\x1b\n" +
"\fAdminBackend\x12#\n" +
"\rrouter_prefix\x18\x01 \x01(\tR\frouterPrefix\x12.\n" +
"\x03jwt\x18\x02 \x01(\v2\x1c.kratos.api.AdminBackend.JWTR\x03jwt\x12:\n" +
@@ -1361,7 +2059,10 @@ const file_conf_conf_proto_rawDesc = "" +
"\x05media\x18\b \x01(\v2\x1e.kratos.api.AdminBackend.MediaR\x05media\x12:\n" +
"\tdisk_list\x18\t \x03(\v2\x1d.kratos.api.AdminBackend.DiskR\bdiskList\x127\n" +
"\x06system\x18\n" +
- " \x01(\v2\x1f.kratos.api.AdminBackend.SystemR\x06system\x1a\xb8\x01\n" +
+ " \x01(\v2\x1f.kratos.api.AdminBackend.SystemR\x06system\x12.\n" +
+ "\x03zap\x18\v \x01(\v2\x1c.kratos.api.AdminBackend.ZapR\x03zap\x121\n" +
+ "\x04cors\x18\f \x01(\v2\x1d.kratos.api.AdminBackend.CORSR\x04cors\x12.\n" +
+ "\x03app\x18\r \x01(\v2\x1c.kratos.api.AdminBackend.AppR\x03app\x1a\xb8\x01\n" +
"\x03JWT\x12\x1f\n" +
"\vsigning_key\x18\x01 \x01(\tR\n" +
"signingKey\x12<\n" +
@@ -1394,12 +2095,42 @@ const file_conf_conf_proto_rawDesc = "" +
"sessionTtl\x1a'\n" +
"\x04Disk\x12\x1f\n" +
"\vmount_point\x18\x01 \x01(\tR\n" +
- "mountPoint\x1a\xa6\x01\n" +
+ "mountPoint\x1a\xc3\x01\n" +
"\x06System\x12\x1b\n" +
"\tuse_redis\x18\x01 \x01(\bR\buseRedis\x12%\n" +
"\x0euse_multipoint\x18\x02 \x01(\bR\ruseMultipoint\x12&\n" +
"\x0fuse_strict_auth\x18\x03 \x01(\bR\ruseStrictAuth\x120\n" +
- "\x14disable_auto_migrate\x18\x04 \x01(\bR\x12disableAutoMigrate\x1a\xe8\x03\n" +
+ "\x14disable_auto_migrate\x18\x04 \x01(\bR\x12disableAutoMigrate\x12\x1b\n" +
+ "\tuse_mongo\x18\x05 \x01(\bR\buseMongo\x1a\xf6\x03\n" +
+ "\x03Zap\x12\x14\n" +
+ "\x05level\x18\x01 \x01(\tR\x05level\x12\x16\n" +
+ "\x06prefix\x18\x02 \x01(\tR\x06prefix\x12\x16\n" +
+ "\x06format\x18\x03 \x01(\tR\x06format\x12\x1a\n" +
+ "\bdirector\x18\x04 \x01(\tR\bdirector\x12!\n" +
+ "\fencode_level\x18\x05 \x01(\tR\vencodeLevel\x12%\n" +
+ "\x0estacktrace_key\x18\x06 \x01(\tR\rstacktraceKey\x12\x1b\n" +
+ "\tshow_line\x18\a \x01(\bR\bshowLine\x12$\n" +
+ "\x0elog_in_console\x18\b \x01(\bR\flogInConsole\x12#\n" +
+ "\rretention_day\x18\t \x01(\x05R\fretentionDay\x12&\n" +
+ "\x0faccess_req_body\x18\n" +
+ " \x01(\bR\raccessReqBody\x12(\n" +
+ "\x10access_resp_data\x18\v \x01(\bR\x0eaccessRespData\x12,\n" +
+ "\x12access_req_headers\x18\f \x01(\bR\x10accessReqHeaders\x12/\n" +
+ "\x14access_log_max_bytes\x18\r \x01(\x05R\x11accessLogMaxBytes\x12*\n" +
+ "\x11file_only_modules\x18\x0e \x03(\tR\x0ffileOnlyModules\x1a[\n" +
+ "\x04CORS\x12\x12\n" +
+ "\x04mode\x18\x01 \x01(\tR\x04mode\x12?\n" +
+ "\twhitelist\x18\x02 \x03(\v2!.kratos.api.AdminBackend.CORSRuleR\twhitelist\x1a\xcb\x01\n" +
+ "\bCORSRule\x12!\n" +
+ "\fallow_origin\x18\x01 \x01(\tR\vallowOrigin\x12#\n" +
+ "\rallow_methods\x18\x02 \x01(\tR\fallowMethods\x12#\n" +
+ "\rallow_headers\x18\x03 \x01(\tR\fallowHeaders\x12%\n" +
+ "\x0eexpose_headers\x18\x04 \x01(\tR\rexposeHeaders\x12+\n" +
+ "\x11allow_credentials\x18\x05 \x01(\bR\x10allowCredentials\x1aB\n" +
+ "\x03App\x12\x12\n" +
+ "\x04node\x18\x01 \x01(\tR\x04node\x12\x15\n" +
+ "\x06app_id\x18\x02 \x01(\tR\x05appId\x12\x10\n" +
+ "\x03env\x18\x03 \x01(\tR\x03env\x1a\xe8\x03\n" +
"\aStorage\x12\x12\n" +
"\x04type\x18\x01 \x01(\tR\x04type\x124\n" +
"\x05qiniu\x18\x02 \x01(\v2\x1e.kratos.api.AdminBackend.QiniuR\x05qiniu\x12C\n" +
@@ -1451,7 +2182,7 @@ func file_conf_conf_proto_rawDescGZIP() []byte {
return file_conf_conf_proto_rawDescData
}
-var file_conf_conf_proto_msgTypes = make([]protoimpl.MessageInfo, 17)
+var file_conf_conf_proto_msgTypes = make([]protoimpl.MessageInfo, 23)
var file_conf_conf_proto_goTypes = []any{
(*Bootstrap)(nil), // 0: kratos.api.Bootstrap
(*Server)(nil), // 1: kratos.api.Server
@@ -1460,17 +2191,23 @@ var file_conf_conf_proto_goTypes = []any{
(*Server_HTTP)(nil), // 4: kratos.api.Server.HTTP
(*Data_Database)(nil), // 5: kratos.api.Data.Database
(*Data_Redis)(nil), // 6: kratos.api.Data.Redis
- (*AdminBackend_JWT)(nil), // 7: kratos.api.AdminBackend.JWT
- (*AdminBackend_Captcha)(nil), // 8: kratos.api.AdminBackend.Captcha
- (*AdminBackend_Local)(nil), // 9: kratos.api.AdminBackend.Local
- (*AdminBackend_Email)(nil), // 10: kratos.api.AdminBackend.Email
- (*AdminBackend_Media)(nil), // 11: kratos.api.AdminBackend.Media
- (*AdminBackend_Disk)(nil), // 12: kratos.api.AdminBackend.Disk
- (*AdminBackend_System)(nil), // 13: kratos.api.AdminBackend.System
- (*AdminBackend_Storage)(nil), // 14: kratos.api.AdminBackend.Storage
- (*AdminBackend_Qiniu)(nil), // 15: kratos.api.AdminBackend.Qiniu
- (*AdminBackend_ObjectStore)(nil), // 16: kratos.api.AdminBackend.ObjectStore
- (*durationpb.Duration)(nil), // 17: google.protobuf.Duration
+ (*Data_MongoHost)(nil), // 7: kratos.api.Data.MongoHost
+ (*Data_Mongo)(nil), // 8: kratos.api.Data.Mongo
+ (*AdminBackend_JWT)(nil), // 9: kratos.api.AdminBackend.JWT
+ (*AdminBackend_Captcha)(nil), // 10: kratos.api.AdminBackend.Captcha
+ (*AdminBackend_Local)(nil), // 11: kratos.api.AdminBackend.Local
+ (*AdminBackend_Email)(nil), // 12: kratos.api.AdminBackend.Email
+ (*AdminBackend_Media)(nil), // 13: kratos.api.AdminBackend.Media
+ (*AdminBackend_Disk)(nil), // 14: kratos.api.AdminBackend.Disk
+ (*AdminBackend_System)(nil), // 15: kratos.api.AdminBackend.System
+ (*AdminBackend_Zap)(nil), // 16: kratos.api.AdminBackend.Zap
+ (*AdminBackend_CORS)(nil), // 17: kratos.api.AdminBackend.CORS
+ (*AdminBackend_CORSRule)(nil), // 18: kratos.api.AdminBackend.CORSRule
+ (*AdminBackend_App)(nil), // 19: kratos.api.AdminBackend.App
+ (*AdminBackend_Storage)(nil), // 20: kratos.api.AdminBackend.Storage
+ (*AdminBackend_Qiniu)(nil), // 21: kratos.api.AdminBackend.Qiniu
+ (*AdminBackend_ObjectStore)(nil), // 22: kratos.api.AdminBackend.ObjectStore
+ (*durationpb.Duration)(nil), // 23: google.protobuf.Duration
}
var file_conf_conf_proto_depIdxs = []int32{
1, // 0: kratos.api.Bootstrap.server:type_name -> kratos.api.Server
@@ -1480,32 +2217,39 @@ var file_conf_conf_proto_depIdxs = []int32{
5, // 4: kratos.api.Data.database:type_name -> kratos.api.Data.Database
6, // 5: kratos.api.Data.redis:type_name -> kratos.api.Data.Redis
5, // 6: kratos.api.Data.database_list:type_name -> kratos.api.Data.Database
- 7, // 7: kratos.api.AdminBackend.jwt:type_name -> kratos.api.AdminBackend.JWT
- 8, // 8: kratos.api.AdminBackend.captcha:type_name -> kratos.api.AdminBackend.Captcha
- 9, // 9: kratos.api.AdminBackend.local:type_name -> kratos.api.AdminBackend.Local
- 10, // 10: kratos.api.AdminBackend.email:type_name -> kratos.api.AdminBackend.Email
- 14, // 11: kratos.api.AdminBackend.storage:type_name -> kratos.api.AdminBackend.Storage
- 11, // 12: kratos.api.AdminBackend.media:type_name -> kratos.api.AdminBackend.Media
- 12, // 13: kratos.api.AdminBackend.disk_list:type_name -> kratos.api.AdminBackend.Disk
- 13, // 14: kratos.api.AdminBackend.system:type_name -> kratos.api.AdminBackend.System
- 17, // 15: kratos.api.Server.HTTP.timeout:type_name -> google.protobuf.Duration
- 17, // 16: kratos.api.Data.Redis.read_timeout:type_name -> google.protobuf.Duration
- 17, // 17: kratos.api.Data.Redis.write_timeout:type_name -> google.protobuf.Duration
- 17, // 18: kratos.api.AdminBackend.JWT.expires_time:type_name -> google.protobuf.Duration
- 17, // 19: kratos.api.AdminBackend.JWT.buffer_time:type_name -> google.protobuf.Duration
- 17, // 20: kratos.api.AdminBackend.Captcha.store_expiration:type_name -> google.protobuf.Duration
- 15, // 21: kratos.api.AdminBackend.Storage.qiniu:type_name -> kratos.api.AdminBackend.Qiniu
- 16, // 22: kratos.api.AdminBackend.Storage.aliyun_oss:type_name -> kratos.api.AdminBackend.ObjectStore
- 16, // 23: kratos.api.AdminBackend.Storage.huawei_obs:type_name -> kratos.api.AdminBackend.ObjectStore
- 16, // 24: kratos.api.AdminBackend.Storage.tencent_cos:type_name -> kratos.api.AdminBackend.ObjectStore
- 16, // 25: kratos.api.AdminBackend.Storage.aws_s3:type_name -> kratos.api.AdminBackend.ObjectStore
- 16, // 26: kratos.api.AdminBackend.Storage.cloudflare_r2:type_name -> kratos.api.AdminBackend.ObjectStore
- 16, // 27: kratos.api.AdminBackend.Storage.minio:type_name -> kratos.api.AdminBackend.ObjectStore
- 28, // [28:28] is the sub-list for method output_type
- 28, // [28:28] is the sub-list for method input_type
- 28, // [28:28] is the sub-list for extension type_name
- 28, // [28:28] is the sub-list for extension extendee
- 0, // [0:28] is the sub-list for field type_name
+ 6, // 7: kratos.api.Data.redis_list:type_name -> kratos.api.Data.Redis
+ 8, // 8: kratos.api.Data.mongo:type_name -> kratos.api.Data.Mongo
+ 9, // 9: kratos.api.AdminBackend.jwt:type_name -> kratos.api.AdminBackend.JWT
+ 10, // 10: kratos.api.AdminBackend.captcha:type_name -> kratos.api.AdminBackend.Captcha
+ 11, // 11: kratos.api.AdminBackend.local:type_name -> kratos.api.AdminBackend.Local
+ 12, // 12: kratos.api.AdminBackend.email:type_name -> kratos.api.AdminBackend.Email
+ 20, // 13: kratos.api.AdminBackend.storage:type_name -> kratos.api.AdminBackend.Storage
+ 13, // 14: kratos.api.AdminBackend.media:type_name -> kratos.api.AdminBackend.Media
+ 14, // 15: kratos.api.AdminBackend.disk_list:type_name -> kratos.api.AdminBackend.Disk
+ 15, // 16: kratos.api.AdminBackend.system:type_name -> kratos.api.AdminBackend.System
+ 16, // 17: kratos.api.AdminBackend.zap:type_name -> kratos.api.AdminBackend.Zap
+ 17, // 18: kratos.api.AdminBackend.cors:type_name -> kratos.api.AdminBackend.CORS
+ 19, // 19: kratos.api.AdminBackend.app:type_name -> kratos.api.AdminBackend.App
+ 23, // 20: kratos.api.Server.HTTP.timeout:type_name -> google.protobuf.Duration
+ 23, // 21: kratos.api.Data.Redis.read_timeout:type_name -> google.protobuf.Duration
+ 23, // 22: kratos.api.Data.Redis.write_timeout:type_name -> google.protobuf.Duration
+ 7, // 23: kratos.api.Data.Mongo.hosts:type_name -> kratos.api.Data.MongoHost
+ 23, // 24: kratos.api.AdminBackend.JWT.expires_time:type_name -> google.protobuf.Duration
+ 23, // 25: kratos.api.AdminBackend.JWT.buffer_time:type_name -> google.protobuf.Duration
+ 23, // 26: kratos.api.AdminBackend.Captcha.store_expiration:type_name -> google.protobuf.Duration
+ 18, // 27: kratos.api.AdminBackend.CORS.whitelist:type_name -> kratos.api.AdminBackend.CORSRule
+ 21, // 28: kratos.api.AdminBackend.Storage.qiniu:type_name -> kratos.api.AdminBackend.Qiniu
+ 22, // 29: kratos.api.AdminBackend.Storage.aliyun_oss:type_name -> kratos.api.AdminBackend.ObjectStore
+ 22, // 30: kratos.api.AdminBackend.Storage.huawei_obs:type_name -> kratos.api.AdminBackend.ObjectStore
+ 22, // 31: kratos.api.AdminBackend.Storage.tencent_cos:type_name -> kratos.api.AdminBackend.ObjectStore
+ 22, // 32: kratos.api.AdminBackend.Storage.aws_s3:type_name -> kratos.api.AdminBackend.ObjectStore
+ 22, // 33: kratos.api.AdminBackend.Storage.cloudflare_r2:type_name -> kratos.api.AdminBackend.ObjectStore
+ 22, // 34: kratos.api.AdminBackend.Storage.minio:type_name -> kratos.api.AdminBackend.ObjectStore
+ 35, // [35:35] is the sub-list for method output_type
+ 35, // [35:35] is the sub-list for method input_type
+ 35, // [35:35] is the sub-list for extension type_name
+ 35, // [35:35] is the sub-list for extension extendee
+ 0, // [0:35] is the sub-list for field type_name
}
func init() { file_conf_conf_proto_init() }
@@ -1519,7 +2263,7 @@ func file_conf_conf_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_conf_conf_proto_rawDesc), len(file_conf_conf_proto_rawDesc)),
NumEnums: 0,
- NumMessages: 17,
+ NumMessages: 23,
NumExtensions: 0,
NumServices: 0,
},
diff --git a/internal/conf/conf.proto b/internal/conf/conf.proto
index c3e9d77..0ea2a98 100644
--- a/internal/conf/conf.proto
+++ b/internal/conf/conf.proto
@@ -1,10 +1,10 @@
syntax = "proto3";
package kratos.api;
-option go_package = "kra/internal/conf;conf";
-
import "google/protobuf/duration.proto";
+option go_package = "kra/internal/conf;conf";
+
message Bootstrap {
Server server = 1;
Data data = 2;
@@ -33,16 +33,48 @@ message Data {
string path = 9;
string alias_name = 10;
bool disable = 11;
+ string prefix = 12;
+ string engine = 13;
+ string log_mode = 14;
+ int32 max_idle_conns = 15;
+ int32 max_open_conns = 16;
+ int32 conn_max_lifetime = 17;
+ bool singular = 18;
}
message Redis {
string network = 1;
string addr = 2;
google.protobuf.Duration read_timeout = 3;
google.protobuf.Duration write_timeout = 4;
+ string name = 5;
+ string password = 6;
+ int32 db = 7;
+ bool use_cluster = 8;
+ repeated string cluster_addrs = 9;
+ }
+ message MongoHost {
+ string host = 1;
+ string port = 2;
+ }
+ message Mongo {
+ string coll = 1;
+ string options = 2;
+ string database = 3;
+ string username = 4;
+ string password = 5;
+ string auth_source = 6;
+ uint64 min_pool_size = 7;
+ uint64 max_pool_size = 8;
+ int64 socket_timeout_ms = 9;
+ int64 connect_timeout_ms = 10;
+ bool is_zap = 11;
+ repeated MongoHost hosts = 12;
}
Database database = 1;
Redis redis = 2;
repeated Database database_list = 3;
+ repeated Redis redis_list = 4;
+ Mongo mongo = 5;
}
// AdminBackend contains settings for the administration HTTP transport.
@@ -58,6 +90,9 @@ message AdminBackend {
Media media = 8;
repeated Disk disk_list = 9;
System system = 10;
+ Zap zap = 11;
+ CORS cors = 12;
+ App app = 13;
message JWT {
string signing_key = 1;
@@ -102,6 +137,41 @@ message AdminBackend {
bool use_multipoint = 2;
bool use_strict_auth = 3;
bool disable_auto_migrate = 4;
+ bool use_mongo = 5;
+ }
+
+ message Zap {
+ string level = 1;
+ string prefix = 2;
+ string format = 3;
+ string director = 4;
+ string encode_level = 5;
+ string stacktrace_key = 6;
+ bool show_line = 7;
+ bool log_in_console = 8;
+ int32 retention_day = 9;
+ bool access_req_body = 10;
+ bool access_resp_data = 11;
+ bool access_req_headers = 12;
+ int32 access_log_max_bytes = 13;
+ repeated string file_only_modules = 14;
+ }
+
+ message CORS {
+ string mode = 1;
+ repeated CORSRule whitelist = 2;
+ }
+ message CORSRule {
+ string allow_origin = 1;
+ string allow_methods = 2;
+ string allow_headers = 3;
+ string expose_headers = 4;
+ bool allow_credentials = 5;
+ }
+ message App {
+ string node = 1;
+ string app_id = 2;
+ string env = 3;
}
message Storage {
diff --git a/internal/data/api.go b/internal/data/api.go
index f3c9f27..b76ba2e 100644
--- a/internal/data/api.go
+++ b/internal/data/api.go
@@ -19,10 +19,10 @@ type apiPO struct {
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
- Path string `gorm:"uniqueIndex:idx_api_path_method"`
+ Path string
Description string
APIGroup string `gorm:"column:api_group"`
- Method string `gorm:"uniqueIndex:idx_api_path_method"`
+ Method string
}
func (apiPO) TableName() string { return "sys_apis" }
diff --git a/internal/data/api_token.go b/internal/data/api_token.go
index 0aa78cd..023d281 100644
--- a/internal/data/api_token.go
+++ b/internal/data/api_token.go
@@ -17,7 +17,7 @@ type apiTokenPO struct {
DeletedAt gorm.DeletedAt `gorm:"index"`
UserID uint
AuthorityID uint
- Token string `gorm:"type:text;uniqueIndex"`
+ Token string `gorm:"type:text"`
Status bool
ExpiresAt time.Time
Remark string
diff --git a/internal/data/authority.go b/internal/data/authority.go
index 737393e..27ff989 100644
--- a/internal/data/authority.go
+++ b/internal/data/authority.go
@@ -318,53 +318,52 @@ func (r *accessRepo) DataScopeDepartmentIDs(ctx context.Context, id uint) ([]uin
return ids, err
}
func (r *accessRepo) ResolveDataScope(ctx context.Context, authorityID, userID uint) (biz.DataScope, error) {
- if authorityID == 888 {
- return biz.DataScope{All: true}, nil
- }
+ identity := biz.DataScope{UserID: userID, AuthorityID: authorityID}
+ var user userPO
+ _ = r.data.gormDB.WithContext(ctx).Select("id", "dept_id").First(&user, userID).Error
+ identity.PrimaryDeptID = user.DeptID
var authority authorityPO
- if err := r.data.gormDB.WithContext(ctx).First(&authority, "authority_id = ?", authorityID).Error; err != nil {
- return biz.DataScope{}, err
+ _ = r.data.gormDB.WithContext(ctx).Select("authority_id", "data_scope").First(&authority, "authority_id = ?", authorityID).Error
+ identity.Scope = authority.DataScope
+ if identity.Scope == 0 {
+ identity.Scope = 1
}
- if authority.DataScope == 1 {
- return biz.DataScope{All: true}, nil
- }
- if authority.DataScope == 4 {
- return biz.DataScope{OwnerUserID: userID}, nil
+ identity.All = identity.Scope == 1
+ if identity.Scope == 4 {
+ identity.OwnerUserID = userID
}
var ids []uint
- if authority.DataScope == 5 {
- var err error
- ids, err = r.DataScopeDepartmentIDs(ctx, authorityID)
- if err != nil {
- return biz.DataScope{}, err
- }
- } else {
- if err := r.data.gormDB.WithContext(ctx).Model(&userDepartmentPO{}).Where("sys_user_id = ?", userID).Pluck("sys_department_id", &ids).Error; err != nil {
- return biz.DataScope{}, err
- }
- if authority.DataScope == 2 && len(ids) > 0 {
- var departments []departmentPO
- if err := r.data.gormDB.WithContext(ctx).Find(&departments).Error; err != nil {
- return biz.DataScope{}, err
- }
- selected := make(map[uint]bool, len(ids))
- for _, id := range ids {
- selected[id] = true
- }
- for _, department := range departments {
- for _, part := range strings.Split(department.Ancestors, ",") {
- value, _ := strconv.ParseUint(part, 10, 64)
- if selected[uint(value)] {
- selected[department.ID] = true
- break
- }
+ _ = r.data.gormDB.WithContext(ctx).Model(&userDepartmentPO{}).Where("sys_user_id = ?", userID).Pluck("sys_department_id", &ids).Error
+ selected := make(map[uint]bool, len(ids)+1)
+ for _, id := range ids {
+ selected[id] = true
+ }
+ if user.DeptID != 0 {
+ selected[user.DeptID] = true
+ }
+ ids = ids[:0]
+ for id := range selected {
+ ids = append(ids, id)
+ }
+ if identity.Scope == 2 && len(ids) > 0 {
+ var departments []departmentPO
+ _ = r.data.gormDB.WithContext(ctx).Find(&departments).Error
+ for _, department := range departments {
+ for _, part := range strings.Split(department.Ancestors, ",") {
+ value, _ := strconv.ParseUint(part, 10, 64)
+ if selected[uint(value)] {
+ selected[department.ID] = true
+ break
}
}
- ids = ids[:0]
- for id := range selected {
- ids = append(ids, id)
- }
}
+ ids = ids[:0]
+ for id := range selected {
+ ids = append(ids, id)
+ }
+ } else if identity.Scope == 5 {
+ ids, _ = r.DataScopeDepartmentIDs(ctx, authorityID)
}
- return biz.DataScope{DepartmentIDs: ids}, nil
+ identity.DepartmentIDs = ids
+ return identity, nil
}
diff --git a/internal/data/cache.go b/internal/data/cache.go
index dd7c9bf..d1b0508 100644
--- a/internal/data/cache.go
+++ b/internal/data/cache.go
@@ -28,7 +28,7 @@ func NewCache(data *Data) biz.Cache {
return &cacheStore{data: data, memory: make(map[string]memoryCacheEntry)}
}
-func (s *cacheStore) client() *redis.Client {
+func (s *cacheStore) client() redis.UniversalClient {
return s.data.redis.load()
}
diff --git a/internal/data/config_store.go b/internal/data/config_store.go
index a868ddd..69b3fd9 100644
--- a/internal/data/config_store.go
+++ b/internal/data/config_store.go
@@ -160,6 +160,14 @@ func (d *Data) reloadConfig(ctx context.Context) error {
}
useRedis := next.Admin.System != nil && next.Admin.System.UseRedis
candidateRedis := openRedis(next.Data.Redis, useRedis)
+ useMongo := next.Admin.System != nil && next.Admin.System.UseMongo
+ candidateMongo, mongoErr := openMongo(next.Data.Mongo, useMongo)
+ mongoAccepted := false
+ defer func() {
+ if !mongoAccepted && candidateMongo != nil {
+ _ = candidateMongo.Disconnect(context.Background())
+ }
+ }()
candidateDBList, err := openDatabaseList(next.Data.DatabaseList)
if err != nil {
return err
@@ -168,6 +176,10 @@ func (d *Data) reloadConfig(ctx context.Context) error {
d.gormDB.replace(candidateDB)
d.replaceDatabaseList(candidateDBList)
d.redis.replace(candidateRedis)
+ if mongoErr == nil {
+ d.mongo.replace(candidateMongo)
+ mongoAccepted = true
+ }
d.runtime.Replace(next.Data, next.Admin)
if d.storage != nil {
d.storage.replace(candidateStorage)
diff --git a/internal/data/data.go b/internal/data/data.go
index 4031b5f..67dee5d 100644
--- a/internal/data/data.go
+++ b/internal/data/data.go
@@ -20,6 +20,7 @@ type Data struct {
configMu sync.Mutex
gormDB *reloadableDB
redis *reloadableRedis
+ mongo *reloadableMongo
runtime *conf.Runtime
storage *reloadableStorage
dbListMu sync.RWMutex
@@ -41,6 +42,7 @@ func openDatabaseList(configs []*conf.Data_Database) (map[string]*gorm.DB, error
}
return nil, fmt.Errorf("open database %q: %w", config.AliasName, err)
}
+ registerDataScopeCallbacks(db)
items[config.AliasName] = db
}
return items, nil
@@ -108,28 +110,45 @@ func NewData(runtime *conf.Runtime) (*Data, func(), error) {
}
useRedis := admin != nil && admin.System != nil && admin.System.UseRedis
d.redis = newReloadableRedis(openRedis(c.Redis, useRedis))
+ useMongo := admin != nil && admin.System != nil && admin.System.UseMongo
+ mongoClient, err := openMongo(c.Mongo, useMongo)
+ if err != nil {
+ log.Printf("mongo unavailable: %v", err)
+ mongoClient = nil
+ }
+ d.mongo = newReloadableMongo(mongoClient)
stopConfigWatcher := d.watchConfig()
cleanup := func() {
stopConfigWatcher()
d.gormDB.close()
closeDatabaseList(d.dbList)
d.redis.close()
+ d.mongo.close()
}
return d, cleanup, nil
}
-func openRedis(config *conf.Data_Redis, enabled bool) *redis.Client {
- if !enabled || config == nil || config.Addr == "" {
+func openRedis(config *conf.Data_Redis, enabled bool) redis.UniversalClient {
+ if !enabled || config == nil || (config.Addr == "" && len(config.ClusterAddrs) == 0) {
return nil
}
- options := &redis.Options{Addr: config.Addr, Network: config.Network}
- if config.ReadTimeout != nil {
- options.ReadTimeout = config.ReadTimeout.AsDuration()
+ var candidate redis.UniversalClient
+ if config.UseCluster {
+ addresses := config.ClusterAddrs
+ if len(addresses) == 0 && config.Addr != "" {
+ addresses = []string{config.Addr}
+ }
+ candidate = redis.NewClusterClient(&redis.ClusterOptions{Addrs: addresses, Password: config.Password})
+ } else {
+ options := &redis.Options{Addr: config.Addr, Network: config.Network, Password: config.Password, DB: int(config.Db)}
+ if config.ReadTimeout != nil {
+ options.ReadTimeout = config.ReadTimeout.AsDuration()
+ }
+ if config.WriteTimeout != nil {
+ options.WriteTimeout = config.WriteTimeout.AsDuration()
+ }
+ candidate = redis.NewClient(options)
}
- if config.WriteTimeout != nil {
- options.WriteTimeout = config.WriteTimeout.AsDuration()
- }
- candidate := redis.NewClient(options)
pingCtx, cancel := context.WithTimeout(context.Background(), 800*time.Millisecond)
defer cancel()
if err := candidate.Ping(pingCtx).Err(); err != nil {
diff --git a/internal/data/data_scope.go b/internal/data/data_scope.go
new file mode 100644
index 0000000..9cf2c9c
--- /dev/null
+++ b/internal/data/data_scope.go
@@ -0,0 +1,230 @@
+package data
+
+import (
+ "database/sql"
+ "log"
+ "reflect"
+ "strings"
+
+ "kra/internal/biz"
+
+ "gorm.io/gorm"
+ "gorm.io/gorm/clause"
+ "gorm.io/gorm/schema"
+)
+
+// registerDataScopeCallbacks installs the global GORM data-scope engine.
+// System tables are deliberately excluded: their access is controlled by
+// Casbin, while ownership columns on business tables are row-level scope.
+func registerDataScopeCallbacks(db *gorm.DB) {
+ if db == nil {
+ return
+ }
+ q := db.Callback().Query()
+ if q.Get("data_scope:query") == nil {
+ _ = q.Before("gorm:query").Register("data_scope:query", applyDataScope("query"))
+ }
+ u := db.Callback().Update()
+ if u.Get("data_scope:update") == nil {
+ _ = u.Before("gorm:update").Register("data_scope:update", applyDataScope("update"))
+ _ = u.Before("gorm:update").Register("data_scope:stamp_update", stampUpdatedBy)
+ _ = u.After("gorm:update").Register("data_scope:audit_update", auditBlockedWrite("update"))
+ }
+ d := db.Callback().Delete()
+ if d.Get("data_scope:delete") == nil {
+ _ = d.Before("gorm:delete").Register("data_scope:delete", applyDataScope("delete"))
+ _ = d.Before("gorm:delete").After("data_scope:delete").Register("data_scope:stamp_delete", stampDeletedBy)
+ _ = d.After("gorm:delete").Register("data_scope:audit_delete", auditBlockedWrite("delete"))
+ }
+ c := db.Callback().Create()
+ if c.Get("data_scope:stamp") == nil {
+ _ = c.Before("gorm:create").Register("data_scope:stamp", stampOwnership)
+ }
+}
+
+func hasScopeField(db *gorm.DB, name string) bool {
+ return db.Statement.Schema != nil && db.Statement.Schema.LookUpField(name) != nil
+}
+
+func isControlledTable(db *gorm.DB) bool {
+ return db.Statement.Schema != nil && !strings.HasPrefix(db.Statement.Table, "sys_") && (hasScopeField(db, "dept_id") || hasScopeField(db, "created_by"))
+}
+
+func applyDataScope(operation string) func(*gorm.DB) {
+ return func(db *gorm.DB) {
+ if !isControlledTable(db) {
+ return
+ }
+ if _, done := db.Statement.Clauses["data_scope:applied"]; done {
+ return
+ }
+ if skip, ok := db.Get("data_scope:skip"); ok {
+ if value, _ := skip.(bool); value {
+ return
+ }
+ }
+ scope, ok := biz.DataScopeFromContext(db.Statement.Context)
+ if !ok {
+ log.Printf("数据权限: 业务表[%s]访问无身份上下文, 已放行(待补 ctx / 或使用系统上下文)", db.Statement.Table)
+ recordDataScopeEvent(db, "no_identity", operation, "无身份上下文访问受控表, 已放行", biz.DataScope{})
+ return
+ }
+ if (operation == "update" || operation == "delete") && !db.AllowGlobalUpdate && !hasWriteConditions(db) {
+ return
+ }
+ db.Statement.Clauses["data_scope:applied"] = clause.Clause{}
+ if scope.All {
+ return
+ }
+ table := db.Statement.Table
+ if scope.OwnerUserID != 0 && hasScopeField(db, "created_by") {
+ db.Where(table+".created_by = ?", scope.OwnerUserID)
+ return
+ }
+ if hasScopeField(db, "dept_id") {
+ ids := scope.DepartmentIDs
+ if len(ids) == 0 {
+ ids = []uint{0}
+ }
+ db.Where(table+".dept_id IN ?", ids)
+ }
+ }
+}
+
+func recordDataScopeEvent(db *gorm.DB, eventType, operation, detail string, scope biz.DataScope) {
+ record := &dataAccessLogPO{EventType: eventType, TargetTable: db.Statement.Table, Operation: operation, UserID: scope.UserID, AuthorityID: scope.AuthorityID, Scope: scope.Scope, RequestID: scope.RequestID, Method: scope.Method, Path: scope.Path, Detail: detail}
+ _ = db.Session(&gorm.Session{NewDB: true, SkipHooks: true}).Create(record).Error
+}
+
+func auditBlockedWrite(operation string) func(*gorm.DB) {
+ return func(db *gorm.DB) {
+ if _, applied := db.Statement.Clauses["data_scope:applied"]; !applied || db.Error != nil || db.RowsAffected != 0 {
+ return
+ }
+ if scope, ok := biz.DataScopeFromContext(db.Statement.Context); ok && !scope.All {
+ recordDataScopeEvent(db, "blocked_write", operation, "数据范围过滤后写操作影响 0 行(疑似越权尝试)", scope)
+ }
+ }
+}
+
+func stampOwnership(db *gorm.DB) {
+ if !isControlledTable(db) {
+ return
+ }
+ scope, ok := biz.DataScopeFromContext(db.Statement.Context)
+ if !ok {
+ return
+ }
+ if hasScopeField(db, "created_by") && scope.UserID != 0 {
+ db.Statement.SetColumn("created_by", scope.UserID, true)
+ }
+ if hasScopeField(db, "dept_id") && scope.PrimaryDeptID != 0 {
+ db.Statement.SetColumn("dept_id", scope.PrimaryDeptID, true)
+ }
+}
+
+func stampUpdatedBy(db *gorm.DB) {
+ stmt := db.Statement
+ if !isControlledTable(db) || stmt.SkipHooks || !hasScopeField(db, "updated_by") {
+ return
+ }
+ scope, ok := biz.DataScopeFromContext(db.Statement.Context)
+ if !ok || scope.UserID == 0 {
+ return
+ }
+ if _, isMap := stmt.Dest.(map[string]interface{}); !isMap {
+ if _, isMaps := stmt.Dest.([]map[string]interface{}); !isMaps && stmt.ReflectValue.Kind() == reflect.Struct && !stmt.ReflectValue.CanAddr() {
+ return
+ }
+ }
+ for _, omitted := range stmt.Omits {
+ if omitted == "updated_by" {
+ return
+ }
+ }
+ if len(stmt.Selects) > 0 {
+ found := false
+ for _, selected := range stmt.Selects {
+ if selected == "updated_by" || selected == "*" {
+ found = true
+ break
+ }
+ }
+ if !found {
+ stmt.Selects = append(stmt.Selects, "updated_by")
+ }
+ }
+ stmt.SetColumn("updated_by", scope.UserID, true)
+}
+
+func stampDeletedBy(db *gorm.DB) {
+ stmt := db.Statement
+ if db.Error != nil || !isControlledTable(db) || stmt.SQL.Len() != 0 || stmt.Unscoped || !hasScopeField(db, "deleted_by") {
+ return
+ }
+ deletedAtType := reflect.TypeOf(gorm.DeletedAt{})
+ var deletedAt *schema.Field
+ for _, field := range stmt.Schema.Fields {
+ if field.FieldType == deletedAtType || field.IndirectFieldType == deletedAtType {
+ deletedAt = field
+ break
+ }
+ }
+ if deletedAt == nil {
+ return
+ }
+ if _, customZero := deletedAt.TagSettings["ZEROVALUE"]; customZero {
+ return
+ }
+ scope, ok := biz.DataScopeFromContext(stmt.Context)
+ if !ok || scope.UserID == 0 {
+ return
+ }
+ var primaryExpressions []clause.Expression
+ _, queryValues := schema.GetIdentityFieldValuesMap(stmt.Context, stmt.ReflectValue, stmt.Schema.PrimaryFields)
+ column, values := schema.ToQueryValues(stmt.Table, stmt.Schema.PrimaryFieldDBNames, queryValues)
+ if len(values) > 0 {
+ primaryExpressions = append(primaryExpressions, clause.IN{Column: column, Values: values})
+ }
+ if stmt.ReflectValue.CanAddr() && stmt.Dest != stmt.Model && stmt.Model != nil {
+ _, queryValues = schema.GetIdentityFieldValuesMap(stmt.Context, reflect.ValueOf(stmt.Model), stmt.Schema.PrimaryFields)
+ column, values = schema.ToQueryValues(stmt.Table, stmt.Schema.PrimaryFieldDBNames, queryValues)
+ if len(values) > 0 {
+ primaryExpressions = append(primaryExpressions, clause.IN{Column: column, Values: values})
+ }
+ }
+ if _, hasWhere := stmt.Clauses["WHERE"]; !hasWhere && len(primaryExpressions) == 0 && !db.AllowGlobalUpdate {
+ return
+ }
+ now := db.NowFunc()
+ stmt.AddClause(clause.Set{{Column: clause.Column{Name: deletedAt.DBName}, Value: now}, {Column: clause.Column{Name: "deleted_by"}, Value: scope.UserID}})
+ stmt.SetColumn(deletedAt.DBName, now, true)
+ stmt.SetColumn("deleted_by", scope.UserID, true)
+ if len(primaryExpressions) > 0 {
+ stmt.AddClause(clause.Where{Exprs: primaryExpressions})
+ }
+ gorm.SoftDeleteQueryClause{ZeroValue: sql.NullString{Valid: false}, Field: deletedAt}.ModifyStatement(stmt)
+ stmt.AddClauseIfNotExists(clause.Update{})
+ stmt.Build(stmt.DB.Callback().Update().Clauses...)
+}
+
+func hasWriteConditions(db *gorm.DB) bool {
+ if value, ok := db.Statement.Clauses["WHERE"]; ok {
+ if where, ok := value.Expression.(clause.Where); ok && len(where.Exprs) > 0 {
+ return true
+ }
+ }
+ if db.Statement.Schema == nil || !db.Statement.ReflectValue.IsValid() {
+ return false
+ }
+ _, values := schema.GetIdentityFieldValuesMap(db.Statement.Context, db.Statement.ReflectValue, db.Statement.Schema.PrimaryFields)
+ if _, query := schema.ToQueryValues(db.Statement.Table, db.Statement.Schema.PrimaryFieldDBNames, values); len(query) > 0 {
+ return true
+ }
+ if db.Statement.ReflectValue.CanAddr() && db.Statement.Dest != db.Statement.Model && db.Statement.Model != nil {
+ _, values = schema.GetIdentityFieldValuesMap(db.Statement.Context, reflect.ValueOf(db.Statement.Model), db.Statement.Schema.PrimaryFields)
+ _, query := schema.ToQueryValues(db.Statement.Table, db.Statement.Schema.PrimaryFieldDBNames, values)
+ return len(query) > 0
+ }
+ return false
+}
diff --git a/internal/data/database.go b/internal/data/database.go
index b89a82f..c363ea5 100644
--- a/internal/data/database.go
+++ b/internal/data/database.go
@@ -8,6 +8,7 @@ import (
"path/filepath"
"regexp"
"strings"
+ "time"
oracle "github.com/dzwvip/gorm-oracle"
"github.com/glebarez/sqlite"
@@ -15,6 +16,8 @@ import (
"gorm.io/driver/postgres"
"gorm.io/driver/sqlserver"
"gorm.io/gorm"
+ "gorm.io/gorm/logger"
+ "gorm.io/gorm/schema"
"kra/internal/conf"
)
@@ -99,21 +102,61 @@ func databaseDSN(c *conf.Data_Database, name string) (string, error) {
return "", fmt.Errorf("unsupported database driver %q", c.Driver)
}
+func gormConfig(config *conf.Data_Database) *gorm.Config {
+ level := logger.Info
+ switch strings.ToLower(config.LogMode) {
+ case "silent":
+ level = logger.Silent
+ case "error":
+ level = logger.Error
+ case "warn":
+ level = logger.Warn
+ }
+ return &gorm.Config{Logger: logger.Default.LogMode(level), NamingStrategy: schema.NamingStrategy{TablePrefix: config.Prefix, SingularTable: config.Singular}}
+}
+
func openWithDriver(driver, dsn string) (*gorm.DB, error) {
+ return openWithDriverConfig(driver, dsn, &conf.Data_Database{Driver: driver})
+}
+
+func openWithDriverConfig(driver, dsn string, config *conf.Data_Database) (*gorm.DB, error) {
+ gormConfig := gormConfig(config)
+ var db *gorm.DB
+ var err error
switch normalizedDriver(driver) {
case "mysql":
- return gorm.Open(mysql.Open(dsn), &gorm.Config{})
+ db, err = gorm.Open(mysql.Open(dsn), gormConfig)
case "pgsql":
- return gorm.Open(postgres.Open(dsn), &gorm.Config{})
+ db, err = gorm.Open(postgres.Open(dsn), gormConfig)
case "mssql":
- return gorm.Open(sqlserver.Open(dsn), &gorm.Config{})
+ db, err = gorm.Open(sqlserver.Open(dsn), gormConfig)
case "oracle":
- return gorm.Open(oracle.Open(dsn), &gorm.Config{})
+ db, err = gorm.Open(oracle.Open(dsn), gormConfig)
case "sqlite":
- return gorm.Open(sqlite.Open(dsn), &gorm.Config{})
+ db, err = gorm.Open(sqlite.Open(dsn), gormConfig)
default:
return nil, fmt.Errorf("unsupported database driver %q", driver)
}
+ if err != nil {
+ return nil, err
+ }
+ sqlDB, err := db.DB()
+ if err != nil {
+ return nil, err
+ }
+ if config.MaxIdleConns > 0 {
+ sqlDB.SetMaxIdleConns(int(config.MaxIdleConns))
+ }
+ if config.MaxOpenConns > 0 {
+ sqlDB.SetMaxOpenConns(int(config.MaxOpenConns))
+ }
+ if config.ConnMaxLifetime > 0 {
+ sqlDB.SetConnMaxLifetime(time.Duration(config.ConnMaxLifetime) * time.Second)
+ }
+ if config.Engine != "" && (normalizedDriver(driver) == "mysql" || normalizedDriver(driver) == "mssql") {
+ db = db.Set("gorm:table_options", "ENGINE="+config.Engine)
+ }
+ return db, nil
}
func openDatabase(c *conf.Data_Database, create bool, template string) (*gorm.DB, error) {
@@ -129,7 +172,7 @@ func openDatabase(c *conf.Data_Database, create bool, template string) (*gorm.DB
if err = os.MkdirAll(filepath.Dir(dsn), 0o755); err != nil {
return nil, err
}
- return openWithDriver(driver, dsn)
+ return openWithDriverConfig(driver, dsn, c)
}
if create && driver != "oracle" {
if !databaseNamePattern.MatchString(c.Name) {
@@ -146,7 +189,7 @@ func openDatabase(c *conf.Data_Database, create bool, template string) (*gorm.DB
if err != nil {
return nil, err
}
- adminDB, err := openWithDriver(driver, dsn)
+ adminDB, err := openWithDriverConfig(driver, dsn, c)
if err != nil {
return nil, fmt.Errorf("connect database server: %w", err)
}
@@ -182,7 +225,7 @@ func openDatabase(c *conf.Data_Database, create bool, template string) (*gorm.DB
if err != nil {
return nil, err
}
- return openWithDriver(driver, dsn)
+ return openWithDriverConfig(driver, dsn, c)
}
func openFallbackDatabase() (*gorm.DB, error) {
diff --git a/internal/data/dictionary.go b/internal/data/dictionary.go
index 6360d90..88628d6 100644
--- a/internal/data/dictionary.go
+++ b/internal/data/dictionary.go
@@ -18,7 +18,7 @@ type dictionaryPO struct {
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
Name string
- Type string `gorm:"uniqueIndex"`
+ Type string
Status bool
Desc string
ParentID *uint
diff --git a/internal/data/email.go b/internal/data/email.go
index 8665729..f925c14 100644
--- a/internal/data/email.go
+++ b/internal/data/email.go
@@ -45,14 +45,7 @@ func (r *emailRepo) DefaultRecipients() []string {
if config == nil {
return nil
}
- parts := strings.Split(config.To, ",")
- result := make([]string, 0, len(parts))
- for _, part := range parts {
- if recipient := strings.TrimSpace(part); recipient != "" {
- result = append(result, recipient)
- }
- }
- return result
+ return []string{config.To}
}
func cleanHeader(value string) string {
diff --git a/internal/data/export.go b/internal/data/export.go
index 0c8155c..41e4a4b 100644
--- a/internal/data/export.go
+++ b/internal/data/export.go
@@ -20,7 +20,7 @@ type exportTemplatePO struct {
DBName string
Name string
DBTableName string `gorm:"column:table_name"`
- TemplateID string `gorm:"uniqueIndex"`
+ TemplateID string
TemplateInfo string `gorm:"type:text"`
SQL string `gorm:"type:text"`
ImportSQL string `gorm:"type:text"`
diff --git a/internal/data/migrations.go b/internal/data/migrations.go
index cf70f9e..01e6c6b 100644
--- a/internal/data/migrations.go
+++ b/internal/data/migrations.go
@@ -1,9 +1,13 @@
package data
-import "gorm.io/gorm"
+import (
+ "fmt"
+
+ "gorm.io/gorm"
+)
func migrateAll(db *gorm.DB) error {
- return db.AutoMigrate(
+ if err := db.AutoMigrate(
&userPO{}, &authorityPO{}, &menuPO{}, &userAuthorityPO{}, &authorityMenuPO{}, &menuParameterPO{},
&apiPO{}, &ignoredAPIPO{}, &authorityAPIPO{}, &menuButtonPO{}, &authorityButtonPO{},
&departmentPO{}, &positionPO{}, &userDepartmentPO{}, &userPositionPO{}, &authorityDepartmentPO{},
@@ -12,5 +16,66 @@ func migrateAll(db *gorm.DB) error {
&operationPO{}, &loginLogPO{}, &dataAccessLogPO{}, &errorRecordPO{},
&taskPO{}, &taskLogPO{}, &mediaPO{}, &categoryPO{}, &uploadSessionPO{}, &uploadChunkPO{},
&announcementPO{},
- )
+ ); err != nil {
+ return err
+ }
+ return reconcileReferenceIndexes(db)
+}
+
+// reconcileReferenceIndexes removes constraints created by older Kra builds
+// that are not part of the administration data model. Business services own
+// duplicate checks and their user-facing error messages.
+func reconcileReferenceIndexes(db *gorm.DB) error {
+ obsolete := []struct {
+ model any
+ name string
+ }{
+ {&apiPO{}, "idx_api_path_method"},
+ {&dictionaryPO{}, "idx_sys_dictionaries_type"},
+ {¶meterPO{}, "idx_sys_params_key"},
+ {&apiTokenPO{}, "idx_sys_api_tokens_token"},
+ {&exportTemplatePO{}, "idx_sys_export_templates_template_id"},
+ }
+ for _, item := range obsolete {
+ if db.Migrator().HasIndex(item.model, item.name) {
+ if err := db.Migrator().DropIndex(item.model, item.name); err != nil {
+ return fmt.Errorf("drop obsolete index %s: %w", item.name, err)
+ }
+ }
+ }
+ for _, item := range []struct {
+ name string
+ field string
+ }{{"idx_sys_users_uuid", "UUID"}, {"idx_sys_users_username", "Username"}} {
+ unique, err := indexIsUnique(db, &userPO{}, item.name)
+ if err != nil {
+ return err
+ }
+ if !unique {
+ continue
+ }
+ if err = db.Migrator().DropIndex(&userPO{}, item.name); err != nil {
+ return fmt.Errorf("drop legacy unique index %s: %w", item.name, err)
+ }
+ if err = db.Migrator().CreateIndex(&userPO{}, item.field); err != nil {
+ return fmt.Errorf("create reference index %s: %w", item.name, err)
+ }
+ }
+ return nil
+}
+
+func indexIsUnique(db *gorm.DB, model any, name string) (bool, error) {
+ indexes, err := db.Migrator().GetIndexes(model)
+ if err != nil {
+ // Some third-party GORM drivers do not implement index inspection.
+ // Fresh schemas are already correct; skip only the legacy repair there.
+ return false, nil
+ }
+ for _, index := range indexes {
+ if index.Name() == name {
+ unique, known := index.Unique()
+ return known && unique, nil
+ }
+ }
+ return false, nil
}
diff --git a/internal/data/mongo.go b/internal/data/mongo.go
new file mode 100644
index 0000000..a1c7d7f
--- /dev/null
+++ b/internal/data/mongo.go
@@ -0,0 +1,63 @@
+package data
+
+import (
+ "context"
+ "fmt"
+ "strings"
+ "time"
+
+ "kra/internal/conf"
+
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+func mongoURI(config *conf.Data_Mongo) string {
+ hosts := make([]string, 0, len(config.Hosts))
+ for _, host := range config.Hosts {
+ if host != nil && host.Host != "" && host.Port != "" {
+ hosts = append(hosts, host.Host+":"+host.Port)
+ }
+ }
+ uri := "mongodb://" + strings.Join(hosts, ",") + "/" + config.Database
+ if config.Options != "" {
+ uri += "?" + config.Options
+ }
+ return uri
+}
+
+func openMongo(config *conf.Data_Mongo, enabled bool) (*mongo.Client, error) {
+ if !enabled {
+ return nil, nil
+ }
+ if config == nil || len(config.Hosts) == 0 {
+ return nil, fmt.Errorf("mongo hosts are required")
+ }
+ clientOptions := options.Client().ApplyURI(mongoURI(config))
+ if config.Username != "" && config.Password != "" {
+ clientOptions.SetAuth(options.Credential{Username: config.Username, Password: config.Password, AuthSource: config.AuthSource})
+ }
+ if config.MinPoolSize > 0 {
+ clientOptions.SetMinPoolSize(config.MinPoolSize)
+ }
+ if config.MaxPoolSize > 0 {
+ clientOptions.SetMaxPoolSize(config.MaxPoolSize)
+ }
+ if config.ConnectTimeoutMs > 0 {
+ clientOptions.SetConnectTimeout(time.Duration(config.ConnectTimeoutMs) * time.Millisecond)
+ }
+ if config.SocketTimeoutMs > 0 {
+ clientOptions.SetSocketTimeout(time.Duration(config.SocketTimeoutMs) * time.Millisecond)
+ }
+ ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
+ defer cancel()
+ client, err := mongo.Connect(ctx, clientOptions)
+ if err != nil {
+ return nil, err
+ }
+ if err = client.Ping(ctx, nil); err != nil {
+ _ = client.Disconnect(context.Background())
+ return nil, err
+ }
+ return client, nil
+}
diff --git a/internal/data/parameter.go b/internal/data/parameter.go
index 03722e7..469d592 100644
--- a/internal/data/parameter.go
+++ b/internal/data/parameter.go
@@ -16,7 +16,7 @@ type parameterPO struct {
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
Name string
- Key string `gorm:"uniqueIndex"`
+ Key string
Value string
Desc string
}
diff --git a/internal/data/runtime_clients.go b/internal/data/runtime_clients.go
index 2995d81..7891df1 100644
--- a/internal/data/runtime_clients.go
+++ b/internal/data/runtime_clients.go
@@ -6,6 +6,7 @@ import (
"sync/atomic"
"github.com/redis/go-redis/v9"
+ "go.mongodb.org/mongo-driver/mongo"
"gorm.io/gorm"
)
@@ -17,8 +18,40 @@ type reloadableDB struct {
retired []*gorm.DB
}
+type reloadableMongo struct {
+ mu sync.RWMutex
+ current *mongo.Client
+ retired []*mongo.Client
+}
+
+func newReloadableMongo(client *mongo.Client) *reloadableMongo {
+ return &reloadableMongo{current: client}
+}
+func (r *reloadableMongo) replace(client *mongo.Client) {
+ r.mu.Lock()
+ old := r.current
+ r.current = client
+ if old != nil && old != client {
+ r.retired = append(r.retired, old)
+ }
+ r.mu.Unlock()
+}
+func (r *reloadableMongo) close() {
+ r.mu.Lock()
+ all := append([]*mongo.Client{r.current}, r.retired...)
+ r.current = nil
+ r.retired = nil
+ r.mu.Unlock()
+ for _, client := range all {
+ if client != nil {
+ _ = client.Disconnect(context.Background())
+ }
+ }
+}
+
func newReloadableDB(db *gorm.DB) *reloadableDB {
r := &reloadableDB{}
+ registerDataScopeCallbacks(db)
r.current.Store(db)
return r
}
@@ -30,6 +63,7 @@ func (r *reloadableDB) WithContext(ctx context.Context) *gorm.DB {
func (r *reloadableDB) DB() *gorm.DB { return r.current.Load() }
func (r *reloadableDB) replace(db *gorm.DB) {
+ registerDataScopeCallbacks(db)
old := r.current.Swap(db)
if old != nil && old != db {
r.mu.Lock()
@@ -60,45 +94,41 @@ func (r *reloadableDB) close() {
}
type reloadableRedis struct {
- current atomic.Pointer[redis.Client]
- mu sync.Mutex
- retired []*redis.Client
+ mu sync.RWMutex
+ current redis.UniversalClient
+ retired []redis.UniversalClient
}
-func newReloadableRedis(client *redis.Client) *reloadableRedis {
- r := &reloadableRedis{}
- if client != nil {
- r.current.Store(client)
- }
- return r
+func newReloadableRedis(client redis.UniversalClient) *reloadableRedis {
+ return &reloadableRedis{current: client}
}
-func (r *reloadableRedis) load() *redis.Client { return r.current.Load() }
+func (r *reloadableRedis) load() redis.UniversalClient {
+ r.mu.RLock()
+ defer r.mu.RUnlock()
+ return r.current
+}
-func (r *reloadableRedis) replace(client *redis.Client) {
- old := r.current.Swap(client)
+func (r *reloadableRedis) replace(client redis.UniversalClient) {
+ r.mu.Lock()
+ old := r.current
+ r.current = client
if old != nil && old != client {
- r.mu.Lock()
r.retired = append(r.retired, old)
- r.mu.Unlock()
}
+ r.mu.Unlock()
}
func (r *reloadableRedis) close() {
- current := r.current.Load()
r.mu.Lock()
- all := append([]*redis.Client{current}, r.retired...)
+ all := append([]redis.UniversalClient{r.current}, r.retired...)
+ r.current = nil
r.retired = nil
r.mu.Unlock()
- seen := map[*redis.Client]struct{}{}
for _, client := range all {
if client == nil {
continue
}
- if _, ok := seen[client]; ok {
- continue
- }
- seen[client] = struct{}{}
_ = client.Close()
}
}
diff --git a/internal/data/system.go b/internal/data/system.go
index 24936a0..0968229 100644
--- a/internal/data/system.go
+++ b/internal/data/system.go
@@ -13,8 +13,8 @@ type userPO struct {
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
- UUID string `gorm:"type:char(36);uniqueIndex"`
- Username string `gorm:"index;uniqueIndex"`
+ UUID string `gorm:"type:char(36);index"`
+ Username string `gorm:"index"`
Password string
NickName string `gorm:"column:nick_name"`
HeaderImg string `gorm:"column:header_img"`
diff --git a/internal/data/system_init.go b/internal/data/system_init.go
index 2967abe..6b7cb61 100644
--- a/internal/data/system_init.go
+++ b/internal/data/system_init.go
@@ -182,6 +182,17 @@ func (r *systemRepo) Initialize(ctx context.Context, input *biz.DatabaseConfig)
return err
}
}
+ for _, ignored := range []ignoredAPIPO{
+ {Method: "GET", Path: "/api/freshCasbin"}, {Method: "GET", Path: "/health"},
+ {Method: "POST", Path: "/system/reloadSystem"}, {Method: "POST", Path: "/base/login"},
+ {Method: "POST", Path: "/base/captcha"}, {Method: "POST", Path: "/init/initdb"},
+ {Method: "POST", Path: "/init/checkdb"}, {Method: "GET", Path: "/info/getInfoDataSource"},
+ {Method: "GET", Path: "/info/getInfoPublic"},
+ } {
+ if err := tx.FirstOrCreate(&ignored, ignored).Error; err != nil {
+ return err
+ }
+ }
return nil
}); err != nil {
return err
@@ -204,11 +215,16 @@ func defaultMenus() []menuPO {
child := func(parent, path, name, component, title, icon string, sort int) menuPO {
return menuPO{MenuLevel: 1, Path: path, Name: name, Component: component, Title: title, Icon: icon, Sort: sort, ActiveName: parent}
}
+ cachedChild := func(parent, path, name, component, title, icon string, sort int) menuPO {
+ value := child(parent, path, name, component, title, icon, sort)
+ value.KeepAlive = true
+ return value
+ }
return []menuPO{
{Path: "dashboard", Name: "dashboard", Component: "view/dashboard/index.vue", Title: "仪表盘", Icon: "odometer", Sort: 1},
root("permission", "permission", "权限管理", "perm-kra", 2), root("org", "org", "组织管理", "share", 3), root("systemConfig", "systemConfig", "系统设置", "config-kra", 4), root("monitor", "monitor", "运维监控", "monitor-kra", 5), root("media", "media", "媒体管理", "folder-opened", 6), root("extensions", "extensions", "扩展功能", "cherry", 10),
{Path: "person", Name: "person", Component: "view/person/person.vue", Title: "个人信息", Icon: "postcard", Hidden: true, Sort: 13},
- child("permission", "authority", "authority", "view/superAdmin/authority/authority.vue", "角色管理", "role-kra", 1), child("permission", "menu", "menu", "view/superAdmin/menu/menu.vue", "菜单管理", "tickets", 2), child("permission", "api", "api", "view/superAdmin/api/api.vue", "api管理", "api-kra", 3), child("permission", "apiToken", "apiToken", "view/systemTools/apiToken/index.vue", "API Token", "key", 4),
+ child("permission", "authority", "authority", "view/superAdmin/authority/authority.vue", "角色管理", "role-kra", 1), cachedChild("permission", "menu", "menu", "view/superAdmin/menu/menu.vue", "菜单管理", "tickets", 2), cachedChild("permission", "api", "api", "view/superAdmin/api/api.vue", "api管理", "api-kra", 3), child("permission", "apiToken", "apiToken", "view/systemTools/apiToken/index.vue", "API Token", "key", 4),
child("org", "user", "user", "view/superAdmin/user/user.vue", "用户管理", "user", 1), child("org", "department", "department", "view/superAdmin/department/department.vue", "部门管理", "office-building", 2), child("org", "position", "position", "view/superAdmin/position/position.vue", "岗位管理", "postcard", 3),
child("systemConfig", "system", "system", "view/systemTools/system/system.vue", "配置文件", "config-file-kra", 1), child("systemConfig", "dictionary", "dictionary", "view/superAdmin/dictionary/sysDictionary.vue", "字典管理", "notebook", 2), child("systemConfig", "sysParams", "sysParams", "view/superAdmin/params/sysParams.vue", "参数管理", "set-up", 3), child("systemConfig", "security", "security", "view/system/security/index.vue", "安全配置", "security-kra", 4),
child("monitor", "operation", "operation", "view/superAdmin/operation/sysOperationRecord.vue", "操作历史", "document", 1), child("monitor", "loginLog", "loginLog", "view/systemTools/loginLog/index.vue", "登录日志", "clock", 2), child("monitor", "sysError", "sysError", "view/systemTools/sysError/sysError.vue", "错误日志", "error-kra", 3), child("monitor", "sysVersion", "sysVersion", "view/systemTools/version/version.vue", "版本管理", "version-kra", 4), child("monitor", "state", "state", "view/system/state.vue", "服务器状态", "server", 5), child("monitor", "dataAccessLog", "dataAccessLog", "view/superAdmin/dataAccessLog/dataAccessLog.vue", "数据权限审计", "warning", 6), child("monitor", "timedTask", "timedTask", "view/systemTools/timedTask/index.vue", "定时任务", "timer", 7), child("monitor", "logViewer", "logViewer", "view/systemTools/logViewer/index.vue", "文件日志", "document", 8),
diff --git a/internal/server/gin.go b/internal/server/gin.go
index d88886b..58a024e 100644
--- a/internal/server/gin.go
+++ b/internal/server/gin.go
@@ -20,10 +20,10 @@ import (
kratoshttp "github.com/go-kratos/kratos/v3/transport/http"
)
-func NewGinServer(c *conf.Server, runtime *conf.Runtime, system *service.SystemService, access *service.AccessService, authority *handler.Authority, menu *handler.Menu, api *handler.API, permission *handler.Permission, organization *handler.Organization, announcement *handler.Announcement, email *handler.Email, task *handler.Task, media *handler.Media, auditHandler *handler.Audit, export *handler.Export, version *handler.Version, dictionary *handler.Dictionary, parameter *handler.Parameter, apiToken *handler.APIToken, systemConfig *handler.SystemConfig, publicHandler *handler.Public, user *handler.User, navigation *handler.Navigation, session *handler.Session, settings *service.SettingsService, audit *service.AuditService, emails *service.EmailService, logger *slog.Logger) *kratoshttp.Server {
+func NewGinServer(c *conf.Server, runtime *conf.Runtime, system *service.SystemService, access *service.AccessService, authority *handler.Authority, menu *handler.Menu, api *handler.API, permission *handler.Permission, organization *handler.Organization, announcement *handler.Announcement, email *handler.Email, task *handler.Task, media *handler.Media, auditHandler *handler.Audit, export *handler.Export, version *handler.Version, dictionary *handler.Dictionary, parameter *handler.Parameter, apiToken *handler.APIToken, systemConfig *handler.SystemConfig, publicHandler *handler.Public, user *handler.User, navigation *handler.Navigation, session *handler.Session, settings *service.SettingsService, audit *service.AuditService, logger *slog.Logger) *kratoshttp.Server {
gin.SetMode(gin.ReleaseMode)
engine := gin.New()
- engine.Use(servermiddleware.RequestMeta(), servermiddleware.EmailErrorAlert(emails, logger), gin.Recovery(), servermiddleware.SecurityRateLimit(system, settings), servermiddleware.OperationAudit(audit))
+ engine.Use(servermiddleware.RequestMeta(), servermiddleware.Recovery(audit, logger), servermiddleware.AccessLog(runtime, logger), servermiddleware.ErrorAudit(audit), servermiddleware.SecurityRateLimit(system, settings), servermiddleware.OperationAudit(runtime, audit))
prefix := ""
config := runtime.Admin()
diff --git a/internal/server/handler/media.go b/internal/server/handler/media.go
index 6af9068..4b09dd2 100644
--- a/internal/server/handler/media.go
+++ b/internal/server/handler/media.go
@@ -83,7 +83,11 @@ func (h *Media) DeleteMany(c *gin.Context) {
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "批量删除成功")
}
func (h *Media) Find(c *gin.Context) {
- id, _ := strconv.ParseUint(c.Query("id"), 10, 64)
+ 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, "查询失败")
diff --git a/internal/server/handler/public.go b/internal/server/handler/public.go
index c172c00..5205d85 100644
--- a/internal/server/handler/public.go
+++ b/internal/server/handler/public.go
@@ -100,7 +100,13 @@ func (h *Public) Login(c *gin.Context) {
if err != nil {
_, _ = h.system.CacheIncrement(c.Request.Context(), c.ClientIP(), ipTTL)
if errors.Is(err, biz.ErrUserDisabled) {
- _ = h.audit.RecordLoginRequest(c.Request.Context(), &dto.LoginLogRequest{Username: req.Username, IP: c.ClientIP(), Status: false, ErrorMessage: "用户被禁止登录", Agent: c.Request.UserAgent()})
+ var disabled *service.UserDisabledError
+ errors.As(err, &disabled)
+ userID := uint(0)
+ if disabled != nil {
+ userID = disabled.UserID
+ }
+ _ = h.audit.RecordLoginRequest(c.Request.Context(), &dto.LoginLogRequest{Username: req.Username, IP: c.ClientIP(), Status: false, ErrorMessage: "用户被禁止登录", Agent: c.Request.UserAgent(), UserID: userID})
httpx.Fail(c, "用户被禁止登录")
return
}
diff --git a/internal/server/middleware/access.go b/internal/server/middleware/access.go
index 2c6cf26..9d4fccd 100644
--- a/internal/server/middleware/access.go
+++ b/internal/server/middleware/access.go
@@ -1,7 +1,6 @@
package middleware
import (
- "net/http"
"strings"
"kra/internal/biz"
@@ -33,13 +32,19 @@ func AccessControl(runtime *conf.Runtime, access *service.AccessService, audit *
requestID, _ := c.Get("request_id")
requestIDText, _ := requestID.(string)
_ = audit.RecordDataAccessRequest(c.Request.Context(), &dto.DataAccessRecordRequest{EventType: "blocked_access", Operation: c.Request.Method, UserID: claims.ID, AuthorityID: claims.AuthorityID, RequestID: requestIDText, Method: c.Request.Method, Path: path, Detail: "Casbin policy denied the request"})
- c.AbortWithStatusJSON(http.StatusForbidden, httpx.Response{Code: httpx.CodeError, Data: nil, Msg: "权限不足"})
+ 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.Fail(c, "数据权限加载失败")
- return
+ requestContext = c.Request.Context()
+ }
+ 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)
diff --git a/internal/server/middleware/access_log.go b/internal/server/middleware/access_log.go
new file mode 100644
index 0000000..f00c550
--- /dev/null
+++ b/internal/server/middleware/access_log.go
@@ -0,0 +1,86 @@
+package middleware
+
+import (
+ "bytes"
+ "io"
+ "log/slog"
+ "strings"
+ "time"
+
+ "kra/internal/conf"
+
+ "github.com/gin-gonic/gin"
+)
+
+// 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) gin.HandlerFunc {
+ return func(c *gin.Context) {
+ var requestBody []byte
+ multipart := strings.Contains(c.GetHeader("Content-Type"), "multipart/form-data")
+ if c.Request.Body != nil && !multipart {
+ requestBody, _ = io.ReadAll(c.Request.Body)
+ c.Request.Body = io.NopCloser(bytes.NewReader(requestBody))
+ }
+ maxBytes := 1 << 20
+ writer := &captureWriter{ResponseWriter: c.Writer, maxBytes: maxBytes}
+ c.Writer = writer
+ started := time.Now()
+ c.Next()
+ if logger == nil {
+ return
+ }
+ requestText, responseText := "", ""
+ config := runtime.Admin()
+ logLimit := 32768
+ if config != nil && config.Zap != nil && config.Zap.AccessLogMaxBytes > 0 {
+ logLimit = int(config.Zap.AccessLogMaxBytes)
+ }
+ if config == nil || config.Zap == nil || config.Zap.AccessReqBody {
+ if multipart {
+ requestText = "[文件]"
+ } else {
+ requestText = redactJSONLimit(requestBody, logLimit)
+ }
+ }
+ if config == nil || config.Zap == nil || config.Zap.AccessRespData {
+ responseText = redactJSONLimit(writer.body.Bytes(), 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"
+ }
+ attributes := []any{
+ "ip", c.ClientIP(), "method", c.Request.Method, "path", c.Request.URL.Path, "http_route", route,
+ "status", c.Writer.Status(), "latency_ms", time.Since(started).Milliseconds(),
+ "request_id", stringValueFromContext(c, "request_id"), "trace_id", stringValueFromContext(c, "trace_id"),
+ "bytes_in", len(requestBody), "bytes_out", c.Writer.Size(), "user_id", userID, "authority_id", authorityID,
+ "ua", c.Request.UserAgent(), "query", c.Request.URL.RawQuery, "request", requestText, "response", responseText}
+ if config != nil && config.Zap != nil && config.Zap.AccessReqHeaders {
+ attributes = append(attributes, "headers", redactHeaders(c.Request.Header))
+ }
+ logger.InfoContext(c.Request.Context(), "http access", attributes...)
+ }
+}
+
+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 strings.Contains(lower, "token") || lower == "authorization" || lower == "cookie" {
+ out[key] = []string{"******"}
+ } else {
+ out[key] = values
+ }
+ }
+ return out
+}
+
+func stringValueFromContext(c *gin.Context, key string) string {
+ value, _ := c.Get(key)
+ return stringValue(value)
+}
diff --git a/internal/server/middleware/audit.go b/internal/server/middleware/audit.go
index ab0d559..843b327 100644
--- a/internal/server/middleware/audit.go
+++ b/internal/server/middleware/audit.go
@@ -6,29 +6,34 @@ import (
"strings"
"time"
+ "kra/internal/conf"
"kra/internal/service"
"kra/internal/service/dto"
"github.com/gin-gonic/gin"
)
-func OperationAudit(service *service.AuditService) gin.HandlerFunc {
+func OperationAudit(runtime *conf.Runtime, service *service.AuditService) gin.HandlerFunc {
return func(c *gin.Context) {
- if isBootstrapPath(c.Request.URL.Path) || c.Request.Method == "GET" || c.Request.Method == "HEAD" || c.Request.Method == "OPTIONS" {
- c.Next()
- return
- }
path := c.Request.URL.Path
- if strings.Contains(path, "sysOperationRecord") || strings.Contains(path, "sysLoginLog") || strings.Contains(path, "dataAccessLog") {
+ if !recordsOperation(c.Request.Method, path) {
c.Next()
return
}
var requestBody []byte
- if c.Request.Body != nil {
- requestBody, _ = io.ReadAll(io.LimitReader(c.Request.Body, 32769))
- c.Request.Body = io.NopCloser(bytes.NewReader(requestBody))
+ maxBytes := 32768
+ if config := runtime.Admin(); config != nil && config.Zap != nil && config.Zap.AccessLogMaxBytes > 0 {
+ maxBytes = int(config.Zap.AccessLogMaxBytes)
}
- writer := &captureWriter{ResponseWriter: c.Writer}
+ if c.Request.Body != nil {
+ if strings.Contains(c.GetHeader("Content-Type"), "multipart/form-data") {
+ requestBody = []byte("[文件]")
+ } else {
+ requestBody, _ = io.ReadAll(c.Request.Body)
+ c.Request.Body = io.NopCloser(bytes.NewReader(requestBody))
+ }
+ }
+ writer := &captureWriter{ResponseWriter: c.Writer, maxBytes: maxBytes}
c.Writer = writer
started := time.Now()
c.Next()
@@ -41,6 +46,49 @@ func OperationAudit(service *service.AuditService) gin.HandlerFunc {
if status >= 400 {
errorMessage = writer.body.String()
}
- _ = 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: redactJSON(requestBody), Response: redactJSON(writer.body.Bytes()), UserID: userID, RequestID: stringValue(requestID), TraceID: c.GetHeader("traceparent"), DeviceID: c.GetHeader("X-Device-Id")})
+ _ = 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: redactJSONLimit(requestBody, maxBytes), Response: writer.body.String(), UserID: userID, RequestID: stringValue(requestID), TraceID: stringValueFromContext(c, "trace_id"), DeviceID: c.GetHeader("X-Device-Id")})
}
}
+
+// 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 /sysOperationRecord/deleteSysOperationRecord", "DELETE /sysOperationRecord/deleteSysOperationRecordByIds", "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
+}()
diff --git a/internal/server/middleware/auth.go b/internal/server/middleware/auth.go
index 1029032..825b987 100644
--- a/internal/server/middleware/auth.go
+++ b/internal/server/middleware/auth.go
@@ -1,6 +1,7 @@
package middleware
import (
+ "errors"
"net/http"
"strconv"
"strings"
@@ -12,16 +13,28 @@ import (
"kra/pkg/adminauth"
"github.com/gin-gonic/gin"
+ "golang.org/x/sync/singleflight"
)
const claimsKey = "admin_claims"
+var refreshTokens singleflight.Group
+
+type refreshedToken struct {
+ token string
+ expiresAt int64
+}
+
func Auth(runtime *conf.Runtime, settings *service.SettingsService) 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
+ }
secret := ""
config := runtime.Admin()
if config != nil && config.Jwt != nil {
@@ -29,15 +42,24 @@ func Auth(runtime *conf.Runtime, settings *service.SettingsService) gin.HandlerF
}
claims, err := adminauth.Parse(token, secret)
if err != nil {
- httpx.NoAuth(c, "未登录或非法访问")
+ message := "无法处理此token"
+ switch {
+ case errors.Is(err, adminauth.ErrTokenExpired):
+ message = "登录已过期,请重新登录"
+ case errors.Is(err, adminauth.ErrTokenMalformed):
+ message = "这不是一个token"
+ case errors.Is(err, adminauth.ErrTokenSignatureInvalid):
+ message = "无效签名"
+ case errors.Is(err, adminauth.ErrTokenNotValidYet):
+ message = "token尚未激活"
+ }
+ httpx.SetTokenCookie(c, "", -1)
+ httpx.NoAuth(c, message)
return
}
if disabled, checkErr := settings.IsTokenDisabled(c.Request.Context(), token); checkErr != nil || disabled {
- httpx.NoAuth(c, "登录状态已失效")
- return
- }
- if active, checkErr := settings.ActiveTokenMatches(c.Request.Context(), claims.Username, token); checkErr != nil || !active {
- httpx.NoAuth(c, "登录状态已失效")
+ httpx.SetTokenCookie(c, "", -1)
+ httpx.NoAuth(c, "您的帐户异地登陆或令牌失效")
return
}
if claims.ExpiresAt != nil && claims.BufferTime > 0 && time.Until(claims.ExpiresAt.Time) < time.Duration(claims.BufferTime)*time.Second {
@@ -55,11 +77,21 @@ func Auth(runtime *conf.Runtime, settings *service.SettingsService) gin.HandlerF
issuer = config.Jwt.Issuer
}
}
- newToken, newClaims, refreshErr := adminauth.Generate(secret, issuer, expires, buffer, claims.ID, claims.AuthorityID, claims.UUID, claims.Username, claims.NickName, claims.MustChangePwd)
- if refreshErr == nil && settings.RotateActiveToken(c.Request.Context(), claims.Username, token, newToken, expires) == nil {
- c.Header("new-token", newToken)
- c.Header("new-expires-at", strconv.FormatInt(newClaims.ExpiresAt.Unix(), 10))
- httpx.SetTokenCookie(c, newToken, int(expires.Seconds()))
+ value, refreshErr, _ := refreshTokens.Do(token, func() (any, error) {
+ newToken, newClaims, generateErr := adminauth.Generate(secret, issuer, expires, buffer, claims.ID, claims.AuthorityID, claims.UUID, claims.Username, claims.NickName, claims.MustChangePwd)
+ if generateErr != nil {
+ return nil, generateErr
+ }
+ if rotateErr := settings.RotateActiveToken(c.Request.Context(), claims.Username, token, newToken, expires); rotateErr != nil {
+ return nil, rotateErr
+ }
+ return refreshedToken{token: newToken, expiresAt: newClaims.ExpiresAt.Unix()}, nil
+ })
+ if refreshErr == nil {
+ refreshed := value.(refreshedToken)
+ c.Header("new-token", refreshed.token)
+ c.Header("new-expires-at", strconv.FormatInt(refreshed.expiresAt, 10))
+ httpx.SetTokenCookie(c, refreshed.token, int(expires.Seconds()))
}
}
c.Set(claimsKey, claims)
@@ -85,6 +117,6 @@ func MustChangePassword() gin.HandlerFunc {
c.Next()
return
}
- c.AbortWithStatusJSON(http.StatusConflict, httpx.Response{Code: httpx.CodePasswordChangeRequired, Data: gin.H{"needChangePassword": true}, Msg: "请先修改初始密码"})
+ c.AbortWithStatusJSON(http.StatusConflict, httpx.Response{Code: httpx.CodePasswordChangeRequired, Data: gin.H{"needChangePassword": true}, Msg: "密码已过期,请先修改密码"})
}
}
diff --git a/internal/server/middleware/capture.go b/internal/server/middleware/capture.go
index b796ac0..d4fd35d 100644
--- a/internal/server/middleware/capture.go
+++ b/internal/server/middleware/capture.go
@@ -10,12 +10,17 @@ import (
type captureWriter struct {
gin.ResponseWriter
- body bytes.Buffer
+ body bytes.Buffer
+ maxBytes int
}
func (w *captureWriter) Write(data []byte) (int, error) {
- if w.body.Len() < 32768 {
- remaining := 32768 - w.body.Len()
+ limit := w.maxBytes
+ if limit <= 0 {
+ limit = 32768
+ }
+ if w.body.Len() < limit {
+ remaining := limit - w.body.Len()
if len(data) > remaining {
_, _ = w.body.Write(data[:remaining])
} else {
@@ -26,11 +31,18 @@ func (w *captureWriter) Write(data []byte) (int, error) {
}
func redactJSON(raw []byte) string {
+ return redactJSONLimit(raw, 32768)
+}
+
+func redactJSONLimit(raw []byte, limit int) string {
if len(raw) == 0 {
return ""
}
- if len(raw) > 32768 {
- raw = raw[:32768]
+ if limit <= 0 {
+ limit = 32768
+ }
+ if len(raw) > limit {
+ raw = raw[:limit]
}
var value any
if json.Unmarshal(raw, &value) != nil {
diff --git a/internal/server/middleware/email.go b/internal/server/middleware/email.go
deleted file mode 100644
index c9e5a52..0000000
--- a/internal/server/middleware/email.go
+++ /dev/null
@@ -1,48 +0,0 @@
-package middleware
-
-import (
- "bytes"
- "encoding/json"
- "fmt"
- "log/slog"
- "strings"
- "time"
-
- "kra/internal/server/httpx"
- "kra/internal/service"
-
- "github.com/gin-gonic/gin"
-)
-
-func EmailErrorAlert(service *service.EmailService, logger *slog.Logger) gin.HandlerFunc {
- return func(c *gin.Context) {
- if strings.Contains(c.Request.URL.Path, "/email/") {
- c.Next()
- return
- }
- writer := &captureWriter{ResponseWriter: c.Writer}
- c.Writer = writer
- started := time.Now()
- c.Next()
- failed := c.Writer.Status() >= 400
- if !failed {
- var result struct {
- Code int `json:"code"`
- }
- failed = json.Unmarshal(writer.body.Bytes(), &result) == nil && result.Code != httpx.CodeSuccess
- }
- if !failed {
- return
- }
- username := "Unknown"
- if claims := Claims(c); claims != nil && claims.Username != "" {
- username = claims.Username
- }
- subject := fmt.Sprintf("%s %s 调用 %s 报错", username, c.ClientIP(), c.Request.URL.Path)
- body := bytes.NewBuffer(nil)
- fmt.Fprintf(body, "请求方式:%s
请求路径:%s
状态码:%d
耗时:%s
错误响应:
%s", c.Request.Method, c.Request.URL.Path, c.Writer.Status(), time.Since(started), redactJSON(writer.body.Bytes())) - if err := service.Alert(c.Request.Context(), subject, body.String()); err != nil && logger != nil { - logger.Error("send HTTP error email", "error", err) - } - } -} diff --git a/internal/server/middleware/error_audit.go b/internal/server/middleware/error_audit.go new file mode 100644 index 0000000..c8f1afe --- /dev/null +++ b/internal/server/middleware/error_audit.go @@ -0,0 +1,41 @@ +package middleware + +import ( + "encoding/json" + "strings" + + "kra/internal/server/httpx" + "kra/internal/service" + "kra/internal/service/dto" + + "github.com/gin-gonic/gin" +) + +// ErrorAudit supplies the database sink that the Error-level logging core +// provides. Expected authentication, permission and input failures are not +// system errors and therefore are not inserted into sys_error. +func ErrorAudit(audit *service.AuditService) gin.HandlerFunc { + return func(c *gin.Context) { + writer := &captureWriter{ResponseWriter: c.Writer, maxBytes: 1 << 20} + c.Writer = writer + c.Next() + if strings.Contains(c.Request.URL.Path, "/sysError/") || c.Writer.Status() >= 300 && c.Writer.Status() < 500 { + return + } + var response httpx.Response + if json.Unmarshal(writer.body.Bytes(), &response) != nil || response.Code == httpx.CodeSuccess || expectedClientFailure(response.Msg) { + return + } + requestID, _ := c.Get("request_id") + _ = audit.CreateErrorRequest(c.Request.Context(), &dto.ErrorRecordRequest{Form: c.Request.URL.Path, Info: response.Msg, Level: "error", RequestID: stringValue(requestID), TraceID: stringValueFromContext(c, "trace_id"), Status: "未解决"}) + } +} + +func expectedClientFailure(message string) bool { + for _, value := range []string{"参数错误", "请输入用户名和密码", "验证码错误", "用户名不存在或者密码错误", "用户被禁止登录", "账号已锁定", "权限不足", "密码已过期", "未登录", "token", "令牌失效"} { + if strings.Contains(message, value) { + return true + } + } + return false +} diff --git a/internal/server/middleware/rate_limit.go b/internal/server/middleware/rate_limit.go index 6ed4aed..72fcb31 100644 --- a/internal/server/middleware/rate_limit.go +++ b/internal/server/middleware/rate_limit.go @@ -1,6 +1,7 @@ package middleware import ( + "strings" "time" "kra/internal/server/httpx" @@ -11,7 +12,8 @@ import ( func SecurityRateLimit(system *service.SystemService, settings *service.SettingsService) gin.HandlerFunc { return func(c *gin.Context) { - if isBootstrapPath(c.Request.URL.Path) { + path := strings.TrimSuffix(c.Request.URL.Path, "/") + if !strings.HasSuffix(path, "/base/login") && !strings.HasSuffix(path, "/base/captcha") { c.Next() return } diff --git a/internal/server/middleware/recovery.go b/internal/server/middleware/recovery.go new file mode 100644 index 0000000..222dd2d --- /dev/null +++ b/internal/server/middleware/recovery.go @@ -0,0 +1,51 @@ +package middleware + +import ( + "fmt" + "log/slog" + "net" + "net/http" + "net/http/httputil" + "os" + "runtime/debug" + "strings" + + "kra/internal/service" + "kra/internal/service/dto" + + "github.com/gin-gonic/gin" +) + +func Recovery(audit *service.AuditService, 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) + info := fmt.Sprintf("error=%v request=%s stack=%s", panicValue, request, debug.Stack()) + if logger != nil { + logger.ErrorContext(c.Request.Context(), "recovery from panic", "error", panicValue, "request", string(request), "stack", string(debug.Stack())) + } + requestID, _ := c.Get("request_id") + _ = audit.CreateErrorRequest(c.Request.Context(), &dto.ErrorRecordRequest{Form: c.Request.URL.Path, Info: info, Level: "error", RequestID: stringValue(requestID), TraceID: stringValueFromContext(c, "trace_id"), Status: "未解决"}) + if brokenPipe { + if err, ok := panicValue.(error); ok { + _ = c.Error(err) + } + c.Abort() + return + } + c.AbortWithStatus(http.StatusInternalServerError) + }() + c.Next() + } +} diff --git a/internal/server/middleware/request.go b/internal/server/middleware/request.go index 8fc2910..a8bc613 100644 --- a/internal/server/middleware/request.go +++ b/internal/server/middleware/request.go @@ -1,18 +1,48 @@ package middleware import ( + "crypto/rand" + "encoding/hex" + "regexp" + "strings" + "github.com/gin-gonic/gin" "github.com/google/uuid" ) +var traceParentPattern = regexp.MustCompile(`^00-([0-9a-f]{32})-([0-9a-f]{16})-[0-9a-f]{2}$`) + +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 requestID == "" { + if requestID == "" || len(requestID) > 128 || strings.ContainsAny(requestID, "\r\n") { requestID = uuid.NewString() } + traceID, parentSpanID := "", "" + if match := traceParentPattern.FindStringSubmatch(strings.ToLower(c.GetHeader("traceparent"))); len(match) == 3 { + traceID, parentSpanID = match[1], match[2] + } else if candidate := c.GetHeader("X-Trace-Id"); len(candidate) <= 128 && !strings.ContainsAny(candidate, "\r\n") { + traceID = candidate + } + if traceID == "" { + traceID = randomHex(16) + } + spanID := randomHex(8) c.Header("X-Request-Id", requestID) + c.Header("X-Trace-Id", traceID) + if len(traceID) == 32 { + 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.Next() } } diff --git a/internal/service/access.go b/internal/service/access.go index f460aef..d85edf4 100644 --- a/internal/service/access.go +++ b/internal/service/access.go @@ -2,14 +2,34 @@ package service import ( "context" + "strings" "kra/internal/biz" + "kra/internal/conf" "kra/internal/service/dto" ) -type AccessService struct{ uc *biz.AccessUsecase } +type AccessService struct { + uc *biz.AccessUsecase + runtime *conf.Runtime +} -func NewAccessService(uc *biz.AccessUsecase) *AccessService { return &AccessService{uc: uc} } +func NewAccessService(uc *biz.AccessUsecase, runtime *conf.Runtime) *AccessService { + return &AccessService{uc: uc, runtime: runtime} +} + +func (s *AccessService) NormalizeRoutePath(path string) string { + config := s.runtime.Admin() + if config == nil || config.RouterPrefix == "" { + return path + } + prefix := strings.TrimSuffix(config.RouterPrefix, "/") + normalized := strings.TrimPrefix(path, prefix) + if normalized == "" { + return "/" + } + return normalized +} func (s *AccessService) Authorize(ctx context.Context, aid uint, path, method string) (bool, error) { return s.uc.Authorize(ctx, aid, path, method) } diff --git a/internal/service/api.go b/internal/service/api.go index 26ff9a2..c927eac 100644 --- a/internal/service/api.go +++ b/internal/service/api.go @@ -88,6 +88,16 @@ func (s *AccessService) PolicyPathResponses(ctx context.Context, authorityID uin func (s *AccessService) SyncAPIResponses(ctx context.Context, routes []dto.APIRequest) (*dto.APISyncResponse, error) { items := make([]*biz.API, 0, len(routes)) for i := range routes { + routes[i].Path = s.NormalizeRoutePath(routes[i].Path) + if routes[i].APIGroup == "" || routes[i].Description == "" { + group, description := routeMetadata(routes[i].Method, routes[i].Path) + if routes[i].APIGroup == "" { + routes[i].APIGroup = group + } + if routes[i].Description == "" { + routes[i].Description = description + } + } items = append(items, apiDomain(&routes[i])) } return s.SyncAPIs(ctx, items) diff --git a/internal/service/api_metadata.go b/internal/service/api_metadata.go new file mode 100644 index 0000000..682d617 --- /dev/null +++ b/internal/service/api_metadata.go @@ -0,0 +1,191 @@ +package service + +import "strings" + +type apiMetadataValue struct{ group, description string } + +var apiMetadata = map[string]apiMetadataValue{ + "DELETE /api/deleteApisByIds": {group: "api", description: "批量删除api"}, + "DELETE /customer/customer": {group: "客户", description: "删除客户"}, + "DELETE /dataAccessLog/deleteDataAccessLogByIds": {group: "数据权限审计", description: "批量删除数据权限审计日志"}, + "DELETE /department/deleteDepartment": {group: "部门", description: "删除部门"}, + "DELETE /info/deleteInfo": {group: "公告", description: "删除公告"}, + "DELETE /info/deleteInfoByIds": {group: "公告", description: "批量删除公告"}, + "DELETE /mediaUpload/:uploadId": {group: "媒体上传", description: "取消大文件上传"}, + "DELETE /position/deletePosition": {group: "岗位", description: "删除岗位"}, + "DELETE /sysDictionary/deleteSysDictionary": {group: "系统字典", description: "删除字典"}, + "DELETE /sysDictionaryDetail/deleteSysDictionaryDetail": {group: "系统字典详情", description: "删除字典内容"}, + "DELETE /sysError/deleteSysError": {group: "错误日志", description: "删除错误日志"}, + "DELETE /sysError/deleteSysErrorByIds": {group: "错误日志", description: "批量删除错误日志"}, + "DELETE /sysExportTemplate/deleteSysExportTemplate": {group: "导出模板", description: "删除导出模板"}, + "DELETE /sysExportTemplate/deleteSysExportTemplateByIds": {group: "导出模板", description: "批量删除导出模板"}, + "DELETE /sysLoginLog/deleteLoginLog": {group: "登录日志", description: "删除登录日志"}, + "DELETE /sysLoginLog/deleteLoginLogByIds": {group: "登录日志", description: "批量删除登录日志"}, + "DELETE /sysOperationRecord/deleteSysOperationRecord": {group: "操作记录", description: "删除操作记录"}, + "DELETE /sysOperationRecord/deleteSysOperationRecordByIds": {group: "操作记录", description: "批量删除操作历史"}, + "DELETE /sysParams/deleteSysParams": {group: "参数管理", description: "删除参数"}, + "DELETE /sysParams/deleteSysParamsByIds": {group: "参数管理", description: "批量删除参数"}, + "DELETE /sysVersion/deleteSysVersion": {group: "版本控制", description: "删除版本"}, + "DELETE /sysVersion/deleteSysVersionByIds": {group: "版本控制", description: "批量删除版本"}, + "DELETE /timedTask/deleteTimedTask": {group: "定时任务", description: "删除定时任务"}, + "DELETE /user/deleteUser": {group: "系统用户", description: "删除用户"}, + "GET /api/getApiGroups": {group: "api", description: "获取路由组"}, + "GET /api/getApiRoles": {group: "api", description: "获取指定API关联角色列表"}, + "GET /api/syncApi": {group: "api", description: "获取待同步API"}, + "GET /attachmentCategory/getCategoryList": {group: "媒体库分类", description: "分类列表"}, + "GET /authority/getDataScopeDepts": {group: "角色", description: "获取角色自定义部门集"}, + "GET /authority/getUsersByAuthority": {group: "角色", description: "获取角色关联用户ID列表"}, + "GET /customer/customer": {group: "客户", description: "获取单一客户"}, + "GET /customer/customerList": {group: "客户", description: "获取客户列表"}, + "GET /department/findDepartment": {group: "部门", description: "根据ID获取部门"}, + "GET /department/getDepartmentUsers": {group: "部门", description: "获取部门成员ID列表"}, + "GET /info/findInfo": {group: "公告", description: "根据ID获取公告"}, + "GET /info/getInfoList": {group: "公告", description: "获取公告列表"}, + "GET /logViewer/content": {group: "文件日志", description: "分块读取日志文件内容"}, + "GET /logViewer/dates": {group: "文件日志", description: "获取存在日志的日期"}, + "GET /logViewer/files": {group: "文件日志", description: "获取日期下的日志文件"}, + "GET /menu/getMenuRoles": {group: "菜单", description: "获取菜单关联角色列表"}, + "GET /position/findPosition": {group: "岗位", description: "根据ID获取岗位"}, + "GET /position/getPositionUsers": {group: "岗位", description: "获取岗位成员ID列表"}, + "GET /securityConfig/getSecurityConfig": {group: "安全配置", description: "获取安全配置"}, + "GET /simpleUploader/checkFileMd5": {group: "断点续传(插件版)", description: "文件完整度验证"}, + "GET /simpleUploader/mergeFileMd5": {group: "断点续传(插件版)", description: "上传完成合并文件"}, + "GET /sysDictionary/exportSysDictionary": {group: "系统字典", description: "导出字典JSON"}, + "GET /sysDictionary/findSysDictionary": {group: "系统字典", description: "根据ID获取字典(建议选择)"}, + "GET /sysDictionary/getSysDictionaryList": {group: "系统字典", description: "获取字典列表"}, + "GET /sysDictionary/getSysDictionaryListWithDetails": {group: "系统字典", description: "获取字典列表(含明细)"}, + "GET /sysDictionaryDetail/findSysDictionaryDetail": {group: "系统字典详情", description: "根据ID获取字典内容"}, + "GET /sysDictionaryDetail/getDictionaryDetailsByParent": {group: "系统字典详情", description: "根据父级ID获取字典详情"}, + "GET /sysDictionaryDetail/getDictionaryPath": {group: "系统字典详情", description: "获取字典详情的完整路径"}, + "GET /sysDictionaryDetail/getDictionaryTreeList": {group: "系统字典详情", description: "获取字典数列表"}, + "GET /sysDictionaryDetail/getDictionaryTreeListByType": {group: "系统字典详情", description: "根据分类获取字典数列表"}, + "GET /sysDictionaryDetail/getSysDictionaryDetailList": {group: "系统字典详情", description: "获取字典内容列表"}, + "GET /sysError/findSysError": {group: "错误日志", description: "根据ID获取错误日志"}, + "GET /sysError/getSysErrorList": {group: "错误日志", description: "获取错误日志列表"}, + "GET /sysError/getSysErrorSolution": {group: "错误日志", description: "触发错误处理(异步)"}, + "GET /sysExportTemplate/exportExcel": {group: "导出模板", description: "导出Excel"}, + "GET /sysExportTemplate/exportTemplate": {group: "导出模板", description: "下载模板"}, + "GET /sysExportTemplate/findSysExportTemplate": {group: "导出模板", description: "根据ID获取导出模板"}, + "GET /sysExportTemplate/getSysExportTemplateList": {group: "导出模板", description: "获取导出模板列表"}, + "GET /sysExportTemplate/previewSQL": {group: "导出模板", description: "预览SQL"}, + "GET /sysLoginLog/findLoginLog": {group: "登录日志", description: "根据ID获取登录日志"}, + "GET /sysLoginLog/getLoginLogList": {group: "登录日志", description: "获取登录日志列表"}, + "GET /sysOperationRecord/findSysOperationRecord": {group: "操作记录", description: "根据ID获取操作记录"}, + "GET /sysOperationRecord/getSysOperationRecordList": {group: "操作记录", description: "获取操作记录列表"}, + "GET /sysParams/findSysParams": {group: "参数管理", description: "根据ID获取参数"}, + "GET /sysParams/getSysParam": {group: "参数管理", description: "获取参数列表"}, + "GET /sysParams/getSysParamsList": {group: "参数管理", description: "获取参数列表"}, + "GET /sysVersion/downloadVersionJson": {group: "版本控制", description: "下载版本json"}, + "GET /sysVersion/findSysVersion": {group: "版本控制", description: "获取单一版本"}, + "GET /sysVersion/getSysVersionList": {group: "版本控制", description: "获取版本列表"}, + "GET /timedTask/alertStream": {group: "定时任务", description: "订阅定时任务失败告警(SSE)"}, + "GET /timedTask/getRegisteredMethods": {group: "定时任务", description: "获取已注册方法列表"}, + "GET /timedTask/getTimedTaskList": {group: "定时任务", description: "获取定时任务列表"}, + "GET /timedTask/getTimedTaskLogList": {group: "定时任务", description: "获取定时任务执行日志"}, + "GET /user/getUserInfo": {group: "系统用户", description: "获取自身信息(必选)"}, + "POST /api/createApi": {group: "api", description: "创建api"}, + "POST /api/deleteApi": {group: "api", description: "删除Api"}, + "POST /api/enterSyncApi": {group: "api", description: "确认同步API"}, + "POST /api/getAllApis": {group: "api", description: "获取所有api"}, + "POST /api/getApiById": {group: "api", description: "获取api详细信息"}, + "POST /api/getApiList": {group: "api", description: "获取api列表"}, + "POST /api/ignoreApi": {group: "api", description: "忽略API"}, + "POST /api/setApiRoles": {group: "api", description: "全量覆盖API关联角色列表"}, + "POST /api/updateApi": {group: "api", description: "更新Api"}, + "POST /attachmentCategory/addCategory": {group: "媒体库分类", description: "添加/编辑分类"}, + "POST /attachmentCategory/deleteCategory": {group: "媒体库分类", description: "删除分类"}, + "POST /authority/copyAuthority": {group: "角色", description: "拷贝角色"}, + "POST /authority/createAuthority": {group: "角色", description: "创建角色"}, + "POST /authority/deleteAuthority": {group: "角色", description: "删除角色"}, + "POST /authority/getAuthorityList": {group: "角色", description: "获取角色列表"}, + "POST /authority/setDataScope": {group: "角色", description: "设置角色数据权限"}, + "POST /authority/setRoleUsers": {group: "角色", description: "全量覆盖角色关联用户"}, + "POST /authorityBtn/canRemoveAuthorityBtn": {group: "按钮权限", description: "删除按钮"}, + "POST /authorityBtn/getAuthorityBtn": {group: "按钮权限", description: "获取已有按钮权限"}, + "POST /authorityBtn/setAuthorityBtn": {group: "按钮权限", description: "设置按钮权限"}, + "POST /autoCode/initAPI": {group: "代码生成器", description: "生成插件 API 初始化文件"}, + "POST /autoCode/initDictionary": {group: "代码生成器", description: "生成插件字典初始化文件"}, + "POST /autoCode/initMenu": {group: "代码生成器", description: "生成插件菜单初始化文件"}, + "POST /casbin/getPolicyPathByAuthorityId": {group: "casbin", description: "获取权限列表"}, + "POST /casbin/updateCasbin": {group: "casbin", description: "更改角色api权限"}, + "POST /customer/customer": {group: "客户", description: "创建客户"}, + "POST /dataAccessLog/getDataAccessLogList": {group: "数据权限审计", description: "获取数据权限审计日志"}, + "POST /department/createDepartment": {group: "部门", description: "创建部门"}, + "POST /department/getDepartmentList": {group: "部门", description: "获取部门树"}, + "POST /department/setDepartmentUsers": {group: "部门", description: "设置部门成员(反向分配)"}, + "POST /email/emailTest": {group: "email", description: "发送测试邮件"}, + "POST /email/sendEmail": {group: "email", description: "发送邮件"}, + "POST /fileUploadAndDownload/deleteFile": {group: "文件上传与下载", description: "删除文件"}, + "POST /fileUploadAndDownload/editFileName": {group: "文件上传与下载", description: "文件名或者备注编辑"}, + "POST /fileUploadAndDownload/getFileList": {group: "文件上传与下载", description: "获取上传文件列表"}, + "POST /fileUploadAndDownload/importURL": {group: "文件上传与下载", description: "导入URL"}, + "POST /fileUploadAndDownload/upload": {group: "文件上传与下载", description: "文件上传(建议选择)"}, + "POST /info/createInfo": {group: "公告", description: "新建公告"}, + "POST /jwt/jsonInBlacklist": {group: "jwt", description: "jwt加入黑名单(退出,必选)"}, + "POST /mediaUpload/chunk": {group: "媒体上传", description: "上传分片"}, + "POST /mediaUpload/complete": {group: "媒体上传", description: "完成大文件上传"}, + "POST /mediaUpload/init": {group: "媒体上传", description: "初始化大文件上传"}, + "POST /menu/addBaseMenu": {group: "菜单", description: "新增菜单"}, + "POST /menu/addMenuAuthority": {group: "菜单", description: "增加menu和角色关联关系"}, + "POST /menu/deleteBaseMenu": {group: "菜单", description: "删除菜单"}, + "POST /menu/getBaseMenuById": {group: "菜单", description: "根据id获取菜单"}, + "POST /menu/getBaseMenuTree": {group: "菜单", description: "获取用户动态路由"}, + "POST /menu/getMenu": {group: "菜单", description: "获取菜单树(必选)"}, + "POST /menu/getMenuAuthority": {group: "菜单", description: "获取指定角色menu"}, + "POST /menu/getMenuList": {group: "菜单", description: "分页获取基础menu列表"}, + "POST /menu/setMenuRoles": {group: "菜单", description: "全量覆盖菜单关联角色列表"}, + "POST /menu/updateBaseMenu": {group: "菜单", description: "更新菜单"}, + "POST /position/createPosition": {group: "岗位", description: "创建岗位"}, + "POST /position/getPositionList": {group: "岗位", description: "获取岗位列表"}, + "POST /position/setPositionUsers": {group: "岗位", description: "设置岗位成员(反向分配)"}, + "POST /securityConfig/setSecurityConfig": {group: "安全配置", description: "设置安全配置"}, + "POST /simpleUploader/upload": {group: "断点续传(插件版)", description: "插件版分片上传"}, + "POST /sysApiToken/createApiToken": {group: "API Token", description: "签发API Token"}, + "POST /sysApiToken/deleteApiToken": {group: "API Token", description: "作废API Token"}, + "POST /sysApiToken/getApiTokenList": {group: "API Token", description: "获取API Token列表"}, + "POST /sysDictionary/createSysDictionary": {group: "系统字典", description: "新增字典"}, + "POST /sysDictionary/importSysDictionary": {group: "系统字典", description: "导入字典JSON"}, + "POST /sysDictionaryDetail/createSysDictionaryDetail": {group: "系统字典详情", description: "新增字典内容"}, + "POST /sysError/createSysError": {group: "错误日志", description: "新建错误日志"}, + "POST /sysExportTemplate/createSysExportTemplate": {group: "导出模板", description: "新增导出模板"}, + "POST /sysExportTemplate/importExcel": {group: "导出模板", description: "导入Excel"}, + "POST /sysOperationRecord/createSysOperationRecord": {group: "操作记录", description: "新增操作记录"}, + "POST /sysParams/createSysParams": {group: "参数管理", description: "新建参数"}, + "POST /system/getServerInfo": {group: "系统服务", description: "获取服务器信息"}, + "POST /system/getSystemConfig": {group: "系统服务", description: "获取配置文件内容"}, + "POST /system/setSystemConfig": {group: "系统服务", description: "设置配置文件内容"}, + "POST /sysVersion/exportVersion": {group: "版本控制", description: "创建版本"}, + "POST /sysVersion/importVersion": {group: "版本控制", description: "同步版本"}, + "POST /timedTask/createTimedTask": {group: "定时任务", description: "创建定时任务"}, + "POST /timedTask/toggleTimedTask": {group: "定时任务", description: "启用/停用定时任务"}, + "POST /timedTask/triggerTimedTask": {group: "定时任务", description: "手动触发定时任务"}, + "POST /user/admin_register": {group: "系统用户", description: "用户注册"}, + "POST /user/changePassword": {group: "系统用户", description: "修改密码(建议选择)"}, + "POST /user/getUserList": {group: "系统用户", description: "获取用户列表"}, + "POST /user/resetPassword": {group: "系统用户", description: "重置用户密码"}, + "POST /user/setUserAuthorities": {group: "系统用户", description: "设置权限组"}, + "POST /user/setUserAuthority": {group: "系统用户", description: "修改用户角色(必选)"}, + "POST /user/setUserDepartments": {group: "系统用户", description: "设置用户归属部门"}, + "POST /user/setUserPositions": {group: "系统用户", description: "设置用户岗位"}, + "PUT /authority/updateAuthority": {group: "角色", description: "更新角色信息"}, + "PUT /customer/customer": {group: "客户", description: "更新客户"}, + "PUT /department/updateDepartment": {group: "部门", description: "更新部门"}, + "PUT /info/updateInfo": {group: "公告", description: "更新公告"}, + "PUT /position/updatePosition": {group: "岗位", description: "更新岗位"}, + "PUT /sysDictionary/updateSysDictionary": {group: "系统字典", description: "更新字典"}, + "PUT /sysDictionaryDetail/updateSysDictionaryDetail": {group: "系统字典详情", description: "更新字典内容"}, + "PUT /sysError/updateSysError": {group: "错误日志", description: "更新错误日志"}, + "PUT /sysExportTemplate/updateSysExportTemplate": {group: "导出模板", description: "更新导出模板"}, + "PUT /sysParams/updateSysParams": {group: "参数管理", description: "更新参数"}, + "PUT /timedTask/updateTimedTask": {group: "定时任务", description: "更新定时任务"}, + "PUT /user/setSelfInfo": {group: "系统用户", description: "设置自身信息(必选)"}, + "PUT /user/setSelfSetting": {group: "系统用户", description: "用户界面配置"}, + "PUT /user/setUserInfo": {group: "系统用户", description: "设置用户信息"}, +} + +func routeMetadata(method, path string) (string, string) { + if value, ok := apiMetadata[strings.ToUpper(method)+" "+path]; ok { + return value.group, value.description + } + return routeGroup(path), "" +} diff --git a/internal/service/authentication.go b/internal/service/authentication.go index f2eeee4..718728f 100644 --- a/internal/service/authentication.go +++ b/internal/service/authentication.go @@ -8,6 +8,11 @@ import ( "kra/pkg/adminauth" ) +type UserDisabledError struct{ UserID uint } + +func (e *UserDisabledError) Error() string { return biz.ErrUserDisabled.Error() } +func (e *UserDisabledError) Unwrap() error { return biz.ErrUserDisabled } + type LoginResult struct { User map[string]any `json:"user"` Token string `json:"token"` @@ -34,7 +39,7 @@ func (s *SystemService) Login(ctx context.Context, username, password string) (* return nil, err } if u.Enable != 1 { - return nil, biz.ErrUserDisabled + return nil, &UserDisabledError{UserID: u.ID} } if security, securityErr := s.settings.CurrentSecurity(ctx); securityErr == nil && security.PwdExpireEnable && u.PasswordUpdatedAt != nil && time.Since(*u.PasswordUpdatedAt) > time.Duration(security.PwdExpireDays)*24*time.Hour { u.MustChangePassword = true diff --git a/internal/service/dto/system_config.go b/internal/service/dto/system_config.go index 40c50c5..4f50f77 100644 --- a/internal/service/dto/system_config.go +++ b/internal/service/dto/system_config.go @@ -29,22 +29,37 @@ type SetSystemConfigRequest struct { Config struct { Data *struct { Database *struct { - Driver string `json:"driver"` - Source string `json:"source"` - Host string `json:"host"` - Port string `json:"port"` - User string `json:"user"` - Password string `json:"password"` - Name string `json:"name"` - Config string `json:"config"` - Path string `json:"path"` + Driver string `json:"driver"` + Source string `json:"source"` + Host string `json:"host"` + Port string `json:"port"` + User string `json:"user"` + Password string `json:"password"` + Name string `json:"name"` + Config string `json:"config"` + Path string `json:"path"` + Prefix string `json:"prefix"` + Engine string `json:"engine"` + LogMode string `json:"log_mode"` + MaxIdleConns int32 `json:"max_idle_conns"` + MaxOpenConns int32 `json:"max_open_conns"` + ConnMaxLifetime int32 `json:"conn_max_lifetime"` + Singular bool `json:"singular"` } `json:"database"` Redis *struct { - Network string `json:"network"` - Addr string `json:"addr"` - ReadTimeout string `json:"read_timeout"` - WriteTimeout string `json:"write_timeout"` + Network string `json:"network"` + Addr string `json:"addr"` + ReadTimeout string `json:"read_timeout"` + WriteTimeout string `json:"write_timeout"` + Name string `json:"name"` + Password string `json:"password"` + DB int32 `json:"db"` + UseCluster bool `json:"use_cluster"` + ClusterAddrs []string `json:"cluster_addrs"` } `json:"redis"` + DatabaseList []*conf.Data_Database `json:"database_list"` + RedisList []*conf.Data_Redis `json:"redis_list"` + Mongo *conf.Data_Mongo `json:"mongo"` } `json:"data"` Admin struct { RouterPrefix string `json:"routerPrefix"` @@ -53,6 +68,7 @@ type SetSystemConfigRequest struct { UseMultipoint bool `json:"useMultipoint"` UseStrictAuth bool `json:"useStrictAuth"` DisableAutoMigrate bool `json:"disableAutoMigrate"` + UseMongo bool `json:"useMongo"` } `json:"system"` JWT struct { SigningKey string `json:"signingKey"` @@ -74,6 +90,9 @@ type SetSystemConfigRequest struct { SessionTTL int32 `json:"sessionTtl"` } `json:"media"` Storage *conf.AdminBackend_Storage `json:"storage"` + Zap *conf.AdminBackend_Zap `json:"zap"` + Cors *conf.AdminBackend_CORS `json:"cors"` + App *conf.AdminBackend_App `json:"app"` } `json:"admin"` Email *struct { To string `json:"to"` diff --git a/internal/service/system_config.go b/internal/service/system_config.go index e288e43..ccc5991 100644 --- a/internal/service/system_config.go +++ b/internal/service/system_config.go @@ -48,7 +48,7 @@ func (s *SystemService) SystemConfig() map[string]any { } admin["routerPrefix"] = config.RouterPrefix if config.System != nil { - admin["system"] = map[string]any{"useRedis": config.System.UseRedis, "useMultipoint": config.System.UseMultipoint, "useStrictAuth": config.System.UseStrictAuth, "disableAutoMigrate": config.System.DisableAutoMigrate} + admin["system"] = map[string]any{"useRedis": config.System.UseRedis, "useMultipoint": config.System.UseMultipoint, "useStrictAuth": config.System.UseStrictAuth, "disableAutoMigrate": config.System.DisableAutoMigrate, "useMongo": config.System.UseMongo} } if config.Jwt != nil { admin["jwt"] = map[string]any{"signingKey": "******", "expiresTime": config.Jwt.ExpiresTime.AsDuration().String(), "bufferTime": config.Jwt.BufferTime.AsDuration().String(), "issuer": config.Jwt.Issuer} @@ -70,16 +70,36 @@ func (s *SystemService) SystemConfig() map[string]any { maskStorageSecrets(storage) admin["storage"] = storage } + if config.Zap != nil { + admin["zap"] = config.Zap + } + if config.Cors != nil { + admin["cors"] = config.Cors + } + if config.App != nil { + admin["app"] = config.App + } data := s.runtime.Data() if data != nil { if data.Database != nil { data.Database.Password = "******" } + if data.Redis != nil { + data.Redis.Password = "******" + } + if data.Mongo != nil { + data.Mongo.Password = "******" + } for _, database := range data.DatabaseList { if database != nil { database.Password = "******" } } + for _, redis := range data.RedisList { + if redis != nil { + redis.Password = "******" + } + } } dataMap := map[string]any{} if raw, err := (protojson.MarshalOptions{UseProtoNames: true}).Marshal(data); err == nil { @@ -103,6 +123,16 @@ func (s *SystemService) SaveSystemConfig(ctx context.Context, req *dto.SetSystem next.System.UseMultipoint = req.Config.Admin.System.UseMultipoint next.System.UseStrictAuth = req.Config.Admin.System.UseStrictAuth next.System.DisableAutoMigrate = req.Config.Admin.System.DisableAutoMigrate + next.System.UseMongo = req.Config.Admin.System.UseMongo + if req.Config.Admin.Zap != nil { + next.Zap = req.Config.Admin.Zap + } + if req.Config.Admin.Cors != nil { + next.Cors = req.Config.Admin.Cors + } + if req.Config.Admin.App != nil { + next.App = req.Config.Admin.App + } if next.Jwt != nil { if req.Config.Admin.JWT.SigningKey != "" && req.Config.Admin.JWT.SigningKey != "******" { next.Jwt.SigningKey = req.Config.Admin.JWT.SigningKey @@ -163,10 +193,14 @@ func (s *SystemService) SaveSystemConfig(ctx context.Context, req *dto.SetSystem if data.Database != nil && (password == "" || password == "******") { password = data.Database.Password } - data.Database = &conf.Data_Database{Driver: value.Driver, Source: value.Source, Host: value.Host, Port: value.Port, User: value.User, Password: password, Name: value.Name, Config: value.Config, Path: value.Path} + data.Database = &conf.Data_Database{Driver: value.Driver, Source: value.Source, Host: value.Host, Port: value.Port, User: value.User, Password: password, Name: value.Name, Config: value.Config, Path: value.Path, Prefix: value.Prefix, Engine: value.Engine, LogMode: value.LogMode, MaxIdleConns: value.MaxIdleConns, MaxOpenConns: value.MaxOpenConns, ConnMaxLifetime: value.ConnMaxLifetime, Singular: value.Singular} } if value := req.Config.Data.Redis; value != nil { - redis := &conf.Data_Redis{Network: value.Network, Addr: value.Addr} + password := value.Password + if data.Redis != nil && (password == "" || password == "******") { + password = data.Redis.Password + } + redis := &conf.Data_Redis{Network: value.Network, Addr: value.Addr, Name: value.Name, Password: password, Db: value.DB, UseCluster: value.UseCluster, ClusterAddrs: value.ClusterAddrs} if duration, parseErr := time.ParseDuration(value.ReadTimeout); parseErr == nil && duration >= 0 { redis.ReadTimeout = durationpb.New(duration) } @@ -175,6 +209,28 @@ func (s *SystemService) SaveSystemConfig(ctx context.Context, req *dto.SetSystem } data.Redis = redis } + if req.Config.Data.DatabaseList != nil { + for i, item := range req.Config.Data.DatabaseList { + if item != nil && (item.Password == "" || item.Password == "******") && i < len(data.DatabaseList) && data.DatabaseList[i] != nil { + item.Password = data.DatabaseList[i].Password + } + } + data.DatabaseList = req.Config.Data.DatabaseList + } + if req.Config.Data.RedisList != nil { + for i, item := range req.Config.Data.RedisList { + if item != nil && (item.Password == "" || item.Password == "******") && i < len(data.RedisList) && data.RedisList[i] != nil { + item.Password = data.RedisList[i].Password + } + } + data.RedisList = req.Config.Data.RedisList + } + if req.Config.Data.Mongo != nil { + if data.Mongo != nil && (req.Config.Data.Mongo.Password == "" || req.Config.Data.Mongo.Password == "******") { + req.Config.Data.Mongo.Password = data.Mongo.Password + } + data.Mongo = req.Config.Data.Mongo + } } dataRaw, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(data) if err != nil { diff --git a/internal/service/system_init.go b/internal/service/system_init.go index b65d256..4acb173 100644 --- a/internal/service/system_init.go +++ b/internal/service/system_init.go @@ -14,10 +14,10 @@ type DatabaseInit struct { Port string `json:"port"` UserName string `json:"userName"` Password string `json:"password"` - DBName string `json:"dbName"` + DBName string `json:"dbName" binding:"required"` DBPath string `json:"dbPath"` Template string `json:"template"` - AdminPassword string `json:"adminPassword"` + AdminPassword string `json:"adminPassword" binding:"required"` } func (s *SystemService) Initialize(ctx context.Context, input *DatabaseInit, apis []*biz.API) error { @@ -34,7 +34,15 @@ func (s *SystemService) Initialize(ctx context.Context, input *DatabaseInit, api func (s *SystemService) InitializeRoutes(ctx context.Context, input *DatabaseInit, routes []dto.Route) error { apis := make([]*biz.API, 0, len(routes)) for _, route := range routes { - apis = append(apis, &biz.API{Path: route.Path, Method: route.Method, APIGroup: routeGroup(route.Path)}) + path := route.Path + if config := s.runtime.Admin(); config != nil && config.RouterPrefix != "" { + path = strings.TrimPrefix(path, strings.TrimSuffix(config.RouterPrefix, "/")) + if path == "" { + path = "/" + } + } + group, description := routeMetadata(route.Method, path) + apis = append(apis, &biz.API{Path: path, Method: route.Method, APIGroup: group, Description: description}) } return s.Initialize(ctx, input, apis) } diff --git a/pkg/adminauth/token.go b/pkg/adminauth/token.go index 49f6fa5..6f12154 100644 --- a/pkg/adminauth/token.go +++ b/pkg/adminauth/token.go @@ -7,6 +7,14 @@ import ( "github.com/golang-jwt/jwt/v5" ) +var ( + ErrTokenExpired = errors.New("token expired") + ErrTokenMalformed = errors.New("token malformed") + ErrTokenSignatureInvalid = errors.New("token signature invalid") + ErrTokenNotValidYet = errors.New("token not valid yet") + ErrTokenInvalid = errors.New("token invalid") +) + type Claims struct { UUID string ID uint @@ -35,8 +43,22 @@ func Parse(tokenString, secret string) (*Claims, error) { } return []byte(secret), nil }) - if err != nil || !token.Valid { - return nil, errors.New("token is invalid") + if err != nil { + switch { + case errors.Is(err, jwt.ErrTokenExpired): + return nil, ErrTokenExpired + case errors.Is(err, jwt.ErrTokenMalformed): + return nil, ErrTokenMalformed + case errors.Is(err, jwt.ErrTokenSignatureInvalid): + return nil, ErrTokenSignatureInvalid + case errors.Is(err, jwt.ErrTokenNotValidYet): + return nil, ErrTokenNotValidYet + default: + return nil, ErrTokenInvalid + } + } + if !token.Valid { + return nil, ErrTokenInvalid } claims, ok := token.Claims.(*Claims) if !ok { diff --git a/pkg/logging/daily.go b/pkg/logging/daily.go index 58d0b6d..fb70ff1 100644 --- a/pkg/logging/daily.go +++ b/pkg/logging/daily.go @@ -11,15 +11,38 @@ import ( // file handle open for the active day and rotates on the first write after // midnight, matching the directory layout consumed by the log viewer. type DailyWriter struct { - mu sync.Mutex - root string - name string - date string - file *os.File + mu sync.Mutex + root string + name string + date string + file *os.File + retentionDay int } -func NewDailyWriter(root, name string) *DailyWriter { - return &DailyWriter{root: root, name: name} +func NewDailyWriter(root, name string, retentionDay int) *DailyWriter { + w := &DailyWriter{root: root, name: name, retentionDay: retentionDay} + w.removeExpired() + return w +} + +func (w *DailyWriter) removeExpired() { + if w.retentionDay <= 0 { + return + } + entries, err := os.ReadDir(w.root) + if err != nil { + return + } + cutoff := time.Now().AddDate(0, 0, -w.retentionDay) + for _, entry := range entries { + if !entry.IsDir() { + continue + } + date, err := time.Parse("2006-01-02", entry.Name()) + if err == nil && date.Before(cutoff) { + _ = os.RemoveAll(filepath.Join(w.root, entry.Name())) + } + } } func (w *DailyWriter) Write(value []byte) (int, error) { diff --git a/pkg/logging/zap.go b/pkg/logging/zap.go index 6fbff3b..6794808 100644 --- a/pkg/logging/zap.go +++ b/pkg/logging/zap.go @@ -3,6 +3,8 @@ package logging import ( "log/slog" "os" + "strings" + "time" "github.com/go-kratos/kratos/contrib/otel/v3/tracing" kratoslog "github.com/go-kratos/kratos/v3/log" @@ -11,20 +13,107 @@ import ( "go.uber.org/zap/zapcore" ) +type Options struct { + Level, Format, EncodeLevel, Prefix, StacktraceKey string + LogInConsole, ShowLine bool + RetentionDay int + FileOnlyModules []string +} + +type moduleFilterCore struct { + zapcore.Core + fileOnly map[string]struct{} + mod string +} + +func (c *moduleFilterCore) With(fields []zapcore.Field) zapcore.Core { + mod := c.mod + if value := moduleField(fields); value != "" { + mod = value + } + return &moduleFilterCore{Core: c.Core.With(fields), fileOnly: c.fileOnly, mod: mod} +} + +func (c *moduleFilterCore) Check(entry zapcore.Entry, checked *zapcore.CheckedEntry) *zapcore.CheckedEntry { + if c.Enabled(entry.Level) { + return checked.AddCore(entry, c) + } + return checked +} + +func (c *moduleFilterCore) Write(entry zapcore.Entry, fields []zapcore.Field) error { + mod := c.mod + if value := moduleField(fields); value != "" { + mod = value + } + if _, excluded := c.fileOnly[mod]; excluded { + return nil + } + return c.Core.Write(entry, fields) +} + +func moduleField(fields []zapcore.Field) string { + for _, field := range fields { + if field.Key == "mod" { + return field.String + } + } + return "" +} + // NewZapLogger adapts a Zap core to the slog logger used by Kratos v3. // The file layout remains compatible with the administration log viewer. -func NewZapLogger(root, filename string, attrs ...any) (*slog.Logger, func()) { - file := NewDailyWriter(root, filename) +func NewZapLogger(root, filename string, options Options, attrs ...any) (*slog.Logger, func()) { + file := NewDailyWriter(root, filename, options.RetentionDay) encoder := zap.NewProductionEncoderConfig() - encoder.EncodeTime = zapcore.ISO8601TimeEncoder - encoder.EncodeLevel = zapcore.LowercaseLevelEncoder - core := zapcore.NewCore( - zapcore.NewJSONEncoder(encoder), - zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout), zapcore.AddSync(file)), - zap.NewAtomicLevelAt(zap.InfoLevel), + encoder.EncodeTime = zapcore.RFC3339NanoTimeEncoder + if options.StacktraceKey != "" { + encoder.StacktraceKey = options.StacktraceKey + } + switch options.EncodeLevel { + case "CapitalLevelEncoder": + encoder.EncodeLevel = zapcore.CapitalLevelEncoder + case "CapitalColorLevelEncoder": + encoder.EncodeLevel = zapcore.CapitalColorLevelEncoder + case "LowercaseColorLevelEncoder": + encoder.EncodeLevel = zapcore.LowercaseColorLevelEncoder + default: + encoder.EncodeLevel = zapcore.LowercaseLevelEncoder + } + var outputEncoder zapcore.Encoder = zapcore.NewJSONEncoder(encoder) + if options.Format != "" && options.Format != "json" { + if options.Prefix != "" { + encoder.EncodeTime = func(value time.Time, output zapcore.PrimitiveArrayEncoder) { + output.AppendString(options.Prefix + value.Format("2006-01-02 15:04:05.000")) + } + } + outputEncoder = zapcore.NewConsoleEncoder(encoder) + } + level := zap.InfoLevel + if parsed := level.Set(strings.ToLower(options.Level)); parsed != nil { + level = zap.DebugLevel + } + levelEnabler := zap.NewAtomicLevelAt(level) + fileCore := zapcore.NewCore( + outputEncoder.Clone(), + zapcore.AddSync(file), + levelEnabler, ) + core := zapcore.Core(fileCore) + if options.LogInConsole { + fileOnly := make(map[string]struct{}, len(options.FileOnlyModules)) + for _, module := range options.FileOnlyModules { + fileOnly[module] = struct{}{} + } + consoleCore := zapcore.NewCore(outputEncoder.Clone(), zapcore.AddSync(os.Stdout), levelEnabler) + core = zapcore.NewTee(fileCore, &moduleFilterCore{Core: consoleCore, fileOnly: fileOnly}) + } zapLogger := zap.New(core) - handler := zapslog.NewHandler(zapLogger.Core(), zapslog.WithCaller(true), zapslog.AddStacktraceAt(slog.LevelError)) + handlerOptions := []zapslog.HandlerOption{zapslog.AddStacktraceAt(slog.LevelError)} + if options.ShowLine { + handlerOptions = append(handlerOptions, zapslog.WithCaller(true)) + } + handler := zapslog.NewHandler(zapLogger.Core(), handlerOptions...) logger := kratoslog.NewLogger(handler, kratoslog.WithExtractor(tracing.TraceAttrs)).With(attrs...) cleanup := func() { _ = zapLogger.Sync() diff --git a/web/src/view/systemTools/system/system.vue b/web/src/view/systemTools/system/system.vue index 3b7a06e..8398790 100644 --- a/web/src/view/systemTools/system/system.vue +++ b/web/src/view/systemTools/system/system.vue @@ -77,6 +77,13 @@
保存后点击“重载服务”,数据库连接会按新配置重建。
@@ -85,11 +92,59 @@