diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..92f55f8
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,17 @@
+.git
+.github
+.idea
+.vscode
+.DS_Store
+.codegraph
+gva
+web/node_modules
+web/dist
+node_modules
+dist
+bin
+coverage
+*.log
+*.tmp
+configs/*.local.yaml
+uploads
diff --git a/Dockerfile b/Dockerfile
index b2ba6a6..aec9e35 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -10,13 +10,19 @@ FROM debian:stable-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
netbase \
- && rm -rf /var/lib/apt/lists/ \
- && apt-get autoremove -y && apt-get autoclean -y
+ && rm -rf /var/lib/apt/lists/ \
+ && apt-get autoremove -y && apt-get autoclean -y
-COPY --from=builder /src/bin /app
+RUN groupadd --system kra && useradd --system --gid kra --home-dir /app kra
+
+COPY --from=builder --chown=kra:kra /src/bin /app
+
+RUN mkdir -p /data/conf /app/uploads/file && chown -R kra:kra /data/conf /app/uploads
WORKDIR /app
+USER kra
+
EXPOSE 8000
EXPOSE 9000
VOLUME /data/conf
diff --git a/README.md b/README.md
index 45bd701..cbd1908 100644
--- a/README.md
+++ b/README.md
@@ -9,8 +9,9 @@ cmd/kratos-admin/ 服务入口与 Wire
configs/ 运行配置
internal/biz/ 领域对象、用例和仓储接口
internal/data/ 数据库、缓存、对象存储及仓储实现
-internal/server/ Gin 服务、路由和定时任务调度
+internal/server/ Gin 服务、路由、中间件和 Handler
internal/service/ HTTP 输入输出与领域对象转换
+internal/worker/ 定时任务调度与后台工作进程
web/ Vue 管理端
```
@@ -24,6 +25,8 @@ go run ./cmd/kratos-admin -conf ./configs
默认 HTTP 地址为 `http://127.0.0.1:8000`。生产环境必须替换 JWT 签名密钥和数据库凭据。
+配置文件发生变化时会自动刷新内存配置;管理端“重载服务”会进一步重建数据库、Redis、对象存储连接并重新载入定时任务。路由前缀和 HTTP 监听地址需要重启进程后生效。
+
## 启动前端
```shell
diff --git a/cmd/kratos-admin/main.go b/cmd/kratos-admin/main.go
index 5bd8d16..7403078 100644
--- a/cmd/kratos-admin/main.go
+++ b/cmd/kratos-admin/main.go
@@ -7,14 +7,14 @@ import (
"path/filepath"
"kra/internal/conf"
- "kra/internal/server"
"kra/internal/worker"
+ "kra/pkg/logging"
- "github.com/go-kratos/kratos/contrib/otel/v3/tracing"
"github.com/go-kratos/kratos/v3"
"github.com/go-kratos/kratos/v3/config"
"github.com/go-kratos/kratos/v3/config/file"
"github.com/go-kratos/kratos/v3/log"
+ kratoshttp "github.com/go-kratos/kratos/v3/transport/http"
_ "go.uber.org/automaxprocs"
)
@@ -35,7 +35,7 @@ func init() {
flag.StringVar(&flagconf, "conf", "../../configs", "config path, eg: -conf config.yaml")
}
-func newApp(logger *slog.Logger, hs *server.GinServer, scheduler *worker.TaskScheduler) *kratos.App {
+func newApp(logger *slog.Logger, hs *kratoshttp.Server, scheduler *worker.TaskScheduler) *kratos.App {
return kratos.New(
kratos.ID(id),
kratos.Name(Name),
@@ -51,17 +51,12 @@ func newApp(logger *slog.Logger, hs *server.GinServer, scheduler *worker.TaskSch
func main() {
flag.Parse()
- logger := log.NewLogger(
- slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
- AddSource: true,
- Level: slog.LevelInfo,
- }),
- log.WithExtractor(tracing.TraceAttrs),
- ).With(
+ 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(
diff --git a/cmd/kratos-admin/wire.go b/cmd/kratos-admin/wire.go
index f113d21..d5d7e47 100644
--- a/cmd/kratos-admin/wire.go
+++ b/cmd/kratos-admin/wire.go
@@ -20,5 +20,5 @@ import (
// wireApp init kratos application.
func wireApp(*conf.Server, *conf.Data, *conf.AdminBackend, *slog.Logger) (*kratos.App, func(), error) {
- panic(wire.Build(server.ProviderSet, data.ProviderSet, biz.ProviderSet, service.ProviderSet, newApp))
+ panic(wire.Build(conf.NewRuntime, server.ProviderSet, data.ProviderSet, biz.ProviderSet, service.ProviderSet, newApp))
}
diff --git a/cmd/kratos-admin/wire_gen.go b/cmd/kratos-admin/wire_gen.go
index d9552f5..9c8a256 100644
--- a/cmd/kratos-admin/wire_gen.go
+++ b/cmd/kratos-admin/wire_gen.go
@@ -26,7 +26,8 @@ import (
// wireApp init kratos application.
func wireApp(confServer *conf.Server, confData *conf.Data, adminBackend *conf.AdminBackend, logger *slog.Logger) (*kratos.App, func(), error) {
- dataData, cleanup, err := data.NewData(confData, adminBackend)
+ runtime := conf.NewRuntime(confData, adminBackend)
+ dataData, cleanup, err := data.NewData(runtime)
if err != nil {
return nil, nil, err
}
@@ -40,8 +41,8 @@ func wireApp(confServer *conf.Server, confData *conf.Data, adminBackend *conf.Ad
systemUsecase := biz.NewSystemUsecase(systemRepo, cache, fileStorage)
settingsRepo := data.NewSettingsRepo(dataData)
settingsUsecase := biz.NewSettingsUsecase(settingsRepo)
- settingsService := service.NewSettingsService(settingsUsecase, adminBackend)
- systemService := service.NewSystemService(systemUsecase, adminBackend, settingsService)
+ settingsService := service.NewSettingsService(settingsUsecase, runtime, cache)
+ systemService := service.NewSystemService(systemUsecase, runtime, settingsService)
accessRepo := data.NewAccessRepo(dataData)
accessUsecase := biz.NewAccessUsecase(accessRepo)
accessService := service.NewAccessService(accessUsecase)
@@ -60,7 +61,7 @@ func wireApp(confServer *conf.Server, confData *conf.Data, adminBackend *conf.Ad
announcementUsecase := biz.NewAnnouncementUsecase(announcementRepo)
announcementService := service.NewAnnouncementService(announcementUsecase)
announcement := handler.NewAnnouncement(announcementService)
- emailRepo := data.NewEmailRepo(adminBackend)
+ emailRepo := data.NewEmailRepo(runtime)
emailUsecase := biz.NewEmailUsecase(emailRepo)
emailService := service.NewEmailService(emailUsecase)
email := handler.NewEmail(emailService)
@@ -68,7 +69,7 @@ func wireApp(confServer *conf.Server, confData *conf.Data, adminBackend *conf.Ad
taskUsecase := biz.NewTaskUsecase(taskRepo)
mediaRepo := data.NewMediaRepo(dataData)
mediaUsecase := biz.NewMediaUsecase(mediaRepo, fileStorage)
- taskService := service.NewTaskService(taskUsecase, mediaUsecase, adminBackend)
+ taskService := service.NewTaskService(taskUsecase, mediaUsecase, runtime)
taskScheduler := worker.NewTaskScheduler(taskService, logger)
task := handler.NewTask(taskService, taskScheduler)
mediaService := service.NewMediaService(mediaUsecase)
@@ -89,12 +90,12 @@ func wireApp(confServer *conf.Server, confData *conf.Data, adminBackend *conf.Ad
parameter := handler.NewParameter(settingsService)
apiToken := handler.NewAPIToken(settingsService)
systemConfig := handler.NewSystemConfig(systemService, settingsService, taskScheduler)
- public := handler.NewPublic(adminBackend, systemService, settingsService, auditService, taskScheduler)
+ public := handler.NewPublic(runtime, systemService, settingsService, auditService, taskScheduler)
user := handler.NewUser(systemService)
navigation := handler.NewNavigation(systemService)
session := handler.NewSession(settingsService)
- ginServer := server.NewGinServer(confServer, adminBackend, 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)
- app := newApp(logger, ginServer, taskScheduler)
+ 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)
+ app := newApp(logger, httpServer, taskScheduler)
return app, func() {
cleanup()
}, nil
diff --git a/configs/config.yaml b/configs/config.yaml
index d483277..75c0806 100644
--- a/configs/config.yaml
+++ b/configs/config.yaml
@@ -1,7 +1,7 @@
server:
http:
addr: 0.0.0.0:8000
- timeout: 1s
+ timeout: 600s
data:
database:
driver: mysql
@@ -16,6 +16,7 @@ data:
addr: 127.0.0.1:6379
read_timeout: 0.2s
write_timeout: 0.2s
+ database_list: []
admin:
router_prefix: ""
jwt:
@@ -34,6 +35,13 @@ admin:
path_prefix: uploads/file
media:
session_ttl: 24
+ system:
+ use_redis: false
+ use_multipoint: false
+ use_strict_auth: false
+ disable_auto_migrate: false
+ disk_list:
+ - mount_point: /
storage:
# local, qiniu, aliyun-oss, huawei-obs, tencent-cos, aws-s3,
# cloudflare-r2 or minio
diff --git a/go.mod b/go.mod
index b71bd53..a282256 100644
--- a/go.mod
+++ b/go.mod
@@ -11,6 +11,7 @@ require (
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.1
github.com/casbin/casbin/v3 v3.10.0
github.com/dzwvip/gorm-oracle v0.1.2
+ github.com/fsnotify/fsnotify v1.10.1
github.com/gin-gonic/gin v1.10.0
github.com/glebarez/sqlite v1.11.0
github.com/go-kratos/kratos/contrib/otel/v3 v3.0.0-20260617100506-4e232a3eff59
@@ -29,6 +30,8 @@ require (
github.com/xuri/excelize/v2 v2.9.0
go.einride.tech/aip v0.86.3
go.uber.org/automaxprocs v1.6.0
+ go.uber.org/zap v1.27.0
+ go.uber.org/zap/exp v0.3.0
golang.org/x/crypto v0.53.0
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
@@ -71,7 +74,6 @@ require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.8.4 // indirect
github.com/emirpasic/gods v1.12.0 // indirect
- github.com/fsnotify/fsnotify v1.10.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gammazero/toposort v0.1.1 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
@@ -134,6 +136,7 @@ require (
go.opentelemetry.io/otel v1.44.0 // indirect
go.opentelemetry.io/otel/metric v1.44.0 // indirect
go.opentelemetry.io/otel/trace v1.44.0 // indirect
+ go.uber.org/multierr v1.10.0 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/image v0.23.0 // indirect
golang.org/x/net v0.56.0 // indirect
diff --git a/go.sum b/go.sum
index 6b7e0a6..9c1cb74 100644
--- a/go.sum
+++ b/go.sum
@@ -175,8 +175,6 @@ github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
-github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE=
-github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -364,6 +362,14 @@ go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
+go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
+go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
+go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
+go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
+go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
@@ -390,8 +396,6 @@ golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
-golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
-golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -487,8 +491,6 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
-golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
-golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
diff --git a/internal/biz/README.md b/internal/biz/README.md
index 26a66b6..97a06f5 100644
--- a/internal/biz/README.md
+++ b/internal/biz/README.md
@@ -1 +1,3 @@
# Biz
+
+本目录只包含领域对象、用例和仓储接口,不依赖 HTTP Handler、数据库驱动或对象存储 SDK。跨多表、必须原子完成的业务动作通过具有业务含义的仓储方法表达。
diff --git a/internal/biz/access.go b/internal/biz/access.go
index d586021..87a9659 100644
--- a/internal/biz/access.go
+++ b/internal/biz/access.go
@@ -2,6 +2,7 @@ package biz
import (
"context"
+ "strings"
"time"
)
@@ -13,9 +14,28 @@ type API struct {
Description string
APIGroup string
Method string
+ OrderKey string
+ Desc bool
+ StrictAll bool
}
-type AccessRepo interface {
+type APIRepo interface {
+ CreateAPI(context.Context, *API) error
+ UpdateAPI(context.Context, *API) error
+ DeleteAPIs(context.Context, []uint) error
+ FindAPI(context.Context, uint) (*API, error)
+ ListAPIs(context.Context, int, int, *API) ([]*API, int64, error)
+ APIRoleIDs(context.Context, string, string) ([]uint, error)
+ SetAPIRoles(context.Context, string, string, []uint) error
+ Authorize(context.Context, uint, string, string) (bool, error)
+ PolicyPaths(context.Context, uint) ([]*API, error)
+ SetPolicyPaths(context.Context, uint, []*API) error
+ IgnoredAPIs(context.Context) ([]*API, error)
+ SetAPIIgnored(context.Context, string, string, bool) error
+ ApplyAPISync(context.Context, []*API, []*API) error
+}
+
+type AuthorityAccessRepo interface {
CreateAuthority(context.Context, *Authority) error
CopyAuthority(context.Context, uint, *Authority) error
UpdateAuthority(context.Context, *Authority) error
@@ -26,21 +46,9 @@ type AccessRepo interface {
SetDataScope(context.Context, uint, int, []uint) error
DataScopeDepartmentIDs(context.Context, uint) ([]uint, error)
ResolveDataScope(context.Context, uint, uint) (DataScope, error)
+}
- CreateAPI(context.Context, *API) error
- UpdateAPI(context.Context, *API) error
- DeleteAPIs(context.Context, []uint) error
- FindAPI(context.Context, uint) (*API, error)
- ListAPIs(context.Context, int, int, *API) ([]*API, int64, error)
- APIRoleIDs(context.Context, uint) ([]uint, error)
- SetAPIRoles(context.Context, uint, []uint) error
- Authorize(context.Context, uint, string, string) (bool, error)
- PolicyPaths(context.Context, uint) ([]*API, error)
- SetPolicyPaths(context.Context, uint, []*API) error
- IgnoredAPIs(context.Context) ([]*API, error)
- SetAPIIgnored(context.Context, string, string, bool) error
- ApplyAPISync(context.Context, []*API, []*API) error
-
+type PermissionRepo interface {
Buttons(context.Context, uint) ([]*MenuButton, error)
SetAuthorityButtons(context.Context, uint, map[uint][]uint) error
AuthorityButtonIDs(context.Context, uint) ([]uint, error)
@@ -49,14 +57,85 @@ type AccessRepo interface {
CanRemoveButton(context.Context, uint) (bool, error)
}
-type AccessUsecase struct{ repo AccessRepo }
+type AccessRepo interface {
+ AuthorityAccessRepo
+ APIRepo
+ PermissionRepo
+}
-func NewAccessUsecase(repo AccessRepo) *AccessUsecase { return &AccessUsecase{repo: repo} }
+type AccessUsecase struct{ AccessRepo }
+
+func NewAccessUsecase(repo AccessRepo) *AccessUsecase { return &AccessUsecase{AccessRepo: repo} }
-func (uc *AccessUsecase) Repo() AccessRepo { return uc.repo }
func (uc *AccessUsecase) Authorize(ctx context.Context, authorityID uint, path, method string) (bool, error) {
if authorityID == 888 {
return true, nil
}
- return uc.repo.Authorize(ctx, authorityID, path, method)
+ return uc.AccessRepo.Authorize(ctx, authorityID, path, method)
+}
+
+type APISyncDiff struct {
+ Added []*API
+ Deleted []*API
+ Ignored []*API
+}
+
+func (uc *AccessUsecase) SyncAPIs(ctx context.Context, routes []*API) (*APISyncDiff, error) {
+ stored, _, err := uc.ListAPIs(ctx, 0, 0, nil)
+ if err != nil {
+ return nil, err
+ }
+ ignored, err := uc.IgnoredAPIs(ctx)
+ if err != nil {
+ return nil, err
+ }
+ key := func(value *API) string { return strings.ToUpper(value.Method) + " " + value.Path }
+ ignoreSet := make(map[string]bool, len(ignored))
+ for _, item := range ignored {
+ ignoreSet[key(item)] = true
+ }
+ routeSet := make(map[string]*API, len(routes))
+ for _, item := range routes {
+ if !ignoreSet[key(item)] {
+ routeSet[key(item)] = item
+ }
+ }
+ storedSet := make(map[string]*API, len(stored))
+ for _, item := range stored {
+ storedSet[key(item)] = item
+ }
+ diff := &APISyncDiff{Ignored: ignored}
+ for routeKey, item := range routeSet {
+ if storedSet[routeKey] == nil {
+ diff.Added = append(diff.Added, item)
+ }
+ }
+ for storedKey, item := range storedSet {
+ if routeSet[storedKey] == nil && !ignoreSet[storedKey] {
+ diff.Deleted = append(diff.Deleted, item)
+ }
+ }
+ return diff, nil
+}
+
+func (uc *AccessUsecase) AuthorityTree(ctx context.Context) ([]*Authority, error) {
+ items, err := uc.ListAuthorities(ctx)
+ if err != nil {
+ return nil, err
+ }
+ byID := make(map[uint]*Authority, len(items))
+ for _, item := range items {
+ item.Children = nil
+ byID[item.AuthorityID] = item
+ }
+ roots := make([]*Authority, 0)
+ for _, item := range items {
+ if item.ParentID != nil && *item.ParentID != 0 && byID[*item.ParentID] != nil {
+ parent := byID[*item.ParentID]
+ parent.Children = append(parent.Children, item)
+ } else {
+ roots = append(roots, item)
+ }
+ }
+ return roots, nil
}
diff --git a/internal/biz/actor.go b/internal/biz/actor.go
new file mode 100644
index 0000000..118a6c5
--- /dev/null
+++ b/internal/biz/actor.go
@@ -0,0 +1,19 @@
+package biz
+
+import "context"
+
+type actorContextKey struct{}
+
+type Actor struct {
+ UserID uint
+ AuthorityID uint
+}
+
+func NewActorContext(ctx context.Context, actor Actor) context.Context {
+ return context.WithValue(ctx, actorContextKey{}, actor)
+}
+
+func ActorFromContext(ctx context.Context) (Actor, bool) {
+ actor, ok := ctx.Value(actorContextKey{}).(Actor)
+ return actor, ok
+}
diff --git a/internal/biz/api_token.go b/internal/biz/api_token.go
index 504574c..0925377 100644
--- a/internal/biz/api_token.go
+++ b/internal/biz/api_token.go
@@ -2,6 +2,7 @@ package biz
import (
"context"
+ "errors"
"time"
)
@@ -23,6 +24,29 @@ type APITokenRepo interface {
CreateAPIToken(context.Context, *APIToken) error
ListAPITokens(context.Context, int, int, uint, *bool) ([]*APIToken, int64, error)
DisableAPIToken(context.Context, uint) (string, error)
+ DisableAndBlacklistAPIToken(context.Context, uint) error
BlacklistToken(context.Context, string) error
IsTokenDisabled(context.Context, string) (bool, error)
}
+
+func (uc *SettingsUsecase) PrepareAPIToken(ctx context.Context, userID, authorityID uint, days int) (*User, time.Duration, error) {
+ user, allowed, err := uc.UserHasAuthority(ctx, userID, authorityID)
+ if err != nil {
+ return nil, 0, err
+ }
+ if !allowed {
+ return nil, 0, errors.New("用户不具备该角色权限")
+ }
+ duration := time.Duration(days) * 24 * time.Hour
+ if days == -1 {
+ duration = 100 * 365 * 24 * time.Hour
+ }
+ if duration <= 0 {
+ return nil, 0, errors.New("有效天数必须大于0或为-1")
+ }
+ return user, duration, nil
+}
+
+func (uc *SettingsUsecase) DisableToken(ctx context.Context, id uint) error {
+ return uc.SettingsRepo.DisableAndBlacklistAPIToken(ctx, id)
+}
diff --git a/internal/biz/audit.go b/internal/biz/audit.go
index 3821248..6b45473 100644
--- a/internal/biz/audit.go
+++ b/internal/biz/audit.go
@@ -14,7 +14,16 @@ type OperationRecord struct {
Agent, ErrorMessage, Body, Response string
UserID uint
RequestID, TraceID, DeviceID string
+ User *User
}
+
+type OperationLogRepo interface {
+ RecordOperation(context.Context, *OperationRecord) error
+ ListOperations(context.Context, int, int, *OperationRecord) ([]*OperationRecord, int64, error)
+ FindOperation(context.Context, uint) (*OperationRecord, error)
+ DeleteOperations(context.Context, []uint) error
+}
+
type LoginLog struct {
ID uint
CreatedAt time.Time
@@ -22,7 +31,17 @@ type LoginLog struct {
Status bool
ErrorMessage, Agent string
UserID uint
+ User *User
+ FilterByStatus bool
}
+
+type LoginLogRepo interface {
+ RecordLogin(context.Context, *LoginLog) error
+ ListLogins(context.Context, int, int, *LoginLog) ([]*LoginLog, int64, error)
+ FindLogin(context.Context, uint) (*LoginLog, error)
+ DeleteLogins(context.Context, []uint) error
+}
+
type DataAccessLog struct {
ID uint
CreatedAt time.Time
@@ -31,6 +50,13 @@ type DataAccessLog struct {
Scope int
RequestID, Method, Path, Detail string
}
+
+type DataAccessLogRepo interface {
+ RecordDataAccess(context.Context, *DataAccessLog) error
+ ListDataAccess(context.Context, int, int, *DataAccessLog) ([]*DataAccessLog, int64, error)
+ DeleteDataAccess(context.Context, []uint) error
+}
+
type LogDate struct {
Date string
FileCount int
@@ -48,33 +74,36 @@ type LogContent struct {
Size int64
ModifiedAt time.Time
}
+
+type LogFileRepo interface {
+ LogDates(context.Context, string) ([]LogDate, error)
+ LogFiles(context.Context, string) ([]LogFile, error)
+ LogContent(context.Context, string, string, *int64) (*LogContent, error)
+}
+
type ErrorRecord struct {
ID uint
CreatedAt time.Time
Form, Info, Level, RequestID, TraceID, Solution, Status string
+ CreatedAtRange []time.Time
}
-type AuditRepo interface {
- RecordOperation(context.Context, *OperationRecord) error
- ListOperations(context.Context, int, int, *OperationRecord) ([]*OperationRecord, int64, error)
- FindOperation(context.Context, uint) (*OperationRecord, error)
- DeleteOperations(context.Context, []uint) error
- RecordLogin(context.Context, *LoginLog) error
- ListLogins(context.Context, int, int, *LoginLog) ([]*LoginLog, int64, error)
- FindLogin(context.Context, uint) (*LoginLog, error)
- DeleteLogins(context.Context, []uint) error
- RecordDataAccess(context.Context, *DataAccessLog) error
- ListDataAccess(context.Context, int, int, *DataAccessLog) ([]*DataAccessLog, int64, error)
- DeleteDataAccess(context.Context, []uint) error
- LogDates(context.Context, string) ([]LogDate, error)
- LogFiles(context.Context, string) ([]LogFile, error)
- LogContent(context.Context, string, string, *int64) (*LogContent, error)
+
+type ErrorRecordRepo interface {
CreateError(context.Context, *ErrorRecord) error
UpdateError(context.Context, *ErrorRecord) error
DeleteErrors(context.Context, []uint) error
FindError(context.Context, uint) (*ErrorRecord, error)
ListErrors(context.Context, int, int, *ErrorRecord) ([]*ErrorRecord, int64, error)
}
-type AuditUsecase struct{ repo AuditRepo }
-func NewAuditUsecase(repo AuditRepo) *AuditUsecase { return &AuditUsecase{repo: repo} }
-func (uc *AuditUsecase) Repo() AuditRepo { return uc.repo }
+type AuditRepo interface {
+ OperationLogRepo
+ LoginLogRepo
+ DataAccessLogRepo
+ LogFileRepo
+ ErrorRecordRepo
+}
+
+type AuditUsecase struct{ AuditRepo }
+
+func NewAuditUsecase(repo AuditRepo) *AuditUsecase { return &AuditUsecase{AuditRepo: repo} }
diff --git a/internal/biz/dictionary.go b/internal/biz/dictionary.go
index fcc32ae..a860d41 100644
--- a/internal/biz/dictionary.go
+++ b/internal/biz/dictionary.go
@@ -5,19 +5,6 @@ import (
"time"
)
-type Dictionary struct {
- ID uint
- CreatedAt time.Time
- UpdatedAt time.Time
- Name string
- Type string
- Status bool
- Desc string
- ParentID *uint
- Children []*Dictionary
- Details []*DictionaryDetail
-}
-
type DictionaryDetail struct {
ID uint
CreatedAt time.Time
@@ -34,16 +21,66 @@ type DictionaryDetail struct {
Children []*DictionaryDetail
}
-type DictionaryRepo interface {
- CreateDictionary(context.Context, *Dictionary) error
- UpdateDictionary(context.Context, *Dictionary) error
- DeleteDictionary(context.Context, uint) error
- FindDictionary(context.Context, uint, string, bool) (*Dictionary, error)
- ListDictionaries(context.Context, int, int, string, string, bool) ([]*Dictionary, int64, error)
+type DictionaryDetailFilter struct {
+ DictionaryID uint
+ Label string
+ Value string
+ Status *bool
+ ParentID *uint
+ Level *int
+}
+
+type DictionaryDetailRepo interface {
CreateDictionaryDetail(context.Context, *DictionaryDetail) error
UpdateDictionaryDetail(context.Context, *DictionaryDetail) error
DeleteDictionaryDetail(context.Context, uint) error
FindDictionaryDetail(context.Context, uint) (*DictionaryDetail, error)
- ListDictionaryDetails(context.Context, int, int, uint, string, string) ([]*DictionaryDetail, int64, error)
+ ListDictionaryDetails(context.Context, int, int, DictionaryDetailFilter) ([]*DictionaryDetail, int64, error)
DictionaryDetailTree(context.Context, uint, string) ([]*DictionaryDetail, error)
}
+
+type Dictionary struct {
+ ID uint
+ CreatedAt time.Time
+ UpdatedAt time.Time
+ Name string
+ Type string
+ Status bool
+ Desc string
+ ParentID *uint
+ Children []*Dictionary
+ Details []*DictionaryDetail
+}
+
+type DictionaryRepo interface {
+ DictionaryMetadataRepo
+ DictionaryDetailRepo
+}
+
+type DictionaryMetadataRepo interface {
+ CreateDictionary(context.Context, *Dictionary) error
+ ImportDictionary(context.Context, *Dictionary, []*DictionaryDetail) error
+ UpdateDictionary(context.Context, *Dictionary) error
+ DeleteDictionary(context.Context, uint) error
+ FindDictionary(context.Context, uint, string, *bool, bool) (*Dictionary, error)
+ ExportDictionary(context.Context, uint) (*Dictionary, error)
+ ListDictionaries(context.Context, int, int, string, string, bool) ([]*Dictionary, int64, error)
+}
+
+func (uc *SettingsUsecase) ImportDictionary(ctx context.Context, dictionary *Dictionary, details []*DictionaryDetail) error {
+ return uc.SettingsRepo.ImportDictionary(ctx, dictionary, details)
+}
+
+func (uc *SettingsUsecase) DictionaryDetailsByParent(ctx context.Context, dictionaryID, parentID uint) ([]*DictionaryDetail, error) {
+ items, _, err := uc.ListDictionaryDetails(ctx, 0, 0, DictionaryDetailFilter{DictionaryID: dictionaryID})
+ if err != nil {
+ return nil, err
+ }
+ out := make([]*DictionaryDetail, 0)
+ for _, item := range items {
+ if (parentID == 0 && item.ParentID == nil) || (item.ParentID != nil && *item.ParentID == parentID) {
+ out = append(out, item)
+ }
+ }
+ return out, nil
+}
diff --git a/internal/biz/errors.go b/internal/biz/errors.go
index 6922727..1eb7be4 100644
--- a/internal/biz/errors.go
+++ b/internal/biz/errors.go
@@ -9,4 +9,5 @@ var (
// ErrInvalidCredentials is returned for any failed login, regardless of
// whether the user exists, to avoid leaking which accounts are registered.
ErrInvalidCredentials = errors.Unauthorized("INVALID_CREDENTIALS", "invalid credentials")
+ ErrUserDisabled = errors.Forbidden("USER_DISABLED", "user disabled")
)
diff --git a/internal/biz/export.go b/internal/biz/export.go
index cc0030d..9457b47 100644
--- a/internal/biz/export.go
+++ b/internal/biz/export.go
@@ -16,6 +16,8 @@ type ExportTemplate struct {
Order string
Conditions []ExportCondition
Joins []ExportJoin
+ StartCreatedAt *time.Time
+ EndCreatedAt *time.Time
}
type ExportRepo interface {
CreateExportTemplate(context.Context, *ExportTemplate) error
@@ -24,9 +26,9 @@ type ExportRepo interface {
FindExportTemplate(context.Context, uint, string) (*ExportTemplate, error)
ListExportTemplates(context.Context, int, int, *ExportTemplate) ([]*ExportTemplate, int64, error)
QueryExport(context.Context, *ExportTemplate, map[string]string) ([]map[string]any, string, error)
+ PreviewExport(context.Context, *ExportTemplate, map[string]string) (string, error)
ImportExportRows(context.Context, *ExportTemplate, []map[string]any) error
}
-type ExportUsecase struct{ repo ExportRepo }
+type ExportUsecase struct{ ExportRepo }
-func NewExportUsecase(repo ExportRepo) *ExportUsecase { return &ExportUsecase{repo: repo} }
-func (uc *ExportUsecase) Repo() ExportRepo { return uc.repo }
+func NewExportUsecase(repo ExportRepo) *ExportUsecase { return &ExportUsecase{ExportRepo: repo} }
diff --git a/internal/biz/media.go b/internal/biz/media.go
index 74e1e58..64430fe 100644
--- a/internal/biz/media.go
+++ b/internal/biz/media.go
@@ -13,62 +13,35 @@ import (
"github.com/google/uuid"
)
-type MediaFile struct {
- ID uint
- CreatedAt time.Time
- UpdatedAt time.Time
- Name string
- CategoryID int
- URL, Tag, Key string
- Size int64
- Mime, MD5 string
- UserID uint
-}
-type AttachmentCategory struct {
- ID uint
- CreatedAt time.Time
- UpdatedAt time.Time
- Name string
- ParentID uint
- Children []*AttachmentCategory
-}
-type MediaMetadataRepo interface {
- CreateMedia(context.Context, *MediaFile) error
- FindMedia(context.Context, uint) (*MediaFile, error)
- FindMediaByHash(context.Context, uint, string) (*MediaFile, error)
- ListMedia(context.Context, int, int, string, int, string, uint) ([]*MediaFile, int64, error)
- UpdateMediaName(context.Context, uint, string) error
- DeleteMedia(context.Context, uint) error
- MediaKeyReferences(context.Context, string) (int64, error)
- CreateMediaBatch(context.Context, []*MediaFile) error
- SaveCategory(context.Context, *AttachmentCategory) error
- DeleteCategory(context.Context, uint) error
- ListCategories(context.Context) ([]*AttachmentCategory, error)
-}
-
type MediaRepo interface {
MediaMetadataRepo
UploadRepo
}
type MediaUsecase struct {
- repo MediaRepo
+ MediaRepo
files FileStorage
}
func NewMediaUsecase(repo MediaRepo, files FileStorage) *MediaUsecase {
- return &MediaUsecase{repo: repo, files: files}
+ return &MediaUsecase{MediaRepo: repo, files: files}
}
-func (uc *MediaUsecase) Repo() MediaRepo { return uc.repo }
-var blockedExtensions = map[string]bool{".exe": true, ".dll": true, ".bat": true, ".cmd": true, ".sh": true, ".php": true, ".jsp": true, ".asp": true, ".aspx": true, ".html": true, ".htm": true, ".svg": true}
+var allowedMediaExtensions = map[string]bool{
+ ".jpg": true, ".jpeg": true, ".png": true, ".gif": true, ".webp": true, ".bmp": true, ".ico": true, ".avif": true,
+ ".mp3": true, ".wav": true, ".ogg": true, ".m4a": true, ".flac": true, ".aac": true,
+ ".mp4": true, ".webm": true, ".mov": true, ".avi": true, ".mkv": true,
+ ".txt": true, ".md": true, ".csv": true, ".json": true, ".log": true, ".pdf": true,
+ ".doc": true, ".docx": true, ".xls": true, ".xlsx": true, ".ppt": true, ".pptx": true,
+ ".zip": true, ".rar": true, ".7z": true, ".tar": true, ".gz": true, ".tgz": true, ".bin": true,
+}
func validateMediaName(name string) error {
- if name == "" || filepath.Base(name) != name {
- return errors.New("文件名不合法")
+ if name == "" || strings.TrimSpace(name) != name || strings.ContainsAny(name, `/\`) {
+ return errors.New("file extension is not allowed")
}
- if blockedExtensions[strings.ToLower(filepath.Ext(name))] {
- return errors.New("文件类型不允许上传")
+ if !allowedMediaExtensions[strings.ToLower(filepath.Ext(name))] {
+ return errors.New("file extension is not allowed")
}
return nil
}
@@ -85,7 +58,7 @@ func (uc *MediaUsecase) Upload(ctx context.Context, userID uint, name, mime stri
}
media := &MediaFile{Name: name, CategoryID: categoryID, URL: stored.URL, Tag: strings.TrimPrefix(ext, "."), Key: key, Size: stored.Size, Mime: mime, MD5: hex.EncodeToString(hash.Sum(nil)), UserID: userID}
if save {
- if err = uc.repo.CreateMedia(ctx, media); err != nil {
+ if err = uc.CreateMedia(ctx, media); err != nil {
_ = uc.files.Delete(ctx, key)
return nil, err
}
@@ -93,11 +66,11 @@ func (uc *MediaUsecase) Upload(ctx context.Context, userID uint, name, mime stri
return media, nil
}
func (uc *MediaUsecase) Delete(ctx context.Context, id uint) error {
- media, err := uc.repo.FindMedia(ctx, id)
+ media, err := uc.FindMedia(ctx, id)
if err != nil {
return err
}
- count, err := uc.repo.MediaKeyReferences(ctx, media.Key)
+ count, err := uc.MediaKeyReferences(ctx, media.Key)
if err != nil {
return err
}
@@ -106,7 +79,7 @@ func (uc *MediaUsecase) Delete(ctx context.Context, id uint) error {
return err
}
}
- return uc.repo.DeleteMedia(ctx, id)
+ return uc.DeleteMedia(ctx, id)
}
func (uc *MediaUsecase) ListStorage(ctx context.Context, prefix, cursor string, limit int) ([]*StoredFile, string, bool, error) {
return uc.files.List(ctx, prefix, cursor, limit)
diff --git a/internal/biz/media_metadata.go b/internal/biz/media_metadata.go
new file mode 100644
index 0000000..48ad5d4
--- /dev/null
+++ b/internal/biz/media_metadata.go
@@ -0,0 +1,53 @@
+package biz
+
+import (
+ "context"
+ "time"
+)
+
+type MediaFile struct {
+ ID uint
+ CreatedAt time.Time
+ UpdatedAt time.Time
+ Name string
+ CategoryID int
+ URL, Tag, Key string
+ Size int64
+ Mime, MD5 string
+ UserID uint
+}
+
+type MediaFilter struct {
+ Page, PageSize int
+ Keyword string
+ CategoryID int
+ Tag string
+ UserID uint
+ StartCreatedAt time.Time
+ EndCreatedAt time.Time
+ OrderKey string
+ Desc bool
+}
+
+type AttachmentCategory struct {
+ ID uint
+ CreatedAt time.Time
+ UpdatedAt time.Time
+ Name string
+ ParentID uint
+ Children []*AttachmentCategory
+}
+
+type MediaMetadataRepo interface {
+ CreateMedia(context.Context, *MediaFile) error
+ FindMedia(context.Context, uint) (*MediaFile, error)
+ FindMediaByHash(context.Context, uint, string) (*MediaFile, error)
+ ListMedia(context.Context, MediaFilter) ([]*MediaFile, int64, error)
+ UpdateMediaName(context.Context, uint, string) error
+ DeleteMedia(context.Context, uint) error
+ MediaKeyReferences(context.Context, string) (int64, error)
+ CreateMediaBatch(context.Context, []*MediaFile) error
+ SaveCategory(context.Context, *AttachmentCategory) error
+ DeleteCategory(context.Context, uint) error
+ ListCategories(context.Context) ([]*AttachmentCategory, error)
+}
diff --git a/internal/biz/media_upload.go b/internal/biz/media_upload.go
index 9dec91f..958e2f4 100644
--- a/internal/biz/media_upload.go
+++ b/internal/biz/media_upload.go
@@ -15,33 +15,6 @@ import (
"github.com/google/uuid"
)
-type UploadSession struct {
- ID uint
- UserID uint
- FileName, FileHash string
- FileSize, ChunkSize int64
- ChunkTotal int
- Status, StorageKey string
- MediaID uint
-}
-type UploadChunk struct {
- Index int
- Hash string
- Size int64
-}
-type UploadRepo interface {
- FindUploadingSession(context.Context, uint, string) (*UploadSession, error)
- CreateUploadSession(context.Context, *UploadSession) error
- FindUploadSession(context.Context, uint) (*UploadSession, error)
- CompleteUploadSession(context.Context, uint, string, uint) error
- DeleteUploadSession(context.Context, uint) error
- UpsertChunk(context.Context, uint, *UploadChunk) error
- ListChunks(context.Context, uint) ([]*UploadChunk, error)
- DeleteChunks(context.Context, uint) error
- StaleUploadSessionIDs(context.Context, time.Time) ([]uint, error)
- DeleteUploadData(context.Context, uint) error
-}
-
func (uc *MediaUsecase) InitUpload(ctx context.Context, userID uint, name, hash string, size, chunkSize int64, total int) (*UploadSession, *MediaFile, []int, error) {
if err := validateMediaName(name); err != nil {
return nil, nil, nil, err
@@ -49,22 +22,22 @@ func (uc *MediaUsecase) InitUpload(ctx context.Context, userID uint, name, hash
if size <= 0 || chunkSize <= 0 || total <= 0 {
return nil, nil, nil, errors.New("上传参数不合法")
}
- if media, err := uc.repo.FindMediaByHash(ctx, userID, hash); err == nil {
+ if media, err := uc.FindMediaByHash(ctx, userID, hash); err == nil {
copy := *media
copy.ID = 0
copy.Name = name
- if err = uc.repo.CreateMedia(ctx, ©); err == nil {
+ if err = uc.CreateMedia(ctx, ©); err == nil {
return nil, ©, nil, nil
}
}
- session, err := uc.repo.FindUploadingSession(ctx, userID, hash)
+ session, err := uc.FindUploadingSession(ctx, userID, hash)
if err != nil {
session = &UploadSession{UserID: userID, FileName: name, FileHash: hash, FileSize: size, ChunkSize: chunkSize, ChunkTotal: total, Status: "uploading"}
- if err = uc.repo.CreateUploadSession(ctx, session); err != nil {
+ if err = uc.CreateUploadSession(ctx, session); err != nil {
return nil, nil, nil, err
}
}
- chunks, err := uc.repo.ListChunks(ctx, session.ID)
+ chunks, err := uc.ListChunks(ctx, session.ID)
if err != nil {
return nil, nil, nil, err
}
@@ -76,7 +49,7 @@ func (uc *MediaUsecase) InitUpload(ctx context.Context, userID uint, name, hash
return session, nil, indices, nil
}
func (uc *MediaUsecase) SaveChunk(ctx context.Context, userID, uploadID uint, index int, expected string, reader io.Reader) error {
- session, err := uc.repo.FindUploadSession(ctx, uploadID)
+ session, err := uc.FindUploadSession(ctx, uploadID)
if err != nil || session.UserID != userID {
return errors.New("上传会话不存在或无权操作")
}
@@ -94,14 +67,14 @@ func (uc *MediaUsecase) SaveChunk(ctx context.Context, userID, uploadID uint, in
_ = uc.files.Delete(ctx, key)
return errors.New("分片校验失败")
}
- return uc.repo.UpsertChunk(ctx, uploadID, &UploadChunk{Index: index, Hash: actual, Size: stored.Size})
+ return uc.UpsertChunk(ctx, uploadID, &UploadChunk{Index: index, Hash: actual, Size: stored.Size})
}
func (uc *MediaUsecase) CompleteUpload(ctx context.Context, userID, uploadID uint, mime string) (*MediaFile, error) {
- session, err := uc.repo.FindUploadSession(ctx, uploadID)
+ session, err := uc.FindUploadSession(ctx, uploadID)
if err != nil || session.UserID != userID {
return nil, errors.New("上传会话不存在或无权操作")
}
- chunks, err := uc.repo.ListChunks(ctx, uploadID)
+ chunks, err := uc.ListChunks(ctx, uploadID)
if err != nil || len(chunks) != session.ChunkTotal {
return nil, errors.New("分片不完整")
}
@@ -124,34 +97,34 @@ func (uc *MediaUsecase) CompleteUpload(ctx context.Context, userID, uploadID uin
return nil, errors.New("整文件校验失败")
}
media := &MediaFile{Name: session.FileName, URL: stored.URL, Tag: strings.TrimPrefix(ext, "."), Key: key, Size: stored.Size, Mime: mime, MD5: hash, UserID: userID}
- if err = uc.repo.CreateMedia(ctx, media); err != nil {
+ if err = uc.CreateMedia(ctx, media); err != nil {
_ = uc.files.Delete(ctx, key)
return nil, err
}
- _ = uc.repo.CompleteUploadSession(ctx, uploadID, key, media.ID)
- _ = uc.repo.DeleteChunks(ctx, uploadID)
+ _ = uc.CompleteUploadSession(ctx, uploadID, key, media.ID)
+ _ = uc.DeleteChunks(ctx, uploadID)
_ = uc.files.DeletePrefix(ctx, fmt.Sprintf(".chunks/%d", uploadID))
return media, nil
}
func (uc *MediaUsecase) CancelUpload(ctx context.Context, userID, uploadID uint) error {
- session, err := uc.repo.FindUploadSession(ctx, uploadID)
+ session, err := uc.FindUploadSession(ctx, uploadID)
if err != nil || session.UserID != userID {
return errors.New("上传会话不存在或无权操作")
}
- _ = uc.repo.DeleteChunks(ctx, uploadID)
+ _ = uc.DeleteChunks(ctx, uploadID)
_ = uc.files.DeletePrefix(ctx, fmt.Sprintf(".chunks/%d", uploadID))
- return uc.repo.DeleteUploadSession(ctx, uploadID)
+ return uc.DeleteUploadSession(ctx, uploadID)
}
func (uc *MediaUsecase) CleanupStale(ctx context.Context, ttlHours int) error {
if ttlHours <= 0 {
ttlHours = 24
}
- ids, err := uc.repo.StaleUploadSessionIDs(ctx, time.Now().Add(-time.Duration(ttlHours)*time.Hour))
+ ids, err := uc.StaleUploadSessionIDs(ctx, time.Now().Add(-time.Duration(ttlHours)*time.Hour))
if err != nil {
return err
}
for _, id := range ids {
- if err = uc.repo.DeleteUploadData(ctx, id); err != nil {
+ if err = uc.DeleteUploadData(ctx, id); err != nil {
return err
}
if err = uc.files.DeletePrefix(ctx, fmt.Sprintf(".chunks/%d", id)); err != nil {
diff --git a/internal/biz/menu.go b/internal/biz/menu.go
index 90430ae..bdc3d00 100644
--- a/internal/biz/menu.go
+++ b/internal/biz/menu.go
@@ -47,6 +47,7 @@ type MenuRepo interface {
SetAuthorityMenus(context.Context, uint, []uint) error
AuthorityMenuIDs(context.Context, uint) ([]uint, error)
MenuRoleIDs(context.Context, uint) ([]uint, error)
+ DefaultRouterRoleIDs(context.Context, uint) ([]uint, error)
SetMenuRoles(context.Context, uint, []uint) error
}
@@ -128,6 +129,9 @@ func (uc *MenuUsecase) AuthorityMenus(ctx context.Context, authorityID uint) ([]
func (uc *MenuUsecase) RoleIDs(ctx context.Context, menuID uint) ([]uint, error) {
return uc.repo.MenuRoleIDs(ctx, menuID)
}
+func (uc *MenuUsecase) DefaultRouterRoleIDs(ctx context.Context, menuID uint) ([]uint, error) {
+ return uc.repo.DefaultRouterRoleIDs(ctx, menuID)
+}
func (uc *MenuUsecase) SetRoles(ctx context.Context, menuID uint, ids []uint) error {
return uc.repo.SetMenuRoles(ctx, menuID, ids)
diff --git a/internal/biz/organization.go b/internal/biz/organization.go
index 7e1abfe..f097c74 100644
--- a/internal/biz/organization.go
+++ b/internal/biz/organization.go
@@ -14,8 +14,28 @@ type Department struct {
Ancestors string
Sort int
LeaderID uint
+ Leader *User
Status bool
Children []*Department
+ NamePath string
+}
+
+type DepartmentRepo interface {
+ CreateDepartment(context.Context, *Department) error
+ UpdateDepartment(context.Context, *Department) error
+ DeleteDepartment(context.Context, uint) error
+ FindDepartment(context.Context, uint) (*Department, error)
+ ListDepartments(context.Context, string) ([]*Department, error)
+ DepartmentUserIDs(context.Context, uint) ([]uint, error)
+ SetDepartmentUsers(context.Context, uint, []uint) error
+ SetUserDepartments(context.Context, uint, []uint, uint) error
+}
+
+func (uc *OrganizationUsecase) Departments(ctx context.Context, name string) ([]*Department, error) {
+ return uc.ListDepartments(ctx, name)
+}
+func (uc *OrganizationUsecase) Department(ctx context.Context, id uint) (*Department, error) {
+ return uc.FindDepartment(ctx, id)
}
type Position struct {
@@ -29,76 +49,37 @@ type Position struct {
Remark string
}
-type OrganizationRepo interface {
- CreateDepartment(context.Context, *Department) error
- UpdateDepartment(context.Context, *Department) error
- DeleteDepartment(context.Context, uint) error
- FindDepartment(context.Context, uint) (*Department, error)
- ListDepartments(context.Context, string) ([]*Department, error)
- DepartmentUserIDs(context.Context, uint) ([]uint, error)
- SetDepartmentUsers(context.Context, uint, []uint) error
- SetUserDepartments(context.Context, uint, []uint, uint) error
+type PositionListFilter struct {
+ Name string
+ Code string
+ Status *bool
+}
+
+type PositionRepo interface {
CreatePosition(context.Context, *Position) error
UpdatePosition(context.Context, *Position) error
DeletePosition(context.Context, uint) error
FindPosition(context.Context, uint) (*Position, error)
- ListPositions(context.Context, int, int, *Position) ([]*Position, int64, error)
+ ListPositions(context.Context, int, int, *PositionListFilter) ([]*Position, int64, error)
PositionUserIDs(context.Context, uint) ([]uint, error)
SetPositionUsers(context.Context, uint, []uint) error
SetUserPositions(context.Context, uint, []uint) error
}
-type OrganizationUsecase struct{ repo OrganizationRepo }
-
-func NewOrganizationUsecase(repo OrganizationRepo) *OrganizationUsecase {
- return &OrganizationUsecase{repo: repo}
-}
-
-func (uc *OrganizationUsecase) Departments(ctx context.Context, name string) ([]*Department, error) {
- return uc.repo.ListDepartments(ctx, name)
-}
-func (uc *OrganizationUsecase) CreateDepartment(ctx context.Context, value *Department) error {
- return uc.repo.CreateDepartment(ctx, value)
-}
-func (uc *OrganizationUsecase) UpdateDepartment(ctx context.Context, value *Department) error {
- return uc.repo.UpdateDepartment(ctx, value)
-}
-func (uc *OrganizationUsecase) DeleteDepartment(ctx context.Context, id uint) error {
- return uc.repo.DeleteDepartment(ctx, id)
-}
-func (uc *OrganizationUsecase) Department(ctx context.Context, id uint) (*Department, error) {
- return uc.repo.FindDepartment(ctx, id)
-}
-func (uc *OrganizationUsecase) DepartmentUserIDs(ctx context.Context, id uint) ([]uint, error) {
- return uc.repo.DepartmentUserIDs(ctx, id)
-}
-func (uc *OrganizationUsecase) SetDepartmentUsers(ctx context.Context, id uint, ids []uint) error {
- return uc.repo.SetDepartmentUsers(ctx, id, ids)
-}
-func (uc *OrganizationUsecase) SetUserDepartments(ctx context.Context, id uint, ids []uint, primary uint) error {
- return uc.repo.SetUserDepartments(ctx, id, ids, primary)
-}
-func (uc *OrganizationUsecase) Positions(ctx context.Context, page, size int, filter *Position) ([]*Position, int64, error) {
- return uc.repo.ListPositions(ctx, page, size, filter)
-}
-func (uc *OrganizationUsecase) CreatePosition(ctx context.Context, value *Position) error {
- return uc.repo.CreatePosition(ctx, value)
-}
-func (uc *OrganizationUsecase) UpdatePosition(ctx context.Context, value *Position) error {
- return uc.repo.UpdatePosition(ctx, value)
-}
-func (uc *OrganizationUsecase) DeletePosition(ctx context.Context, id uint) error {
- return uc.repo.DeletePosition(ctx, id)
+func (uc *OrganizationUsecase) Positions(ctx context.Context, page, size int, filter *PositionListFilter) ([]*Position, int64, error) {
+ return uc.ListPositions(ctx, page, size, filter)
}
func (uc *OrganizationUsecase) Position(ctx context.Context, id uint) (*Position, error) {
- return uc.repo.FindPosition(ctx, id)
+ return uc.FindPosition(ctx, id)
}
-func (uc *OrganizationUsecase) PositionUserIDs(ctx context.Context, id uint) ([]uint, error) {
- return uc.repo.PositionUserIDs(ctx, id)
+
+type OrganizationRepo interface {
+ DepartmentRepo
+ PositionRepo
}
-func (uc *OrganizationUsecase) SetPositionUsers(ctx context.Context, id uint, ids []uint) error {
- return uc.repo.SetPositionUsers(ctx, id, ids)
-}
-func (uc *OrganizationUsecase) SetUserPositions(ctx context.Context, id uint, ids []uint) error {
- return uc.repo.SetUserPositions(ctx, id, ids)
+
+type OrganizationUsecase struct{ OrganizationRepo }
+
+func NewOrganizationUsecase(repo OrganizationRepo) *OrganizationUsecase {
+ return &OrganizationUsecase{OrganizationRepo: repo}
}
diff --git a/internal/biz/parameter.go b/internal/biz/parameter.go
index 7bfa0e9..a89d850 100644
--- a/internal/biz/parameter.go
+++ b/internal/biz/parameter.go
@@ -6,13 +6,15 @@ import (
)
type SystemParameter struct {
- ID uint
- CreatedAt time.Time
- UpdatedAt time.Time
- Name string
- Key string
- Value string
- Desc string
+ ID uint
+ CreatedAt time.Time
+ UpdatedAt time.Time
+ Name string
+ Key string
+ Value string
+ Desc string
+ StartCreatedAt *time.Time
+ EndCreatedAt *time.Time
}
type ParameterRepo interface {
diff --git a/internal/biz/security.go b/internal/biz/security.go
index 611ffeb..5fe8b17 100644
--- a/internal/biz/security.go
+++ b/internal/biz/security.go
@@ -1,6 +1,9 @@
package biz
-import "context"
+import (
+ "context"
+ "errors"
+)
type SecurityConfig struct {
ID uint
@@ -29,3 +32,42 @@ type SecurityRepo interface {
SecurityConfig(context.Context) (*SecurityConfig, error)
SaveSecurityConfig(context.Context, *SecurityConfig) error
}
+
+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)
+}
+
+func (uc *SettingsUsecase) ValidatePassword(value *SecurityConfig, password string) error {
+ if len([]rune(password)) < value.PwdMinLength {
+ return errors.New("密码长度不足")
+ }
+ hasUpper, hasLower, hasDigit, hasSpecial := false, false, false, false
+ for _, ch := range password {
+ switch {
+ case ch >= 'A' && ch <= 'Z':
+ hasUpper = true
+ case ch >= 'a' && ch <= 'z':
+ hasLower = true
+ case ch >= '0' && ch <= '9':
+ hasDigit = true
+ default:
+ hasSpecial = true
+ }
+ }
+ if value.PwdRequireUpper && !hasUpper {
+ return errors.New("密码必须包含大写字母")
+ }
+ if value.PwdRequireLower && !hasLower {
+ return errors.New("密码必须包含小写字母")
+ }
+ if value.PwdRequireDigit && !hasDigit {
+ return errors.New("密码必须包含数字")
+ }
+ if value.PwdRequireSpecial && !hasSpecial {
+ return errors.New("密码必须包含特殊字符")
+ }
+ return nil
+}
diff --git a/internal/biz/settings.go b/internal/biz/settings.go
index a387d7d..ac7901b 100644
--- a/internal/biz/settings.go
+++ b/internal/biz/settings.go
@@ -7,7 +7,8 @@ type SettingsRepo interface {
SecurityRepo
}
-type SettingsUsecase struct{ repo SettingsRepo }
+type SettingsUsecase struct{ SettingsRepo }
-func NewSettingsUsecase(repo SettingsRepo) *SettingsUsecase { return &SettingsUsecase{repo: repo} }
-func (uc *SettingsUsecase) Repo() SettingsRepo { return uc.repo }
+func NewSettingsUsecase(repo SettingsRepo) *SettingsUsecase {
+ return &SettingsUsecase{SettingsRepo: repo}
+}
diff --git a/internal/biz/system_init.go b/internal/biz/system_init.go
index ad85687..ac2a8a4 100644
--- a/internal/biz/system_init.go
+++ b/internal/biz/system_init.go
@@ -12,6 +12,7 @@ type InitializationRepo interface {
Initialize(context.Context, *DatabaseConfig) error
PersistConfig(context.Context) error
PersistAdminConfig(context.Context, []byte) error
+ PersistRuntimeConfig(context.Context, []byte, []byte) error
ReloadConfig(context.Context) error
}
@@ -28,4 +29,7 @@ func (uc *SystemUsecase) PersistConfig(ctx context.Context) error { return uc.re
func (uc *SystemUsecase) PersistAdminConfig(ctx context.Context, value []byte) error {
return uc.repo.PersistAdminConfig(ctx, value)
}
+func (uc *SystemUsecase) PersistRuntimeConfig(ctx context.Context, data, admin []byte) error {
+ return uc.repo.PersistRuntimeConfig(ctx, data, admin)
+}
func (uc *SystemUsecase) ReloadConfig(ctx context.Context) error { return uc.repo.ReloadConfig(ctx) }
diff --git a/internal/biz/task.go b/internal/biz/task.go
index b4a4fe8..3eb628c 100644
--- a/internal/biz/task.go
+++ b/internal/biz/task.go
@@ -2,7 +2,13 @@ package biz
import (
"context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net/url"
"time"
+
+ "github.com/robfig/cron/v3"
)
type TimedTask struct {
@@ -28,6 +34,37 @@ type TimedTaskLog struct {
DurationMS int64
Status, ErrorMsg, Output string
}
+
+const (
+ TaskExecutorMethod = "method"
+ TaskExecutorHTTP = "http"
+ TaskMethodClearDB = "ClearDB"
+ TaskMethodUploads = "CleanStaleUploads"
+)
+
+type TaskMethod struct {
+ Name string
+ Description string
+}
+
+var registeredTaskMethods = []TaskMethod{
+ {Name: TaskMethodClearDB, Description: "清理数据库过期日志(操作记录、JWT黑名单、定时任务日志)"},
+ {Name: TaskMethodUploads, Description: "清理过期大文件上传会话"},
+}
+
+func RegisteredTaskMethods() []TaskMethod {
+ return append([]TaskMethod(nil), registeredTaskMethods...)
+}
+
+func registeredTaskMethod(name string) bool {
+ for _, method := range registeredTaskMethods {
+ if method.Name == name {
+ return true
+ }
+ }
+ return false
+}
+
type TaskRepo interface {
CreateTask(context.Context, *TimedTask) error
UpdateTask(context.Context, *TimedTask) error
@@ -40,7 +77,75 @@ type TaskRepo interface {
CleanupLogs(context.Context) error
TaskNameExists(context.Context, string, uint) (bool, error)
}
-type TaskUsecase struct{ repo TaskRepo }
+type TaskUsecase struct{ TaskRepo }
-func NewTaskUsecase(repo TaskRepo) *TaskUsecase { return &TaskUsecase{repo: repo} }
-func (uc *TaskUsecase) Repo() TaskRepo { return uc.repo }
+func NewTaskUsecase(repo TaskRepo) *TaskUsecase { return &TaskUsecase{TaskRepo: repo} }
+
+func (uc *TaskUsecase) Validate(value *TimedTask) error {
+ if value.Name == "" {
+ return errors.New("任务名不能为空")
+ }
+ var err error
+ if value.WithSeconds {
+ _, err = cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor).Parse(value.Spec)
+ } else {
+ _, err = cron.ParseStandard(value.Spec)
+ }
+ if err != nil {
+ return fmt.Errorf("cron 表达式非法: %w", err)
+ }
+ switch value.ExecutorType {
+ case TaskExecutorMethod:
+ if !registeredTaskMethod(value.MethodName) {
+ return errors.New("方法未注册")
+ }
+ if len(value.Params) > 0 && !json.Valid(value.Params) {
+ return errors.New("params 必须是合法 JSON")
+ }
+ case TaskExecutorHTTP:
+ parsed, parseErr := url.Parse(value.HTTPURL)
+ if parseErr != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.Hostname() == "" {
+ return errors.New("httpUrl 必须是合法的 http/https 地址")
+ }
+ if len(value.HTTPHeader) > 0 {
+ headers := map[string]string{}
+ if json.Unmarshal(value.HTTPHeader, &headers) != nil {
+ return errors.New("httpHeader 必须是 JSON 对象")
+ }
+ }
+ default:
+ return errors.New("executorType 必须为 method 或 http")
+ }
+ return nil
+}
+
+func (uc *TaskUsecase) Create(ctx context.Context, value *TimedTask) error {
+ if err := uc.Validate(value); err != nil {
+ return err
+ }
+ exists, err := uc.TaskNameExists(ctx, value.Name, 0)
+ if err != nil {
+ return err
+ }
+ if exists {
+ return fmt.Errorf("任务名 %s 已存在", value.Name)
+ }
+ return uc.CreateTask(ctx, value)
+}
+
+func (uc *TaskUsecase) Update(ctx context.Context, value *TimedTask) error {
+ if value.ID == 0 {
+ return errors.New("缺少任务 ID")
+ }
+ if err := uc.Validate(value); err != nil {
+ return err
+ }
+ exists, err := uc.TaskNameExists(ctx, value.Name, value.ID)
+ if err != nil {
+ return err
+ }
+ if exists {
+ return fmt.Errorf("任务名 %s 已存在", value.Name)
+ }
+ return uc.UpdateTask(ctx, value)
+}
diff --git a/internal/biz/upload_session.go b/internal/biz/upload_session.go
new file mode 100644
index 0000000..bae39aa
--- /dev/null
+++ b/internal/biz/upload_session.go
@@ -0,0 +1,35 @@
+package biz
+
+import (
+ "context"
+ "time"
+)
+
+type UploadSession struct {
+ ID uint
+ UserID uint
+ FileName, FileHash string
+ FileSize, ChunkSize int64
+ ChunkTotal int
+ Status, StorageKey string
+ MediaID uint
+}
+
+type UploadChunk struct {
+ Index int
+ Hash string
+ Size int64
+}
+
+type UploadRepo interface {
+ FindUploadingSession(context.Context, uint, string) (*UploadSession, error)
+ CreateUploadSession(context.Context, *UploadSession) error
+ FindUploadSession(context.Context, uint) (*UploadSession, error)
+ CompleteUploadSession(context.Context, uint, string, uint) error
+ DeleteUploadSession(context.Context, uint) error
+ UpsertChunk(context.Context, uint, *UploadChunk) error
+ ListChunks(context.Context, uint) ([]*UploadChunk, error)
+ DeleteChunks(context.Context, uint) error
+ StaleUploadSessionIDs(context.Context, time.Time) ([]uint, error)
+ DeleteUploadData(context.Context, uint) error
+}
diff --git a/internal/biz/user.go b/internal/biz/user.go
index d2f15d5..85f0849 100644
--- a/internal/biz/user.go
+++ b/internal/biz/user.go
@@ -41,6 +41,7 @@ type Authority struct {
ParentID *uint
DataScope int
DefaultRouter string
+ Children []*Authority
}
type UserRepo interface {
@@ -49,13 +50,17 @@ type UserRepo interface {
MenusByAuthority(context.Context, uint) ([]*Menu, error)
ListUsers(context.Context, int, int, *UserListFilter) ([]*User, int64, error)
CreateUser(context.Context, *User) (*User, error)
+ CreateUserWithAuthorities(context.Context, *User, []uint) (*User, error)
UpdateUser(context.Context, *User) error
+ UpdateSelfUser(context.Context, *User) error
+ UpdateUserWithAuthorities(context.Context, *User, []uint) error
DeleteUser(context.Context, uint) error
UpdatePassword(context.Context, uint, string, bool) error
SetUserAuthorities(context.Context, uint, []uint) error
SetUserAuthority(context.Context, uint, uint) error
SetUserSetting(context.Context, uint, map[string]any) error
ListAuthorities(context.Context) ([]*Authority, error)
+ FillDepartmentNamePaths(context.Context, *User) error
}
func (uc *SystemUsecase) Login(ctx context.Context, username, password string) (*User, error) {
@@ -63,11 +68,37 @@ func (uc *SystemUsecase) Login(ctx context.Context, username, password string) (
if err != nil || bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password)) != nil {
return nil, ErrInvalidCredentials
}
+ uc.fallbackDefaultRouter(ctx, u)
return u, nil
}
func (uc *SystemUsecase) User(ctx context.Context, id uint) (*User, error) {
- return uc.repo.FindUserByID(ctx, id)
+ user, err := uc.repo.FindUserByID(ctx, id)
+ if err != nil {
+ return nil, err
+ }
+ _ = uc.repo.FillDepartmentNamePaths(ctx, user)
+ uc.fallbackDefaultRouter(ctx, user)
+ return user, nil
+}
+
+func (uc *SystemUsecase) fallbackDefaultRouter(ctx context.Context, user *User) {
+ if user == nil || user.Authority.DefaultRouter == "" {
+ return
+ }
+ menus, err := uc.repo.MenusByAuthority(ctx, user.AuthorityID)
+ if err != nil || !menuNameExists(menus, user.Authority.DefaultRouter) {
+ user.Authority.DefaultRouter = "404"
+ }
+}
+
+func menuNameExists(menus []*Menu, name string) bool {
+ for _, menu := range menus {
+ if menu.Name == name || menuNameExists(menu.Children, name) {
+ return true
+ }
+ }
+ return false
}
func (uc *SystemUsecase) Menus(ctx context.Context, authorityID uint) ([]*Menu, error) {
@@ -77,16 +108,37 @@ func (uc *SystemUsecase) Menus(ctx context.Context, authorityID uint) ([]*Menu,
func (uc *SystemUsecase) ListUsers(ctx context.Context, page, pageSize int, filter *UserListFilter) ([]*User, int64, error) {
return uc.repo.ListUsers(ctx, page, pageSize, filter)
}
-func (uc *SystemUsecase) CreateUser(ctx context.Context, user *User) (*User, error) {
+func (uc *SystemUsecase) CreateUser(ctx context.Context, user *User, authorityIDs []uint) (*User, error) {
+ if user.AuthorityID == 0 && len(authorityIDs) > 0 {
+ user.AuthorityID = authorityIDs[0]
+ }
+ authorityIDs = includeAuthority(authorityIDs, user.AuthorityID)
hash, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
if err != nil {
return nil, err
}
user.Password = string(hash)
- return uc.repo.CreateUser(ctx, user)
+ return uc.repo.CreateUserWithAuthorities(ctx, user, authorityIDs)
}
-func (uc *SystemUsecase) UpdateUser(ctx context.Context, user *User) error {
- return uc.repo.UpdateUser(ctx, user)
+func (uc *SystemUsecase) UpdateUser(ctx context.Context, user *User, authorityIDs []uint) error {
+ authorityIDs = includeAuthority(authorityIDs, user.AuthorityID)
+ return uc.repo.UpdateUserWithAuthorities(ctx, user, authorityIDs)
+}
+
+func (uc *SystemUsecase) UpdateSelfUser(ctx context.Context, user *User) error {
+ return uc.repo.UpdateSelfUser(ctx, user)
+}
+
+func includeAuthority(ids []uint, primary uint) []uint {
+ if primary == 0 {
+ return ids
+ }
+ for _, id := range ids {
+ if id == primary {
+ return ids
+ }
+ }
+ return append(ids, primary)
}
func (uc *SystemUsecase) DeleteUser(ctx context.Context, id uint) error {
return uc.repo.DeleteUser(ctx, id)
diff --git a/internal/biz/version.go b/internal/biz/version.go
index ad404eb..ed23d24 100644
--- a/internal/biz/version.go
+++ b/internal/biz/version.go
@@ -2,9 +2,12 @@ package biz
import (
"context"
+ "errors"
"time"
)
+var ErrInvalidVersion = errors.New("版本信息格式错误")
+
type Version struct {
ID uint
CreatedAt time.Time
@@ -27,11 +30,10 @@ type VersionRepo interface {
CreateVersion(context.Context, *Version) error
DeleteVersions(context.Context, []uint) error
FindVersion(context.Context, uint) (*Version, error)
- ListVersions(context.Context, int, int, string, string) ([]*Version, int64, error)
+ ListVersions(context.Context, int, int, string, string, []*time.Time) ([]*Version, int64, error)
BuildVersionBundle(context.Context, []uint, []uint, []uint) (*VersionBundle, error)
ImportVersionBundle(context.Context, *VersionBundle) error
}
-type VersionUsecase struct{ repo VersionRepo }
+type VersionUsecase struct{ VersionRepo }
-func NewVersionUsecase(repo VersionRepo) *VersionUsecase { return &VersionUsecase{repo: repo} }
-func (uc *VersionUsecase) Repo() VersionRepo { return uc.repo }
+func NewVersionUsecase(repo VersionRepo) *VersionUsecase { return &VersionUsecase{VersionRepo: repo} }
diff --git a/internal/conf/conf.pb.go b/internal/conf/conf.pb.go
index 24633dd..1e50a77 100644
--- a/internal/conf/conf.pb.go
+++ b/internal/conf/conf.pb.go
@@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
-// protoc-gen-go v1.36.11
+// protoc-gen-go v1.36.12
// protoc v7.35.1
// source: conf/conf.proto
@@ -130,6 +130,7 @@ type Data struct {
state protoimpl.MessageState `protogen:"open.v1"`
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"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -178,6 +179,13 @@ func (x *Data) GetRedis() *Data_Redis {
return nil
}
+func (x *Data) GetDatabaseList() []*Data_Database {
+ if x != nil {
+ return x.DatabaseList
+ }
+ return nil
+}
+
// AdminBackend contains settings for the administration HTTP transport.
type AdminBackend struct {
state protoimpl.MessageState `protogen:"open.v1"`
@@ -188,8 +196,10 @@ type AdminBackend struct {
Email *AdminBackend_Email `protobuf:"bytes,5,opt,name=email,proto3" json:"email,omitempty"`
Storage *AdminBackend_Storage `protobuf:"bytes,6,opt,name=storage,proto3" json:"storage,omitempty"`
// ConfigPath is populated by the entrypoint and is not required in YAML.
- ConfigPath string `protobuf:"bytes,7,opt,name=config_path,json=configPath,proto3" json:"config_path,omitempty"`
- Media *AdminBackend_Media `protobuf:"bytes,8,opt,name=media,proto3" json:"media,omitempty"`
+ ConfigPath string `protobuf:"bytes,7,opt,name=config_path,json=configPath,proto3" json:"config_path,omitempty"`
+ 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"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -280,6 +290,20 @@ func (x *AdminBackend) GetMedia() *AdminBackend_Media {
return nil
}
+func (x *AdminBackend) GetDiskList() []*AdminBackend_Disk {
+ if x != nil {
+ return x.DiskList
+ }
+ return nil
+}
+
+func (x *AdminBackend) GetSystem() *AdminBackend_System {
+ if x != nil {
+ return x.System
+ }
+ return nil
+}
+
type Server_HTTP struct {
state protoimpl.MessageState `protogen:"open.v1"`
Network string `protobuf:"bytes,1,opt,name=network,proto3" json:"network,omitempty"`
@@ -351,6 +375,8 @@ type Data_Database struct {
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
}
@@ -448,6 +474,20 @@ func (x *Data_Database) GetPath() string {
return ""
}
+func (x *Data_Database) GetAliasName() string {
+ if x != nil {
+ return x.AliasName
+ }
+ return ""
+}
+
+func (x *Data_Database) GetDisable() bool {
+ if x != nil {
+ return x.Disable
+ }
+ return false
+}
+
type Data_Redis struct {
state protoimpl.MessageState `protogen:"open.v1"`
Network string `protobuf:"bytes,1,opt,name=network,proto3" json:"network,omitempty"`
@@ -848,6 +888,118 @@ func (x *AdminBackend_Media) GetSessionTtl() int32 {
return 0
}
+type AdminBackend_Disk struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ MountPoint string `protobuf:"bytes,1,opt,name=mount_point,json=mountPoint,proto3" json:"mount_point,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *AdminBackend_Disk) Reset() {
+ *x = AdminBackend_Disk{}
+ mi := &file_conf_conf_proto_msgTypes[12]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *AdminBackend_Disk) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*AdminBackend_Disk) ProtoMessage() {}
+
+func (x *AdminBackend_Disk) ProtoReflect() protoreflect.Message {
+ mi := &file_conf_conf_proto_msgTypes[12]
+ 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_Disk.ProtoReflect.Descriptor instead.
+func (*AdminBackend_Disk) Descriptor() ([]byte, []int) {
+ return file_conf_conf_proto_rawDescGZIP(), []int{3, 5}
+}
+
+func (x *AdminBackend_Disk) GetMountPoint() string {
+ if x != nil {
+ return x.MountPoint
+ }
+ return ""
+}
+
+type AdminBackend_System struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ UseRedis bool `protobuf:"varint,1,opt,name=use_redis,json=useRedis,proto3" json:"use_redis,omitempty"`
+ 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"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *AdminBackend_System) Reset() {
+ *x = AdminBackend_System{}
+ mi := &file_conf_conf_proto_msgTypes[13]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *AdminBackend_System) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*AdminBackend_System) ProtoMessage() {}
+
+func (x *AdminBackend_System) ProtoReflect() protoreflect.Message {
+ mi := &file_conf_conf_proto_msgTypes[13]
+ 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_System.ProtoReflect.Descriptor instead.
+func (*AdminBackend_System) Descriptor() ([]byte, []int) {
+ return file_conf_conf_proto_rawDescGZIP(), []int{3, 6}
+}
+
+func (x *AdminBackend_System) GetUseRedis() bool {
+ if x != nil {
+ return x.UseRedis
+ }
+ return false
+}
+
+func (x *AdminBackend_System) GetUseMultipoint() bool {
+ if x != nil {
+ return x.UseMultipoint
+ }
+ return false
+}
+
+func (x *AdminBackend_System) GetUseStrictAuth() bool {
+ if x != nil {
+ return x.UseStrictAuth
+ }
+ return false
+}
+
+func (x *AdminBackend_System) GetDisableAutoMigrate() bool {
+ if x != nil {
+ return x.DisableAutoMigrate
+ }
+ return false
+}
+
type AdminBackend_Storage struct {
state protoimpl.MessageState `protogen:"open.v1"`
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"`
@@ -864,7 +1016,7 @@ type AdminBackend_Storage struct {
func (x *AdminBackend_Storage) Reset() {
*x = AdminBackend_Storage{}
- mi := &file_conf_conf_proto_msgTypes[12]
+ mi := &file_conf_conf_proto_msgTypes[14]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -876,7 +1028,7 @@ func (x *AdminBackend_Storage) String() string {
func (*AdminBackend_Storage) ProtoMessage() {}
func (x *AdminBackend_Storage) 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 {
@@ -889,7 +1041,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, 5}
+ return file_conf_conf_proto_rawDescGZIP(), []int{3, 7}
}
func (x *AdminBackend_Storage) GetType() string {
@@ -963,7 +1115,7 @@ type AdminBackend_Qiniu struct {
func (x *AdminBackend_Qiniu) Reset() {
*x = AdminBackend_Qiniu{}
- mi := &file_conf_conf_proto_msgTypes[13]
+ mi := &file_conf_conf_proto_msgTypes[15]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -975,7 +1127,7 @@ func (x *AdminBackend_Qiniu) String() string {
func (*AdminBackend_Qiniu) ProtoMessage() {}
func (x *AdminBackend_Qiniu) 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 {
@@ -988,7 +1140,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, 6}
+ return file_conf_conf_proto_rawDescGZIP(), []int{3, 8}
}
func (x *AdminBackend_Qiniu) GetZone() string {
@@ -1060,7 +1212,7 @@ type AdminBackend_ObjectStore struct {
func (x *AdminBackend_ObjectStore) Reset() {
*x = AdminBackend_ObjectStore{}
- mi := &file_conf_conf_proto_msgTypes[14]
+ mi := &file_conf_conf_proto_msgTypes[16]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1072,7 +1224,7 @@ func (x *AdminBackend_ObjectStore) String() string {
func (*AdminBackend_ObjectStore) ProtoMessage() {}
func (x *AdminBackend_ObjectStore) ProtoReflect() protoreflect.Message {
- mi := &file_conf_conf_proto_msgTypes[14]
+ mi := &file_conf_conf_proto_msgTypes[16]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1085,7 +1237,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, 7}
+ return file_conf_conf_proto_rawDescGZIP(), []int{3, 9}
}
func (x *AdminBackend_ObjectStore) GetEndpoint() string {
@@ -1173,10 +1325,11 @@ 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\"\xf6\x03\n" +
+ "\atimeout\x18\x03 \x01(\v2\x19.google.protobuf.DurationR\atimeout\"\xef\x04\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\x1a\xd2\x01\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" +
"\bDatabase\x12\x16\n" +
"\x06driver\x18\x01 \x01(\tR\x06driver\x12\x16\n" +
"\x06source\x18\x02 \x01(\tR\x06source\x12\x12\n" +
@@ -1186,12 +1339,16 @@ const file_conf_conf_proto_rawDesc = "" +
"\bpassword\x18\x06 \x01(\tR\bpassword\x12\x12\n" +
"\x04name\x18\a \x01(\tR\x04name\x12\x16\n" +
"\x06config\x18\b \x01(\tR\x06config\x12\x12\n" +
- "\x04path\x18\t \x01(\tR\x04path\x1a\xb3\x01\n" +
+ "\x04path\x18\t \x01(\tR\x04path\x12\x1d\n" +
+ "\n" +
+ "alias_name\x18\n" +
+ " \x01(\tR\taliasName\x12\x18\n" +
+ "\adisable\x18\v \x01(\bR\adisable\x1a\xb3\x01\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\"\xb1\x10\n" +
+ "\rwrite_timeout\x18\x04 \x01(\v2\x19.google.protobuf.DurationR\fwriteTimeout\"\xf8\x12\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" +
@@ -1201,7 +1358,10 @@ const file_conf_conf_proto_rawDesc = "" +
"\astorage\x18\x06 \x01(\v2 .kratos.api.AdminBackend.StorageR\astorage\x12\x1f\n" +
"\vconfig_path\x18\a \x01(\tR\n" +
"configPath\x124\n" +
- "\x05media\x18\b \x01(\v2\x1e.kratos.api.AdminBackend.MediaR\x05media\x1a\xb8\x01\n" +
+ "\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" +
"\x03JWT\x12\x1f\n" +
"\vsigning_key\x18\x01 \x01(\tR\n" +
"signingKey\x12<\n" +
@@ -1231,7 +1391,15 @@ const file_conf_conf_proto_rawDesc = "" +
"\ris_login_auth\x18\b \x01(\bR\visLoginAuth\x1a(\n" +
"\x05Media\x12\x1f\n" +
"\vsession_ttl\x18\x01 \x01(\x05R\n" +
- "sessionTtl\x1a\xe8\x03\n" +
+ "sessionTtl\x1a'\n" +
+ "\x04Disk\x12\x1f\n" +
+ "\vmount_point\x18\x01 \x01(\tR\n" +
+ "mountPoint\x1a\xa6\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" +
"\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" +
@@ -1283,7 +1451,7 @@ func file_conf_conf_proto_rawDescGZIP() []byte {
return file_conf_conf_proto_rawDescData
}
-var file_conf_conf_proto_msgTypes = make([]protoimpl.MessageInfo, 15)
+var file_conf_conf_proto_msgTypes = make([]protoimpl.MessageInfo, 17)
var file_conf_conf_proto_goTypes = []any{
(*Bootstrap)(nil), // 0: kratos.api.Bootstrap
(*Server)(nil), // 1: kratos.api.Server
@@ -1297,10 +1465,12 @@ var file_conf_conf_proto_goTypes = []any{
(*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_Storage)(nil), // 12: kratos.api.AdminBackend.Storage
- (*AdminBackend_Qiniu)(nil), // 13: kratos.api.AdminBackend.Qiniu
- (*AdminBackend_ObjectStore)(nil), // 14: kratos.api.AdminBackend.ObjectStore
- (*durationpb.Duration)(nil), // 15: google.protobuf.Duration
+ (*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
}
var file_conf_conf_proto_depIdxs = []int32{
1, // 0: kratos.api.Bootstrap.server:type_name -> kratos.api.Server
@@ -1309,30 +1479,33 @@ var file_conf_conf_proto_depIdxs = []int32{
4, // 3: kratos.api.Server.http:type_name -> kratos.api.Server.HTTP
5, // 4: kratos.api.Data.database:type_name -> kratos.api.Data.Database
6, // 5: kratos.api.Data.redis:type_name -> kratos.api.Data.Redis
- 7, // 6: kratos.api.AdminBackend.jwt:type_name -> kratos.api.AdminBackend.JWT
- 8, // 7: kratos.api.AdminBackend.captcha:type_name -> kratos.api.AdminBackend.Captcha
- 9, // 8: kratos.api.AdminBackend.local:type_name -> kratos.api.AdminBackend.Local
- 10, // 9: kratos.api.AdminBackend.email:type_name -> kratos.api.AdminBackend.Email
- 12, // 10: kratos.api.AdminBackend.storage:type_name -> kratos.api.AdminBackend.Storage
- 11, // 11: kratos.api.AdminBackend.media:type_name -> kratos.api.AdminBackend.Media
- 15, // 12: kratos.api.Server.HTTP.timeout:type_name -> google.protobuf.Duration
- 15, // 13: kratos.api.Data.Redis.read_timeout:type_name -> google.protobuf.Duration
- 15, // 14: kratos.api.Data.Redis.write_timeout:type_name -> google.protobuf.Duration
- 15, // 15: kratos.api.AdminBackend.JWT.expires_time:type_name -> google.protobuf.Duration
- 15, // 16: kratos.api.AdminBackend.JWT.buffer_time:type_name -> google.protobuf.Duration
- 15, // 17: kratos.api.AdminBackend.Captcha.store_expiration:type_name -> google.protobuf.Duration
- 13, // 18: kratos.api.AdminBackend.Storage.qiniu:type_name -> kratos.api.AdminBackend.Qiniu
- 14, // 19: kratos.api.AdminBackend.Storage.aliyun_oss:type_name -> kratos.api.AdminBackend.ObjectStore
- 14, // 20: kratos.api.AdminBackend.Storage.huawei_obs:type_name -> kratos.api.AdminBackend.ObjectStore
- 14, // 21: kratos.api.AdminBackend.Storage.tencent_cos:type_name -> kratos.api.AdminBackend.ObjectStore
- 14, // 22: kratos.api.AdminBackend.Storage.aws_s3:type_name -> kratos.api.AdminBackend.ObjectStore
- 14, // 23: kratos.api.AdminBackend.Storage.cloudflare_r2:type_name -> kratos.api.AdminBackend.ObjectStore
- 14, // 24: kratos.api.AdminBackend.Storage.minio:type_name -> kratos.api.AdminBackend.ObjectStore
- 25, // [25:25] is the sub-list for method output_type
- 25, // [25:25] is the sub-list for method input_type
- 25, // [25:25] is the sub-list for extension type_name
- 25, // [25:25] is the sub-list for extension extendee
- 0, // [0:25] is the sub-list for field type_name
+ 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
}
func init() { file_conf_conf_proto_init() }
@@ -1346,7 +1519,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: 15,
+ NumMessages: 17,
NumExtensions: 0,
NumServices: 0,
},
diff --git a/internal/conf/conf.proto b/internal/conf/conf.proto
index 89e5419..c3e9d77 100644
--- a/internal/conf/conf.proto
+++ b/internal/conf/conf.proto
@@ -31,6 +31,8 @@ message Data {
string name = 7;
string config = 8;
string path = 9;
+ string alias_name = 10;
+ bool disable = 11;
}
message Redis {
string network = 1;
@@ -40,6 +42,7 @@ message Data {
}
Database database = 1;
Redis redis = 2;
+ repeated Database database_list = 3;
}
// AdminBackend contains settings for the administration HTTP transport.
@@ -53,6 +56,8 @@ message AdminBackend {
// ConfigPath is populated by the entrypoint and is not required in YAML.
string config_path = 7;
Media media = 8;
+ repeated Disk disk_list = 9;
+ System system = 10;
message JWT {
string signing_key = 1;
@@ -88,6 +93,17 @@ message AdminBackend {
int32 session_ttl = 1;
}
+ message Disk {
+ string mount_point = 1;
+ }
+
+ message System {
+ bool use_redis = 1;
+ bool use_multipoint = 2;
+ bool use_strict_auth = 3;
+ bool disable_auto_migrate = 4;
+ }
+
message Storage {
string type = 1;
Qiniu qiniu = 2;
diff --git a/internal/conf/runtime.go b/internal/conf/runtime.go
new file mode 100644
index 0000000..5c26a76
--- /dev/null
+++ b/internal/conf/runtime.go
@@ -0,0 +1,94 @@
+package conf
+
+import (
+ "sync/atomic"
+
+ "google.golang.org/protobuf/proto"
+)
+
+// Runtime owns the active immutable configuration snapshot. Callers always
+// receive clones so a config file reload cannot race with request handling.
+type Runtime struct {
+ snapshot atomic.Pointer[runtimeSnapshot]
+}
+
+type runtimeSnapshot struct {
+ data *Data
+ admin *AdminBackend
+}
+
+func NewRuntime(data *Data, admin *AdminBackend) *Runtime {
+ runtime := &Runtime{}
+ runtime.Replace(data, admin)
+ return runtime
+}
+
+func cloneData(value *Data) *Data {
+ if value == nil {
+ return nil
+ }
+ return proto.Clone(value).(*Data)
+}
+
+func cloneAdmin(value *AdminBackend) *AdminBackend {
+ if value == nil {
+ return nil
+ }
+ return proto.Clone(value).(*AdminBackend)
+}
+
+func (r *Runtime) Replace(data *Data, admin *AdminBackend) {
+ r.snapshot.Store(&runtimeSnapshot{data: cloneData(data), admin: cloneAdmin(admin)})
+}
+
+func (r *Runtime) Data() *Data {
+ snapshot := r.snapshot.Load()
+ if snapshot == nil {
+ return nil
+ }
+ return cloneData(snapshot.data)
+}
+
+func (r *Runtime) Admin() *AdminBackend {
+ snapshot := r.snapshot.Load()
+ if snapshot == nil {
+ return nil
+ }
+ return cloneAdmin(snapshot.admin)
+}
+
+func (r *Runtime) Values() (*Data, *AdminBackend) {
+ snapshot := r.snapshot.Load()
+ if snapshot == nil {
+ return nil, nil
+ }
+ return cloneData(snapshot.data), cloneAdmin(snapshot.admin)
+}
+
+func (r *Runtime) ConfigPath() string {
+ snapshot := r.snapshot.Load()
+ if snapshot == nil || snapshot.admin == nil {
+ return ""
+ }
+ return snapshot.admin.ConfigPath
+}
+
+func (r *Runtime) UpdateDatabase(database *Data_Database) {
+ for {
+ current := r.snapshot.Load()
+ var data *Data
+ var admin *AdminBackend
+ if current != nil {
+ data = cloneData(current.data)
+ admin = cloneAdmin(current.admin)
+ }
+ if data == nil {
+ data = &Data{}
+ }
+ data.Database = proto.Clone(database).(*Data_Database)
+ next := &runtimeSnapshot{data: data, admin: admin}
+ if r.snapshot.CompareAndSwap(current, next) {
+ return
+ }
+ }
+}
diff --git a/internal/conf/runtime_test.go b/internal/conf/runtime_test.go
new file mode 100644
index 0000000..b886961
--- /dev/null
+++ b/internal/conf/runtime_test.go
@@ -0,0 +1,41 @@
+package conf
+
+import (
+ "sync"
+ "testing"
+)
+
+func TestRuntimeSnapshotsAreImmutable(t *testing.T) {
+ runtime := NewRuntime(&Data{Database: &Data_Database{Name: "first"}}, &AdminBackend{RouterPrefix: "/api", Jwt: &AdminBackend_JWT{SigningKey: "secret"}})
+ admin := runtime.Admin()
+ admin.RouterPrefix = "/changed"
+ admin.Jwt.SigningKey = "changed"
+ data := runtime.Data()
+ data.Database.Name = "changed"
+
+ if got := runtime.Admin(); got.RouterPrefix != "/api" || got.Jwt.SigningKey != "secret" {
+ t.Fatalf("admin snapshot mutated: %+v", got)
+ }
+ if got := runtime.Data().Database.Name; got != "first" {
+ t.Fatalf("data snapshot mutated: %s", got)
+ }
+}
+
+func TestRuntimeConcurrentReplaceAndRead(t *testing.T) {
+ runtime := NewRuntime(&Data{Database: &Data_Database{Name: "initial"}}, &AdminBackend{Jwt: &AdminBackend_JWT{SigningKey: "initial"}})
+ var wait sync.WaitGroup
+ for worker := 0; worker < 8; worker++ {
+ wait.Add(1)
+ go func() {
+ defer wait.Done()
+ for index := 0; index < 500; index++ {
+ _ = runtime.Admin().Jwt.SigningKey
+ _ = runtime.Data().Database.Name
+ }
+ }()
+ }
+ for index := 0; index < 500; index++ {
+ runtime.Replace(&Data{Database: &Data_Database{Name: "next"}}, &AdminBackend{Jwt: &AdminBackend_JWT{SigningKey: "next"}})
+ }
+ wait.Wait()
+}
diff --git a/internal/data/announcement.go b/internal/data/announcement.go
index 2b54944..452d5b4 100644
--- a/internal/data/announcement.go
+++ b/internal/data/announcement.go
@@ -93,11 +93,7 @@ func (r *announcementRepo) List(ctx context.Context, filter biz.AnnouncementFilt
return nil, 0, err
}
if filter.PageSize > 0 {
- page := filter.Page
- if page < 1 {
- page = 1
- }
- db = db.Offset((page - 1) * filter.PageSize).Limit(filter.PageSize)
+ db = applyPagination(db, filter.Page, filter.PageSize, 100)
}
var pos []announcementPO
if err := db.Find(&pos).Error; err != nil {
diff --git a/internal/data/api.go b/internal/data/api.go
index 1e7ee1b..f3c9f27 100644
--- a/internal/data/api.go
+++ b/internal/data/api.go
@@ -45,7 +45,12 @@ func apiFromPO(po apiPO) *biz.API {
return &biz.API{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Path: po.Path, Description: po.Description, APIGroup: po.APIGroup, Method: po.Method}
}
func (r *accessRepo) CreateAPI(ctx context.Context, v *biz.API) error {
- return r.data.gormDB.WithContext(ctx).Create(&apiPO{Path: v.Path, Description: v.Description, APIGroup: v.APIGroup, Method: strings.ToUpper(v.Method)}).Error
+ po := apiPO{Path: v.Path, Description: v.Description, APIGroup: v.APIGroup, Method: strings.ToUpper(v.Method)}
+ if err := r.data.gormDB.WithContext(ctx).Create(&po).Error; err != nil {
+ return err
+ }
+ v.ID, v.CreatedAt, v.UpdatedAt, v.Method = po.ID, po.CreatedAt, po.UpdatedAt, po.Method
+ return nil
}
func (r *accessRepo) UpdateAPI(ctx context.Context, v *biz.API) error {
return r.data.gormDB.WithContext(ctx).Model(&apiPO{}).Where("id = ?", v.ID).Updates(map[string]any{"path": v.Path, "description": v.Description, "api_group": v.APIGroup, "method": strings.ToUpper(v.Method)}).Error
@@ -66,17 +71,26 @@ func (r *accessRepo) FindAPI(ctx context.Context, id uint) (*biz.API, error) {
return apiFromPO(po), nil
}
func (r *accessRepo) ListAPIs(ctx context.Context, page, size int, q *biz.API) ([]*biz.API, int64, error) {
- if page < 1 {
- page = 1
- }
- if size < 1 {
- size = 10
- }
db := r.data.gormDB.WithContext(ctx).Model(&apiPO{})
+ if q != nil && q.StrictAll {
+ config := r.data.runtime.Admin()
+ if actor, ok := biz.ActorFromContext(ctx); ok && config != nil && config.System != nil && config.System.UseStrictAuth {
+ var authority authorityPO
+ if err := r.data.gormDB.WithContext(ctx).Where("authority_id = ?", actor.AuthorityID).First(&authority).Error; err != nil {
+ return nil, 0, err
+ }
+ if authority.ParentID != nil && *authority.ParentID != 0 {
+ db = db.Where("id IN (?)", r.data.gormDB.WithContext(ctx).Model(&authorityAPIPO{}).Select("api_id").Where("authority_id = ?", actor.AuthorityID))
+ }
+ }
+ }
if q != nil {
if q.Path != "" {
db = db.Where("path LIKE ?", "%"+q.Path+"%")
}
+ if q.Description != "" {
+ db = db.Where("description LIKE ?", "%"+q.Description+"%")
+ }
if q.APIGroup != "" {
db = db.Where("api_group = ?", q.APIGroup)
}
@@ -88,8 +102,23 @@ func (r *accessRepo) ListAPIs(ctx context.Context, page, size int, q *biz.API) (
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
+ order := "id desc"
+ if q != nil && q.OrderKey != "" {
+ allowed := map[string]bool{"id": true, "path": true, "api_group": true, "description": true, "method": true}
+ if !allowed[q.OrderKey] {
+ return nil, 0, errors.New("非法的排序字段: " + q.OrderKey)
+ }
+ order = q.OrderKey
+ if q.Desc {
+ order += " desc"
+ }
+ }
var pos []apiPO
- if err := db.Order("api_group,path,method").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
+ maxSize := 100
+ if q != nil && q.StrictAll {
+ maxSize = 0
+ }
+ if err := applyPagination(db.Order(order), page, size, maxSize).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.API, 0, len(pos))
@@ -98,19 +127,26 @@ func (r *accessRepo) ListAPIs(ctx context.Context, page, size int, q *biz.API) (
}
return out, total, nil
}
-func (r *accessRepo) APIRoleIDs(ctx context.Context, id uint) ([]uint, error) {
+func (r *accessRepo) APIRoleIDs(ctx context.Context, path, method string) ([]uint, error) {
var ids []uint
- err := r.data.gormDB.WithContext(ctx).Model(&authorityAPIPO{}).Where("api_id = ?", id).Pluck("authority_id", &ids).Error
+ err := r.data.gormDB.WithContext(ctx).Table("sys_authority_apis").
+ Joins("JOIN sys_apis ON sys_apis.id = sys_authority_apis.api_id").
+ Where("sys_apis.path = ? AND sys_apis.method = ? AND sys_apis.deleted_at IS NULL", path, strings.ToUpper(method)).
+ Pluck("sys_authority_apis.authority_id", &ids).Error
return ids, err
}
-func (r *accessRepo) SetAPIRoles(ctx context.Context, id uint, ids []uint) error {
+func (r *accessRepo) SetAPIRoles(ctx context.Context, path, method string, ids []uint) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
- if err := tx.Where("api_id = ?", id).Delete(&authorityAPIPO{}).Error; err != nil {
+ var api apiPO
+ if err := tx.Where("path = ? AND method = ?", path, strings.ToUpper(method)).First(&api).Error; err != nil {
+ return err
+ }
+ if err := tx.Where("api_id = ?", api.ID).Delete(&authorityAPIPO{}).Error; err != nil {
return err
}
links := make([]authorityAPIPO, 0, len(ids))
for _, aid := range ids {
- links = append(links, authorityAPIPO{AuthorityID: aid, APIID: id})
+ links = append(links, authorityAPIPO{AuthorityID: aid, APIID: api.ID})
}
if len(links) > 0 {
return tx.Create(&links).Error
@@ -159,6 +195,31 @@ func (r *accessRepo) PolicyPaths(ctx context.Context, aid uint) ([]*biz.API, err
return out, nil
}
func (r *accessRepo) SetPolicyPaths(ctx context.Context, aid uint, paths []*biz.API) error {
+ if err := r.checkAuthorityIDAuth(ctx, aid); err != nil {
+ return err
+ }
+ config := r.data.runtime.Admin()
+ if actor, ok := biz.ActorFromContext(ctx); ok && config != nil && config.System != nil && config.System.UseStrictAuth {
+ var authority authorityPO
+ if err := r.data.gormDB.WithContext(ctx).Where("authority_id = ?", actor.AuthorityID).First(&authority).Error; err != nil {
+ return err
+ }
+ if authority.ParentID != nil && *authority.ParentID != 0 {
+ allowed, err := r.PolicyPaths(ctx, actor.AuthorityID)
+ if err != nil {
+ return err
+ }
+ allowedSet := make(map[string]bool, len(allowed))
+ for _, item := range allowed {
+ allowedSet[item.Path+"\x00"+strings.ToUpper(item.Method)] = true
+ }
+ for _, item := range paths {
+ if !allowedSet[item.Path+"\x00"+strings.ToUpper(item.Method)] {
+ return errors.New("存在api不在权限列表中")
+ }
+ }
+ }
+ }
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("authority_id = ?", aid).Delete(&authorityAPIPO{}).Error; err != nil {
return err
diff --git a/internal/data/api_token.go b/internal/data/api_token.go
index fe08dab..0aa78cd 100644
--- a/internal/data/api_token.go
+++ b/internal/data/api_token.go
@@ -58,12 +58,6 @@ func (r *settingsRepo) CreateAPIToken(ctx context.Context, v *biz.APIToken) erro
return nil
}
func (r *settingsRepo) ListAPITokens(ctx context.Context, page, size int, userID uint, status *bool) ([]*biz.APIToken, int64, error) {
- if page < 1 {
- page = 1
- }
- if size < 1 {
- size = 10
- }
db := r.data.gormDB.WithContext(ctx).Model(&apiTokenPO{})
if userID != 0 {
db = db.Where("user_id = ?", userID)
@@ -76,16 +70,26 @@ func (r *settingsRepo) ListAPITokens(ctx context.Context, page, size int, userID
return nil, 0, err
}
var pos []apiTokenPO
- if err := db.Order("created_at desc").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
+ if err := applyPagination(db.Order("created_at desc"), page, size, 100).Find(&pos).Error; err != nil {
return nil, 0, err
}
+ userIDs := make([]uint, 0, len(pos))
+ for _, po := range pos {
+ userIDs = append(userIDs, po.UserID)
+ }
+ var userPOs []userPO
+ if len(userIDs) > 0 {
+ if err := r.data.gormDB.WithContext(ctx).Where("id IN ?", userIDs).Find(&userPOs).Error; err != nil {
+ return nil, 0, err
+ }
+ }
+ users := make(map[uint]*biz.User, len(userPOs))
+ for i := range userPOs {
+ users[userPOs[i].ID] = baseBizUser(&userPOs[i])
+ }
out := make([]*biz.APIToken, 0, len(pos))
for _, po := range pos {
- v := &biz.APIToken{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, UserID: po.UserID, AuthorityID: po.AuthorityID, Token: po.Token, Status: po.Status, ExpiresAt: po.ExpiresAt, Remark: po.Remark}
- var userPO userPO
- if r.data.gormDB.WithContext(ctx).First(&userPO, po.UserID).Error == nil {
- v.User, _ = (&systemRepo{data: r.data}).toBizUser(ctx, &userPO)
- }
+ v := &biz.APIToken{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, UserID: po.UserID, AuthorityID: po.AuthorityID, Token: po.Token, Status: po.Status, ExpiresAt: po.ExpiresAt, Remark: po.Remark, User: users[po.UserID]}
out = append(out, v)
}
return out, total, nil
@@ -97,6 +101,19 @@ func (r *settingsRepo) DisableAPIToken(ctx context.Context, id uint) (string, er
}
return po.Token, r.data.gormDB.WithContext(ctx).Model(&po).Update("status", false).Error
}
+func (r *settingsRepo) DisableAndBlacklistAPIToken(ctx context.Context, id uint) error {
+ return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ var po apiTokenPO
+ if err := tx.First(&po, id).Error; err != nil {
+ return err
+ }
+ // Blacklist the token before disabling it while making both writes atomic.
+ if err := tx.Create(&jwtBlacklistPO{JWT: po.Token}).Error; err != nil {
+ return err
+ }
+ return tx.Model(&po).Update("status", false).Error
+ })
+}
func (r *settingsRepo) BlacklistToken(ctx context.Context, token string) error {
if token == "" {
return errors.New("token is empty")
diff --git a/internal/data/authority.go b/internal/data/authority.go
index c7445f1..737393e 100644
--- a/internal/data/authority.go
+++ b/internal/data/authority.go
@@ -11,7 +11,67 @@ import (
"gorm.io/gorm"
)
+func (r *accessRepo) strictAuthorityIDs(ctx context.Context, actorID uint) (map[uint]bool, error) {
+ allowed := make(map[uint]bool)
+ if actorID == 0 {
+ return allowed, nil
+ }
+ var actor authorityPO
+ if err := r.data.gormDB.WithContext(ctx).Where("authority_id = ?", actorID).First(&actor).Error; err != nil {
+ return nil, err
+ }
+ var authorities []authorityPO
+ if err := r.data.gormDB.WithContext(ctx).Find(&authorities).Error; err != nil {
+ return nil, err
+ }
+ children := make(map[uint][]uint)
+ for _, authority := range authorities {
+ if authority.ParentID != nil {
+ children[*authority.ParentID] = append(children[*authority.ParentID], authority.AuthorityID)
+ }
+ }
+ var walk func(uint)
+ walk = func(id uint) {
+ for _, child := range children[id] {
+ if !allowed[child] {
+ allowed[child] = true
+ walk(child)
+ }
+ }
+ }
+ if actor.ParentID == nil || *actor.ParentID == 0 {
+ allowed[actorID] = true
+ }
+ walk(actorID)
+ return allowed, nil
+}
+
+func (r *accessRepo) checkAuthorityIDAuth(ctx context.Context, targetID uint) error {
+ config := r.data.runtime.Admin()
+ if config == nil || config.System == nil || !config.System.UseStrictAuth {
+ return nil
+ }
+ actor, ok := biz.ActorFromContext(ctx)
+ if !ok {
+ return errors.New("您提交的角色ID不合法")
+ }
+ allowed, err := r.strictAuthorityIDs(ctx, actor.AuthorityID)
+ if err != nil {
+ return err
+ }
+ if !allowed[targetID] {
+ return errors.New("您提交的角色ID不合法")
+ }
+ return nil
+}
+
func (r *accessRepo) CreateAuthority(ctx context.Context, value *biz.Authority) error {
+ config := r.data.runtime.Admin()
+ if config != nil && config.System != nil && config.System.UseStrictAuth && (value.ParentID == nil || *value.ParentID == 0) {
+ if actor, ok := biz.ActorFromContext(ctx); ok {
+ value.ParentID = &actor.AuthorityID
+ }
+ }
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var count int64
if err := tx.Model(&authorityPO{}).Where("authority_id = ?", value.AuthorityID).Count(&count).Error; err != nil {
@@ -46,6 +106,18 @@ func (r *accessRepo) CreateAuthority(ctx context.Context, value *biz.Authority)
}
func (r *accessRepo) CopyAuthority(ctx context.Context, sourceID uint, value *biz.Authority) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ config := r.data.runtime.Admin()
+ actor, hasActor := biz.ActorFromContext(ctx)
+ strict := hasActor && actor.AuthorityID != 888 && config != nil && config.System != nil && config.System.UseStrictAuth
+ if strict {
+ allowed, err := r.strictAuthorityIDs(ctx, actor.AuthorityID)
+ if err != nil {
+ return err
+ }
+ if value.ParentID == nil || !allowed[*value.ParentID] {
+ return errors.New("严格角色模式下只能在自己的角色树中复制角色")
+ }
+ }
var count int64
if err := tx.Model(&authorityPO{}).Where("authority_id = ?", value.AuthorityID).Count(&count).Error; err != nil {
return err
@@ -76,6 +148,15 @@ func (r *accessRepo) CopyAuthority(ctx context.Context, sourceID uint, value *bi
return err
}
for i := range apis {
+ if strict {
+ var allowedCount int64
+ if err := tx.Model(&authorityAPIPO{}).Where("authority_id = ? AND api_id = ?", actor.AuthorityID, apis[i].APIID).Count(&allowedCount).Error; err != nil {
+ return err
+ }
+ if allowedCount == 0 {
+ return errors.New("严格角色模式下不能复制当前角色未拥有的 API")
+ }
+ }
apis[i].AuthorityID = value.AuthorityID
}
if len(apis) > 0 {
@@ -95,16 +176,6 @@ func (r *accessRepo) CopyAuthority(ctx context.Context, sourceID uint, value *bi
return err
}
}
- var departments []authorityDepartmentPO
- if err := copyLinks("sys_authority_departments", &departments, map[string]any{"authority_id": sourceID}); err != nil {
- return err
- }
- for i := range departments {
- departments[i].AuthorityID = value.AuthorityID
- }
- if len(departments) > 0 {
- return tx.Create(&departments).Error
- }
return nil
})
}
@@ -155,8 +226,20 @@ func (r *accessRepo) ListAuthorities(ctx context.Context) ([]*biz.Authority, err
if err := r.data.gormDB.WithContext(ctx).Order("authority_id").Find(&pos).Error; err != nil {
return nil, err
}
+ var allowed map[uint]bool
+ config := r.data.runtime.Admin()
+ if actor, ok := biz.ActorFromContext(ctx); ok && config != nil && config.System != nil && config.System.UseStrictAuth {
+ var err error
+ allowed, err = r.strictAuthorityIDs(ctx, actor.AuthorityID)
+ if err != nil {
+ return nil, err
+ }
+ }
out := make([]*biz.Authority, 0, len(pos))
for _, po := range pos {
+ if allowed != nil && !allowed[po.AuthorityID] {
+ continue
+ }
v := toBizAuthority(po)
out = append(out, &v)
}
@@ -190,11 +273,13 @@ func (r *accessRepo) SetAuthorityUsers(ctx context.Context, id uint, ids []uint)
}
var user userPO
if err := tx.First(&user, uid).Error; err == nil && user.AuthorityID == id {
- if err = tx.Model(&user).Update("authority_id", 888).Error; err != nil {
+ var another userAuthorityPO
+ if err = tx.Where("sys_user_id = ?", uid).First(&another).Error; errors.Is(err, gorm.ErrRecordNotFound) {
+ continue
+ } else if err != nil {
return err
}
- fallback := userAuthorityPO{SysUserID: uid, SysAuthorityAuthorityID: 888}
- if err = tx.FirstOrCreate(&fallback, fallback).Error; err != nil {
+ if err = tx.Model(&user).Update("authority_id", another.SysAuthorityAuthorityID).Error; err != nil {
return err
}
}
@@ -215,12 +300,14 @@ func (r *accessRepo) SetDataScope(ctx context.Context, id uint, scope int, deptI
if err := tx.Where("authority_id = ?", id).Delete(&authorityDepartmentPO{}).Error; err != nil {
return err
}
- links := make([]authorityDepartmentPO, 0, len(deptIDs))
- for _, did := range deptIDs {
- links = append(links, authorityDepartmentPO{AuthorityID: id, DepartmentID: did})
- }
- if len(links) > 0 {
- return tx.Create(&links).Error
+ if scope == 5 {
+ links := make([]authorityDepartmentPO, 0, len(deptIDs))
+ for _, did := range deptIDs {
+ links = append(links, authorityDepartmentPO{AuthorityID: id, DepartmentID: did})
+ }
+ if len(links) > 0 {
+ return tx.Create(&links).Error
+ }
}
return nil
})
diff --git a/internal/data/cache.go b/internal/data/cache.go
index 2f69c8c..dd7c9bf 100644
--- a/internal/data/cache.go
+++ b/internal/data/cache.go
@@ -22,14 +22,14 @@ type cacheStore struct {
memory map[string]memoryCacheEntry
}
+const maxMemoryCacheEntries = 10000
+
func NewCache(data *Data) biz.Cache {
return &cacheStore{data: data, memory: make(map[string]memoryCacheEntry)}
}
func (s *cacheStore) client() *redis.Client {
- s.data.mu.RLock()
- defer s.data.mu.RUnlock()
- return s.data.redis
+ return s.data.redis.load()
}
func (s *cacheStore) Get(ctx context.Context, key string) (string, bool, error) {
@@ -62,6 +62,7 @@ func (s *cacheStore) Set(ctx context.Context, key, value string, expiration time
}
s.mu.Lock()
defer s.mu.Unlock()
+ s.makeMemoryRoom(key)
entry := memoryCacheEntry{value: value}
if expiration > 0 {
entry.expiresAt = time.Now().Add(expiration)
@@ -95,6 +96,7 @@ func (s *cacheStore) Increment(ctx context.Context, key string, expiration time.
}
s.mu.Lock()
defer s.mu.Unlock()
+ s.makeMemoryRoom(key)
entry, ok := s.memory[key]
if ok && !entry.expiresAt.IsZero() && time.Now().After(entry.expiresAt) {
ok = false
@@ -111,3 +113,27 @@ func (s *cacheStore) Increment(ctx context.Context, key string, expiration time.
s.memory[key] = entry
return value, nil
}
+
+func (s *cacheStore) makeMemoryRoom(incoming string) {
+ if len(s.memory) < maxMemoryCacheEntries {
+ return
+ }
+ now := time.Now()
+ for key, entry := range s.memory {
+ if !entry.expiresAt.IsZero() && now.After(entry.expiresAt) {
+ delete(s.memory, key)
+ }
+ }
+ if len(s.memory) < maxMemoryCacheEntries {
+ return
+ }
+ if _, exists := s.memory[incoming]; exists {
+ return
+ }
+ // The in-memory store is only a Redis fallback. A bounded arbitrary eviction
+ // is preferable to unbounded growth under IP churn.
+ for key := range s.memory {
+ delete(s.memory, key)
+ break
+ }
+}
diff --git a/internal/data/config_store.go b/internal/data/config_store.go
index 5d6b041..a868ddd 100644
--- a/internal/data/config_store.go
+++ b/internal/data/config_store.go
@@ -53,14 +53,22 @@ func setYAMLMapping(node *yaml.Node, key string, value any) error {
}
func (d *Data) persistConfig() error {
- return d.persistConfigValues(d.config, d.admin)
+ dataConfig, adminConfig := d.runtime.Values()
+ return d.persistConfigValues(dataConfig, adminConfig)
}
func (d *Data) persistConfigValues(dataConfig *conf.Data, adminConfig *conf.AdminBackend) error {
- if d.admin == nil || d.admin.ConfigPath == "" {
+ d.configMu.Lock()
+ defer d.configMu.Unlock()
+ return d.persistConfigValuesLocked(dataConfig, adminConfig)
+}
+
+func (d *Data) persistConfigValuesLocked(dataConfig *conf.Data, adminConfig *conf.AdminBackend) error {
+ if adminConfig == nil || adminConfig.ConfigPath == "" {
return nil
}
- raw, err := os.ReadFile(d.admin.ConfigPath)
+ configPath := adminConfig.ConfigPath
+ raw, err := os.ReadFile(configPath)
if err != nil {
return err
}
@@ -86,10 +94,10 @@ func (d *Data) persistConfigValues(dataConfig *conf.Data, adminConfig *conf.Admi
if err != nil {
return err
}
- if err = os.MkdirAll(filepath.Dir(d.admin.ConfigPath), 0o755); err != nil {
+ if err = os.MkdirAll(filepath.Dir(configPath), 0o755); err != nil {
return err
}
- temporary, err := os.CreateTemp(filepath.Dir(d.admin.ConfigPath), ".kra-config-*.yaml")
+ temporary, err := os.CreateTemp(filepath.Dir(configPath), ".kra-config-*.yaml")
if err != nil {
return err
}
@@ -106,33 +114,24 @@ func (d *Data) persistConfigValues(dataConfig *conf.Data, adminConfig *conf.Admi
if err = temporary.Close(); err != nil {
return err
}
- return os.Rename(tempName, d.admin.ConfigPath)
+ return os.Rename(tempName, configPath)
}
func (d *Data) reloadConfig(ctx context.Context) error {
- if d.admin == nil || d.admin.ConfigPath == "" {
+ d.configMu.Lock()
+ defer d.configMu.Unlock()
+ configPath := d.runtime.ConfigPath()
+ if configPath == "" {
return fmt.Errorf("configuration path is not set")
}
- raw, err := os.ReadFile(d.admin.ConfigPath)
+ next, err := readBootstrap(configPath)
if err != nil {
return err
}
- var plain map[string]any
- if err = yaml.Unmarshal(raw, &plain); err != nil {
- return err
- }
- encoded, err := json.Marshal(plain)
- if err != nil {
- return err
- }
- var next conf.Bootstrap
- if err = (protojson.UnmarshalOptions{DiscardUnknown: true}).Unmarshal(encoded, &next); err != nil {
- return err
- }
if next.Data == nil || next.Data.Database == nil || next.Admin == nil {
return fmt.Errorf("data.database and admin configuration are required")
}
- next.Admin.ConfigPath = d.admin.ConfigPath
+ next.Admin.ConfigPath = configPath
candidateDB, err := openDatabase(next.Data.Database, false, "")
if err != nil {
return fmt.Errorf("reload database: %w", err)
@@ -150,35 +149,49 @@ func (d *Data) reloadConfig(ctx context.Context) error {
} else if err = sqlDB.PingContext(ctx); err != nil {
return fmt.Errorf("reload database: %w", err)
}
- if err = migrateAll(candidateDB.WithContext(ctx)); err != nil {
- return fmt.Errorf("reload database migrations: %w", err)
+ if next.Admin.System == nil || !next.Admin.System.DisableAutoMigrate {
+ if err = migrateAll(candidateDB.WithContext(ctx)); err != nil {
+ return fmt.Errorf("reload database migrations: %w", err)
+ }
}
candidateStorage, err := buildFileStorage(next.Admin)
if err != nil {
return fmt.Errorf("reload storage: %w", err)
}
- candidateRedis := openRedis(next.Data.Redis)
+ useRedis := next.Admin.System != nil && next.Admin.System.UseRedis
+ candidateRedis := openRedis(next.Data.Redis, useRedis)
+ candidateDBList, err := openDatabaseList(next.Data.DatabaseList)
+ if err != nil {
+ return err
+ }
- d.mu.Lock()
- oldDB, oldRedis := d.gormDB, d.redis
- d.gormDB, d.redis = candidateDB, candidateRedis
- proto.Reset(d.config)
- proto.Merge(d.config, next.Data)
- proto.Reset(d.admin)
- proto.Merge(d.admin, next.Admin)
- d.database = d.config.Database
- d.mu.Unlock()
+ d.gormDB.replace(candidateDB)
+ d.replaceDatabaseList(candidateDBList)
+ d.redis.replace(candidateRedis)
+ d.runtime.Replace(next.Data, next.Admin)
if d.storage != nil {
d.storage.replace(candidateStorage)
}
closeCandidate = false
- if oldDB != nil {
- if sqlDB, closeErr := oldDB.DB(); closeErr == nil {
- _ = sqlDB.Close()
- }
- }
- if oldRedis != nil {
- _ = oldRedis.Close()
- }
return nil
}
+
+func readBootstrap(configPath string) (*conf.Bootstrap, error) {
+ raw, err := os.ReadFile(configPath)
+ if err != nil {
+ return nil, err
+ }
+ var plain map[string]any
+ if err = yaml.Unmarshal(raw, &plain); err != nil {
+ return nil, err
+ }
+ encoded, err := json.Marshal(plain)
+ if err != nil {
+ return nil, err
+ }
+ var next conf.Bootstrap
+ if err = (protojson.UnmarshalOptions{DiscardUnknown: true}).Unmarshal(encoded, &next); err != nil {
+ return nil, err
+ }
+ return &next, nil
+}
diff --git a/internal/data/config_watch.go b/internal/data/config_watch.go
new file mode 100644
index 0000000..0230401
--- /dev/null
+++ b/internal/data/config_watch.go
@@ -0,0 +1,80 @@
+package data
+
+import (
+ "log"
+ "path/filepath"
+ "sync"
+ "time"
+
+ "github.com/fsnotify/fsnotify"
+)
+
+// watchConfig refreshes the in-memory config snapshot on file changes, while
+// /system/reloadSystem remains responsible
+// for rebuilding database, Redis, storage, and scheduled tasks.
+func (d *Data) watchConfig() func() {
+ configPath := d.runtime.ConfigPath()
+ if configPath == "" {
+ return func() {}
+ }
+ absolute, err := filepath.Abs(configPath)
+ if err != nil {
+ log.Printf("resolve config watch path: %v", err)
+ return func() {}
+ }
+ watcher, err := fsnotify.NewWatcher()
+ if err != nil {
+ log.Printf("create config watcher: %v", err)
+ return func() {}
+ }
+ if err = watcher.Add(filepath.Dir(absolute)); err != nil {
+ log.Printf("watch config directory: %v", err)
+ _ = watcher.Close()
+ return func() {}
+ }
+ done := make(chan struct{})
+ var once sync.Once
+ go func() {
+ var timer *time.Timer
+ for {
+ select {
+ case event, ok := <-watcher.Events:
+ if !ok || filepath.Clean(event.Name) != absolute || event.Op&(fsnotify.Write|fsnotify.Create|fsnotify.Rename) == 0 {
+ continue
+ }
+ if timer != nil {
+ timer.Stop()
+ }
+ timer = time.AfterFunc(100*time.Millisecond, func() {
+ next, loadErr := readBootstrap(absolute)
+ if loadErr != nil {
+ log.Printf("reload changed config: %v", loadErr)
+ return
+ }
+ if next.Data == nil || next.Admin == nil {
+ log.Printf("reload changed config: data and admin configuration are required")
+ return
+ }
+ next.Admin.ConfigPath = absolute
+ d.runtime.Replace(next.Data, next.Admin)
+ log.Printf("config file changed: %s", absolute)
+ })
+ case watchErr, ok := <-watcher.Errors:
+ if ok {
+ log.Printf("config watcher error: %v", watchErr)
+ }
+ case <-done:
+ if timer != nil {
+ timer.Stop()
+ }
+ return
+ }
+ }
+ }()
+ return func() {
+ once.Do(func() {
+ close(done)
+ _ = watcher.Close()
+ })
+ }
+}
diff --git a/internal/data/data.go b/internal/data/data.go
index e1048cd..4031b5f 100644
--- a/internal/data/data.go
+++ b/internal/data/data.go
@@ -16,21 +16,71 @@ import (
var ProviderSet = wire.NewSet(NewData, NewSystemRepo, NewAccessRepo, NewMenuRepo, NewOrganizationRepo, NewSettingsRepo, NewVersionRepo, NewExportRepo, NewAuditRepo, NewTaskRepo, NewMediaRepo, NewAnnouncementRepo, NewEmailRepo, NewCache, NewFileStorage)
type Data struct {
- mu sync.RWMutex
initMu sync.Mutex
- gormDB *gorm.DB
- redis *redis.Client
- database *conf.Data_Database
- config *conf.Data
- admin *conf.AdminBackend
+ configMu sync.Mutex
+ gormDB *reloadableDB
+ redis *reloadableRedis
+ runtime *conf.Runtime
storage *reloadableStorage
+ dbListMu sync.RWMutex
+ dbList map[string]*gorm.DB
}
-func NewData(c *conf.Data, admin *conf.AdminBackend) (*Data, func(), error) {
+func openDatabaseList(configs []*conf.Data_Database) (map[string]*gorm.DB, error) {
+ items := make(map[string]*gorm.DB)
+ for _, config := range configs {
+ if config == nil || config.Disable || config.AliasName == "" {
+ continue
+ }
+ db, err := openDatabase(config, false, "")
+ if err != nil {
+ for _, opened := range items {
+ if sqlDB, dbErr := opened.DB(); dbErr == nil {
+ _ = sqlDB.Close()
+ }
+ }
+ return nil, fmt.Errorf("open database %q: %w", config.AliasName, err)
+ }
+ items[config.AliasName] = db
+ }
+ return items, nil
+}
+
+func closeDatabaseList(items map[string]*gorm.DB) {
+ for _, db := range items {
+ if sqlDB, err := db.DB(); err == nil {
+ _ = sqlDB.Close()
+ }
+ }
+}
+
+func (d *Data) replaceDatabaseList(items map[string]*gorm.DB) {
+ d.dbListMu.Lock()
+ old := d.dbList
+ d.dbList = items
+ d.dbListMu.Unlock()
+ closeDatabaseList(old)
+}
+
+func (d *Data) database(name string) (*gorm.DB, error) {
+ if name == "" {
+ return d.gormDB.DB(), nil
+ }
+ d.dbListMu.RLock()
+ db := d.dbList[name]
+ d.dbListMu.RUnlock()
+ if db == nil {
+ return nil, fmt.Errorf("database %q not found", name)
+ }
+ return db, nil
+}
+
+func NewData(runtime *conf.Runtime) (*Data, func(), error) {
+ c := runtime.Data()
if c == nil || c.Database == nil {
return nil, nil, fmt.Errorf("database configuration is required")
}
- d := &Data{database: c.Database, config: c, admin: admin}
+ d := &Data{runtime: runtime}
db, err := openDatabase(c.Database, false, "")
usingFallback := false
if err != nil {
@@ -43,31 +93,33 @@ func NewData(c *conf.Data, admin *conf.AdminBackend) (*Data, func(), error) {
}
usingFallback = true
}
- d.gormDB = db
- if !usingFallback {
+ d.gormDB = newReloadableDB(db)
+ d.dbList, err = openDatabaseList(c.DatabaseList)
+ if err != nil {
+ d.gormDB.close()
+ return nil, nil, err
+ }
+ admin := runtime.Admin()
+ disableAutoMigrate := admin != nil && admin.System != nil && admin.System.DisableAutoMigrate
+ if !usingFallback && !disableAutoMigrate {
if err = migrateAll(db); err != nil {
return nil, nil, fmt.Errorf("migrate tables: %w", err)
}
}
- if candidate := openRedis(c.Redis); candidate != nil {
- d.redis = candidate
- }
+ useRedis := admin != nil && admin.System != nil && admin.System.UseRedis
+ d.redis = newReloadableRedis(openRedis(c.Redis, useRedis))
+ stopConfigWatcher := d.watchConfig()
cleanup := func() {
- d.mu.RLock()
- db := d.gormDB
- d.mu.RUnlock()
- if sqlDB, closeErr := db.DB(); closeErr == nil {
- _ = sqlDB.Close()
- }
- if d.redis != nil {
- _ = d.redis.Close()
- }
+ stopConfigWatcher()
+ d.gormDB.close()
+ closeDatabaseList(d.dbList)
+ d.redis.close()
}
return d, cleanup, nil
}
-func openRedis(config *conf.Data_Redis) *redis.Client {
- if config == nil || config.Addr == "" {
+func openRedis(config *conf.Data_Redis, enabled bool) *redis.Client {
+ if !enabled || config == nil || config.Addr == "" {
return nil
}
options := &redis.Options{Addr: config.Addr, Network: config.Network}
@@ -89,15 +141,6 @@ func openRedis(config *conf.Data_Redis) *redis.Client {
}
func (d *Data) activateDatabase(db *gorm.DB, config *conf.Data_Database) {
- d.mu.Lock()
- old := d.gormDB
- d.gormDB = db
- d.database = config
- d.config.Database = config
- d.mu.Unlock()
- if old != nil {
- if sqlDB, e := old.DB(); e == nil {
- _ = sqlDB.Close()
- }
- }
+ d.gormDB.replace(db)
+ d.runtime.UpdateDatabase(config)
}
diff --git a/internal/data/data_access_log.go b/internal/data/data_access_log.go
index a7c56db..ea756ae 100644
--- a/internal/data/data_access_log.go
+++ b/internal/data/data_access_log.go
@@ -29,12 +29,6 @@ func dataAccessFromPO(v dataAccessLogPO) *biz.DataAccessLog {
return &biz.DataAccessLog{ID: v.ID, CreatedAt: v.CreatedAt, EventType: v.EventType, TargetTable: v.TargetTable, Operation: v.Operation, UserID: v.UserID, AuthorityID: v.AuthorityID, Scope: v.Scope, RequestID: v.RequestID, Method: v.Method, Path: v.Path, Detail: v.Detail}
}
func (r *auditRepo) ListDataAccess(ctx context.Context, page, size int, q *biz.DataAccessLog) ([]*biz.DataAccessLog, int64, error) {
- if page < 1 {
- page = 1
- }
- if size < 1 {
- size = 10
- }
db := r.data.gormDB.WithContext(ctx).Model(&dataAccessLogPO{})
if q != nil {
if q.EventType != "" {
@@ -52,7 +46,7 @@ func (r *auditRepo) ListDataAccess(ctx context.Context, page, size int, q *biz.D
return nil, 0, err
}
var pos []dataAccessLogPO
- if err := db.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
+ if err := applyPagination(db.Order("id desc"), page, size, 100).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.DataAccessLog, 0, len(pos))
diff --git a/internal/data/department.go b/internal/data/department.go
index 1abea14..be3104d 100644
--- a/internal/data/department.go
+++ b/internal/data/department.go
@@ -44,6 +44,30 @@ func (authorityDepartmentPO) TableName() string { return "sys_authority_departme
func deptFromPO(po departmentPO) *biz.Department {
return &biz.Department{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Name: po.Name, ParentID: po.ParentID, Ancestors: po.Ancestors, Sort: po.Sort, LeaderID: po.LeaderID, Status: po.Status}
}
+
+func (r *organizationRepo) attachDepartmentLeaders(ctx context.Context, departments []*biz.Department) error {
+ leaderIDs := make([]uint, 0, len(departments))
+ for _, department := range departments {
+ if department.LeaderID != 0 {
+ leaderIDs = append(leaderIDs, department.LeaderID)
+ }
+ }
+ if len(leaderIDs) == 0 {
+ return nil
+ }
+ var leaders []userPO
+ if err := r.data.gormDB.WithContext(ctx).Where("id IN ?", leaderIDs).Find(&leaders).Error; err != nil {
+ return err
+ }
+ leaderByID := make(map[uint]*biz.User, len(leaders))
+ for i := range leaders {
+ leaderByID[leaders[i].ID] = baseBizUser(&leaders[i])
+ }
+ for _, department := range departments {
+ department.Leader = leaderByID[department.LeaderID]
+ }
+ return nil
+}
func (r *organizationRepo) CreateDepartment(ctx context.Context, v *biz.Department) error {
v.Ancestors = "0"
if v.ParentID != 0 {
@@ -99,7 +123,11 @@ func (r *organizationRepo) FindDepartment(ctx context.Context, id uint) (*biz.De
if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err
}
- return deptFromPO(po), nil
+ department := deptFromPO(po)
+ if err := r.attachDepartmentLeaders(ctx, []*biz.Department{department}); err != nil {
+ return nil, err
+ }
+ return department, nil
}
func (r *organizationRepo) ListDepartments(ctx context.Context, name string) ([]*biz.Department, error) {
var pos []departmentPO
@@ -111,8 +139,17 @@ func (r *organizationRepo) ListDepartments(ctx context.Context, name string) ([]
return nil, err
}
nodes := map[uint]*biz.Department{}
+ items := make([]*biz.Department, 0, len(pos))
for _, po := range pos {
- nodes[po.ID] = deptFromPO(po)
+ item := deptFromPO(po)
+ nodes[po.ID] = item
+ items = append(items, item)
+ }
+ if err := r.attachDepartmentLeaders(ctx, items); err != nil {
+ return nil, err
+ }
+ if name != "" {
+ return items, nil
}
var roots []*biz.Department
for _, po := range pos {
diff --git a/internal/data/dictionary.go b/internal/data/dictionary.go
index 41b8b1f..6360d90 100644
--- a/internal/data/dictionary.go
+++ b/internal/data/dictionary.go
@@ -62,6 +62,47 @@ func (r *settingsRepo) CreateDictionary(ctx context.Context, v *biz.Dictionary)
v.ID = po.ID
return nil
}
+
+func (r *settingsRepo) ImportDictionary(ctx context.Context, dictionary *biz.Dictionary, details []*biz.DictionaryDetail) error {
+ return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ po := dictionaryPO{Name: dictionary.Name, Type: dictionary.Type, Status: dictionary.Status, Desc: dictionary.Desc, ParentID: dictionary.ParentID}
+ if err := tx.Create(&po).Error; err != nil {
+ return err
+ }
+ dictionary.ID = po.ID
+ idMap := make(map[uint]uint, len(details))
+ created := make(map[uint]uint, len(details))
+ for index, item := range details {
+ // Incomplete detail rows are ignored to preserve import behavior.
+ if item.Label == "" || item.Value == "" {
+ continue
+ }
+ oldID := item.ID
+ detail := dictionaryDetailPO{Label: item.Label, Value: item.Value, Extend: item.Extend, Status: item.Status, Sort: item.Sort, DictionaryID: po.ID, Level: item.Level, Path: item.Path}
+ if err := tx.Create(&detail).Error; err != nil {
+ return err
+ }
+ created[uint(index)] = detail.ID
+ if oldID > 0 {
+ idMap[oldID] = detail.ID
+ }
+ }
+ for index, item := range details {
+ if item.ID == 0 || item.ParentID == nil || *item.ParentID == 0 {
+ continue
+ }
+ newID, exists := created[uint(index)]
+ newParentID, parentExists := idMap[*item.ParentID]
+ if !exists || !parentExists {
+ continue
+ }
+ if err := tx.Model(&dictionaryDetailPO{}).Where("id = ?", newID).Update("parent_id", newParentID).Error; err != nil {
+ return err
+ }
+ }
+ return nil
+ })
+}
func (r *settingsRepo) UpdateDictionary(ctx context.Context, v *biz.Dictionary) error {
return r.data.gormDB.WithContext(ctx).Model(&dictionaryPO{}).Where("id = ?", v.ID).Updates(map[string]any{"name": v.Name, "type": v.Type, "status": v.Status, "desc": v.Desc, "parent_id": v.ParentID}).Error
}
@@ -80,51 +121,98 @@ func (r *settingsRepo) DeleteDictionary(ctx context.Context, id uint) error {
return tx.Delete(&dictionaryPO{}, id).Error
})
}
-func (r *settingsRepo) FindDictionary(ctx context.Context, id uint, typ string, details bool) (*biz.Dictionary, error) {
+func (r *settingsRepo) FindDictionary(ctx context.Context, id uint, typ string, status *bool, details bool) (*biz.Dictionary, error) {
var po dictionaryPO
db := r.data.gormDB.WithContext(ctx)
- var err error
- if id != 0 {
- err = db.First(&po, id).Error
- } else {
- err = db.Where("type = ?", typ).First(&po).Error
+ active := true
+ if status != nil {
+ active = *status
}
+ err := db.Where("(type = ? OR id = ?) AND status = ?", typ, id, active).First(&po).Error
if err != nil {
return nil, err
}
out := dictionaryFromPO(po)
if details {
- out.Details, _, err = r.ListDictionaryDetails(ctx, 1, 10000, out.ID, "", "")
+ out.Details, _, err = r.ListDictionaryDetails(ctx, 0, 0, biz.DictionaryDetailFilter{DictionaryID: out.ID})
+ if err == nil {
+ filtered := out.Details[:0]
+ for _, item := range out.Details {
+ if item.Status {
+ filtered = append(filtered, item)
+ }
+ }
+ out.Details = filtered
+ }
}
return out, err
}
+
+func (r *settingsRepo) ExportDictionary(ctx context.Context, id uint) (*biz.Dictionary, error) {
+ var po dictionaryPO
+ if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil {
+ return nil, err
+ }
+ value := dictionaryFromPO(po)
+ details, _, err := r.ListDictionaryDetails(ctx, 0, 0, biz.DictionaryDetailFilter{DictionaryID: id})
+ if err != nil {
+ return nil, err
+ }
+ value.Details = details
+ return value, nil
+}
func (r *settingsRepo) ListDictionaries(ctx context.Context, page, size int, name, typ string, details bool) ([]*biz.Dictionary, int64, error) {
- if page < 1 {
- page = 1
- }
- if size < 1 {
- size = 10
- }
db := r.data.gormDB.WithContext(ctx).Model(&dictionaryPO{})
if name != "" {
- db = db.Where("name LIKE ?", "%"+name+"%")
- }
- if typ != "" {
- db = db.Where("type LIKE ?", "%"+typ+"%")
+ like := "%" + name + "%"
+ db = db.Where("name LIKE ? OR type LIKE ?", like, like)
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
var pos []dictionaryPO
- if err := db.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
+ if err := applyPagination(db.Order("id"), page, size, 0).Find(&pos).Error; err != nil {
return nil, 0, err
}
+ parentIDs := make([]uint, 0, len(pos))
+ dictionaryIDs := make([]uint, 0, len(pos))
+ for _, po := range pos {
+ parentIDs = append(parentIDs, po.ID)
+ dictionaryIDs = append(dictionaryIDs, po.ID)
+ }
+ childrenByParent := make(map[uint][]dictionaryPO)
+ if len(parentIDs) > 0 {
+ var children []dictionaryPO
+ if err := r.data.gormDB.WithContext(ctx).Where("parent_id IN ?", parentIDs).Order("id").Find(&children).Error; err != nil {
+ return nil, 0, err
+ }
+ for _, child := range children {
+ if child.ParentID != nil {
+ childrenByParent[*child.ParentID] = append(childrenByParent[*child.ParentID], child)
+ }
+ }
+ }
+ detailsByDictionary := make(map[uint][]dictionaryDetailPO)
+ if details && len(dictionaryIDs) > 0 {
+ var values []dictionaryDetailPO
+ if err := r.data.gormDB.WithContext(ctx).Where("sys_dictionary_id IN ?", dictionaryIDs).Order("sort").Find(&values).Error; err != nil {
+ return nil, 0, err
+ }
+ for _, value := range values {
+ detailsByDictionary[value.DictionaryID] = append(detailsByDictionary[value.DictionaryID], value)
+ }
+ }
out := make([]*biz.Dictionary, 0, len(pos))
for _, po := range pos {
v := dictionaryFromPO(po)
+ for _, child := range childrenByParent[po.ID] {
+ v.Children = append(v.Children, dictionaryFromPO(child))
+ }
if details {
- v.Details, _, _ = r.ListDictionaryDetails(ctx, 1, 10000, v.ID, "", "")
+ for _, detail := range detailsByDictionary[v.ID] {
+ v.Details = append(v.Details, detailFromPO(detail))
+ }
}
out = append(out, v)
}
@@ -197,29 +285,32 @@ func (r *settingsRepo) FindDictionaryDetail(ctx context.Context, id uint) (*biz.
}
return detailFromPO(po), nil
}
-func (r *settingsRepo) ListDictionaryDetails(ctx context.Context, page, size int, dictionaryID uint, label, value string) ([]*biz.DictionaryDetail, int64, error) {
- if page < 1 {
- page = 1
- }
- if size < 1 {
- size = 10
- }
+func (r *settingsRepo) ListDictionaryDetails(ctx context.Context, page, size int, filter biz.DictionaryDetailFilter) ([]*biz.DictionaryDetail, int64, error) {
db := r.data.gormDB.WithContext(ctx).Model(&dictionaryDetailPO{})
- if dictionaryID != 0 {
- db = db.Where("sys_dictionary_id = ?", dictionaryID)
+ if filter.DictionaryID != 0 {
+ db = db.Where("sys_dictionary_id = ?", filter.DictionaryID)
}
- if label != "" {
- db = db.Where("label LIKE ?", "%"+label+"%")
+ if filter.Label != "" {
+ db = db.Where("label LIKE ?", "%"+filter.Label+"%")
}
- if value != "" {
- db = db.Where("value LIKE ?", "%"+value+"%")
+ if filter.Value != "" {
+ db = db.Where("value = ?", filter.Value)
+ }
+ if filter.Status != nil {
+ db = db.Where("status = ?", *filter.Status)
+ }
+ if filter.ParentID != nil {
+ db = db.Where("parent_id = ?", *filter.ParentID)
+ }
+ if filter.Level != nil {
+ db = db.Where("level = ?", *filter.Level)
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
var pos []dictionaryDetailPO
- if err := db.Order("sort,id").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
+ if err := applyPagination(db.Order("sort,id"), page, size, 100).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.DictionaryDetail, 0, len(pos))
@@ -230,13 +321,14 @@ func (r *settingsRepo) ListDictionaryDetails(ctx context.Context, page, size int
}
func (r *settingsRepo) DictionaryDetailTree(ctx context.Context, dictionaryID uint, typ string) ([]*biz.DictionaryDetail, error) {
if dictionaryID == 0 {
- dictionary, err := r.FindDictionary(ctx, 0, typ, false)
+ active := true
+ dictionary, err := r.FindDictionary(ctx, 0, typ, &active, false)
if err != nil {
return nil, err
}
dictionaryID = dictionary.ID
}
- items, _, err := r.ListDictionaryDetails(ctx, 1, 100000, dictionaryID, "", "")
+ items, _, err := r.ListDictionaryDetails(ctx, 0, 0, biz.DictionaryDetailFilter{DictionaryID: dictionaryID})
if err != nil {
return nil, err
}
diff --git a/internal/data/email.go b/internal/data/email.go
index 712946b..8665729 100644
--- a/internal/data/email.go
+++ b/internal/data/email.go
@@ -17,21 +17,22 @@ import (
)
type emailRepo struct {
- config *conf.AdminBackend
+ runtime *conf.Runtime
}
-func NewEmailRepo(config *conf.AdminBackend) biz.EmailRepo {
- if config == nil {
- return &emailRepo{}
- }
- return &emailRepo{config: config}
+func NewEmailRepo(runtime *conf.Runtime) biz.EmailRepo {
+ return &emailRepo{runtime: runtime}
}
func (r *emailRepo) email() *conf.AdminBackend_Email {
- if r.config == nil {
+ if r.runtime == nil {
return nil
}
- return r.config.Email
+ config := r.runtime.Admin()
+ if config == nil {
+ return nil
+ }
+ return config.Email
}
func (r *emailRepo) Enabled() bool {
diff --git a/internal/data/error_record.go b/internal/data/error_record.go
index 293405b..f474ca9 100644
--- a/internal/data/error_record.go
+++ b/internal/data/error_record.go
@@ -48,22 +48,16 @@ func (r *auditRepo) FindError(ctx context.Context, id uint) (*biz.ErrorRecord, e
return errorFromPO(po), nil
}
func (r *auditRepo) ListErrors(ctx context.Context, page, size int, q *biz.ErrorRecord) ([]*biz.ErrorRecord, int64, error) {
- if page < 1 {
- page = 1
- }
- if size < 1 {
- size = 10
- }
db := r.data.gormDB.WithContext(ctx).Model(&errorRecordPO{})
if q != nil {
+ if len(q.CreatedAtRange) == 2 {
+ db = db.Where("created_at BETWEEN ? AND ?", q.CreatedAtRange[0], q.CreatedAtRange[1])
+ }
if q.Form != "" {
- db = db.Where("form LIKE ?", "%"+q.Form+"%")
+ db = db.Where("form = ?", q.Form)
}
- if q.Level != "" {
- db = db.Where("level = ?", q.Level)
- }
- if q.Status != "" {
- db = db.Where("status = ?", q.Status)
+ if q.Info != "" {
+ db = db.Where("info LIKE ?", "%"+q.Info+"%")
}
}
var total int64
@@ -71,7 +65,7 @@ func (r *auditRepo) ListErrors(ctx context.Context, page, size int, q *biz.Error
return nil, 0, err
}
var pos []errorRecordPO
- if err := db.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
+ if err := applyPagination(db.Order("created_at desc"), page, size, 100).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.ErrorRecord, 0, len(pos))
diff --git a/internal/data/export.go b/internal/data/export.go
index b2b5b6a..0c8155c 100644
--- a/internal/data/export.go
+++ b/internal/data/export.go
@@ -2,12 +2,11 @@ package data
import (
"context"
+ "encoding/json"
"errors"
"fmt"
"gorm.io/gorm"
"kra/internal/biz"
- "regexp"
- "sort"
"strconv"
"strings"
"time"
@@ -151,14 +150,11 @@ func (r *exportRepo) FindExportTemplate(ctx context.Context, id uint, tid string
return exportFromPO(po, conditions, joins), nil
}
func (r *exportRepo) ListExportTemplates(ctx context.Context, page, size int, q *biz.ExportTemplate) ([]*biz.ExportTemplate, int64, error) {
- if page < 1 {
- page = 1
- }
- if size < 1 {
- size = 10
- }
db := r.data.gormDB.WithContext(ctx).Model(&exportTemplatePO{})
if q != nil {
+ if q.StartCreatedAt != nil && q.EndCreatedAt != nil {
+ db = db.Where("created_at BETWEEN ? AND ?", q.StartCreatedAt, q.EndCreatedAt)
+ }
if q.Name != "" {
db = db.Where("name LIKE ?", "%"+q.Name+"%")
}
@@ -174,37 +170,46 @@ func (r *exportRepo) ListExportTemplates(ctx context.Context, page, size int, q
return nil, 0, err
}
var pos []exportTemplatePO
- if err := db.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
+ if err := applyPagination(db, page, size, 0).Find(&pos).Error; err != nil {
return nil, 0, err
}
+ templateIDs := make([]string, 0, len(pos))
+ for _, po := range pos {
+ templateIDs = append(templateIDs, po.TemplateID)
+ }
+ var conditions []exportConditionPO
+ var joins []exportJoinPO
+ if len(templateIDs) > 0 {
+ if err := r.data.gormDB.WithContext(ctx).Where("template_id IN ?", templateIDs).Find(&conditions).Error; err != nil {
+ return nil, 0, err
+ }
+ if err := r.data.gormDB.WithContext(ctx).Where("template_id IN ?", templateIDs).Find(&joins).Error; err != nil {
+ return nil, 0, err
+ }
+ }
+ conditionsByTemplate := make(map[string][]exportConditionPO, len(pos))
+ for _, condition := range conditions {
+ conditionsByTemplate[condition.TemplateID] = append(conditionsByTemplate[condition.TemplateID], condition)
+ }
+ joinsByTemplate := make(map[string][]exportJoinPO, len(pos))
+ for _, join := range joins {
+ joinsByTemplate[join.TemplateID] = append(joinsByTemplate[join.TemplateID], join)
+ }
out := make([]*biz.ExportTemplate, 0, len(pos))
for _, po := range pos {
- v, err := r.FindExportTemplate(ctx, po.ID, "")
- if err != nil {
- return nil, 0, err
- }
- out = append(out, v)
+ out = append(out, exportFromPO(po, conditionsByTemplate[po.TemplateID], joinsByTemplate[po.TemplateID]))
}
return out, total, nil
}
-var safeIdentifier = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_.]*$`)
-var allowedOperator = map[string]bool{"=": true, "!=": true, ">": true, ">=": true, "<": true, "<=": true, "LIKE": true, "IN": true, "NOT IN": true, "BETWEEN": true}
-
-func validateSelectSQL(sql string) error {
- normalized := strings.ToLower(strings.TrimSpace(sql))
- if !strings.HasPrefix(normalized, "select ") || strings.Contains(normalized, ";") {
- return errors.New("仅允许单条 SELECT 查询")
- }
- return nil
-}
func (r *exportRepo) QueryExport(ctx context.Context, t *biz.ExportTemplate, params map[string]string) ([]map[string]any, string, error) {
- db := r.data.gormDB.WithContext(ctx)
+ selected, err := r.data.database(t.DBName)
+ if err != nil {
+ return nil, "", err
+ }
+ db := selected.WithContext(ctx)
var rows []map[string]any
if t.SQL != "" {
- if err := validateSelectSQL(t.SQL); err != nil {
- return nil, "", err
- }
args := map[string]any{}
for k, v := range params {
args[k] = v
@@ -212,52 +217,54 @@ func (r *exportRepo) QueryExport(ctx context.Context, t *biz.ExportTemplate, par
err := db.Raw(t.SQL, args).Scan(&rows).Error
return rows, t.SQL, err
}
- if !safeIdentifier.MatchString(t.TableName) {
- return nil, "", errors.New("表名不合法")
- }
columns := make([]string, 0)
- for key := range parseTemplateColumns(t.TemplateInfo) {
- if !safeIdentifier.MatchString(key) {
- return nil, "", errors.New("列名不合法")
- }
- columns = append(columns, key)
+ if err := json.Unmarshal([]byte(t.TemplateInfo), &map[string]string{}); err != nil {
+ return nil, "", err
+ }
+ decoder := json.NewDecoder(strings.NewReader(t.TemplateInfo))
+ _, _ = decoder.Token()
+ for decoder.More() {
+ key, _ := decoder.Token()
+ columns = append(columns, key.(string))
+ var ignored any
+ _ = decoder.Decode(&ignored)
}
- sort.Strings(columns)
if len(columns) == 0 {
return nil, "", errors.New("模板列为空")
}
query := db.Table(t.TableName).Select(strings.Join(columns, ","))
for _, j := range t.Joins {
- joinType := strings.ToUpper(strings.TrimSpace(j.Join))
- if joinType != "LEFT JOIN" && joinType != "RIGHT JOIN" && joinType != "INNER JOIN" && joinType != "JOIN" {
- return nil, "", errors.New("关联类型不合法")
+ query = query.Joins(j.Join + " " + j.Table + " ON " + j.On)
+ }
+ if params["filterDeleted"] == "true" {
+ query = query.Where(fmt.Sprintf("%s.deleted_at IS NULL", t.TableName))
+ for _, join := range t.Joins {
+ if db.Migrator().HasColumn(join.Table, "deleted_at") {
+ query = query.Where(fmt.Sprintf("%s.deleted_at IS NULL", join.Table))
+ }
}
- if !safeIdentifier.MatchString(j.Table) {
- return nil, "", errors.New("关联表不合法")
- }
- query = query.Joins(joinType + " " + j.Table + " ON " + j.On)
}
for _, condition := range t.Conditions {
- operator := strings.ToUpper(strings.TrimSpace(condition.Operator))
- if !allowedOperator[operator] || !safeIdentifier.MatchString(condition.Column) {
- return nil, "", errors.New("查询条件不合法")
- }
+ operator := condition.Operator
value := params[condition.From]
- if value == "" {
- continue
- }
switch operator {
case "LIKE":
- query = query.Where(condition.Column+" LIKE ?", "%"+value+"%")
+ if value != "" {
+ query = query.Where(condition.Column+" LIKE ?", "%"+value+"%")
+ }
case "IN", "NOT IN":
- query = query.Where(condition.Column+" "+operator+" ?", strings.Split(value, ","))
+ if value != "" {
+ query = query.Where(condition.Column+" "+operator+" (?)", value)
+ }
case "BETWEEN":
start, end := params["start"+condition.From], params["end"+condition.From]
if start != "" && end != "" {
query = query.Where(condition.Column+" BETWEEN ? AND ?", start, end)
}
default:
- query = query.Where(condition.Column+" "+operator+" ?", value)
+ if value != "" {
+ query = query.Where(condition.Column+" "+operator+" ?", value)
+ }
}
}
limit := 0
@@ -269,17 +276,141 @@ func (r *exportRepo) QueryExport(ctx context.Context, t *biz.ExportTemplate, par
if limit > 0 {
query = query.Limit(limit)
}
- if t.Order != "" {
- parts := strings.Fields(t.Order)
- if len(parts) < 1 || len(parts) > 2 || !safeIdentifier.MatchString(parts[0]) || (len(parts) == 2 && strings.ToUpper(parts[1]) != "ASC" && strings.ToUpper(parts[1]) != "DESC") {
- return nil, "", errors.New("排序不合法")
+ if raw := params["offset"]; raw != "" {
+ if offset, parseErr := strconv.Atoi(raw); parseErr == nil {
+ query = query.Offset(offset)
}
- query = query.Order(t.Order)
+ }
+ order := params["order"]
+ if order == "" {
+ order = t.Order
+ }
+ if order != "" {
+ parts := strings.Split(order, " ")
+ fields := map[string]bool{}
+ columnTypes, columnErr := db.Migrator().ColumnTypes(t.TableName)
+ if columnErr != nil {
+ return nil, "", columnErr
+ }
+ for _, column := range columnTypes {
+ fields[column.Name()] = true
+ }
+ if !fields[parts[0]] {
+ return nil, "", fmt.Errorf("order by %s is not in the fields", order)
+ }
+ orderSQL := parts[0]
+ if len(parts) > 1 {
+ if parts[1] != "asc" && parts[1] != "desc" {
+ return nil, "", fmt.Errorf("order by %s is not secure", order)
+ }
+ orderSQL += " " + parts[1]
+ }
+ query = query.Order(orderSQL)
}
preview := query.ToSQL(func(tx *gorm.DB) *gorm.DB { return tx.Find(&[]map[string]any{}) })
- err := query.Find(&rows).Error
+ err = query.Find(&rows).Error
return rows, preview, err
}
+
+func (r *exportRepo) PreviewExport(ctx context.Context, t *biz.ExportTemplate, params map[string]string) (string, error) {
+ columns := make([]string, 0)
+ decoder := json.NewDecoder(strings.NewReader(t.TemplateInfo))
+ if token, err := decoder.Token(); err != nil || token != json.Delim('{') {
+ return "", err
+ }
+ for decoder.More() {
+ key, err := decoder.Token()
+ if err != nil {
+ return "", err
+ }
+ columns = append(columns, key.(string))
+ var ignored any
+ if err = decoder.Decode(&ignored); err != nil {
+ return "", err
+ }
+ }
+ var builder strings.Builder
+ builder.WriteString("SELECT ")
+ builder.WriteString(strings.Join(columns, ", "))
+ builder.WriteString(" FROM ")
+ builder.WriteString(t.TableName)
+ for _, join := range t.Joins {
+ builder.WriteString(" " + join.Join + " " + join.Table + " ON " + join.On)
+ }
+ wheres := make([]string, 0)
+ if params["filterDeleted"] == "true" {
+ wheres = append(wheres, fmt.Sprintf("%s.deleted_at IS NULL", t.TableName))
+ selected, err := r.data.database(t.DBName)
+ if err != nil {
+ return "", err
+ }
+ db := selected.WithContext(ctx)
+ for _, join := range t.Joins {
+ if db.Migrator().HasColumn(join.Table, "deleted_at") {
+ wheres = append(wheres, fmt.Sprintf("%s.deleted_at IS NULL", join.Table))
+ }
+ }
+ }
+ for _, condition := range t.Conditions {
+ op, column, value := strings.ToUpper(strings.TrimSpace(condition.Operator)), strings.TrimSpace(condition.Column), params[condition.From]
+ switch op {
+ case "BETWEEN":
+ start, end := params["start"+condition.From], params["end"+condition.From]
+ if start != "" && end != "" {
+ wheres = append(wheres, fmt.Sprintf("%s BETWEEN '%s' AND '%s'", column, start, end))
+ } else {
+ wheres = append(wheres, fmt.Sprintf("%s BETWEEN {start%s} AND {end%s}", column, condition.From, condition.From))
+ }
+ case "IN", "NOT IN":
+ if value != "" {
+ parts := strings.Split(value, ",")
+ for index := range parts {
+ parts[index] = strings.TrimSpace(parts[index])
+ }
+ wheres = append(wheres, fmt.Sprintf("%s %s ('%s')", column, op, strings.Join(parts, "','")))
+ } else {
+ wheres = append(wheres, fmt.Sprintf("%s %s ({%s})", column, op, condition.From))
+ }
+ case "LIKE":
+ if value != "" {
+ wheres = append(wheres, fmt.Sprintf("%s LIKE '%%%s%%'", column, value))
+ } else {
+ wheres = append(wheres, fmt.Sprintf("%s LIKE {%%%s%%}", column, condition.From))
+ }
+ default:
+ if value != "" {
+ wheres = append(wheres, fmt.Sprintf("%s %s '%s'", column, op, value))
+ } else {
+ wheres = append(wheres, fmt.Sprintf("%s %s {%s}", column, op, condition.From))
+ }
+ }
+ }
+ if len(wheres) > 0 {
+ builder.WriteString(" WHERE " + strings.Join(wheres, " AND "))
+ }
+ order := params["order"]
+ if order == "" {
+ order = t.Order
+ }
+ if order != "" {
+ builder.WriteString(" ORDER BY " + order)
+ }
+ limitRaw, offsetRaw := params["limit"], params["offset"]
+ if limitRaw == "" && t.Limit != nil && *t.Limit != 0 {
+ limitRaw = strconv.Itoa(*t.Limit)
+ }
+ limit, _ := strconv.Atoi(limitRaw)
+ offset, _ := strconv.Atoi(offsetRaw)
+ if limit > 0 {
+ builder.WriteString(" LIMIT " + strconv.Itoa(limit))
+ if offset > 0 {
+ builder.WriteString(" OFFSET " + strconv.Itoa(offset))
+ }
+ } else if offset > 0 {
+ builder.WriteString(" OFFSET " + strconv.Itoa(offset))
+ }
+ return builder.String(), nil
+}
func parseTemplateColumns(raw string) map[string]string {
out := map[string]string{}
parts := strings.Split(strings.Trim(strings.TrimSpace(raw), "{}"), ",")
@@ -293,16 +424,29 @@ func parseTemplateColumns(raw string) map[string]string {
}
func (r *exportRepo) ImportExportRows(ctx context.Context, t *biz.ExportTemplate, rows []map[string]any) error {
sql := strings.TrimSpace(t.ImportSQL)
- lower := strings.ToLower(sql)
- if sql == "" || strings.Contains(sql, ";") || (!strings.HasPrefix(lower, "insert ") && !strings.HasPrefix(lower, "update ")) {
- return errors.New("导入 SQL 仅允许单条 INSERT 或 UPDATE")
+ selected, err := r.data.database(t.DBName)
+ if err != nil {
+ return err
}
- return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
- for index, row := range rows {
- if err := tx.Exec(sql, row).Error; err != nil {
- return fmt.Errorf("第%d行导入失败: %w", index+2, err)
+ return selected.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ if sql != "" {
+ for _, row := range rows {
+ if err := tx.Exec(sql, row).Error; err != nil {
+ return err
+ }
+ }
+ return nil
+ }
+ needCreated := tx.Migrator().HasColumn(t.TableName, "created_at")
+ needUpdated := tx.Migrator().HasColumn(t.TableName, "updated_at")
+ for _, row := range rows {
+ if row["created_at"] == nil && needCreated {
+ row["created_at"] = time.Now()
+ }
+ if row["updated_at"] == nil && needUpdated {
+ row["updated_at"] = time.Now()
}
}
- return nil
+ return tx.Table(t.TableName).CreateInBatches(&rows, 1000).Error
})
}
diff --git a/internal/data/file_storage.go b/internal/data/file_storage.go
index 1e40f55..a1b5268 100644
--- a/internal/data/file_storage.go
+++ b/internal/data/file_storage.go
@@ -16,7 +16,7 @@ type reloadableStorage struct {
}
func NewFileStorage(data *Data) (biz.FileStorage, error) {
- storage, err := buildFileStorage(data.admin)
+ storage, err := buildFileStorage(data.runtime.Admin())
if err != nil {
return nil, err
}
diff --git a/internal/data/login_log.go b/internal/data/login_log.go
index e2a567a..01a464d 100644
--- a/internal/data/login_log.go
+++ b/internal/data/login_log.go
@@ -29,19 +29,13 @@ func loginFromPO(v loginLogPO) *biz.LoginLog {
return &biz.LoginLog{ID: v.ID, CreatedAt: v.CreatedAt, Username: v.Username, IP: v.IP, Status: v.Status, ErrorMessage: v.ErrorMessage, Agent: v.Agent, UserID: v.UserID}
}
func (r *auditRepo) ListLogins(ctx context.Context, page, size int, q *biz.LoginLog) ([]*biz.LoginLog, int64, error) {
- if page < 1 {
- page = 1
- }
- if size < 1 {
- size = 10
- }
db := r.data.gormDB.WithContext(ctx).Model(&loginLogPO{})
if q != nil {
if q.Username != "" {
db = db.Where("username LIKE ?", "%"+q.Username+"%")
}
- if q.IP != "" {
- db = db.Where("ip LIKE ?", "%"+q.IP+"%")
+ if q.FilterByStatus {
+ db = db.Where("status = ?", q.Status)
}
}
var total int64
@@ -49,12 +43,21 @@ func (r *auditRepo) ListLogins(ctx context.Context, page, size int, q *biz.Login
return nil, 0, err
}
var pos []loginLogPO
- if err := db.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
+ if err := applyPagination(db.Order("id desc"), page, size, 100).Find(&pos).Error; err != nil {
return nil, 0, err
}
+ ids := make([]uint, 0, len(pos))
+ for _, value := range pos {
+ if value.UserID != 0 {
+ ids = append(ids, value.UserID)
+ }
+ }
+ users := auditUsers(ctx, r.data.gormDB.WithContext(ctx), ids)
out := make([]*biz.LoginLog, 0, len(pos))
for _, po := range pos {
- out = append(out, loginFromPO(po))
+ value := loginFromPO(po)
+ value.User = users[po.UserID]
+ out = append(out, value)
}
return out, total, nil
}
diff --git a/internal/data/media.go b/internal/data/media.go
index 73b92bc..772764c 100644
--- a/internal/data/media.go
+++ b/internal/data/media.go
@@ -71,32 +71,45 @@ func (r *mediaRepo) FindMediaByHash(ctx context.Context, userID uint, hash strin
}
return mediaFromPO(po), nil
}
-func (r *mediaRepo) ListMedia(ctx context.Context, page, size int, keyword string, categoryID int, tag string, userID uint) ([]*biz.MediaFile, int64, error) {
- if page < 1 {
- page = 1
+func (r *mediaRepo) ListMedia(ctx context.Context, filter biz.MediaFilter) ([]*biz.MediaFile, int64, error) {
+ if filter.Page < 1 {
+ filter.Page = 1
}
- if size < 1 {
- size = 10
+ if filter.PageSize < 1 {
+ filter.PageSize = 10
}
db := r.data.gormDB.WithContext(ctx).Model(&mediaPO{})
- if keyword != "" {
- db = db.Where("name LIKE ?", "%"+keyword+"%")
+ if filter.Keyword != "" {
+ db = db.Where("name LIKE ?", "%"+filter.Keyword+"%")
}
- if categoryID > 0 {
- db = db.Where("class_id = ?", categoryID)
+ if filter.CategoryID > 0 {
+ db = db.Where("class_id = ?", filter.CategoryID)
}
- if tag != "" {
- db = db.Where("tag = ?", tag)
+ if filter.Tag != "" {
+ db = db.Where("tag = ?", filter.Tag)
}
- if userID > 0 {
- db = db.Where("user_id = ?", userID)
+ if filter.UserID > 0 {
+ db = db.Where("user_id = ?", filter.UserID)
+ }
+ if !filter.StartCreatedAt.IsZero() {
+ db = db.Where("created_at >= ?", filter.StartCreatedAt)
+ }
+ if !filter.EndCreatedAt.IsZero() {
+ db = db.Where("created_at < ?", filter.EndCreatedAt)
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
var pos []mediaPO
- if err := db.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
+ orderKey := filter.OrderKey
+ if orderKey != "id" && orderKey != "size" && orderKey != "created_at" && orderKey != "name" {
+ orderKey = "id"
+ }
+ if filter.Desc {
+ orderKey += " desc"
+ }
+ if err := applyPagination(db.Order(orderKey), filter.Page, filter.PageSize, 100).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.MediaFile, 0, len(pos))
@@ -130,9 +143,6 @@ func (r *mediaRepo) SaveCategory(ctx context.Context, v *biz.AttachmentCategory)
db := r.data.gormDB.WithContext(ctx)
var count int64
query := db.Model(&categoryPO{}).Where("name = ? AND pid = ?", v.Name, v.ParentID)
- if v.ID != 0 {
- query = query.Where("id <> ?", v.ID)
- }
if err := query.Count(&count).Error; err != nil {
return err
}
diff --git a/internal/data/menu.go b/internal/data/menu.go
index 09386a2..c75147d 100644
--- a/internal/data/menu.go
+++ b/internal/data/menu.go
@@ -193,7 +193,18 @@ func (r *menuRepo) Find(ctx context.Context, id uint) (*biz.Menu, error) {
func (r *menuRepo) List(ctx context.Context) ([]*biz.Menu, error) {
var pos []menuPO
- if err := r.data.gormDB.WithContext(ctx).Order("sort,id").Find(&pos).Error; err != nil {
+ db := r.data.gormDB.WithContext(ctx).Model(&menuPO{}).Order("sort,id")
+ config := r.data.runtime.Admin()
+ if actor, ok := biz.ActorFromContext(ctx); ok && config != nil && config.System != nil && config.System.UseStrictAuth {
+ var authority authorityPO
+ if err := r.data.gormDB.WithContext(ctx).Where("authority_id = ?", actor.AuthorityID).First(&authority).Error; err != nil {
+ return nil, err
+ }
+ if authority.ParentID != nil && *authority.ParentID != 0 {
+ db = db.Where("id IN (?)", r.data.gormDB.WithContext(ctx).Model(&authorityMenuPO{}).Select("sys_base_menu_id").Where("sys_authority_authority_id = ?", actor.AuthorityID))
+ }
+ }
+ if err := db.Find(&pos).Error; err != nil {
return nil, err
}
out := make([]*biz.Menu, 0, len(pos))
@@ -231,6 +242,31 @@ func (r *menuRepo) List(ctx context.Context) ([]*biz.Menu, error) {
}
func (r *menuRepo) SetAuthorityMenus(ctx context.Context, id uint, ids []uint) error {
+ if err := (&accessRepo{data: r.data}).checkAuthorityIDAuth(ctx, id); err != nil {
+ return err
+ }
+ config := r.data.runtime.Admin()
+ if actor, ok := biz.ActorFromContext(ctx); ok && config != nil && config.System != nil && config.System.UseStrictAuth {
+ var authority authorityPO
+ if err := r.data.gormDB.WithContext(ctx).Where("authority_id = ?", actor.AuthorityID).First(&authority).Error; err != nil {
+ return err
+ }
+ if authority.ParentID != nil && *authority.ParentID != 0 {
+ var allowedIDs []uint
+ if err := r.data.gormDB.WithContext(ctx).Model(&authorityMenuPO{}).Where("sys_authority_authority_id = ?", actor.AuthorityID).Pluck("sys_base_menu_id", &allowedIDs).Error; err != nil {
+ return err
+ }
+ allowed := make(map[uint]bool, len(allowedIDs))
+ for _, menuID := range allowedIDs {
+ allowed[menuID] = true
+ }
+ for _, menuID := range ids {
+ if !allowed[menuID] {
+ return errors.New("添加失败,请勿跨级操作")
+ }
+ }
+ }
+ }
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("sys_authority_authority_id = ?", id).Delete(&authorityMenuPO{}).Error; err != nil {
return err
@@ -258,6 +294,16 @@ func (r *menuRepo) MenuRoleIDs(ctx context.Context, id uint) ([]uint, error) {
return ids, err
}
+func (r *menuRepo) DefaultRouterRoleIDs(ctx context.Context, id uint) ([]uint, error) {
+ var menu menuPO
+ if err := r.data.gormDB.WithContext(ctx).First(&menu, id).Error; err != nil {
+ return nil, err
+ }
+ var ids []uint
+ err := r.data.gormDB.WithContext(ctx).Model(&authorityPO{}).Where("default_router = ?", menu.Name).Pluck("authority_id", &ids).Error
+ return ids, err
+}
+
func (r *menuRepo) SetMenuRoles(ctx context.Context, id uint, ids []uint) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("sys_base_menu_id = ?", id).Delete(&authorityMenuPO{}).Error; err != nil {
diff --git a/internal/data/operation_log.go b/internal/data/operation_log.go
index 173f4ce..8642225 100644
--- a/internal/data/operation_log.go
+++ b/internal/data/operation_log.go
@@ -36,12 +36,6 @@ func opFromPO(v operationPO) *biz.OperationRecord {
return &biz.OperationRecord{ID: v.ID, CreatedAt: v.CreatedAt, IP: v.IP, Method: v.Method, Path: v.Path, Status: v.Status, LatencyMS: v.LatencyMS, Agent: v.Agent, ErrorMessage: v.ErrorMessage, Body: v.Body, Response: v.Response, UserID: v.UserID, RequestID: v.RequestID, TraceID: v.TraceID, DeviceID: v.DeviceID}
}
func (r *auditRepo) ListOperations(ctx context.Context, page, size int, q *biz.OperationRecord) ([]*biz.OperationRecord, int64, error) {
- if page < 1 {
- page = 1
- }
- if size < 1 {
- size = 10
- }
db := r.data.gormDB.WithContext(ctx).Model(&operationPO{})
if q != nil {
if q.Path != "" {
@@ -53,27 +47,49 @@ func (r *auditRepo) ListOperations(ctx context.Context, page, size int, q *biz.O
if q.Status != 0 {
db = db.Where("status = ?", q.Status)
}
- if q.IP != "" {
- db = db.Where("ip LIKE ?", "%"+q.IP+"%")
- }
- if q.UserID != 0 {
- db = db.Where("user_id = ?", q.UserID)
- }
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
var pos []operationPO
- if err := db.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
+ if err := applyPagination(db.Order("id desc"), page, size, 100).Find(&pos).Error; err != nil {
return nil, 0, err
}
+ users := auditUsers(ctx, r.data.gormDB.WithContext(ctx), operationUserIDs(pos))
out := make([]*biz.OperationRecord, 0, len(pos))
for _, po := range pos {
- out = append(out, opFromPO(po))
+ value := opFromPO(po)
+ value.User = users[po.UserID]
+ out = append(out, value)
}
return out, total, nil
}
+
+func operationUserIDs(values []operationPO) []uint {
+ ids := make([]uint, 0, len(values))
+ for _, value := range values {
+ if value.UserID != 0 {
+ ids = append(ids, value.UserID)
+ }
+ }
+ return ids
+}
+
+func auditUsers(ctx context.Context, db *gorm.DB, ids []uint) map[uint]*biz.User {
+ result := make(map[uint]*biz.User)
+ if len(ids) == 0 {
+ return result
+ }
+ var users []userPO
+ if db.WithContext(ctx).Where("id IN ?", ids).Find(&users).Error != nil {
+ return result
+ }
+ for _, value := range users {
+ result[value.ID] = &biz.User{ID: value.ID, CreatedAt: value.CreatedAt, UpdatedAt: value.UpdatedAt, UUID: value.UUID, Username: value.Username, NickName: value.NickName, HeaderImg: value.HeaderImg, AuthorityID: value.AuthorityID, DeptID: value.DeptID, Phone: value.Phone, Email: value.Email, Enable: value.Enable}
+ }
+ return result
+}
func (r *auditRepo) FindOperation(ctx context.Context, id uint) (*biz.OperationRecord, error) {
var po operationPO
if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil {
diff --git a/internal/data/pagination.go b/internal/data/pagination.go
new file mode 100644
index 0000000..40c8c71
--- /dev/null
+++ b/internal/data/pagination.go
@@ -0,0 +1,16 @@
+package data
+
+import "gorm.io/gorm"
+
+func applyPagination(db *gorm.DB, page, size, maxSize int) *gorm.DB {
+ if size <= 0 {
+ return db
+ }
+ if maxSize > 0 && size > maxSize {
+ size = maxSize
+ }
+ if page <= 0 {
+ page = 1
+ }
+ return db.Offset((page - 1) * size).Limit(size)
+}
diff --git a/internal/data/parameter.go b/internal/data/parameter.go
index ed972af..03722e7 100644
--- a/internal/data/parameter.go
+++ b/internal/data/parameter.go
@@ -52,14 +52,11 @@ func (r *settingsRepo) FindParameter(ctx context.Context, id uint, key string) (
return parameterFromPO(po), nil
}
func (r *settingsRepo) ListParameters(ctx context.Context, page, size int, q *biz.SystemParameter) ([]*biz.SystemParameter, int64, error) {
- if page < 1 {
- page = 1
- }
- if size < 1 {
- size = 10
- }
db := r.data.gormDB.WithContext(ctx).Model(¶meterPO{})
if q != nil {
+ if q.StartCreatedAt != nil && q.EndCreatedAt != nil {
+ db = db.Where("created_at BETWEEN ? AND ?", q.StartCreatedAt, q.EndCreatedAt)
+ }
if q.Name != "" {
db = db.Where("name LIKE ?", "%"+q.Name+"%")
}
@@ -72,7 +69,7 @@ func (r *settingsRepo) ListParameters(ctx context.Context, page, size int, q *bi
return nil, 0, err
}
var pos []parameterPO
- if err := db.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
+ if err := applyPagination(db, page, size, 100).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.SystemParameter, 0, len(pos))
diff --git a/internal/data/position.go b/internal/data/position.go
index 607e46b..e4d68c1 100644
--- a/internal/data/position.go
+++ b/internal/data/position.go
@@ -60,13 +60,7 @@ func (r *organizationRepo) FindPosition(ctx context.Context, id uint) (*biz.Posi
}
return posFromPO(po), nil
}
-func (r *organizationRepo) ListPositions(ctx context.Context, page, size int, q *biz.Position) ([]*biz.Position, int64, error) {
- if page < 1 {
- page = 1
- }
- if size < 1 {
- size = 10
- }
+func (r *organizationRepo) ListPositions(ctx context.Context, page, size int, q *biz.PositionListFilter) ([]*biz.Position, int64, error) {
db := r.data.gormDB.WithContext(ctx).Model(&positionPO{})
if q != nil {
if q.Name != "" {
@@ -75,13 +69,16 @@ func (r *organizationRepo) ListPositions(ctx context.Context, page, size int, q
if q.Code != "" {
db = db.Where("code LIKE ?", "%"+q.Code+"%")
}
+ if q.Status != nil {
+ db = db.Where("status = ?", *q.Status)
+ }
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
var pos []positionPO
- if err := db.Order("sort,id").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
+ if err := applyPagination(db.Order("sort"), page, size, 0).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.Position, 0, len(pos))
@@ -96,6 +93,9 @@ func (r *organizationRepo) PositionUserIDs(ctx context.Context, id uint) ([]uint
return ids, err
}
func (r *organizationRepo) SetPositionUsers(ctx context.Context, id uint, ids []uint) error {
+ if id == 0 {
+ return errors.New("岗位ID不能为空")
+ }
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("sys_position_id = ?", id).Delete(&userPositionPO{}).Error; err != nil {
return err
diff --git a/internal/data/runtime_clients.go b/internal/data/runtime_clients.go
new file mode 100644
index 0000000..2995d81
--- /dev/null
+++ b/internal/data/runtime_clients.go
@@ -0,0 +1,104 @@
+package data
+
+import (
+ "context"
+ "sync"
+ "sync/atomic"
+
+ "github.com/redis/go-redis/v9"
+ "gorm.io/gorm"
+)
+
+// reloadableDB makes the pointer swap atomic. Replaced pools are retained
+// until application shutdown so in-flight GORM operations remain valid.
+type reloadableDB struct {
+ current atomic.Pointer[gorm.DB]
+ mu sync.Mutex
+ retired []*gorm.DB
+}
+
+func newReloadableDB(db *gorm.DB) *reloadableDB {
+ r := &reloadableDB{}
+ r.current.Store(db)
+ return r
+}
+
+func (r *reloadableDB) WithContext(ctx context.Context) *gorm.DB {
+ return r.current.Load().WithContext(ctx)
+}
+
+func (r *reloadableDB) DB() *gorm.DB { return r.current.Load() }
+
+func (r *reloadableDB) replace(db *gorm.DB) {
+ old := r.current.Swap(db)
+ if old != nil && old != db {
+ r.mu.Lock()
+ r.retired = append(r.retired, old)
+ r.mu.Unlock()
+ }
+}
+
+func (r *reloadableDB) close() {
+ current := r.current.Load()
+ r.mu.Lock()
+ all := append([]*gorm.DB{current}, r.retired...)
+ r.retired = nil
+ r.mu.Unlock()
+ seen := map[*gorm.DB]struct{}{}
+ for _, db := range all {
+ if db == nil {
+ continue
+ }
+ if _, ok := seen[db]; ok {
+ continue
+ }
+ seen[db] = struct{}{}
+ if sqlDB, err := db.DB(); err == nil {
+ _ = sqlDB.Close()
+ }
+ }
+}
+
+type reloadableRedis struct {
+ current atomic.Pointer[redis.Client]
+ mu sync.Mutex
+ retired []*redis.Client
+}
+
+func newReloadableRedis(client *redis.Client) *reloadableRedis {
+ r := &reloadableRedis{}
+ if client != nil {
+ r.current.Store(client)
+ }
+ return r
+}
+
+func (r *reloadableRedis) load() *redis.Client { return r.current.Load() }
+
+func (r *reloadableRedis) replace(client *redis.Client) {
+ old := r.current.Swap(client)
+ if old != nil && old != client {
+ r.mu.Lock()
+ r.retired = append(r.retired, old)
+ r.mu.Unlock()
+ }
+}
+
+func (r *reloadableRedis) close() {
+ current := r.current.Load()
+ r.mu.Lock()
+ all := append([]*redis.Client{current}, r.retired...)
+ 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_init.go b/internal/data/system_init.go
index d7c67e7..2967abe 100644
--- a/internal/data/system_init.go
+++ b/internal/data/system_init.go
@@ -19,12 +19,37 @@ import (
func (r *systemRepo) PersistConfig(context.Context) error { return r.data.persistConfig() }
func (r *systemRepo) PersistAdminConfig(_ context.Context, raw []byte) error {
- next := proto.Clone(r.data.admin).(*conf.AdminBackend)
+ currentData, currentAdmin := r.data.runtime.Values()
+ next := proto.Clone(currentAdmin).(*conf.AdminBackend)
if err := (protojson.UnmarshalOptions{DiscardUnknown: true}).Unmarshal(raw, next); err != nil {
return err
}
- next.ConfigPath = r.data.admin.ConfigPath
- return r.data.persistConfigValues(r.data.config, next)
+ next.ConfigPath = currentAdmin.ConfigPath
+ if err := r.data.persistConfigValues(currentData, next); err != nil {
+ return err
+ }
+ // Writing through the management API updates the same in-memory values
+ // immediately; the file watcher remains the fallback for external edits.
+ r.data.runtime.Replace(currentData, next)
+ return nil
+}
+func (r *systemRepo) PersistRuntimeConfig(_ context.Context, dataRaw, adminRaw []byte) error {
+ currentData, currentAdmin := r.data.runtime.Values()
+ nextData := proto.Clone(currentData).(*conf.Data)
+ nextAdmin := proto.Clone(currentAdmin).(*conf.AdminBackend)
+ options := protojson.UnmarshalOptions{DiscardUnknown: true}
+ if err := options.Unmarshal(dataRaw, nextData); err != nil {
+ return err
+ }
+ if err := options.Unmarshal(adminRaw, nextAdmin); err != nil {
+ return err
+ }
+ nextAdmin.ConfigPath = currentAdmin.ConfigPath
+ if err := r.data.persistConfigValues(nextData, nextAdmin); err != nil {
+ return err
+ }
+ r.data.runtime.Replace(nextData, nextAdmin)
+ return nil
}
func (r *systemRepo) ReloadConfig(ctx context.Context) error {
return r.data.reloadConfig(ctx)
@@ -161,9 +186,10 @@ func (r *systemRepo) Initialize(ctx context.Context, input *biz.DatabaseConfig)
}); err != nil {
return err
}
- nextData := proto.Clone(r.data.config).(*conf.Data)
+ currentData, currentAdmin := r.data.runtime.Values()
+ nextData := proto.Clone(currentData).(*conf.Data)
nextData.Database = config
- if err := r.data.persistConfigValues(nextData, r.data.admin); err != nil {
+ if err := r.data.persistConfigValues(nextData, currentAdmin); err != nil {
return fmt.Errorf("persist database configuration: %w", err)
}
r.data.activateDatabase(candidate, config)
diff --git a/internal/data/task.go b/internal/data/task.go
index 2a68e39..395a563 100644
--- a/internal/data/task.go
+++ b/internal/data/task.go
@@ -83,12 +83,6 @@ func (r *taskRepo) FindTask(ctx context.Context, id uint) (*biz.TimedTask, error
return taskFromPO(po), nil
}
func (r *taskRepo) ListTasks(ctx context.Context, page, size int, q *biz.TimedTask) ([]*biz.TimedTask, int64, error) {
- if page < 1 {
- page = 1
- }
- if size < 1 {
- size = 10
- }
db := r.data.gormDB.WithContext(ctx).Model(&taskPO{})
if q != nil {
if q.Name != "" {
@@ -106,7 +100,7 @@ func (r *taskRepo) ListTasks(ctx context.Context, page, size int, q *biz.TimedTa
return nil, 0, err
}
var pos []taskPO
- if err := db.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
+ if err := applyPagination(db.Order("id desc"), page, size, 100).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.TimedTask, 0, len(pos))
@@ -125,12 +119,6 @@ func taskLogFromPO(v taskLogPO) *biz.TimedTaskLog {
return &biz.TimedTaskLog{ID: v.ID, CreatedAt: v.CreatedAt, TaskID: v.TaskID, TaskName: v.TaskName, TriggerType: v.TriggerType, StartedAt: v.StartedAt, FinishedAt: v.FinishedAt, DurationMS: v.DurationMS, Status: v.Status, ErrorMsg: v.ErrorMsg, Output: v.Output}
}
func (r *taskRepo) ListTaskLogs(ctx context.Context, page, size int, taskID uint, status string) ([]*biz.TimedTaskLog, int64, error) {
- if page < 1 {
- page = 1
- }
- if size < 1 {
- size = 10
- }
db := r.data.gormDB.WithContext(ctx).Model(&taskLogPO{})
if taskID != 0 {
db = db.Where("task_id = ?", taskID)
@@ -143,7 +131,7 @@ func (r *taskRepo) ListTaskLogs(ctx context.Context, page, size int, taskID uint
return nil, 0, err
}
var pos []taskLogPO
- if err := db.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
+ if err := applyPagination(db.Order("id desc"), page, size, 100).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.TimedTaskLog, 0, len(pos))
diff --git a/internal/data/transactions_test.go b/internal/data/transactions_test.go
new file mode 100644
index 0000000..e2d4f86
--- /dev/null
+++ b/internal/data/transactions_test.go
@@ -0,0 +1,83 @@
+package data
+
+import (
+ "context"
+ "strings"
+ "testing"
+
+ "kra/internal/biz"
+ "kra/internal/conf"
+)
+
+func newTransactionTestData(t *testing.T) *Data {
+ t.Helper()
+ name := strings.NewReplacer("/", "_", " ", "_").Replace(t.Name())
+ db, err := openWithDriver("sqlite", "file:"+name+"?mode=memory&cache=shared")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err = migrateAll(db); err != nil {
+ t.Fatal(err)
+ }
+ data := &Data{gormDB: newReloadableDB(db), redis: newReloadableRedis(nil), runtime: conf.NewRuntime(&conf.Data{Database: &conf.Data_Database{Driver: "sqlite"}}, &conf.AdminBackend{})}
+ t.Cleanup(func() { data.gormDB.close() })
+ return data
+}
+
+func TestUserAuthorityWritesAreAtomic(t *testing.T) {
+ data := newTransactionTestData(t)
+ ctx := context.Background()
+ if err := data.gormDB.WithContext(ctx).Create(&[]authorityPO{{AuthorityID: 888, AuthorityName: "admin"}, {AuthorityID: 999, AuthorityName: "operator"}}).Error; err != nil {
+ t.Fatal(err)
+ }
+ repo := &systemRepo{data: data}
+ user := &biz.User{Username: "atomic", Password: "hash", NickName: "before", AuthorityID: 888, Enable: 1}
+ created, err := repo.CreateUserWithAuthorities(ctx, user, []uint{888, 999})
+ if err != nil {
+ t.Fatal(err)
+ }
+ var links int64
+ if err = data.gormDB.WithContext(ctx).Model(&userAuthorityPO{}).Where("sys_user_id = ?", created.ID).Count(&links).Error; err != nil || links != 2 {
+ t.Fatalf("authority links = %d, err = %v", links, err)
+ }
+
+ created.NickName = "must rollback"
+ if err = repo.UpdateUserWithAuthorities(ctx, created, []uint{888, 123456}); err == nil {
+ t.Fatal("expected invalid authority update to fail")
+ }
+ var persisted userPO
+ if err = data.gormDB.WithContext(ctx).First(&persisted, created.ID).Error; err != nil {
+ t.Fatal(err)
+ }
+ if persisted.NickName != "before" {
+ t.Fatalf("user update was not rolled back: %q", persisted.NickName)
+ }
+
+ invalid := &biz.User{Username: "invalid", Password: "hash", AuthorityID: 888, Enable: 1}
+ if _, err = repo.CreateUserWithAuthorities(ctx, invalid, []uint{888, 123456}); err == nil {
+ t.Fatal("expected invalid authority create to fail")
+ }
+ var users int64
+ if err = data.gormDB.WithContext(ctx).Model(&userPO{}).Where("username = ?", "invalid").Count(&users).Error; err != nil || users != 0 {
+ t.Fatalf("partial user persisted: count=%d err=%v", users, err)
+ }
+}
+
+func TestDictionaryImportKeepsHierarchyInOneTransaction(t *testing.T) {
+ data := newTransactionTestData(t)
+ ctx := context.Background()
+ repo := &settingsRepo{data: data}
+ parentID := uint(10)
+ dictionary := &biz.Dictionary{Name: "status", Type: "status", Status: true}
+ details := []*biz.DictionaryDetail{{ID: 10, Label: "parent", Value: "1", Status: true}, {ID: 11, Label: "child", Value: "2", ParentID: &parentID, Status: true}}
+ if err := repo.ImportDictionary(ctx, dictionary, details); err != nil {
+ t.Fatal(err)
+ }
+ var persisted []dictionaryDetailPO
+ if err := data.gormDB.WithContext(ctx).Where("sys_dictionary_id = ?", dictionary.ID).Order("id").Find(&persisted).Error; err != nil {
+ t.Fatal(err)
+ }
+ if len(persisted) != 2 || persisted[1].ParentID == nil || *persisted[1].ParentID != persisted[0].ID {
+ t.Fatalf("unexpected imported hierarchy: %+v", persisted)
+ }
+}
diff --git a/internal/data/user.go b/internal/data/user.go
index d21a893..bcdf018 100644
--- a/internal/data/user.go
+++ b/internal/data/user.go
@@ -4,6 +4,8 @@ import (
"context"
"encoding/json"
"errors"
+ "strconv"
+ "strings"
"time"
"kra/internal/biz"
@@ -80,6 +82,119 @@ func toBizAuthority(po authorityPO) biz.Authority {
return biz.Authority{AuthorityID: po.AuthorityID, AuthorityName: po.AuthorityName, ParentID: po.ParentID, DataScope: po.DataScope, DefaultRouter: po.DefaultRouter}
}
+func baseBizUser(po *userPO) *biz.User {
+ setting := map[string]any(nil)
+ if po.OriginSetting != "" {
+ _ = json.Unmarshal([]byte(po.OriginSetting), &setting)
+ }
+ return &biz.User{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, UUID: po.UUID, Username: po.Username, Password: po.Password, NickName: po.NickName, HeaderImg: po.HeaderImg, AuthorityID: po.AuthorityID, DeptID: po.DeptID, Phone: po.Phone, Email: po.Email, Enable: po.Enable, OriginSetting: setting, MustChangePassword: po.MustChangePassword, PasswordUpdatedAt: po.PasswordUpdatedAt}
+}
+
+func (r *systemRepo) toBizUsers(ctx context.Context, pos []userPO) ([]*biz.User, error) {
+ if len(pos) == 0 {
+ return []*biz.User{}, nil
+ }
+ db := r.data.gormDB.WithContext(ctx)
+ userIDs := make([]uint, 0, len(pos))
+ authorityIDs := make([]uint, 0, len(pos))
+ departmentIDs := make([]uint, 0, len(pos))
+ for i := range pos {
+ userIDs = append(userIDs, pos[i].ID)
+ authorityIDs = append(authorityIDs, pos[i].AuthorityID)
+ if pos[i].DeptID != 0 {
+ departmentIDs = append(departmentIDs, pos[i].DeptID)
+ }
+ }
+ var authorityLinks []userAuthorityPO
+ if err := db.Where("sys_user_id IN ?", userIDs).Find(&authorityLinks).Error; err != nil {
+ return nil, err
+ }
+ authorityByUser := make(map[uint][]uint, len(pos))
+ for _, link := range authorityLinks {
+ authorityByUser[link.SysUserID] = append(authorityByUser[link.SysUserID], link.SysAuthorityAuthorityID)
+ authorityIDs = append(authorityIDs, link.SysAuthorityAuthorityID)
+ }
+ var authorityPOs []authorityPO
+ if err := db.Where("authority_id IN ?", authorityIDs).Find(&authorityPOs).Error; err != nil {
+ return nil, err
+ }
+ authorityByID := make(map[uint]authorityPO, len(authorityPOs))
+ for _, item := range authorityPOs {
+ authorityByID[item.AuthorityID] = item
+ }
+
+ var departmentLinks []userDepartmentPO
+ if err := db.Where("sys_user_id IN ?", userIDs).Find(&departmentLinks).Error; err != nil {
+ return nil, err
+ }
+ departmentByUser := make(map[uint][]uint, len(pos))
+ for _, link := range departmentLinks {
+ departmentByUser[link.UserID] = append(departmentByUser[link.UserID], link.DepartmentID)
+ departmentIDs = append(departmentIDs, link.DepartmentID)
+ }
+ var departmentPOs []departmentPO
+ if len(departmentIDs) > 0 {
+ if err := db.Where("id IN ?", departmentIDs).Find(&departmentPOs).Error; err != nil {
+ return nil, err
+ }
+ }
+ departmentByID := make(map[uint]departmentPO, len(departmentPOs))
+ for _, item := range departmentPOs {
+ departmentByID[item.ID] = item
+ }
+
+ var positionLinks []userPositionPO
+ if err := db.Where("sys_user_id IN ?", userIDs).Find(&positionLinks).Error; err != nil {
+ return nil, err
+ }
+ positionByUser := make(map[uint][]uint, len(pos))
+ positionIDs := make([]uint, 0, len(positionLinks))
+ for _, link := range positionLinks {
+ positionByUser[link.UserID] = append(positionByUser[link.UserID], link.PositionID)
+ positionIDs = append(positionIDs, link.PositionID)
+ }
+ var positionPOs []positionPO
+ if len(positionIDs) > 0 {
+ if err := db.Where("id IN ?", positionIDs).Find(&positionPOs).Error; err != nil {
+ return nil, err
+ }
+ }
+ positionByID := make(map[uint]positionPO, len(positionPOs))
+ for _, item := range positionPOs {
+ positionByID[item.ID] = item
+ }
+
+ users := make([]*biz.User, 0, len(pos))
+ for i := range pos {
+ po := &pos[i]
+ user := baseBizUser(po)
+ user.Authority = toBizAuthority(authorityByID[po.AuthorityID])
+ user.Authorities = make([]biz.Authority, 0, len(authorityByUser[po.ID]))
+ for _, id := range authorityByUser[po.ID] {
+ if item, ok := authorityByID[id]; ok {
+ user.Authorities = append(user.Authorities, toBizAuthority(item))
+ }
+ }
+ user.Departments = make([]biz.Department, 0, len(departmentByUser[po.ID]))
+ for _, id := range departmentByUser[po.ID] {
+ if item, ok := departmentByID[id]; ok {
+ user.Departments = append(user.Departments, *deptFromPO(item))
+ }
+ }
+ if primary, ok := departmentByID[po.DeptID]; ok {
+ user.Department = deptFromPO(primary)
+ }
+ user.Positions = make([]biz.Position, 0, len(positionByUser[po.ID]))
+ for _, id := range positionByUser[po.ID] {
+ if item, ok := positionByID[id]; ok {
+ user.Positions = append(user.Positions, *posFromPO(item))
+ }
+ }
+ users = append(users, user)
+ }
+ return users, nil
+}
+
func (r *systemRepo) MenusByAuthority(ctx context.Context, authorityID uint) ([]*biz.Menu, error) {
var pos []menuPO
err := r.data.gormDB.WithContext(ctx).Table("sys_base_menus").
@@ -90,23 +205,31 @@ func (r *systemRepo) MenusByAuthority(ctx context.Context, authorityID uint) ([]
return nil, err
}
byID := make(map[uint]*biz.Menu, len(pos))
+ menuIDs := make([]uint, 0, len(pos))
for _, po := range pos {
menu := &biz.Menu{ID: po.ID, ParentID: po.ParentID, Path: po.Path, Name: po.Name, Hidden: po.Hidden, Component: po.Component, Sort: po.Sort, ActiveName: po.ActiveName, KeepAlive: po.KeepAlive, DefaultMenu: po.DefaultMenu, Title: po.Title, Icon: po.Icon, CloseTab: po.CloseTab, TransitionType: po.TransitionType, Children: []*biz.Menu{}}
+ menu.Parameters = []*biz.MenuParameter{}
+ menu.Buttons = []*biz.MenuButton{}
+ byID[po.ID] = menu
+ menuIDs = append(menuIDs, po.ID)
+ }
+ if len(menuIDs) > 0 {
var parameters []menuParameterPO
- if err := r.data.gormDB.WithContext(ctx).Where("sys_base_menu_id = ?", po.ID).Order("id").Find(¶meters).Error; err != nil {
+ if err := r.data.gormDB.WithContext(ctx).Where("sys_base_menu_id IN ?", menuIDs).Order("id").Find(¶meters).Error; err != nil {
return nil, err
}
for _, parameter := range parameters {
+ menu := byID[parameter.MenuID]
menu.Parameters = append(menu.Parameters, &biz.MenuParameter{ID: parameter.ID, MenuID: parameter.MenuID, Type: parameter.Type, Key: parameter.Key, Value: parameter.Value})
}
var buttons []menuButtonPO
- if err := r.data.gormDB.WithContext(ctx).Table("sys_base_menu_btns").Joins("JOIN sys_authority_btns ON sys_authority_btns.sys_base_menu_btn_id = sys_base_menu_btns.id").Where("sys_authority_btns.authority_id = ? AND sys_authority_btns.sys_menu_id = ?", authorityID, po.ID).Find(&buttons).Error; err != nil {
+ if err := r.data.gormDB.WithContext(ctx).Table("sys_base_menu_btns").Joins("JOIN sys_authority_btns ON sys_authority_btns.sys_base_menu_btn_id = sys_base_menu_btns.id").Where("sys_authority_btns.authority_id = ? AND sys_authority_btns.sys_menu_id IN ?", authorityID, menuIDs).Find(&buttons).Error; err != nil {
return nil, err
}
for _, button := range buttons {
+ menu := byID[button.MenuID]
menu.Buttons = append(menu.Buttons, &biz.MenuButton{ID: button.ID, Name: button.Name, Description: button.Description, MenuID: button.MenuID, AuthorityID: authorityID})
}
- byID[po.ID] = menu
}
roots := make([]*biz.Menu, 0)
for _, po := range pos {
@@ -121,22 +244,7 @@ func (r *systemRepo) MenusByAuthority(ctx context.Context, authorityID uint) ([]
}
func (r *systemRepo) ListUsers(ctx context.Context, page, pageSize int, filter *biz.UserListFilter) ([]*biz.User, int64, error) {
- if page < 1 {
- page = 1
- }
- if pageSize < 1 {
- pageSize = 10
- }
db := r.data.gormDB.WithContext(ctx).Model(&userPO{})
- if scope, ok := biz.DataScopeFromContext(ctx); ok && !scope.All {
- if scope.OwnerUserID != 0 {
- db = db.Where("sys_users.id = ?", scope.OwnerUserID)
- } else if len(scope.DepartmentIDs) > 0 {
- db = db.Joins("JOIN sys_user_departments ON sys_user_departments.sys_user_id = sys_users.id").Where("sys_user_departments.sys_department_id IN ?", scope.DepartmentIDs).Distinct("sys_users.id")
- } else {
- db = db.Where("1 = 0")
- }
- }
order := "id desc"
if filter != nil {
if filter.Username != "" {
@@ -151,7 +259,7 @@ func (r *systemRepo) ListUsers(ctx context.Context, page, pageSize int, filter *
if filter.Email != "" {
db = db.Where("email LIKE ?", "%"+filter.Email+"%")
}
- allowed := map[string]bool{"id": true, "username": true, "nick_name": true, "phone": true, "email": true, "created_at": true}
+ allowed := map[string]bool{"id": true, "username": true, "nick_name": true, "phone": true, "email": true}
if allowed[filter.OrderKey] {
order = filter.OrderKey
if filter.Desc {
@@ -164,21 +272,18 @@ func (r *systemRepo) ListUsers(ctx context.Context, page, pageSize int, filter *
return nil, 0, err
}
var pos []userPO
- if err := db.Order(order).Offset((page - 1) * pageSize).Limit(pageSize).Find(&pos).Error; err != nil {
+ if err := applyPagination(db.Order(order), page, pageSize, 100).Find(&pos).Error; err != nil {
return nil, 0, err
}
- users := make([]*biz.User, 0, len(pos))
- for i := range pos {
- user, err := r.toBizUser(ctx, &pos[i])
- if err != nil {
- return nil, 0, err
- }
- users = append(users, user)
- }
- return users, total, nil
+ users, err := r.toBizUsers(ctx, pos)
+ return users, total, err
}
func (r *systemRepo) CreateUser(ctx context.Context, user *biz.User) (*biz.User, error) {
+ return r.CreateUserWithAuthorities(ctx, user, nil)
+}
+
+func (r *systemRepo) CreateUserWithAuthorities(ctx context.Context, user *biz.User, authorityIDs []uint) (*biz.User, error) {
if user.UUID == "" {
user.UUID = uuid.NewString()
}
@@ -193,23 +298,134 @@ func (r *systemRepo) CreateUser(ctx context.Context, user *biz.User) (*biz.User,
}
now := time.Now()
po := userPO{UUID: user.UUID, Username: user.Username, Password: user.Password, NickName: user.NickName, HeaderImg: user.HeaderImg, AuthorityID: user.AuthorityID, Phone: user.Phone, Email: user.Email, Enable: user.Enable, PasswordUpdatedAt: &now, MustChangePassword: user.MustChangePassword}
+ if len(authorityIDs) == 0 {
+ authorityIDs = []uint{user.AuthorityID}
+ }
if err := r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ if err := validateAuthorities(tx, authorityIDs); err != nil {
+ return err
+ }
if err := tx.Create(&po).Error; err != nil {
return err
}
- return tx.Create(&userAuthorityPO{SysUserID: po.ID, SysAuthorityAuthorityID: po.AuthorityID}).Error
+ links := make([]userAuthorityPO, 0, len(authorityIDs))
+ for _, authorityID := range authorityIDs {
+ links = append(links, userAuthorityPO{SysUserID: po.ID, SysAuthorityAuthorityID: authorityID})
+ }
+ return tx.Create(&links).Error
}); err != nil {
return nil, err
}
return r.FindUserByID(ctx, po.ID)
}
+func validateAuthorities(tx *gorm.DB, authorityIDs []uint) error {
+ for _, authorityID := range authorityIDs {
+ var count int64
+ if err := tx.Model(&authorityPO{}).Where("authority_id = ?", authorityID).Count(&count).Error; err != nil {
+ return err
+ }
+ if count == 0 {
+ return errors.New("角色不存在")
+ }
+ }
+ return nil
+}
+
func (r *systemRepo) UpdateUser(ctx context.Context, user *biz.User) error {
+ return r.updateUser(r.data.gormDB.WithContext(ctx), user)
+}
+
+func (r *systemRepo) UpdateSelfUser(ctx context.Context, user *biz.User) error {
+ updates := make(map[string]any)
+ if user.NickName != "" {
+ updates["nick_name"] = user.NickName
+ }
+ if user.HeaderImg != "" {
+ updates["header_img"] = user.HeaderImg
+ }
+ if user.Phone != "" {
+ updates["phone"] = user.Phone
+ }
+ if user.Email != "" {
+ updates["email"] = user.Email
+ }
+ if user.Enable != 0 {
+ updates["enable"] = user.Enable
+ }
+ if len(updates) == 0 {
+ return nil
+ }
+ return r.data.gormDB.WithContext(ctx).Model(&userPO{}).Where("id = ?", user.ID).Updates(updates).Error
+}
+
+func (r *systemRepo) FillDepartmentNamePaths(ctx context.Context, user *biz.User) error {
+ if user == nil || len(user.Departments) == 0 {
+ return nil
+ }
+ idSet := make(map[uint]bool)
+ for i := range user.Departments {
+ for _, part := range strings.Split(user.Departments[i].Ancestors, ",") {
+ id, err := strconv.ParseUint(strings.TrimSpace(part), 10, 64)
+ if err == nil && id != 0 {
+ idSet[uint(id)] = true
+ }
+ }
+ }
+ ids := make([]uint, 0, len(idSet))
+ for id := range idSet {
+ ids = append(ids, id)
+ }
+ nameByID := make(map[uint]string, len(ids))
+ if len(ids) > 0 {
+ var rows []departmentPO
+ if err := r.data.gormDB.WithContext(ctx).Select("id", "name").Where("id IN ?", ids).Find(&rows).Error; err != nil {
+ return err
+ }
+ for _, row := range rows {
+ nameByID[row.ID] = row.Name
+ }
+ }
+ for i := range user.Departments {
+ parts := make([]string, 0)
+ for _, part := range strings.Split(user.Departments[i].Ancestors, ",") {
+ id, err := strconv.ParseUint(strings.TrimSpace(part), 10, 64)
+ if err == nil && id != 0 && nameByID[uint(id)] != "" {
+ parts = append(parts, nameByID[uint(id)])
+ }
+ }
+ parts = append(parts, user.Departments[i].Name)
+ user.Departments[i].NamePath = strings.Join(parts, "/")
+ }
+ if user.Department != nil {
+ for i := range user.Departments {
+ if user.Departments[i].ID == user.Department.ID {
+ user.Department.NamePath = user.Departments[i].NamePath
+ break
+ }
+ }
+ }
+ return nil
+}
+
+func (r *systemRepo) updateUser(tx *gorm.DB, user *biz.User) error {
updates := map[string]any{"nick_name": user.NickName, "header_img": user.HeaderImg, "phone": user.Phone, "email": user.Email, "enable": user.Enable}
if user.AuthorityID != 0 {
updates["authority_id"] = user.AuthorityID
}
- return r.data.gormDB.WithContext(ctx).Model(&userPO{}).Where("id = ?", user.ID).Updates(updates).Error
+ return tx.Model(&userPO{}).Where("id = ?", user.ID).Updates(updates).Error
+}
+
+func (r *systemRepo) UpdateUserWithAuthorities(ctx context.Context, user *biz.User, authorityIDs []uint) error {
+ return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ if err := r.updateUser(tx, user); err != nil {
+ return err
+ }
+ if len(authorityIDs) == 0 {
+ return nil
+ }
+ return setUserAuthorities(tx, user.ID, authorityIDs)
+ })
}
func (r *systemRepo) DeleteUser(ctx context.Context, id uint) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
@@ -248,36 +464,60 @@ func (r *systemRepo) ListAuthorities(ctx context.Context) ([]*biz.Authority, err
func (r *systemRepo) SetUserAuthorities(ctx context.Context, id uint, authorityIDs []uint) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
- if err := tx.Where("sys_user_id = ?", id).Delete(&userAuthorityPO{}).Error; err != nil {
- return err
- }
- links := make([]userAuthorityPO, 0, len(authorityIDs))
+ access := &accessRepo{data: r.data}
for _, authorityID := range authorityIDs {
- var count int64
- if err := tx.Model(&authorityPO{}).Where("authority_id = ?", authorityID).Count(&count).Error; err != nil || count == 0 {
- if err != nil {
- return err
- }
- return errors.New("角色不存在")
+ if err := access.checkAuthorityIDAuth(ctx, authorityID); err != nil {
+ return err
}
- links = append(links, userAuthorityPO{SysUserID: id, SysAuthorityAuthorityID: authorityID})
}
- if err := tx.Create(&links).Error; err != nil {
- return err
- }
- return tx.Model(&userPO{}).Where("id = ?", id).Update("authority_id", authorityIDs[0]).Error
+ return setUserAuthorities(tx, id, authorityIDs)
})
}
+func setUserAuthorities(tx *gorm.DB, id uint, authorityIDs []uint) error {
+ if len(authorityIDs) == 0 {
+ return errors.New("角色不能为空")
+ }
+ if err := validateAuthorities(tx, authorityIDs); err != nil {
+ return err
+ }
+ if err := tx.Where("sys_user_id = ?", id).Delete(&userAuthorityPO{}).Error; err != nil {
+ return err
+ }
+ links := make([]userAuthorityPO, 0, len(authorityIDs))
+ for _, authorityID := range authorityIDs {
+ links = append(links, userAuthorityPO{SysUserID: id, SysAuthorityAuthorityID: authorityID})
+ }
+ if err := tx.Create(&links).Error; err != nil {
+ return err
+ }
+ return tx.Model(&userPO{}).Where("id = ?", id).Update("authority_id", authorityIDs[0]).Error
+}
+
func (r *systemRepo) SetUserAuthority(ctx context.Context, id, authorityID uint) error {
+ db := r.data.gormDB.WithContext(ctx)
var count int64
- if err := r.data.gormDB.WithContext(ctx).Model(&userAuthorityPO{}).Where("sys_user_id = ? AND sys_authority_authority_id = ?", id, authorityID).Count(&count).Error; err != nil {
+ if err := db.Model(&userAuthorityPO{}).Where("sys_user_id = ? AND sys_authority_authority_id = ?", id, authorityID).Count(&count).Error; err != nil {
return err
}
if count == 0 {
return errors.New("该用户无此角色")
}
- return r.data.gormDB.WithContext(ctx).Model(&userPO{}).Where("id = ?", id).Update("authority_id", authorityID).Error
+ var authority authorityPO
+ if err := db.Where("authority_id = ?", authorityID).First(&authority).Error; err != nil {
+ return err
+ }
+ var menuCount int64
+ if err := db.Model(&menuPO{}).
+ Joins("JOIN sys_authority_menus ON sys_authority_menus.sys_base_menu_id = sys_base_menus.id").
+ Where("sys_authority_menus.sys_authority_authority_id = ? AND sys_base_menus.name = ?", authorityID, authority.DefaultRouter).
+ Count(&menuCount).Error; err != nil {
+ return err
+ }
+ if menuCount == 0 {
+ return errors.New("找不到默认路由,无法切换本角色")
+ }
+ return db.Model(&userPO{}).Where("id = ?", id).Update("authority_id", authorityID).Error
}
func (r *systemRepo) SetUserSetting(ctx context.Context, id uint, setting map[string]any) error {
diff --git a/internal/data/version.go b/internal/data/version.go
index d5a3ec5..8f45dfd 100644
--- a/internal/data/version.go
+++ b/internal/data/version.go
@@ -47,14 +47,11 @@ func (r *versionRepo) FindVersion(ctx context.Context, id uint) (*biz.Version, e
}
return versionFromPO(po), nil
}
-func (r *versionRepo) ListVersions(ctx context.Context, page, size int, name, code string) ([]*biz.Version, int64, error) {
- if page < 1 {
- page = 1
- }
- if size < 1 {
- size = 10
- }
+func (r *versionRepo) ListVersions(ctx context.Context, page, size int, name, code string, createdAtRange []*time.Time) ([]*biz.Version, int64, error) {
db := r.data.gormDB.WithContext(ctx).Model(&versionPO{})
+ if len(createdAtRange) == 2 && createdAtRange[0] != nil && createdAtRange[1] != nil {
+ db = db.Where("created_at BETWEEN ? AND ?", createdAtRange[0], createdAtRange[1])
+ }
if name != "" {
db = db.Where("version_name LIKE ?", "%"+name+"%")
}
@@ -66,7 +63,7 @@ func (r *versionRepo) ListVersions(ctx context.Context, page, size int, name, co
return nil, 0, err
}
var pos []versionPO
- if err := db.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
+ if err := applyPagination(db, page, size, 100).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.Version, 0, len(pos))
@@ -94,14 +91,45 @@ func (r *versionRepo) BuildVersionBundle(ctx context.Context, menuIDs, apiIDs, d
}
byID[po.ID] = v
}
+ if len(menuIDs) > 0 {
+ var parameters []menuParameterPO
+ if err := r.data.gormDB.WithContext(ctx).Where("sys_base_menu_id IN ?", menuIDs).Find(¶meters).Error; err != nil {
+ return nil, err
+ }
+ for _, parameter := range parameters {
+ if menu := byID[parameter.MenuID]; menu != nil {
+ menu.Parameters = append(menu.Parameters, &biz.MenuParameter{Type: parameter.Type, Key: parameter.Key, Value: parameter.Value})
+ }
+ }
+ }
for _, po := range menus {
v := byID[po.ID]
if parent := byID[po.ParentID]; parent != nil {
parent.Children = append(parent.Children, v)
- } else {
+ } else if po.ParentID == 0 {
bundle.Menus = append(bundle.Menus, v)
}
}
+ var cleanMenu func(*biz.Menu)
+ cleanMenu = func(menu *biz.Menu) {
+ menu.ID = 0
+ menu.ParentID = 0
+ for _, button := range menu.Buttons {
+ button.ID = 0
+ button.MenuID = 0
+ button.AuthorityID = 0
+ }
+ for _, parameter := range menu.Parameters {
+ parameter.ID = 0
+ parameter.MenuID = 0
+ }
+ for _, child := range menu.Children {
+ cleanMenu(child)
+ }
+ }
+ for _, menu := range bundle.Menus {
+ cleanMenu(menu)
+ }
var apis []apiPO
if len(apiIDs) > 0 {
if err := r.data.gormDB.WithContext(ctx).Where("id IN ?", apiIDs).Find(&apis).Error; err != nil {
@@ -123,7 +151,7 @@ func (r *versionRepo) BuildVersionBundle(ctx context.Context, menuIDs, apiIDs, d
for _, po := range dictionaries {
v := dictionaryFromPO(po)
v.ID = 0
- details, _, err := settings.ListDictionaryDetails(ctx, 1, 100000, po.ID, "", "")
+ details, _, err := settings.ListDictionaryDetails(ctx, 0, 0, biz.DictionaryDetailFilter{DictionaryID: po.ID})
if err != nil {
return nil, err
}
@@ -159,6 +187,11 @@ func (r *versionRepo) ImportVersionBundle(ctx context.Context, b *biz.VersionBun
return err
}
}
+ for _, parameter := range item.Parameters {
+ if err = tx.Create(&menuParameterPO{MenuID: po.ID, Type: parameter.Type, Key: parameter.Key, Value: parameter.Value}).Error; err != nil {
+ return err
+ }
+ }
} else if err != nil {
return err
}
diff --git a/internal/server/gin.go b/internal/server/gin.go
index 01c85dd..d88886b 100644
--- a/internal/server/gin.go
+++ b/internal/server/gin.go
@@ -1,13 +1,13 @@
package server
import (
- "context"
- "errors"
"log/slog"
- "net"
+ "mime"
"net/http"
"os"
+ "path"
"strings"
+ "time"
"kra/internal/conf"
"kra/internal/server/handler"
@@ -17,23 +17,16 @@ import (
"kra/internal/service"
"github.com/gin-gonic/gin"
+ kratoshttp "github.com/go-kratos/kratos/v3/transport/http"
)
-// GinServer adapts Gin to the Kratos transport.Server lifecycle.
-type GinServer struct {
- network string
- address string
- server *http.Server
- listener net.Listener
- logger *slog.Logger
-}
-
-func NewGinServer(c *conf.Server, config *conf.AdminBackend, 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) *GinServer {
+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 {
gin.SetMode(gin.ReleaseMode)
engine := gin.New()
engine.Use(servermiddleware.RequestMeta(), servermiddleware.EmailErrorAlert(emails, logger), gin.Recovery(), servermiddleware.SecurityRateLimit(system, settings), servermiddleware.OperationAudit(audit))
prefix := ""
+ config := runtime.Admin()
if config != nil {
prefix = strings.TrimSuffix(config.RouterPrefix, "/")
}
@@ -42,13 +35,13 @@ func NewGinServer(c *conf.Server, config *conf.AdminBackend, system *service.Sys
serverrouter.RegisterPublic(public, engine, publicHandler)
private := engine.Group(prefix)
- private.Use(servermiddleware.Auth(config, settings), servermiddleware.MustChangePassword(), servermiddleware.AccessControl(access, audit))
+ private.Use(servermiddleware.Auth(runtime, settings), servermiddleware.MustChangePassword(), servermiddleware.AccessControl(runtime, access, audit))
serverrouter.RegisterUser(private, user)
serverrouter.RegisterNavigation(private, navigation)
serverrouter.RegisterSession(private, session)
serverrouter.RegisterAuthority(private, authority)
serverrouter.RegisterMenu(private, menu)
- serverrouter.RegisterAPI(private, engine, api)
+ serverrouter.RegisterAPI(private, public, engine, api)
serverrouter.RegisterPermission(private, permission)
serverrouter.RegisterOrganization(private, organization)
serverrouter.RegisterDictionary(private, dictionary)
@@ -64,7 +57,7 @@ func NewGinServer(c *conf.Server, config *conf.AdminBackend, system *service.Sys
serverrouter.RegisterEmail(private, email)
engine.NoRoute(func(c *gin.Context) {
- if serveLocalStorage(c, config) {
+ if serveLocalStorage(c, runtime) {
return
}
httpx.Fail(c, "请求的接口不存在")
@@ -79,12 +72,24 @@ func NewGinServer(c *conf.Server, config *conf.AdminBackend, system *service.Sys
address = c.Http.Addr
}
}
- return &GinServer{network: network, address: address, server: &http.Server{Addr: address, Handler: engine}, logger: logger}
+ timeout := 10 * time.Minute
+ if c != nil && c.Http != nil && c.Http.Timeout != nil && c.Http.Timeout.AsDuration() > 0 {
+ timeout = c.Http.Timeout.AsDuration()
+ }
+ server := kratoshttp.NewServer(kratoshttp.Network(network), kratoshttp.Address(address), kratoshttp.Timeout(timeout))
+ server.HandlePrefix("/", engine)
+ server.Server.ReadHeaderTimeout = 10 * time.Second
+ server.Server.ReadTimeout = timeout
+ server.Server.WriteTimeout = timeout
+ server.Server.IdleTimeout = 2 * time.Minute
+ server.Server.MaxHeaderBytes = 1 << 20
+ return server
}
// serveLocalStorage resolves the local path for every request so a config
// reload takes effect without rebuilding the Gin engine.
-func serveLocalStorage(c *gin.Context, config *conf.AdminBackend) bool {
+func serveLocalStorage(c *gin.Context, runtime *conf.Runtime) bool {
+ config := runtime.Admin()
if config == nil || config.Local == nil || config.Local.StorePath == "" {
return false
}
@@ -95,10 +100,26 @@ func serveLocalStorage(c *gin.Context, config *conf.AdminBackend) bool {
if prefix == "/" || (c.Request.URL.Path != prefix && !strings.HasPrefix(c.Request.URL.Path, prefix+"/")) {
return false
}
+ c.Header("X-Content-Type-Options", "nosniff")
+ filename := path.Base(c.Request.URL.Path)
+ if !canServeUploadInline(filename) {
+ c.Header("Content-Disposition", mime.FormatMediaType("attachment", map[string]string{"filename": filename}))
+ }
http.StripPrefix(prefix, http.FileServer(filesOnly{FileSystem: http.Dir(config.Local.StorePath)})).ServeHTTP(c.Writer, c.Request)
return true
}
+func canServeUploadInline(filename string) bool {
+ switch strings.ToLower(path.Ext(filename)) {
+ case ".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".ico", ".avif",
+ ".mp3", ".wav", ".ogg", ".m4a", ".flac", ".aac",
+ ".mp4", ".webm", ".mov", ".avi", ".mkv":
+ return true
+ default:
+ return false
+ }
+}
+
type filesOnly struct{ http.FileSystem }
func (f filesOnly) Open(name string) (http.File, error) {
@@ -117,22 +138,3 @@ func (f filesOnly) Open(name string) (http.File, error) {
}
return file, nil
}
-
-func (s *GinServer) Start(ctx context.Context) error {
- listener, err := net.Listen(s.network, s.address)
- if err != nil {
- return err
- }
- s.listener = listener
- s.server.BaseContext = func(net.Listener) context.Context { return ctx }
- s.logger.InfoContext(ctx, "Gin HTTP server listening", "addr", listener.Addr().String())
- if err := s.server.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) {
- return err
- }
- return nil
-}
-
-func (s *GinServer) Stop(ctx context.Context) error {
- s.logger.InfoContext(ctx, "Gin HTTP server stopping")
- return s.server.Shutdown(ctx)
-}
diff --git a/internal/server/handler/announcement.go b/internal/server/handler/announcement.go
index 16b7556..50f9271 100644
--- a/internal/server/handler/announcement.go
+++ b/internal/server/handler/announcement.go
@@ -88,8 +88,8 @@ func (h *Announcement) Find(c *gin.Context) {
}
func (h *Announcement) List(c *gin.Context) {
- page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
- pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
+ page, _ := strconv.Atoi(c.Query("page"))
+ pageSize, _ := strconv.Atoi(c.Query("pageSize"))
start, startErr := parseTime(c.Query("startCreatedAt"))
end, endErr := parseTime(c.Query("endCreatedAt"))
if startErr != nil || endErr != nil || (start == nil) != (end == nil) {
diff --git a/internal/server/handler/api.go b/internal/server/handler/api.go
index 0aa9710..5629c4f 100644
--- a/internal/server/handler/api.go
+++ b/internal/server/handler/api.go
@@ -1,7 +1,7 @@
package handler
import (
- "strconv"
+ "strings"
"kra/internal/server/httpx"
"kra/internal/service"
@@ -28,7 +28,7 @@ func (h *API) List(c *gin.Context) {
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功")
}
func (h *API) All(c *gin.Context) {
- items, err := h.service.AllAPI(c.Request.Context())
+ items, err := h.service.AllAPI(c.Request.Context(), true)
if err != nil {
httpx.Fail(c, "获取失败")
return
@@ -41,11 +41,12 @@ func (h *API) Create(c *gin.Context) {
httpx.Fail(c, "参数错误")
return
}
- if err := h.service.CreateAPIRequest(c.Request.Context(), &req); err != nil {
+ created, err := h.service.CreateAPIRequest(c.Request.Context(), &req)
+ if err != nil {
httpx.Fail(c, "创建失败")
return
}
- httpx.OK(c)
+ httpx.Write(c, httpx.CodeSuccess, created, "创建成功")
}
func (h *API) Update(c *gin.Context) {
var req dto.APIRequest
@@ -97,28 +98,40 @@ func (h *API) Find(c *gin.Context) {
httpx.Write(c, httpx.CodeSuccess, gin.H{"api": item}, "获取成功")
}
func (h *API) Groups(c *gin.Context) {
- items, err := h.service.AllAPI(c.Request.Context())
+ items, err := h.service.AllAPI(c.Request.Context(), false)
if err != nil {
httpx.Fail(c, "获取失败")
return
}
seen := map[string]bool{}
groups := []string{}
+ groupAPIMap := map[string]string{}
for _, item := range items {
- if item.APIGroup != "" && !seen[item.APIGroup] {
+ if !seen[item.APIGroup] {
seen[item.APIGroup] = true
groups = append(groups, item.APIGroup)
}
+ parts := strings.Split(item.Path, "/")
+ if len(parts) > 1 {
+ groupAPIMap[parts[1]] = item.APIGroup
+ }
}
- httpx.Write(c, httpx.CodeSuccess, gin.H{"groups": groups}, "获取成功")
+ httpx.Write(c, httpx.CodeSuccess, gin.H{"groups": groups, "apiGroupMap": groupAPIMap}, "获取成功")
}
func (h *API) Roles(c *gin.Context) {
- id, _ := strconv.ParseUint(c.Query("apiId"), 10, 64)
- ids, err := h.service.APIRoleIDs(c.Request.Context(), uint(id))
+ path, method := c.Query("path"), c.Query("method")
+ if path == "" || method == "" {
+ httpx.Fail(c, "API路径和请求方法不能为空")
+ return
+ }
+ ids, err := h.service.APIRoleIDs(c.Request.Context(), path, method)
if err != nil {
httpx.Fail(c, "获取失败")
return
}
+ if ids == nil {
+ ids = []uint{}
+ }
httpx.Write(c, httpx.CodeSuccess, ids, "获取成功")
}
func (h *API) SetRoles(c *gin.Context) {
@@ -127,7 +140,11 @@ func (h *API) SetRoles(c *gin.Context) {
httpx.Fail(c, "参数错误")
return
}
- if err := h.service.SetAPIRoles(c.Request.Context(), req.APIID, req.AuthorityIDs); err != nil {
+ if req.Path == "" || req.Method == "" {
+ httpx.Fail(c, "API路径和请求方法不能为空")
+ return
+ }
+ if err := h.service.SetAPIRoles(c.Request.Context(), req.Path, req.Method, req.AuthorityIDs); err != nil {
httpx.Fail(c, "设置失败")
return
}
diff --git a/internal/server/handler/audit.go b/internal/server/handler/audit.go
index 74b9e64..6b70d48 100644
--- a/internal/server/handler/audit.go
+++ b/internal/server/handler/audit.go
@@ -2,6 +2,7 @@ package handler
import (
"strconv"
+ "time"
"kra/internal/server/httpx"
"kra/internal/service"
@@ -15,8 +16,8 @@ type Audit struct{ service *service.AuditService }
func NewAudit(service *service.AuditService) *Audit { return &Audit{service: service} }
func page(c *gin.Context) (int, int) {
- value, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
- size, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
+ value, _ := strconv.Atoi(c.Query("page"))
+ size, _ := strconv.Atoi(c.Query("pageSize"))
return value, size
}
func IDsFromQuery(c *gin.Context) []uint {
@@ -37,8 +38,7 @@ func IDsFromQuery(c *gin.Context) []uint {
func (h *Audit) Operations(c *gin.Context) {
p, size := page(c)
status, _ := strconv.Atoi(c.Query("status"))
- userID, _ := strconv.ParseUint(c.Query("user_id"), 10, 64)
- items, total, err := h.service.OperationsFilter(c.Request.Context(), p, size, c.Query("path"), c.Query("method"), status, c.Query("ip"), uint(userID))
+ items, total, err := h.service.OperationsFilter(c.Request.Context(), p, size, c.Query("path"), c.Query("method"), status)
if err != nil {
httpx.Fail(c, "获取失败")
return
@@ -52,7 +52,7 @@ func (h *Audit) Operation(c *gin.Context) {
httpx.Fail(c, "查询失败")
return
}
- httpx.Write(c, httpx.CodeSuccess, gin.H{"resysOperationRecord": item}, "查询成功")
+ httpx.Write(c, httpx.CodeSuccess, gin.H{"reSysOperationRecord": item}, "查询成功")
}
func (h *Audit) DeleteOperation(c *gin.Context) {
var req dto.IDRequest
@@ -81,7 +81,8 @@ func (h *Audit) DeleteOperations(c *gin.Context) {
func (h *Audit) Logins(c *gin.Context) {
p, size := page(c)
- items, total, err := h.service.LoginsFilter(c.Request.Context(), p, size, c.Query("username"), c.Query("ip"))
+ status, _ := strconv.ParseBool(c.Query("status"))
+ items, total, err := h.service.LoginsFilter(c.Request.Context(), p, size, c.Query("username"), status)
if err != nil {
httpx.Fail(c, "获取失败")
return
@@ -216,11 +217,20 @@ func (h *Audit) Error(c *gin.Context) {
httpx.Fail(c, "查询失败")
return
}
- httpx.Write(c, httpx.CodeSuccess, gin.H{"resysError": item}, "查询成功")
+ httpx.Write(c, httpx.CodeSuccess, item, "查询成功")
}
func (h *Audit) Errors(c *gin.Context) {
p, size := page(c)
- items, total, err := h.service.ErrorsFilter(c.Request.Context(), p, size, c.Query("form"), c.Query("level"), c.Query("status"))
+ createdAtRange := make([]time.Time, 0, 2)
+ for _, raw := range c.QueryArray("createdAtRange[]") {
+ value, parseErr := time.Parse(time.RFC3339, raw)
+ if parseErr != nil {
+ httpx.Fail(c, parseErr.Error())
+ return
+ }
+ createdAtRange = append(createdAtRange, value)
+ }
+ items, total, err := h.service.ErrorsFilter(c.Request.Context(), p, size, c.Query("form"), c.Query("info"), createdAtRange)
if err != nil {
httpx.Fail(c, "获取失败")
return
diff --git a/internal/server/handler/authority.go b/internal/server/handler/authority.go
index d4900f4..9104d4d 100644
--- a/internal/server/handler/authority.go
+++ b/internal/server/handler/authority.go
@@ -92,6 +92,9 @@ func (h *Authority) Users(c *gin.Context) {
httpx.Fail(c, "获取失败")
return
}
+ if ids == nil {
+ ids = []uint{}
+ }
httpx.Write(c, httpx.CodeSuccess, ids, "获取成功")
}
func (h *Authority) SetDataScope(c *gin.Context) {
@@ -113,5 +116,8 @@ func (h *Authority) DataScopeDepartments(c *gin.Context) {
httpx.Fail(c, "获取失败")
return
}
+ if ids == nil {
+ ids = []uint{}
+ }
httpx.Write(c, httpx.CodeSuccess, ids, "获取成功")
}
diff --git a/internal/server/handler/dictionary.go b/internal/server/handler/dictionary.go
index c07bc5b..b072cda 100644
--- a/internal/server/handler/dictionary.go
+++ b/internal/server/handler/dictionary.go
@@ -3,6 +3,7 @@ package handler
import (
"strconv"
+ "kra/internal/biz"
"kra/internal/server/httpx"
"kra/internal/service"
"kra/internal/service/dto"
@@ -54,27 +55,35 @@ func (h *Dictionary) Delete(c *gin.Context) {
}
func (h *Dictionary) Find(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
- item, err := h.service.Dictionary(c.Request.Context(), uint(id), c.Query("type"), true)
+ var status *bool
+ if raw, exists := c.GetQuery("status"); exists {
+ value, parseErr := strconv.ParseBool(raw)
+ if parseErr != nil {
+ httpx.Fail(c, parseErr.Error())
+ return
+ }
+ status = &value
+ }
+ item, err := h.service.Dictionary(c.Request.Context(), uint(id), c.Query("type"), status, true)
if err != nil {
httpx.Fail(c, "查询失败")
return
}
- httpx.Write(c, httpx.CodeSuccess, gin.H{"reSysDictionary": item}, "查询成功")
+ httpx.Write(c, httpx.CodeSuccess, gin.H{"resysDictionary": item}, "查询成功")
}
func (h *Dictionary) List(details bool) gin.HandlerFunc {
return func(c *gin.Context) {
- p, size := page(c)
- items, total, err := h.service.Dictionaries(c.Request.Context(), p, size, c.Query("name"), c.Query("type"), details)
+ items, _, err := h.service.Dictionaries(c.Request.Context(), 0, 0, c.Query("name"), "", details)
if err != nil {
httpx.Fail(c, "获取失败")
return
}
- httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: p, PageSize: size}, "获取成功")
+ httpx.Write(c, httpx.CodeSuccess, items, "获取成功")
}
}
func (h *Dictionary) Export(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
- item, err := h.service.Dictionary(c.Request.Context(), uint(id), "", true)
+ item, err := h.service.ExportDictionary(c.Request.Context(), uint(id))
if err != nil {
httpx.Fail(c, "导出失败")
return
@@ -83,6 +92,13 @@ func (h *Dictionary) Export(c *gin.Context) {
delete(item, "CreatedAt")
delete(item, "UpdatedAt")
delete(item, "DeletedAt")
+ if details, ok := item["sysDictionaryDetails"].([]map[string]any); ok {
+ for _, detail := range details {
+ for _, key := range []string{"ID", "CreatedAt", "UpdatedAt", "DeletedAt", "sysDictionaryID", "parentID", "disabled", "children"} {
+ delete(detail, key)
+ }
+ }
+ }
httpx.Write(c, httpx.CodeSuccess, item, "导出成功")
}
func (h *Dictionary) Import(c *gin.Context) {
@@ -146,7 +162,33 @@ func (h *Dictionary) FindDetail(c *gin.Context) {
func (h *Dictionary) Details(c *gin.Context) {
p, size := page(c)
dictionaryID, _ := strconv.ParseUint(c.Query("sysDictionaryID"), 10, 64)
- items, total, err := h.service.DictionaryDetails(c.Request.Context(), p, size, uint(dictionaryID), c.Query("label"), c.Query("value"))
+ filter := biz.DictionaryDetailFilter{DictionaryID: uint(dictionaryID), Label: c.Query("label"), Value: c.Query("value")}
+ if raw, exists := c.GetQuery("status"); exists {
+ value, parseErr := strconv.ParseBool(raw)
+ if parseErr != nil {
+ httpx.Fail(c, parseErr.Error())
+ return
+ }
+ filter.Status = &value
+ }
+ if raw, exists := c.GetQuery("parentID"); exists {
+ value, parseErr := strconv.ParseUint(raw, 10, 64)
+ if parseErr != nil {
+ httpx.Fail(c, parseErr.Error())
+ return
+ }
+ parentID := uint(value)
+ filter.ParentID = &parentID
+ }
+ if raw, exists := c.GetQuery("level"); exists {
+ value, parseErr := strconv.Atoi(raw)
+ if parseErr != nil {
+ httpx.Fail(c, parseErr.Error())
+ return
+ }
+ filter.Level = &value
+ }
+ items, total, err := h.service.DictionaryDetails(c.Request.Context(), p, size, filter)
if err != nil {
httpx.Fail(c, "获取失败")
return
diff --git a/internal/server/handler/email.go b/internal/server/handler/email.go
index 5f2ac4f..a6d7bb6 100644
--- a/internal/server/handler/email.go
+++ b/internal/server/handler/email.go
@@ -14,7 +14,7 @@ func NewEmail(service *service.EmailService) *Email { return &Email{service: ser
func (h *Email) Test(c *gin.Context) {
if err := h.service.Test(c.Request.Context()); err != nil {
- httpx.Fail(c, "发送失败:"+err.Error())
+ httpx.Fail(c, "发送失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "发送成功")
@@ -22,12 +22,12 @@ func (h *Email) Test(c *gin.Context) {
func (h *Email) Send(c *gin.Context) {
var req dto.SendEmailRequest
- if c.ShouldBindJSON(&req) != nil {
- httpx.Fail(c, "参数错误")
+ if err := c.ShouldBindJSON(&req); err != nil {
+ httpx.Fail(c, err.Error())
return
}
if err := h.service.Send(c.Request.Context(), req.To, req.Subject, req.Body); err != nil {
- httpx.Fail(c, "发送失败:"+err.Error())
+ httpx.Fail(c, "发送失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "发送成功")
diff --git a/internal/server/handler/export.go b/internal/server/handler/export.go
index 78752ec..e746368 100644
--- a/internal/server/handler/export.go
+++ b/internal/server/handler/export.go
@@ -106,7 +106,13 @@ func (h *Export) Find(c *gin.Context) {
}
func (h *Export) List(c *gin.Context) {
p, size := page(c)
- items, total, err := h.service.TemplatesFilter(c.Request.Context(), p, size, c.Query("name"), c.Query("tableName"), c.Query("templateID"))
+ start, startErr := parseTime(c.Query("startCreatedAt"))
+ end, endErr := parseTime(c.Query("endCreatedAt"))
+ if startErr != nil || endErr != nil {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ items, total, err := h.service.TemplatesFilter(c.Request.Context(), p, size, c.Query("name"), c.Query("tableName"), c.Query("templateID"), start, end)
if err != nil {
httpx.Fail(c, "获取失败")
return
diff --git a/internal/server/handler/media.go b/internal/server/handler/media.go
index ac914d7..6af9068 100644
--- a/internal/server/handler/media.go
+++ b/internal/server/handler/media.go
@@ -3,6 +3,7 @@ package handler
import (
"strconv"
+ "kra/internal/biz"
"kra/internal/server/httpx"
servermiddleware "kra/internal/server/middleware"
"kra/internal/service"
@@ -32,7 +33,7 @@ func (h *Media) Upload(c *gin.Context) {
save := c.DefaultQuery("noSave", "0") == "0"
item, err := h.service.Upload(c.Request.Context(), claims.ID, header.Filename, header.Header.Get("Content-Type"), category, opened, save)
if err != nil {
- httpx.Fail(c, "上传失败:"+err.Error())
+ httpx.Fail(c, "上传文件失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{"file": item}, "上传成功")
@@ -44,7 +45,7 @@ func (h *Media) List(c *gin.Context) {
httpx.Fail(c, "参数错误")
return
}
- items, total, err := h.service.MediaList(c.Request.Context(), req.Page, req.PageSize, req.Keyword, req.ClassID, req.Tag, req.UserID)
+ items, total, err := h.service.MediaList(c.Request.Context(), biz.MediaFilter{Page: req.Page, PageSize: req.PageSize, Keyword: req.Keyword, CategoryID: req.ClassID, Tag: req.Tag, UserID: req.UserID, StartCreatedAt: req.StartCreatedAt, EndCreatedAt: req.EndCreatedAt, OrderKey: req.OrderKey, Desc: req.Desc})
if err != nil {
httpx.Fail(c, "获取失败")
return
@@ -58,10 +59,10 @@ func (h *Media) Delete(c *gin.Context) {
return
}
if err := h.service.Delete(c.Request.Context(), req.ID); err != nil {
- httpx.Fail(c, "删除失败:"+err.Error())
+ httpx.Fail(c, "删除失败")
return
}
- httpx.OK(c)
+ httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
}
func (h *Media) DeleteMany(c *gin.Context) {
var req dto.IDsRequest
@@ -79,7 +80,7 @@ func (h *Media) DeleteMany(c *gin.Context) {
httpx.Write(c, httpx.CodeSuccess, gin.H{"failedIds": failed}, "部分文件删除失败")
return
}
- httpx.OK(c)
+ httpx.Write(c, httpx.CodeSuccess, gin.H{}, "批量删除成功")
}
func (h *Media) Find(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("id"), 10, 64)
@@ -88,7 +89,7 @@ func (h *Media) Find(c *gin.Context) {
httpx.Fail(c, "查询失败")
return
}
- httpx.Write(c, httpx.CodeSuccess, item, "获取成功")
+ httpx.OKWithData(c, item)
}
func (h *Media) Rename(c *gin.Context) {
var req dto.RenameMediaRequest
@@ -100,7 +101,7 @@ func (h *Media) Rename(c *gin.Context) {
httpx.Fail(c, "编辑失败")
return
}
- httpx.OK(c)
+ httpx.Write(c, httpx.CodeSuccess, gin.H{}, "编辑成功")
}
func (h *Media) ImportURLs(c *gin.Context) {
var req []dto.ImportMediaRequest
@@ -108,16 +109,11 @@ func (h *Media) ImportURLs(c *gin.Context) {
httpx.Fail(c, "参数错误")
return
}
- claims := servermiddleware.Claims(c)
- if claims == nil {
- httpx.Fail(c, "未登录")
+ if err := h.service.ImportURLRequests(c.Request.Context(), req); err != nil {
+ httpx.Fail(c, "导入URL失败")
return
}
- if err := h.service.ImportURLRequests(c.Request.Context(), claims.ID, req); err != nil {
- httpx.Fail(c, "导入失败")
- return
- }
- httpx.OK(c)
+ httpx.Write(c, httpx.CodeSuccess, gin.H{}, "导入URL成功")
}
func (h *Media) Storage(c *gin.Context) {
var req dto.StorageListRequest
@@ -148,10 +144,10 @@ func (h *Media) SaveCategory(c *gin.Context) {
return
}
if err := h.service.SaveCategoryRequest(c.Request.Context(), &req); err != nil {
- httpx.Fail(c, err.Error())
+ httpx.Fail(c, "创建/更新失败:"+err.Error())
return
}
- httpx.OK(c)
+ httpx.Write(c, httpx.CodeSuccess, gin.H{}, "创建/更新成功")
}
func (h *Media) DeleteCategory(c *gin.Context) {
var req dto.IDRequest
@@ -160,10 +156,10 @@ func (h *Media) DeleteCategory(c *gin.Context) {
return
}
if err := h.service.DeleteCategory(c.Request.Context(), req.ID); err != nil {
- httpx.Fail(c, err.Error())
+ httpx.Fail(c, "删除失败")
return
}
- httpx.OK(c)
+ httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
}
func (h *Media) InitUpload(c *gin.Context) {
diff --git a/internal/server/handler/menu.go b/internal/server/handler/menu.go
index 5bf23c8..e41db48 100644
--- a/internal/server/handler/menu.go
+++ b/internal/server/handler/menu.go
@@ -15,12 +15,12 @@ type Menu struct{ service *service.MenuService }
func NewMenu(service *service.MenuService) *Menu { return &Menu{service: service} }
func (h *Menu) List(c *gin.Context) {
- items, err := h.service.List(c.Request.Context())
+ items, err := h.service.Tree(c.Request.Context())
if err != nil {
httpx.Fail(c, "获取失败")
return
}
- httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: int64(len(items)), Page: 1, PageSize: 999}, "获取成功")
+ httpx.Write(c, httpx.CodeSuccess, items, "获取成功")
}
func (h *Menu) Tree(c *gin.Context) {
@@ -119,7 +119,18 @@ func (h *Menu) RoleIDs(c *gin.Context) {
httpx.Fail(c, "获取失败")
return
}
- httpx.Write(c, httpx.CodeSuccess, ids, "获取成功")
+ if ids == nil {
+ ids = []uint{}
+ }
+ defaultIDs, err := h.service.DefaultRouterRoleIDs(c.Request.Context(), uint(id))
+ if err != nil {
+ httpx.Fail(c, "获取失败")
+ return
+ }
+ if defaultIDs == nil {
+ defaultIDs = []uint{}
+ }
+ httpx.Write(c, httpx.CodeSuccess, gin.H{"authorityIds": ids, "defaultRouterAuthorityIds": defaultIDs}, "获取成功")
}
func (h *Menu) SetRoles(c *gin.Context) {
diff --git a/internal/server/handler/organization.go b/internal/server/handler/organization.go
index d302429..4052b4f 100644
--- a/internal/server/handler/organization.go
+++ b/internal/server/handler/organization.go
@@ -28,27 +28,35 @@ func (h *Organization) ListDepartments(c *gin.Context) {
}
func (h *Organization) CreateDepartment(c *gin.Context) {
var req dto.DepartmentRequest
- if c.ShouldBindJSON(&req) != nil {
- httpx.Fail(c, "参数错误")
+ if err := c.ShouldBindJSON(&req); err != nil {
+ httpx.Fail(c, err.Error())
+ return
+ }
+ if req.Name == "" {
+ httpx.Fail(c, "部门名称不能为空")
return
}
if err := h.service.CreateDepartment(c.Request.Context(), &req); err != nil {
- httpx.Fail(c, "创建失败")
+ httpx.Fail(c, "创建失败:"+err.Error())
return
}
- httpx.OK(c)
+ httpx.Write(c, httpx.CodeSuccess, nil, "创建成功")
}
func (h *Organization) UpdateDepartment(c *gin.Context) {
var req dto.DepartmentRequest
- if c.ShouldBindJSON(&req) != nil {
- httpx.Fail(c, "参数错误")
+ if err := c.ShouldBindJSON(&req); err != nil {
+ httpx.Fail(c, err.Error())
+ return
+ }
+ if req.ID == 0 {
+ httpx.Fail(c, "部门ID不能为空")
return
}
if err := h.service.UpdateDepartment(c.Request.Context(), &req); err != nil {
- httpx.Fail(c, "更新失败")
+ httpx.Fail(c, "更新失败:"+err.Error())
return
}
- httpx.OK(c)
+ httpx.Write(c, httpx.CodeSuccess, nil, "更新成功")
}
func (h *Organization) DeleteDepartment(c *gin.Context) {
var req dto.DeleteDepartmentRequest
@@ -60,7 +68,7 @@ func (h *Organization) DeleteDepartment(c *gin.Context) {
httpx.Fail(c, err.Error())
return
}
- httpx.OK(c)
+ httpx.Write(c, httpx.CodeSuccess, nil, "删除成功")
}
func (h *Organization) FindDepartment(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("id"), 10, 64)
@@ -78,6 +86,9 @@ func (h *Organization) DepartmentUsers(c *gin.Context) {
httpx.Fail(c, "获取失败")
return
}
+ if ids == nil {
+ ids = []uint{}
+ }
httpx.Write(c, httpx.CodeSuccess, ids, "获取成功")
}
func (h *Organization) SetDepartmentUsers(c *gin.Context) {
@@ -120,27 +131,35 @@ func (h *Organization) ListPositions(c *gin.Context) {
}
func (h *Organization) CreatePosition(c *gin.Context) {
var req dto.PositionRequest
- if c.ShouldBindJSON(&req) != nil {
- httpx.Fail(c, "参数错误")
+ if err := c.ShouldBindJSON(&req); err != nil {
+ httpx.Fail(c, err.Error())
+ return
+ }
+ if req.Name == "" {
+ httpx.Fail(c, "岗位名称不能为空")
return
}
if err := h.service.CreatePosition(c.Request.Context(), &req); err != nil {
- httpx.Fail(c, "创建失败")
+ httpx.Fail(c, "创建失败:"+err.Error())
return
}
- httpx.OK(c)
+ httpx.Write(c, httpx.CodeSuccess, nil, "创建成功")
}
func (h *Organization) UpdatePosition(c *gin.Context) {
var req dto.PositionRequest
- if c.ShouldBindJSON(&req) != nil {
- httpx.Fail(c, "参数错误")
+ if err := c.ShouldBindJSON(&req); err != nil {
+ httpx.Fail(c, err.Error())
+ return
+ }
+ if req.ID == 0 {
+ httpx.Fail(c, "岗位ID不能为空")
return
}
if err := h.service.UpdatePosition(c.Request.Context(), &req); err != nil {
- httpx.Fail(c, "更新失败")
+ httpx.Fail(c, "更新失败:"+err.Error())
return
}
- httpx.OK(c)
+ httpx.Write(c, httpx.CodeSuccess, nil, "更新成功")
}
func (h *Organization) DeletePosition(c *gin.Context) {
var req dto.DeletePositionRequest
@@ -149,10 +168,10 @@ func (h *Organization) DeletePosition(c *gin.Context) {
return
}
if err := h.service.DeletePosition(c.Request.Context(), req.ID); err != nil {
- httpx.Fail(c, "删除失败")
+ httpx.Fail(c, "删除失败:"+err.Error())
return
}
- httpx.OK(c)
+ httpx.Write(c, httpx.CodeSuccess, nil, "删除成功")
}
func (h *Organization) FindPosition(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("id"), 10, 64)
@@ -170,6 +189,9 @@ func (h *Organization) PositionUsers(c *gin.Context) {
httpx.Fail(c, "获取失败")
return
}
+ if ids == nil {
+ ids = []uint{}
+ }
httpx.Write(c, httpx.CodeSuccess, ids, "获取成功")
}
func (h *Organization) SetPositionUsers(c *gin.Context) {
diff --git a/internal/server/handler/parameter.go b/internal/server/handler/parameter.go
index bbfdb56..c08f52a 100644
--- a/internal/server/handler/parameter.go
+++ b/internal/server/handler/parameter.go
@@ -77,7 +77,7 @@ func (h *Parameter) Find(c *gin.Context) {
httpx.Fail(c, "查询失败")
return
}
- httpx.Write(c, httpx.CodeSuccess, gin.H{"resysParams": item}, "查询成功")
+ httpx.Write(c, httpx.CodeSuccess, item, "查询成功")
}
func (h *Parameter) Get(c *gin.Context) {
@@ -91,7 +91,13 @@ func (h *Parameter) Get(c *gin.Context) {
func (h *Parameter) List(c *gin.Context) {
p, size := page(c)
- items, total, err := h.service.ParametersFilter(c.Request.Context(), p, size, c.Query("name"), c.Query("key"))
+ start, startErr := parseTime(c.Query("startCreatedAt"))
+ end, endErr := parseTime(c.Query("endCreatedAt"))
+ if startErr != nil || endErr != nil {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ items, total, err := h.service.ParametersFilter(c.Request.Context(), p, size, c.Query("name"), c.Query("key"), start, end)
if err != nil {
httpx.Fail(c, "获取失败")
return
diff --git a/internal/server/handler/public.go b/internal/server/handler/public.go
index b01755f..c172c00 100644
--- a/internal/server/handler/public.go
+++ b/internal/server/handler/public.go
@@ -2,11 +2,12 @@ package handler
import (
"context"
- "net/http"
+ "errors"
"strconv"
"strings"
"time"
+ "kra/internal/biz"
"kra/internal/conf"
"kra/internal/server/httpx"
"kra/internal/service"
@@ -18,23 +19,22 @@ import (
)
type Public struct {
- system *service.SystemService
- settings *service.SettingsService
- audit *service.AuditService
- scheduler *worker.TaskScheduler
- store *captchaStore
- keyLong int
- imageWidth int
- imageHeight int
+ system *service.SystemService
+ settings *service.SettingsService
+ audit *service.AuditService
+ scheduler *worker.TaskScheduler
+ runtime *conf.Runtime
+ store *captchaStore
}
-func NewPublic(config *conf.AdminBackend, system *service.SystemService, settings *service.SettingsService, audit *service.AuditService, scheduler *worker.TaskScheduler) *Public {
- expiration := 3 * time.Minute
+func NewPublic(runtime *conf.Runtime, system *service.SystemService, settings *service.SettingsService, audit *service.AuditService, scheduler *worker.TaskScheduler) *Public {
+ return &Public{system: system, settings: settings, audit: audit, scheduler: scheduler, runtime: runtime, store: &captchaStore{service: system, runtime: runtime}}
+}
+
+func (h *Public) captchaConfig() (int, int, int) {
keyLong, width, height := 6, 240, 80
+ config := h.runtime.Admin()
if config != nil && config.Captcha != nil {
- if config.Captcha.StoreExpiration != nil {
- expiration = config.Captcha.StoreExpiration.AsDuration()
- }
if config.Captcha.KeyLong > 0 {
keyLong = int(config.Captcha.KeyLong)
}
@@ -45,23 +45,18 @@ func NewPublic(config *conf.AdminBackend, system *service.SystemService, setting
height = int(config.Captcha.ImgHeight)
}
}
- return &Public{system: system, settings: settings, audit: audit, scheduler: scheduler, store: &captchaStore{service: system, expiration: expiration}, keyLong: keyLong, imageWidth: width, imageHeight: height}
+ return keyLong, width, height
}
func (h *Public) Captcha(c *gin.Context) {
- keyLong, width, height := h.keyLong, h.imageWidth, h.imageHeight
+ keyLong, width, height := h.captchaConfig()
security, _ := h.settings.CurrentSecurity(c.Request.Context())
openCaptcha := true
if security != nil {
keyLong, width, height = security.KeyLong, security.ImgWidth, security.ImgHeight
- if security.CaptchaOpen > 0 {
- failures, _ := cachedInt(c.Request.Context(), h.system, "login:fail:"+c.ClientIP())
- openCaptcha = failures >= security.CaptchaOpen
- }
- }
- if !openCaptcha {
- httpx.Write(c, httpx.CodeSuccess, gin.H{"captchaId": "", "picPath": "", "captchaLength": keyLong, "openCaptcha": false}, "验证码获取成功")
- return
+ ttl := time.Duration(security.CaptchaTimeout) * time.Second
+ failures, _ := ensureLoginIPCounter(c.Request.Context(), h.system, c.ClientIP(), ttl)
+ openCaptcha = security.CaptchaOpen == 0 || failures > security.CaptchaOpen
}
driver := base64Captcha.NewDriverDigit(height, width, keyLong, 0.7, 80)
id, picture, _, err := base64Captcha.NewCaptcha(driver, h.store).Generate()
@@ -81,42 +76,71 @@ func (h *Public) Login(c *gin.Context) {
security, _ := h.settings.CurrentSecurity(c.Request.Context())
if security != nil && security.LockEnable {
if _, locked, _ := h.system.CacheGet(c.Request.Context(), "login:lock:"+req.Username); locked {
- httpx.Fail(c, "账户已锁定,请稍后重试")
+ httpx.Fail(c, "账号已锁定,请 "+strconv.Itoa(security.LockDuration)+" 分钟后再试")
+ _ = h.audit.RecordLoginRequest(c.Request.Context(), &dto.LoginLogRequest{Username: req.Username, IP: c.ClientIP(), Status: false, ErrorMessage: "账号已锁定", Agent: c.Request.UserAgent()})
return
}
}
requireCaptcha := security == nil || security.CaptchaOpen == 0
- if security != nil && security.CaptchaOpen > 0 {
- failures, _ := cachedInt(c.Request.Context(), h.system, "login:fail:"+c.ClientIP())
- requireCaptcha = failures >= security.CaptchaOpen
+ ipTTL := time.Hour
+ if security != nil {
+ if security.CaptchaTimeout > 0 {
+ ipTTL = time.Duration(security.CaptchaTimeout) * time.Second
+ }
+ failures, _ := ensureLoginIPCounter(c.Request.Context(), h.system, c.ClientIP(), ipTTL)
+ requireCaptcha = security.CaptchaOpen == 0 || failures > security.CaptchaOpen
}
if requireCaptcha && (req.CaptchaID == "" || req.Captcha == "" || !h.store.Verify(req.CaptchaID, req.Captcha, true)) {
+ _, _ = h.system.CacheIncrement(c.Request.Context(), c.ClientIP(), ipTTL)
+ _ = h.audit.RecordLoginRequest(c.Request.Context(), &dto.LoginLogRequest{Username: req.Username, IP: c.ClientIP(), Status: false, ErrorMessage: "验证码错误", Agent: c.Request.UserAgent()})
httpx.Fail(c, "验证码错误")
return
}
result, err := h.system.Login(c.Request.Context(), req.Username, req.Password)
if err != nil {
- _ = h.audit.RecordLoginRequest(c.Request.Context(), &dto.LoginLogRequest{Username: req.Username, IP: c.ClientIP(), Status: false, ErrorMessage: "用户名不存在或者密码错误", Agent: c.Request.UserAgent()})
- ttl := time.Hour
- if security != nil && security.CaptchaTimeout > 0 {
- ttl = time.Duration(security.CaptchaTimeout) * time.Second
+ _, _ = 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()})
+ httpx.Fail(c, "用户被禁止登录")
+ return
}
- failures, _ := incrementCached(c.Request.Context(), h.system, "login:fail:"+c.ClientIP(), ttl)
- if security != nil && security.LockEnable && failures >= security.LockThreshold {
- _ = h.system.CacheSet(c.Request.Context(), "login:lock:"+req.Username, "1", time.Duration(security.LockDuration)*time.Minute)
+ _ = h.audit.RecordLoginRequest(c.Request.Context(), &dto.LoginLogRequest{Username: req.Username, IP: c.ClientIP(), Status: false, ErrorMessage: "用户名不存在或者密码错误", Agent: c.Request.UserAgent()})
+ if security != nil && security.LockEnable {
+ lockTTL := time.Duration(security.LockDuration) * time.Minute
+ failures, _ := h.system.CacheIncrement(c.Request.Context(), "login:fail:"+req.Username, lockTTL)
+ if int(failures) >= security.LockThreshold {
+ _ = h.system.CacheSet(c.Request.Context(), "login:lock:"+req.Username, "1", lockTTL)
+ }
}
httpx.Fail(c, "用户名不存在或者密码错误")
return
}
userID, _ := result.User["ID"].(uint)
_ = h.audit.RecordLoginRequest(c.Request.Context(), &dto.LoginLogRequest{Username: req.Username, IP: c.ClientIP(), Status: true, Agent: c.Request.UserAgent(), UserID: userID})
- _ = h.system.CacheDelete(c.Request.Context(), "login:fail:"+c.ClientIP())
+ _ = h.system.CacheDelete(c.Request.Context(), "login:fail:"+req.Username)
_ = h.system.CacheDelete(c.Request.Context(), "login:lock:"+req.Username)
maxAge := int(time.Until(time.UnixMilli(result.ExpiresAt)).Seconds())
- http.SetCookie(c.Writer, &http.Cookie{Name: "x-token", Value: result.Token, Path: "/", MaxAge: maxAge, HttpOnly: true, SameSite: http.SameSiteStrictMode})
+ httpx.SetTokenCookie(c, result.Token, maxAge)
httpx.Write(c, httpx.CodeSuccess, result, "登录成功")
}
+func ensureLoginIPCounter(ctx context.Context, system *service.SystemService, ip string, expiration time.Duration) (int, error) {
+ value, exists, err := system.CacheGet(ctx, ip)
+ if err != nil {
+ return 0, err
+ }
+ if exists {
+ return strconv.Atoi(value)
+ }
+ if expiration <= 0 {
+ expiration = time.Hour
+ }
+ if err = system.CacheSet(ctx, ip, "1", expiration); err != nil {
+ return 0, err
+ }
+ return 1, nil
+}
+
func (h *Public) CheckDatabase(c *gin.Context) {
initialized, err := h.system.IsInitialized(c.Request.Context())
if err != nil {
@@ -163,27 +187,19 @@ func (h *Public) InitializeDatabase(engine *gin.Engine) gin.HandlerFunc {
}
}
-func cachedInt(ctx context.Context, system *service.SystemService, key string) (int, error) {
- value, ok, err := system.CacheGet(ctx, key)
- if err != nil || !ok {
- return 0, err
- }
- return strconv.Atoi(value)
-}
-
-func incrementCached(ctx context.Context, system *service.SystemService, key string, ttl time.Duration) (int, error) {
- value, err := system.CacheIncrement(ctx, key, ttl)
- return int(value), err
-}
-
type captchaStore struct {
- service *service.SystemService
- expiration time.Duration
+ service *service.SystemService
+ runtime *conf.Runtime
}
func (s *captchaStore) key(id string) string { return "captcha:" + id }
func (s *captchaStore) Set(id, value string) error {
- return s.service.CacheSet(context.Background(), s.key(id), value, s.expiration)
+ expiration := 3 * time.Minute
+ config := s.runtime.Admin()
+ if config != nil && config.Captcha != nil && config.Captcha.StoreExpiration != nil {
+ expiration = config.Captcha.StoreExpiration.AsDuration()
+ }
+ return s.service.CacheSet(context.Background(), s.key(id), value, expiration)
}
func (s *captchaStore) Get(id string, clear bool) string {
value, ok, err := s.service.CacheGet(context.Background(), s.key(id))
diff --git a/internal/server/handler/session.go b/internal/server/handler/session.go
index 2badae4..a89ca48 100644
--- a/internal/server/handler/session.go
+++ b/internal/server/handler/session.go
@@ -1,8 +1,6 @@
package handler
import (
- "net/http"
-
"kra/internal/server/httpx"
"kra/internal/service"
@@ -21,6 +19,6 @@ func (h *Session) Logout(c *gin.Context) {
httpx.Fail(c, "退出失败")
return
}
- http.SetCookie(c.Writer, &http.Cookie{Name: "x-token", Value: "", Path: "/", MaxAge: -1, HttpOnly: true})
+ httpx.SetTokenCookie(c, "", -1)
httpx.OK(c)
}
diff --git a/internal/server/handler/system_config.go b/internal/server/handler/system_config.go
index 849691f..1b45015 100644
--- a/internal/server/handler/system_config.go
+++ b/internal/server/handler/system_config.go
@@ -2,7 +2,6 @@ package handler
import (
"runtime"
- "syscall"
"time"
"kra/internal/server/httpx"
@@ -12,6 +11,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/shirou/gopsutil/v4/cpu"
+ "github.com/shirou/gopsutil/v4/disk"
"github.com/shirou/gopsutil/v4/mem"
)
@@ -78,24 +78,31 @@ func (h *SystemConfig) Reload(c *gin.Context) {
}
func (h *SystemConfig) ServerInfo(c *gin.Context) {
- cpuPercent, _ := cpu.Percent(200*time.Millisecond, false)
- memory, _ := mem.VirtualMemory()
- disk := []gin.H{}
- var stat syscall.Statfs_t
- if syscall.Statfs(".", &stat) == nil {
- total := uint64(stat.Blocks) * uint64(stat.Bsize)
- free := uint64(stat.Bavail) * uint64(stat.Bsize)
- used := total - free
- percent := 0
- if total > 0 {
- percent = int(used * 100 / total)
+ physicalCores, err := cpu.Counts(false)
+ if err != nil {
+ httpx.Fail(c, "获取失败")
+ return
+ }
+ cpuPercent, err := cpu.Percent(200*time.Millisecond, true)
+ if err != nil {
+ httpx.Fail(c, "获取失败")
+ return
+ }
+ memory, err := mem.VirtualMemory()
+ if err != nil {
+ httpx.Fail(c, "获取失败")
+ return
+ }
+ diskInfo := make([]gin.H, 0)
+ for _, mountPoint := range h.system.DiskMountPoints() {
+ usage, usageErr := disk.Usage(mountPoint)
+ if usageErr != nil {
+ httpx.Fail(c, "获取失败")
+ return
}
- disk = append(disk, gin.H{"mountPoint": ".", "usedMb": used / 1024 / 1024, "usedGb": used / 1024 / 1024 / 1024, "totalMb": total / 1024 / 1024, "totalGb": total / 1024 / 1024 / 1024, "usedPercent": percent})
+ diskInfo = append(diskInfo, gin.H{"mountPoint": mountPoint, "usedMb": usage.Used / 1024 / 1024, "usedGb": usage.Used / 1024 / 1024 / 1024, "totalMb": usage.Total / 1024 / 1024, "totalGb": usage.Total / 1024 / 1024 / 1024, "usedPercent": int(usage.UsedPercent)})
}
- usedMB, totalMB, memoryPercent := uint64(0), uint64(0), float64(0)
- if memory != nil {
- usedMB, totalMB, memoryPercent = memory.Used/1024/1024, memory.Total/1024/1024, memory.UsedPercent
- }
- server := gin.H{"os": gin.H{"goos": runtime.GOOS, "numCpu": runtime.NumCPU(), "compiler": runtime.Compiler, "goVersion": runtime.Version(), "numGoroutine": runtime.NumGoroutine()}, "cpu": gin.H{"cpus": cpuPercent, "cores": runtime.NumCPU()}, "ram": gin.H{"usedMb": usedMB, "totalMb": totalMB, "usedPercent": memoryPercent}, "disk": disk}
+ usedMB, totalMB, memoryPercent := memory.Used/1024/1024, memory.Total/1024/1024, int(memory.UsedPercent)
+ server := gin.H{"os": gin.H{"goos": runtime.GOOS, "numCpu": runtime.NumCPU(), "compiler": runtime.Compiler, "goVersion": runtime.Version(), "numGoroutine": runtime.NumGoroutine()}, "cpu": gin.H{"cpus": cpuPercent, "cores": physicalCores}, "ram": gin.H{"usedMb": usedMB, "totalMb": totalMB, "usedPercent": memoryPercent}, "disk": diskInfo}
httpx.Write(c, httpx.CodeSuccess, gin.H{"server": server}, "获取成功")
}
diff --git a/internal/server/handler/task.go b/internal/server/handler/task.go
index fc4b1c0..a34fc76 100644
--- a/internal/server/handler/task.go
+++ b/internal/server/handler/task.go
@@ -103,8 +103,8 @@ func (h *Task) Trigger(c *gin.Context) {
}
func (h *Task) List(c *gin.Context) {
- page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
- size, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
+ page, _ := strconv.Atoi(c.Query("page"))
+ size, _ := strconv.Atoi(c.Query("pageSize"))
var enabled *bool
if raw := c.Query("enabled"); raw != "" {
value, _ := strconv.ParseBool(raw)
@@ -119,8 +119,8 @@ func (h *Task) List(c *gin.Context) {
}
func (h *Task) Logs(c *gin.Context) {
- page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
- size, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
+ page, _ := strconv.Atoi(c.Query("page"))
+ size, _ := strconv.Atoi(c.Query("pageSize"))
taskID, _ := strconv.ParseUint(c.Query("taskId"), 10, 64)
items, total, err := h.service.Logs(c.Request.Context(), page, size, uint(taskID), c.Query("status"))
if err != nil {
@@ -144,6 +144,8 @@ func (h *Task) AlertStream(c *gin.Context) {
httpx.Fail(c, "SSE 不受支持")
return
}
+ // Remove the normal server deadline for long-lived SSE connections.
+ _ = http.NewResponseController(c.Writer).SetWriteDeadline(time.Time{})
events := h.scheduler.Subscribe()
defer h.scheduler.Unsubscribe(events)
ticker := time.NewTicker(20 * time.Second)
diff --git a/internal/server/handler/user.go b/internal/server/handler/user.go
index 4fd722a..420f5f2 100644
--- a/internal/server/handler/user.go
+++ b/internal/server/handler/user.go
@@ -1,7 +1,6 @@
package handler
import (
- "net/http"
"strconv"
"time"
@@ -69,8 +68,8 @@ func (h *User) UpdateSelf(c *gin.Context) {
httpx.Fail(c, "参数错误")
return
}
- value := &dto.UserRequest{ID: claims.ID, NickName: req.NickName, HeaderImg: req.HeaderImg, Phone: req.Phone, Email: req.Email, Enable: 1}
- if err := h.service.UpdateUserRequest(c.Request.Context(), value); err != nil {
+ value := service.UserInput{ID: claims.ID, NickName: req.NickName, HeaderImg: req.HeaderImg, Phone: req.Phone, Email: req.Email, Enable: req.Enable}
+ if err := h.service.UpdateSelfUser(c.Request.Context(), value); err != nil {
httpx.Fail(c, "修改失败")
return
}
@@ -83,6 +82,11 @@ func (h *User) Delete(c *gin.Context) {
httpx.Fail(c, "参数错误")
return
}
+ claims := middleware.Claims(c)
+ if claims != nil && claims.ID == req.ID {
+ httpx.Fail(c, "删除失败, 无法删除自己。")
+ return
+ }
if err := h.service.DeleteUser(c.Request.Context(), req.ID); err != nil {
httpx.Fail(c, "删除失败")
return
@@ -159,7 +163,7 @@ func (h *User) SwitchAuthority(c *gin.Context) {
c.Header("new-token", login.Token)
c.Header("new-expires-at", strconv.FormatInt(login.ExpiresAt/1000, 10))
maxAge := int(time.Until(time.UnixMilli(login.ExpiresAt)).Seconds())
- http.SetCookie(c.Writer, &http.Cookie{Name: "x-token", Value: login.Token, Path: "/", MaxAge: maxAge, HttpOnly: true, SameSite: http.SameSiteStrictMode})
+ httpx.SetTokenCookie(c, login.Token, maxAge)
httpx.OK(c)
}
diff --git a/internal/server/handler/version.go b/internal/server/handler/version.go
index 825da53..e9ca1d8 100644
--- a/internal/server/handler/version.go
+++ b/internal/server/handler/version.go
@@ -23,7 +23,7 @@ func (h *Version) Delete(c *gin.Context) {
httpx.Fail(c, "删除失败")
return
}
- httpx.OK(c)
+ httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
}
func (h *Version) DeleteMany(c *gin.Context) {
raw := c.QueryArray("IDs[]")
@@ -36,7 +36,7 @@ func (h *Version) DeleteMany(c *gin.Context) {
httpx.Fail(c, "删除失败")
return
}
- httpx.OK(c)
+ httpx.Write(c, httpx.CodeSuccess, gin.H{}, "批量删除成功")
}
func (h *Version) Find(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
@@ -49,7 +49,16 @@ func (h *Version) Find(c *gin.Context) {
}
func (h *Version) List(c *gin.Context) {
p, size := page(c)
- items, total, err := h.service.Versions(c.Request.Context(), p, size, c.Query("versionName"), c.Query("versionCode"))
+ createdAtRange := make([]*time.Time, 0, 2)
+ for _, raw := range c.QueryArray("createdAtRange[]") {
+ value, parseErr := parseTime(raw)
+ if parseErr != nil {
+ httpx.Fail(c, parseErr.Error())
+ return
+ }
+ createdAtRange = append(createdAtRange, value)
+ }
+ items, total, err := h.service.Versions(c.Request.Context(), p, size, c.Query("versionName"), c.Query("versionCode"), createdAtRange)
if err != nil {
httpx.Fail(c, "获取失败")
return
@@ -66,7 +75,7 @@ func (h *Version) Export(c *gin.Context) {
httpx.Fail(c, "创建发版失败:"+err.Error())
return
}
- httpx.OK(c)
+ httpx.Write(c, httpx.CodeSuccess, gin.H{}, "创建发版成功")
}
func (h *Version) Download(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
@@ -89,5 +98,5 @@ func (h *Version) Import(c *gin.Context) {
httpx.Fail(c, "导入失败:"+err.Error())
return
}
- httpx.OK(c)
+ httpx.Write(c, httpx.CodeSuccess, gin.H{}, "导入成功")
}
diff --git a/internal/server/httpx/cookie.go b/internal/server/httpx/cookie.go
new file mode 100644
index 0000000..be82833
--- /dev/null
+++ b/internal/server/httpx/cookie.go
@@ -0,0 +1,27 @@
+package httpx
+
+import (
+ "net/http"
+ "strings"
+ "time"
+
+ "github.com/gin-gonic/gin"
+)
+
+func requestUsesHTTPS(request *http.Request) bool {
+ if request.TLS != nil {
+ return true
+ }
+ forwarded := strings.SplitN(request.Header.Get("X-Forwarded-Proto"), ",", 2)[0]
+ return strings.EqualFold(strings.TrimSpace(forwarded), "https")
+}
+
+// SetTokenCookie also marks cookies Secure behind TLS or a TLS-terminating
+// reverse proxy.
+func SetTokenCookie(c *gin.Context, value string, maxAge int) {
+ cookie := &http.Cookie{Name: "x-token", Value: value, Path: "/", MaxAge: maxAge, Secure: requestUsesHTTPS(c.Request), HttpOnly: true, SameSite: http.SameSiteStrictMode}
+ if maxAge < 0 {
+ cookie.Expires = time.Unix(1, 0)
+ }
+ http.SetCookie(c.Writer, cookie)
+}
diff --git a/internal/server/middleware/access.go b/internal/server/middleware/access.go
index cda5e74..2c6cf26 100644
--- a/internal/server/middleware/access.go
+++ b/internal/server/middleware/access.go
@@ -2,7 +2,10 @@ package middleware
import (
"net/http"
+ "strings"
+ "kra/internal/biz"
+ "kra/internal/conf"
"kra/internal/server/httpx"
"kra/internal/service"
"kra/internal/service/dto"
@@ -10,7 +13,7 @@ import (
"github.com/gin-gonic/gin"
)
-func AccessControl(access *service.AccessService, audit *service.AuditService) gin.HandlerFunc {
+func AccessControl(runtime *conf.Runtime, access *service.AccessService, audit *service.AuditService) gin.HandlerFunc {
return func(c *gin.Context) {
claims := Claims(c)
if claims == nil {
@@ -18,7 +21,14 @@ func AccessControl(access *service.AccessService, audit *service.AuditService) g
return
}
path := c.Request.URL.Path
- allowed, err := access.Authorize(c.Request.Context(), claims.AuthorityID, path, c.Request.Method)
+ policyPath := path
+ if config := runtime.Admin(); config != nil && config.RouterPrefix != "" {
+ policyPath = strings.TrimPrefix(policyPath, strings.TrimSuffix(config.RouterPrefix, "/"))
+ if policyPath == "" {
+ policyPath = "/"
+ }
+ }
+ allowed, err := access.Authorize(c.Request.Context(), claims.AuthorityID, policyPath, c.Request.Method)
if err != nil || !allowed {
requestID, _ := c.Get("request_id")
requestIDText, _ := requestID.(string)
@@ -31,6 +41,7 @@ func AccessControl(access *service.AccessService, audit *service.AuditService) g
httpx.Fail(c, "数据权限加载失败")
return
}
+ requestContext = biz.NewActorContext(requestContext, biz.Actor{UserID: claims.ID, AuthorityID: claims.AuthorityID})
c.Request = c.Request.WithContext(requestContext)
c.Next()
}
diff --git a/internal/server/middleware/auth.go b/internal/server/middleware/auth.go
index 22ae6e9..1029032 100644
--- a/internal/server/middleware/auth.go
+++ b/internal/server/middleware/auth.go
@@ -16,13 +16,14 @@ import (
const claimsKey = "admin_claims"
-func Auth(config *conf.AdminBackend, settings *service.SettingsService) gin.HandlerFunc {
+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")
}
secret := ""
+ config := runtime.Admin()
if config != nil && config.Jwt != nil {
secret = config.Jwt.SigningKey
}
@@ -35,9 +36,14 @@ func Auth(config *conf.AdminBackend, settings *service.SettingsService) gin.Hand
httpx.NoAuth(c, "登录状态已失效")
return
}
+ if active, checkErr := settings.ActiveTokenMatches(c.Request.Context(), claims.Username, token); checkErr != nil || !active {
+ httpx.NoAuth(c, "登录状态已失效")
+ return
+ }
if claims.ExpiresAt != nil && claims.BufferTime > 0 && time.Until(claims.ExpiresAt.Time) < time.Duration(claims.BufferTime)*time.Second {
expires, buffer := 7*24*time.Hour, time.Duration(claims.BufferTime)*time.Second
issuer := claims.Issuer
+ config = runtime.Admin()
if config != nil && config.Jwt != nil {
if config.Jwt.ExpiresTime != nil {
expires = config.Jwt.ExpiresTime.AsDuration()
@@ -50,10 +56,10 @@ func Auth(config *conf.AdminBackend, settings *service.SettingsService) gin.Hand
}
}
newToken, newClaims, refreshErr := adminauth.Generate(secret, issuer, expires, buffer, claims.ID, claims.AuthorityID, claims.UUID, claims.Username, claims.NickName, claims.MustChangePwd)
- if refreshErr == nil {
+ 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))
- http.SetCookie(c.Writer, &http.Cookie{Name: "x-token", Value: newToken, Path: "/", MaxAge: int(expires.Seconds()), HttpOnly: true, SameSite: http.SameSiteStrictMode})
+ httpx.SetTokenCookie(c, newToken, int(expires.Seconds()))
}
}
c.Set(claimsKey, claims)
diff --git a/internal/server/middleware/rate_limit.go b/internal/server/middleware/rate_limit.go
index b45c69e..6ed4aed 100644
--- a/internal/server/middleware/rate_limit.go
+++ b/internal/server/middleware/rate_limit.go
@@ -1,8 +1,6 @@
package middleware
import (
- "net/http"
- "strconv"
"time"
"kra/internal/server/httpx"
@@ -26,11 +24,11 @@ func SecurityRateLimit(system *service.SystemService, settings *service.Settings
if window < 1 {
window = 60
}
- bucket := time.Now().Unix() / int64(window)
- key := "rate:" + c.ClientIP() + ":" + strconv.FormatInt(bucket, 10)
- count, cacheErr := system.CacheIncrement(c.Request.Context(), key, time.Duration(window+1)*time.Second)
+ key := "KRA_SecLimit" + c.ClientIP() + c.FullPath()
+ count, cacheErr := system.CacheIncrement(c.Request.Context(), key, time.Duration(window)*time.Second)
if cacheErr == nil && int(count) > config.LimitCount {
- c.AbortWithStatusJSON(http.StatusTooManyRequests, httpx.Response{Code: httpx.CodeError, Data: nil, Msg: "请求过于频繁"})
+ httpx.Fail(c, "请求太过频繁,请稍后再试")
+ c.Abort()
return
}
c.Next()
diff --git a/internal/server/router/api.go b/internal/server/router/api.go
index 58681fe..13b3e6f 100644
--- a/internal/server/router/api.go
+++ b/internal/server/router/api.go
@@ -6,7 +6,7 @@ import (
"github.com/gin-gonic/gin"
)
-func RegisterAPI(group *gin.RouterGroup, engine *gin.Engine, h *handler.API) {
+func RegisterAPI(group, public *gin.RouterGroup, engine *gin.Engine, h *handler.API) {
api := group.Group("/api")
api.POST("/getApiList", h.List)
api.POST("/getAllApis", h.All)
@@ -21,7 +21,7 @@ func RegisterAPI(group *gin.RouterGroup, engine *gin.Engine, h *handler.API) {
api.GET("/syncApi", h.Sync(engine))
api.POST("/ignoreApi", h.Ignore)
api.POST("/enterSyncApi", h.ApplySync)
- api.GET("/freshCasbin", h.FreshCasbin)
+ public.Group("/api").GET("/freshCasbin", h.FreshCasbin)
casbin := group.Group("/casbin")
casbin.POST("/updateCasbin", h.SetPolicyPaths)
casbin.POST("/getPolicyPathByAuthorityId", h.PolicyPaths)
diff --git a/internal/service/README.md b/internal/service/README.md
index 42321b7..e36f059 100644
--- a/internal/service/README.md
+++ b/internal/service/README.md
@@ -1 +1,3 @@
# Service
+
+本目录负责管理端 DTO 与领域对象之间的转换和输入校验,不直接访问数据库。固定 JSON 契约放在 `dto/`,只有用户设置、导出行等真正动态的数据使用 `map[string]any`。
diff --git a/internal/service/access.go b/internal/service/access.go
index d0d3e01..f460aef 100644
--- a/internal/service/access.go
+++ b/internal/service/access.go
@@ -2,7 +2,6 @@ package service
import (
"context"
- "strings"
"kra/internal/biz"
"kra/internal/service/dto"
@@ -15,54 +14,37 @@ func (s *AccessService) Authorize(ctx context.Context, aid uint, path, method st
return s.uc.Authorize(ctx, aid, path, method)
}
-func authorityDTO(v *biz.Authority) map[string]any { return convertAuthority(*v) }
-func apiDTO(v *biz.API) map[string]any {
- return map[string]any{"ID": v.ID, "CreatedAt": v.CreatedAt, "UpdatedAt": v.UpdatedAt, "DeletedAt": nil, "path": v.Path, "description": v.Description, "apiGroup": v.APIGroup, "method": v.Method}
-}
-func departmentDTO(v *biz.Department) map[string]any {
+func authorityDTO(v *biz.Authority) map[string]any {
+ out := convertAuthority(*v)
children := make([]map[string]any, 0, len(v.Children))
- for _, c := range v.Children {
- children = append(children, departmentDTO(c))
+ for _, child := range v.Children {
+ children = append(children, authorityDTO(child))
}
- return map[string]any{"ID": v.ID, "CreatedAt": v.CreatedAt, "UpdatedAt": v.UpdatedAt, "DeletedAt": nil, "name": v.Name, "parentId": v.ParentID, "ancestors": v.Ancestors, "sort": v.Sort, "leaderId": v.LeaderID, "leader": nil, "status": v.Status, "children": children, "namePath": ""}
+ out["children"] = children
+ return out
}
-func positionDTO(v *biz.Position) map[string]any {
- return map[string]any{"ID": v.ID, "CreatedAt": v.CreatedAt, "UpdatedAt": v.UpdatedAt, "DeletedAt": nil, "name": v.Name, "code": v.Code, "sort": v.Sort, "status": v.Status, "remark": v.Remark}
-}
-
func (s *AccessService) Authorities(ctx context.Context) ([]map[string]any, error) {
- items, err := s.uc.Repo().ListAuthorities(ctx)
+ items, err := s.uc.AuthorityTree(ctx)
if err != nil {
return nil, err
}
- byID := make(map[uint]map[string]any, len(items))
+ roots := make([]map[string]any, 0, len(items))
for _, v := range items {
- byID[v.AuthorityID] = authorityDTO(v)
- }
- roots := make([]map[string]any, 0)
- for _, v := range items {
- item := byID[v.AuthorityID]
- if v.ParentID != nil && *v.ParentID != 0 && byID[*v.ParentID] != nil {
- parent := byID[*v.ParentID]
- children, _ := parent["children"].([]map[string]any)
- parent["children"] = append(children, item)
- } else {
- roots = append(roots, item)
- }
+ roots = append(roots, authorityDTO(v))
}
return roots, nil
}
func (s *AccessService) CreateAuthority(ctx context.Context, v *biz.Authority) error {
- return s.uc.Repo().CreateAuthority(ctx, v)
+ return s.uc.CreateAuthority(ctx, v)
}
func (s *AccessService) CopyAuthority(ctx context.Context, sourceID uint, v *biz.Authority) error {
- return s.uc.Repo().CopyAuthority(ctx, sourceID, v)
+ return s.uc.CopyAuthority(ctx, sourceID, v)
}
func (s *AccessService) UpdateAuthority(ctx context.Context, v *biz.Authority) error {
- return s.uc.Repo().UpdateAuthority(ctx, v)
+ return s.uc.UpdateAuthority(ctx, v)
}
func (s *AccessService) DeleteAuthority(ctx context.Context, id uint) error {
- return s.uc.Repo().DeleteAuthority(ctx, id)
+ return s.uc.DeleteAuthority(ctx, id)
}
func authorityDomain(req *dto.AuthorityRequest) *biz.Authority {
return &biz.Authority{AuthorityID: req.AuthorityID, AuthorityName: req.AuthorityName, ParentID: req.ParentID, DataScope: req.DataScope, DefaultRouter: req.DefaultRouter}
@@ -92,19 +74,19 @@ func (s *AccessService) UpdateAuthorityRequest(ctx context.Context, req *dto.Aut
return authorityResponse(value), nil
}
func (s *AccessService) SetAuthorityUsers(ctx context.Context, id uint, ids []uint) error {
- return s.uc.Repo().SetAuthorityUsers(ctx, id, ids)
+ return s.uc.SetAuthorityUsers(ctx, id, ids)
}
func (s *AccessService) AuthorityUserIDs(ctx context.Context, id uint) ([]uint, error) {
- return s.uc.Repo().AuthorityUserIDs(ctx, id)
+ return s.uc.AuthorityUserIDs(ctx, id)
}
func (s *AccessService) SetDataScope(ctx context.Context, id uint, scope int, ids []uint) error {
- return s.uc.Repo().SetDataScope(ctx, id, scope, ids)
+ return s.uc.SetDataScope(ctx, id, scope, ids)
}
func (s *AccessService) DataScopeDepartmentIDs(ctx context.Context, id uint) ([]uint, error) {
- return s.uc.Repo().DataScopeDepartmentIDs(ctx, id)
+ return s.uc.DataScopeDepartmentIDs(ctx, id)
}
func (s *AccessService) ResolveDataScope(ctx context.Context, authorityID, userID uint) (biz.DataScope, error) {
- return s.uc.Repo().ResolveDataScope(ctx, authorityID, userID)
+ return s.uc.ResolveDataScope(ctx, authorityID, userID)
}
func (s *AccessService) ContextWithDataScope(ctx context.Context, authorityID, userID uint) (context.Context, error) {
scope, err := s.ResolveDataScope(ctx, authorityID, userID)
@@ -114,62 +96,8 @@ func (s *AccessService) ContextWithDataScope(ctx context.Context, authorityID, u
return biz.NewDataScopeContext(ctx, scope), nil
}
-func (s *AccessService) DeleteAPIs(ctx context.Context, ids []uint) error {
- return s.uc.Repo().DeleteAPIs(ctx, ids)
-}
-func (s *AccessService) APIRoleIDs(ctx context.Context, id uint) ([]uint, error) {
- return s.uc.Repo().APIRoleIDs(ctx, id)
-}
-func (s *AccessService) SetAPIRoles(ctx context.Context, id uint, ids []uint) error {
- return s.uc.Repo().SetAPIRoles(ctx, id, ids)
-}
-func (s *AccessService) SyncAPIs(ctx context.Context, routes []*biz.API) (*dto.APISyncResponse, error) {
- stored, _, err := s.uc.Repo().ListAPIs(ctx, 1, 100000, nil)
- if err != nil {
- return nil, err
- }
- ignored, err := s.uc.Repo().IgnoredAPIs(ctx)
- if err != nil {
- return nil, err
- }
- key := func(v *biz.API) string { return strings.ToUpper(v.Method) + " " + v.Path }
- ignoreSet := make(map[string]bool, len(ignored))
- for _, item := range ignored {
- ignoreSet[key(item)] = true
- }
- routeSet := make(map[string]*biz.API, len(routes))
- for _, item := range routes {
- if !ignoreSet[key(item)] {
- routeSet[key(item)] = item
- }
- }
- storedSet := make(map[string]*biz.API, len(stored))
- for _, item := range stored {
- storedSet[key(item)] = item
- }
- added := make([]*dto.APIResponse, 0)
- deleted := make([]*dto.APIResponse, 0)
- ignoredDTO := make([]*dto.APIResponse, 0, len(ignored))
- for k, item := range routeSet {
- if storedSet[k] == nil {
- added = append(added, apiResponse(item))
- }
- }
- for k, item := range storedSet {
- if routeSet[k] == nil && !ignoreSet[k] {
- deleted = append(deleted, apiResponse(item))
- }
- }
- for _, item := range ignored {
- ignoredDTO = append(ignoredDTO, apiResponse(item))
- }
- return &dto.APISyncResponse{NewAPIs: added, DeleteAPIs: deleted, IgnoreAPIs: ignoredDTO}, nil
-}
-func (s *AccessService) SetAPIIgnored(ctx context.Context, path, method string, ignored bool) error {
- return s.uc.Repo().SetAPIIgnored(ctx, path, method, ignored)
-}
func (s *AccessService) Buttons(ctx context.Context, id uint) ([]map[string]any, error) {
- items, err := s.uc.Repo().Buttons(ctx, id)
+ items, err := s.uc.Buttons(ctx, id)
if err != nil {
return nil, err
}
@@ -180,17 +108,17 @@ func (s *AccessService) Buttons(ctx context.Context, id uint) ([]map[string]any,
return out, nil
}
func (s *AccessService) SetAuthorityButtons(ctx context.Context, id uint, v map[uint][]uint) error {
- return s.uc.Repo().SetAuthorityButtons(ctx, id, v)
+ return s.uc.SetAuthorityButtons(ctx, id, v)
}
func (s *AccessService) AuthorityButtonIDs(ctx context.Context, id uint) ([]uint, error) {
- return s.uc.Repo().AuthorityButtonIDs(ctx, id)
+ return s.uc.AuthorityButtonIDs(ctx, id)
}
func (s *AccessService) SelectedButtons(ctx context.Context, aid, menuID uint) ([]uint, error) {
- return s.uc.Repo().SelectedButtons(ctx, aid, menuID)
+ return s.uc.SelectedButtons(ctx, aid, menuID)
}
func (s *AccessService) SetSelectedButtons(ctx context.Context, aid, menuID uint, ids []uint) error {
- return s.uc.Repo().SetSelectedButtons(ctx, aid, menuID, ids)
+ return s.uc.SetSelectedButtons(ctx, aid, menuID, ids)
}
func (s *AccessService) CanRemoveButton(ctx context.Context, id uint) (bool, error) {
- return s.uc.Repo().CanRemoveButton(ctx, id)
+ return s.uc.CanRemoveButton(ctx, id)
}
diff --git a/internal/service/api.go b/internal/service/api.go
index 1ab08a7..26ff9a2 100644
--- a/internal/service/api.go
+++ b/internal/service/api.go
@@ -24,29 +24,33 @@ func apiResponses(items []*biz.API) []*dto.APIResponse {
}
func (s *AccessService) ListAPI(ctx context.Context, req *dto.APIListRequest) ([]*dto.APIResponse, int64, error) {
- items, total, err := s.uc.Repo().ListAPIs(ctx, req.Page, req.PageSize, &biz.API{Path: req.Path, Description: req.Description, APIGroup: req.APIGroup, Method: req.Method})
+ items, total, err := s.uc.ListAPIs(ctx, req.Page, req.PageSize, &biz.API{Path: req.Path, Description: req.Description, APIGroup: req.APIGroup, Method: req.Method, OrderKey: req.OrderKey, Desc: req.Desc})
if err != nil {
return nil, 0, err
}
return apiResponses(items), total, nil
}
-func (s *AccessService) AllAPI(ctx context.Context) ([]*dto.APIResponse, error) {
- items, _, err := s.uc.Repo().ListAPIs(ctx, 1, 10000, nil)
+func (s *AccessService) AllAPI(ctx context.Context, strict bool) ([]*dto.APIResponse, error) {
+ items, _, err := s.uc.ListAPIs(ctx, 0, 0, &biz.API{StrictAll: strict})
if err != nil {
return nil, err
}
return apiResponses(items), nil
}
-func (s *AccessService) CreateAPIRequest(ctx context.Context, req *dto.APIRequest) error {
- return s.uc.Repo().CreateAPI(ctx, apiDomain(req))
+func (s *AccessService) CreateAPIRequest(ctx context.Context, req *dto.APIRequest) (*dto.APIResponse, error) {
+ value := apiDomain(req)
+ if err := s.uc.CreateAPI(ctx, value); err != nil {
+ return nil, err
+ }
+ return apiResponse(value), nil
}
func (s *AccessService) UpdateAPIRequest(ctx context.Context, req *dto.APIRequest) error {
- return s.uc.Repo().UpdateAPI(ctx, apiDomain(req))
+ return s.uc.UpdateAPI(ctx, apiDomain(req))
}
func (s *AccessService) FindAPIResponse(ctx context.Context, id uint) (*dto.APIResponse, error) {
- value, err := s.uc.Repo().FindAPI(ctx, id)
+ value, err := s.uc.FindAPI(ctx, id)
if err != nil {
return nil, err
}
@@ -61,17 +65,17 @@ func (s *AccessService) ApplyAPISyncRequest(ctx context.Context, req *dto.ApplyA
for i := range req.DeleteAPIs {
deleted = append(deleted, apiDomain(&req.DeleteAPIs[i]))
}
- return s.uc.Repo().ApplyAPISync(ctx, added, deleted)
+ return s.uc.ApplyAPISync(ctx, added, deleted)
}
func (s *AccessService) SetPolicyPathsRequest(ctx context.Context, req *dto.SetPolicyPathsRequest) error {
paths := make([]*biz.API, 0, len(req.Infos))
for _, value := range req.Infos {
paths = append(paths, &biz.API{Path: value.Path, Method: value.Method})
}
- return s.uc.Repo().SetPolicyPaths(ctx, req.AuthorityID, paths)
+ return s.uc.SetPolicyPaths(ctx, req.AuthorityID, paths)
}
func (s *AccessService) PolicyPathResponses(ctx context.Context, authorityID uint) ([]dto.PolicyPathRequest, error) {
- items, err := s.uc.Repo().PolicyPaths(ctx, authorityID)
+ items, err := s.uc.PolicyPaths(ctx, authorityID)
if err != nil {
return nil, err
}
@@ -88,3 +92,26 @@ func (s *AccessService) SyncAPIResponses(ctx context.Context, routes []dto.APIRe
}
return s.SyncAPIs(ctx, items)
}
+
+func apiDTO(v *biz.API) map[string]any {
+ return map[string]any{"ID": v.ID, "CreatedAt": v.CreatedAt, "UpdatedAt": v.UpdatedAt, "DeletedAt": nil, "path": v.Path, "description": v.Description, "apiGroup": v.APIGroup, "method": v.Method}
+}
+func (s *AccessService) DeleteAPIs(ctx context.Context, ids []uint) error {
+ return s.uc.DeleteAPIs(ctx, ids)
+}
+func (s *AccessService) APIRoleIDs(ctx context.Context, path, method string) ([]uint, error) {
+ return s.uc.APIRoleIDs(ctx, path, method)
+}
+func (s *AccessService) SetAPIRoles(ctx context.Context, path, method string, ids []uint) error {
+ return s.uc.SetAPIRoles(ctx, path, method, ids)
+}
+func (s *AccessService) SyncAPIs(ctx context.Context, routes []*biz.API) (*dto.APISyncResponse, error) {
+ diff, err := s.uc.SyncAPIs(ctx, routes)
+ if err != nil {
+ return nil, err
+ }
+ return &dto.APISyncResponse{NewAPIs: apiResponses(diff.Added), DeleteAPIs: apiResponses(diff.Deleted), IgnoreAPIs: apiResponses(diff.Ignored)}, nil
+}
+func (s *AccessService) SetAPIIgnored(ctx context.Context, path, method string, ignored bool) error {
+ return s.uc.SetAPIIgnored(ctx, path, method, ignored)
+}
diff --git a/internal/service/api_token.go b/internal/service/api_token.go
index a97286e..ea00ed7 100644
--- a/internal/service/api_token.go
+++ b/internal/service/api_token.go
@@ -2,7 +2,6 @@ package service
import (
"context"
- "errors"
"time"
"kra/internal/biz"
@@ -18,29 +17,20 @@ func tokenDTO(v *biz.APIToken) map[string]any {
}
func (s *SettingsService) CreateAPIToken(ctx context.Context, userID, authorityID uint, days int, remark string) (string, error) {
- user, allowed, err := s.Repo().UserHasAuthority(ctx, userID, authorityID)
+ user, duration, err := s.uc.PrepareAPIToken(ctx, userID, authorityID, days)
if err != nil {
return "", err
}
- if !allowed {
- return "", errors.New("用户不具备该角色权限")
- }
- duration := time.Duration(days) * 24 * time.Hour
- if days == -1 {
- duration = 100 * 365 * 24 * time.Hour
- }
- if duration <= 0 {
- return "", errors.New("有效天数必须大于0或为-1")
- }
secret, issuer := "", "kra"
buffer := 24 * time.Hour
- if s.config != nil && s.config.Jwt != nil {
- secret = s.config.Jwt.SigningKey
- if s.config.Jwt.Issuer != "" {
- issuer = s.config.Jwt.Issuer
+ config := s.runtime.Admin()
+ if config != nil && config.Jwt != nil {
+ secret = config.Jwt.SigningKey
+ if config.Jwt.Issuer != "" {
+ issuer = config.Jwt.Issuer
}
- if s.config.Jwt.BufferTime != nil {
- buffer = s.config.Jwt.BufferTime.AsDuration()
+ if config.Jwt.BufferTime != nil {
+ buffer = config.Jwt.BufferTime.AsDuration()
}
}
token, _, err := adminauth.Generate(secret, issuer, duration, buffer, user.ID, authorityID, user.UUID, user.Username, user.NickName, false)
@@ -48,13 +38,13 @@ func (s *SettingsService) CreateAPIToken(ctx context.Context, userID, authorityI
return "", err
}
record := &biz.APIToken{UserID: userID, AuthorityID: authorityID, Token: token, Status: true, ExpiresAt: time.Now().Add(duration), Remark: remark}
- if err = s.Repo().CreateAPIToken(ctx, record); err != nil {
+ if err = s.uc.CreateAPIToken(ctx, record); err != nil {
return "", err
}
return token, nil
}
func (s *SettingsService) APITokens(ctx context.Context, page, size int, userID uint, status *bool) ([]map[string]any, int64, error) {
- items, total, err := s.Repo().ListAPITokens(ctx, page, size, userID, status)
+ items, total, err := s.uc.ListAPITokens(ctx, page, size, userID, status)
if err != nil {
return nil, 0, err
}
@@ -65,15 +55,11 @@ func (s *SettingsService) APITokens(ctx context.Context, page, size int, userID
return out, total, nil
}
func (s *SettingsService) DisableAPIToken(ctx context.Context, id uint) error {
- token, err := s.Repo().DisableAPIToken(ctx, id)
- if err != nil {
- return err
- }
- return s.Repo().BlacklistToken(ctx, token)
+ return s.uc.DisableToken(ctx, id)
}
func (s *SettingsService) BlacklistToken(ctx context.Context, token string) error {
- return s.Repo().BlacklistToken(ctx, token)
+ return s.uc.BlacklistToken(ctx, token)
}
func (s *SettingsService) IsTokenDisabled(ctx context.Context, token string) (bool, error) {
- return s.Repo().IsTokenDisabled(ctx, token)
+ return s.uc.IsTokenDisabled(ctx, token)
}
diff --git a/internal/service/audit.go b/internal/service/audit.go
index 20b06f4..5931ffa 100644
--- a/internal/service/audit.go
+++ b/internal/service/audit.go
@@ -2,6 +2,7 @@ package service
import (
"context"
+
"kra/internal/biz"
"kra/internal/service/dto"
)
@@ -9,44 +10,25 @@ import (
type AuditService struct{ uc *biz.AuditUsecase }
func NewAuditService(uc *biz.AuditUsecase) *AuditService { return &AuditService{uc: uc} }
+
func (s *AuditService) RecordOperation(ctx context.Context, v *biz.OperationRecord) error {
- return s.uc.Repo().RecordOperation(ctx, v)
+ return s.uc.RecordOperation(ctx, v)
}
func (s *AuditService) RecordOperationRequest(ctx context.Context, value *dto.OperationRecordRequest) error {
return s.RecordOperation(ctx, &biz.OperationRecord{IP: value.IP, Method: value.Method, Path: value.Path, Status: value.Status, LatencyMS: value.LatencyMS, Agent: value.Agent, ErrorMessage: value.ErrorMessage, Body: value.Body, Response: value.Response, UserID: value.UserID, RequestID: value.RequestID, TraceID: value.TraceID, DeviceID: value.DeviceID})
}
-func (s *AuditService) OperationsFilter(ctx context.Context, page, size int, path, method string, status int, ip string, userID uint) ([]map[string]any, int64, error) {
- return s.Operations(ctx, page, size, &biz.OperationRecord{Path: path, Method: method, Status: status, IP: ip, UserID: userID})
-}
-func (s *AuditService) RecordLogin(ctx context.Context, v *biz.LoginLog) error {
- return s.uc.Repo().RecordLogin(ctx, v)
-}
-func (s *AuditService) RecordLoginRequest(ctx context.Context, value *dto.LoginLogRequest) error {
- return s.RecordLogin(ctx, &biz.LoginLog{Username: value.Username, IP: value.IP, Status: value.Status, ErrorMessage: value.ErrorMessage, Agent: value.Agent, UserID: value.UserID})
-}
-func (s *AuditService) LoginsFilter(ctx context.Context, page, size int, username, ip string) ([]map[string]any, int64, error) {
- return s.Logins(ctx, page, size, &biz.LoginLog{Username: username, IP: ip})
-}
-func (s *AuditService) RecordDataAccess(ctx context.Context, v *biz.DataAccessLog) error {
- return s.uc.Repo().RecordDataAccess(ctx, v)
-}
-func (s *AuditService) RecordDataAccessRequest(ctx context.Context, value *dto.DataAccessRecordRequest) error {
- return s.RecordDataAccess(ctx, &biz.DataAccessLog{EventType: value.EventType, Operation: value.Operation, UserID: value.UserID, AuthorityID: value.AuthorityID, RequestID: value.RequestID, Method: value.Method, Path: value.Path, Detail: value.Detail})
-}
-func (s *AuditService) DataAccessRequest(ctx context.Context, req *dto.DataAccessListRequest) ([]map[string]any, int64, error) {
- return s.DataAccess(ctx, req.Page, req.PageSize, &biz.DataAccessLog{EventType: req.EventType, TargetTable: req.TargetTable, UserID: req.UserID})
+func (s *AuditService) OperationsFilter(ctx context.Context, page, size int, path, method string, status int) ([]map[string]any, int64, error) {
+ return s.Operations(ctx, page, size, &biz.OperationRecord{Path: path, Method: method, Status: status})
}
func opDTO(v *biz.OperationRecord) map[string]any {
- return map[string]any{"ID": v.ID, "CreatedAt": v.CreatedAt, "DeletedAt": nil, "ip": v.IP, "method": v.Method, "path": v.Path, "status": v.Status, "latency_ms": v.LatencyMS, "agent": v.Agent, "error_message": v.ErrorMessage, "body": v.Body, "resp": v.Response, "user_id": v.UserID, "request_id": v.RequestID, "trace_id": v.TraceID, "device_id": v.DeviceID, "user": nil}
-}
-func loginDTO(v *biz.LoginLog) map[string]any {
- return map[string]any{"ID": v.ID, "CreatedAt": v.CreatedAt, "DeletedAt": nil, "username": v.Username, "ip": v.IP, "status": v.Status, "errorMessage": v.ErrorMessage, "agent": v.Agent, "userId": v.UserID, "user": nil}
-}
-func dataAccessDTO(v *biz.DataAccessLog) map[string]any {
- return map[string]any{"ID": v.ID, "CreatedAt": v.CreatedAt, "DeletedAt": nil, "eventType": v.EventType, "targetTable": v.TargetTable, "operation": v.Operation, "userId": v.UserID, "authorityId": v.AuthorityID, "scope": v.Scope, "requestId": v.RequestID, "method": v.Method, "path": v.Path, "detail": v.Detail}
+ user := map[string]any{}
+ if v.User != nil {
+ user = convertUser(v.User)
+ }
+ return map[string]any{"ID": v.ID, "CreatedAt": v.CreatedAt, "DeletedAt": nil, "ip": v.IP, "method": v.Method, "path": v.Path, "status": v.Status, "latency_ms": v.LatencyMS, "agent": v.Agent, "error_message": v.ErrorMessage, "body": v.Body, "resp": v.Response, "user_id": v.UserID, "request_id": v.RequestID, "trace_id": v.TraceID, "device_id": v.DeviceID, "user": user}
}
func (s *AuditService) Operations(ctx context.Context, page, size int, q *biz.OperationRecord) ([]map[string]any, int64, error) {
- items, total, err := s.uc.Repo().ListOperations(ctx, page, size, q)
+ items, total, err := s.uc.ListOperations(ctx, page, size, q)
if err != nil {
return nil, 0, err
}
@@ -57,17 +39,34 @@ func (s *AuditService) Operations(ctx context.Context, page, size int, q *biz.Op
return out, total, nil
}
func (s *AuditService) Operation(ctx context.Context, id uint) (map[string]any, error) {
- v, err := s.uc.Repo().FindOperation(ctx, id)
+ v, err := s.uc.FindOperation(ctx, id)
if err != nil {
return nil, err
}
return opDTO(v), nil
}
func (s *AuditService) DeleteOperations(ctx context.Context, ids []uint) error {
- return s.uc.Repo().DeleteOperations(ctx, ids)
+ return s.uc.DeleteOperations(ctx, ids)
+}
+
+func (s *AuditService) RecordLogin(ctx context.Context, v *biz.LoginLog) error {
+ return s.uc.RecordLogin(ctx, v)
+}
+func (s *AuditService) RecordLoginRequest(ctx context.Context, value *dto.LoginLogRequest) error {
+ return s.RecordLogin(ctx, &biz.LoginLog{Username: value.Username, IP: value.IP, Status: value.Status, ErrorMessage: value.ErrorMessage, Agent: value.Agent, UserID: value.UserID})
+}
+func (s *AuditService) LoginsFilter(ctx context.Context, page, size int, username string, status bool) ([]map[string]any, int64, error) {
+ return s.Logins(ctx, page, size, &biz.LoginLog{Username: username, Status: status, FilterByStatus: status})
+}
+func loginDTO(v *biz.LoginLog) map[string]any {
+ user := map[string]any{}
+ if v.User != nil {
+ user = convertUser(v.User)
+ }
+ return map[string]any{"ID": v.ID, "CreatedAt": v.CreatedAt, "DeletedAt": nil, "username": v.Username, "ip": v.IP, "status": v.Status, "errorMessage": v.ErrorMessage, "agent": v.Agent, "userId": v.UserID, "user": user}
}
func (s *AuditService) Logins(ctx context.Context, page, size int, q *biz.LoginLog) ([]map[string]any, int64, error) {
- items, total, err := s.uc.Repo().ListLogins(ctx, page, size, q)
+ items, total, err := s.uc.ListLogins(ctx, page, size, q)
if err != nil {
return nil, 0, err
}
@@ -78,17 +77,30 @@ func (s *AuditService) Logins(ctx context.Context, page, size int, q *biz.LoginL
return out, total, nil
}
func (s *AuditService) Login(ctx context.Context, id uint) (map[string]any, error) {
- v, err := s.uc.Repo().FindLogin(ctx, id)
+ v, err := s.uc.FindLogin(ctx, id)
if err != nil {
return nil, err
}
return loginDTO(v), nil
}
func (s *AuditService) DeleteLogins(ctx context.Context, ids []uint) error {
- return s.uc.Repo().DeleteLogins(ctx, ids)
+ return s.uc.DeleteLogins(ctx, ids)
+}
+
+func (s *AuditService) RecordDataAccess(ctx context.Context, v *biz.DataAccessLog) error {
+ return s.uc.RecordDataAccess(ctx, v)
+}
+func (s *AuditService) RecordDataAccessRequest(ctx context.Context, value *dto.DataAccessRecordRequest) error {
+ return s.RecordDataAccess(ctx, &biz.DataAccessLog{EventType: value.EventType, Operation: value.Operation, UserID: value.UserID, AuthorityID: value.AuthorityID, RequestID: value.RequestID, Method: value.Method, Path: value.Path, Detail: value.Detail})
+}
+func (s *AuditService) DataAccessRequest(ctx context.Context, req *dto.DataAccessListRequest) ([]map[string]any, int64, error) {
+ return s.DataAccess(ctx, req.Page, req.PageSize, &biz.DataAccessLog{EventType: req.EventType, TargetTable: req.TargetTable, UserID: req.UserID})
+}
+func dataAccessDTO(v *biz.DataAccessLog) map[string]any {
+ return map[string]any{"ID": v.ID, "CreatedAt": v.CreatedAt, "DeletedAt": nil, "eventType": v.EventType, "targetTable": v.TargetTable, "operation": v.Operation, "userId": v.UserID, "authorityId": v.AuthorityID, "scope": v.Scope, "requestId": v.RequestID, "method": v.Method, "path": v.Path, "detail": v.Detail}
}
func (s *AuditService) DataAccess(ctx context.Context, page, size int, q *biz.DataAccessLog) ([]map[string]any, int64, error) {
- items, total, err := s.uc.Repo().ListDataAccess(ctx, page, size, q)
+ items, total, err := s.uc.ListDataAccess(ctx, page, size, q)
if err != nil {
return nil, 0, err
}
@@ -99,76 +111,5 @@ func (s *AuditService) DataAccess(ctx context.Context, page, size int, q *biz.Da
return out, total, nil
}
func (s *AuditService) DeleteDataAccess(ctx context.Context, ids []uint) error {
- return s.uc.Repo().DeleteDataAccess(ctx, ids)
-}
-func (s *AuditService) LogDates(ctx context.Context, month string) (map[string]any, error) {
- items, err := s.uc.Repo().LogDates(ctx, month)
- if err != nil {
- return nil, err
- }
- out := make([]map[string]any, 0, len(items))
- for _, v := range items {
- out = append(out, map[string]any{"date": v.Date, "fileCount": v.FileCount})
- }
- return map[string]any{"month": month, "dates": out}, nil
-}
-func (s *AuditService) LogFiles(ctx context.Context, date string) (map[string]any, error) {
- items, err := s.uc.Repo().LogFiles(ctx, date)
- if err != nil {
- return nil, err
- }
- out := make([]map[string]any, 0, len(items))
- for _, v := range items {
- out = append(out, map[string]any{"path": v.Path, "name": v.Name, "size": v.Size, "modifiedAt": v.ModifiedAt})
- }
- return map[string]any{"date": date, "files": out}, nil
-}
-func (s *AuditService) LogContent(ctx context.Context, date, path string, cursor *int64) (map[string]any, error) {
- v, err := s.uc.Repo().LogContent(ctx, date, path, cursor)
- if err != nil {
- return nil, err
- }
- return map[string]any{"date": v.Date, "path": v.Path, "content": v.Content, "lineCount": v.LineCount, "nextCursor": v.NextCursor, "hasMore": v.HasMore, "limitedByBytes": v.LimitedByBytes, "size": v.Size, "modifiedAt": v.ModifiedAt}, nil
-}
-func errorDTO(v *biz.ErrorRecord) map[string]any {
- return map[string]any{"ID": v.ID, "CreatedAt": v.CreatedAt, "DeletedAt": nil, "form": v.Form, "info": v.Info, "level": v.Level, "request_id": v.RequestID, "trace_id": v.TraceID, "solution": v.Solution, "status": v.Status}
-}
-func (s *AuditService) CreateError(ctx context.Context, v *biz.ErrorRecord) error {
- return s.uc.Repo().CreateError(ctx, v)
-}
-func errorDomain(value *dto.ErrorRecordRequest) *biz.ErrorRecord {
- return &biz.ErrorRecord{ID: value.ID, Form: value.Form, Info: value.Info, Level: value.Level, RequestID: value.RequestID, TraceID: value.TraceID, Solution: value.Solution, Status: value.Status}
-}
-func (s *AuditService) CreateErrorRequest(ctx context.Context, req *dto.ErrorRecordRequest) error {
- return s.CreateError(ctx, errorDomain(req))
-}
-func (s *AuditService) UpdateErrorRequest(ctx context.Context, req *dto.ErrorRecordRequest) error {
- return s.UpdateError(ctx, errorDomain(req))
-}
-func (s *AuditService) ErrorsFilter(ctx context.Context, page, size int, form, level, status string) ([]map[string]any, int64, error) {
- return s.Errors(ctx, page, size, &biz.ErrorRecord{Form: form, Level: level, Status: status})
-}
-func (s *AuditService) UpdateError(ctx context.Context, v *biz.ErrorRecord) error {
- return s.uc.Repo().UpdateError(ctx, v)
-}
-func (s *AuditService) DeleteErrors(ctx context.Context, ids []uint) error {
- return s.uc.Repo().DeleteErrors(ctx, ids)
-}
-func (s *AuditService) Error(ctx context.Context, id uint) (map[string]any, error) {
- v, err := s.uc.Repo().FindError(ctx, id)
- if err != nil {
- return nil, err
- }
- return errorDTO(v), nil
-}
-func (s *AuditService) Errors(ctx context.Context, page, size int, q *biz.ErrorRecord) ([]map[string]any, int64, error) {
- items, total, err := s.uc.Repo().ListErrors(ctx, page, size, q)
- if err != nil {
- return nil, 0, err
- }
- out := make([]map[string]any, 0, len(items))
- for _, v := range items {
- out = append(out, errorDTO(v))
- }
- return out, total, nil
+ return s.uc.DeleteDataAccess(ctx, ids)
}
diff --git a/internal/service/audit_error.go b/internal/service/audit_error.go
new file mode 100644
index 0000000..644703f
--- /dev/null
+++ b/internal/service/audit_error.go
@@ -0,0 +1,52 @@
+package service
+
+import (
+ "context"
+ "time"
+
+ "kra/internal/biz"
+ "kra/internal/service/dto"
+)
+
+func errorDTO(v *biz.ErrorRecord) map[string]any {
+ return map[string]any{"ID": v.ID, "CreatedAt": v.CreatedAt, "DeletedAt": nil, "form": v.Form, "info": v.Info, "level": v.Level, "request_id": v.RequestID, "trace_id": v.TraceID, "solution": v.Solution, "status": v.Status}
+}
+func (s *AuditService) CreateError(ctx context.Context, v *biz.ErrorRecord) error {
+ return s.uc.CreateError(ctx, v)
+}
+func errorDomain(value *dto.ErrorRecordRequest) *biz.ErrorRecord {
+ return &biz.ErrorRecord{ID: value.ID, Form: value.Form, Info: value.Info, Level: value.Level, RequestID: value.RequestID, TraceID: value.TraceID, Solution: value.Solution, Status: value.Status}
+}
+func (s *AuditService) CreateErrorRequest(ctx context.Context, req *dto.ErrorRecordRequest) error {
+ return s.CreateError(ctx, errorDomain(req))
+}
+func (s *AuditService) UpdateErrorRequest(ctx context.Context, req *dto.ErrorRecordRequest) error {
+ return s.UpdateError(ctx, errorDomain(req))
+}
+func (s *AuditService) ErrorsFilter(ctx context.Context, page, size int, form, info string, createdAtRange []time.Time) ([]map[string]any, int64, error) {
+ return s.Errors(ctx, page, size, &biz.ErrorRecord{Form: form, Info: info, CreatedAtRange: createdAtRange})
+}
+func (s *AuditService) UpdateError(ctx context.Context, v *biz.ErrorRecord) error {
+ return s.uc.UpdateError(ctx, v)
+}
+func (s *AuditService) DeleteErrors(ctx context.Context, ids []uint) error {
+ return s.uc.DeleteErrors(ctx, ids)
+}
+func (s *AuditService) Error(ctx context.Context, id uint) (map[string]any, error) {
+ v, err := s.uc.FindError(ctx, id)
+ if err != nil {
+ return nil, err
+ }
+ return errorDTO(v), nil
+}
+func (s *AuditService) Errors(ctx context.Context, page, size int, q *biz.ErrorRecord) ([]map[string]any, int64, error) {
+ items, total, err := s.uc.ListErrors(ctx, page, size, q)
+ if err != nil {
+ return nil, 0, err
+ }
+ out := make([]map[string]any, 0, len(items))
+ for _, v := range items {
+ out = append(out, errorDTO(v))
+ }
+ return out, total, nil
+}
diff --git a/internal/service/audit_log_file.go b/internal/service/audit_log_file.go
new file mode 100644
index 0000000..63d433d
--- /dev/null
+++ b/internal/service/audit_log_file.go
@@ -0,0 +1,35 @@
+package service
+
+import (
+ "context"
+)
+
+func (s *AuditService) LogDates(ctx context.Context, month string) (map[string]any, error) {
+ items, err := s.uc.LogDates(ctx, month)
+ if err != nil {
+ return nil, err
+ }
+ out := make([]map[string]any, 0, len(items))
+ for _, v := range items {
+ out = append(out, map[string]any{"date": v.Date, "fileCount": v.FileCount})
+ }
+ return map[string]any{"month": month, "dates": out}, nil
+}
+func (s *AuditService) LogFiles(ctx context.Context, date string) (map[string]any, error) {
+ items, err := s.uc.LogFiles(ctx, date)
+ if err != nil {
+ return nil, err
+ }
+ out := make([]map[string]any, 0, len(items))
+ for _, v := range items {
+ out = append(out, map[string]any{"path": v.Path, "name": v.Name, "size": v.Size, "modifiedAt": v.ModifiedAt})
+ }
+ return map[string]any{"date": date, "files": out}, nil
+}
+func (s *AuditService) LogContent(ctx context.Context, date, path string, cursor *int64) (map[string]any, error) {
+ v, err := s.uc.LogContent(ctx, date, path, cursor)
+ if err != nil {
+ return nil, err
+ }
+ return map[string]any{"date": v.Date, "path": v.Path, "content": v.Content, "lineCount": v.LineCount, "nextCursor": v.NextCursor, "hasMore": v.HasMore, "limitedByBytes": v.LimitedByBytes, "size": v.Size, "modifiedAt": v.ModifiedAt}, nil
+}
diff --git a/internal/service/authentication.go b/internal/service/authentication.go
index ff3a033..f2eeee4 100644
--- a/internal/service/authentication.go
+++ b/internal/service/authentication.go
@@ -34,29 +34,39 @@ func (s *SystemService) Login(ctx context.Context, username, password string) (*
return nil, err
}
if u.Enable != 1 {
- return nil, biz.ErrInvalidCredentials
+ return nil, biz.ErrUserDisabled
}
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
}
- return s.issueLogin(u, u.AuthorityID)
+ return s.issueLogin(ctx, u, u.AuthorityID)
}
-func (s *SystemService) issueLogin(user *biz.User, authorityID uint) (*LoginResult, error) {
+func (s *SystemService) issueLogin(ctx context.Context, user *biz.User, authorityID uint) (*LoginResult, error) {
expires, buffer := 7*24*time.Hour, 24*time.Hour
secret, issuer := "", "kra"
- if s.config != nil && s.config.Jwt != nil {
- secret, issuer = s.config.Jwt.SigningKey, s.config.Jwt.Issuer
- if s.config.Jwt.ExpiresTime != nil {
- expires = s.config.Jwt.ExpiresTime.AsDuration()
+ config := s.runtime.Admin()
+ if config != nil && config.Jwt != nil {
+ secret, issuer = config.Jwt.SigningKey, config.Jwt.Issuer
+ if config.Jwt.ExpiresTime != nil {
+ expires = config.Jwt.ExpiresTime.AsDuration()
}
- if s.config.Jwt.BufferTime != nil {
- buffer = s.config.Jwt.BufferTime.AsDuration()
+ if config.Jwt.BufferTime != nil {
+ buffer = config.Jwt.BufferTime.AsDuration()
}
}
token, claims, err := adminauth.Generate(secret, issuer, expires, buffer, user.ID, authorityID, user.UUID, user.Username, user.NickName, user.MustChangePassword)
if err != nil {
return nil, err
}
+ if s.settings.UseMultipoint() {
+ oldToken, _, cacheErr := s.settings.cache.Get(ctx, activeTokenKey(user.Username))
+ if cacheErr != nil {
+ return nil, cacheErr
+ }
+ if cacheErr = s.settings.RotateActiveToken(ctx, user.Username, oldToken, token, expires); cacheErr != nil {
+ return nil, cacheErr
+ }
+ }
return &LoginResult{User: convertUser(user), Token: token, ExpiresAt: claims.ExpiresAt.UnixMilli(), NeedChangePassword: user.MustChangePassword}, nil
}
diff --git a/internal/service/dictionary.go b/internal/service/dictionary.go
index f644f9f..6fb77d6 100644
--- a/internal/service/dictionary.go
+++ b/internal/service/dictionary.go
@@ -2,8 +2,6 @@ package service
import (
"context"
- "encoding/json"
- "errors"
"kra/internal/biz"
"kra/internal/service/dto"
@@ -16,25 +14,12 @@ func dictionaryDomain(value *dto.DictionaryRequest) *biz.Dictionary {
}
return &biz.Dictionary{ID: value.ID, Name: value.Name, Type: value.Type, Status: status, Desc: value.Description, ParentID: value.ParentID}
}
-func detailDomain(value *dto.DictionaryDetailRequest) *biz.DictionaryDetail {
- status := true
- if value.Status != nil {
- status = *value.Status
- }
- return &biz.DictionaryDetail{ID: value.ID, Label: value.Label, Value: value.Value, Extend: value.Extend, Status: status, Sort: value.Sort, DictionaryID: value.DictionaryID, ParentID: value.ParentID, Level: value.Level, Path: value.Path}
-}
func (s *SettingsService) CreateDictionaryRequest(ctx context.Context, req *dto.DictionaryRequest) error {
return s.CreateDictionary(ctx, dictionaryDomain(req))
}
func (s *SettingsService) UpdateDictionaryRequest(ctx context.Context, req *dto.DictionaryRequest) error {
return s.UpdateDictionary(ctx, dictionaryDomain(req))
}
-func (s *SettingsService) CreateDictionaryDetailRequest(ctx context.Context, req *dto.DictionaryDetailRequest) error {
- return s.CreateDictionaryDetail(ctx, detailDomain(req))
-}
-func (s *SettingsService) UpdateDictionaryDetailRequest(ctx context.Context, req *dto.DictionaryDetailRequest) error {
- return s.UpdateDictionaryDetail(ctx, detailDomain(req))
-}
func dictionaryDTO(v *biz.Dictionary) map[string]any {
children := make([]map[string]any, 0, len(v.Children))
for _, x := range v.Children {
@@ -46,15 +31,8 @@ func dictionaryDTO(v *biz.Dictionary) map[string]any {
}
return map[string]any{"ID": v.ID, "CreatedAt": v.CreatedAt, "UpdatedAt": v.UpdatedAt, "DeletedAt": nil, "name": v.Name, "type": v.Type, "status": v.Status, "desc": v.Desc, "parentID": v.ParentID, "children": children, "sysDictionaryDetails": details}
}
-func detailDTO(v *biz.DictionaryDetail) map[string]any {
- children := make([]map[string]any, 0, len(v.Children))
- for _, x := range v.Children {
- children = append(children, detailDTO(x))
- }
- return map[string]any{"ID": v.ID, "CreatedAt": v.CreatedAt, "UpdatedAt": v.UpdatedAt, "DeletedAt": nil, "label": v.Label, "value": v.Value, "extend": v.Extend, "status": v.Status, "sort": v.Sort, "sysDictionaryID": v.DictionaryID, "parentID": v.ParentID, "level": v.Level, "path": v.Path, "disabled": !v.Status, "children": children}
-}
func (s *SettingsService) Dictionaries(ctx context.Context, page, size int, name, typ string, details bool) ([]map[string]any, int64, error) {
- items, total, err := s.Repo().ListDictionaries(ctx, page, size, name, typ, details)
+ items, total, err := s.uc.ListDictionaries(ctx, page, size, name, typ, details)
if err != nil {
return nil, 0, err
}
@@ -64,24 +42,52 @@ func (s *SettingsService) Dictionaries(ctx context.Context, page, size int, name
}
return out, total, nil
}
-func (s *SettingsService) Dictionary(ctx context.Context, id uint, typ string, details bool) (map[string]any, error) {
- v, err := s.Repo().FindDictionary(ctx, id, typ, details)
+func (s *SettingsService) Dictionary(ctx context.Context, id uint, typ string, status *bool, details bool) (map[string]any, error) {
+ v, err := s.uc.FindDictionary(ctx, id, typ, status, details)
+ if err != nil {
+ return nil, err
+ }
+ return dictionaryDTO(v), nil
+}
+func (s *SettingsService) ExportDictionary(ctx context.Context, id uint) (map[string]any, error) {
+ v, err := s.uc.ExportDictionary(ctx, id)
if err != nil {
return nil, err
}
return dictionaryDTO(v), nil
}
func (s *SettingsService) CreateDictionary(ctx context.Context, v *biz.Dictionary) error {
- return s.Repo().CreateDictionary(ctx, v)
+ return s.uc.CreateDictionary(ctx, v)
}
func (s *SettingsService) UpdateDictionary(ctx context.Context, v *biz.Dictionary) error {
- return s.Repo().UpdateDictionary(ctx, v)
+ return s.uc.UpdateDictionary(ctx, v)
}
func (s *SettingsService) DeleteDictionary(ctx context.Context, id uint) error {
- return s.Repo().DeleteDictionary(ctx, id)
+ return s.uc.DeleteDictionary(ctx, id)
}
-func (s *SettingsService) DictionaryDetails(ctx context.Context, page, size int, dictionaryID uint, label, value string) ([]map[string]any, int64, error) {
- items, total, err := s.Repo().ListDictionaryDetails(ctx, page, size, dictionaryID, label, value)
+
+func detailDomain(value *dto.DictionaryDetailRequest) *biz.DictionaryDetail {
+ status := true
+ if value.Status != nil {
+ status = *value.Status
+ }
+ return &biz.DictionaryDetail{ID: value.ID, Label: value.Label, Value: value.Value, Extend: value.Extend, Status: status, Sort: value.Sort, DictionaryID: value.DictionaryID, ParentID: value.ParentID, Level: value.Level, Path: value.Path}
+}
+func (s *SettingsService) CreateDictionaryDetailRequest(ctx context.Context, req *dto.DictionaryDetailRequest) error {
+ return s.CreateDictionaryDetail(ctx, detailDomain(req))
+}
+func (s *SettingsService) UpdateDictionaryDetailRequest(ctx context.Context, req *dto.DictionaryDetailRequest) error {
+ return s.UpdateDictionaryDetail(ctx, detailDomain(req))
+}
+func detailDTO(v *biz.DictionaryDetail) map[string]any {
+ children := make([]map[string]any, 0, len(v.Children))
+ for _, x := range v.Children {
+ children = append(children, detailDTO(x))
+ }
+ return map[string]any{"ID": v.ID, "CreatedAt": v.CreatedAt, "UpdatedAt": v.UpdatedAt, "DeletedAt": nil, "label": v.Label, "value": v.Value, "extend": v.Extend, "status": v.Status, "sort": v.Sort, "sysDictionaryID": v.DictionaryID, "parentID": v.ParentID, "level": v.Level, "path": v.Path, "disabled": !v.Status, "children": children}
+}
+func (s *SettingsService) DictionaryDetails(ctx context.Context, page, size int, filter biz.DictionaryDetailFilter) ([]map[string]any, int64, error) {
+ items, total, err := s.uc.ListDictionaryDetails(ctx, page, size, filter)
if err != nil {
return nil, 0, err
}
@@ -92,14 +98,14 @@ func (s *SettingsService) DictionaryDetails(ctx context.Context, page, size int,
return out, total, nil
}
func (s *SettingsService) DictionaryDetail(ctx context.Context, id uint) (map[string]any, error) {
- v, err := s.Repo().FindDictionaryDetail(ctx, id)
+ v, err := s.uc.FindDictionaryDetail(ctx, id)
if err != nil {
return nil, err
}
return detailDTO(v), nil
}
func (s *SettingsService) DictionaryTree(ctx context.Context, id uint, typ string) ([]map[string]any, error) {
- items, err := s.Repo().DictionaryDetailTree(ctx, id, typ)
+ items, err := s.uc.DictionaryDetailTree(ctx, id, typ)
if err != nil {
return nil, err
}
@@ -110,73 +116,11 @@ func (s *SettingsService) DictionaryTree(ctx context.Context, id uint, typ strin
return out, nil
}
func (s *SettingsService) CreateDictionaryDetail(ctx context.Context, v *biz.DictionaryDetail) error {
- return s.Repo().CreateDictionaryDetail(ctx, v)
+ return s.uc.CreateDictionaryDetail(ctx, v)
}
func (s *SettingsService) UpdateDictionaryDetail(ctx context.Context, v *biz.DictionaryDetail) error {
- return s.Repo().UpdateDictionaryDetail(ctx, v)
+ return s.uc.UpdateDictionaryDetail(ctx, v)
}
func (s *SettingsService) DeleteDictionaryDetail(ctx context.Context, id uint) error {
- return s.Repo().DeleteDictionaryDetail(ctx, id)
-}
-func (s *SettingsService) ImportDictionaryJSON(ctx context.Context, raw string) error {
- var payload struct {
- Name string `json:"name"`
- Type string `json:"type"`
- Status *bool `json:"status"`
- Description string `json:"desc"`
- Details []dto.DictionaryDetailRequest `json:"sysDictionaryDetails"`
- }
- if json.Unmarshal([]byte(raw), &payload) != nil || payload.Name == "" || payload.Type == "" {
- return errors.New("JSON 格式错误")
- }
- dictionary := dictionaryDomain(&dto.DictionaryRequest{Name: payload.Name, Type: payload.Type, Status: payload.Status, Description: payload.Description})
- if err := s.CreateDictionary(ctx, dictionary); err != nil {
- return err
- }
- mapping := map[uint]uint{}
- pending := append([]dto.DictionaryDetailRequest(nil), payload.Details...)
- for len(pending) > 0 {
- progress := false
- next := pending[:0]
- for _, item := range pending {
- oldID := item.ID
- if item.ParentID != nil && *item.ParentID != 0 {
- parent, exists := mapping[*item.ParentID]
- if !exists {
- next = append(next, item)
- continue
- }
- item.ParentID = &parent
- }
- value := detailDomain(&item)
- value.ID = 0
- value.DictionaryID = dictionary.ID
- if err := s.CreateDictionaryDetail(ctx, value); err != nil {
- return errors.New("导入详情失败")
- }
- if oldID != 0 {
- mapping[oldID] = value.ID
- }
- progress = true
- }
- if !progress {
- return errors.New("字典层级关系无效")
- }
- pending = next
- }
- return nil
-}
-func (s *SettingsService) DictionaryDetailsByParent(ctx context.Context, dictionaryID, parentID uint) ([]map[string]any, error) {
- items, _, err := s.DictionaryDetails(ctx, 1, 10000, dictionaryID, "", "")
- if err != nil {
- return nil, err
- }
- out := []map[string]any{}
- for _, item := range items {
- parent, _ := item["parentID"].(*uint)
- if (parentID == 0 && parent == nil) || (parent != nil && *parent == parentID) {
- out = append(out, item)
- }
- }
- return out, nil
+ return s.uc.DeleteDictionaryDetail(ctx, id)
}
diff --git a/internal/service/dictionary_import.go b/internal/service/dictionary_import.go
new file mode 100644
index 0000000..bac157d
--- /dev/null
+++ b/internal/service/dictionary_import.go
@@ -0,0 +1,40 @@
+package service
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+
+ "kra/internal/biz"
+ "kra/internal/service/dto"
+)
+
+func (s *SettingsService) ImportDictionaryJSON(ctx context.Context, raw string) error {
+ var payload struct {
+ Name string `json:"name"`
+ Type string `json:"type"`
+ Status *bool `json:"status"`
+ Description string `json:"desc"`
+ Details []dto.DictionaryDetailRequest `json:"sysDictionaryDetails"`
+ }
+ if json.Unmarshal([]byte(raw), &payload) != nil || payload.Name == "" || payload.Type == "" {
+ return errors.New("JSON 格式错误")
+ }
+ dictionary := dictionaryDomain(&dto.DictionaryRequest{Name: payload.Name, Type: payload.Type, Status: payload.Status, Description: payload.Description})
+ details := make([]*biz.DictionaryDetail, 0, len(payload.Details))
+ for i := range payload.Details {
+ details = append(details, detailDomain(&payload.Details[i]))
+ }
+ return s.uc.ImportDictionary(ctx, dictionary, details)
+}
+func (s *SettingsService) DictionaryDetailsByParent(ctx context.Context, dictionaryID, parentID uint) ([]map[string]any, error) {
+ items, err := s.uc.DictionaryDetailsByParent(ctx, dictionaryID, parentID)
+ if err != nil {
+ return nil, err
+ }
+ out := make([]map[string]any, 0, len(items))
+ for _, item := range items {
+ out = append(out, detailDTO(item))
+ }
+ return out, nil
+}
diff --git a/internal/service/dto/api.go b/internal/service/dto/api.go
index 5d90f6e..29ae7ae 100644
--- a/internal/service/dto/api.go
+++ b/internal/service/dto/api.go
@@ -17,6 +17,8 @@ type APIListRequest struct {
Description string `json:"description"`
APIGroup string `json:"apiGroup"`
Method string `json:"method"`
+ OrderKey string `json:"orderKey"`
+ Desc bool `json:"desc"`
}
type APIResponse struct {
@@ -40,7 +42,8 @@ type GetAPIRequest struct {
ID uint `json:"id"`
}
type SetAPIRolesRequest struct {
- APIID uint `json:"apiId"`
+ Path string `json:"path"`
+ Method string `json:"method"`
AuthorityIDs []uint `json:"authorityIds"`
}
type IgnoreAPIRequest struct {
diff --git a/internal/service/dto/media.go b/internal/service/dto/media.go
index 6ac3d91..7dc7f6f 100644
--- a/internal/service/dto/media.go
+++ b/internal/service/dto/media.go
@@ -1,12 +1,18 @@
package dto
+import "time"
+
type MediaListRequest struct {
- Page int `json:"page"`
- PageSize int `json:"pageSize"`
- Keyword string `json:"keyword"`
- ClassID int `json:"classId"`
- Tag string `json:"tag"`
- UserID uint `json:"userId"`
+ Page int `json:"page"`
+ PageSize int `json:"pageSize"`
+ Keyword string `json:"keyword"`
+ ClassID int `json:"classId"`
+ Tag string `json:"tag"`
+ UserID uint `json:"userId"`
+ StartCreatedAt time.Time `json:"startCreatedAt"`
+ EndCreatedAt time.Time `json:"endCreatedAt"`
+ OrderKey string `json:"orderKey"`
+ Desc bool `json:"desc"`
}
type IDRequest struct {
ID uint `json:"ID" form:"id"`
@@ -27,6 +33,7 @@ type ImportMediaRequest struct {
Size int64 `json:"size"`
Mime string `json:"mime"`
MD5 string `json:"md5"`
+ UserID uint `json:"userId"`
}
type StorageListRequest struct {
Prefix string `json:"prefix"`
diff --git a/internal/service/dto/organization.go b/internal/service/dto/organization.go
index 742d6ec..0f1c009 100644
--- a/internal/service/dto/organization.go
+++ b/internal/service/dto/organization.go
@@ -57,6 +57,7 @@ type PositionListRequest struct {
PageSize int `json:"pageSize"`
Name string `json:"name"`
Code string `json:"code"`
+ Status *bool `json:"status"`
}
type DeletePositionRequest struct {
ID uint `json:"ID"`
diff --git a/internal/service/dto/system.go b/internal/service/dto/system.go
index a059903..a7bd8ab 100644
--- a/internal/service/dto/system.go
+++ b/internal/service/dto/system.go
@@ -41,6 +41,7 @@ type SelfUserRequest struct {
HeaderImg string `json:"headerImg"`
Phone string `json:"phone"`
Email string `json:"email"`
+ Enable int `json:"enable"`
}
type ResetPasswordRequest struct {
diff --git a/internal/service/dto/system_config.go b/internal/service/dto/system_config.go
index 5cdffec..40c50c5 100644
--- a/internal/service/dto/system_config.go
+++ b/internal/service/dto/system_config.go
@@ -27,12 +27,45 @@ type SecurityConfigRequest struct {
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"`
+ } `json:"database"`
+ Redis *struct {
+ Network string `json:"network"`
+ Addr string `json:"addr"`
+ ReadTimeout string `json:"read_timeout"`
+ WriteTimeout string `json:"write_timeout"`
+ } `json:"redis"`
+ } `json:"data"`
Admin struct {
RouterPrefix string `json:"routerPrefix"`
- JWT struct {
- SigningKey string `json:"signingKey"`
- Issuer string `json:"issuer"`
+ System struct {
+ UseRedis bool `json:"useRedis"`
+ UseMultipoint bool `json:"useMultipoint"`
+ UseStrictAuth bool `json:"useStrictAuth"`
+ DisableAutoMigrate bool `json:"disableAutoMigrate"`
+ } `json:"system"`
+ JWT struct {
+ SigningKey string `json:"signingKey"`
+ ExpiresTime string `json:"expiresTime"`
+ BufferTime string `json:"bufferTime"`
+ Issuer string `json:"issuer"`
} `json:"jwt"`
+ Captcha struct {
+ KeyLong int32 `json:"keyLong"`
+ ImgWidth int32 `json:"imgWidth"`
+ ImgHeight int32 `json:"imgHeight"`
+ StoreExpiration string `json:"storeExpiration"`
+ } `json:"captcha"`
Local struct {
StorePath string `json:"storePath"`
PathPrefix string `json:"pathPrefix"`
diff --git a/internal/service/dto/version.go b/internal/service/dto/version.go
index d5b6ffd..e5e6fb2 100644
--- a/internal/service/dto/version.go
+++ b/internal/service/dto/version.go
@@ -13,15 +13,21 @@ type VersionMenuButtonRequest struct {
Name string `json:"name"`
Description string `json:"desc"`
}
+type VersionMenuParameterRequest struct {
+ Type string `json:"type"`
+ Key string `json:"key"`
+ Value string `json:"value"`
+}
type VersionMenuRequest struct {
- Path string `json:"path"`
- Name string `json:"name"`
- Hidden bool `json:"hidden"`
- Component string `json:"component"`
- Sort int `json:"sort"`
- Meta VersionMenuMetaRequest `json:"meta"`
- MenuButtons []VersionMenuButtonRequest `json:"menuBtn"`
- Children []VersionMenuRequest `json:"children"`
+ Path string `json:"path"`
+ Name string `json:"name"`
+ Hidden bool `json:"hidden"`
+ Component string `json:"component"`
+ Sort int `json:"sort"`
+ Meta VersionMenuMetaRequest `json:"meta"`
+ MenuButtons []VersionMenuButtonRequest `json:"menuBtn"`
+ Parameters []VersionMenuParameterRequest `json:"parameters"`
+ Children []VersionMenuRequest `json:"children"`
}
type VersionAPIRequest struct {
Path string `json:"path"`
diff --git a/internal/service/export.go b/internal/service/export.go
index 11ec55f..c0cdd04 100644
--- a/internal/service/export.go
+++ b/internal/service/export.go
@@ -2,14 +2,10 @@ package service
import (
"context"
- "encoding/json"
- "errors"
- "fmt"
- "github.com/xuri/excelize/v2"
- "io"
+ "time"
+
"kra/internal/biz"
"kra/internal/service/dto"
- "sort"
)
type ExportService struct{ uc *biz.ExportUsecase }
@@ -31,8 +27,8 @@ func (s *ExportService) CreateRequest(ctx context.Context, req *dto.ExportTempla
func (s *ExportService) UpdateRequest(ctx context.Context, req *dto.ExportTemplateRequest) error {
return s.Update(ctx, exportTemplateDomain(req))
}
-func (s *ExportService) TemplatesFilter(ctx context.Context, page, size int, name, tableName, templateID string) ([]map[string]any, int64, error) {
- return s.Templates(ctx, page, size, &biz.ExportTemplate{Name: name, TableName: tableName, TemplateID: templateID})
+func (s *ExportService) TemplatesFilter(ctx context.Context, page, size int, name, tableName, templateID string, start, end *time.Time) ([]map[string]any, int64, error) {
+ return s.Templates(ctx, page, size, &biz.ExportTemplate{Name: name, TableName: tableName, TemplateID: templateID, StartCreatedAt: start, EndCreatedAt: end})
}
func exportDTO(v *biz.ExportTemplate) map[string]any {
conditions := make([]map[string]any, 0, len(v.Conditions))
@@ -46,23 +42,23 @@ func exportDTO(v *biz.ExportTemplate) map[string]any {
return map[string]any{"ID": v.ID, "CreatedAt": v.CreatedAt, "UpdatedAt": v.UpdatedAt, "DeletedAt": nil, "dbName": v.DBName, "name": v.Name, "tableName": v.TableName, "templateID": v.TemplateID, "templateInfo": v.TemplateInfo, "sql": v.SQL, "importSql": v.ImportSQL, "limit": v.Limit, "order": v.Order, "conditions": conditions, "joinTemplate": joins}
}
func (s *ExportService) Create(ctx context.Context, v *biz.ExportTemplate) error {
- return s.uc.Repo().CreateExportTemplate(ctx, v)
+ return s.uc.CreateExportTemplate(ctx, v)
}
func (s *ExportService) Update(ctx context.Context, v *biz.ExportTemplate) error {
- return s.uc.Repo().UpdateExportTemplate(ctx, v)
+ return s.uc.UpdateExportTemplate(ctx, v)
}
func (s *ExportService) Delete(ctx context.Context, ids []uint) error {
- return s.uc.Repo().DeleteExportTemplates(ctx, ids)
+ return s.uc.DeleteExportTemplates(ctx, ids)
}
func (s *ExportService) Template(ctx context.Context, id uint, tid string) (map[string]any, error) {
- v, err := s.uc.Repo().FindExportTemplate(ctx, id, tid)
+ v, err := s.uc.FindExportTemplate(ctx, id, tid)
if err != nil {
return nil, err
}
return exportDTO(v), nil
}
func (s *ExportService) Templates(ctx context.Context, page, size int, q *biz.ExportTemplate) ([]map[string]any, int64, error) {
- items, total, err := s.uc.Repo().ListExportTemplates(ctx, page, size, q)
+ items, total, err := s.uc.ListExportTemplates(ctx, page, size, q)
if err != nil {
return nil, 0, err
}
@@ -72,133 +68,3 @@ func (s *ExportService) Templates(ctx context.Context, page, size int, q *biz.Ex
}
return out, total, nil
}
-func templateColumns(raw string) ([]string, map[string]string, error) {
- labels := map[string]string{}
- if err := json.Unmarshal([]byte(raw), &labels); err != nil {
- return nil, nil, err
- }
- keys := make([]string, 0, len(labels))
- for key := range labels {
- keys = append(keys, key)
- }
- sort.Strings(keys)
- return keys, labels, nil
-}
-func (s *ExportService) Export(ctx context.Context, tid string, params map[string]string) ([]byte, string, error) {
- template, err := s.uc.Repo().FindExportTemplate(ctx, 0, tid)
- if err != nil {
- return nil, "", err
- }
- rows, _, err := s.uc.Repo().QueryExport(ctx, template, params)
- if err != nil {
- return nil, "", err
- }
- keys, labels, err := templateColumns(template.TemplateInfo)
- if err != nil {
- return nil, "", err
- }
- file := excelize.NewFile()
- sheet := file.GetSheetName(0)
- for column, key := range keys {
- cell, _ := excelize.CoordinatesToCellName(column+1, 1)
- _ = file.SetCellValue(sheet, cell, labels[key])
- }
- for rowIndex, row := range rows {
- for column, key := range keys {
- cell, _ := excelize.CoordinatesToCellName(column+1, rowIndex+2)
- _ = file.SetCellValue(sheet, cell, row[key])
- }
- }
- buffer, err := file.WriteToBuffer()
- _ = file.Close()
- if err != nil {
- return nil, "", err
- }
- return buffer.Bytes(), template.Name + ".xlsx", nil
-}
-func (s *ExportService) ExportBlankTemplate(ctx context.Context, tid string) ([]byte, string, error) {
- template, err := s.uc.Repo().FindExportTemplate(ctx, 0, tid)
- if err != nil {
- return nil, "", err
- }
- keys, labels, err := templateColumns(template.TemplateInfo)
- if err != nil {
- return nil, "", err
- }
- file := excelize.NewFile()
- sheet := file.GetSheetName(0)
- for column, key := range keys {
- cell, _ := excelize.CoordinatesToCellName(column+1, 1)
- _ = file.SetCellValue(sheet, cell, labels[key])
- }
- buffer, err := file.WriteToBuffer()
- _ = file.Close()
- if err != nil {
- return nil, "", err
- }
- return buffer.Bytes(), template.Name + "_template.xlsx", nil
-}
-func (s *ExportService) Preview(ctx context.Context, tid string, params map[string]string) (string, error) {
- template, err := s.uc.Repo().FindExportTemplate(ctx, 0, tid)
- if err != nil {
- return "", err
- }
- _, sql, err := s.uc.Repo().QueryExport(ctx, template, params)
- return sql, err
-}
-func (s *ExportService) Import(ctx context.Context, tid string, reader io.Reader) error {
- template, err := s.uc.Repo().FindExportTemplate(ctx, 0, tid)
- if err != nil {
- return err
- }
- file, err := excelize.OpenReader(reader)
- if err != nil {
- return err
- }
- defer file.Close()
- sheets := file.GetSheetList()
- if len(sheets) == 0 {
- return errors.New("工作簿没有工作表")
- }
- rows, err := file.GetRows(sheets[0])
- if err != nil {
- return err
- }
- if len(rows) < 2 {
- return nil
- }
- keys, labels, err := templateColumns(template.TemplateInfo)
- if err != nil {
- return err
- }
- reverse := map[string]string{}
- for _, key := range keys {
- reverse[labels[key]] = key
- }
- headers := rows[0]
- items := make([]map[string]any, 0, len(rows)-1)
- for rowIndex, row := range rows[1:] {
- item := map[string]any{}
- empty := true
- for column, value := range row {
- if column >= len(headers) {
- continue
- }
- key := reverse[headers[column]]
- if key != "" {
- item[key] = value
- if value != "" {
- empty = false
- }
- }
- }
- if !empty {
- items = append(items, item)
- }
- _ = rowIndex
- }
- if err = s.uc.Repo().ImportExportRows(ctx, template, items); err != nil {
- return fmt.Errorf("导入失败: %w", err)
- }
- return nil
-}
diff --git a/internal/service/export_excel.go b/internal/service/export_excel.go
new file mode 100644
index 0000000..e7bbc31
--- /dev/null
+++ b/internal/service/export_excel.go
@@ -0,0 +1,149 @@
+package service
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "strings"
+
+ "github.com/xuri/excelize/v2"
+)
+
+func templateColumns(raw string) ([]string, map[string]string, error) {
+ labels := map[string]string{}
+ decoder := json.NewDecoder(strings.NewReader(raw))
+ token, err := decoder.Token()
+ if err != nil || token != json.Delim('{') {
+ return nil, nil, err
+ }
+ keys := make([]string, 0)
+ for decoder.More() {
+ keyToken, tokenErr := decoder.Token()
+ if tokenErr != nil {
+ return nil, nil, tokenErr
+ }
+ key := keyToken.(string)
+ var label string
+ if tokenErr = decoder.Decode(&label); tokenErr != nil {
+ return nil, nil, tokenErr
+ }
+ keys = append(keys, key)
+ labels[key] = label
+ }
+ if _, err = decoder.Token(); err != nil {
+ return nil, nil, err
+ }
+ return keys, labels, nil
+}
+func (s *ExportService) Export(ctx context.Context, tid string, params map[string]string) ([]byte, string, error) {
+ template, err := s.uc.FindExportTemplate(ctx, 0, tid)
+ if err != nil {
+ return nil, "", err
+ }
+ rows, _, err := s.uc.QueryExport(ctx, template, params)
+ if err != nil {
+ return nil, "", err
+ }
+ keys, labels, err := templateColumns(template.TemplateInfo)
+ if err != nil {
+ return nil, "", err
+ }
+ file := excelize.NewFile()
+ sheet := file.GetSheetName(0)
+ for column, key := range keys {
+ cell, _ := excelize.CoordinatesToCellName(column+1, 1)
+ _ = file.SetCellValue(sheet, cell, labels[key])
+ }
+ for rowIndex, row := range rows {
+ for column, key := range keys {
+ cell, _ := excelize.CoordinatesToCellName(column+1, rowIndex+2)
+ _ = file.SetCellValue(sheet, cell, row[key])
+ }
+ }
+ buffer, err := file.WriteToBuffer()
+ _ = file.Close()
+ if err != nil {
+ return nil, "", err
+ }
+ return buffer.Bytes(), template.Name + ".xlsx", nil
+}
+func (s *ExportService) ExportBlankTemplate(ctx context.Context, tid string) ([]byte, string, error) {
+ template, err := s.uc.FindExportTemplate(ctx, 0, tid)
+ if err != nil {
+ return nil, "", err
+ }
+ keys, labels, err := templateColumns(template.TemplateInfo)
+ if err != nil {
+ return nil, "", err
+ }
+ file := excelize.NewFile()
+ sheet := file.GetSheetName(0)
+ for column, key := range keys {
+ cell, _ := excelize.CoordinatesToCellName(column+1, 1)
+ _ = file.SetCellValue(sheet, cell, labels[key])
+ }
+ buffer, err := file.WriteToBuffer()
+ _ = file.Close()
+ if err != nil {
+ return nil, "", err
+ }
+ return buffer.Bytes(), template.Name + "_template.xlsx", nil
+}
+func (s *ExportService) Preview(ctx context.Context, tid string, params map[string]string) (string, error) {
+ template, err := s.uc.FindExportTemplate(ctx, 0, tid)
+ if err != nil {
+ return "", err
+ }
+ return s.uc.PreviewExport(ctx, template, params)
+}
+func (s *ExportService) Import(ctx context.Context, tid string, reader io.Reader) error {
+ template, err := s.uc.FindExportTemplate(ctx, 0, tid)
+ if err != nil {
+ return err
+ }
+ file, err := excelize.OpenReader(reader)
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+ sheets := file.GetSheetList()
+ if len(sheets) == 0 {
+ return errors.New("工作簿没有工作表")
+ }
+ rows, err := file.GetRows(sheets[0])
+ if err != nil {
+ return err
+ }
+ if len(rows) < 2 {
+ return errors.New("Excel data is not enough.\nIt should contain title row and data")
+ }
+ keys, labels, err := templateColumns(template.TemplateInfo)
+ if err != nil {
+ return err
+ }
+ reverse := map[string]string{}
+ for _, key := range keys {
+ reverse[labels[key]] = key
+ }
+ headers := rows[0]
+ items := make([]map[string]any, 0, len(rows)-1)
+ for _, row := range rows[1:] {
+ item := map[string]any{}
+ for column, value := range row {
+ if column >= len(headers) {
+ continue
+ }
+ key := reverse[headers[column]]
+ if key != "" {
+ item[key] = value
+ }
+ }
+ items = append(items, item)
+ }
+ if err = s.uc.ImportExportRows(ctx, template, items); err != nil {
+ return fmt.Errorf("导入失败: %w", err)
+ }
+ return nil
+}
diff --git a/internal/service/media.go b/internal/service/media.go
index 8bc8186..0947441 100644
--- a/internal/service/media.go
+++ b/internal/service/media.go
@@ -29,14 +29,14 @@ func (s *MediaService) Upload(ctx context.Context, userID uint, name, mime strin
return mediaDTO(v), nil
}
func (s *MediaService) Media(ctx context.Context, id uint) (map[string]any, error) {
- v, err := s.uc.Repo().FindMedia(ctx, id)
+ v, err := s.uc.FindMedia(ctx, id)
if err != nil {
return nil, err
}
return mediaDTO(v), nil
}
-func (s *MediaService) MediaList(ctx context.Context, page, size int, keyword string, categoryID int, tag string, userID uint) ([]map[string]any, int64, error) {
- items, total, err := s.uc.Repo().ListMedia(ctx, page, size, keyword, categoryID, tag, userID)
+func (s *MediaService) MediaList(ctx context.Context, filter biz.MediaFilter) ([]map[string]any, int64, error) {
+ items, total, err := s.uc.ListMedia(ctx, filter)
if err != nil {
return nil, 0, err
}
@@ -48,15 +48,15 @@ func (s *MediaService) MediaList(ctx context.Context, page, size int, keyword st
}
func (s *MediaService) Delete(ctx context.Context, id uint) error { return s.uc.Delete(ctx, id) }
func (s *MediaService) Rename(ctx context.Context, id uint, name string) error {
- return s.uc.Repo().UpdateMediaName(ctx, id, name)
+ return s.uc.UpdateMediaName(ctx, id, name)
}
func (s *MediaService) ImportURLs(ctx context.Context, items []*biz.MediaFile) error {
- return s.uc.Repo().CreateMediaBatch(ctx, items)
+ return s.uc.CreateMediaBatch(ctx, items)
}
-func (s *MediaService) ImportURLRequests(ctx context.Context, userID uint, values []dto.ImportMediaRequest) error {
+func (s *MediaService) ImportURLRequests(ctx context.Context, values []dto.ImportMediaRequest) error {
items := make([]*biz.MediaFile, 0, len(values))
for _, value := range values {
- items = append(items, &biz.MediaFile{Name: value.Name, CategoryID: value.ClassID, URL: value.URL, Tag: value.Tag, Key: value.Key, Size: value.Size, Mime: value.Mime, MD5: value.MD5, UserID: userID})
+ items = append(items, &biz.MediaFile{Name: value.Name, CategoryID: value.ClassID, URL: value.URL, Tag: value.Tag, Key: value.Key, Size: value.Size, Mime: value.Mime, MD5: value.MD5, UserID: value.UserID})
}
return s.ImportURLs(ctx, items)
}
@@ -72,7 +72,7 @@ func (s *MediaService) Storage(ctx context.Context, prefix, cursor string, limit
return out, next, more, nil
}
func (s *MediaService) Categories(ctx context.Context) ([]map[string]any, error) {
- items, err := s.uc.Repo().ListCategories(ctx)
+ items, err := s.uc.ListCategories(ctx)
if err != nil {
return nil, err
}
@@ -83,11 +83,11 @@ func (s *MediaService) Categories(ctx context.Context) ([]map[string]any, error)
return out, nil
}
func (s *MediaService) SaveCategory(ctx context.Context, v *biz.AttachmentCategory) error {
- return s.uc.Repo().SaveCategory(ctx, v)
+ return s.uc.SaveCategory(ctx, v)
}
func (s *MediaService) SaveCategoryRequest(ctx context.Context, value *dto.CategoryRequest) error {
return s.SaveCategory(ctx, &biz.AttachmentCategory{ID: value.ID, Name: value.Name, ParentID: value.PID})
}
func (s *MediaService) DeleteCategory(ctx context.Context, id uint) error {
- return s.uc.Repo().DeleteCategory(ctx, id)
+ return s.uc.DeleteCategory(ctx, id)
}
diff --git a/internal/service/menu.go b/internal/service/menu.go
index 6a9d922..1fdfa6b 100644
--- a/internal/service/menu.go
+++ b/internal/service/menu.go
@@ -11,47 +11,6 @@ type MenuService struct{ uc *biz.MenuUsecase }
func NewMenuService(uc *biz.MenuUsecase) *MenuService { return &MenuService{uc: uc} }
-func menuDomain(v *dto.MenuRequest) *biz.Menu {
- menu := &biz.Menu{ID: v.ID, ParentID: v.ParentID, Path: v.Path, Name: v.Name, Hidden: v.Hidden, Component: v.Component, Sort: v.Sort, ActiveName: v.Meta.ActiveName, KeepAlive: v.Meta.KeepAlive, DefaultMenu: v.Meta.DefaultMenu, Title: v.Meta.Title, Icon: v.Meta.Icon, CloseTab: v.Meta.CloseTab, TransitionType: v.Meta.TransitionType}
- menu.Buttons = make([]*biz.MenuButton, 0, len(v.MenuButtons))
- for _, button := range v.MenuButtons {
- menu.Buttons = append(menu.Buttons, &biz.MenuButton{ID: button.ID, Name: button.Name, Description: button.Description, MenuID: v.ID})
- }
- menu.Parameters = make([]*biz.MenuParameter, 0, len(v.Parameters))
- for _, parameter := range v.Parameters {
- menu.Parameters = append(menu.Parameters, &biz.MenuParameter{ID: parameter.ID, MenuID: v.ID, Type: parameter.Type, Key: parameter.Key, Value: parameter.Value})
- }
- return menu
-}
-
-func menuResponse(menu *biz.Menu) *dto.MenuResponse {
- children := make([]*dto.MenuResponse, 0, len(menu.Children))
- for _, child := range menu.Children {
- children = append(children, menuResponse(child))
- }
- buttons := make([]dto.MenuButtonResponse, 0, len(menu.Buttons))
- buttonAuthorities := make(map[string]uint, len(menu.Buttons))
- for _, button := range menu.Buttons {
- buttons = append(buttons, dto.MenuButtonResponse{ID: button.ID, Name: button.Name, Description: button.Description, SysBaseMenuID: button.MenuID})
- if button.AuthorityID != 0 {
- buttonAuthorities[button.Name] = button.AuthorityID
- }
- }
- parameters := make([]dto.MenuParameterResponse, 0, len(menu.Parameters))
- for _, parameter := range menu.Parameters {
- parameters = append(parameters, dto.MenuParameterResponse{ID: parameter.ID, SysBaseMenuID: parameter.MenuID, Type: parameter.Type, Key: parameter.Key, Value: parameter.Value})
- }
- return &dto.MenuResponse{ID: menu.ID, ParentID: menu.ParentID, Path: menu.Path, Name: menu.Name, Hidden: menu.Hidden, Component: menu.Component, Sort: menu.Sort, Meta: dto.MenuMetaResponse{ActiveName: menu.ActiveName, KeepAlive: menu.KeepAlive, DefaultMenu: menu.DefaultMenu, Title: menu.Title, Icon: menu.Icon, CloseTab: menu.CloseTab, TransitionType: menu.TransitionType}, Children: children, Parameters: parameters, MenuButtons: buttons, Buttons: buttonAuthorities}
-}
-
-func menuResponses(items []*biz.Menu) []*dto.MenuResponse {
- out := make([]*dto.MenuResponse, 0, len(items))
- for _, item := range items {
- out = append(out, menuResponse(item))
- }
- return out
-}
-
func (s *MenuService) List(ctx context.Context) ([]*dto.MenuResponse, error) {
items, err := s.uc.List(ctx)
if err != nil {
@@ -107,6 +66,50 @@ func (s *MenuService) AuthorityMenus(ctx context.Context, authorityID uint) ([]*
func (s *MenuService) RoleIDs(ctx context.Context, menuID uint) ([]uint, error) {
return s.uc.RoleIDs(ctx, menuID)
}
+func (s *MenuService) DefaultRouterRoleIDs(ctx context.Context, menuID uint) ([]uint, error) {
+ return s.uc.DefaultRouterRoleIDs(ctx, menuID)
+}
func (s *MenuService) SetRoles(ctx context.Context, req *dto.SetMenuRolesRequest) error {
return s.uc.SetRoles(ctx, req.MenuID, req.AuthorityIDs)
}
+
+func menuDomain(v *dto.MenuRequest) *biz.Menu {
+ menu := &biz.Menu{ID: v.ID, ParentID: v.ParentID, Path: v.Path, Name: v.Name, Hidden: v.Hidden, Component: v.Component, Sort: v.Sort, ActiveName: v.Meta.ActiveName, KeepAlive: v.Meta.KeepAlive, DefaultMenu: v.Meta.DefaultMenu, Title: v.Meta.Title, Icon: v.Meta.Icon, CloseTab: v.Meta.CloseTab, TransitionType: v.Meta.TransitionType}
+ menu.Buttons = make([]*biz.MenuButton, 0, len(v.MenuButtons))
+ for _, button := range v.MenuButtons {
+ menu.Buttons = append(menu.Buttons, &biz.MenuButton{ID: button.ID, Name: button.Name, Description: button.Description, MenuID: v.ID})
+ }
+ menu.Parameters = make([]*biz.MenuParameter, 0, len(v.Parameters))
+ for _, parameter := range v.Parameters {
+ menu.Parameters = append(menu.Parameters, &biz.MenuParameter{ID: parameter.ID, MenuID: v.ID, Type: parameter.Type, Key: parameter.Key, Value: parameter.Value})
+ }
+ return menu
+}
+
+func menuResponse(menu *biz.Menu) *dto.MenuResponse {
+ children := make([]*dto.MenuResponse, 0, len(menu.Children))
+ for _, child := range menu.Children {
+ children = append(children, menuResponse(child))
+ }
+ buttons := make([]dto.MenuButtonResponse, 0, len(menu.Buttons))
+ buttonAuthorities := make(map[string]uint, len(menu.Buttons))
+ for _, button := range menu.Buttons {
+ buttons = append(buttons, dto.MenuButtonResponse{ID: button.ID, Name: button.Name, Description: button.Description, SysBaseMenuID: button.MenuID})
+ if button.AuthorityID != 0 {
+ buttonAuthorities[button.Name] = button.AuthorityID
+ }
+ }
+ parameters := make([]dto.MenuParameterResponse, 0, len(menu.Parameters))
+ for _, parameter := range menu.Parameters {
+ parameters = append(parameters, dto.MenuParameterResponse{ID: parameter.ID, SysBaseMenuID: parameter.MenuID, Type: parameter.Type, Key: parameter.Key, Value: parameter.Value})
+ }
+ return &dto.MenuResponse{ID: menu.ID, ParentID: menu.ParentID, Path: menu.Path, Name: menu.Name, Hidden: menu.Hidden, Component: menu.Component, Sort: menu.Sort, Meta: dto.MenuMetaResponse{ActiveName: menu.ActiveName, KeepAlive: menu.KeepAlive, DefaultMenu: menu.DefaultMenu, Title: menu.Title, Icon: menu.Icon, CloseTab: menu.CloseTab, TransitionType: menu.TransitionType}, Children: children, Parameters: parameters, MenuButtons: buttons, Buttons: buttonAuthorities}
+}
+
+func menuResponses(items []*biz.Menu) []*dto.MenuResponse {
+ out := make([]*dto.MenuResponse, 0, len(items))
+ for _, item := range items {
+ out = append(out, menuResponse(item))
+ }
+ return out
+}
diff --git a/internal/service/organization.go b/internal/service/organization.go
index f8bd1c0..7792d2a 100644
--- a/internal/service/organization.go
+++ b/internal/service/organization.go
@@ -18,7 +18,11 @@ func departmentResponse(value *biz.Department) *dto.DepartmentResponse {
for _, child := range value.Children {
children = append(children, departmentResponse(child))
}
- return &dto.DepartmentResponse{ID: value.ID, CreatedAt: value.CreatedAt, UpdatedAt: value.UpdatedAt, DeletedAt: nil, Name: value.Name, ParentID: value.ParentID, Ancestors: value.Ancestors, Sort: value.Sort, LeaderID: value.LeaderID, Leader: nil, Status: value.Status, Children: children, NamePath: ""}
+ var leader any
+ if value.Leader != nil {
+ leader = convertUser(value.Leader)
+ }
+ return &dto.DepartmentResponse{ID: value.ID, CreatedAt: value.CreatedAt, UpdatedAt: value.UpdatedAt, DeletedAt: nil, Name: value.Name, ParentID: value.ParentID, Ancestors: value.Ancestors, Sort: value.Sort, LeaderID: value.LeaderID, Leader: leader, Status: value.Status, Children: children, NamePath: value.NamePath}
}
func departmentDomain(value *dto.DepartmentRequest) *biz.Department {
@@ -69,7 +73,7 @@ func positionResponse(value *biz.Position) *dto.PositionResponse {
return &dto.PositionResponse{ID: value.ID, CreatedAt: value.CreatedAt, UpdatedAt: value.UpdatedAt, DeletedAt: nil, Name: value.Name, Code: value.Code, Sort: value.Sort, Status: value.Status, Remark: value.Remark}
}
func (s *OrganizationService) Positions(ctx context.Context, req *dto.PositionListRequest) ([]*dto.PositionResponse, int64, error) {
- items, total, err := s.uc.Positions(ctx, req.Page, req.PageSize, &biz.Position{Name: req.Name, Code: req.Code})
+ items, total, err := s.uc.Positions(ctx, req.Page, req.PageSize, &biz.PositionListFilter{Name: req.Name, Code: req.Code, Status: req.Status})
if err != nil {
return nil, 0, err
}
diff --git a/internal/service/parameter.go b/internal/service/parameter.go
index b25f094..b184067 100644
--- a/internal/service/parameter.go
+++ b/internal/service/parameter.go
@@ -2,6 +2,7 @@ package service
import (
"context"
+ "time"
"kra/internal/biz"
"kra/internal/service/dto"
@@ -16,14 +17,14 @@ func (s *SettingsService) CreateParameterRequest(ctx context.Context, req *dto.S
func (s *SettingsService) UpdateParameterRequest(ctx context.Context, req *dto.SystemParameterRequest) error {
return s.UpdateParameter(ctx, parameterDomain(req))
}
-func (s *SettingsService) ParametersFilter(ctx context.Context, page, size int, name, key string) ([]map[string]any, int64, error) {
- return s.Parameters(ctx, page, size, &biz.SystemParameter{Name: name, Key: key})
+func (s *SettingsService) ParametersFilter(ctx context.Context, page, size int, name, key string, start, end *time.Time) ([]map[string]any, int64, error) {
+ return s.Parameters(ctx, page, size, &biz.SystemParameter{Name: name, Key: key, StartCreatedAt: start, EndCreatedAt: end})
}
func parameterDTO(v *biz.SystemParameter) map[string]any {
return map[string]any{"ID": v.ID, "CreatedAt": v.CreatedAt, "UpdatedAt": v.UpdatedAt, "DeletedAt": nil, "name": v.Name, "key": v.Key, "value": v.Value, "desc": v.Desc}
}
func (s *SettingsService) Parameters(ctx context.Context, page, size int, q *biz.SystemParameter) ([]map[string]any, int64, error) {
- items, total, err := s.Repo().ListParameters(ctx, page, size, q)
+ items, total, err := s.uc.ListParameters(ctx, page, size, q)
if err != nil {
return nil, 0, err
}
@@ -34,18 +35,18 @@ func (s *SettingsService) Parameters(ctx context.Context, page, size int, q *biz
return out, total, nil
}
func (s *SettingsService) Parameter(ctx context.Context, id uint, key string) (map[string]any, error) {
- v, err := s.Repo().FindParameter(ctx, id, key)
+ v, err := s.uc.FindParameter(ctx, id, key)
if err != nil {
return nil, err
}
return parameterDTO(v), nil
}
func (s *SettingsService) CreateParameter(ctx context.Context, v *biz.SystemParameter) error {
- return s.Repo().CreateParameter(ctx, v)
+ return s.uc.CreateParameter(ctx, v)
}
func (s *SettingsService) UpdateParameter(ctx context.Context, v *biz.SystemParameter) error {
- return s.Repo().UpdateParameter(ctx, v)
+ return s.uc.UpdateParameter(ctx, v)
}
func (s *SettingsService) DeleteParameters(ctx context.Context, ids []uint) error {
- return s.Repo().DeleteParameters(ctx, ids)
+ return s.uc.DeleteParameters(ctx, ids)
}
diff --git a/internal/service/security.go b/internal/service/security.go
index 7986124..058e05d 100644
--- a/internal/service/security.go
+++ b/internal/service/security.go
@@ -2,7 +2,6 @@ package service
import (
"context"
- "errors"
"kra/internal/biz"
"kra/internal/service/dto"
@@ -19,7 +18,7 @@ func (s *SettingsService) CurrentSecurity(ctx context.Context) (*biz.SecurityCon
return ©, nil
}
s.securityMu.RUnlock()
- value, err := s.Repo().SecurityConfig(ctx)
+ value, err := s.uc.SecurityConfig(ctx)
if err != nil {
return nil, err
}
@@ -37,10 +36,7 @@ func (s *SettingsService) Security(ctx context.Context) (map[string]any, error)
return securityDTO(value), nil
}
func (s *SettingsService) SaveSecurity(ctx context.Context, value *biz.SecurityConfig) (map[string]any, 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 nil, errors.New("安全配置数值必须大于0")
- }
- if err := s.Repo().SaveSecurityConfig(ctx, value); err != nil {
+ if err := s.uc.UpdateSecurity(ctx, value); err != nil {
return nil, err
}
s.securityMu.Lock()
@@ -66,33 +62,5 @@ func (s *SettingsService) ValidatePassword(ctx context.Context, password string)
if err != nil {
return err
}
- if len([]rune(password)) < cfg.PwdMinLength {
- return errors.New("密码长度不足")
- }
- hasUpper, hasLower, hasDigit, hasSpecial := false, false, false, false
- for _, ch := range password {
- switch {
- case ch >= 'A' && ch <= 'Z':
- hasUpper = true
- case ch >= 'a' && ch <= 'z':
- hasLower = true
- case ch >= '0' && ch <= '9':
- hasDigit = true
- default:
- hasSpecial = true
- }
- }
- if cfg.PwdRequireUpper && !hasUpper {
- return errors.New("密码必须包含大写字母")
- }
- if cfg.PwdRequireLower && !hasLower {
- return errors.New("密码必须包含小写字母")
- }
- if cfg.PwdRequireDigit && !hasDigit {
- return errors.New("密码必须包含数字")
- }
- if cfg.PwdRequireSpecial && !hasSpecial {
- return errors.New("密码必须包含特殊字符")
- }
- return nil
+ return s.uc.ValidatePassword(cfg, password)
}
diff --git a/internal/service/settings.go b/internal/service/settings.go
index baefc3e..708c1e6 100644
--- a/internal/service/settings.go
+++ b/internal/service/settings.go
@@ -1,7 +1,9 @@
package service
import (
+ "context"
"sync"
+ "time"
"kra/internal/biz"
"kra/internal/conf"
@@ -9,12 +11,39 @@ import (
type SettingsService struct {
uc *biz.SettingsUsecase
- config *conf.AdminBackend
+ runtime *conf.Runtime
securityMu sync.RWMutex
securityCache *biz.SecurityConfig
+ cache biz.Cache
}
-func NewSettingsService(uc *biz.SettingsUsecase, config *conf.AdminBackend) *SettingsService {
- return &SettingsService{uc: uc, config: config}
+func NewSettingsService(uc *biz.SettingsUsecase, runtime *conf.Runtime, cache biz.Cache) *SettingsService {
+ return &SettingsService{uc: uc, runtime: runtime, cache: cache}
+}
+
+func (s *SettingsService) UseMultipoint() bool {
+ config := s.runtime.Admin()
+ return config != nil && config.System != nil && config.System.UseMultipoint
+}
+
+func activeTokenKey(username string) string { return "jwt:active:" + username }
+
+func (s *SettingsService) ActiveTokenMatches(ctx context.Context, username, token string) (bool, error) {
+ if !s.UseMultipoint() {
+ return true, nil
+ }
+ active, ok, err := s.cache.Get(ctx, activeTokenKey(username))
+ return ok && active == token, err
+}
+
+func (s *SettingsService) RotateActiveToken(ctx context.Context, username, oldToken, newToken string, expiration time.Duration) error {
+ if !s.UseMultipoint() {
+ return nil
+ }
+ if oldToken != "" && oldToken != newToken {
+ if err := s.BlacklistToken(ctx, oldToken); err != nil {
+ return err
+ }
+ }
+ return s.cache.Set(ctx, activeTokenKey(username), newToken, expiration)
}
-func (s *SettingsService) Repo() biz.SettingsRepo { return s.uc.Repo() }
diff --git a/internal/service/system.go b/internal/service/system.go
index 5094931..ba0f352 100644
--- a/internal/service/system.go
+++ b/internal/service/system.go
@@ -9,12 +9,12 @@ import (
type SystemService struct {
uc *biz.SystemUsecase
- config *conf.AdminBackend
+ runtime *conf.Runtime
settings *SettingsService
}
-func NewSystemService(uc *biz.SystemUsecase, config *conf.AdminBackend, settings *SettingsService) *SystemService {
- return &SystemService{uc: uc, config: config, settings: settings}
+func NewSystemService(uc *biz.SystemUsecase, runtime *conf.Runtime, settings *SettingsService) *SystemService {
+ return &SystemService{uc: uc, runtime: runtime, settings: settings}
}
func (s *SystemService) IsInitialized(ctx context.Context) (bool, error) {
diff --git a/internal/service/system_config.go b/internal/service/system_config.go
index eae0cb9..e288e43 100644
--- a/internal/service/system_config.go
+++ b/internal/service/system_config.go
@@ -2,12 +2,15 @@ package service
import (
"context"
+ "encoding/json"
+ "time"
"kra/internal/conf"
"kra/internal/service/dto"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
+ "google.golang.org/protobuf/types/known/durationpb"
)
func (s *SystemService) PersistConfig(ctx context.Context) error { return s.uc.PersistConfig(ctx) }
@@ -22,42 +25,84 @@ func (s *SystemService) PersistAdminConfig(ctx context.Context, value *conf.Admi
func (s *SystemService) ReloadConfig(ctx context.Context) error { return s.uc.ReloadConfig(ctx) }
+func (s *SystemService) DiskMountPoints() []string {
+ config := s.runtime.Admin()
+ if config == nil {
+ return nil
+ }
+ points := make([]string, 0, len(config.DiskList))
+ for _, item := range config.DiskList {
+ if item != nil && item.MountPoint != "" {
+ points = append(points, item.MountPoint)
+ }
+ }
+ return points
+}
+
func (s *SystemService) SystemConfig() map[string]any {
admin := map[string]any{"routerPrefix": ""}
email := map[string]any{}
- if s.config == nil {
- return map[string]any{"config": map[string]any{"admin": admin, "email": email}}
+ config := s.runtime.Admin()
+ if config == nil {
+ return map[string]any{"config": map[string]any{"admin": admin, "email": email, "data": map[string]any{}}}
}
- admin["routerPrefix"] = s.config.RouterPrefix
- if s.config.Jwt != nil {
- admin["jwt"] = map[string]any{"signingKey": "******", "expiresTime": s.config.Jwt.ExpiresTime.AsDuration().String(), "bufferTime": s.config.Jwt.BufferTime.AsDuration().String(), "issuer": s.config.Jwt.Issuer}
+ 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}
}
- if s.config.Captcha != nil {
- admin["captcha"] = map[string]any{"keyLong": s.config.Captcha.KeyLong, "imgWidth": s.config.Captcha.ImgWidth, "imgHeight": s.config.Captcha.ImgHeight, "storeExpiration": s.config.Captcha.StoreExpiration.AsDuration().String()}
+ 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}
}
- if s.config.Local != nil {
- admin["local"] = map[string]any{"storePath": s.config.Local.StorePath, "pathPrefix": s.config.Local.PathPrefix}
+ if config.Captcha != nil {
+ admin["captcha"] = map[string]any{"keyLong": config.Captcha.KeyLong, "imgWidth": config.Captcha.ImgWidth, "imgHeight": config.Captcha.ImgHeight, "storeExpiration": config.Captcha.StoreExpiration.AsDuration().String()}
}
- if s.config.Media != nil {
- admin["media"] = map[string]any{"sessionTtl": s.config.Media.SessionTtl}
+ if config.Local != nil {
+ admin["local"] = map[string]any{"storePath": config.Local.StorePath, "pathPrefix": config.Local.PathPrefix}
}
- if s.config.Email != nil {
- email = map[string]any{"to": s.config.Email.To, "from": s.config.Email.From, "host": s.config.Email.Host, "secret": "******", "nickname": s.config.Email.Nickname, "port": s.config.Email.Port, "is-ssl": s.config.Email.IsSsl, "is-loginauth": s.config.Email.IsLoginAuth}
+ if config.Media != nil {
+ admin["media"] = map[string]any{"sessionTtl": config.Media.SessionTtl}
}
- if s.config.Storage != nil {
- storage := proto.Clone(s.config.Storage).(*conf.AdminBackend_Storage)
+ if config.Email != nil {
+ email = map[string]any{"to": config.Email.To, "from": config.Email.From, "host": config.Email.Host, "secret": "******", "nickname": config.Email.Nickname, "port": config.Email.Port, "is-ssl": config.Email.IsSsl, "is-loginauth": config.Email.IsLoginAuth}
+ }
+ if config.Storage != nil {
+ storage := proto.Clone(config.Storage).(*conf.AdminBackend_Storage)
maskStorageSecrets(storage)
admin["storage"] = storage
}
- return map[string]any{"config": map[string]any{"admin": admin, "email": email}}
+ data := s.runtime.Data()
+ if data != nil {
+ if data.Database != nil {
+ data.Database.Password = "******"
+ }
+ for _, database := range data.DatabaseList {
+ if database != nil {
+ database.Password = "******"
+ }
+ }
+ }
+ dataMap := map[string]any{}
+ if raw, err := (protojson.MarshalOptions{UseProtoNames: true}).Marshal(data); err == nil {
+ _ = json.Unmarshal(raw, &dataMap)
+ }
+ return map[string]any{"config": map[string]any{"admin": admin, "email": email, "data": dataMap}}
}
func (s *SystemService) SaveSystemConfig(ctx context.Context, req *dto.SetSystemConfigRequest) error {
- if s.config == nil {
+ config := s.runtime.Admin()
+ if config == nil {
return nil
}
- next := proto.Clone(s.config).(*conf.AdminBackend)
+ next := proto.Clone(config).(*conf.AdminBackend)
+ data := s.runtime.Data()
next.RouterPrefix = req.Config.Admin.RouterPrefix
+ if next.System == nil {
+ next.System = &conf.AdminBackend_System{}
+ }
+ next.System.UseRedis = req.Config.Admin.System.UseRedis
+ next.System.UseMultipoint = req.Config.Admin.System.UseMultipoint
+ next.System.UseStrictAuth = req.Config.Admin.System.UseStrictAuth
+ next.System.DisableAutoMigrate = req.Config.Admin.System.DisableAutoMigrate
if next.Jwt != nil {
if req.Config.Admin.JWT.SigningKey != "" && req.Config.Admin.JWT.SigningKey != "******" {
next.Jwt.SigningKey = req.Config.Admin.JWT.SigningKey
@@ -65,6 +110,26 @@ func (s *SystemService) SaveSystemConfig(ctx context.Context, req *dto.SetSystem
if req.Config.Admin.JWT.Issuer != "" {
next.Jwt.Issuer = req.Config.Admin.JWT.Issuer
}
+ if value, err := time.ParseDuration(req.Config.Admin.JWT.ExpiresTime); err == nil && value > 0 {
+ next.Jwt.ExpiresTime = durationpb.New(value)
+ }
+ if value, err := time.ParseDuration(req.Config.Admin.JWT.BufferTime); err == nil && value >= 0 {
+ next.Jwt.BufferTime = durationpb.New(value)
+ }
+ }
+ if next.Captcha != nil {
+ if req.Config.Admin.Captcha.KeyLong > 0 {
+ next.Captcha.KeyLong = req.Config.Admin.Captcha.KeyLong
+ }
+ if req.Config.Admin.Captcha.ImgWidth > 0 {
+ next.Captcha.ImgWidth = req.Config.Admin.Captcha.ImgWidth
+ }
+ if req.Config.Admin.Captcha.ImgHeight > 0 {
+ next.Captcha.ImgHeight = req.Config.Admin.Captcha.ImgHeight
+ }
+ if value, err := time.ParseDuration(req.Config.Admin.Captcha.StoreExpiration); err == nil && value > 0 {
+ next.Captcha.StoreExpiration = durationpb.New(value)
+ }
}
if next.Local != nil {
if req.Config.Admin.Local.StorePath != "" {
@@ -89,7 +154,37 @@ func (s *SystemService) SaveSystemConfig(ctx context.Context, req *dto.SetSystem
next.Email.Secret = req.Config.Email.Secret
}
}
- return s.PersistAdminConfig(ctx, next)
+ if req.Config.Data != nil {
+ if data == nil {
+ data = &conf.Data{}
+ }
+ if value := req.Config.Data.Database; value != nil {
+ password := value.Password
+ 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}
+ }
+ if value := req.Config.Data.Redis; value != nil {
+ redis := &conf.Data_Redis{Network: value.Network, Addr: value.Addr}
+ if duration, parseErr := time.ParseDuration(value.ReadTimeout); parseErr == nil && duration >= 0 {
+ redis.ReadTimeout = durationpb.New(duration)
+ }
+ if duration, parseErr := time.ParseDuration(value.WriteTimeout); parseErr == nil && duration >= 0 {
+ redis.WriteTimeout = durationpb.New(duration)
+ }
+ data.Redis = redis
+ }
+ }
+ dataRaw, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(data)
+ if err != nil {
+ return err
+ }
+ adminRaw, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(next)
+ if err != nil {
+ return err
+ }
+ return s.uc.PersistRuntimeConfig(ctx, dataRaw, adminRaw)
}
func objectStores(storage *conf.AdminBackend_Storage) []*conf.AdminBackend_ObjectStore {
diff --git a/internal/service/task.go b/internal/service/task.go
index 70e6824..a231ca5 100644
--- a/internal/service/task.go
+++ b/internal/service/task.go
@@ -1,31 +1,23 @@
package service
import (
- "bytes"
"context"
"encoding/json"
- "errors"
- "fmt"
- "github.com/robfig/cron/v3"
- "io"
+ "time"
+
"kra/internal/biz"
"kra/internal/conf"
"kra/internal/service/dto"
- "net"
- "net/http"
- "net/url"
- "strings"
- "time"
)
type TaskService struct {
- uc *biz.TaskUsecase
- media *biz.MediaUsecase
- config *conf.AdminBackend
+ uc *biz.TaskUsecase
+ media *biz.MediaUsecase
+ runtime *conf.Runtime
}
-func NewTaskService(uc *biz.TaskUsecase, media *biz.MediaUsecase, config *conf.AdminBackend) *TaskService {
- return &TaskService{uc: uc, media: media, config: config}
+func NewTaskService(uc *biz.TaskUsecase, media *biz.MediaUsecase, runtime *conf.Runtime) *TaskService {
+ return &TaskService{uc: uc, media: media, runtime: runtime}
}
func taskDomain(v *dto.TaskRequest) *biz.TimedTask {
return &biz.TimedTask{ID: v.ID, Name: v.Name, Description: v.Description, Spec: v.Spec, WithSeconds: v.WithSeconds, ExecutorType: v.ExecutorType, MethodName: v.MethodName, Params: v.Params, HTTPURL: v.HTTPURL, HTTPMethod: v.HTTPMethod, HTTPHeader: v.HTTPHeader, HTTPBody: v.HTTPBody, HTTPAllowPrivate: v.HTTPAllowPrivate, Enabled: v.Enabled}
@@ -43,86 +35,26 @@ func (s *TaskService) UpdateRequest(ctx context.Context, req *dto.TaskRequest) e
func (s *TaskService) ListRequest(ctx context.Context, page, size int, name, executorType string, enabled *bool, next map[uint]time.Time) ([]map[string]any, int64, error) {
return s.Tasks(ctx, page, size, &biz.TimedTask{Name: name, ExecutorType: executorType, EnabledFilter: enabled}, next)
}
-func (s *TaskService) Validate(v *biz.TimedTask) error {
- if v.Name == "" {
- return errors.New("任务名不能为空")
- }
- var err error
- if v.WithSeconds {
- _, err = cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor).Parse(v.Spec)
- } else {
- _, err = cron.ParseStandard(v.Spec)
- }
- if err != nil {
- return fmt.Errorf("cron 表达式非法: %w", err)
- }
- switch v.ExecutorType {
- case "method":
- if v.MethodName != "ClearDB" && v.MethodName != "CleanStaleUploads" {
- return errors.New("方法未注册")
- }
- if len(v.Params) > 0 && !json.Valid(v.Params) {
- return errors.New("params 必须是合法 JSON")
- }
- case "http":
- parsed, parseErr := url.Parse(v.HTTPURL)
- if parseErr != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.Hostname() == "" || parsed.User != nil {
- return errors.New("httpUrl 必须是合法的 http/https 地址")
- }
- if len(v.HTTPHeader) > 0 {
- headers := map[string]string{}
- if json.Unmarshal(v.HTTPHeader, &headers) != nil {
- return errors.New("httpHeader 必须是 JSON 对象")
- }
- }
- default:
- return errors.New("executorType 必须为 method 或 http")
- }
- return nil
-}
func (s *TaskService) Create(ctx context.Context, v *biz.TimedTask) error {
- if err := s.Validate(v); err != nil {
- return err
- }
- exists, err := s.uc.Repo().TaskNameExists(ctx, v.Name, 0)
- if err != nil {
- return err
- }
- if exists {
- return fmt.Errorf("任务名 %s 已存在", v.Name)
- }
- return s.uc.Repo().CreateTask(ctx, v)
+ return s.uc.Create(ctx, v)
}
func (s *TaskService) Update(ctx context.Context, v *biz.TimedTask) error {
- if v.ID == 0 {
- return errors.New("缺少任务 ID")
- }
- if err := s.Validate(v); err != nil {
- return err
- }
- exists, err := s.uc.Repo().TaskNameExists(ctx, v.Name, v.ID)
- if err != nil {
- return err
- }
- if exists {
- return fmt.Errorf("任务名 %s 已存在", v.Name)
- }
- return s.uc.Repo().UpdateTask(ctx, v)
+ return s.uc.Update(ctx, v)
}
func (s *TaskService) Delete(ctx context.Context, id uint) error {
- return s.uc.Repo().DeleteTask(ctx, id)
+ return s.uc.DeleteTask(ctx, id)
}
func (s *TaskService) Toggle(ctx context.Context, id uint, enabled bool) error {
- return s.uc.Repo().ToggleTask(ctx, id, enabled)
+ return s.uc.ToggleTask(ctx, id, enabled)
}
func (s *TaskService) Task(ctx context.Context, id uint) (*biz.TimedTask, error) {
- return s.uc.Repo().FindTask(ctx, id)
+ return s.uc.FindTask(ctx, id)
}
func taskDTO(v *biz.TimedTask, next *time.Time) map[string]any {
return map[string]any{"ID": v.ID, "CreatedAt": v.CreatedAt, "UpdatedAt": v.UpdatedAt, "DeletedAt": nil, "name": v.Name, "description": v.Description, "spec": v.Spec, "withSeconds": v.WithSeconds, "executorType": v.ExecutorType, "methodName": v.MethodName, "params": json.RawMessage(v.Params), "httpUrl": v.HTTPURL, "httpMethod": v.HTTPMethod, "httpHeader": json.RawMessage(v.HTTPHeader), "httpBody": v.HTTPBody, "httpAllowPrivate": v.HTTPAllowPrivate, "enabled": v.Enabled, "nextRunAt": next}
}
func (s *TaskService) Tasks(ctx context.Context, page, size int, q *biz.TimedTask, next map[uint]time.Time) ([]map[string]any, int64, error) {
- items, total, err := s.uc.Repo().ListTasks(ctx, page, size, q)
+ items, total, err := s.uc.ListTasks(ctx, page, size, q)
if err != nil {
return nil, 0, err
}
@@ -137,8 +69,15 @@ func (s *TaskService) Tasks(ctx context.Context, page, size int, q *biz.TimedTas
}
return out, total, nil
}
+
+// ScheduledTasks is the typed internal contract used by the scheduler and
+// deliberately bypasses transport response maps.
+func (s *TaskService) ScheduledTasks(ctx context.Context) ([]*biz.TimedTask, error) {
+ items, _, err := s.uc.ListTasks(ctx, 0, 0, nil)
+ return items, err
+}
func (s *TaskService) Logs(ctx context.Context, page, size int, taskID uint, status string) ([]map[string]any, int64, error) {
- items, total, err := s.uc.Repo().ListTaskLogs(ctx, page, size, taskID, status)
+ items, total, err := s.uc.ListTaskLogs(ctx, page, size, taskID, status)
if err != nil {
return nil, 0, err
}
@@ -148,85 +87,3 @@ func (s *TaskService) Logs(ctx context.Context, page, size int, taskID uint, sta
}
return out, total, nil
}
-func privateIP(ip net.IP) bool {
- return ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalMulticast() || ip.IsLinkLocalUnicast() || ip.IsUnspecified()
-}
-func (s *TaskService) runHTTP(ctx context.Context, v *biz.TimedTask) (string, error) {
- parsed, _ := url.Parse(v.HTTPURL)
- if !v.HTTPAllowPrivate {
- ips, err := net.DefaultResolver.LookupIP(ctx, "ip", parsed.Hostname())
- if err != nil {
- return "", err
- }
- for _, ip := range ips {
- if privateIP(ip) {
- return "", errors.New("禁止访问内网或环回地址")
- }
- }
- }
- method := strings.ToUpper(v.HTTPMethod)
- if method == "" {
- method = http.MethodGet
- }
- request, err := http.NewRequestWithContext(ctx, method, v.HTTPURL, bytes.NewBufferString(v.HTTPBody))
- if err != nil {
- return "", err
- }
- headers := map[string]string{}
- _ = json.Unmarshal(v.HTTPHeader, &headers)
- for key, value := range headers {
- request.Header.Set(key, value)
- }
- response, err := (&http.Client{Timeout: 30 * time.Second}).Do(request)
- if err != nil {
- return "", err
- }
- defer response.Body.Close()
- body, _ := io.ReadAll(io.LimitReader(response.Body, 4096))
- output := fmt.Sprintf("HTTP %d: %s", response.StatusCode, string(body))
- if response.StatusCode < 200 || response.StatusCode >= 300 {
- return output, fmt.Errorf("HTTP 状态码 %d", response.StatusCode)
- }
- return output, nil
-}
-func (s *TaskService) Run(ctx context.Context, v *biz.TimedTask, trigger string) (log *biz.TimedTaskLog) {
- started := time.Now()
- log = &biz.TimedTaskLog{TaskID: v.ID, TaskName: v.Name, TriggerType: trigger, StartedAt: started, Status: "success"}
- defer func() {
- if recovered := recover(); recovered != nil {
- log.Status = "fail"
- log.ErrorMsg = fmt.Sprint(recovered)
- }
- log.FinishedAt = time.Now()
- log.DurationMS = log.FinishedAt.Sub(started).Milliseconds()
- _ = s.uc.Repo().RecordTaskLog(context.Background(), log)
- }()
- runCtx, cancel := context.WithTimeout(ctx, 35*time.Second)
- defer cancel()
- var err error
- switch v.ExecutorType {
- case "method":
- switch v.MethodName {
- case "ClearDB":
- err = s.uc.Repo().CleanupLogs(runCtx)
- log.Output = "过期日志清理完成"
- case "CleanStaleUploads":
- ttl := 24
- if s.config != nil && s.config.Media != nil && s.config.Media.SessionTtl > 0 {
- ttl = int(s.config.Media.SessionTtl)
- }
- err = s.media.CleanupStale(runCtx, ttl)
- log.Output = "过期大文件上传会话清理完成"
- }
- case "http":
- log.Output, err = s.runHTTP(runCtx, v)
- }
- if err != nil {
- log.Status = "fail"
- log.ErrorMsg = err.Error()
- }
- return log
-}
-func (s *TaskService) RegisteredMethods() []map[string]any {
- return []map[string]any{{"name": "ClearDB", "description": "清理数据库过期日志(操作记录、JWT黑名单、定时任务日志)"}, {"name": "CleanStaleUploads", "description": "清理过期大文件上传会话"}}
-}
diff --git a/internal/service/task_execution.go b/internal/service/task_execution.go
new file mode 100644
index 0000000..8ea28e4
--- /dev/null
+++ b/internal/service/task_execution.go
@@ -0,0 +1,167 @@
+package service
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net"
+ "net/http"
+ "net/url"
+ "strings"
+ "syscall"
+ "time"
+
+ "kra/internal/biz"
+)
+
+func privateIP(ip net.IP) bool {
+ return ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalMulticast() || ip.IsLinkLocalUnicast() || ip.IsUnspecified()
+}
+
+func taskHTTPClient(allowPrivate bool) *http.Client {
+ dialer := &net.Dialer{Timeout: 10 * time.Second, Control: func(_ string, address string, _ syscall.RawConn) error {
+ if allowPrivate {
+ return nil
+ }
+ host, _, err := net.SplitHostPort(address)
+ if err != nil {
+ return fmt.Errorf("解析拨号地址失败: %w", err)
+ }
+ ip := net.ParseIP(host)
+ if ip == nil {
+ return fmt.Errorf("非法拨号 IP: %s", host)
+ }
+ if privateIP(ip) {
+ return fmt.Errorf("目标解析为内网/环回/链路本地地址, 已被 SSRF 防护拒绝(可在任务上开启\"允许内网\"豁免): %s", ip)
+ }
+ return nil
+ }}
+ return &http.Client{Timeout: 30 * time.Second, Transport: &http.Transport{Proxy: nil, DialContext: dialer.DialContext}}
+}
+
+func (s *TaskService) runHTTP(ctx context.Context, v *biz.TimedTask) (string, error) {
+ parsed, err := url.Parse(v.HTTPURL)
+ if err != nil {
+ return "", err
+ }
+ if parsed.Scheme != "http" && parsed.Scheme != "https" {
+ return "", fmt.Errorf("仅允许 http/https, 实际为 %q", parsed.Scheme)
+ }
+ method := strings.ToUpper(v.HTTPMethod)
+ if method == "" {
+ method = http.MethodGet
+ }
+ request, err := http.NewRequestWithContext(ctx, method, v.HTTPURL, bytes.NewBufferString(v.HTTPBody))
+ if err != nil {
+ return "", err
+ }
+ headers := map[string]string{}
+ _ = json.Unmarshal(v.HTTPHeader, &headers)
+ for key, value := range headers {
+ request.Header.Set(key, value)
+ }
+ response, err := taskHTTPClient(v.HTTPAllowPrivate).Do(request)
+ if err != nil {
+ return "", err
+ }
+ defer response.Body.Close()
+ body, _ := io.ReadAll(io.LimitReader(response.Body, 1<<20))
+ output := fmt.Sprintf("HTTP %d: %s", response.StatusCode, string(body))
+ if response.StatusCode < 200 || response.StatusCode >= 300 {
+ return output, fmt.Errorf("非 2xx 响应: %d", response.StatusCode)
+ }
+ return output, nil
+}
+
+var errTaskTimeout = errors.New("任务执行超时")
+
+func truncateTaskText(value string) string {
+ const limit = 4000
+ if len(value) <= limit {
+ return value
+ }
+ return value[:limit] + "...(截断)"
+}
+
+func (s *TaskService) runMethod(v *biz.TimedTask) error {
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
+ defer cancel()
+ done := make(chan error, 1)
+ go func() {
+ defer func() {
+ if recovered := recover(); recovered != nil {
+ done <- fmt.Errorf("panic: %v", recovered)
+ }
+ }()
+ var err error
+ switch v.MethodName {
+ case biz.TaskMethodClearDB:
+ err = s.uc.CleanupLogs(ctx)
+ case biz.TaskMethodUploads:
+ ttl := 24
+ config := s.runtime.Admin()
+ if config != nil && config.Media != nil && config.Media.SessionTtl > 0 {
+ ttl = int(config.Media.SessionTtl)
+ }
+ err = s.media.CleanupStale(ctx, ttl)
+ }
+ done <- err
+ }()
+ select {
+ case err := <-done:
+ if errors.Is(err, context.DeadlineExceeded) {
+ return errTaskTimeout
+ }
+ return err
+ case <-ctx.Done():
+ return errTaskTimeout
+ }
+}
+
+func (s *TaskService) Run(ctx context.Context, v *biz.TimedTask, trigger string) (log *biz.TimedTaskLog) {
+ started := time.Now()
+ log = &biz.TimedTaskLog{TaskID: v.ID, TaskName: v.Name, TriggerType: trigger, StartedAt: started, Status: "success"}
+ defer func() {
+ if recovered := recover(); recovered != nil {
+ log.Status = "fail"
+ log.ErrorMsg = truncateTaskText(fmt.Sprint(recovered))
+ }
+ log.Output = truncateTaskText(log.Output)
+ log.FinishedAt = time.Now()
+ log.DurationMS = log.FinishedAt.Sub(started).Milliseconds()
+ logCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 3*time.Second)
+ defer cancel()
+ _ = s.uc.RecordTaskLog(logCtx, log)
+ }()
+ var err error
+ switch v.ExecutorType {
+ case biz.TaskExecutorMethod:
+ err = s.runMethod(v)
+ case biz.TaskExecutorHTTP:
+ runCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+ log.Output, err = s.runHTTP(runCtx, v)
+ cancel()
+ default:
+ err = fmt.Errorf("未知执行器类型: %s", v.ExecutorType)
+ }
+ if err != nil {
+ if errors.Is(err, errTaskTimeout) || errors.Is(err, context.DeadlineExceeded) {
+ log.Status = "timeout"
+ } else {
+ log.Status = "fail"
+ }
+ log.ErrorMsg = truncateTaskText(err.Error())
+ }
+ return log
+}
+func (s *TaskService) RegisteredMethods() []map[string]any {
+ methods := biz.RegisteredTaskMethods()
+ out := make([]map[string]any, 0, len(methods))
+ for _, method := range methods {
+ out = append(out, map[string]any{"name": method.Name, "description": method.Description})
+ }
+ return out
+}
diff --git a/internal/service/task_execution_test.go b/internal/service/task_execution_test.go
new file mode 100644
index 0000000..a8c323f
--- /dev/null
+++ b/internal/service/task_execution_test.go
@@ -0,0 +1,17 @@
+package service
+
+import (
+ "net"
+ "testing"
+)
+
+func TestPrivateIP(t *testing.T) {
+ for _, value := range []string{"127.0.0.1", "10.0.0.1", "172.16.0.1", "192.168.1.1", "169.254.1.1", "::1"} {
+ if !privateIP(net.ParseIP(value)) {
+ t.Fatalf("expected %s to be private", value)
+ }
+ }
+ if privateIP(net.ParseIP("8.8.8.8")) {
+ t.Fatal("public IP was classified as private")
+ }
+}
diff --git a/internal/service/user.go b/internal/service/user.go
index 32632ff..9c2ab99 100644
--- a/internal/service/user.go
+++ b/internal/service/user.go
@@ -7,26 +7,6 @@ import (
"kra/internal/service/dto"
)
-func (s *SystemService) User(ctx context.Context, id uint) (map[string]any, error) {
- value, err := s.uc.User(ctx, id)
- if err != nil {
- return nil, err
- }
- return convertUser(value), nil
-}
-
-func (s *SystemService) Menus(ctx context.Context, authorityID uint) ([]map[string]any, error) {
- menus, err := s.uc.Menus(ctx, authorityID)
- if err != nil {
- return nil, err
- }
- result := make([]map[string]any, 0, len(menus))
- for _, menu := range menus {
- result = append(result, convertMenu(menu))
- }
- return result, nil
-}
-
type UserInput struct {
ID uint
Username, Password, NickName, HeaderImg, Phone, Email string
@@ -48,6 +28,26 @@ func (s *SystemService) UpdateUserRequest(ctx context.Context, value *dto.UserRe
return s.UpdateUser(ctx, userInput(value))
}
+func (s *SystemService) User(ctx context.Context, id uint) (map[string]any, error) {
+ value, err := s.uc.User(ctx, id)
+ if err != nil {
+ return nil, err
+ }
+ return convertUser(value), nil
+}
+
+func (s *SystemService) Menus(ctx context.Context, authorityID uint) ([]map[string]any, error) {
+ menus, err := s.uc.Menus(ctx, authorityID)
+ if err != nil {
+ return nil, err
+ }
+ result := make([]map[string]any, 0, len(menus))
+ for _, menu := range menus {
+ result = append(result, convertMenu(menu))
+ }
+ return result, nil
+}
+
func (s *SystemService) ListUsers(ctx context.Context, page, pageSize int, filter *biz.UserListFilter) ([]map[string]any, int64, error) {
users, total, err := s.uc.ListUsers(ctx, page, pageSize, filter)
if err != nil {
@@ -59,51 +59,36 @@ func (s *SystemService) ListUsers(ctx context.Context, page, pageSize int, filte
}
return result, total, nil
}
+
+func (s *SystemService) Authorities(ctx context.Context) ([]map[string]any, error) {
+ values, err := s.uc.Authorities(ctx)
+ if err != nil {
+ return nil, err
+ }
+ result := make([]map[string]any, 0, len(values))
+ for _, value := range values {
+ result = append(result, convertAuthority(*value))
+ }
+ return result, nil
+}
+
func (s *SystemService) CreateUser(ctx context.Context, input UserInput) (map[string]any, error) {
if err := s.settings.ValidatePassword(ctx, input.Password); err != nil {
return nil, err
}
security, _ := s.settings.CurrentSecurity(ctx)
mustChange := security != nil && security.ForceNewUserChangePassword
- if input.AuthorityID == 0 && len(input.AuthorityIDs) > 0 {
- input.AuthorityID = input.AuthorityIDs[0]
- }
- input.AuthorityIDs = includeAuthority(input.AuthorityIDs, input.AuthorityID)
- user, err := s.uc.CreateUser(ctx, &biz.User{Username: input.Username, Password: input.Password, NickName: input.NickName, HeaderImg: input.HeaderImg, AuthorityID: input.AuthorityID, Phone: input.Phone, Email: input.Email, Enable: input.Enable, MustChangePassword: mustChange})
+ user, err := s.uc.CreateUser(ctx, &biz.User{Username: input.Username, Password: input.Password, NickName: input.NickName, HeaderImg: input.HeaderImg, AuthorityID: input.AuthorityID, Phone: input.Phone, Email: input.Email, Enable: input.Enable, MustChangePassword: mustChange}, input.AuthorityIDs)
if err != nil {
return nil, err
}
- if len(input.AuthorityIDs) > 0 {
- if err = s.uc.SetUserAuthorities(ctx, user.ID, input.AuthorityIDs); err != nil {
- return nil, err
- }
- user, err = s.uc.User(ctx, user.ID)
- if err != nil {
- return nil, err
- }
- }
return convertUser(user), nil
}
func (s *SystemService) UpdateUser(ctx context.Context, input UserInput) error {
- input.AuthorityIDs = includeAuthority(input.AuthorityIDs, input.AuthorityID)
- if err := s.uc.UpdateUser(ctx, &biz.User{ID: input.ID, NickName: input.NickName, HeaderImg: input.HeaderImg, AuthorityID: input.AuthorityID, Phone: input.Phone, Email: input.Email, Enable: input.Enable}); err != nil {
- return err
- }
- if len(input.AuthorityIDs) > 0 {
- return s.uc.SetUserAuthorities(ctx, input.ID, input.AuthorityIDs)
- }
- return nil
+ return s.uc.UpdateUser(ctx, &biz.User{ID: input.ID, NickName: input.NickName, HeaderImg: input.HeaderImg, AuthorityID: input.AuthorityID, Phone: input.Phone, Email: input.Email, Enable: input.Enable}, input.AuthorityIDs)
}
-func includeAuthority(ids []uint, primary uint) []uint {
- if primary == 0 {
- return ids
- }
- for _, id := range ids {
- if id == primary {
- return ids
- }
- }
- return append(ids, primary)
+func (s *SystemService) UpdateSelfUser(ctx context.Context, input UserInput) error {
+ return s.uc.UpdateSelfUser(ctx, &biz.User{ID: input.ID, NickName: input.NickName, HeaderImg: input.HeaderImg, Phone: input.Phone, Email: input.Email, Enable: input.Enable})
}
func (s *SystemService) DeleteUser(ctx context.Context, id uint) error {
return s.uc.DeleteUser(ctx, id)
@@ -120,17 +105,7 @@ func (s *SystemService) ChangePassword(ctx context.Context, id uint, oldPassword
}
return s.uc.ChangePassword(ctx, id, oldPassword, newPassword)
}
-func (s *SystemService) Authorities(ctx context.Context) ([]map[string]any, error) {
- values, err := s.uc.Authorities(ctx)
- if err != nil {
- return nil, err
- }
- result := make([]map[string]any, 0, len(values))
- for _, value := range values {
- result = append(result, convertAuthority(*value))
- }
- return result, nil
-}
+
func (s *SystemService) SetUserAuthorities(ctx context.Context, id uint, authorityIDs []uint) error {
return s.uc.SetUserAuthorities(ctx, id, authorityIDs)
}
@@ -142,50 +117,8 @@ func (s *SystemService) SwitchAuthority(ctx context.Context, id, authorityID uin
if err != nil {
return nil, err
}
- return s.issueLogin(user, authorityID)
+ return s.issueLogin(ctx, user, authorityID)
}
func (s *SystemService) SetUserSetting(ctx context.Context, id uint, setting map[string]any) error {
return s.uc.SetUserSetting(ctx, id, setting)
}
-
-func convertAuthority(value biz.Authority) map[string]any {
- return map[string]any{"authorityId": value.AuthorityID, "authorityName": value.AuthorityName, "parentId": value.ParentID, "children": []map[string]any{}, "menus": []any{}, "dataScope": value.DataScope, "defaultRouter": value.DefaultRouter}
-}
-func convertUser(user *biz.User) map[string]any {
- authorities := make([]map[string]any, 0, len(user.Authorities))
- for _, value := range user.Authorities {
- authorities = append(authorities, convertAuthority(value))
- }
- departments := make([]map[string]any, 0, len(user.Departments))
- for i := range user.Departments {
- departments = append(departments, departmentDTO(&user.Departments[i]))
- }
- positions := make([]map[string]any, 0, len(user.Positions))
- for i := range user.Positions {
- positions = append(positions, positionDTO(&user.Positions[i]))
- }
- department := map[string]any{}
- if user.Department != nil {
- department = departmentDTO(user.Department)
- }
- return map[string]any{"ID": user.ID, "CreatedAt": user.CreatedAt, "UpdatedAt": user.UpdatedAt, "DeletedAt": nil, "uuid": user.UUID, "userName": user.Username, "nickName": user.NickName, "headerImg": user.HeaderImg, "authorityId": user.AuthorityID, "authority": convertAuthority(user.Authority), "authorities": authorities, "deptId": user.DeptID, "dept": department, "departments": departments, "positions": positions, "phone": user.Phone, "email": user.Email, "enable": user.Enable, "originSetting": user.OriginSetting}
-}
-func convertMenu(menu *biz.Menu) map[string]any {
- children := make([]map[string]any, 0, len(menu.Children))
- for _, child := range menu.Children {
- children = append(children, convertMenu(child))
- }
- buttons := make([]map[string]any, 0, len(menu.Buttons))
- buttonAuthorities := make(map[string]uint, len(menu.Buttons))
- for _, button := range menu.Buttons {
- buttons = append(buttons, map[string]any{"ID": button.ID, "name": button.Name, "desc": button.Description, "sysBaseMenuID": button.MenuID})
- if button.AuthorityID != 0 {
- buttonAuthorities[button.Name] = button.AuthorityID
- }
- }
- parameters := make([]map[string]any, 0, len(menu.Parameters))
- for _, parameter := range menu.Parameters {
- parameters = append(parameters, map[string]any{"ID": parameter.ID, "SysBaseMenuID": parameter.MenuID, "type": parameter.Type, "key": parameter.Key, "value": parameter.Value})
- }
- return map[string]any{"ID": menu.ID, "parentId": menu.ParentID, "path": menu.Path, "name": menu.Name, "hidden": menu.Hidden, "component": menu.Component, "sort": menu.Sort, "meta": map[string]any{"activeName": menu.ActiveName, "keepAlive": menu.KeepAlive, "defaultMenu": menu.DefaultMenu, "title": menu.Title, "icon": menu.Icon, "closeTab": menu.CloseTab, "transitionType": menu.TransitionType}, "children": children, "parameters": parameters, "menuBtn": buttons, "btns": buttonAuthorities}
-}
diff --git a/internal/service/user_conversion.go b/internal/service/user_conversion.go
new file mode 100644
index 0000000..2e07b97
--- /dev/null
+++ b/internal/service/user_conversion.go
@@ -0,0 +1,56 @@
+package service
+
+import "kra/internal/biz"
+
+func departmentDTO(v *biz.Department) map[string]any {
+ children := make([]map[string]any, 0, len(v.Children))
+ for _, c := range v.Children {
+ children = append(children, departmentDTO(c))
+ }
+ return map[string]any{"ID": v.ID, "CreatedAt": v.CreatedAt, "UpdatedAt": v.UpdatedAt, "DeletedAt": nil, "name": v.Name, "parentId": v.ParentID, "ancestors": v.Ancestors, "sort": v.Sort, "leaderId": v.LeaderID, "leader": nil, "status": v.Status, "children": children, "namePath": v.NamePath}
+}
+func positionDTO(v *biz.Position) map[string]any {
+ return map[string]any{"ID": v.ID, "CreatedAt": v.CreatedAt, "UpdatedAt": v.UpdatedAt, "DeletedAt": nil, "name": v.Name, "code": v.Code, "sort": v.Sort, "status": v.Status, "remark": v.Remark}
+}
+
+func convertAuthority(value biz.Authority) map[string]any {
+ return map[string]any{"authorityId": value.AuthorityID, "authorityName": value.AuthorityName, "parentId": value.ParentID, "children": []map[string]any{}, "menus": []any{}, "dataScope": value.DataScope, "defaultRouter": value.DefaultRouter}
+}
+func convertUser(user *biz.User) map[string]any {
+ authorities := make([]map[string]any, 0, len(user.Authorities))
+ for _, value := range user.Authorities {
+ authorities = append(authorities, convertAuthority(value))
+ }
+ departments := make([]map[string]any, 0, len(user.Departments))
+ for i := range user.Departments {
+ departments = append(departments, departmentDTO(&user.Departments[i]))
+ }
+ positions := make([]map[string]any, 0, len(user.Positions))
+ for i := range user.Positions {
+ positions = append(positions, positionDTO(&user.Positions[i]))
+ }
+ department := map[string]any{}
+ if user.Department != nil {
+ department = departmentDTO(user.Department)
+ }
+ return map[string]any{"ID": user.ID, "CreatedAt": user.CreatedAt, "UpdatedAt": user.UpdatedAt, "DeletedAt": nil, "uuid": user.UUID, "userName": user.Username, "nickName": user.NickName, "headerImg": user.HeaderImg, "authorityId": user.AuthorityID, "authority": convertAuthority(user.Authority), "authorities": authorities, "deptId": user.DeptID, "dept": department, "departments": departments, "positions": positions, "phone": user.Phone, "email": user.Email, "enable": user.Enable, "originSetting": user.OriginSetting}
+}
+func convertMenu(menu *biz.Menu) map[string]any {
+ children := make([]map[string]any, 0, len(menu.Children))
+ for _, child := range menu.Children {
+ children = append(children, convertMenu(child))
+ }
+ buttons := make([]map[string]any, 0, len(menu.Buttons))
+ buttonAuthorities := make(map[string]uint, len(menu.Buttons))
+ for _, button := range menu.Buttons {
+ buttons = append(buttons, map[string]any{"ID": button.ID, "name": button.Name, "desc": button.Description, "sysBaseMenuID": button.MenuID})
+ if button.AuthorityID != 0 {
+ buttonAuthorities[button.Name] = button.AuthorityID
+ }
+ }
+ parameters := make([]map[string]any, 0, len(menu.Parameters))
+ for _, parameter := range menu.Parameters {
+ parameters = append(parameters, map[string]any{"ID": parameter.ID, "SysBaseMenuID": parameter.MenuID, "type": parameter.Type, "key": parameter.Key, "value": parameter.Value})
+ }
+ return map[string]any{"ID": menu.ID, "parentId": menu.ParentID, "path": menu.Path, "name": menu.Name, "hidden": menu.Hidden, "component": menu.Component, "sort": menu.Sort, "meta": map[string]any{"activeName": menu.ActiveName, "keepAlive": menu.KeepAlive, "defaultMenu": menu.DefaultMenu, "title": menu.Title, "icon": menu.Icon, "closeTab": menu.CloseTab, "transitionType": menu.TransitionType}, "children": children, "parameters": parameters, "menuBtn": buttons, "btns": buttonAuthorities}
+}
diff --git a/internal/service/version.go b/internal/service/version.go
index 2af80fc..37d00ca 100644
--- a/internal/service/version.go
+++ b/internal/service/version.go
@@ -15,8 +15,8 @@ func NewVersionService(uc *biz.VersionUsecase) *VersionService { return &Version
func versionDTO(v *biz.Version) map[string]any {
return map[string]any{"ID": v.ID, "CreatedAt": v.CreatedAt, "UpdatedAt": v.UpdatedAt, "DeletedAt": nil, "versionName": v.Name, "versionCode": v.Code, "description": v.Description, "versionData": v.Data}
}
-func (s *VersionService) Versions(ctx context.Context, page, size int, name, code string) ([]map[string]any, int64, error) {
- items, total, err := s.uc.Repo().ListVersions(ctx, page, size, name, code)
+func (s *VersionService) Versions(ctx context.Context, page, size int, name, code string, createdAtRange []*time.Time) ([]map[string]any, int64, error) {
+ items, total, err := s.uc.ListVersions(ctx, page, size, name, code, createdAtRange)
if err != nil {
return nil, 0, err
}
@@ -27,24 +27,25 @@ func (s *VersionService) Versions(ctx context.Context, page, size int, name, cod
return out, total, nil
}
func (s *VersionService) Version(ctx context.Context, id uint) (map[string]any, error) {
- v, err := s.uc.Repo().FindVersion(ctx, id)
+ v, err := s.uc.FindVersion(ctx, id)
if err != nil {
return nil, err
}
return versionDTO(v), nil
}
func (s *VersionService) VersionData(ctx context.Context, id uint) (string, string, error) {
- v, err := s.uc.Repo().FindVersion(ctx, id)
+ v, err := s.uc.FindVersion(ctx, id)
if err != nil {
return "", "", err
}
return v.Data, v.Code, nil
}
func (s *VersionService) DeleteVersions(ctx context.Context, ids []uint) error {
- return s.uc.Repo().DeleteVersions(ctx, ids)
+ return s.uc.DeleteVersions(ctx, ids)
}
+
func (s *VersionService) Export(ctx context.Context, name, code, description string, menuIDs, apiIDs, dictIDs []uint) error {
- bundle, err := s.uc.Repo().BuildVersionBundle(ctx, menuIDs, apiIDs, dictIDs)
+ bundle, err := s.uc.BuildVersionBundle(ctx, menuIDs, apiIDs, dictIDs)
if err != nil {
return err
}
@@ -69,22 +70,28 @@ func (s *VersionService) Export(ctx context.Context, name, code, description str
if err != nil {
return err
}
- return s.uc.Repo().CreateVersion(ctx, &biz.Version{Name: name, Code: code, Description: description, Data: string(raw)})
+ return s.uc.CreateVersion(ctx, &biz.Version{Name: name, Code: code, Description: description, Data: string(raw)})
}
func (s *VersionService) Import(ctx context.Context, bundle *biz.VersionBundle) error {
- return s.uc.Repo().ImportVersionBundle(ctx, bundle)
+ return s.uc.ImportVersionBundle(ctx, bundle)
}
func versionMenuDomain(value dto.VersionMenuRequest) *biz.Menu {
menu := &biz.Menu{Path: value.Path, Name: value.Name, Hidden: value.Hidden, Component: value.Component, Sort: value.Sort, ActiveName: value.Meta.ActiveName, KeepAlive: value.Meta.KeepAlive, DefaultMenu: value.Meta.DefaultMenu, Title: value.Meta.Title, Icon: value.Meta.Icon, CloseTab: value.Meta.CloseTab, TransitionType: value.Meta.TransitionType}
for _, button := range value.MenuButtons {
menu.Buttons = append(menu.Buttons, &biz.MenuButton{Name: button.Name, Description: button.Description})
}
+ for _, parameter := range value.Parameters {
+ menu.Parameters = append(menu.Parameters, &biz.MenuParameter{Type: parameter.Type, Key: parameter.Key, Value: parameter.Value})
+ }
for _, child := range value.Children {
menu.Children = append(menu.Children, versionMenuDomain(child))
}
return menu
}
func (s *VersionService) ImportRequest(ctx context.Context, req *dto.ImportVersionRequest) error {
+ if req.Version.Name == "" || req.Version.Code == "" {
+ return biz.ErrInvalidVersion
+ }
bundle := &biz.VersionBundle{Name: req.Version.Name, Code: req.Version.Code, Description: req.Version.Description, ExportTime: req.Version.ExportTime}
for _, menu := range req.Menus {
bundle.Menus = append(bundle.Menus, versionMenuDomain(menu))
@@ -103,5 +110,15 @@ func (s *VersionService) ImportRequest(ctx context.Context, req *dto.ImportVersi
}
bundle.Dictionaries = append(bundle.Dictionaries, item)
}
- return s.Import(ctx, bundle)
+ if err := s.Import(ctx, bundle); err != nil {
+ return err
+ }
+ raw, _ := json.Marshal(req)
+ _ = s.uc.CreateVersion(ctx, &biz.Version{
+ Name: req.Version.Name,
+ Code: req.Version.Code + "_imported_" + time.Now().Format("20060102150405"),
+ Description: "导入版本: " + req.Version.Description,
+ Data: string(raw),
+ })
+ return nil
}
diff --git a/internal/worker/task_scheduler.go b/internal/worker/task_scheduler.go
index 3c5fb52..a9ca4f9 100644
--- a/internal/worker/task_scheduler.go
+++ b/internal/worker/task_scheduler.go
@@ -21,6 +21,9 @@ type TaskScheduler struct {
seconds *cron.Cron
mu sync.Mutex
entries map[uint]scheduledEntry
+ ctxMu sync.RWMutex
+ runContext context.Context
+ cancel context.CancelFunc
subMu sync.RWMutex
subscribers map[chan []byte]struct{}
}
@@ -35,15 +38,18 @@ func NewTaskScheduler(service *service.TaskService, logger *slog.Logger) *TaskSc
}
func (s *TaskScheduler) Start(ctx context.Context) error {
+ runContext, cancel := context.WithCancel(ctx)
+ s.ctxMu.Lock()
+ s.runContext, s.cancel = runContext, cancel
+ s.ctxMu.Unlock()
s.standard.Start()
s.seconds.Start()
- items, _, err := s.service.Tasks(ctx, 1, 100000, nil, nil)
+ items, err := s.service.ScheduledTasks(ctx)
if err == nil {
- for _, raw := range items {
- id, _ := raw["ID"].(uint)
- if task, findErr := s.service.Task(ctx, id); findErr == nil && task.Enabled {
+ for _, task := range items {
+ if task.Enabled {
if scheduleErr := s.Schedule(task); scheduleErr != nil {
- s.logger.ErrorContext(ctx, "restore timed task failed", "id", id, "error", scheduleErr)
+ s.logger.ErrorContext(ctx, "restore timed task failed", "id", task.ID, "error", scheduleErr)
}
}
}
@@ -55,6 +61,11 @@ func (s *TaskScheduler) Start(ctx context.Context) error {
}
func (s *TaskScheduler) Stop(ctx context.Context) error {
+ s.ctxMu.Lock()
+ if s.cancel != nil {
+ s.cancel()
+ }
+ s.ctxMu.Unlock()
standardDone, secondsDone := s.standard.Stop().Done(), s.seconds.Stop().Done()
select {
case <-standardDone:
@@ -93,16 +104,11 @@ func (s *TaskScheduler) Reload(ctx context.Context) error {
delete(s.entries, id)
}
s.mu.Unlock()
- items, _, err := s.service.Tasks(ctx, 1, 100000, nil, nil)
+ items, err := s.service.ScheduledTasks(ctx)
if err != nil {
return err
}
- for _, raw := range items {
- id, _ := raw["ID"].(uint)
- task, findErr := s.service.Task(ctx, id)
- if findErr != nil {
- return findErr
- }
+ for _, task := range items {
if task.Enabled {
if err = s.Schedule(task); err != nil {
return err
@@ -112,6 +118,22 @@ func (s *TaskScheduler) Reload(ctx context.Context) error {
return nil
}
+func (s *TaskScheduler) executionContext() context.Context {
+ s.ctxMu.RLock()
+ defer s.ctxMu.RUnlock()
+ if s.runContext != nil {
+ return s.runContext
+ }
+ return context.Background()
+}
+
+func (s *TaskScheduler) run(task *biz.TimedTask, trigger string) {
+ log := s.service.Run(s.executionContext(), task, trigger)
+ if log.Status != "success" {
+ s.Broadcast(map[string]any{"taskId": task.ID, "taskName": task.Name, "status": log.Status, "errorMsg": log.ErrorMsg, "time": time.Now()})
+ }
+}
+
func (s *TaskScheduler) Schedule(task *biz.TimedTask) error {
s.Remove(task.ID)
if !task.Enabled {
@@ -119,10 +141,7 @@ func (s *TaskScheduler) Schedule(task *biz.TimedTask) error {
}
copy := *task
run := func() {
- log := s.service.Run(context.Background(), ©, "auto")
- if log.Status != "success" {
- s.Broadcast(map[string]any{"taskId": copy.ID, "taskName": copy.Name, "status": log.Status, "errorMsg": log.ErrorMsg, "time": time.Now()})
- }
+ s.run(©, "auto")
}
var id cron.EntryID
var err error
@@ -172,12 +191,8 @@ func (s *TaskScheduler) NextRuns() map[uint]time.Time {
}
func (s *TaskScheduler) Trigger(task *biz.TimedTask) {
- go func() {
- log := s.service.Run(context.Background(), task, "manual")
- if log.Status != "success" {
- s.Broadcast(map[string]any{"taskId": task.ID, "taskName": task.Name, "status": log.Status, "errorMsg": log.ErrorMsg, "time": time.Now()})
- }
- }()
+ copy := *task
+ go s.run(©, "manual")
}
func (s *TaskScheduler) Subscribe() chan []byte {
diff --git a/pkg/logging/daily.go b/pkg/logging/daily.go
new file mode 100644
index 0000000..58d0b6d
--- /dev/null
+++ b/pkg/logging/daily.go
@@ -0,0 +1,55 @@
+package logging
+
+import (
+ "os"
+ "path/filepath"
+ "sync"
+ "time"
+)
+
+// DailyWriter writes log entries to
- 后端需要提前
+ 后端通过
import
- "github.com/kra/kra/server/service/system"SettingsUsecase.ParameterByKey(ctx, key)
-
- 然后调用
- new(system.SysParamsService).GetSysParam("{{
- formData.key
- }}")
- 来获取对应的 value 值。
+ 获取对应参数值,业务代码应通过 biz 层接口调用。
保存后点击“重载服务”,数据库连接会按新配置重建。
+