From 8c4c6ba661406a2550bfac9f0bcd32ddfeb0d24a Mon Sep 17 00:00:00 2001
From: yvan <8574526@qq.com>
Date: Fri, 14 Aug 2026 01:37:56 +0800
Subject: [PATCH] refactor: separate backend domains and storage adapters
---
cmd/kratos-admin/wire_gen.go | 28 +-
internal/biz/api_token.go | 28 +
internal/biz/dictionary.go | 49 +
internal/biz/media.go | 155 +--
internal/biz/media_upload.go | 162 ++++
internal/biz/parameter.go | 24 +
internal/biz/security.go | 31 +
internal/biz/settings.go | 110 +--
internal/biz/system.go | 147 +--
internal/biz/system_init.go | 31 +
internal/biz/user.go | 129 +++
internal/data/access.go | 897 +-----------------
internal/data/api.go | 229 +++++
internal/data/api_token.go | 123 +++
internal/data/audit.go | 425 +--------
internal/data/authority.go | 283 ++++++
internal/data/data_access_log.go | 66 ++
internal/data/department.go | 215 +++++
internal/data/dictionary.go | 257 +++++
internal/data/error_record.go | 82 ++
internal/data/file_storage.go | 210 +---
internal/data/log_file.go | 166 ++++
internal/data/login_log.go | 70 ++
internal/data/media.go | 100 +-
internal/data/media_upload.go | 105 ++
internal/data/operation_log.go | 86 ++
internal/data/organization.go | 7 +
internal/data/parameter.go | 83 ++
internal/data/permission.go | 85 ++
internal/data/position.go | 127 +++
internal/data/security.go | 74 ++
internal/data/settings.go | 504 +---------
internal/data/{ => storage}/aliyun_storage.go | 2 +-
internal/data/{ => storage}/aws_storage.go | 2 +-
internal/data/{ => storage}/huawei_storage.go | 2 +-
internal/data/storage/local.go | 219 +++++
internal/data/{ => storage}/qiniu_storage.go | 2 +-
internal/data/{ => storage}/s3_storage.go | 2 +-
.../data/{ => storage}/tencent_storage.go | 2 +-
internal/data/system.go | 463 +--------
internal/data/system_init.go | 192 ++++
internal/data/user.go | 292 ++++++
internal/server/audit_middleware.go | 101 --
internal/server/audit_routes.go | 263 -----
internal/server/export_routes.go | 224 -----
internal/server/gin.go | 492 +---------
internal/server/handler/api_token.go | 58 ++
internal/server/handler/audit.go | 241 +++++
internal/server/handler/dictionary.go | 189 ++++
internal/server/handler/export.go | 193 ++++
internal/server/handler/media.go | 230 +++++
internal/server/handler/navigation.go | 26 +
internal/server/handler/parameter.go | 100 ++
internal/server/handler/public.go | 200 ++++
internal/server/handler/session.go | 26 +
internal/server/handler/system_config.go | 101 ++
internal/server/handler/user.go | 178 ++++
internal/server/handler/version.go | 93 ++
internal/server/media_routes.go | 265 ------
internal/server/middleware/access.go | 8 +-
internal/server/middleware/audit.go | 46 +
internal/server/middleware/capture.go | 66 ++
.../{email_routes.go => middleware/email.go} | 11 +-
internal/server/middleware/rate_limit.go | 38 +
internal/server/response.go | 29 -
internal/server/router/api_token.go | 14 +
internal/server/router/audit.go | 34 +
internal/server/router/dictionary.go | 29 +
internal/server/router/export.go | 24 +
internal/server/router/media.go | 28 +
internal/server/router/parameter.go | 18 +
internal/server/router/public.go | 16 +
internal/server/router/system_config.go | 19 +
internal/server/router/user.go | 30 +
internal/server/router/version.go | 18 +
internal/server/server.go | 2 +-
internal/server/settings_routes.go | 435 ---------
internal/server/system_config_routes.go | 215 -----
internal/server/version_routes.go | 165 ----
internal/service/access.go | 7 +
internal/service/api_token.go | 79 ++
internal/service/audit.go | 31 +
internal/service/authentication.go | 62 ++
internal/service/dictionary.go | 182 ++++
internal/service/dto/audit.go | 46 +
internal/service/dto/export.go | 26 +
internal/service/dto/media.go | 50 +
internal/service/dto/settings.go | 44 +
internal/service/dto/system.go | 72 ++
internal/service/dto/system_config.go | 56 ++
internal/service/dto/version.go | 67 ++
internal/service/export.go | 20 +
internal/service/media.go | 35 +-
internal/service/media_upload.go | 30 +
internal/service/parameter.go | 51 +
internal/service/security.go | 98 ++
internal/service/settings.go | 264 ------
internal/service/system.go | 271 ------
internal/service/system_config.go | 126 +++
internal/service/system_init.go | 48 +
internal/service/user.go | 191 ++++
internal/service/version.go | 32 +
102 files changed, 6644 insertions(+), 5735 deletions(-)
create mode 100644 internal/biz/api_token.go
create mode 100644 internal/biz/dictionary.go
create mode 100644 internal/biz/media_upload.go
create mode 100644 internal/biz/parameter.go
create mode 100644 internal/biz/security.go
create mode 100644 internal/biz/system_init.go
create mode 100644 internal/biz/user.go
create mode 100644 internal/data/api.go
create mode 100644 internal/data/api_token.go
create mode 100644 internal/data/authority.go
create mode 100644 internal/data/data_access_log.go
create mode 100644 internal/data/department.go
create mode 100644 internal/data/dictionary.go
create mode 100644 internal/data/error_record.go
create mode 100644 internal/data/log_file.go
create mode 100644 internal/data/login_log.go
create mode 100644 internal/data/media_upload.go
create mode 100644 internal/data/operation_log.go
create mode 100644 internal/data/organization.go
create mode 100644 internal/data/parameter.go
create mode 100644 internal/data/permission.go
create mode 100644 internal/data/position.go
create mode 100644 internal/data/security.go
rename internal/data/{ => storage}/aliyun_storage.go (99%)
rename internal/data/{ => storage}/aws_storage.go (99%)
rename internal/data/{ => storage}/huawei_storage.go (99%)
create mode 100644 internal/data/storage/local.go
rename internal/data/{ => storage}/qiniu_storage.go (99%)
rename internal/data/{ => storage}/s3_storage.go (99%)
rename internal/data/{ => storage}/tencent_storage.go (99%)
create mode 100644 internal/data/system_init.go
create mode 100644 internal/data/user.go
delete mode 100644 internal/server/audit_middleware.go
delete mode 100644 internal/server/audit_routes.go
delete mode 100644 internal/server/export_routes.go
create mode 100644 internal/server/handler/api_token.go
create mode 100644 internal/server/handler/audit.go
create mode 100644 internal/server/handler/dictionary.go
create mode 100644 internal/server/handler/export.go
create mode 100644 internal/server/handler/media.go
create mode 100644 internal/server/handler/navigation.go
create mode 100644 internal/server/handler/parameter.go
create mode 100644 internal/server/handler/public.go
create mode 100644 internal/server/handler/session.go
create mode 100644 internal/server/handler/system_config.go
create mode 100644 internal/server/handler/user.go
create mode 100644 internal/server/handler/version.go
delete mode 100644 internal/server/media_routes.go
create mode 100644 internal/server/middleware/audit.go
create mode 100644 internal/server/middleware/capture.go
rename internal/server/{email_routes.go => middleware/email.go} (75%)
create mode 100644 internal/server/middleware/rate_limit.go
delete mode 100644 internal/server/response.go
create mode 100644 internal/server/router/api_token.go
create mode 100644 internal/server/router/audit.go
create mode 100644 internal/server/router/dictionary.go
create mode 100644 internal/server/router/export.go
create mode 100644 internal/server/router/media.go
create mode 100644 internal/server/router/parameter.go
create mode 100644 internal/server/router/public.go
create mode 100644 internal/server/router/system_config.go
create mode 100644 internal/server/router/user.go
create mode 100644 internal/server/router/version.go
delete mode 100644 internal/server/settings_routes.go
delete mode 100644 internal/server/system_config_routes.go
delete mode 100644 internal/server/version_routes.go
create mode 100644 internal/service/api_token.go
create mode 100644 internal/service/authentication.go
create mode 100644 internal/service/dictionary.go
create mode 100644 internal/service/dto/audit.go
create mode 100644 internal/service/dto/export.go
create mode 100644 internal/service/dto/media.go
create mode 100644 internal/service/dto/settings.go
create mode 100644 internal/service/dto/system.go
create mode 100644 internal/service/dto/system_config.go
create mode 100644 internal/service/dto/version.go
create mode 100644 internal/service/media_upload.go
create mode 100644 internal/service/parameter.go
create mode 100644 internal/service/security.go
create mode 100644 internal/service/system_config.go
create mode 100644 internal/service/system_init.go
create mode 100644 internal/service/user.go
diff --git a/cmd/kratos-admin/wire_gen.go b/cmd/kratos-admin/wire_gen.go
index 6d2150d..d9552f5 100644
--- a/cmd/kratos-admin/wire_gen.go
+++ b/cmd/kratos-admin/wire_gen.go
@@ -71,17 +71,29 @@ func wireApp(confServer *conf.Server, confData *conf.Data, adminBackend *conf.Ad
taskService := service.NewTaskService(taskUsecase, mediaUsecase, adminBackend)
taskScheduler := worker.NewTaskScheduler(taskService, logger)
task := handler.NewTask(taskService, taskScheduler)
- versionRepo := data.NewVersionRepo(dataData)
- versionUsecase := biz.NewVersionUsecase(versionRepo)
- versionService := service.NewVersionService(versionUsecase)
- exportRepo := data.NewExportRepo(dataData)
- exportUsecase := biz.NewExportUsecase(exportRepo)
- exportService := service.NewExportService(exportUsecase)
+ mediaService := service.NewMediaService(mediaUsecase)
+ media := handler.NewMedia(mediaService)
auditRepo := data.NewAuditRepo(dataData)
auditUsecase := biz.NewAuditUsecase(auditRepo)
auditService := service.NewAuditService(auditUsecase)
- mediaService := service.NewMediaService(mediaUsecase)
- ginServer := server.NewGinServer(confServer, adminBackend, systemService, accessService, authority, menu, api, permission, organization, announcement, email, task, settingsService, versionService, exportService, auditService, mediaService, emailService, taskScheduler, logger)
+ audit := handler.NewAudit(auditService)
+ exportRepo := data.NewExportRepo(dataData)
+ exportUsecase := biz.NewExportUsecase(exportRepo)
+ exportService := service.NewExportService(exportUsecase)
+ export := handler.NewExport(systemService, exportService)
+ versionRepo := data.NewVersionRepo(dataData)
+ versionUsecase := biz.NewVersionUsecase(versionRepo)
+ versionService := service.NewVersionService(versionUsecase)
+ version := handler.NewVersion(versionService)
+ dictionary := handler.NewDictionary(settingsService)
+ parameter := handler.NewParameter(settingsService)
+ apiToken := handler.NewAPIToken(settingsService)
+ systemConfig := handler.NewSystemConfig(systemService, settingsService, taskScheduler)
+ public := handler.NewPublic(adminBackend, 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)
return app, func() {
cleanup()
diff --git a/internal/biz/api_token.go b/internal/biz/api_token.go
new file mode 100644
index 0000000..504574c
--- /dev/null
+++ b/internal/biz/api_token.go
@@ -0,0 +1,28 @@
+package biz
+
+import (
+ "context"
+ "time"
+)
+
+type APIToken struct {
+ ID uint
+ CreatedAt time.Time
+ UpdatedAt time.Time
+ UserID uint
+ AuthorityID uint
+ Token string
+ Status bool
+ ExpiresAt time.Time
+ Remark string
+ User *User
+}
+
+type APITokenRepo interface {
+ UserHasAuthority(context.Context, uint, uint) (*User, bool, error)
+ CreateAPIToken(context.Context, *APIToken) error
+ ListAPITokens(context.Context, int, int, uint, *bool) ([]*APIToken, int64, error)
+ DisableAPIToken(context.Context, uint) (string, error)
+ BlacklistToken(context.Context, string) error
+ IsTokenDisabled(context.Context, string) (bool, error)
+}
diff --git a/internal/biz/dictionary.go b/internal/biz/dictionary.go
new file mode 100644
index 0000000..fcc32ae
--- /dev/null
+++ b/internal/biz/dictionary.go
@@ -0,0 +1,49 @@
+package biz
+
+import (
+ "context"
+ "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
+ UpdatedAt time.Time
+ Label string
+ Value string
+ Extend string
+ Status bool
+ Sort int
+ DictionaryID uint
+ ParentID *uint
+ Level int
+ Path string
+ 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)
+ 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)
+ DictionaryDetailTree(context.Context, uint, string) ([]*DictionaryDetail, error)
+}
diff --git a/internal/biz/media.go b/internal/biz/media.go
index 12d6213..74e1e58 100644
--- a/internal/biz/media.go
+++ b/internal/biz/media.go
@@ -5,13 +5,12 @@ import (
"crypto/md5"
"encoding/hex"
"errors"
- "fmt"
- "github.com/google/uuid"
"io"
"path/filepath"
- "sort"
"strings"
"time"
+
+ "github.com/google/uuid"
)
type MediaFile struct {
@@ -33,21 +32,7 @@ type AttachmentCategory struct {
ParentID uint
Children []*AttachmentCategory
}
-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 MediaRepo interface {
+type MediaMetadataRepo interface {
CreateMedia(context.Context, *MediaFile) error
FindMedia(context.Context, uint) (*MediaFile, error)
FindMediaByHash(context.Context, uint, string) (*MediaFile, error)
@@ -59,17 +44,13 @@ type MediaRepo interface {
SaveCategory(context.Context, *AttachmentCategory) error
DeleteCategory(context.Context, uint) error
ListCategories(context.Context) ([]*AttachmentCategory, error)
- 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
}
+
+type MediaRepo interface {
+ MediaMetadataRepo
+ UploadRepo
+}
+
type MediaUsecase struct {
repo MediaRepo
files FileStorage
@@ -127,124 +108,6 @@ func (uc *MediaUsecase) Delete(ctx context.Context, id uint) error {
}
return uc.repo.DeleteMedia(ctx, id)
}
-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
- }
- if size <= 0 || chunkSize <= 0 || total <= 0 {
- return nil, nil, nil, errors.New("上传参数不合法")
- }
- if media, err := uc.repo.FindMediaByHash(ctx, userID, hash); err == nil {
- copy := *media
- copy.ID = 0
- copy.Name = name
- if err = uc.repo.CreateMedia(ctx, ©); err == nil {
- return nil, ©, nil, nil
- }
- }
- session, err := uc.repo.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 {
- return nil, nil, nil, err
- }
- }
- chunks, err := uc.repo.ListChunks(ctx, session.ID)
- if err != nil {
- return nil, nil, nil, err
- }
- indices := make([]int, 0, len(chunks))
- for _, v := range chunks {
- indices = append(indices, v.Index)
- }
- sort.Ints(indices)
- 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)
- if err != nil || session.UserID != userID {
- return errors.New("上传会话不存在或无权操作")
- }
- if session.Status != "uploading" || index < 0 || index >= session.ChunkTotal {
- return errors.New("上传会话状态或分片序号不合法")
- }
- hash := md5.New()
- key := fmt.Sprintf(".chunks/%d/%08d", uploadID, index)
- stored, err := uc.files.Put(ctx, key, io.TeeReader(reader, hash))
- if err != nil {
- return err
- }
- actual := hex.EncodeToString(hash.Sum(nil))
- if !strings.EqualFold(actual, expected) {
- _ = uc.files.Delete(ctx, key)
- return errors.New("分片校验失败")
- }
- return uc.repo.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)
- if err != nil || session.UserID != userID {
- return nil, errors.New("上传会话不存在或无权操作")
- }
- chunks, err := uc.repo.ListChunks(ctx, uploadID)
- if err != nil || len(chunks) != session.ChunkTotal {
- return nil, errors.New("分片不完整")
- }
- sort.Slice(chunks, func(i, j int) bool { return chunks[i].Index < chunks[j].Index })
- names := make([]string, 0, len(chunks))
- for index, chunk := range chunks {
- if chunk.Index != index {
- return nil, errors.New("分片序号不连续")
- }
- names = append(names, fmt.Sprintf(".chunks/%d/%08d", uploadID, index))
- }
- ext := strings.ToLower(filepath.Ext(session.FileName))
- key := time.Now().Format("20060102") + "/" + uuid.NewString() + ext
- stored, hash, err := uc.files.Compose(ctx, names, key)
- if err != nil {
- return nil, err
- }
- if !strings.EqualFold(hash, session.FileHash) {
- _ = uc.files.Delete(ctx, key)
- 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 {
- _ = uc.files.Delete(ctx, key)
- return nil, err
- }
- _ = uc.repo.CompleteUploadSession(ctx, uploadID, key, media.ID)
- _ = uc.repo.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)
- if err != nil || session.UserID != userID {
- return errors.New("上传会话不存在或无权操作")
- }
- _ = uc.repo.DeleteChunks(ctx, uploadID)
- _ = uc.files.DeletePrefix(ctx, fmt.Sprintf(".chunks/%d", uploadID))
- return uc.repo.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))
- if err != nil {
- return err
- }
- for _, id := range ids {
- if err = uc.repo.DeleteUploadData(ctx, id); err != nil {
- return err
- }
- if err = uc.files.DeletePrefix(ctx, fmt.Sprintf(".chunks/%d", id)); err != nil {
- return err
- }
- }
- return nil
-}
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_upload.go b/internal/biz/media_upload.go
new file mode 100644
index 0000000..9dec91f
--- /dev/null
+++ b/internal/biz/media_upload.go
@@ -0,0 +1,162 @@
+package biz
+
+import (
+ "context"
+ "crypto/md5"
+ "encoding/hex"
+ "errors"
+ "fmt"
+ "io"
+ "path/filepath"
+ "sort"
+ "strings"
+ "time"
+
+ "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
+ }
+ if size <= 0 || chunkSize <= 0 || total <= 0 {
+ return nil, nil, nil, errors.New("上传参数不合法")
+ }
+ if media, err := uc.repo.FindMediaByHash(ctx, userID, hash); err == nil {
+ copy := *media
+ copy.ID = 0
+ copy.Name = name
+ if err = uc.repo.CreateMedia(ctx, ©); err == nil {
+ return nil, ©, nil, nil
+ }
+ }
+ session, err := uc.repo.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 {
+ return nil, nil, nil, err
+ }
+ }
+ chunks, err := uc.repo.ListChunks(ctx, session.ID)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+ indices := make([]int, 0, len(chunks))
+ for _, v := range chunks {
+ indices = append(indices, v.Index)
+ }
+ sort.Ints(indices)
+ 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)
+ if err != nil || session.UserID != userID {
+ return errors.New("上传会话不存在或无权操作")
+ }
+ if session.Status != "uploading" || index < 0 || index >= session.ChunkTotal {
+ return errors.New("上传会话状态或分片序号不合法")
+ }
+ hash := md5.New()
+ key := fmt.Sprintf(".chunks/%d/%08d", uploadID, index)
+ stored, err := uc.files.Put(ctx, key, io.TeeReader(reader, hash))
+ if err != nil {
+ return err
+ }
+ actual := hex.EncodeToString(hash.Sum(nil))
+ if !strings.EqualFold(actual, expected) {
+ _ = uc.files.Delete(ctx, key)
+ return errors.New("分片校验失败")
+ }
+ return uc.repo.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)
+ if err != nil || session.UserID != userID {
+ return nil, errors.New("上传会话不存在或无权操作")
+ }
+ chunks, err := uc.repo.ListChunks(ctx, uploadID)
+ if err != nil || len(chunks) != session.ChunkTotal {
+ return nil, errors.New("分片不完整")
+ }
+ sort.Slice(chunks, func(i, j int) bool { return chunks[i].Index < chunks[j].Index })
+ names := make([]string, 0, len(chunks))
+ for index, chunk := range chunks {
+ if chunk.Index != index {
+ return nil, errors.New("分片序号不连续")
+ }
+ names = append(names, fmt.Sprintf(".chunks/%d/%08d", uploadID, index))
+ }
+ ext := strings.ToLower(filepath.Ext(session.FileName))
+ key := time.Now().Format("20060102") + "/" + uuid.NewString() + ext
+ stored, hash, err := uc.files.Compose(ctx, names, key)
+ if err != nil {
+ return nil, err
+ }
+ if !strings.EqualFold(hash, session.FileHash) {
+ _ = uc.files.Delete(ctx, key)
+ 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 {
+ _ = uc.files.Delete(ctx, key)
+ return nil, err
+ }
+ _ = uc.repo.CompleteUploadSession(ctx, uploadID, key, media.ID)
+ _ = uc.repo.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)
+ if err != nil || session.UserID != userID {
+ return errors.New("上传会话不存在或无权操作")
+ }
+ _ = uc.repo.DeleteChunks(ctx, uploadID)
+ _ = uc.files.DeletePrefix(ctx, fmt.Sprintf(".chunks/%d", uploadID))
+ return uc.repo.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))
+ if err != nil {
+ return err
+ }
+ for _, id := range ids {
+ if err = uc.repo.DeleteUploadData(ctx, id); err != nil {
+ return err
+ }
+ if err = uc.files.DeletePrefix(ctx, fmt.Sprintf(".chunks/%d", id)); err != nil {
+ return err
+ }
+ }
+ return nil
+}
diff --git a/internal/biz/parameter.go b/internal/biz/parameter.go
new file mode 100644
index 0000000..7bfa0e9
--- /dev/null
+++ b/internal/biz/parameter.go
@@ -0,0 +1,24 @@
+package biz
+
+import (
+ "context"
+ "time"
+)
+
+type SystemParameter struct {
+ ID uint
+ CreatedAt time.Time
+ UpdatedAt time.Time
+ Name string
+ Key string
+ Value string
+ Desc string
+}
+
+type ParameterRepo interface {
+ CreateParameter(context.Context, *SystemParameter) error
+ UpdateParameter(context.Context, *SystemParameter) error
+ DeleteParameters(context.Context, []uint) error
+ FindParameter(context.Context, uint, string) (*SystemParameter, error)
+ ListParameters(context.Context, int, int, *SystemParameter) ([]*SystemParameter, int64, error)
+}
diff --git a/internal/biz/security.go b/internal/biz/security.go
new file mode 100644
index 0000000..611ffeb
--- /dev/null
+++ b/internal/biz/security.go
@@ -0,0 +1,31 @@
+package biz
+
+import "context"
+
+type SecurityConfig struct {
+ ID uint
+ CaptchaOpen int
+ CaptchaTimeout int
+ KeyLong int
+ ImgWidth int
+ ImgHeight int
+ PwdMinLength int
+ PwdRequireUpper bool
+ PwdRequireLower bool
+ PwdRequireDigit bool
+ PwdRequireSpecial bool
+ LimitEnable bool
+ LimitWindow int
+ LimitCount int
+ LockEnable bool
+ LockThreshold int
+ LockDuration int
+ PwdExpireEnable bool
+ PwdExpireDays int
+ ForceNewUserChangePassword bool
+}
+
+type SecurityRepo interface {
+ SecurityConfig(context.Context) (*SecurityConfig, error)
+ SaveSecurityConfig(context.Context, *SecurityConfig) error
+}
diff --git a/internal/biz/settings.go b/internal/biz/settings.go
index e7dcfc3..a387d7d 100644
--- a/internal/biz/settings.go
+++ b/internal/biz/settings.go
@@ -1,112 +1,10 @@
package biz
-import (
- "context"
- "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
- UpdatedAt time.Time
- Label string
- Value string
- Extend string
- Status bool
- Sort int
- DictionaryID uint
- ParentID *uint
- Level int
- Path string
- Children []*DictionaryDetail
-}
-
-type SystemParameter struct {
- ID uint
- CreatedAt time.Time
- UpdatedAt time.Time
- Name string
- Key string
- Value string
- Desc string
-}
-
-type APIToken struct {
- ID uint
- CreatedAt time.Time
- UpdatedAt time.Time
- UserID uint
- AuthorityID uint
- Token string
- Status bool
- ExpiresAt time.Time
- Remark string
- User *User
-}
-
-type SecurityConfig struct {
- ID uint
- CaptchaOpen int
- CaptchaTimeout int
- KeyLong int
- ImgWidth int
- ImgHeight int
- PwdMinLength int
- PwdRequireUpper bool
- PwdRequireLower bool
- PwdRequireDigit bool
- PwdRequireSpecial bool
- LimitEnable bool
- LimitWindow int
- LimitCount int
- LockEnable bool
- LockThreshold int
- LockDuration int
- PwdExpireEnable bool
- PwdExpireDays int
- ForceNewUserChangePassword bool
-}
-
type SettingsRepo 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)
- 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)
- DictionaryDetailTree(context.Context, uint, string) ([]*DictionaryDetail, error)
-
- CreateParameter(context.Context, *SystemParameter) error
- UpdateParameter(context.Context, *SystemParameter) error
- DeleteParameters(context.Context, []uint) error
- FindParameter(context.Context, uint, string) (*SystemParameter, error)
- ListParameters(context.Context, int, int, *SystemParameter) ([]*SystemParameter, int64, error)
-
- UserHasAuthority(context.Context, uint, uint) (*User, bool, error)
- CreateAPIToken(context.Context, *APIToken) error
- ListAPITokens(context.Context, int, int, uint, *bool) ([]*APIToken, int64, error)
- DisableAPIToken(context.Context, uint) (string, error)
- BlacklistToken(context.Context, string) error
- IsTokenDisabled(context.Context, string) (bool, error)
- SecurityConfig(context.Context) (*SecurityConfig, error)
- SaveSecurityConfig(context.Context, *SecurityConfig) error
+ DictionaryRepo
+ ParameterRepo
+ APITokenRepo
+ SecurityRepo
}
type SettingsUsecase struct{ repo SettingsRepo }
diff --git a/internal/biz/system.go b/internal/biz/system.go
index c02ad12..589ff7c 100644
--- a/internal/biz/system.go
+++ b/internal/biz/system.go
@@ -3,69 +3,11 @@ package biz
import (
"context"
"time"
-
- "golang.org/x/crypto/bcrypt"
)
-type User struct {
- ID uint
- CreatedAt time.Time
- UpdatedAt time.Time
- UUID string
- Username string
- Password string
- NickName string
- HeaderImg string
- AuthorityID uint
- Authority Authority
- Authorities []Authority
- DeptID uint
- Department *Department
- Departments []Department
- Positions []Position
- Phone string
- Email string
- Enable int
- OriginSetting map[string]any
- MustChangePassword bool
- PasswordUpdatedAt *time.Time
-}
-type UserListFilter struct {
- Username, NickName, Phone, Email, OrderKey string
- Desc bool
-}
-
-type DatabaseConfig struct {
- Driver, Host, Port, User, Password, Name, Path, Config, Template, AdminPassword string
- APIs []*API
-}
-
-type Authority struct {
- AuthorityID uint
- AuthorityName string
- ParentID *uint
- DataScope int
- DefaultRouter string
-}
-
type SystemRepo interface {
- IsInitialized(context.Context) (bool, error)
- Initialize(context.Context, *DatabaseConfig) error
- FindUserByUsername(context.Context, string) (*User, error)
- FindUserByID(context.Context, uint) (*User, error)
- MenusByAuthority(context.Context, uint) ([]*Menu, error)
- ListUsers(context.Context, int, int, *UserListFilter) ([]*User, int64, error)
- CreateUser(context.Context, *User) (*User, error)
- UpdateUser(context.Context, *User) 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)
- PersistConfig(context.Context) error
- PersistAdminConfig(context.Context, []byte) error
- ReloadConfig(context.Context) error
+ InitializationRepo
+ UserRepo
}
type SystemUsecase struct {
@@ -90,88 +32,3 @@ func (uc *SystemUsecase) CacheDelete(ctx context.Context, key string) error {
func (uc *SystemUsecase) CacheIncrement(ctx context.Context, key string, expiration time.Duration) (int64, error) {
return uc.cache.Increment(ctx, key, expiration)
}
-
-func (uc *SystemUsecase) IsInitialized(ctx context.Context) (bool, error) {
- return uc.repo.IsInitialized(ctx)
-}
-func (uc *SystemUsecase) Initialize(ctx context.Context, config *DatabaseConfig) error {
- if config == nil || len(config.AdminPassword) < 6 {
- return ErrInvalidCredentials
- }
- return uc.repo.Initialize(ctx, config)
-}
-func (uc *SystemUsecase) PersistConfig(ctx context.Context) error { return uc.repo.PersistConfig(ctx) }
-func (uc *SystemUsecase) PersistAdminConfig(ctx context.Context, value []byte) error {
- return uc.repo.PersistAdminConfig(ctx, value)
-}
-func (uc *SystemUsecase) ReloadConfig(ctx context.Context) error { return uc.repo.ReloadConfig(ctx) }
-
-func (uc *SystemUsecase) Login(ctx context.Context, username, password string) (*User, error) {
- u, err := uc.repo.FindUserByUsername(ctx, username)
- if err != nil || bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password)) != nil {
- return nil, ErrInvalidCredentials
- }
- return u, nil
-}
-
-func (uc *SystemUsecase) User(ctx context.Context, id uint) (*User, error) {
- return uc.repo.FindUserByID(ctx, id)
-}
-
-func (uc *SystemUsecase) Menus(ctx context.Context, authorityID uint) ([]*Menu, error) {
- return uc.repo.MenusByAuthority(ctx, authorityID)
-}
-
-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) {
- 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)
-}
-func (uc *SystemUsecase) UpdateUser(ctx context.Context, user *User) error {
- return uc.repo.UpdateUser(ctx, user)
-}
-func (uc *SystemUsecase) DeleteUser(ctx context.Context, id uint) error {
- return uc.repo.DeleteUser(ctx, id)
-}
-func (uc *SystemUsecase) ResetPassword(ctx context.Context, id uint, password string) error {
- hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
- if err != nil {
- return err
- }
- return uc.repo.UpdatePassword(ctx, id, string(hash), false)
-}
-func (uc *SystemUsecase) ChangePassword(ctx context.Context, id uint, oldPassword, newPassword string) error {
- user, err := uc.repo.FindUserByID(ctx, id)
- if err != nil {
- return err
- }
- if bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(oldPassword)) != nil {
- return ErrInvalidCredentials
- }
- hash, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
- if err != nil {
- return err
- }
- return uc.repo.UpdatePassword(ctx, id, string(hash), true)
-}
-func (uc *SystemUsecase) Authorities(ctx context.Context) ([]*Authority, error) {
- return uc.repo.ListAuthorities(ctx)
-}
-func (uc *SystemUsecase) SetUserAuthorities(ctx context.Context, id uint, authorityIDs []uint) error {
- if len(authorityIDs) == 0 {
- return ErrInvalidCredentials
- }
- return uc.repo.SetUserAuthorities(ctx, id, authorityIDs)
-}
-func (uc *SystemUsecase) SetUserAuthority(ctx context.Context, id, authorityID uint) error {
- return uc.repo.SetUserAuthority(ctx, id, authorityID)
-}
-func (uc *SystemUsecase) SetUserSetting(ctx context.Context, id uint, setting map[string]any) error {
- return uc.repo.SetUserSetting(ctx, id, setting)
-}
diff --git a/internal/biz/system_init.go b/internal/biz/system_init.go
new file mode 100644
index 0000000..ad85687
--- /dev/null
+++ b/internal/biz/system_init.go
@@ -0,0 +1,31 @@
+package biz
+
+import "context"
+
+type DatabaseConfig struct {
+ Driver, Host, Port, User, Password, Name, Path, Config, Template, AdminPassword string
+ APIs []*API
+}
+
+type InitializationRepo interface {
+ IsInitialized(context.Context) (bool, error)
+ Initialize(context.Context, *DatabaseConfig) error
+ PersistConfig(context.Context) error
+ PersistAdminConfig(context.Context, []byte) error
+ ReloadConfig(context.Context) error
+}
+
+func (uc *SystemUsecase) IsInitialized(ctx context.Context) (bool, error) {
+ return uc.repo.IsInitialized(ctx)
+}
+func (uc *SystemUsecase) Initialize(ctx context.Context, config *DatabaseConfig) error {
+ if config == nil || len(config.AdminPassword) < 6 {
+ return ErrInvalidCredentials
+ }
+ return uc.repo.Initialize(ctx, config)
+}
+func (uc *SystemUsecase) PersistConfig(ctx context.Context) error { return uc.repo.PersistConfig(ctx) }
+func (uc *SystemUsecase) PersistAdminConfig(ctx context.Context, value []byte) error {
+ return uc.repo.PersistAdminConfig(ctx, value)
+}
+func (uc *SystemUsecase) ReloadConfig(ctx context.Context) error { return uc.repo.ReloadConfig(ctx) }
diff --git a/internal/biz/user.go b/internal/biz/user.go
new file mode 100644
index 0000000..d2f15d5
--- /dev/null
+++ b/internal/biz/user.go
@@ -0,0 +1,129 @@
+package biz
+
+import (
+ "context"
+ "time"
+
+ "golang.org/x/crypto/bcrypt"
+)
+
+type User struct {
+ ID uint
+ CreatedAt time.Time
+ UpdatedAt time.Time
+ UUID string
+ Username string
+ Password string
+ NickName string
+ HeaderImg string
+ AuthorityID uint
+ Authority Authority
+ Authorities []Authority
+ DeptID uint
+ Department *Department
+ Departments []Department
+ Positions []Position
+ Phone string
+ Email string
+ Enable int
+ OriginSetting map[string]any
+ MustChangePassword bool
+ PasswordUpdatedAt *time.Time
+}
+type UserListFilter struct {
+ Username, NickName, Phone, Email, OrderKey string
+ Desc bool
+}
+
+type Authority struct {
+ AuthorityID uint
+ AuthorityName string
+ ParentID *uint
+ DataScope int
+ DefaultRouter string
+}
+
+type UserRepo interface {
+ FindUserByUsername(context.Context, string) (*User, error)
+ FindUserByID(context.Context, uint) (*User, error)
+ MenusByAuthority(context.Context, uint) ([]*Menu, error)
+ ListUsers(context.Context, int, int, *UserListFilter) ([]*User, int64, error)
+ CreateUser(context.Context, *User) (*User, error)
+ UpdateUser(context.Context, *User) 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)
+}
+
+func (uc *SystemUsecase) Login(ctx context.Context, username, password string) (*User, error) {
+ u, err := uc.repo.FindUserByUsername(ctx, username)
+ if err != nil || bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password)) != nil {
+ return nil, ErrInvalidCredentials
+ }
+ return u, nil
+}
+
+func (uc *SystemUsecase) User(ctx context.Context, id uint) (*User, error) {
+ return uc.repo.FindUserByID(ctx, id)
+}
+
+func (uc *SystemUsecase) Menus(ctx context.Context, authorityID uint) ([]*Menu, error) {
+ return uc.repo.MenusByAuthority(ctx, authorityID)
+}
+
+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) {
+ 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)
+}
+func (uc *SystemUsecase) UpdateUser(ctx context.Context, user *User) error {
+ return uc.repo.UpdateUser(ctx, user)
+}
+func (uc *SystemUsecase) DeleteUser(ctx context.Context, id uint) error {
+ return uc.repo.DeleteUser(ctx, id)
+}
+func (uc *SystemUsecase) ResetPassword(ctx context.Context, id uint, password string) error {
+ hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
+ if err != nil {
+ return err
+ }
+ return uc.repo.UpdatePassword(ctx, id, string(hash), false)
+}
+func (uc *SystemUsecase) ChangePassword(ctx context.Context, id uint, oldPassword, newPassword string) error {
+ user, err := uc.repo.FindUserByID(ctx, id)
+ if err != nil {
+ return err
+ }
+ if bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(oldPassword)) != nil {
+ return ErrInvalidCredentials
+ }
+ hash, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
+ if err != nil {
+ return err
+ }
+ return uc.repo.UpdatePassword(ctx, id, string(hash), true)
+}
+func (uc *SystemUsecase) Authorities(ctx context.Context) ([]*Authority, error) {
+ return uc.repo.ListAuthorities(ctx)
+}
+func (uc *SystemUsecase) SetUserAuthorities(ctx context.Context, id uint, authorityIDs []uint) error {
+ if len(authorityIDs) == 0 {
+ return ErrInvalidCredentials
+ }
+ return uc.repo.SetUserAuthorities(ctx, id, authorityIDs)
+}
+func (uc *SystemUsecase) SetUserAuthority(ctx context.Context, id, authorityID uint) error {
+ return uc.repo.SetUserAuthority(ctx, id, authorityID)
+}
+func (uc *SystemUsecase) SetUserSetting(ctx context.Context, id uint, setting map[string]any) error {
+ return uc.repo.SetUserSetting(ctx, id, setting)
+}
diff --git a/internal/data/access.go b/internal/data/access.go
index f77a8d1..262594c 100644
--- a/internal/data/access.go
+++ b/internal/data/access.go
@@ -1,900 +1,7 @@
package data
-import (
- "context"
- "errors"
- "strconv"
- "strings"
- "time"
-
- "kra/internal/biz"
-
- "github.com/casbin/casbin/v3"
- casbinmodel "github.com/casbin/casbin/v3/model"
- "gorm.io/gorm"
-)
-
-type apiPO struct {
- ID uint `gorm:"primaryKey"`
- CreatedAt time.Time
- UpdatedAt time.Time
- DeletedAt gorm.DeletedAt `gorm:"index"`
- Path string `gorm:"uniqueIndex:idx_api_path_method"`
- Description string
- APIGroup string `gorm:"column:api_group"`
- Method string `gorm:"uniqueIndex:idx_api_path_method"`
-}
-
-func (apiPO) TableName() string { return "sys_apis" }
-
-type ignoredAPIPO struct {
- Path string `gorm:"primaryKey"`
- Method string `gorm:"primaryKey"`
-}
-
-func (ignoredAPIPO) TableName() string { return "sys_ignore_apis" }
-
-type authorityAPIPO struct {
- AuthorityID uint `gorm:"primaryKey;column:authority_id"`
- APIID uint `gorm:"primaryKey;column:api_id"`
-}
-
-func (authorityAPIPO) TableName() string { return "sys_authority_apis" }
-
-type menuButtonPO struct {
- ID uint `gorm:"primaryKey"`
- Name string
- Description string `gorm:"column:desc"`
- MenuID uint `gorm:"column:sys_base_menu_id"`
-}
-
-func (menuButtonPO) TableName() string { return "sys_base_menu_btns" }
-
-type authorityButtonPO struct {
- AuthorityID uint `gorm:"primaryKey;column:authority_id"`
- MenuID uint `gorm:"primaryKey;column:sys_menu_id"`
- ButtonID uint `gorm:"primaryKey;column:sys_base_menu_btn_id"`
-}
-
-func (authorityButtonPO) TableName() string { return "sys_authority_btns" }
-
-type departmentPO struct {
- ID uint `gorm:"primaryKey"`
- CreatedAt time.Time
- UpdatedAt time.Time
- DeletedAt gorm.DeletedAt `gorm:"index"`
- Name string `gorm:"index"`
- ParentID uint
- Ancestors string
- Sort int
- LeaderID uint
- Status bool `gorm:"default:true"`
-}
-
-func (departmentPO) TableName() string { return "sys_departments" }
-
-type positionPO struct {
- ID uint `gorm:"primaryKey"`
- CreatedAt time.Time
- UpdatedAt time.Time
- DeletedAt gorm.DeletedAt `gorm:"index"`
- Name string `gorm:"index"`
- Code string
- Sort int
- Status bool `gorm:"default:true"`
- Remark string
-}
-
-func (positionPO) TableName() string { return "sys_positions" }
-
-type userDepartmentPO struct {
- UserID uint `gorm:"primaryKey;column:sys_user_id"`
- DepartmentID uint `gorm:"primaryKey;column:sys_department_id"`
-}
-
-func (userDepartmentPO) TableName() string { return "sys_user_departments" }
-
-type userPositionPO struct {
- UserID uint `gorm:"primaryKey;column:sys_user_id"`
- PositionID uint `gorm:"primaryKey;column:sys_position_id"`
-}
-
-func (userPositionPO) TableName() string { return "sys_user_positions" }
-
-type authorityDepartmentPO struct {
- AuthorityID uint `gorm:"primaryKey;column:authority_id"`
- DepartmentID uint `gorm:"primaryKey;column:department_id"`
-}
-
-func (authorityDepartmentPO) TableName() string { return "sys_authority_departments" }
+import "kra/internal/biz"
type accessRepo struct{ data *Data }
-type organizationRepo struct{ data *Data }
-func NewAccessRepo(data *Data) biz.AccessRepo { return &accessRepo{data: data} }
-func NewOrganizationRepo(data *Data) biz.OrganizationRepo { return &organizationRepo{data: data} }
-
-func (r *accessRepo) CreateAuthority(ctx context.Context, value *biz.Authority) error {
- 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 {
- return err
- }
- if count > 0 {
- return errors.New("存在相同角色id")
- }
- if value.DefaultRouter == "" {
- value.DefaultRouter = "dashboard"
- }
- if err := tx.Create(&authorityPO{AuthorityID: value.AuthorityID, AuthorityName: value.AuthorityName, ParentID: value.ParentID, DataScope: value.DataScope, DefaultRouter: value.DefaultRouter}).Error; err != nil {
- return err
- }
- var dashboard menuPO
- if err := tx.Where("name = ?", "dashboard").First(&dashboard).Error; err == nil {
- if err = tx.Create(&authorityMenuPO{SysAuthorityAuthorityID: value.AuthorityID, SysBaseMenuID: dashboard.ID}).Error; err != nil {
- return err
- }
- }
- defaults := []struct{ path, method string }{{"/menu/getMenu", "POST"}, {"/jwt/jsonInBlacklist", "POST"}, {"/user/changePassword", "POST"}, {"/user/setUserAuthority", "POST"}, {"/user/getUserInfo", "GET"}, {"/user/setSelfInfo", "PUT"}, {"/fileUploadAndDownload/upload", "POST"}, {"/sysDictionary/findSysDictionary", "GET"}}
- for _, item := range defaults {
- var api apiPO
- if err := tx.Where("path = ? AND method = ?", item.path, item.method).First(&api).Error; err == nil {
- if err = tx.Create(&authorityAPIPO{AuthorityID: value.AuthorityID, APIID: api.ID}).Error; err != nil {
- return err
- }
- }
- }
- return nil
- })
-}
-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 {
- var count int64
- if err := tx.Model(&authorityPO{}).Where("authority_id = ?", value.AuthorityID).Count(&count).Error; err != nil {
- return err
- }
- if count != 0 {
- return errors.New("存在相同角色id")
- }
- if err := tx.Create(&authorityPO{AuthorityID: value.AuthorityID, AuthorityName: value.AuthorityName, ParentID: value.ParentID, DataScope: value.DataScope, DefaultRouter: value.DefaultRouter}).Error; err != nil {
- return err
- }
- copyLinks := func(table string, destination any, columns map[string]any) error {
- return tx.Table(table).Where(columns).Find(destination).Error
- }
- var menus []authorityMenuPO
- if err := copyLinks("sys_authority_menus", &menus, map[string]any{"sys_authority_authority_id": sourceID}); err != nil {
- return err
- }
- for i := range menus {
- menus[i].SysAuthorityAuthorityID = value.AuthorityID
- }
- if len(menus) > 0 {
- if err := tx.Create(&menus).Error; err != nil {
- return err
- }
- }
- var apis []authorityAPIPO
- if err := copyLinks("sys_authority_apis", &apis, map[string]any{"authority_id": sourceID}); err != nil {
- return err
- }
- for i := range apis {
- apis[i].AuthorityID = value.AuthorityID
- }
- if len(apis) > 0 {
- if err := tx.Create(&apis).Error; err != nil {
- return err
- }
- }
- var buttons []authorityButtonPO
- if err := copyLinks("sys_authority_btns", &buttons, map[string]any{"authority_id": sourceID}); err != nil {
- return err
- }
- for i := range buttons {
- buttons[i].AuthorityID = value.AuthorityID
- }
- if len(buttons) > 0 {
- if err := tx.Create(&buttons).Error; err != nil {
- 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
- })
-}
-func (r *accessRepo) UpdateAuthority(ctx context.Context, value *biz.Authority) error {
- return r.data.gormDB.WithContext(ctx).Model(&authorityPO{}).Where("authority_id = ?", value.AuthorityID).Updates(map[string]any{"authority_name": value.AuthorityName, "parent_id": value.ParentID, "data_scope": value.DataScope, "default_router": value.DefaultRouter}).Error
-}
-func (r *accessRepo) DeleteAuthority(ctx context.Context, id uint) error {
- if id == 888 {
- return errors.New("不能删除超级管理员")
- }
- return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
- var users, children int64
- if err := tx.Model(&userAuthorityPO{}).Where("sys_authority_authority_id = ?", id).Count(&users).Error; err != nil {
- return err
- }
- if users > 0 {
- return errors.New("此角色有用户正在使用禁止删除")
- }
- if err := tx.Model(&userPO{}).Where("authority_id = ?", id).Count(&users).Error; err != nil {
- return err
- }
- if users > 0 {
- return errors.New("此角色有用户正在使用禁止删除")
- }
- if err := tx.Model(&authorityPO{}).Where("parent_id = ?", id).Count(&children).Error; err != nil {
- return err
- }
- if children > 0 {
- return errors.New("此角色存在子角色不允许删除")
- }
- if err := tx.Where("sys_authority_authority_id = ?", id).Delete(&authorityMenuPO{}).Error; err != nil {
- return err
- }
- if err := tx.Where("authority_id = ?", id).Delete(&authorityAPIPO{}).Error; err != nil {
- return err
- }
- if err := tx.Where("authority_id = ?", id).Delete(&authorityButtonPO{}).Error; err != nil {
- return err
- }
- if err := tx.Where("authority_id = ?", id).Delete(&authorityDepartmentPO{}).Error; err != nil {
- return err
- }
- return tx.Delete(&authorityPO{}, "authority_id = ?", id).Error
- })
-}
-func (r *accessRepo) ListAuthorities(ctx context.Context) ([]*biz.Authority, error) {
- var pos []authorityPO
- if err := r.data.gormDB.WithContext(ctx).Order("authority_id").Find(&pos).Error; err != nil {
- return nil, err
- }
- out := make([]*biz.Authority, 0, len(pos))
- for _, po := range pos {
- v := toBizAuthority(po)
- out = append(out, &v)
- }
- return out, nil
-}
-func (r *accessRepo) SetAuthorityUsers(ctx context.Context, id uint, ids []uint) error {
- return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
- var oldIDs []uint
- if err := tx.Model(&userAuthorityPO{}).Where("sys_authority_authority_id = ?", id).Pluck("sys_user_id", &oldIDs).Error; err != nil {
- return err
- }
- if err := tx.Where("sys_authority_authority_id = ?", id).Delete(&userAuthorityPO{}).Error; err != nil {
- return err
- }
- links := make([]userAuthorityPO, 0, len(ids))
- for _, uid := range ids {
- links = append(links, userAuthorityPO{SysUserID: uid, SysAuthorityAuthorityID: id})
- }
- if len(links) > 0 {
- if err := tx.Create(&links).Error; err != nil {
- return err
- }
- }
- selected := map[uint]bool{}
- for _, uid := range ids {
- selected[uid] = true
- }
- for _, uid := range oldIDs {
- if selected[uid] {
- continue
- }
- 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 {
- return err
- }
- fallback := userAuthorityPO{SysUserID: uid, SysAuthorityAuthorityID: 888}
- if err = tx.FirstOrCreate(&fallback, fallback).Error; err != nil {
- return err
- }
- }
- }
- return nil
- })
-}
-func (r *accessRepo) AuthorityUserIDs(ctx context.Context, id uint) ([]uint, error) {
- var ids []uint
- err := r.data.gormDB.WithContext(ctx).Model(&userAuthorityPO{}).Where("sys_authority_authority_id = ?", id).Pluck("sys_user_id", &ids).Error
- return ids, err
-}
-func (r *accessRepo) SetDataScope(ctx context.Context, id uint, scope int, deptIDs []uint) error {
- return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
- if err := tx.Model(&authorityPO{}).Where("authority_id = ?", id).Update("data_scope", scope).Error; err != nil {
- return err
- }
- 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
- }
- return nil
- })
-}
-func (r *accessRepo) DataScopeDepartmentIDs(ctx context.Context, id uint) ([]uint, error) {
- var ids []uint
- err := r.data.gormDB.WithContext(ctx).Model(&authorityDepartmentPO{}).Where("authority_id = ?", id).Pluck("department_id", &ids).Error
- return ids, err
-}
-func (r *accessRepo) ResolveDataScope(ctx context.Context, authorityID, userID uint) (biz.DataScope, error) {
- if authorityID == 888 {
- return biz.DataScope{All: true}, nil
- }
- var authority authorityPO
- if err := r.data.gormDB.WithContext(ctx).First(&authority, "authority_id = ?", authorityID).Error; err != nil {
- return biz.DataScope{}, err
- }
- if authority.DataScope == 1 {
- return biz.DataScope{All: true}, nil
- }
- if authority.DataScope == 4 {
- return biz.DataScope{OwnerUserID: userID}, nil
- }
- var ids []uint
- if authority.DataScope == 5 {
- var err error
- ids, err = r.DataScopeDepartmentIDs(ctx, authorityID)
- if err != nil {
- return biz.DataScope{}, err
- }
- } else {
- if err := r.data.gormDB.WithContext(ctx).Model(&userDepartmentPO{}).Where("sys_user_id = ?", userID).Pluck("sys_department_id", &ids).Error; err != nil {
- return biz.DataScope{}, err
- }
- if authority.DataScope == 2 && len(ids) > 0 {
- var departments []departmentPO
- if err := r.data.gormDB.WithContext(ctx).Find(&departments).Error; err != nil {
- return biz.DataScope{}, err
- }
- selected := make(map[uint]bool, len(ids))
- for _, id := range ids {
- selected[id] = true
- }
- for _, department := range departments {
- for _, part := range strings.Split(department.Ancestors, ",") {
- value, _ := strconv.ParseUint(part, 10, 64)
- if selected[uint(value)] {
- selected[department.ID] = true
- break
- }
- }
- }
- ids = ids[:0]
- for id := range selected {
- ids = append(ids, id)
- }
- }
- }
- return biz.DataScope{DepartmentIDs: ids}, nil
-}
-
-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
-}
-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
-}
-func (r *accessRepo) DeleteAPIs(ctx context.Context, ids []uint) error {
- return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
- if err := tx.Where("api_id IN ?", ids).Delete(&authorityAPIPO{}).Error; err != nil {
- return err
- }
- return tx.Delete(&apiPO{}, ids).Error
- })
-}
-func (r *accessRepo) FindAPI(ctx context.Context, id uint) (*biz.API, error) {
- var po apiPO
- if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil {
- return nil, err
- }
- 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 {
- if q.Path != "" {
- db = db.Where("path LIKE ?", "%"+q.Path+"%")
- }
- if q.APIGroup != "" {
- db = db.Where("api_group = ?", q.APIGroup)
- }
- if q.Method != "" {
- db = db.Where("method = ?", strings.ToUpper(q.Method))
- }
- }
- var total int64
- if err := db.Count(&total).Error; err != nil {
- return nil, 0, err
- }
- var pos []apiPO
- if err := db.Order("api_group,path,method").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
- return nil, 0, err
- }
- out := make([]*biz.API, 0, len(pos))
- for _, po := range pos {
- out = append(out, apiFromPO(po))
- }
- return out, total, nil
-}
-func (r *accessRepo) APIRoleIDs(ctx context.Context, id uint) ([]uint, error) {
- var ids []uint
- err := r.data.gormDB.WithContext(ctx).Model(&authorityAPIPO{}).Where("api_id = ?", id).Pluck("authority_id", &ids).Error
- return ids, err
-}
-func (r *accessRepo) SetAPIRoles(ctx context.Context, id uint, 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 {
- return err
- }
- links := make([]authorityAPIPO, 0, len(ids))
- for _, aid := range ids {
- links = append(links, authorityAPIPO{AuthorityID: aid, APIID: id})
- }
- if len(links) > 0 {
- return tx.Create(&links).Error
- }
- return nil
- })
-}
-func (r *accessRepo) Authorize(ctx context.Context, aid uint, path, method string) (bool, error) {
- policies, err := r.PolicyPaths(ctx, aid)
- if err != nil {
- return false, err
- }
- model, err := casbinmodel.NewModelFromString(`[request_definition]
-r = sub, obj, act
-[policy_definition]
-p = sub, obj, act
-[policy_effect]
-e = some(where (p.eft == allow))
-[matchers]
-m = r.sub == p.sub && keyMatch2(r.obj, p.obj) && regexMatch(r.act, p.act)`)
- if err != nil {
- return false, err
- }
- enforcer, err := casbin.NewEnforcer(model)
- if err != nil {
- return false, err
- }
- subject := strconv.FormatUint(uint64(aid), 10)
- for _, policy := range policies {
- if _, err := enforcer.AddPolicy(subject, policy.Path, strings.ToUpper(policy.Method)); err != nil {
- return false, err
- }
- }
- return enforcer.Enforce(subject, path, strings.ToUpper(method))
-}
-func (r *accessRepo) PolicyPaths(ctx context.Context, aid uint) ([]*biz.API, error) {
- var pos []apiPO
- err := r.data.gormDB.WithContext(ctx).Table("sys_apis").Joins("JOIN sys_authority_apis ON sys_authority_apis.api_id = sys_apis.id").Where("sys_authority_apis.authority_id = ? AND sys_apis.deleted_at IS NULL", aid).Order("sys_apis.path,sys_apis.method").Find(&pos).Error
- if err != nil {
- return nil, err
- }
- out := make([]*biz.API, 0, len(pos))
- for _, po := range pos {
- out = append(out, apiFromPO(po))
- }
- return out, nil
-}
-func (r *accessRepo) SetPolicyPaths(ctx context.Context, aid uint, paths []*biz.API) error {
- 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
- }
- links := make([]authorityAPIPO, 0, len(paths))
- for _, path := range paths {
- var po apiPO
- err := tx.Where("path = ? AND method = ?", path.Path, strings.ToUpper(path.Method)).First(&po).Error
- if errors.Is(err, gorm.ErrRecordNotFound) {
- po = apiPO{Path: path.Path, Method: strings.ToUpper(path.Method), Description: path.Description, APIGroup: path.APIGroup}
- if err = tx.Create(&po).Error; err != nil {
- return err
- }
- } else if err != nil {
- return err
- }
- links = append(links, authorityAPIPO{AuthorityID: aid, APIID: po.ID})
- }
- if len(links) > 0 {
- return tx.Create(&links).Error
- }
- return nil
- })
-}
-
-func (r *accessRepo) IgnoredAPIs(ctx context.Context) ([]*biz.API, error) {
- var pos []ignoredAPIPO
- if err := r.data.gormDB.WithContext(ctx).Order("path,method").Find(&pos).Error; err != nil {
- return nil, err
- }
- out := make([]*biz.API, 0, len(pos))
- for _, po := range pos {
- out = append(out, &biz.API{Path: po.Path, Method: po.Method})
- }
- return out, nil
-}
-func (r *accessRepo) SetAPIIgnored(ctx context.Context, path, method string, ignored bool) error {
- po := ignoredAPIPO{Path: path, Method: strings.ToUpper(method)}
- if ignored {
- return r.data.gormDB.WithContext(ctx).FirstOrCreate(&po, po).Error
- }
- return r.data.gormDB.WithContext(ctx).Where("path = ? AND method = ?", po.Path, po.Method).Delete(&ignoredAPIPO{}).Error
-}
-func (r *accessRepo) ApplyAPISync(ctx context.Context, added, deleted []*biz.API) error {
- return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
- for _, item := range added {
- po := apiPO{Path: item.Path, Method: strings.ToUpper(item.Method), Description: item.Description, APIGroup: item.APIGroup}
- if err := tx.Where("path = ? AND method = ?", po.Path, po.Method).FirstOrCreate(&po).Error; err != nil {
- return err
- }
- }
- for _, item := range deleted {
- var ids []uint
- if err := tx.Model(&apiPO{}).Where("path = ? AND method = ?", item.Path, strings.ToUpper(item.Method)).Pluck("id", &ids).Error; err != nil {
- return err
- }
- if len(ids) > 0 {
- if err := tx.Where("api_id IN ?", ids).Delete(&authorityAPIPO{}).Error; err != nil {
- return err
- }
- if err := tx.Where("id IN ?", ids).Delete(&apiPO{}).Error; err != nil {
- return err
- }
- }
- }
- return nil
- })
-}
-
-func (r *accessRepo) Buttons(ctx context.Context, menuID uint) ([]*biz.MenuButton, error) {
- var pos []menuButtonPO
- if err := r.data.gormDB.WithContext(ctx).Where("sys_base_menu_id = ?", menuID).Find(&pos).Error; err != nil {
- return nil, err
- }
- out := make([]*biz.MenuButton, 0, len(pos))
- for _, po := range pos {
- out = append(out, &biz.MenuButton{ID: po.ID, Name: po.Name, Description: po.Description, MenuID: po.MenuID})
- }
- return out, nil
-}
-func (r *accessRepo) SetAuthorityButtons(ctx context.Context, aid uint, buttons map[uint][]uint) error {
- return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
- if err := tx.Where("authority_id = ?", aid).Delete(&authorityButtonPO{}).Error; err != nil {
- return err
- }
- var links []authorityButtonPO
- for menuID, ids := range buttons {
- for _, id := range ids {
- links = append(links, authorityButtonPO{AuthorityID: aid, MenuID: menuID, ButtonID: id})
- }
- }
- if len(links) > 0 {
- return tx.Create(&links).Error
- }
- return nil
- })
-}
-func (r *accessRepo) SelectedButtons(ctx context.Context, aid, menuID uint) ([]uint, error) {
- var ids []uint
- err := r.data.gormDB.WithContext(ctx).Model(&authorityButtonPO{}).Where("authority_id = ? AND sys_menu_id = ?", aid, menuID).Pluck("sys_base_menu_btn_id", &ids).Error
- return ids, err
-}
-func (r *accessRepo) SetSelectedButtons(ctx context.Context, aid, menuID uint, ids []uint) error {
- return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
- if err := tx.Where("authority_id = ? AND sys_menu_id = ?", aid, menuID).Delete(&authorityButtonPO{}).Error; err != nil {
- return err
- }
- links := make([]authorityButtonPO, 0, len(ids))
- for _, id := range ids {
- links = append(links, authorityButtonPO{AuthorityID: aid, MenuID: menuID, ButtonID: id})
- }
- if len(links) > 0 {
- return tx.Create(&links).Error
- }
- return nil
- })
-}
-func (r *accessRepo) CanRemoveButton(ctx context.Context, id uint) (bool, error) {
- var count int64
- err := r.data.gormDB.WithContext(ctx).Model(&authorityButtonPO{}).Where("sys_base_menu_btn_id = ?", id).Count(&count).Error
- return count == 0, err
-}
-func (r *accessRepo) AuthorityButtonIDs(ctx context.Context, aid uint) ([]uint, error) {
- var ids []uint
- err := r.data.gormDB.WithContext(ctx).Model(&authorityButtonPO{}).Where("authority_id = ?", aid).Pluck("sys_base_menu_btn_id", &ids).Error
- return ids, err
-}
-
-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) CreateDepartment(ctx context.Context, v *biz.Department) error {
- v.Ancestors = "0"
- if v.ParentID != 0 {
- var parent departmentPO
- if err := r.data.gormDB.WithContext(ctx).First(&parent, v.ParentID).Error; err != nil {
- return err
- }
- v.Ancestors = strings.Trim(parent.Ancestors+","+strconv.FormatUint(uint64(parent.ID), 10), ",")
- }
- return r.data.gormDB.WithContext(ctx).Create(&departmentPO{Name: v.Name, ParentID: v.ParentID, Ancestors: v.Ancestors, Sort: v.Sort, LeaderID: v.LeaderID, Status: v.Status}).Error
-}
-func (r *organizationRepo) UpdateDepartment(ctx context.Context, v *biz.Department) error {
- if v.ParentID == v.ID {
- return errors.New("父部门不能是自己")
- }
- v.Ancestors = "0"
- if v.ParentID != 0 {
- var parent departmentPO
- if err := r.data.gormDB.WithContext(ctx).First(&parent, v.ParentID).Error; err != nil {
- return err
- }
- v.Ancestors = strings.Trim(parent.Ancestors+","+strconv.FormatUint(uint64(parent.ID), 10), ",")
- }
- return r.data.gormDB.WithContext(ctx).Model(&departmentPO{}).Where("id = ?", v.ID).Updates(map[string]any{"name": v.Name, "parent_id": v.ParentID, "ancestors": v.Ancestors, "sort": v.Sort, "leader_id": v.LeaderID, "status": v.Status}).Error
-}
-func (r *organizationRepo) DeleteDepartment(ctx context.Context, id uint) error {
- if id == 0 {
- return errors.New("部门ID不能为空")
- }
- var count int64
- if err := r.data.gormDB.WithContext(ctx).Model(&departmentPO{}).Where("parent_id = ?", id).Count(&count).Error; err != nil {
- return err
- }
- if count > 0 {
- return errors.New("存在子部门,不允许删除")
- }
- if err := r.data.gormDB.WithContext(ctx).Model(&userPO{}).Where("dept_id = ?", id).Count(&count).Error; err != nil {
- return err
- }
- if count > 0 {
- return errors.New("该部门下存在用户,不允许删除")
- }
- if err := r.data.gormDB.WithContext(ctx).Model(&userDepartmentPO{}).Where("sys_department_id = ?", id).Count(&count).Error; err != nil {
- return err
- }
- if count > 0 {
- return errors.New("该部门下存在用户,不允许删除")
- }
- return r.data.gormDB.WithContext(ctx).Delete(&departmentPO{}, id).Error
-}
-func (r *organizationRepo) FindDepartment(ctx context.Context, id uint) (*biz.Department, error) {
- var po departmentPO
- if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil {
- return nil, err
- }
- return deptFromPO(po), nil
-}
-func (r *organizationRepo) ListDepartments(ctx context.Context, name string) ([]*biz.Department, error) {
- var pos []departmentPO
- db := r.data.gormDB.WithContext(ctx).Order("sort,id")
- if name != "" {
- db = db.Where("name LIKE ?", "%"+name+"%")
- }
- if err := db.Find(&pos).Error; err != nil {
- return nil, err
- }
- nodes := map[uint]*biz.Department{}
- for _, po := range pos {
- nodes[po.ID] = deptFromPO(po)
- }
- var roots []*biz.Department
- for _, po := range pos {
- n := nodes[po.ID]
- if p := nodes[po.ParentID]; p != nil {
- p.Children = append(p.Children, n)
- } else {
- roots = append(roots, n)
- }
- }
- return roots, nil
-}
-func (r *organizationRepo) DepartmentUserIDs(ctx context.Context, id uint) ([]uint, error) {
- var ids []uint
- err := r.data.gormDB.WithContext(ctx).Model(&userDepartmentPO{}).Where("sys_department_id = ?", id).Pluck("sys_user_id", &ids).Error
- return ids, err
-}
-func (r *organizationRepo) SetDepartmentUsers(ctx context.Context, id uint, ids []uint) error {
- return r.replaceUserDepartments(ctx, id, ids)
-}
-func (r *organizationRepo) replaceUserDepartments(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 {
- var current []uint
- if err := tx.Model(&userDepartmentPO{}).Where("sys_department_id = ?", id).Pluck("sys_user_id", ¤t).Error; err != nil {
- return err
- }
- if err := tx.Where("sys_department_id = ?", id).Delete(&userDepartmentPO{}).Error; err != nil {
- return err
- }
- links := make([]userDepartmentPO, 0, len(ids))
- for _, uid := range ids {
- links = append(links, userDepartmentPO{UserID: uid, DepartmentID: id})
- }
- if len(links) > 0 {
- if err := tx.Create(&links).Error; err != nil {
- return err
- }
- }
- target := make(map[uint]struct{}, len(ids))
- for _, uid := range ids {
- target[uid] = struct{}{}
- }
- removed := make([]uint, 0)
- for _, uid := range current {
- if _, ok := target[uid]; !ok {
- removed = append(removed, uid)
- }
- }
- if len(removed) > 0 {
- if err := tx.Model(&userPO{}).Where("dept_id = ? AND id IN ?", id, removed).Update("dept_id", 0).Error; err != nil {
- return err
- }
- }
- if len(ids) > 0 {
- if err := tx.Model(&userPO{}).Where("dept_id = ? AND id IN ?", 0, ids).Update("dept_id", id).Error; err != nil {
- return err
- }
- }
- return nil
- })
-}
-func (r *organizationRepo) SetUserDepartments(ctx context.Context, uid uint, ids []uint, primary uint) error {
- return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
- var user userPO
- if err := tx.First(&user, uid).Error; err != nil {
- return errors.New("查询用户数据失败")
- }
- if err := tx.Where("sys_user_id = ?", uid).Delete(&userDepartmentPO{}).Error; err != nil {
- return err
- }
- links := make([]userDepartmentPO, 0, len(ids))
- for _, id := range ids {
- links = append(links, userDepartmentPO{UserID: uid, DepartmentID: id})
- }
- if len(links) > 0 {
- if err := tx.Create(&links).Error; err != nil {
- return err
- }
- }
- if primary == 0 && len(ids) > 0 {
- primary = ids[0]
- }
- if primary != 0 {
- inSet := false
- for _, id := range ids {
- if id == primary {
- inSet = true
- break
- }
- }
- if !inSet {
- return errors.New("主部门必须在归属部门范围内")
- }
- }
- return tx.Model(&userPO{}).Where("id = ?", uid).Update("dept_id", primary).Error
- })
-}
-
-func posFromPO(po positionPO) *biz.Position {
- return &biz.Position{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Name: po.Name, Code: po.Code, Sort: po.Sort, Status: po.Status, Remark: po.Remark}
-}
-func (r *organizationRepo) CreatePosition(ctx context.Context, v *biz.Position) error {
- return r.data.gormDB.WithContext(ctx).Create(&positionPO{Name: v.Name, Code: v.Code, Sort: v.Sort, Status: v.Status, Remark: v.Remark}).Error
-}
-func (r *organizationRepo) UpdatePosition(ctx context.Context, v *biz.Position) error {
- return r.data.gormDB.WithContext(ctx).Model(&positionPO{}).Where("id = ?", v.ID).Updates(map[string]any{"name": v.Name, "code": v.Code, "sort": v.Sort, "status": v.Status, "remark": v.Remark}).Error
-}
-func (r *organizationRepo) DeletePosition(ctx context.Context, id uint) error {
- if id == 0 {
- return errors.New("岗位ID不能为空")
- }
- var count int64
- if err := r.data.gormDB.WithContext(ctx).Model(&userPositionPO{}).Where("sys_position_id = ?", id).Count(&count).Error; err != nil {
- return err
- }
- if count > 0 {
- return errors.New("该岗位下存在用户,不允许删除")
- }
- return r.data.gormDB.WithContext(ctx).Delete(&positionPO{}, id).Error
-}
-func (r *organizationRepo) FindPosition(ctx context.Context, id uint) (*biz.Position, error) {
- var po positionPO
- if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil {
- return nil, err
- }
- 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
- }
- db := r.data.gormDB.WithContext(ctx).Model(&positionPO{})
- if q != nil {
- if q.Name != "" {
- db = db.Where("name LIKE ?", "%"+q.Name+"%")
- }
- if q.Code != "" {
- db = db.Where("code LIKE ?", "%"+q.Code+"%")
- }
- }
- 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 {
- return nil, 0, err
- }
- out := make([]*biz.Position, 0, len(pos))
- for _, po := range pos {
- out = append(out, posFromPO(po))
- }
- return out, total, nil
-}
-func (r *organizationRepo) PositionUserIDs(ctx context.Context, id uint) ([]uint, error) {
- var ids []uint
- err := r.data.gormDB.WithContext(ctx).Model(&userPositionPO{}).Where("sys_position_id = ?", id).Pluck("sys_user_id", &ids).Error
- return ids, err
-}
-func (r *organizationRepo) SetPositionUsers(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_position_id = ?", id).Delete(&userPositionPO{}).Error; err != nil {
- return err
- }
- links := make([]userPositionPO, 0, len(ids))
- for _, uid := range ids {
- links = append(links, userPositionPO{UserID: uid, PositionID: id})
- }
- if len(links) > 0 {
- return tx.Create(&links).Error
- }
- return nil
- })
-}
-func (r *organizationRepo) SetUserPositions(ctx context.Context, uid uint, ids []uint) error {
- return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
- if err := tx.Where("sys_user_id = ?", uid).Delete(&userPositionPO{}).Error; err != nil {
- return err
- }
- links := make([]userPositionPO, 0, len(ids))
- for _, id := range ids {
- links = append(links, userPositionPO{UserID: uid, PositionID: id})
- }
- if len(links) > 0 {
- return tx.Create(&links).Error
- }
- return nil
- })
-}
+func NewAccessRepo(data *Data) biz.AccessRepo { return &accessRepo{data: data} }
diff --git a/internal/data/api.go b/internal/data/api.go
new file mode 100644
index 0000000..1e7ee1b
--- /dev/null
+++ b/internal/data/api.go
@@ -0,0 +1,229 @@
+package data
+
+import (
+ "context"
+ "errors"
+ "strconv"
+ "strings"
+ "time"
+
+ "kra/internal/biz"
+
+ "github.com/casbin/casbin/v3"
+ casbinmodel "github.com/casbin/casbin/v3/model"
+ "gorm.io/gorm"
+)
+
+type apiPO struct {
+ ID uint `gorm:"primaryKey"`
+ CreatedAt time.Time
+ UpdatedAt time.Time
+ DeletedAt gorm.DeletedAt `gorm:"index"`
+ Path string `gorm:"uniqueIndex:idx_api_path_method"`
+ Description string
+ APIGroup string `gorm:"column:api_group"`
+ Method string `gorm:"uniqueIndex:idx_api_path_method"`
+}
+
+func (apiPO) TableName() string { return "sys_apis" }
+
+type ignoredAPIPO struct {
+ Path string `gorm:"primaryKey"`
+ Method string `gorm:"primaryKey"`
+}
+
+func (ignoredAPIPO) TableName() string { return "sys_ignore_apis" }
+
+type authorityAPIPO struct {
+ AuthorityID uint `gorm:"primaryKey;column:authority_id"`
+ APIID uint `gorm:"primaryKey;column:api_id"`
+}
+
+func (authorityAPIPO) TableName() string { return "sys_authority_apis" }
+
+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
+}
+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
+}
+func (r *accessRepo) DeleteAPIs(ctx context.Context, ids []uint) error {
+ return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ if err := tx.Where("api_id IN ?", ids).Delete(&authorityAPIPO{}).Error; err != nil {
+ return err
+ }
+ return tx.Delete(&apiPO{}, ids).Error
+ })
+}
+func (r *accessRepo) FindAPI(ctx context.Context, id uint) (*biz.API, error) {
+ var po apiPO
+ if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil {
+ return nil, err
+ }
+ 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 {
+ if q.Path != "" {
+ db = db.Where("path LIKE ?", "%"+q.Path+"%")
+ }
+ if q.APIGroup != "" {
+ db = db.Where("api_group = ?", q.APIGroup)
+ }
+ if q.Method != "" {
+ db = db.Where("method = ?", strings.ToUpper(q.Method))
+ }
+ }
+ var total int64
+ if err := db.Count(&total).Error; err != nil {
+ return nil, 0, err
+ }
+ var pos []apiPO
+ if err := db.Order("api_group,path,method").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
+ return nil, 0, err
+ }
+ out := make([]*biz.API, 0, len(pos))
+ for _, po := range pos {
+ out = append(out, apiFromPO(po))
+ }
+ return out, total, nil
+}
+func (r *accessRepo) APIRoleIDs(ctx context.Context, id uint) ([]uint, error) {
+ var ids []uint
+ err := r.data.gormDB.WithContext(ctx).Model(&authorityAPIPO{}).Where("api_id = ?", id).Pluck("authority_id", &ids).Error
+ return ids, err
+}
+func (r *accessRepo) SetAPIRoles(ctx context.Context, id uint, 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 {
+ return err
+ }
+ links := make([]authorityAPIPO, 0, len(ids))
+ for _, aid := range ids {
+ links = append(links, authorityAPIPO{AuthorityID: aid, APIID: id})
+ }
+ if len(links) > 0 {
+ return tx.Create(&links).Error
+ }
+ return nil
+ })
+}
+func (r *accessRepo) Authorize(ctx context.Context, aid uint, path, method string) (bool, error) {
+ policies, err := r.PolicyPaths(ctx, aid)
+ if err != nil {
+ return false, err
+ }
+ model, err := casbinmodel.NewModelFromString(`[request_definition]
+r = sub, obj, act
+[policy_definition]
+p = sub, obj, act
+[policy_effect]
+e = some(where (p.eft == allow))
+[matchers]
+m = r.sub == p.sub && keyMatch2(r.obj, p.obj) && regexMatch(r.act, p.act)`)
+ if err != nil {
+ return false, err
+ }
+ enforcer, err := casbin.NewEnforcer(model)
+ if err != nil {
+ return false, err
+ }
+ subject := strconv.FormatUint(uint64(aid), 10)
+ for _, policy := range policies {
+ if _, err := enforcer.AddPolicy(subject, policy.Path, strings.ToUpper(policy.Method)); err != nil {
+ return false, err
+ }
+ }
+ return enforcer.Enforce(subject, path, strings.ToUpper(method))
+}
+func (r *accessRepo) PolicyPaths(ctx context.Context, aid uint) ([]*biz.API, error) {
+ var pos []apiPO
+ err := r.data.gormDB.WithContext(ctx).Table("sys_apis").Joins("JOIN sys_authority_apis ON sys_authority_apis.api_id = sys_apis.id").Where("sys_authority_apis.authority_id = ? AND sys_apis.deleted_at IS NULL", aid).Order("sys_apis.path,sys_apis.method").Find(&pos).Error
+ if err != nil {
+ return nil, err
+ }
+ out := make([]*biz.API, 0, len(pos))
+ for _, po := range pos {
+ out = append(out, apiFromPO(po))
+ }
+ return out, nil
+}
+func (r *accessRepo) SetPolicyPaths(ctx context.Context, aid uint, paths []*biz.API) error {
+ 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
+ }
+ links := make([]authorityAPIPO, 0, len(paths))
+ for _, path := range paths {
+ var po apiPO
+ err := tx.Where("path = ? AND method = ?", path.Path, strings.ToUpper(path.Method)).First(&po).Error
+ if errors.Is(err, gorm.ErrRecordNotFound) {
+ po = apiPO{Path: path.Path, Method: strings.ToUpper(path.Method), Description: path.Description, APIGroup: path.APIGroup}
+ if err = tx.Create(&po).Error; err != nil {
+ return err
+ }
+ } else if err != nil {
+ return err
+ }
+ links = append(links, authorityAPIPO{AuthorityID: aid, APIID: po.ID})
+ }
+ if len(links) > 0 {
+ return tx.Create(&links).Error
+ }
+ return nil
+ })
+}
+
+func (r *accessRepo) IgnoredAPIs(ctx context.Context) ([]*biz.API, error) {
+ var pos []ignoredAPIPO
+ if err := r.data.gormDB.WithContext(ctx).Order("path,method").Find(&pos).Error; err != nil {
+ return nil, err
+ }
+ out := make([]*biz.API, 0, len(pos))
+ for _, po := range pos {
+ out = append(out, &biz.API{Path: po.Path, Method: po.Method})
+ }
+ return out, nil
+}
+func (r *accessRepo) SetAPIIgnored(ctx context.Context, path, method string, ignored bool) error {
+ po := ignoredAPIPO{Path: path, Method: strings.ToUpper(method)}
+ if ignored {
+ return r.data.gormDB.WithContext(ctx).FirstOrCreate(&po, po).Error
+ }
+ return r.data.gormDB.WithContext(ctx).Where("path = ? AND method = ?", po.Path, po.Method).Delete(&ignoredAPIPO{}).Error
+}
+func (r *accessRepo) ApplyAPISync(ctx context.Context, added, deleted []*biz.API) error {
+ return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ for _, item := range added {
+ po := apiPO{Path: item.Path, Method: strings.ToUpper(item.Method), Description: item.Description, APIGroup: item.APIGroup}
+ if err := tx.Where("path = ? AND method = ?", po.Path, po.Method).FirstOrCreate(&po).Error; err != nil {
+ return err
+ }
+ }
+ for _, item := range deleted {
+ var ids []uint
+ if err := tx.Model(&apiPO{}).Where("path = ? AND method = ?", item.Path, strings.ToUpper(item.Method)).Pluck("id", &ids).Error; err != nil {
+ return err
+ }
+ if len(ids) > 0 {
+ if err := tx.Where("api_id IN ?", ids).Delete(&authorityAPIPO{}).Error; err != nil {
+ return err
+ }
+ if err := tx.Where("id IN ?", ids).Delete(&apiPO{}).Error; err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+ })
+}
diff --git a/internal/data/api_token.go b/internal/data/api_token.go
new file mode 100644
index 0000000..fe08dab
--- /dev/null
+++ b/internal/data/api_token.go
@@ -0,0 +1,123 @@
+package data
+
+import (
+ "context"
+ "errors"
+ "time"
+
+ "kra/internal/biz"
+
+ "gorm.io/gorm"
+)
+
+type apiTokenPO struct {
+ ID uint `gorm:"primaryKey"`
+ CreatedAt time.Time
+ UpdatedAt time.Time
+ DeletedAt gorm.DeletedAt `gorm:"index"`
+ UserID uint
+ AuthorityID uint
+ Token string `gorm:"type:text;uniqueIndex"`
+ Status bool
+ ExpiresAt time.Time
+ Remark string
+}
+
+func (apiTokenPO) TableName() string { return "sys_api_tokens" }
+
+type jwtBlacklistPO struct {
+ ID uint `gorm:"primaryKey"`
+ CreatedAt time.Time
+ UpdatedAt time.Time
+ DeletedAt gorm.DeletedAt `gorm:"index"`
+ JWT string `gorm:"column:jwt;type:text"`
+}
+
+func (jwtBlacklistPO) TableName() string { return "jwt_blacklists" }
+
+func (r *settingsRepo) UserHasAuthority(ctx context.Context, userID, authorityID uint) (*biz.User, bool, error) {
+ var po userPO
+ if err := r.data.gormDB.WithContext(ctx).First(&po, userID).Error; err != nil {
+ return nil, false, err
+ }
+ var count int64
+ err := r.data.gormDB.WithContext(ctx).Model(&userAuthorityPO{}).Where("sys_user_id = ? AND sys_authority_authority_id = ?", userID, authorityID).Count(&count).Error
+ if err != nil {
+ return nil, false, err
+ }
+ user, err := (&systemRepo{data: r.data}).toBizUser(ctx, &po)
+ return user, count > 0 || po.AuthorityID == authorityID, err
+}
+func (r *settingsRepo) CreateAPIToken(ctx context.Context, v *biz.APIToken) error {
+ po := apiTokenPO{UserID: v.UserID, AuthorityID: v.AuthorityID, Token: v.Token, Status: v.Status, ExpiresAt: v.ExpiresAt, Remark: v.Remark}
+ if err := r.data.gormDB.WithContext(ctx).Create(&po).Error; err != nil {
+ return err
+ }
+ v.ID = po.ID
+ v.CreatedAt = po.CreatedAt
+ 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)
+ }
+ if status != nil {
+ db = db.Where("status = ?", *status)
+ }
+ var total int64
+ if err := db.Count(&total).Error; err != nil {
+ 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 {
+ return nil, 0, err
+ }
+ 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)
+ }
+ out = append(out, v)
+ }
+ return out, total, nil
+}
+func (r *settingsRepo) DisableAPIToken(ctx context.Context, id uint) (string, error) {
+ var po apiTokenPO
+ if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil {
+ return "", err
+ }
+ return po.Token, r.data.gormDB.WithContext(ctx).Model(&po).Update("status", false).Error
+}
+func (r *settingsRepo) BlacklistToken(ctx context.Context, token string) error {
+ if token == "" {
+ return errors.New("token is empty")
+ }
+ return r.data.gormDB.WithContext(ctx).Create(&jwtBlacklistPO{JWT: token}).Error
+}
+func (r *settingsRepo) IsTokenDisabled(ctx context.Context, token string) (bool, error) {
+ var blacklistCount int64
+ if err := r.data.gormDB.WithContext(ctx).Model(&jwtBlacklistPO{}).Where("jwt = ?", token).Count(&blacklistCount).Error; err != nil {
+ return false, err
+ }
+ if blacklistCount > 0 {
+ return true, nil
+ }
+ var po apiTokenPO
+ result := r.data.gormDB.WithContext(ctx).Where("token = ?", token).Limit(1).Find(&po)
+ if result.Error != nil {
+ return false, result.Error
+ }
+ if result.RowsAffected == 0 {
+ return false, nil
+ }
+ return !po.Status || time.Now().After(po.ExpiresAt), nil
+}
diff --git a/internal/data/audit.go b/internal/data/audit.go
index 0dfd24d..644cd98 100644
--- a/internal/data/audit.go
+++ b/internal/data/audit.go
@@ -1,430 +1,7 @@
package data
-import (
- "bytes"
- "context"
- "errors"
- "gorm.io/gorm"
- "io/fs"
- "kra/internal/biz"
- "os"
- "path/filepath"
- "sort"
- "strings"
- "time"
-)
-
-type operationPO struct {
- ID uint `gorm:"primaryKey"`
- CreatedAt time.Time
- UpdatedAt time.Time
- DeletedAt gorm.DeletedAt `gorm:"index"`
- IP, Method, Path string
- Status int
- LatencyMS int64 `gorm:"column:latency_ms"`
- Agent string `gorm:"type:text"`
- ErrorMessage string
- Body string `gorm:"type:text"`
- Response string `gorm:"column:resp;type:text"`
- UserID uint
- RequestID string `gorm:"index"`
- TraceID string `gorm:"index"`
- DeviceID string
-}
-
-func (operationPO) TableName() string { return "sys_operation_records" }
-
-type loginLogPO struct {
- ID uint `gorm:"primaryKey"`
- CreatedAt time.Time
- UpdatedAt time.Time
- DeletedAt gorm.DeletedAt `gorm:"index"`
- Username, IP string
- Status bool
- ErrorMessage, Agent string
- UserID uint
-}
-
-func (loginLogPO) TableName() string { return "sys_login_logs" }
-
-type dataAccessLogPO struct {
- ID uint `gorm:"primaryKey"`
- CreatedAt time.Time
- UpdatedAt time.Time
- DeletedAt gorm.DeletedAt `gorm:"index"`
- EventType, TargetTable, Operation string
- UserID, AuthorityID uint
- Scope int
- RequestID, Method, Path, Detail string
-}
-
-func (dataAccessLogPO) TableName() string { return "sys_data_access_logs" }
-
-type errorRecordPO struct {
- ID uint `gorm:"primaryKey"`
- CreatedAt time.Time
- UpdatedAt time.Time
- DeletedAt gorm.DeletedAt `gorm:"index"`
- Form string `gorm:"type:text"`
- Info string `gorm:"type:text"`
- Level string
- RequestID string `gorm:"index"`
- TraceID string `gorm:"index"`
- Solution string `gorm:"type:text"`
- Status string `gorm:"default:未处理"`
-}
-
-func (errorRecordPO) TableName() string { return "sys_error" }
+import "kra/internal/biz"
type auditRepo struct{ data *Data }
func NewAuditRepo(data *Data) biz.AuditRepo { return &auditRepo{data: data} }
-func (r *auditRepo) RecordOperation(ctx context.Context, v *biz.OperationRecord) error {
- return r.data.gormDB.WithContext(ctx).Create(&operationPO{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}).Error
-}
-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 != "" {
- db = db.Where("path LIKE ?", "%"+q.Path+"%")
- }
- if q.Method != "" {
- db = db.Where("method = ?", q.Method)
- }
- 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 {
- return nil, 0, err
- }
- out := make([]*biz.OperationRecord, 0, len(pos))
- for _, po := range pos {
- out = append(out, opFromPO(po))
- }
- return out, total, nil
-}
-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 {
- return nil, err
- }
- return opFromPO(po), nil
-}
-func (r *auditRepo) DeleteOperations(ctx context.Context, ids []uint) error {
- return r.data.gormDB.WithContext(ctx).Delete(&operationPO{}, ids).Error
-}
-func (r *auditRepo) RecordLogin(ctx context.Context, v *biz.LoginLog) error {
- return r.data.gormDB.WithContext(ctx).Create(&loginLogPO{Username: v.Username, IP: v.IP, Status: v.Status, ErrorMessage: v.ErrorMessage, Agent: v.Agent, UserID: v.UserID}).Error
-}
-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+"%")
- }
- }
- var total int64
- if err := db.Count(&total).Error; err != nil {
- return nil, 0, err
- }
- var pos []loginLogPO
- if err := db.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
- return nil, 0, err
- }
- out := make([]*biz.LoginLog, 0, len(pos))
- for _, po := range pos {
- out = append(out, loginFromPO(po))
- }
- return out, total, nil
-}
-func (r *auditRepo) FindLogin(ctx context.Context, id uint) (*biz.LoginLog, error) {
- var po loginLogPO
- if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil {
- return nil, err
- }
- return loginFromPO(po), nil
-}
-func (r *auditRepo) DeleteLogins(ctx context.Context, ids []uint) error {
- return r.data.gormDB.WithContext(ctx).Delete(&loginLogPO{}, ids).Error
-}
-func (r *auditRepo) RecordDataAccess(ctx context.Context, v *biz.DataAccessLog) error {
- return r.data.gormDB.WithContext(ctx).Create(&dataAccessLogPO{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}).Error
-}
-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 != "" {
- db = db.Where("event_type = ?", q.EventType)
- }
- if q.TargetTable != "" {
- db = db.Where("target_table LIKE ?", "%"+q.TargetTable+"%")
- }
- 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 []dataAccessLogPO
- if err := db.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
- return nil, 0, err
- }
- out := make([]*biz.DataAccessLog, 0, len(pos))
- for _, po := range pos {
- out = append(out, dataAccessFromPO(po))
- }
- return out, total, nil
-}
-func (r *auditRepo) DeleteDataAccess(ctx context.Context, ids []uint) error {
- return r.data.gormDB.WithContext(ctx).Delete(&dataAccessLogPO{}, ids).Error
-}
-
-func logRoot() (string, error) {
- root, err := filepath.Abs("logs")
- if err != nil {
- return "", err
- }
- info, err := os.Stat(root)
- if errors.Is(err, fs.ErrNotExist) {
- return root, nil
- }
- if err != nil {
- return "", err
- }
- if !info.IsDir() {
- return "", errors.New("日志路径不是目录")
- }
- return root, nil
-}
-func (r *auditRepo) LogDates(ctx context.Context, month string) ([]biz.LogDate, error) {
- if parsed, err := time.Parse("2006-01", month); err != nil || parsed.Format("2006-01") != month {
- return nil, errors.New("日志月份格式不正确")
- }
- root, err := logRoot()
- if err != nil {
- return nil, err
- }
- entries, err := os.ReadDir(root)
- if errors.Is(err, fs.ErrNotExist) {
- return []biz.LogDate{}, nil
- }
- if err != nil {
- return nil, err
- }
- out := []biz.LogDate{}
- for _, entry := range entries {
- if ctx.Err() != nil {
- return nil, ctx.Err()
- }
- if entry.Type()&os.ModeSymlink != 0 || !entry.IsDir() || !strings.HasPrefix(entry.Name(), month+"-") {
- continue
- }
- if parsed, parseErr := time.Parse("2006-01-02", entry.Name()); parseErr != nil || parsed.Format("2006-01-02") != entry.Name() {
- continue
- }
- count := 0
- _ = filepath.WalkDir(filepath.Join(root, entry.Name()), func(_ string, item fs.DirEntry, walkErr error) error {
- if walkErr == nil && !item.IsDir() && strings.EqualFold(filepath.Ext(item.Name()), ".log") {
- count++
- }
- return nil
- })
- if count > 0 {
- out = append(out, biz.LogDate{Date: entry.Name(), FileCount: count})
- }
- }
- sort.Slice(out, func(i, j int) bool { return out[i].Date < out[j].Date })
- return out, nil
-}
-func (r *auditRepo) LogFiles(ctx context.Context, date string) ([]biz.LogFile, error) {
- if parsed, err := time.Parse("2006-01-02", date); err != nil || parsed.Format("2006-01-02") != date {
- return nil, errors.New("日志日期格式不正确")
- }
- root, err := logRoot()
- if err != nil {
- return nil, err
- }
- dateRoot := filepath.Join(root, date)
- out := []biz.LogFile{}
- err = filepath.WalkDir(dateRoot, func(path string, entry fs.DirEntry, walkErr error) error {
- if walkErr != nil {
- if errors.Is(walkErr, fs.ErrNotExist) {
- return nil
- }
- return walkErr
- }
- if ctx.Err() != nil {
- return ctx.Err()
- }
- if entry.Type()&os.ModeSymlink != 0 {
- if entry.IsDir() {
- return filepath.SkipDir
- }
- return nil
- }
- if entry.IsDir() || !strings.EqualFold(filepath.Ext(entry.Name()), ".log") {
- return nil
- }
- info, infoErr := entry.Info()
- if infoErr != nil {
- return infoErr
- }
- relative, _ := filepath.Rel(dateRoot, path)
- out = append(out, biz.LogFile{Path: filepath.ToSlash(relative), Name: entry.Name(), Size: info.Size(), ModifiedAt: info.ModTime()})
- return nil
- })
- sort.Slice(out, func(i, j int) bool { return out[i].Path < out[j].Path })
- return out, err
-}
-func (r *auditRepo) LogContent(ctx context.Context, date, path string, cursor *int64) (*biz.LogContent, error) {
- if _, err := time.Parse("2006-01-02", date); err != nil {
- return nil, errors.New("日志日期格式不正确")
- }
- root, err := logRoot()
- if err != nil {
- return nil, err
- }
- dateRoot := filepath.Join(root, date)
- target := filepath.Clean(filepath.Join(dateRoot, filepath.FromSlash(path)))
- if target == dateRoot || !strings.HasPrefix(target, dateRoot+string(os.PathSeparator)) || !strings.EqualFold(filepath.Ext(target), ".log") {
- return nil, errors.New("日志文件路径不合法")
- }
- info, err := os.Lstat(target)
- if err != nil {
- return nil, err
- }
- if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() {
- return nil, errors.New("日志文件路径不合法")
- }
- end := info.Size()
- if cursor != nil && *cursor >= 0 && *cursor < end {
- end = *cursor
- }
- start := end - int64(2*1024*1024)
- limited := start > 0
- if start < 0 {
- start = 0
- }
- file, err := os.Open(target)
- if err != nil {
- return nil, err
- }
- defer file.Close()
- data := make([]byte, end-start)
- n, err := file.ReadAt(data, start)
- if err != nil && n == 0 {
- return nil, err
- }
- data = data[:n]
- lines := bytes.Split(data, []byte("\n"))
- if len(lines) > 501 {
- drop := len(lines) - 501
- offset := 0
- for _, line := range lines[:drop] {
- offset += len(line) + 1
- }
- start += int64(offset)
- data = data[offset:]
- limited = true
- }
- return &biz.LogContent{Date: date, Path: path, Content: string(data), LineCount: bytes.Count(data, []byte("\n")), NextCursor: start, HasMore: start > 0, LimitedByBytes: limited, Size: info.Size(), ModifiedAt: info.ModTime()}, nil
-}
-func errorFromPO(v errorRecordPO) *biz.ErrorRecord {
- return &biz.ErrorRecord{ID: v.ID, CreatedAt: v.CreatedAt, Form: v.Form, Info: v.Info, Level: v.Level, RequestID: v.RequestID, TraceID: v.TraceID, Solution: v.Solution, Status: v.Status}
-}
-func (r *auditRepo) CreateError(ctx context.Context, v *biz.ErrorRecord) error {
- if v.Status == "" {
- v.Status = "未处理"
- }
- return r.data.gormDB.WithContext(ctx).Create(&errorRecordPO{Form: v.Form, Info: v.Info, Level: v.Level, RequestID: v.RequestID, TraceID: v.TraceID, Solution: v.Solution, Status: v.Status}).Error
-}
-func (r *auditRepo) UpdateError(ctx context.Context, v *biz.ErrorRecord) error {
- return r.data.gormDB.WithContext(ctx).Model(&errorRecordPO{}).Where("id = ?", v.ID).Updates(map[string]any{"form": v.Form, "info": v.Info, "level": v.Level, "solution": v.Solution, "status": v.Status}).Error
-}
-func (r *auditRepo) DeleteErrors(ctx context.Context, ids []uint) error {
- return r.data.gormDB.WithContext(ctx).Delete(&errorRecordPO{}, ids).Error
-}
-func (r *auditRepo) FindError(ctx context.Context, id uint) (*biz.ErrorRecord, error) {
- var po errorRecordPO
- if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil {
- return nil, err
- }
- 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 q.Form != "" {
- db = db.Where("form LIKE ?", "%"+q.Form+"%")
- }
- if q.Level != "" {
- db = db.Where("level = ?", q.Level)
- }
- if q.Status != "" {
- db = db.Where("status = ?", q.Status)
- }
- }
- var total int64
- if err := db.Count(&total).Error; err != nil {
- return nil, 0, err
- }
- var pos []errorRecordPO
- if err := db.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
- return nil, 0, err
- }
- out := make([]*biz.ErrorRecord, 0, len(pos))
- for _, po := range pos {
- out = append(out, errorFromPO(po))
- }
- return out, total, nil
-}
diff --git a/internal/data/authority.go b/internal/data/authority.go
new file mode 100644
index 0000000..c7445f1
--- /dev/null
+++ b/internal/data/authority.go
@@ -0,0 +1,283 @@
+package data
+
+import (
+ "context"
+ "errors"
+ "strconv"
+ "strings"
+
+ "kra/internal/biz"
+
+ "gorm.io/gorm"
+)
+
+func (r *accessRepo) CreateAuthority(ctx context.Context, value *biz.Authority) error {
+ 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 {
+ return err
+ }
+ if count > 0 {
+ return errors.New("存在相同角色id")
+ }
+ if value.DefaultRouter == "" {
+ value.DefaultRouter = "dashboard"
+ }
+ if err := tx.Create(&authorityPO{AuthorityID: value.AuthorityID, AuthorityName: value.AuthorityName, ParentID: value.ParentID, DataScope: value.DataScope, DefaultRouter: value.DefaultRouter}).Error; err != nil {
+ return err
+ }
+ var dashboard menuPO
+ if err := tx.Where("name = ?", "dashboard").First(&dashboard).Error; err == nil {
+ if err = tx.Create(&authorityMenuPO{SysAuthorityAuthorityID: value.AuthorityID, SysBaseMenuID: dashboard.ID}).Error; err != nil {
+ return err
+ }
+ }
+ defaults := []struct{ path, method string }{{"/menu/getMenu", "POST"}, {"/jwt/jsonInBlacklist", "POST"}, {"/user/changePassword", "POST"}, {"/user/setUserAuthority", "POST"}, {"/user/getUserInfo", "GET"}, {"/user/setSelfInfo", "PUT"}, {"/fileUploadAndDownload/upload", "POST"}, {"/sysDictionary/findSysDictionary", "GET"}}
+ for _, item := range defaults {
+ var api apiPO
+ if err := tx.Where("path = ? AND method = ?", item.path, item.method).First(&api).Error; err == nil {
+ if err = tx.Create(&authorityAPIPO{AuthorityID: value.AuthorityID, APIID: api.ID}).Error; err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+ })
+}
+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 {
+ var count int64
+ if err := tx.Model(&authorityPO{}).Where("authority_id = ?", value.AuthorityID).Count(&count).Error; err != nil {
+ return err
+ }
+ if count != 0 {
+ return errors.New("存在相同角色id")
+ }
+ if err := tx.Create(&authorityPO{AuthorityID: value.AuthorityID, AuthorityName: value.AuthorityName, ParentID: value.ParentID, DataScope: value.DataScope, DefaultRouter: value.DefaultRouter}).Error; err != nil {
+ return err
+ }
+ copyLinks := func(table string, destination any, columns map[string]any) error {
+ return tx.Table(table).Where(columns).Find(destination).Error
+ }
+ var menus []authorityMenuPO
+ if err := copyLinks("sys_authority_menus", &menus, map[string]any{"sys_authority_authority_id": sourceID}); err != nil {
+ return err
+ }
+ for i := range menus {
+ menus[i].SysAuthorityAuthorityID = value.AuthorityID
+ }
+ if len(menus) > 0 {
+ if err := tx.Create(&menus).Error; err != nil {
+ return err
+ }
+ }
+ var apis []authorityAPIPO
+ if err := copyLinks("sys_authority_apis", &apis, map[string]any{"authority_id": sourceID}); err != nil {
+ return err
+ }
+ for i := range apis {
+ apis[i].AuthorityID = value.AuthorityID
+ }
+ if len(apis) > 0 {
+ if err := tx.Create(&apis).Error; err != nil {
+ return err
+ }
+ }
+ var buttons []authorityButtonPO
+ if err := copyLinks("sys_authority_btns", &buttons, map[string]any{"authority_id": sourceID}); err != nil {
+ return err
+ }
+ for i := range buttons {
+ buttons[i].AuthorityID = value.AuthorityID
+ }
+ if len(buttons) > 0 {
+ if err := tx.Create(&buttons).Error; err != nil {
+ 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
+ })
+}
+func (r *accessRepo) UpdateAuthority(ctx context.Context, value *biz.Authority) error {
+ return r.data.gormDB.WithContext(ctx).Model(&authorityPO{}).Where("authority_id = ?", value.AuthorityID).Updates(map[string]any{"authority_name": value.AuthorityName, "parent_id": value.ParentID, "data_scope": value.DataScope, "default_router": value.DefaultRouter}).Error
+}
+func (r *accessRepo) DeleteAuthority(ctx context.Context, id uint) error {
+ if id == 888 {
+ return errors.New("不能删除超级管理员")
+ }
+ return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ var users, children int64
+ if err := tx.Model(&userAuthorityPO{}).Where("sys_authority_authority_id = ?", id).Count(&users).Error; err != nil {
+ return err
+ }
+ if users > 0 {
+ return errors.New("此角色有用户正在使用禁止删除")
+ }
+ if err := tx.Model(&userPO{}).Where("authority_id = ?", id).Count(&users).Error; err != nil {
+ return err
+ }
+ if users > 0 {
+ return errors.New("此角色有用户正在使用禁止删除")
+ }
+ if err := tx.Model(&authorityPO{}).Where("parent_id = ?", id).Count(&children).Error; err != nil {
+ return err
+ }
+ if children > 0 {
+ return errors.New("此角色存在子角色不允许删除")
+ }
+ if err := tx.Where("sys_authority_authority_id = ?", id).Delete(&authorityMenuPO{}).Error; err != nil {
+ return err
+ }
+ if err := tx.Where("authority_id = ?", id).Delete(&authorityAPIPO{}).Error; err != nil {
+ return err
+ }
+ if err := tx.Where("authority_id = ?", id).Delete(&authorityButtonPO{}).Error; err != nil {
+ return err
+ }
+ if err := tx.Where("authority_id = ?", id).Delete(&authorityDepartmentPO{}).Error; err != nil {
+ return err
+ }
+ return tx.Delete(&authorityPO{}, "authority_id = ?", id).Error
+ })
+}
+func (r *accessRepo) ListAuthorities(ctx context.Context) ([]*biz.Authority, error) {
+ var pos []authorityPO
+ if err := r.data.gormDB.WithContext(ctx).Order("authority_id").Find(&pos).Error; err != nil {
+ return nil, err
+ }
+ out := make([]*biz.Authority, 0, len(pos))
+ for _, po := range pos {
+ v := toBizAuthority(po)
+ out = append(out, &v)
+ }
+ return out, nil
+}
+func (r *accessRepo) SetAuthorityUsers(ctx context.Context, id uint, ids []uint) error {
+ return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ var oldIDs []uint
+ if err := tx.Model(&userAuthorityPO{}).Where("sys_authority_authority_id = ?", id).Pluck("sys_user_id", &oldIDs).Error; err != nil {
+ return err
+ }
+ if err := tx.Where("sys_authority_authority_id = ?", id).Delete(&userAuthorityPO{}).Error; err != nil {
+ return err
+ }
+ links := make([]userAuthorityPO, 0, len(ids))
+ for _, uid := range ids {
+ links = append(links, userAuthorityPO{SysUserID: uid, SysAuthorityAuthorityID: id})
+ }
+ if len(links) > 0 {
+ if err := tx.Create(&links).Error; err != nil {
+ return err
+ }
+ }
+ selected := map[uint]bool{}
+ for _, uid := range ids {
+ selected[uid] = true
+ }
+ for _, uid := range oldIDs {
+ if selected[uid] {
+ continue
+ }
+ 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 {
+ return err
+ }
+ fallback := userAuthorityPO{SysUserID: uid, SysAuthorityAuthorityID: 888}
+ if err = tx.FirstOrCreate(&fallback, fallback).Error; err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+ })
+}
+func (r *accessRepo) AuthorityUserIDs(ctx context.Context, id uint) ([]uint, error) {
+ var ids []uint
+ err := r.data.gormDB.WithContext(ctx).Model(&userAuthorityPO{}).Where("sys_authority_authority_id = ?", id).Pluck("sys_user_id", &ids).Error
+ return ids, err
+}
+func (r *accessRepo) SetDataScope(ctx context.Context, id uint, scope int, deptIDs []uint) error {
+ return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ if err := tx.Model(&authorityPO{}).Where("authority_id = ?", id).Update("data_scope", scope).Error; err != nil {
+ return err
+ }
+ 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
+ }
+ return nil
+ })
+}
+func (r *accessRepo) DataScopeDepartmentIDs(ctx context.Context, id uint) ([]uint, error) {
+ var ids []uint
+ err := r.data.gormDB.WithContext(ctx).Model(&authorityDepartmentPO{}).Where("authority_id = ?", id).Pluck("department_id", &ids).Error
+ return ids, err
+}
+func (r *accessRepo) ResolveDataScope(ctx context.Context, authorityID, userID uint) (biz.DataScope, error) {
+ if authorityID == 888 {
+ return biz.DataScope{All: true}, nil
+ }
+ var authority authorityPO
+ if err := r.data.gormDB.WithContext(ctx).First(&authority, "authority_id = ?", authorityID).Error; err != nil {
+ return biz.DataScope{}, err
+ }
+ if authority.DataScope == 1 {
+ return biz.DataScope{All: true}, nil
+ }
+ if authority.DataScope == 4 {
+ return biz.DataScope{OwnerUserID: userID}, nil
+ }
+ var ids []uint
+ if authority.DataScope == 5 {
+ var err error
+ ids, err = r.DataScopeDepartmentIDs(ctx, authorityID)
+ if err != nil {
+ return biz.DataScope{}, err
+ }
+ } else {
+ if err := r.data.gormDB.WithContext(ctx).Model(&userDepartmentPO{}).Where("sys_user_id = ?", userID).Pluck("sys_department_id", &ids).Error; err != nil {
+ return biz.DataScope{}, err
+ }
+ if authority.DataScope == 2 && len(ids) > 0 {
+ var departments []departmentPO
+ if err := r.data.gormDB.WithContext(ctx).Find(&departments).Error; err != nil {
+ return biz.DataScope{}, err
+ }
+ selected := make(map[uint]bool, len(ids))
+ for _, id := range ids {
+ selected[id] = true
+ }
+ for _, department := range departments {
+ for _, part := range strings.Split(department.Ancestors, ",") {
+ value, _ := strconv.ParseUint(part, 10, 64)
+ if selected[uint(value)] {
+ selected[department.ID] = true
+ break
+ }
+ }
+ }
+ ids = ids[:0]
+ for id := range selected {
+ ids = append(ids, id)
+ }
+ }
+ }
+ return biz.DataScope{DepartmentIDs: ids}, nil
+}
diff --git a/internal/data/data_access_log.go b/internal/data/data_access_log.go
new file mode 100644
index 0000000..a7c56db
--- /dev/null
+++ b/internal/data/data_access_log.go
@@ -0,0 +1,66 @@
+package data
+
+import (
+ "context"
+ "time"
+
+ "kra/internal/biz"
+
+ "gorm.io/gorm"
+)
+
+type dataAccessLogPO struct {
+ ID uint `gorm:"primaryKey"`
+ CreatedAt time.Time
+ UpdatedAt time.Time
+ DeletedAt gorm.DeletedAt `gorm:"index"`
+ EventType, TargetTable, Operation string
+ UserID, AuthorityID uint
+ Scope int
+ RequestID, Method, Path, Detail string
+}
+
+func (dataAccessLogPO) TableName() string { return "sys_data_access_logs" }
+
+func (r *auditRepo) RecordDataAccess(ctx context.Context, v *biz.DataAccessLog) error {
+ return r.data.gormDB.WithContext(ctx).Create(&dataAccessLogPO{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}).Error
+}
+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 != "" {
+ db = db.Where("event_type = ?", q.EventType)
+ }
+ if q.TargetTable != "" {
+ db = db.Where("target_table LIKE ?", "%"+q.TargetTable+"%")
+ }
+ 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 []dataAccessLogPO
+ if err := db.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
+ return nil, 0, err
+ }
+ out := make([]*biz.DataAccessLog, 0, len(pos))
+ for _, po := range pos {
+ out = append(out, dataAccessFromPO(po))
+ }
+ return out, total, nil
+}
+func (r *auditRepo) DeleteDataAccess(ctx context.Context, ids []uint) error {
+ return r.data.gormDB.WithContext(ctx).Delete(&dataAccessLogPO{}, ids).Error
+}
diff --git a/internal/data/department.go b/internal/data/department.go
new file mode 100644
index 0000000..1abea14
--- /dev/null
+++ b/internal/data/department.go
@@ -0,0 +1,215 @@
+package data
+
+import (
+ "context"
+ "errors"
+ "strconv"
+ "strings"
+ "time"
+
+ "kra/internal/biz"
+
+ "gorm.io/gorm"
+)
+
+type departmentPO struct {
+ ID uint `gorm:"primaryKey"`
+ CreatedAt time.Time
+ UpdatedAt time.Time
+ DeletedAt gorm.DeletedAt `gorm:"index"`
+ Name string `gorm:"index"`
+ ParentID uint
+ Ancestors string
+ Sort int
+ LeaderID uint
+ Status bool `gorm:"default:true"`
+}
+
+func (departmentPO) TableName() string { return "sys_departments" }
+
+type userDepartmentPO struct {
+ UserID uint `gorm:"primaryKey;column:sys_user_id"`
+ DepartmentID uint `gorm:"primaryKey;column:sys_department_id"`
+}
+
+func (userDepartmentPO) TableName() string { return "sys_user_departments" }
+
+type authorityDepartmentPO struct {
+ AuthorityID uint `gorm:"primaryKey;column:authority_id"`
+ DepartmentID uint `gorm:"primaryKey;column:department_id"`
+}
+
+func (authorityDepartmentPO) TableName() string { return "sys_authority_departments" }
+
+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) CreateDepartment(ctx context.Context, v *biz.Department) error {
+ v.Ancestors = "0"
+ if v.ParentID != 0 {
+ var parent departmentPO
+ if err := r.data.gormDB.WithContext(ctx).First(&parent, v.ParentID).Error; err != nil {
+ return err
+ }
+ v.Ancestors = strings.Trim(parent.Ancestors+","+strconv.FormatUint(uint64(parent.ID), 10), ",")
+ }
+ return r.data.gormDB.WithContext(ctx).Create(&departmentPO{Name: v.Name, ParentID: v.ParentID, Ancestors: v.Ancestors, Sort: v.Sort, LeaderID: v.LeaderID, Status: v.Status}).Error
+}
+func (r *organizationRepo) UpdateDepartment(ctx context.Context, v *biz.Department) error {
+ if v.ParentID == v.ID {
+ return errors.New("父部门不能是自己")
+ }
+ v.Ancestors = "0"
+ if v.ParentID != 0 {
+ var parent departmentPO
+ if err := r.data.gormDB.WithContext(ctx).First(&parent, v.ParentID).Error; err != nil {
+ return err
+ }
+ v.Ancestors = strings.Trim(parent.Ancestors+","+strconv.FormatUint(uint64(parent.ID), 10), ",")
+ }
+ return r.data.gormDB.WithContext(ctx).Model(&departmentPO{}).Where("id = ?", v.ID).Updates(map[string]any{"name": v.Name, "parent_id": v.ParentID, "ancestors": v.Ancestors, "sort": v.Sort, "leader_id": v.LeaderID, "status": v.Status}).Error
+}
+func (r *organizationRepo) DeleteDepartment(ctx context.Context, id uint) error {
+ if id == 0 {
+ return errors.New("部门ID不能为空")
+ }
+ var count int64
+ if err := r.data.gormDB.WithContext(ctx).Model(&departmentPO{}).Where("parent_id = ?", id).Count(&count).Error; err != nil {
+ return err
+ }
+ if count > 0 {
+ return errors.New("存在子部门,不允许删除")
+ }
+ if err := r.data.gormDB.WithContext(ctx).Model(&userPO{}).Where("dept_id = ?", id).Count(&count).Error; err != nil {
+ return err
+ }
+ if count > 0 {
+ return errors.New("该部门下存在用户,不允许删除")
+ }
+ if err := r.data.gormDB.WithContext(ctx).Model(&userDepartmentPO{}).Where("sys_department_id = ?", id).Count(&count).Error; err != nil {
+ return err
+ }
+ if count > 0 {
+ return errors.New("该部门下存在用户,不允许删除")
+ }
+ return r.data.gormDB.WithContext(ctx).Delete(&departmentPO{}, id).Error
+}
+func (r *organizationRepo) FindDepartment(ctx context.Context, id uint) (*biz.Department, error) {
+ var po departmentPO
+ if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil {
+ return nil, err
+ }
+ return deptFromPO(po), nil
+}
+func (r *organizationRepo) ListDepartments(ctx context.Context, name string) ([]*biz.Department, error) {
+ var pos []departmentPO
+ db := r.data.gormDB.WithContext(ctx).Order("sort,id")
+ if name != "" {
+ db = db.Where("name LIKE ?", "%"+name+"%")
+ }
+ if err := db.Find(&pos).Error; err != nil {
+ return nil, err
+ }
+ nodes := map[uint]*biz.Department{}
+ for _, po := range pos {
+ nodes[po.ID] = deptFromPO(po)
+ }
+ var roots []*biz.Department
+ for _, po := range pos {
+ n := nodes[po.ID]
+ if p := nodes[po.ParentID]; p != nil {
+ p.Children = append(p.Children, n)
+ } else {
+ roots = append(roots, n)
+ }
+ }
+ return roots, nil
+}
+func (r *organizationRepo) DepartmentUserIDs(ctx context.Context, id uint) ([]uint, error) {
+ var ids []uint
+ err := r.data.gormDB.WithContext(ctx).Model(&userDepartmentPO{}).Where("sys_department_id = ?", id).Pluck("sys_user_id", &ids).Error
+ return ids, err
+}
+func (r *organizationRepo) SetDepartmentUsers(ctx context.Context, id uint, ids []uint) error {
+ return r.replaceUserDepartments(ctx, id, ids)
+}
+func (r *organizationRepo) replaceUserDepartments(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 {
+ var current []uint
+ if err := tx.Model(&userDepartmentPO{}).Where("sys_department_id = ?", id).Pluck("sys_user_id", ¤t).Error; err != nil {
+ return err
+ }
+ if err := tx.Where("sys_department_id = ?", id).Delete(&userDepartmentPO{}).Error; err != nil {
+ return err
+ }
+ links := make([]userDepartmentPO, 0, len(ids))
+ for _, uid := range ids {
+ links = append(links, userDepartmentPO{UserID: uid, DepartmentID: id})
+ }
+ if len(links) > 0 {
+ if err := tx.Create(&links).Error; err != nil {
+ return err
+ }
+ }
+ target := make(map[uint]struct{}, len(ids))
+ for _, uid := range ids {
+ target[uid] = struct{}{}
+ }
+ removed := make([]uint, 0)
+ for _, uid := range current {
+ if _, ok := target[uid]; !ok {
+ removed = append(removed, uid)
+ }
+ }
+ if len(removed) > 0 {
+ if err := tx.Model(&userPO{}).Where("dept_id = ? AND id IN ?", id, removed).Update("dept_id", 0).Error; err != nil {
+ return err
+ }
+ }
+ if len(ids) > 0 {
+ if err := tx.Model(&userPO{}).Where("dept_id = ? AND id IN ?", 0, ids).Update("dept_id", id).Error; err != nil {
+ return err
+ }
+ }
+ return nil
+ })
+}
+func (r *organizationRepo) SetUserDepartments(ctx context.Context, uid uint, ids []uint, primary uint) error {
+ return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ var user userPO
+ if err := tx.First(&user, uid).Error; err != nil {
+ return errors.New("查询用户数据失败")
+ }
+ if err := tx.Where("sys_user_id = ?", uid).Delete(&userDepartmentPO{}).Error; err != nil {
+ return err
+ }
+ links := make([]userDepartmentPO, 0, len(ids))
+ for _, id := range ids {
+ links = append(links, userDepartmentPO{UserID: uid, DepartmentID: id})
+ }
+ if len(links) > 0 {
+ if err := tx.Create(&links).Error; err != nil {
+ return err
+ }
+ }
+ if primary == 0 && len(ids) > 0 {
+ primary = ids[0]
+ }
+ if primary != 0 {
+ inSet := false
+ for _, id := range ids {
+ if id == primary {
+ inSet = true
+ break
+ }
+ }
+ if !inSet {
+ return errors.New("主部门必须在归属部门范围内")
+ }
+ }
+ return tx.Model(&userPO{}).Where("id = ?", uid).Update("dept_id", primary).Error
+ })
+}
diff --git a/internal/data/dictionary.go b/internal/data/dictionary.go
new file mode 100644
index 0000000..41b8b1f
--- /dev/null
+++ b/internal/data/dictionary.go
@@ -0,0 +1,257 @@
+package data
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "strings"
+ "time"
+
+ "kra/internal/biz"
+
+ "gorm.io/gorm"
+)
+
+type dictionaryPO struct {
+ ID uint `gorm:"primaryKey"`
+ CreatedAt time.Time
+ UpdatedAt time.Time
+ DeletedAt gorm.DeletedAt `gorm:"index"`
+ Name string
+ Type string `gorm:"uniqueIndex"`
+ Status bool
+ Desc string
+ ParentID *uint
+}
+
+func (dictionaryPO) TableName() string { return "sys_dictionaries" }
+
+type dictionaryDetailPO struct {
+ ID uint `gorm:"primaryKey"`
+ CreatedAt time.Time
+ UpdatedAt time.Time
+ DeletedAt gorm.DeletedAt `gorm:"index"`
+ Label string
+ Value string
+ Extend string
+ Status bool
+ Sort int
+ DictionaryID uint `gorm:"column:sys_dictionary_id;index"`
+ ParentID *uint
+ Level int
+ Path string
+}
+
+func (dictionaryDetailPO) TableName() string { return "sys_dictionary_details" }
+
+func dictionaryFromPO(po dictionaryPO) *biz.Dictionary {
+ return &biz.Dictionary{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Name: po.Name, Type: po.Type, Status: po.Status, Desc: po.Desc, ParentID: po.ParentID}
+}
+func detailFromPO(po dictionaryDetailPO) *biz.DictionaryDetail {
+ return &biz.DictionaryDetail{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Label: po.Label, Value: po.Value, Extend: po.Extend, Status: po.Status, Sort: po.Sort, DictionaryID: po.DictionaryID, ParentID: po.ParentID, Level: po.Level, Path: po.Path}
+}
+func parameterFromPO(po parameterPO) *biz.SystemParameter {
+ return &biz.SystemParameter{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Name: po.Name, Key: po.Key, Value: po.Value, Desc: po.Desc}
+}
+
+func (r *settingsRepo) CreateDictionary(ctx context.Context, v *biz.Dictionary) error {
+ po := dictionaryPO{Name: v.Name, Type: v.Type, Status: v.Status, Desc: v.Desc, ParentID: v.ParentID}
+ if err := r.data.gormDB.WithContext(ctx).Create(&po).Error; err != nil {
+ return err
+ }
+ v.ID = po.ID
+ 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
+}
+func (r *settingsRepo) DeleteDictionary(ctx context.Context, id uint) error {
+ return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ var count int64
+ if err := tx.Model(&dictionaryPO{}).Where("parent_id = ?", id).Count(&count).Error; err != nil {
+ return err
+ }
+ if count > 0 {
+ return errors.New("存在子字典不可删除")
+ }
+ if err := tx.Where("sys_dictionary_id = ?", id).Delete(&dictionaryDetailPO{}).Error; err != nil {
+ return err
+ }
+ return tx.Delete(&dictionaryPO{}, id).Error
+ })
+}
+func (r *settingsRepo) FindDictionary(ctx context.Context, id uint, typ string, 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
+ }
+ if err != nil {
+ return nil, err
+ }
+ out := dictionaryFromPO(po)
+ if details {
+ out.Details, _, err = r.ListDictionaryDetails(ctx, 1, 10000, out.ID, "", "")
+ }
+ return out, err
+}
+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+"%")
+ }
+ 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 {
+ return nil, 0, err
+ }
+ out := make([]*biz.Dictionary, 0, len(pos))
+ for _, po := range pos {
+ v := dictionaryFromPO(po)
+ if details {
+ v.Details, _, _ = r.ListDictionaryDetails(ctx, 1, 10000, v.ID, "", "")
+ }
+ out = append(out, v)
+ }
+ return out, total, nil
+}
+
+func (r *settingsRepo) CreateDictionaryDetail(ctx context.Context, v *biz.DictionaryDetail) error {
+ po := dictionaryDetailPO{Label: v.Label, Value: v.Value, Extend: v.Extend, Status: v.Status, Sort: v.Sort, DictionaryID: v.DictionaryID, ParentID: v.ParentID}
+ po.Level = 0
+ po.Path = ""
+ if v.ParentID != nil && *v.ParentID != 0 {
+ var parent dictionaryDetailPO
+ if err := r.data.gormDB.WithContext(ctx).First(&parent, *v.ParentID).Error; err != nil {
+ return err
+ }
+ po.Level = parent.Level + 1
+ po.Path = strings.Trim(strings.Join([]string{parent.Path, fmt.Sprint(parent.ID)}, ","), ",")
+ }
+ if err := r.data.gormDB.WithContext(ctx).Create(&po).Error; err != nil {
+ return err
+ }
+ v.ID = po.ID
+ v.Level = po.Level
+ v.Path = po.Path
+ return nil
+}
+func (r *settingsRepo) UpdateDictionaryDetail(ctx context.Context, v *biz.DictionaryDetail) error {
+ var po dictionaryDetailPO
+ if err := r.data.gormDB.WithContext(ctx).First(&po, v.ID).Error; err != nil {
+ return err
+ }
+ level, path := 0, ""
+ if v.ParentID != nil && *v.ParentID != 0 {
+ if *v.ParentID == v.ID {
+ return errors.New("不能将自身设为父级")
+ }
+ var parent dictionaryDetailPO
+ if err := r.data.gormDB.WithContext(ctx).First(&parent, *v.ParentID).Error; err != nil {
+ return err
+ }
+ if strings.Contains(","+parent.Path+",", fmt.Sprintf(",%d,", v.ID)) {
+ return errors.New("不能移动到自己的子级")
+ }
+ level = parent.Level + 1
+ path = strings.Trim(strings.Join([]string{parent.Path, fmt.Sprint(parent.ID)}, ","), ",")
+ }
+ oldPrefix := strings.Trim(strings.Join([]string{po.Path, fmt.Sprint(po.ID)}, ","), ",")
+ newPrefix := strings.Trim(strings.Join([]string{path, fmt.Sprint(po.ID)}, ","), ",")
+ return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ if err := tx.Model(&dictionaryDetailPO{}).Where("id = ?", v.ID).Updates(map[string]any{"label": v.Label, "value": v.Value, "extend": v.Extend, "status": v.Status, "sort": v.Sort, "sys_dictionary_id": v.DictionaryID, "parent_id": v.ParentID, "level": level, "path": path}).Error; err != nil {
+ return err
+ }
+ return tx.Model(&dictionaryDetailPO{}).Where("path = ? OR path LIKE ?", oldPrefix, oldPrefix+",%").Update("path", gorm.Expr("REPLACE(path, ?, ?)", oldPrefix, newPrefix)).Error
+ })
+}
+func (r *settingsRepo) DeleteDictionaryDetail(ctx context.Context, id uint) error {
+ var count int64
+ if err := r.data.gormDB.WithContext(ctx).Model(&dictionaryDetailPO{}).Where("parent_id = ?", id).Count(&count).Error; err != nil {
+ return err
+ }
+ if count > 0 {
+ return errors.New("存在子级不可删除")
+ }
+ return r.data.gormDB.WithContext(ctx).Delete(&dictionaryDetailPO{}, id).Error
+}
+func (r *settingsRepo) FindDictionaryDetail(ctx context.Context, id uint) (*biz.DictionaryDetail, error) {
+ var po dictionaryDetailPO
+ if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil {
+ return nil, err
+ }
+ 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
+ }
+ db := r.data.gormDB.WithContext(ctx).Model(&dictionaryDetailPO{})
+ if dictionaryID != 0 {
+ db = db.Where("sys_dictionary_id = ?", dictionaryID)
+ }
+ if label != "" {
+ db = db.Where("label LIKE ?", "%"+label+"%")
+ }
+ if value != "" {
+ db = db.Where("value LIKE ?", "%"+value+"%")
+ }
+ 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 {
+ return nil, 0, err
+ }
+ out := make([]*biz.DictionaryDetail, 0, len(pos))
+ for _, po := range pos {
+ out = append(out, detailFromPO(po))
+ }
+ return out, total, nil
+}
+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)
+ if err != nil {
+ return nil, err
+ }
+ dictionaryID = dictionary.ID
+ }
+ items, _, err := r.ListDictionaryDetails(ctx, 1, 100000, dictionaryID, "", "")
+ if err != nil {
+ return nil, err
+ }
+ byID := map[uint]*biz.DictionaryDetail{}
+ for _, item := range items {
+ item.Children = []*biz.DictionaryDetail{}
+ byID[item.ID] = item
+ }
+ roots := []*biz.DictionaryDetail{}
+ for _, item := range items {
+ if item.ParentID != nil && byID[*item.ParentID] != nil {
+ byID[*item.ParentID].Children = append(byID[*item.ParentID].Children, item)
+ } else {
+ roots = append(roots, item)
+ }
+ }
+ return roots, nil
+}
diff --git a/internal/data/error_record.go b/internal/data/error_record.go
new file mode 100644
index 0000000..293405b
--- /dev/null
+++ b/internal/data/error_record.go
@@ -0,0 +1,82 @@
+package data
+
+import (
+ "context"
+ "time"
+
+ "kra/internal/biz"
+
+ "gorm.io/gorm"
+)
+
+type errorRecordPO struct {
+ ID uint `gorm:"primaryKey"`
+ CreatedAt time.Time
+ UpdatedAt time.Time
+ DeletedAt gorm.DeletedAt `gorm:"index"`
+ Form string `gorm:"type:text"`
+ Info string `gorm:"type:text"`
+ Level string
+ RequestID string `gorm:"index"`
+ TraceID string `gorm:"index"`
+ Solution string `gorm:"type:text"`
+ Status string `gorm:"default:未处理"`
+}
+
+func (errorRecordPO) TableName() string { return "sys_error" }
+
+func errorFromPO(v errorRecordPO) *biz.ErrorRecord {
+ return &biz.ErrorRecord{ID: v.ID, CreatedAt: v.CreatedAt, Form: v.Form, Info: v.Info, Level: v.Level, RequestID: v.RequestID, TraceID: v.TraceID, Solution: v.Solution, Status: v.Status}
+}
+func (r *auditRepo) CreateError(ctx context.Context, v *biz.ErrorRecord) error {
+ if v.Status == "" {
+ v.Status = "未处理"
+ }
+ return r.data.gormDB.WithContext(ctx).Create(&errorRecordPO{Form: v.Form, Info: v.Info, Level: v.Level, RequestID: v.RequestID, TraceID: v.TraceID, Solution: v.Solution, Status: v.Status}).Error
+}
+func (r *auditRepo) UpdateError(ctx context.Context, v *biz.ErrorRecord) error {
+ return r.data.gormDB.WithContext(ctx).Model(&errorRecordPO{}).Where("id = ?", v.ID).Updates(map[string]any{"form": v.Form, "info": v.Info, "level": v.Level, "solution": v.Solution, "status": v.Status}).Error
+}
+func (r *auditRepo) DeleteErrors(ctx context.Context, ids []uint) error {
+ return r.data.gormDB.WithContext(ctx).Delete(&errorRecordPO{}, ids).Error
+}
+func (r *auditRepo) FindError(ctx context.Context, id uint) (*biz.ErrorRecord, error) {
+ var po errorRecordPO
+ if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil {
+ return nil, err
+ }
+ 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 q.Form != "" {
+ db = db.Where("form LIKE ?", "%"+q.Form+"%")
+ }
+ if q.Level != "" {
+ db = db.Where("level = ?", q.Level)
+ }
+ if q.Status != "" {
+ db = db.Where("status = ?", q.Status)
+ }
+ }
+ var total int64
+ if err := db.Count(&total).Error; err != nil {
+ return nil, 0, err
+ }
+ var pos []errorRecordPO
+ if err := db.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
+ return nil, 0, err
+ }
+ out := make([]*biz.ErrorRecord, 0, len(pos))
+ for _, po := range pos {
+ out = append(out, errorFromPO(po))
+ }
+ return out, total, nil
+}
diff --git a/internal/data/file_storage.go b/internal/data/file_storage.go
index 792f6b2..1e40f55 100644
--- a/internal/data/file_storage.go
+++ b/internal/data/file_storage.go
@@ -2,25 +2,14 @@ package data
import (
"context"
- "crypto/md5"
- "encoding/hex"
- "fmt"
"io"
- "os"
- "path/filepath"
- "sort"
- "strings"
"sync"
"kra/internal/biz"
"kra/internal/conf"
+ datastorage "kra/internal/data/storage"
)
-type fileStorage struct {
- root string
- urlPrefix string
-}
-
type reloadableStorage struct {
mu sync.RWMutex
current biz.FileStorage
@@ -37,49 +26,8 @@ func NewFileStorage(data *Data) (biz.FileStorage, error) {
}
func buildFileStorage(config *conf.AdminBackend) (biz.FileStorage, error) {
- storageType := "local"
- if config != nil && config.Storage != nil && config.Storage.Type != "" {
- storageType = strings.ToLower(config.Storage.Type)
- }
- if storageType == "qiniu" {
- return newQiniuStorage(config.Storage.Qiniu)
- }
- if storageType != "local" {
- var object *conf.AdminBackend_ObjectStore
- switch storageType {
- case "aliyun-oss":
- return newAliyunStorage(config.Storage.AliyunOss)
- case "huawei-obs":
- return newHuaweiStorage(config.Storage.HuaweiObs)
- case "tencent-cos":
- return newTencentStorage(config.Storage.TencentCos)
- case "aws-s3":
- return newAWSStorage(storageType, config.Storage.AwsS3)
- case "cloudflare-r2":
- return newAWSStorage(storageType, config.Storage.CloudflareR2)
- case "minio":
- object = config.Storage.Minio
- default:
- return nil, fmt.Errorf("unsupported storage type %q", storageType)
- }
- return newS3Storage(storageType, object)
- }
- root, prefix := "uploads/file", "uploads/file"
- if config != nil && config.Local != nil {
- if config.Local.StorePath != "" {
- root = config.Local.StorePath
- }
- if config.Local.PathPrefix != "" {
- prefix = config.Local.PathPrefix
- }
- }
- root = filepath.Clean(root)
- if err := os.MkdirAll(root, 0o755); err != nil {
- return nil, fmt.Errorf("create upload directory: %w", err)
- }
- return &fileStorage{root: root, urlPrefix: "/" + strings.Trim(prefix, "/")}, nil
+ return datastorage.New(config)
}
-
func (s *reloadableStorage) replace(storage biz.FileStorage) {
s.mu.Lock()
s.current = storage
@@ -115,157 +63,3 @@ func (s *reloadableStorage) List(ctx context.Context, prefix, cursor string, lim
defer s.mu.RUnlock()
return s.current.List(ctx, prefix, cursor, limit)
}
-
-func composeFiles(ctx context.Context, storage biz.FileStorage, names []string, destination string) (*biz.StoredFile, string, error) {
- reader, writer := io.Pipe()
- hash := md5.New()
- errCh := make(chan error, 1)
- go func() {
- defer writer.Close()
- for _, name := range names {
- if ctx.Err() != nil {
- errCh <- ctx.Err()
- return
- }
- file, err := storage.Open(ctx, name)
- if err != nil {
- errCh <- err
- return
- }
- _, err = io.Copy(io.MultiWriter(writer, hash), file)
- _ = file.Close()
- if err != nil {
- errCh <- err
- return
- }
- }
- errCh <- nil
- }()
- stored, err := storage.Put(ctx, destination, reader)
- composeErr := <-errCh
- if err != nil {
- return nil, "", err
- }
- if composeErr != nil {
- _ = storage.Delete(ctx, destination)
- return nil, "", composeErr
- }
- return stored, hex.EncodeToString(hash.Sum(nil)), nil
-}
-
-func (s *fileStorage) resolve(name string) (string, error) {
- clean := filepath.Clean(strings.TrimPrefix(name, "/"))
- if clean == "." || filepath.IsAbs(clean) || clean == ".." || strings.HasPrefix(clean, ".."+string(filepath.Separator)) {
- return "", fmt.Errorf("invalid storage path")
- }
- return filepath.Join(s.root, clean), nil
-}
-
-func (s *fileStorage) Put(ctx context.Context, name string, reader io.Reader) (*biz.StoredFile, error) {
- path, err := s.resolve(name)
- if err != nil {
- return nil, err
- }
- if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
- return nil, err
- }
- file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
- if err != nil {
- return nil, err
- }
- size, copyErr := io.Copy(file, reader)
- closeErr := file.Close()
- if copyErr != nil {
- return nil, copyErr
- }
- if closeErr != nil {
- return nil, closeErr
- }
- return &biz.StoredFile{Name: filepath.Base(name), Path: name, URL: s.urlPrefix + "/" + strings.TrimPrefix(filepath.ToSlash(name), "/"), Size: size}, nil
-}
-
-func (s *fileStorage) Open(ctx context.Context, name string) (io.ReadCloser, error) {
- path, err := s.resolve(name)
- if err != nil {
- return nil, err
- }
- return os.Open(path)
-}
-
-func (s *fileStorage) Delete(ctx context.Context, name string) error {
- path, err := s.resolve(name)
- if err != nil {
- return err
- }
- if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
- return err
- }
- return nil
-}
-
-func (s *fileStorage) Compose(ctx context.Context, names []string, destination string) (*biz.StoredFile, string, error) {
- return composeFiles(ctx, s, names, destination)
-}
-func (s *fileStorage) DeletePrefix(ctx context.Context, prefix string) error {
- path, err := s.resolve(strings.TrimSuffix(prefix, "/") + "/placeholder")
- if err != nil {
- return err
- }
- dir := filepath.Dir(path)
- if !strings.HasPrefix(dir, s.root+string(filepath.Separator)) {
- return fmt.Errorf("invalid storage path")
- }
- if err = os.RemoveAll(dir); err != nil {
- return err
- }
- return nil
-}
-func (s *fileStorage) List(ctx context.Context, prefix, cursor string, limit int) ([]*biz.StoredFile, string, bool, error) {
- if limit < 1 || limit > 1000 {
- limit = 100
- }
- root, err := s.resolve(strings.TrimSuffix(prefix, "/") + "/placeholder")
- if err != nil {
- return nil, "", false, err
- }
- walkRoot := filepath.Dir(root)
- items := []*biz.StoredFile{}
- err = filepath.WalkDir(walkRoot, func(path string, entry os.DirEntry, walkErr error) error {
- if walkErr != nil {
- if os.IsNotExist(walkErr) {
- return nil
- }
- return walkErr
- }
- if ctx.Err() != nil {
- return ctx.Err()
- }
- if entry.IsDir() || entry.Type()&os.ModeSymlink != 0 {
- return nil
- }
- relative, _ := filepath.Rel(s.root, path)
- relative = filepath.ToSlash(relative)
- if relative <= cursor {
- return nil
- }
- info, infoErr := entry.Info()
- if infoErr != nil {
- return infoErr
- }
- items = append(items, &biz.StoredFile{Name: entry.Name(), Path: relative, URL: s.urlPrefix + "/" + relative, Size: info.Size()})
- return nil
- })
- if err != nil {
- return nil, "", false, err
- }
- sort.Slice(items, func(i, j int) bool { return items[i].Path < items[j].Path })
- hasMore := len(items) > limit
- if hasMore {
- items = items[:limit]
- }
- next := ""
- if len(items) > 0 {
- next = items[len(items)-1].Path
- }
- return items, next, hasMore, nil
-}
diff --git a/internal/data/log_file.go b/internal/data/log_file.go
new file mode 100644
index 0000000..1a6cfb4
--- /dev/null
+++ b/internal/data/log_file.go
@@ -0,0 +1,166 @@
+package data
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "io/fs"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+ "time"
+
+ "kra/internal/biz"
+)
+
+func logRoot() (string, error) {
+ root, err := filepath.Abs("logs")
+ if err != nil {
+ return "", err
+ }
+ info, err := os.Stat(root)
+ if errors.Is(err, fs.ErrNotExist) {
+ return root, nil
+ }
+ if err != nil {
+ return "", err
+ }
+ if !info.IsDir() {
+ return "", errors.New("日志路径不是目录")
+ }
+ return root, nil
+}
+func (r *auditRepo) LogDates(ctx context.Context, month string) ([]biz.LogDate, error) {
+ if parsed, err := time.Parse("2006-01", month); err != nil || parsed.Format("2006-01") != month {
+ return nil, errors.New("日志月份格式不正确")
+ }
+ root, err := logRoot()
+ if err != nil {
+ return nil, err
+ }
+ entries, err := os.ReadDir(root)
+ if errors.Is(err, fs.ErrNotExist) {
+ return []biz.LogDate{}, nil
+ }
+ if err != nil {
+ return nil, err
+ }
+ out := []biz.LogDate{}
+ for _, entry := range entries {
+ if ctx.Err() != nil {
+ return nil, ctx.Err()
+ }
+ if entry.Type()&os.ModeSymlink != 0 || !entry.IsDir() || !strings.HasPrefix(entry.Name(), month+"-") {
+ continue
+ }
+ if parsed, parseErr := time.Parse("2006-01-02", entry.Name()); parseErr != nil || parsed.Format("2006-01-02") != entry.Name() {
+ continue
+ }
+ count := 0
+ _ = filepath.WalkDir(filepath.Join(root, entry.Name()), func(_ string, item fs.DirEntry, walkErr error) error {
+ if walkErr == nil && !item.IsDir() && strings.EqualFold(filepath.Ext(item.Name()), ".log") {
+ count++
+ }
+ return nil
+ })
+ if count > 0 {
+ out = append(out, biz.LogDate{Date: entry.Name(), FileCount: count})
+ }
+ }
+ sort.Slice(out, func(i, j int) bool { return out[i].Date < out[j].Date })
+ return out, nil
+}
+func (r *auditRepo) LogFiles(ctx context.Context, date string) ([]biz.LogFile, error) {
+ if parsed, err := time.Parse("2006-01-02", date); err != nil || parsed.Format("2006-01-02") != date {
+ return nil, errors.New("日志日期格式不正确")
+ }
+ root, err := logRoot()
+ if err != nil {
+ return nil, err
+ }
+ dateRoot := filepath.Join(root, date)
+ out := []biz.LogFile{}
+ err = filepath.WalkDir(dateRoot, func(path string, entry fs.DirEntry, walkErr error) error {
+ if walkErr != nil {
+ if errors.Is(walkErr, fs.ErrNotExist) {
+ return nil
+ }
+ return walkErr
+ }
+ if ctx.Err() != nil {
+ return ctx.Err()
+ }
+ if entry.Type()&os.ModeSymlink != 0 {
+ if entry.IsDir() {
+ return filepath.SkipDir
+ }
+ return nil
+ }
+ if entry.IsDir() || !strings.EqualFold(filepath.Ext(entry.Name()), ".log") {
+ return nil
+ }
+ info, infoErr := entry.Info()
+ if infoErr != nil {
+ return infoErr
+ }
+ relative, _ := filepath.Rel(dateRoot, path)
+ out = append(out, biz.LogFile{Path: filepath.ToSlash(relative), Name: entry.Name(), Size: info.Size(), ModifiedAt: info.ModTime()})
+ return nil
+ })
+ sort.Slice(out, func(i, j int) bool { return out[i].Path < out[j].Path })
+ return out, err
+}
+func (r *auditRepo) LogContent(ctx context.Context, date, path string, cursor *int64) (*biz.LogContent, error) {
+ if _, err := time.Parse("2006-01-02", date); err != nil {
+ return nil, errors.New("日志日期格式不正确")
+ }
+ root, err := logRoot()
+ if err != nil {
+ return nil, err
+ }
+ dateRoot := filepath.Join(root, date)
+ target := filepath.Clean(filepath.Join(dateRoot, filepath.FromSlash(path)))
+ if target == dateRoot || !strings.HasPrefix(target, dateRoot+string(os.PathSeparator)) || !strings.EqualFold(filepath.Ext(target), ".log") {
+ return nil, errors.New("日志文件路径不合法")
+ }
+ info, err := os.Lstat(target)
+ if err != nil {
+ return nil, err
+ }
+ if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() {
+ return nil, errors.New("日志文件路径不合法")
+ }
+ end := info.Size()
+ if cursor != nil && *cursor >= 0 && *cursor < end {
+ end = *cursor
+ }
+ start := end - int64(2*1024*1024)
+ limited := start > 0
+ if start < 0 {
+ start = 0
+ }
+ file, err := os.Open(target)
+ if err != nil {
+ return nil, err
+ }
+ defer file.Close()
+ data := make([]byte, end-start)
+ n, err := file.ReadAt(data, start)
+ if err != nil && n == 0 {
+ return nil, err
+ }
+ data = data[:n]
+ lines := bytes.Split(data, []byte("\n"))
+ if len(lines) > 501 {
+ drop := len(lines) - 501
+ offset := 0
+ for _, line := range lines[:drop] {
+ offset += len(line) + 1
+ }
+ start += int64(offset)
+ data = data[offset:]
+ limited = true
+ }
+ return &biz.LogContent{Date: date, Path: path, Content: string(data), LineCount: bytes.Count(data, []byte("\n")), NextCursor: start, HasMore: start > 0, LimitedByBytes: limited, Size: info.Size(), ModifiedAt: info.ModTime()}, nil
+}
diff --git a/internal/data/login_log.go b/internal/data/login_log.go
new file mode 100644
index 0000000..e2a567a
--- /dev/null
+++ b/internal/data/login_log.go
@@ -0,0 +1,70 @@
+package data
+
+import (
+ "context"
+ "time"
+
+ "kra/internal/biz"
+
+ "gorm.io/gorm"
+)
+
+type loginLogPO struct {
+ ID uint `gorm:"primaryKey"`
+ CreatedAt time.Time
+ UpdatedAt time.Time
+ DeletedAt gorm.DeletedAt `gorm:"index"`
+ Username, IP string
+ Status bool
+ ErrorMessage, Agent string
+ UserID uint
+}
+
+func (loginLogPO) TableName() string { return "sys_login_logs" }
+
+func (r *auditRepo) RecordLogin(ctx context.Context, v *biz.LoginLog) error {
+ return r.data.gormDB.WithContext(ctx).Create(&loginLogPO{Username: v.Username, IP: v.IP, Status: v.Status, ErrorMessage: v.ErrorMessage, Agent: v.Agent, UserID: v.UserID}).Error
+}
+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+"%")
+ }
+ }
+ var total int64
+ if err := db.Count(&total).Error; err != nil {
+ return nil, 0, err
+ }
+ var pos []loginLogPO
+ if err := db.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
+ return nil, 0, err
+ }
+ out := make([]*biz.LoginLog, 0, len(pos))
+ for _, po := range pos {
+ out = append(out, loginFromPO(po))
+ }
+ return out, total, nil
+}
+func (r *auditRepo) FindLogin(ctx context.Context, id uint) (*biz.LoginLog, error) {
+ var po loginLogPO
+ if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil {
+ return nil, err
+ }
+ return loginFromPO(po), nil
+}
+func (r *auditRepo) DeleteLogins(ctx context.Context, ids []uint) error {
+ return r.data.gormDB.WithContext(ctx).Delete(&loginLogPO{}, ids).Error
+}
diff --git a/internal/data/media.go b/internal/data/media.go
index f78917f..73b92bc 100644
--- a/internal/data/media.go
+++ b/internal/data/media.go
@@ -3,10 +3,11 @@ package data
import (
"context"
"errors"
- "gorm.io/gorm"
- "gorm.io/gorm/clause"
- "kra/internal/biz"
"time"
+
+ "kra/internal/biz"
+
+ "gorm.io/gorm"
)
type mediaPO struct {
@@ -38,37 +39,6 @@ type categoryPO struct {
func (categoryPO) TableName() string { return "media_attachment_category" }
-type uploadSessionPO struct {
- ID uint `gorm:"primaryKey"`
- CreatedAt time.Time
- UpdatedAt time.Time
- DeletedAt gorm.DeletedAt `gorm:"index"`
- UserID uint `gorm:"index"`
- FileName string
- FileHash string `gorm:"index"`
- FileSize int64
- ChunkSize int64
- ChunkTotal int
- Status string
- StorageKey string
- MediaID uint
-}
-
-func (uploadSessionPO) TableName() string { return "media_uploads" }
-
-type uploadChunkPO struct {
- ID uint `gorm:"primaryKey"`
- CreatedAt time.Time
- UpdatedAt time.Time
- DeletedAt gorm.DeletedAt `gorm:"index"`
- UploadID uint `gorm:"uniqueIndex:idx_upload_chunk"`
- ChunkIndex int `gorm:"uniqueIndex:idx_upload_chunk"`
- ChunkHash string
- Size int64
-}
-
-func (uploadChunkPO) TableName() string { return "media_upload_chunks" }
-
type mediaRepo struct{ data *Data }
func NewMediaRepo(data *Data) biz.MediaRepo { return &mediaRepo{data: data} }
@@ -209,65 +179,3 @@ func (r *mediaRepo) ListCategories(ctx context.Context) ([]*biz.AttachmentCatego
}
return roots, nil
}
-func uploadFromPO(v uploadSessionPO) *biz.UploadSession {
- return &biz.UploadSession{ID: v.ID, UserID: v.UserID, FileName: v.FileName, FileHash: v.FileHash, FileSize: v.FileSize, ChunkSize: v.ChunkSize, ChunkTotal: v.ChunkTotal, Status: v.Status, StorageKey: v.StorageKey, MediaID: v.MediaID}
-}
-func (r *mediaRepo) FindUploadingSession(ctx context.Context, userID uint, hash string) (*biz.UploadSession, error) {
- var po uploadSessionPO
- if err := r.data.gormDB.WithContext(ctx).Where("user_id = ? AND file_hash = ? AND status = ?", userID, hash, "uploading").First(&po).Error; err != nil {
- return nil, err
- }
- return uploadFromPO(po), nil
-}
-func (r *mediaRepo) CreateUploadSession(ctx context.Context, v *biz.UploadSession) error {
- po := uploadSessionPO{UserID: v.UserID, FileName: v.FileName, FileHash: v.FileHash, FileSize: v.FileSize, ChunkSize: v.ChunkSize, ChunkTotal: v.ChunkTotal, Status: v.Status}
- if err := r.data.gormDB.WithContext(ctx).Create(&po).Error; err != nil {
- return err
- }
- v.ID = po.ID
- return nil
-}
-func (r *mediaRepo) FindUploadSession(ctx context.Context, id uint) (*biz.UploadSession, error) {
- var po uploadSessionPO
- if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil {
- return nil, err
- }
- return uploadFromPO(po), nil
-}
-func (r *mediaRepo) CompleteUploadSession(ctx context.Context, id uint, key string, mediaID uint) error {
- return r.data.gormDB.WithContext(ctx).Model(&uploadSessionPO{}).Where("id = ?", id).Updates(map[string]any{"status": "completed", "storage_key": key, "media_id": mediaID}).Error
-}
-func (r *mediaRepo) DeleteUploadSession(ctx context.Context, id uint) error {
- return r.data.gormDB.WithContext(ctx).Unscoped().Delete(&uploadSessionPO{}, id).Error
-}
-func (r *mediaRepo) UpsertChunk(ctx context.Context, uploadID uint, v *biz.UploadChunk) error {
- po := uploadChunkPO{UploadID: uploadID, ChunkIndex: v.Index, ChunkHash: v.Hash, Size: v.Size}
- return r.data.gormDB.WithContext(ctx).Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "upload_id"}, {Name: "chunk_index"}}, DoUpdates: clause.AssignmentColumns([]string{"chunk_hash", "size", "updated_at"})}).Create(&po).Error
-}
-func (r *mediaRepo) ListChunks(ctx context.Context, uploadID uint) ([]*biz.UploadChunk, error) {
- var pos []uploadChunkPO
- if err := r.data.gormDB.WithContext(ctx).Where("upload_id = ?", uploadID).Order("chunk_index").Find(&pos).Error; err != nil {
- return nil, err
- }
- out := make([]*biz.UploadChunk, 0, len(pos))
- for _, po := range pos {
- out = append(out, &biz.UploadChunk{Index: po.ChunkIndex, Hash: po.ChunkHash, Size: po.Size})
- }
- return out, nil
-}
-func (r *mediaRepo) DeleteChunks(ctx context.Context, uploadID uint) error {
- return r.data.gormDB.WithContext(ctx).Unscoped().Where("upload_id = ?", uploadID).Delete(&uploadChunkPO{}).Error
-}
-func (r *mediaRepo) StaleUploadSessionIDs(ctx context.Context, before time.Time) ([]uint, error) {
- var ids []uint
- err := r.data.gormDB.WithContext(ctx).Model(&uploadSessionPO{}).Where("status = ? AND updated_at < ?", "uploading", before).Pluck("id", &ids).Error
- return ids, err
-}
-func (r *mediaRepo) DeleteUploadData(ctx context.Context, uploadID uint) error {
- return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
- if err := tx.Unscoped().Where("upload_id = ?", uploadID).Delete(&uploadChunkPO{}).Error; err != nil {
- return err
- }
- return tx.Unscoped().Delete(&uploadSessionPO{}, uploadID).Error
- })
-}
diff --git a/internal/data/media_upload.go b/internal/data/media_upload.go
new file mode 100644
index 0000000..6717e85
--- /dev/null
+++ b/internal/data/media_upload.go
@@ -0,0 +1,105 @@
+package data
+
+import (
+ "context"
+ "time"
+
+ "kra/internal/biz"
+
+ "gorm.io/gorm"
+ "gorm.io/gorm/clause"
+)
+
+type uploadSessionPO struct {
+ ID uint `gorm:"primaryKey"`
+ CreatedAt time.Time
+ UpdatedAt time.Time
+ DeletedAt gorm.DeletedAt `gorm:"index"`
+ UserID uint `gorm:"index"`
+ FileName string
+ FileHash string `gorm:"index"`
+ FileSize int64
+ ChunkSize int64
+ ChunkTotal int
+ Status string
+ StorageKey string
+ MediaID uint
+}
+
+func (uploadSessionPO) TableName() string { return "media_uploads" }
+
+type uploadChunkPO struct {
+ ID uint `gorm:"primaryKey"`
+ CreatedAt time.Time
+ UpdatedAt time.Time
+ DeletedAt gorm.DeletedAt `gorm:"index"`
+ UploadID uint `gorm:"uniqueIndex:idx_upload_chunk"`
+ ChunkIndex int `gorm:"uniqueIndex:idx_upload_chunk"`
+ ChunkHash string
+ Size int64
+}
+
+func (uploadChunkPO) TableName() string { return "media_upload_chunks" }
+
+func uploadFromPO(v uploadSessionPO) *biz.UploadSession {
+ return &biz.UploadSession{ID: v.ID, UserID: v.UserID, FileName: v.FileName, FileHash: v.FileHash, FileSize: v.FileSize, ChunkSize: v.ChunkSize, ChunkTotal: v.ChunkTotal, Status: v.Status, StorageKey: v.StorageKey, MediaID: v.MediaID}
+}
+func (r *mediaRepo) FindUploadingSession(ctx context.Context, userID uint, hash string) (*biz.UploadSession, error) {
+ var po uploadSessionPO
+ if err := r.data.gormDB.WithContext(ctx).Where("user_id = ? AND file_hash = ? AND status = ?", userID, hash, "uploading").First(&po).Error; err != nil {
+ return nil, err
+ }
+ return uploadFromPO(po), nil
+}
+func (r *mediaRepo) CreateUploadSession(ctx context.Context, v *biz.UploadSession) error {
+ po := uploadSessionPO{UserID: v.UserID, FileName: v.FileName, FileHash: v.FileHash, FileSize: v.FileSize, ChunkSize: v.ChunkSize, ChunkTotal: v.ChunkTotal, Status: v.Status}
+ if err := r.data.gormDB.WithContext(ctx).Create(&po).Error; err != nil {
+ return err
+ }
+ v.ID = po.ID
+ return nil
+}
+func (r *mediaRepo) FindUploadSession(ctx context.Context, id uint) (*biz.UploadSession, error) {
+ var po uploadSessionPO
+ if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil {
+ return nil, err
+ }
+ return uploadFromPO(po), nil
+}
+func (r *mediaRepo) CompleteUploadSession(ctx context.Context, id uint, key string, mediaID uint) error {
+ return r.data.gormDB.WithContext(ctx).Model(&uploadSessionPO{}).Where("id = ?", id).Updates(map[string]any{"status": "completed", "storage_key": key, "media_id": mediaID}).Error
+}
+func (r *mediaRepo) DeleteUploadSession(ctx context.Context, id uint) error {
+ return r.data.gormDB.WithContext(ctx).Unscoped().Delete(&uploadSessionPO{}, id).Error
+}
+func (r *mediaRepo) UpsertChunk(ctx context.Context, uploadID uint, v *biz.UploadChunk) error {
+ po := uploadChunkPO{UploadID: uploadID, ChunkIndex: v.Index, ChunkHash: v.Hash, Size: v.Size}
+ return r.data.gormDB.WithContext(ctx).Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "upload_id"}, {Name: "chunk_index"}}, DoUpdates: clause.AssignmentColumns([]string{"chunk_hash", "size", "updated_at"})}).Create(&po).Error
+}
+func (r *mediaRepo) ListChunks(ctx context.Context, uploadID uint) ([]*biz.UploadChunk, error) {
+ var pos []uploadChunkPO
+ if err := r.data.gormDB.WithContext(ctx).Where("upload_id = ?", uploadID).Order("chunk_index").Find(&pos).Error; err != nil {
+ return nil, err
+ }
+ out := make([]*biz.UploadChunk, 0, len(pos))
+ for _, po := range pos {
+ out = append(out, &biz.UploadChunk{Index: po.ChunkIndex, Hash: po.ChunkHash, Size: po.Size})
+ }
+ return out, nil
+}
+func (r *mediaRepo) DeleteChunks(ctx context.Context, uploadID uint) error {
+ return r.data.gormDB.WithContext(ctx).Unscoped().Where("upload_id = ?", uploadID).Delete(&uploadChunkPO{}).Error
+}
+func (r *mediaRepo) StaleUploadSessionIDs(ctx context.Context, before time.Time) ([]uint, error) {
+ var ids []uint
+ err := r.data.gormDB.WithContext(ctx).Model(&uploadSessionPO{}).Where("status = ? AND updated_at < ?", "uploading", before).Pluck("id", &ids).Error
+ return ids, err
+}
+func (r *mediaRepo) DeleteUploadData(ctx context.Context, uploadID uint) error {
+ return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ if err := tx.Unscoped().Where("upload_id = ?", uploadID).Delete(&uploadChunkPO{}).Error; err != nil {
+ return err
+ }
+ return tx.Unscoped().Delete(&uploadSessionPO{}, uploadID).Error
+ })
+}
diff --git a/internal/data/operation_log.go b/internal/data/operation_log.go
new file mode 100644
index 0000000..173f4ce
--- /dev/null
+++ b/internal/data/operation_log.go
@@ -0,0 +1,86 @@
+package data
+
+import (
+ "context"
+ "time"
+
+ "kra/internal/biz"
+
+ "gorm.io/gorm"
+)
+
+type operationPO struct {
+ ID uint `gorm:"primaryKey"`
+ CreatedAt time.Time
+ UpdatedAt time.Time
+ DeletedAt gorm.DeletedAt `gorm:"index"`
+ IP, Method, Path string
+ Status int
+ LatencyMS int64 `gorm:"column:latency_ms"`
+ Agent string `gorm:"type:text"`
+ ErrorMessage string
+ Body string `gorm:"type:text"`
+ Response string `gorm:"column:resp;type:text"`
+ UserID uint
+ RequestID string `gorm:"index"`
+ TraceID string `gorm:"index"`
+ DeviceID string
+}
+
+func (operationPO) TableName() string { return "sys_operation_records" }
+
+func (r *auditRepo) RecordOperation(ctx context.Context, v *biz.OperationRecord) error {
+ return r.data.gormDB.WithContext(ctx).Create(&operationPO{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}).Error
+}
+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 != "" {
+ db = db.Where("path LIKE ?", "%"+q.Path+"%")
+ }
+ if q.Method != "" {
+ db = db.Where("method = ?", q.Method)
+ }
+ 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 {
+ return nil, 0, err
+ }
+ out := make([]*biz.OperationRecord, 0, len(pos))
+ for _, po := range pos {
+ out = append(out, opFromPO(po))
+ }
+ return out, total, nil
+}
+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 {
+ return nil, err
+ }
+ return opFromPO(po), nil
+}
+func (r *auditRepo) DeleteOperations(ctx context.Context, ids []uint) error {
+ return r.data.gormDB.WithContext(ctx).Delete(&operationPO{}, ids).Error
+}
diff --git a/internal/data/organization.go b/internal/data/organization.go
new file mode 100644
index 0000000..5165fe9
--- /dev/null
+++ b/internal/data/organization.go
@@ -0,0 +1,7 @@
+package data
+
+import "kra/internal/biz"
+
+type organizationRepo struct{ data *Data }
+
+func NewOrganizationRepo(data *Data) biz.OrganizationRepo { return &organizationRepo{data: data} }
diff --git a/internal/data/parameter.go b/internal/data/parameter.go
new file mode 100644
index 0000000..ed972af
--- /dev/null
+++ b/internal/data/parameter.go
@@ -0,0 +1,83 @@
+package data
+
+import (
+ "context"
+ "time"
+
+ "kra/internal/biz"
+
+ "gorm.io/gorm"
+ "gorm.io/gorm/clause"
+)
+
+type parameterPO struct {
+ ID uint `gorm:"primaryKey"`
+ CreatedAt time.Time
+ UpdatedAt time.Time
+ DeletedAt gorm.DeletedAt `gorm:"index"`
+ Name string
+ Key string `gorm:"uniqueIndex"`
+ Value string
+ Desc string
+}
+
+func (parameterPO) TableName() string { return "sys_params" }
+
+func (r *settingsRepo) CreateParameter(ctx context.Context, v *biz.SystemParameter) error {
+ po := parameterPO{Name: v.Name, Key: v.Key, Value: v.Value, Desc: v.Desc}
+ if err := r.data.gormDB.WithContext(ctx).Create(&po).Error; err != nil {
+ return err
+ }
+ v.ID = po.ID
+ return nil
+}
+func (r *settingsRepo) UpdateParameter(ctx context.Context, v *biz.SystemParameter) error {
+ return r.data.gormDB.WithContext(ctx).Model(¶meterPO{}).Where("id = ?", v.ID).Updates(map[string]any{"name": v.Name, "key": v.Key, "value": v.Value, "desc": v.Desc}).Error
+}
+func (r *settingsRepo) DeleteParameters(ctx context.Context, ids []uint) error {
+ return r.data.gormDB.WithContext(ctx).Delete(¶meterPO{}, ids).Error
+}
+func (r *settingsRepo) FindParameter(ctx context.Context, id uint, key string) (*biz.SystemParameter, error) {
+ var po parameterPO
+ db := r.data.gormDB.WithContext(ctx)
+ var err error
+ if id != 0 {
+ err = db.First(&po, id).Error
+ } else {
+ err = db.Where(map[string]any{"key": key}).First(&po).Error
+ }
+ if err != nil {
+ return nil, err
+ }
+ 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.Name != "" {
+ db = db.Where("name LIKE ?", "%"+q.Name+"%")
+ }
+ if q.Key != "" {
+ db = db.Where(clause.Like{Column: clause.Column{Name: "key"}, Value: "%" + q.Key + "%"})
+ }
+ }
+ var total int64
+ if err := db.Count(&total).Error; err != nil {
+ return nil, 0, err
+ }
+ var pos []parameterPO
+ if err := db.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
+ return nil, 0, err
+ }
+ out := make([]*biz.SystemParameter, 0, len(pos))
+ for _, po := range pos {
+ out = append(out, parameterFromPO(po))
+ }
+ return out, total, nil
+}
diff --git a/internal/data/permission.go b/internal/data/permission.go
new file mode 100644
index 0000000..d324ca8
--- /dev/null
+++ b/internal/data/permission.go
@@ -0,0 +1,85 @@
+package data
+
+import (
+ "context"
+
+ "kra/internal/biz"
+
+ "gorm.io/gorm"
+)
+
+type menuButtonPO struct {
+ ID uint `gorm:"primaryKey"`
+ Name string
+ Description string `gorm:"column:desc"`
+ MenuID uint `gorm:"column:sys_base_menu_id"`
+}
+
+func (menuButtonPO) TableName() string { return "sys_base_menu_btns" }
+
+type authorityButtonPO struct {
+ AuthorityID uint `gorm:"primaryKey;column:authority_id"`
+ MenuID uint `gorm:"primaryKey;column:sys_menu_id"`
+ ButtonID uint `gorm:"primaryKey;column:sys_base_menu_btn_id"`
+}
+
+func (authorityButtonPO) TableName() string { return "sys_authority_btns" }
+
+func (r *accessRepo) Buttons(ctx context.Context, menuID uint) ([]*biz.MenuButton, error) {
+ var pos []menuButtonPO
+ if err := r.data.gormDB.WithContext(ctx).Where("sys_base_menu_id = ?", menuID).Find(&pos).Error; err != nil {
+ return nil, err
+ }
+ out := make([]*biz.MenuButton, 0, len(pos))
+ for _, po := range pos {
+ out = append(out, &biz.MenuButton{ID: po.ID, Name: po.Name, Description: po.Description, MenuID: po.MenuID})
+ }
+ return out, nil
+}
+func (r *accessRepo) SetAuthorityButtons(ctx context.Context, aid uint, buttons map[uint][]uint) error {
+ return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ if err := tx.Where("authority_id = ?", aid).Delete(&authorityButtonPO{}).Error; err != nil {
+ return err
+ }
+ var links []authorityButtonPO
+ for menuID, ids := range buttons {
+ for _, id := range ids {
+ links = append(links, authorityButtonPO{AuthorityID: aid, MenuID: menuID, ButtonID: id})
+ }
+ }
+ if len(links) > 0 {
+ return tx.Create(&links).Error
+ }
+ return nil
+ })
+}
+func (r *accessRepo) SelectedButtons(ctx context.Context, aid, menuID uint) ([]uint, error) {
+ var ids []uint
+ err := r.data.gormDB.WithContext(ctx).Model(&authorityButtonPO{}).Where("authority_id = ? AND sys_menu_id = ?", aid, menuID).Pluck("sys_base_menu_btn_id", &ids).Error
+ return ids, err
+}
+func (r *accessRepo) SetSelectedButtons(ctx context.Context, aid, menuID uint, ids []uint) error {
+ return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ if err := tx.Where("authority_id = ? AND sys_menu_id = ?", aid, menuID).Delete(&authorityButtonPO{}).Error; err != nil {
+ return err
+ }
+ links := make([]authorityButtonPO, 0, len(ids))
+ for _, id := range ids {
+ links = append(links, authorityButtonPO{AuthorityID: aid, MenuID: menuID, ButtonID: id})
+ }
+ if len(links) > 0 {
+ return tx.Create(&links).Error
+ }
+ return nil
+ })
+}
+func (r *accessRepo) CanRemoveButton(ctx context.Context, id uint) (bool, error) {
+ var count int64
+ err := r.data.gormDB.WithContext(ctx).Model(&authorityButtonPO{}).Where("sys_base_menu_btn_id = ?", id).Count(&count).Error
+ return count == 0, err
+}
+func (r *accessRepo) AuthorityButtonIDs(ctx context.Context, aid uint) ([]uint, error) {
+ var ids []uint
+ err := r.data.gormDB.WithContext(ctx).Model(&authorityButtonPO{}).Where("authority_id = ?", aid).Pluck("sys_base_menu_btn_id", &ids).Error
+ return ids, err
+}
diff --git a/internal/data/position.go b/internal/data/position.go
new file mode 100644
index 0000000..607e46b
--- /dev/null
+++ b/internal/data/position.go
@@ -0,0 +1,127 @@
+package data
+
+import (
+ "context"
+ "errors"
+ "time"
+
+ "kra/internal/biz"
+
+ "gorm.io/gorm"
+)
+
+type positionPO struct {
+ ID uint `gorm:"primaryKey"`
+ CreatedAt time.Time
+ UpdatedAt time.Time
+ DeletedAt gorm.DeletedAt `gorm:"index"`
+ Name string `gorm:"index"`
+ Code string
+ Sort int
+ Status bool `gorm:"default:true"`
+ Remark string
+}
+
+func (positionPO) TableName() string { return "sys_positions" }
+
+type userPositionPO struct {
+ UserID uint `gorm:"primaryKey;column:sys_user_id"`
+ PositionID uint `gorm:"primaryKey;column:sys_position_id"`
+}
+
+func (userPositionPO) TableName() string { return "sys_user_positions" }
+
+func posFromPO(po positionPO) *biz.Position {
+ return &biz.Position{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Name: po.Name, Code: po.Code, Sort: po.Sort, Status: po.Status, Remark: po.Remark}
+}
+func (r *organizationRepo) CreatePosition(ctx context.Context, v *biz.Position) error {
+ return r.data.gormDB.WithContext(ctx).Create(&positionPO{Name: v.Name, Code: v.Code, Sort: v.Sort, Status: v.Status, Remark: v.Remark}).Error
+}
+func (r *organizationRepo) UpdatePosition(ctx context.Context, v *biz.Position) error {
+ return r.data.gormDB.WithContext(ctx).Model(&positionPO{}).Where("id = ?", v.ID).Updates(map[string]any{"name": v.Name, "code": v.Code, "sort": v.Sort, "status": v.Status, "remark": v.Remark}).Error
+}
+func (r *organizationRepo) DeletePosition(ctx context.Context, id uint) error {
+ if id == 0 {
+ return errors.New("岗位ID不能为空")
+ }
+ var count int64
+ if err := r.data.gormDB.WithContext(ctx).Model(&userPositionPO{}).Where("sys_position_id = ?", id).Count(&count).Error; err != nil {
+ return err
+ }
+ if count > 0 {
+ return errors.New("该岗位下存在用户,不允许删除")
+ }
+ return r.data.gormDB.WithContext(ctx).Delete(&positionPO{}, id).Error
+}
+func (r *organizationRepo) FindPosition(ctx context.Context, id uint) (*biz.Position, error) {
+ var po positionPO
+ if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil {
+ return nil, err
+ }
+ 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
+ }
+ db := r.data.gormDB.WithContext(ctx).Model(&positionPO{})
+ if q != nil {
+ if q.Name != "" {
+ db = db.Where("name LIKE ?", "%"+q.Name+"%")
+ }
+ if q.Code != "" {
+ db = db.Where("code LIKE ?", "%"+q.Code+"%")
+ }
+ }
+ 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 {
+ return nil, 0, err
+ }
+ out := make([]*biz.Position, 0, len(pos))
+ for _, po := range pos {
+ out = append(out, posFromPO(po))
+ }
+ return out, total, nil
+}
+func (r *organizationRepo) PositionUserIDs(ctx context.Context, id uint) ([]uint, error) {
+ var ids []uint
+ err := r.data.gormDB.WithContext(ctx).Model(&userPositionPO{}).Where("sys_position_id = ?", id).Pluck("sys_user_id", &ids).Error
+ return ids, err
+}
+func (r *organizationRepo) SetPositionUsers(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_position_id = ?", id).Delete(&userPositionPO{}).Error; err != nil {
+ return err
+ }
+ links := make([]userPositionPO, 0, len(ids))
+ for _, uid := range ids {
+ links = append(links, userPositionPO{UserID: uid, PositionID: id})
+ }
+ if len(links) > 0 {
+ return tx.Create(&links).Error
+ }
+ return nil
+ })
+}
+func (r *organizationRepo) SetUserPositions(ctx context.Context, uid uint, ids []uint) error {
+ return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ if err := tx.Where("sys_user_id = ?", uid).Delete(&userPositionPO{}).Error; err != nil {
+ return err
+ }
+ links := make([]userPositionPO, 0, len(ids))
+ for _, id := range ids {
+ links = append(links, userPositionPO{UserID: uid, PositionID: id})
+ }
+ if len(links) > 0 {
+ return tx.Create(&links).Error
+ }
+ return nil
+ })
+}
diff --git a/internal/data/security.go b/internal/data/security.go
new file mode 100644
index 0000000..62da3ca
--- /dev/null
+++ b/internal/data/security.go
@@ -0,0 +1,74 @@
+package data
+
+import (
+ "context"
+ "errors"
+ "time"
+
+ "kra/internal/biz"
+
+ "gorm.io/gorm"
+)
+
+type securityConfigPO struct {
+ ID uint `gorm:"primaryKey"`
+ CaptchaOpen int
+ CaptchaTimeout int
+ KeyLong int
+ ImgWidth int
+ ImgHeight int
+ PwdMinLength int
+ PwdRequireUpper bool
+ PwdRequireLower bool
+ PwdRequireDigit bool
+ PwdRequireSpecial bool
+ LimitEnable bool
+ LimitWindow int
+ LimitCount int
+ LockEnable bool
+ LockThreshold int
+ LockDuration int
+ PwdExpireEnable bool
+ PwdExpireDays int
+ ForceNewUserChangePassword bool
+}
+
+func (securityConfigPO) TableName() string { return "sys_security_config" }
+
+func defaultSecurityConfig() securityConfigPO {
+ return securityConfigPO{ID: 1, CaptchaTimeout: 3600, KeyLong: 6, ImgWidth: 240, ImgHeight: 80, PwdMinLength: 8, LimitWindow: 60, LimitCount: 30, LockThreshold: 5, LockDuration: 30, PwdExpireDays: 90}
+}
+func securityFromPO(v securityConfigPO) *biz.SecurityConfig {
+ return &biz.SecurityConfig{ID: v.ID, CaptchaOpen: v.CaptchaOpen, CaptchaTimeout: v.CaptchaTimeout, KeyLong: v.KeyLong, ImgWidth: v.ImgWidth, ImgHeight: v.ImgHeight, PwdMinLength: v.PwdMinLength, PwdRequireUpper: v.PwdRequireUpper, PwdRequireLower: v.PwdRequireLower, PwdRequireDigit: v.PwdRequireDigit, PwdRequireSpecial: v.PwdRequireSpecial, LimitEnable: v.LimitEnable, LimitWindow: v.LimitWindow, LimitCount: v.LimitCount, LockEnable: v.LockEnable, LockThreshold: v.LockThreshold, LockDuration: v.LockDuration, PwdExpireEnable: v.PwdExpireEnable, PwdExpireDays: v.PwdExpireDays, ForceNewUserChangePassword: v.ForceNewUserChangePassword}
+}
+func securityToPO(v *biz.SecurityConfig) securityConfigPO {
+ return securityConfigPO{ID: 1, CaptchaOpen: v.CaptchaOpen, CaptchaTimeout: v.CaptchaTimeout, KeyLong: v.KeyLong, ImgWidth: v.ImgWidth, ImgHeight: v.ImgHeight, PwdMinLength: v.PwdMinLength, PwdRequireUpper: v.PwdRequireUpper, PwdRequireLower: v.PwdRequireLower, PwdRequireDigit: v.PwdRequireDigit, PwdRequireSpecial: v.PwdRequireSpecial, LimitEnable: v.LimitEnable, LimitWindow: v.LimitWindow, LimitCount: v.LimitCount, LockEnable: v.LockEnable, LockThreshold: v.LockThreshold, LockDuration: v.LockDuration, PwdExpireEnable: v.PwdExpireEnable, PwdExpireDays: v.PwdExpireDays, ForceNewUserChangePassword: v.ForceNewUserChangePassword}
+}
+func (r *settingsRepo) SecurityConfig(ctx context.Context) (*biz.SecurityConfig, error) {
+ var po securityConfigPO
+ err := r.data.gormDB.WithContext(ctx).First(&po, 1).Error
+ if errors.Is(err, gorm.ErrRecordNotFound) {
+ po = defaultSecurityConfig()
+ err = r.data.gormDB.WithContext(ctx).Create(&po).Error
+ }
+ if err != nil {
+ return nil, err
+ }
+ return securityFromPO(po), nil
+}
+func (r *settingsRepo) SaveSecurityConfig(ctx context.Context, v *biz.SecurityConfig) error {
+ previous, err := r.SecurityConfig(ctx)
+ if err != nil {
+ return err
+ }
+ po := securityToPO(v)
+ return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ if err := tx.Save(&po).Error; err != nil {
+ return err
+ }
+ if v.PwdExpireEnable && !previous.PwdExpireEnable {
+ return tx.Model(&userPO{}).Where("password_updated_at IS NULL").Update("password_updated_at", time.Now()).Error
+ }
+ return nil
+ })
+}
diff --git a/internal/data/settings.go b/internal/data/settings.go
index e84b4c2..9f128a0 100644
--- a/internal/data/settings.go
+++ b/internal/data/settings.go
@@ -1,509 +1,7 @@
package data
-import (
- "context"
- "errors"
- "fmt"
- "strings"
- "time"
-
- "kra/internal/biz"
-
- "gorm.io/gorm"
- "gorm.io/gorm/clause"
-)
-
-type dictionaryPO struct {
- ID uint `gorm:"primaryKey"`
- CreatedAt time.Time
- UpdatedAt time.Time
- DeletedAt gorm.DeletedAt `gorm:"index"`
- Name string
- Type string `gorm:"uniqueIndex"`
- Status bool
- Desc string
- ParentID *uint
-}
-
-func (dictionaryPO) TableName() string { return "sys_dictionaries" }
-
-type dictionaryDetailPO struct {
- ID uint `gorm:"primaryKey"`
- CreatedAt time.Time
- UpdatedAt time.Time
- DeletedAt gorm.DeletedAt `gorm:"index"`
- Label string
- Value string
- Extend string
- Status bool
- Sort int
- DictionaryID uint `gorm:"column:sys_dictionary_id;index"`
- ParentID *uint
- Level int
- Path string
-}
-
-func (dictionaryDetailPO) TableName() string { return "sys_dictionary_details" }
-
-type parameterPO struct {
- ID uint `gorm:"primaryKey"`
- CreatedAt time.Time
- UpdatedAt time.Time
- DeletedAt gorm.DeletedAt `gorm:"index"`
- Name string
- Key string `gorm:"uniqueIndex"`
- Value string
- Desc string
-}
-
-func (parameterPO) TableName() string { return "sys_params" }
-
-type apiTokenPO struct {
- ID uint `gorm:"primaryKey"`
- CreatedAt time.Time
- UpdatedAt time.Time
- DeletedAt gorm.DeletedAt `gorm:"index"`
- UserID uint
- AuthorityID uint
- Token string `gorm:"type:text;uniqueIndex"`
- Status bool
- ExpiresAt time.Time
- Remark string
-}
-
-func (apiTokenPO) TableName() string { return "sys_api_tokens" }
-
-type jwtBlacklistPO struct {
- ID uint `gorm:"primaryKey"`
- CreatedAt time.Time
- UpdatedAt time.Time
- DeletedAt gorm.DeletedAt `gorm:"index"`
- JWT string `gorm:"column:jwt;type:text"`
-}
-
-func (jwtBlacklistPO) TableName() string { return "jwt_blacklists" }
-
-type securityConfigPO struct {
- ID uint `gorm:"primaryKey"`
- CaptchaOpen int
- CaptchaTimeout int
- KeyLong int
- ImgWidth int
- ImgHeight int
- PwdMinLength int
- PwdRequireUpper bool
- PwdRequireLower bool
- PwdRequireDigit bool
- PwdRequireSpecial bool
- LimitEnable bool
- LimitWindow int
- LimitCount int
- LockEnable bool
- LockThreshold int
- LockDuration int
- PwdExpireEnable bool
- PwdExpireDays int
- ForceNewUserChangePassword bool
-}
-
-func (securityConfigPO) TableName() string { return "sys_security_config" }
+import "kra/internal/biz"
type settingsRepo struct{ data *Data }
func NewSettingsRepo(data *Data) biz.SettingsRepo { return &settingsRepo{data: data} }
-
-func dictionaryFromPO(po dictionaryPO) *biz.Dictionary {
- return &biz.Dictionary{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Name: po.Name, Type: po.Type, Status: po.Status, Desc: po.Desc, ParentID: po.ParentID}
-}
-func detailFromPO(po dictionaryDetailPO) *biz.DictionaryDetail {
- return &biz.DictionaryDetail{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Label: po.Label, Value: po.Value, Extend: po.Extend, Status: po.Status, Sort: po.Sort, DictionaryID: po.DictionaryID, ParentID: po.ParentID, Level: po.Level, Path: po.Path}
-}
-func parameterFromPO(po parameterPO) *biz.SystemParameter {
- return &biz.SystemParameter{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Name: po.Name, Key: po.Key, Value: po.Value, Desc: po.Desc}
-}
-
-func (r *settingsRepo) CreateDictionary(ctx context.Context, v *biz.Dictionary) error {
- po := dictionaryPO{Name: v.Name, Type: v.Type, Status: v.Status, Desc: v.Desc, ParentID: v.ParentID}
- if err := r.data.gormDB.WithContext(ctx).Create(&po).Error; err != nil {
- return err
- }
- v.ID = po.ID
- 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
-}
-func (r *settingsRepo) DeleteDictionary(ctx context.Context, id uint) error {
- return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
- var count int64
- if err := tx.Model(&dictionaryPO{}).Where("parent_id = ?", id).Count(&count).Error; err != nil {
- return err
- }
- if count > 0 {
- return errors.New("存在子字典不可删除")
- }
- if err := tx.Where("sys_dictionary_id = ?", id).Delete(&dictionaryDetailPO{}).Error; err != nil {
- return err
- }
- return tx.Delete(&dictionaryPO{}, id).Error
- })
-}
-func (r *settingsRepo) FindDictionary(ctx context.Context, id uint, typ string, 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
- }
- if err != nil {
- return nil, err
- }
- out := dictionaryFromPO(po)
- if details {
- out.Details, _, err = r.ListDictionaryDetails(ctx, 1, 10000, out.ID, "", "")
- }
- return out, err
-}
-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+"%")
- }
- 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 {
- return nil, 0, err
- }
- out := make([]*biz.Dictionary, 0, len(pos))
- for _, po := range pos {
- v := dictionaryFromPO(po)
- if details {
- v.Details, _, _ = r.ListDictionaryDetails(ctx, 1, 10000, v.ID, "", "")
- }
- out = append(out, v)
- }
- return out, total, nil
-}
-
-func (r *settingsRepo) CreateDictionaryDetail(ctx context.Context, v *biz.DictionaryDetail) error {
- po := dictionaryDetailPO{Label: v.Label, Value: v.Value, Extend: v.Extend, Status: v.Status, Sort: v.Sort, DictionaryID: v.DictionaryID, ParentID: v.ParentID}
- po.Level = 0
- po.Path = ""
- if v.ParentID != nil && *v.ParentID != 0 {
- var parent dictionaryDetailPO
- if err := r.data.gormDB.WithContext(ctx).First(&parent, *v.ParentID).Error; err != nil {
- return err
- }
- po.Level = parent.Level + 1
- po.Path = strings.Trim(strings.Join([]string{parent.Path, fmt.Sprint(parent.ID)}, ","), ",")
- }
- if err := r.data.gormDB.WithContext(ctx).Create(&po).Error; err != nil {
- return err
- }
- v.ID = po.ID
- v.Level = po.Level
- v.Path = po.Path
- return nil
-}
-func (r *settingsRepo) UpdateDictionaryDetail(ctx context.Context, v *biz.DictionaryDetail) error {
- var po dictionaryDetailPO
- if err := r.data.gormDB.WithContext(ctx).First(&po, v.ID).Error; err != nil {
- return err
- }
- level, path := 0, ""
- if v.ParentID != nil && *v.ParentID != 0 {
- if *v.ParentID == v.ID {
- return errors.New("不能将自身设为父级")
- }
- var parent dictionaryDetailPO
- if err := r.data.gormDB.WithContext(ctx).First(&parent, *v.ParentID).Error; err != nil {
- return err
- }
- if strings.Contains(","+parent.Path+",", fmt.Sprintf(",%d,", v.ID)) {
- return errors.New("不能移动到自己的子级")
- }
- level = parent.Level + 1
- path = strings.Trim(strings.Join([]string{parent.Path, fmt.Sprint(parent.ID)}, ","), ",")
- }
- oldPrefix := strings.Trim(strings.Join([]string{po.Path, fmt.Sprint(po.ID)}, ","), ",")
- newPrefix := strings.Trim(strings.Join([]string{path, fmt.Sprint(po.ID)}, ","), ",")
- return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
- if err := tx.Model(&dictionaryDetailPO{}).Where("id = ?", v.ID).Updates(map[string]any{"label": v.Label, "value": v.Value, "extend": v.Extend, "status": v.Status, "sort": v.Sort, "sys_dictionary_id": v.DictionaryID, "parent_id": v.ParentID, "level": level, "path": path}).Error; err != nil {
- return err
- }
- return tx.Model(&dictionaryDetailPO{}).Where("path = ? OR path LIKE ?", oldPrefix, oldPrefix+",%").Update("path", gorm.Expr("REPLACE(path, ?, ?)", oldPrefix, newPrefix)).Error
- })
-}
-func (r *settingsRepo) DeleteDictionaryDetail(ctx context.Context, id uint) error {
- var count int64
- if err := r.data.gormDB.WithContext(ctx).Model(&dictionaryDetailPO{}).Where("parent_id = ?", id).Count(&count).Error; err != nil {
- return err
- }
- if count > 0 {
- return errors.New("存在子级不可删除")
- }
- return r.data.gormDB.WithContext(ctx).Delete(&dictionaryDetailPO{}, id).Error
-}
-func (r *settingsRepo) FindDictionaryDetail(ctx context.Context, id uint) (*biz.DictionaryDetail, error) {
- var po dictionaryDetailPO
- if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil {
- return nil, err
- }
- 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
- }
- db := r.data.gormDB.WithContext(ctx).Model(&dictionaryDetailPO{})
- if dictionaryID != 0 {
- db = db.Where("sys_dictionary_id = ?", dictionaryID)
- }
- if label != "" {
- db = db.Where("label LIKE ?", "%"+label+"%")
- }
- if value != "" {
- db = db.Where("value LIKE ?", "%"+value+"%")
- }
- 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 {
- return nil, 0, err
- }
- out := make([]*biz.DictionaryDetail, 0, len(pos))
- for _, po := range pos {
- out = append(out, detailFromPO(po))
- }
- return out, total, nil
-}
-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)
- if err != nil {
- return nil, err
- }
- dictionaryID = dictionary.ID
- }
- items, _, err := r.ListDictionaryDetails(ctx, 1, 100000, dictionaryID, "", "")
- if err != nil {
- return nil, err
- }
- byID := map[uint]*biz.DictionaryDetail{}
- for _, item := range items {
- item.Children = []*biz.DictionaryDetail{}
- byID[item.ID] = item
- }
- roots := []*biz.DictionaryDetail{}
- for _, item := range items {
- if item.ParentID != nil && byID[*item.ParentID] != nil {
- byID[*item.ParentID].Children = append(byID[*item.ParentID].Children, item)
- } else {
- roots = append(roots, item)
- }
- }
- return roots, nil
-}
-
-func (r *settingsRepo) CreateParameter(ctx context.Context, v *biz.SystemParameter) error {
- po := parameterPO{Name: v.Name, Key: v.Key, Value: v.Value, Desc: v.Desc}
- if err := r.data.gormDB.WithContext(ctx).Create(&po).Error; err != nil {
- return err
- }
- v.ID = po.ID
- return nil
-}
-func (r *settingsRepo) UpdateParameter(ctx context.Context, v *biz.SystemParameter) error {
- return r.data.gormDB.WithContext(ctx).Model(¶meterPO{}).Where("id = ?", v.ID).Updates(map[string]any{"name": v.Name, "key": v.Key, "value": v.Value, "desc": v.Desc}).Error
-}
-func (r *settingsRepo) DeleteParameters(ctx context.Context, ids []uint) error {
- return r.data.gormDB.WithContext(ctx).Delete(¶meterPO{}, ids).Error
-}
-func (r *settingsRepo) FindParameter(ctx context.Context, id uint, key string) (*biz.SystemParameter, error) {
- var po parameterPO
- db := r.data.gormDB.WithContext(ctx)
- var err error
- if id != 0 {
- err = db.First(&po, id).Error
- } else {
- err = db.Where(map[string]any{"key": key}).First(&po).Error
- }
- if err != nil {
- return nil, err
- }
- 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.Name != "" {
- db = db.Where("name LIKE ?", "%"+q.Name+"%")
- }
- if q.Key != "" {
- db = db.Where(clause.Like{Column: clause.Column{Name: "key"}, Value: "%" + q.Key + "%"})
- }
- }
- var total int64
- if err := db.Count(&total).Error; err != nil {
- return nil, 0, err
- }
- var pos []parameterPO
- if err := db.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
- return nil, 0, err
- }
- out := make([]*biz.SystemParameter, 0, len(pos))
- for _, po := range pos {
- out = append(out, parameterFromPO(po))
- }
- return out, total, nil
-}
-
-func (r *settingsRepo) UserHasAuthority(ctx context.Context, userID, authorityID uint) (*biz.User, bool, error) {
- var po userPO
- if err := r.data.gormDB.WithContext(ctx).First(&po, userID).Error; err != nil {
- return nil, false, err
- }
- var count int64
- err := r.data.gormDB.WithContext(ctx).Model(&userAuthorityPO{}).Where("sys_user_id = ? AND sys_authority_authority_id = ?", userID, authorityID).Count(&count).Error
- if err != nil {
- return nil, false, err
- }
- user, err := (&systemRepo{data: r.data}).toBizUser(ctx, &po)
- return user, count > 0 || po.AuthorityID == authorityID, err
-}
-func (r *settingsRepo) CreateAPIToken(ctx context.Context, v *biz.APIToken) error {
- po := apiTokenPO{UserID: v.UserID, AuthorityID: v.AuthorityID, Token: v.Token, Status: v.Status, ExpiresAt: v.ExpiresAt, Remark: v.Remark}
- if err := r.data.gormDB.WithContext(ctx).Create(&po).Error; err != nil {
- return err
- }
- v.ID = po.ID
- v.CreatedAt = po.CreatedAt
- 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)
- }
- if status != nil {
- db = db.Where("status = ?", *status)
- }
- var total int64
- if err := db.Count(&total).Error; err != nil {
- 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 {
- return nil, 0, err
- }
- 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)
- }
- out = append(out, v)
- }
- return out, total, nil
-}
-func (r *settingsRepo) DisableAPIToken(ctx context.Context, id uint) (string, error) {
- var po apiTokenPO
- if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil {
- return "", err
- }
- return po.Token, r.data.gormDB.WithContext(ctx).Model(&po).Update("status", false).Error
-}
-func (r *settingsRepo) BlacklistToken(ctx context.Context, token string) error {
- if token == "" {
- return errors.New("token is empty")
- }
- return r.data.gormDB.WithContext(ctx).Create(&jwtBlacklistPO{JWT: token}).Error
-}
-func (r *settingsRepo) IsTokenDisabled(ctx context.Context, token string) (bool, error) {
- var blacklistCount int64
- if err := r.data.gormDB.WithContext(ctx).Model(&jwtBlacklistPO{}).Where("jwt = ?", token).Count(&blacklistCount).Error; err != nil {
- return false, err
- }
- if blacklistCount > 0 {
- return true, nil
- }
- var po apiTokenPO
- result := r.data.gormDB.WithContext(ctx).Where("token = ?", token).Limit(1).Find(&po)
- if result.Error != nil {
- return false, result.Error
- }
- if result.RowsAffected == 0 {
- return false, nil
- }
- return !po.Status || time.Now().After(po.ExpiresAt), nil
-}
-
-func defaultSecurityConfig() securityConfigPO {
- return securityConfigPO{ID: 1, CaptchaTimeout: 3600, KeyLong: 6, ImgWidth: 240, ImgHeight: 80, PwdMinLength: 8, LimitWindow: 60, LimitCount: 30, LockThreshold: 5, LockDuration: 30, PwdExpireDays: 90}
-}
-func securityFromPO(v securityConfigPO) *biz.SecurityConfig {
- return &biz.SecurityConfig{ID: v.ID, CaptchaOpen: v.CaptchaOpen, CaptchaTimeout: v.CaptchaTimeout, KeyLong: v.KeyLong, ImgWidth: v.ImgWidth, ImgHeight: v.ImgHeight, PwdMinLength: v.PwdMinLength, PwdRequireUpper: v.PwdRequireUpper, PwdRequireLower: v.PwdRequireLower, PwdRequireDigit: v.PwdRequireDigit, PwdRequireSpecial: v.PwdRequireSpecial, LimitEnable: v.LimitEnable, LimitWindow: v.LimitWindow, LimitCount: v.LimitCount, LockEnable: v.LockEnable, LockThreshold: v.LockThreshold, LockDuration: v.LockDuration, PwdExpireEnable: v.PwdExpireEnable, PwdExpireDays: v.PwdExpireDays, ForceNewUserChangePassword: v.ForceNewUserChangePassword}
-}
-func securityToPO(v *biz.SecurityConfig) securityConfigPO {
- return securityConfigPO{ID: 1, CaptchaOpen: v.CaptchaOpen, CaptchaTimeout: v.CaptchaTimeout, KeyLong: v.KeyLong, ImgWidth: v.ImgWidth, ImgHeight: v.ImgHeight, PwdMinLength: v.PwdMinLength, PwdRequireUpper: v.PwdRequireUpper, PwdRequireLower: v.PwdRequireLower, PwdRequireDigit: v.PwdRequireDigit, PwdRequireSpecial: v.PwdRequireSpecial, LimitEnable: v.LimitEnable, LimitWindow: v.LimitWindow, LimitCount: v.LimitCount, LockEnable: v.LockEnable, LockThreshold: v.LockThreshold, LockDuration: v.LockDuration, PwdExpireEnable: v.PwdExpireEnable, PwdExpireDays: v.PwdExpireDays, ForceNewUserChangePassword: v.ForceNewUserChangePassword}
-}
-func (r *settingsRepo) SecurityConfig(ctx context.Context) (*biz.SecurityConfig, error) {
- var po securityConfigPO
- err := r.data.gormDB.WithContext(ctx).First(&po, 1).Error
- if errors.Is(err, gorm.ErrRecordNotFound) {
- po = defaultSecurityConfig()
- err = r.data.gormDB.WithContext(ctx).Create(&po).Error
- }
- if err != nil {
- return nil, err
- }
- return securityFromPO(po), nil
-}
-func (r *settingsRepo) SaveSecurityConfig(ctx context.Context, v *biz.SecurityConfig) error {
- previous, err := r.SecurityConfig(ctx)
- if err != nil {
- return err
- }
- po := securityToPO(v)
- return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
- if err := tx.Save(&po).Error; err != nil {
- return err
- }
- if v.PwdExpireEnable && !previous.PwdExpireEnable {
- return tx.Model(&userPO{}).Where("password_updated_at IS NULL").Update("password_updated_at", time.Now()).Error
- }
- return nil
- })
-}
diff --git a/internal/data/aliyun_storage.go b/internal/data/storage/aliyun_storage.go
similarity index 99%
rename from internal/data/aliyun_storage.go
rename to internal/data/storage/aliyun_storage.go
index bb5d228..e6d7ffc 100644
--- a/internal/data/aliyun_storage.go
+++ b/internal/data/storage/aliyun_storage.go
@@ -1,4 +1,4 @@
-package data
+package storage
import (
"context"
diff --git a/internal/data/aws_storage.go b/internal/data/storage/aws_storage.go
similarity index 99%
rename from internal/data/aws_storage.go
rename to internal/data/storage/aws_storage.go
index dc8bae7..e18a37d 100644
--- a/internal/data/aws_storage.go
+++ b/internal/data/storage/aws_storage.go
@@ -1,4 +1,4 @@
-package data
+package storage
import (
"context"
diff --git a/internal/data/huawei_storage.go b/internal/data/storage/huawei_storage.go
similarity index 99%
rename from internal/data/huawei_storage.go
rename to internal/data/storage/huawei_storage.go
index 2b26f65..59cb76a 100644
--- a/internal/data/huawei_storage.go
+++ b/internal/data/storage/huawei_storage.go
@@ -1,4 +1,4 @@
-package data
+package storage
import (
"context"
diff --git a/internal/data/storage/local.go b/internal/data/storage/local.go
new file mode 100644
index 0000000..fe018dd
--- /dev/null
+++ b/internal/data/storage/local.go
@@ -0,0 +1,219 @@
+package storage
+
+import (
+ "context"
+ "crypto/md5"
+ "encoding/hex"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+
+ "kra/internal/biz"
+ "kra/internal/conf"
+)
+
+type fileStorage struct {
+ root string
+ urlPrefix string
+}
+
+func New(config *conf.AdminBackend) (biz.FileStorage, error) {
+ storageType := "local"
+ if config != nil && config.Storage != nil && config.Storage.Type != "" {
+ storageType = strings.ToLower(config.Storage.Type)
+ }
+ if storageType == "qiniu" {
+ return newQiniuStorage(config.Storage.Qiniu)
+ }
+ if storageType != "local" {
+ var object *conf.AdminBackend_ObjectStore
+ switch storageType {
+ case "aliyun-oss":
+ return newAliyunStorage(config.Storage.AliyunOss)
+ case "huawei-obs":
+ return newHuaweiStorage(config.Storage.HuaweiObs)
+ case "tencent-cos":
+ return newTencentStorage(config.Storage.TencentCos)
+ case "aws-s3":
+ return newAWSStorage(storageType, config.Storage.AwsS3)
+ case "cloudflare-r2":
+ return newAWSStorage(storageType, config.Storage.CloudflareR2)
+ case "minio":
+ object = config.Storage.Minio
+ default:
+ return nil, fmt.Errorf("unsupported storage type %q", storageType)
+ }
+ return newS3Storage(storageType, object)
+ }
+ root, prefix := "uploads/file", "uploads/file"
+ if config != nil && config.Local != nil {
+ if config.Local.StorePath != "" {
+ root = config.Local.StorePath
+ }
+ if config.Local.PathPrefix != "" {
+ prefix = config.Local.PathPrefix
+ }
+ }
+ root = filepath.Clean(root)
+ if err := os.MkdirAll(root, 0o755); err != nil {
+ return nil, fmt.Errorf("create upload directory: %w", err)
+ }
+ return &fileStorage{root: root, urlPrefix: "/" + strings.Trim(prefix, "/")}, nil
+}
+
+func composeFiles(ctx context.Context, storage biz.FileStorage, names []string, destination string) (*biz.StoredFile, string, error) {
+ reader, writer := io.Pipe()
+ hash := md5.New()
+ errCh := make(chan error, 1)
+ go func() {
+ defer writer.Close()
+ for _, name := range names {
+ if ctx.Err() != nil {
+ errCh <- ctx.Err()
+ return
+ }
+ file, err := storage.Open(ctx, name)
+ if err != nil {
+ errCh <- err
+ return
+ }
+ _, err = io.Copy(io.MultiWriter(writer, hash), file)
+ _ = file.Close()
+ if err != nil {
+ errCh <- err
+ return
+ }
+ }
+ errCh <- nil
+ }()
+ stored, err := storage.Put(ctx, destination, reader)
+ composeErr := <-errCh
+ if err != nil {
+ return nil, "", err
+ }
+ if composeErr != nil {
+ _ = storage.Delete(ctx, destination)
+ return nil, "", composeErr
+ }
+ return stored, hex.EncodeToString(hash.Sum(nil)), nil
+}
+
+func (s *fileStorage) resolve(name string) (string, error) {
+ clean := filepath.Clean(strings.TrimPrefix(name, "/"))
+ if clean == "." || filepath.IsAbs(clean) || clean == ".." || strings.HasPrefix(clean, ".."+string(filepath.Separator)) {
+ return "", fmt.Errorf("invalid storage path")
+ }
+ return filepath.Join(s.root, clean), nil
+}
+
+func (s *fileStorage) Put(ctx context.Context, name string, reader io.Reader) (*biz.StoredFile, error) {
+ path, err := s.resolve(name)
+ if err != nil {
+ return nil, err
+ }
+ if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
+ return nil, err
+ }
+ file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
+ if err != nil {
+ return nil, err
+ }
+ size, copyErr := io.Copy(file, reader)
+ closeErr := file.Close()
+ if copyErr != nil {
+ return nil, copyErr
+ }
+ if closeErr != nil {
+ return nil, closeErr
+ }
+ return &biz.StoredFile{Name: filepath.Base(name), Path: name, URL: s.urlPrefix + "/" + strings.TrimPrefix(filepath.ToSlash(name), "/"), Size: size}, nil
+}
+
+func (s *fileStorage) Open(ctx context.Context, name string) (io.ReadCloser, error) {
+ path, err := s.resolve(name)
+ if err != nil {
+ return nil, err
+ }
+ return os.Open(path)
+}
+
+func (s *fileStorage) Delete(ctx context.Context, name string) error {
+ path, err := s.resolve(name)
+ if err != nil {
+ return err
+ }
+ if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
+ return err
+ }
+ return nil
+}
+
+func (s *fileStorage) Compose(ctx context.Context, names []string, destination string) (*biz.StoredFile, string, error) {
+ return composeFiles(ctx, s, names, destination)
+}
+func (s *fileStorage) DeletePrefix(ctx context.Context, prefix string) error {
+ path, err := s.resolve(strings.TrimSuffix(prefix, "/") + "/placeholder")
+ if err != nil {
+ return err
+ }
+ dir := filepath.Dir(path)
+ if !strings.HasPrefix(dir, s.root+string(filepath.Separator)) {
+ return fmt.Errorf("invalid storage path")
+ }
+ if err = os.RemoveAll(dir); err != nil {
+ return err
+ }
+ return nil
+}
+func (s *fileStorage) List(ctx context.Context, prefix, cursor string, limit int) ([]*biz.StoredFile, string, bool, error) {
+ if limit < 1 || limit > 1000 {
+ limit = 100
+ }
+ root, err := s.resolve(strings.TrimSuffix(prefix, "/") + "/placeholder")
+ if err != nil {
+ return nil, "", false, err
+ }
+ walkRoot := filepath.Dir(root)
+ items := []*biz.StoredFile{}
+ err = filepath.WalkDir(walkRoot, func(path string, entry os.DirEntry, walkErr error) error {
+ if walkErr != nil {
+ if os.IsNotExist(walkErr) {
+ return nil
+ }
+ return walkErr
+ }
+ if ctx.Err() != nil {
+ return ctx.Err()
+ }
+ if entry.IsDir() || entry.Type()&os.ModeSymlink != 0 {
+ return nil
+ }
+ relative, _ := filepath.Rel(s.root, path)
+ relative = filepath.ToSlash(relative)
+ if relative <= cursor {
+ return nil
+ }
+ info, infoErr := entry.Info()
+ if infoErr != nil {
+ return infoErr
+ }
+ items = append(items, &biz.StoredFile{Name: entry.Name(), Path: relative, URL: s.urlPrefix + "/" + relative, Size: info.Size()})
+ return nil
+ })
+ if err != nil {
+ return nil, "", false, err
+ }
+ sort.Slice(items, func(i, j int) bool { return items[i].Path < items[j].Path })
+ hasMore := len(items) > limit
+ if hasMore {
+ items = items[:limit]
+ }
+ next := ""
+ if len(items) > 0 {
+ next = items[len(items)-1].Path
+ }
+ return items, next, hasMore, nil
+}
diff --git a/internal/data/qiniu_storage.go b/internal/data/storage/qiniu_storage.go
similarity index 99%
rename from internal/data/qiniu_storage.go
rename to internal/data/storage/qiniu_storage.go
index 54ea980..fe272d5 100644
--- a/internal/data/qiniu_storage.go
+++ b/internal/data/storage/qiniu_storage.go
@@ -1,4 +1,4 @@
-package data
+package storage
import (
"context"
diff --git a/internal/data/s3_storage.go b/internal/data/storage/s3_storage.go
similarity index 99%
rename from internal/data/s3_storage.go
rename to internal/data/storage/s3_storage.go
index 30a39c9..29c5a95 100644
--- a/internal/data/s3_storage.go
+++ b/internal/data/storage/s3_storage.go
@@ -1,4 +1,4 @@
-package data
+package storage
import (
"context"
diff --git a/internal/data/tencent_storage.go b/internal/data/storage/tencent_storage.go
similarity index 99%
rename from internal/data/tencent_storage.go
rename to internal/data/storage/tencent_storage.go
index 20ffd27..a5dc5d0 100644
--- a/internal/data/tencent_storage.go
+++ b/internal/data/storage/tencent_storage.go
@@ -1,4 +1,4 @@
-package data
+package storage
import (
"context"
diff --git a/internal/data/system.go b/internal/data/system.go
index 2856d0c..24936a0 100644
--- a/internal/data/system.go
+++ b/internal/data/system.go
@@ -1,20 +1,10 @@
package data
import (
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "strings"
"time"
"kra/internal/biz"
- "kra/internal/conf"
- "github.com/google/uuid"
- "golang.org/x/crypto/bcrypt"
- "google.golang.org/protobuf/encoding/protojson"
- "google.golang.org/protobuf/proto"
"gorm.io/gorm"
)
@@ -102,455 +92,4 @@ func (menuParameterPO) TableName() string { return "sys_base_menu_parameters" }
type systemRepo struct{ data *Data }
-func NewSystemRepo(data *Data) biz.SystemRepo { return &systemRepo{data: data} }
-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)
- 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)
-}
-func (r *systemRepo) ReloadConfig(ctx context.Context) error {
- return r.data.reloadConfig(ctx)
-}
-
-func (r *systemRepo) IsInitialized(ctx context.Context) (bool, error) {
- db := r.data.gormDB.WithContext(ctx)
- if !db.Migrator().HasTable(&userPO{}) {
- return false, nil
- }
- var count int64
- if err := db.Model(&userPO{}).Where("username = ?", "admin").Count(&count).Error; err != nil {
- return false, err
- }
- return count > 0, nil
-}
-
-func (r *systemRepo) Initialize(ctx context.Context, input *biz.DatabaseConfig) error {
- config := &conf.Data_Database{Driver: input.Driver, Host: input.Host, Port: input.Port, User: input.User, Password: input.Password, Name: input.Name, Path: input.Path, Config: input.Config}
- r.data.initMu.Lock()
- defer r.data.initMu.Unlock()
- initialized, err := r.IsInitialized(ctx)
- if err != nil {
- return err
- }
- if initialized {
- return errors.New("数据库已初始化,无需重复初始化")
- }
- candidate, err := openDatabase(config, true, input.Template)
- if err != nil {
- return err
- }
- activated := false
- defer func() {
- if !activated {
- if sqlDB, closeErr := candidate.DB(); closeErr == nil {
- _ = sqlDB.Close()
- }
- }
- }()
- db := candidate.WithContext(ctx)
- if err := migrateAll(db); err != nil {
- return err
- }
- if err := db.Transaction(func(tx *gorm.DB) error {
- authority := authorityPO{AuthorityID: 888, AuthorityName: "超级管理员", DataScope: 1, DefaultRouter: "dashboard"}
- if err := tx.FirstOrCreate(&authority, authorityPO{AuthorityID: 888}).Error; err != nil {
- return err
- }
- menus := defaultMenus()
- for i := range menus {
- if err := tx.Where("name = ?", menus[i].Name).FirstOrCreate(&menus[i]).Error; err != nil {
- return err
- }
- }
- var persisted []menuPO
- if err := tx.Order("sort asc, id asc").Find(&persisted).Error; err != nil {
- return err
- }
- nameID := make(map[string]uint, len(persisted))
- for _, menu := range persisted {
- nameID[menu.Name] = menu.ID
- }
- for i := range menus {
- if menus[i].ActiveName == "" {
- continue
- }
- if err := tx.Model(&menuPO{}).Where("name = ?", menus[i].Name).Updates(map[string]any{"parent_id": nameID[menus[i].ActiveName], "active_name": ""}).Error; err != nil {
- return err
- }
- }
- if err := tx.Where("sys_authority_authority_id = ?", 888).Delete(&authorityMenuPO{}).Error; err != nil {
- return err
- }
- links := make([]authorityMenuPO, 0, len(persisted))
- for _, menu := range persisted {
- links = append(links, authorityMenuPO{SysAuthorityAuthorityID: 888, SysBaseMenuID: menu.ID})
- }
- if len(links) > 0 {
- if err := tx.Create(&links).Error; err != nil {
- return err
- }
- }
- var count int64
- if err := tx.Model(&userPO{}).Where("username = ?", "admin").Count(&count).Error; err != nil {
- return err
- }
- if count == 0 {
- hash, err := bcrypt.GenerateFromPassword([]byte(input.AdminPassword), bcrypt.DefaultCost)
- if err != nil {
- return err
- }
- now := time.Now()
- user := userPO{UUID: uuid.NewString(), Username: "admin", Password: string(hash), NickName: "超级管理员", AuthorityID: 888, Enable: 1, PasswordUpdatedAt: &now}
- if err := tx.Create(&user).Error; err != nil {
- return err
- }
- if err := tx.Create(&userAuthorityPO{SysUserID: user.ID, SysAuthorityAuthorityID: 888}).Error; err != nil {
- return err
- }
- }
- department := departmentPO{Name: "总公司", ParentID: 0, Ancestors: "0", Sort: 0, Status: true}
- if err := tx.Where("name = ?", department.Name).FirstOrCreate(&department).Error; err != nil {
- return err
- }
- for _, position := range []positionPO{{Name: "总经理", Code: "CEO", Sort: 1, Status: true}, {Name: "普通员工", Code: "STAFF", Sort: 2, Status: true}} {
- if err := tx.Where("code = ?", position.Code).FirstOrCreate(&position).Error; err != nil {
- return err
- }
- }
- security := defaultSecurityConfig()
- if err := tx.FirstOrCreate(&security, securityConfigPO{ID: 1}).Error; err != nil {
- return err
- }
- exportTemplate := exportTemplatePO{Name: "api", DBTableName: "sys_apis", TemplateID: "api", TemplateInfo: "{\n\"path\":\"路径\",\n\"method\":\"方法(大写)\",\n\"description\":\"方法介绍\",\n\"api_group\":\"方法分组\"\n}"}
- if err := tx.Where("template_id = ?", exportTemplate.TemplateID).FirstOrCreate(&exportTemplate).Error; err != nil {
- return err
- }
- for _, task := range []taskPO{{Name: "ClearDB", Description: "定时清理数据库过期日志(操作记录/JWT黑名单/定时任务执行日志)", Spec: "@daily", ExecutorType: "method", MethodName: "ClearDB", Enabled: true}, {Name: "CleanStaleUploads", Description: "定时清理过期大文件上传会话", Spec: "@hourly", ExecutorType: "method", MethodName: "CleanStaleUploads", Enabled: true}} {
- if err := tx.Where("name = ?", task.Name).FirstOrCreate(&task).Error; err != nil {
- return err
- }
- }
- for _, item := range input.APIs {
- if item == nil {
- continue
- }
- po := apiPO{Path: item.Path, Method: strings.ToUpper(item.Method), Description: item.Description, APIGroup: item.APIGroup}
- if err := tx.Where("path = ? AND method = ?", po.Path, po.Method).FirstOrCreate(&po).Error; err != nil {
- return err
- }
- }
- return nil
- }); err != nil {
- return err
- }
- nextData := proto.Clone(r.data.config).(*conf.Data)
- nextData.Database = config
- if err := r.data.persistConfigValues(nextData, r.data.admin); err != nil {
- return fmt.Errorf("persist database configuration: %w", err)
- }
- r.data.activateDatabase(candidate, config)
- activated = true
- return nil
-}
-
-func (r *systemRepo) FindUserByUsername(ctx context.Context, username string) (*biz.User, error) {
- var po userPO
- if err := r.data.gormDB.WithContext(ctx).Where("username = ?", username).First(&po).Error; err != nil {
- if errors.Is(err, gorm.ErrRecordNotFound) {
- return nil, biz.ErrUserNotFound
- }
- return nil, err
- }
- return r.toBizUser(ctx, &po)
-}
-
-func (r *systemRepo) FindUserByID(ctx context.Context, id uint) (*biz.User, error) {
- var po userPO
- if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil {
- if errors.Is(err, gorm.ErrRecordNotFound) {
- return nil, biz.ErrUserNotFound
- }
- return nil, err
- }
- return r.toBizUser(ctx, &po)
-}
-
-func (r *systemRepo) toBizUser(ctx context.Context, po *userPO) (*biz.User, error) {
- var authority authorityPO
- if err := r.data.gormDB.WithContext(ctx).First(&authority, "authority_id = ?", po.AuthorityID).Error; err != nil {
- return nil, err
- }
- var authorityPOs []authorityPO
- if err := r.data.gormDB.WithContext(ctx).Table("sys_authorities").Joins("JOIN sys_user_authority ON sys_user_authority.sys_authority_authority_id = sys_authorities.authority_id").Where("sys_user_authority.sys_user_id = ? AND sys_authorities.deleted_at IS NULL", po.ID).Find(&authorityPOs).Error; err != nil {
- return nil, err
- }
- authorities := make([]biz.Authority, 0, len(authorityPOs))
- for _, item := range authorityPOs {
- authorities = append(authorities, toBizAuthority(item))
- }
- setting := map[string]any(nil)
- if po.OriginSetting != "" {
- _ = json.Unmarshal([]byte(po.OriginSetting), &setting)
- }
- var departmentPOs []departmentPO
- if err := r.data.gormDB.WithContext(ctx).Table("sys_departments").Joins("JOIN sys_user_departments ON sys_user_departments.sys_department_id = sys_departments.id").Where("sys_user_departments.sys_user_id = ? AND sys_departments.deleted_at IS NULL", po.ID).Find(&departmentPOs).Error; err != nil {
- return nil, err
- }
- departments := make([]biz.Department, 0, len(departmentPOs))
- var primary *biz.Department
- for _, item := range departmentPOs {
- value := deptFromPO(item)
- departments = append(departments, *value)
- if item.ID == po.DeptID {
- copy := *value
- primary = ©
- }
- }
- var positionPOs []positionPO
- if err := r.data.gormDB.WithContext(ctx).Table("sys_positions").Joins("JOIN sys_user_positions ON sys_user_positions.sys_position_id = sys_positions.id").Where("sys_user_positions.sys_user_id = ? AND sys_positions.deleted_at IS NULL", po.ID).Find(&positionPOs).Error; err != nil {
- return nil, err
- }
- positions := make([]biz.Position, 0, len(positionPOs))
- for _, item := range positionPOs {
- positions = append(positions, *posFromPO(item))
- }
- 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, Authority: toBizAuthority(authority), Authorities: authorities, DeptID: po.DeptID, Department: primary, Departments: departments, Positions: positions, Phone: po.Phone, Email: po.Email, Enable: po.Enable, OriginSetting: setting, MustChangePassword: po.MustChangePassword, PasswordUpdatedAt: po.PasswordUpdatedAt}, nil
-}
-
-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 (r *systemRepo) MenusByAuthority(ctx context.Context, authorityID uint) ([]*biz.Menu, error) {
- var pos []menuPO
- err := r.data.gormDB.WithContext(ctx).Table("sys_base_menus").
- 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.deleted_at IS NULL", authorityID).
- Order("sys_base_menus.sort asc, sys_base_menus.id asc").Scan(&pos).Error
- if err != nil {
- return nil, err
- }
- byID := make(map[uint]*biz.Menu, 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{}}
- var parameters []menuParameterPO
- if err := r.data.gormDB.WithContext(ctx).Where("sys_base_menu_id = ?", po.ID).Order("id").Find(¶meters).Error; err != nil {
- return nil, err
- }
- for _, parameter := range parameters {
- 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 {
- return nil, err
- }
- for _, button := range buttons {
- 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 {
- menu := byID[po.ID]
- if parent := byID[po.ParentID]; parent != nil {
- parent.Children = append(parent.Children, menu)
- } else {
- roots = append(roots, menu)
- }
- }
- return roots, nil
-}
-
-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 != "" {
- db = db.Where("username LIKE ?", "%"+filter.Username+"%")
- }
- if filter.NickName != "" {
- db = db.Where("nick_name LIKE ?", "%"+filter.NickName+"%")
- }
- if filter.Phone != "" {
- db = db.Where("phone LIKE ?", "%"+filter.Phone+"%")
- }
- 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}
- if allowed[filter.OrderKey] {
- order = filter.OrderKey
- if filter.Desc {
- order += " desc"
- }
- }
- }
- var total int64
- if err := db.Count(&total).Error; err != nil {
- return nil, 0, err
- }
- var pos []userPO
- if err := db.Order(order).Offset((page - 1) * pageSize).Limit(pageSize).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
-}
-
-func (r *systemRepo) CreateUser(ctx context.Context, user *biz.User) (*biz.User, error) {
- if user.UUID == "" {
- user.UUID = uuid.NewString()
- }
- if user.Enable == 0 {
- user.Enable = 1
- }
- if user.AuthorityID == 0 {
- user.AuthorityID = 888
- }
- if user.NickName == "" {
- user.NickName = "系统用户"
- }
- 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 err := r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
- if err := tx.Create(&po).Error; err != nil {
- return err
- }
- return tx.Create(&userAuthorityPO{SysUserID: po.ID, SysAuthorityAuthorityID: po.AuthorityID}).Error
- }); err != nil {
- return nil, err
- }
- return r.FindUserByID(ctx, po.ID)
-}
-
-func (r *systemRepo) UpdateUser(ctx context.Context, 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
-}
-func (r *systemRepo) DeleteUser(ctx context.Context, id 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
- }
- if err := tx.Where("sys_user_id = ?", id).Delete(&userDepartmentPO{}).Error; err != nil {
- return err
- }
- if err := tx.Where("sys_user_id = ?", id).Delete(&userPositionPO{}).Error; err != nil {
- return err
- }
- return tx.Delete(&userPO{}, id).Error
- })
-}
-func (r *systemRepo) UpdatePassword(ctx context.Context, id uint, password string, clearMustChange bool) error {
- now := time.Now()
- updates := map[string]any{"password": password, "password_updated_at": now}
- if clearMustChange {
- updates["must_change_password"] = false
- }
- return r.data.gormDB.WithContext(ctx).Model(&userPO{}).Where("id = ?", id).Updates(updates).Error
-}
-func (r *systemRepo) ListAuthorities(ctx context.Context) ([]*biz.Authority, error) {
- var pos []authorityPO
- if err := r.data.gormDB.WithContext(ctx).Order("authority_id").Find(&pos).Error; err != nil {
- return nil, err
- }
- result := make([]*biz.Authority, 0, len(pos))
- for _, po := range pos {
- a := toBizAuthority(po)
- result = append(result, &a)
- }
- return result, nil
-}
-
-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))
- 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("角色不存在")
- }
- 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 {
- 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 {
- return err
- }
- if count == 0 {
- return errors.New("该用户无此角色")
- }
- return r.data.gormDB.WithContext(ctx).Model(&userPO{}).Where("id = ?", id).Update("authority_id", authorityID).Error
-}
-
-func (r *systemRepo) SetUserSetting(ctx context.Context, id uint, setting map[string]any) error {
- value, err := json.Marshal(setting)
- if err != nil {
- return err
- }
- return r.data.gormDB.WithContext(ctx).Model(&userPO{}).Where("id = ?", id).Update("origin_setting", string(value)).Error
-}
-
-// ActiveName temporarily carries the parent menu name during seeding. It is
-// cleared from responses by the service for these records.
-func defaultMenus() []menuPO {
- root := func(path, name, title, icon string, sort int) menuPO {
- return menuPO{Path: path, Name: name, Component: "view/routerHolder.vue", Title: title, Icon: icon, Sort: sort}
- }
- child := func(parent, path, name, component, title, icon string, sort int) menuPO {
- return menuPO{MenuLevel: 1, Path: path, Name: name, Component: component, Title: title, Icon: icon, Sort: sort, ActiveName: parent}
- }
- return []menuPO{
- {Path: "dashboard", Name: "dashboard", Component: "view/dashboard/index.vue", Title: "仪表盘", Icon: "odometer", Sort: 1},
- root("permission", "permission", "权限管理", "perm-kra", 2), root("org", "org", "组织管理", "share", 3), root("systemConfig", "systemConfig", "系统设置", "config-kra", 4), root("monitor", "monitor", "运维监控", "monitor-kra", 5), root("media", "media", "媒体管理", "folder-opened", 6), root("extensions", "extensions", "扩展功能", "cherry", 10),
- {Path: "person", Name: "person", Component: "view/person/person.vue", Title: "个人信息", Icon: "postcard", Hidden: true, Sort: 13},
- child("permission", "authority", "authority", "view/superAdmin/authority/authority.vue", "角色管理", "role-kra", 1), child("permission", "menu", "menu", "view/superAdmin/menu/menu.vue", "菜单管理", "tickets", 2), child("permission", "api", "api", "view/superAdmin/api/api.vue", "api管理", "api-kra", 3), child("permission", "apiToken", "apiToken", "view/systemTools/apiToken/index.vue", "API Token", "key", 4),
- child("org", "user", "user", "view/superAdmin/user/user.vue", "用户管理", "user", 1), child("org", "department", "department", "view/superAdmin/department/department.vue", "部门管理", "office-building", 2), child("org", "position", "position", "view/superAdmin/position/position.vue", "岗位管理", "postcard", 3),
- child("systemConfig", "system", "system", "view/systemTools/system/system.vue", "配置文件", "config-file-kra", 1), child("systemConfig", "dictionary", "dictionary", "view/superAdmin/dictionary/sysDictionary.vue", "字典管理", "notebook", 2), child("systemConfig", "sysParams", "sysParams", "view/superAdmin/params/sysParams.vue", "参数管理", "set-up", 3), child("systemConfig", "security", "security", "view/system/security/index.vue", "安全配置", "security-kra", 4),
- child("monitor", "operation", "operation", "view/superAdmin/operation/sysOperationRecord.vue", "操作历史", "document", 1), child("monitor", "loginLog", "loginLog", "view/systemTools/loginLog/index.vue", "登录日志", "clock", 2), child("monitor", "sysError", "sysError", "view/systemTools/sysError/sysError.vue", "错误日志", "error-kra", 3), child("monitor", "sysVersion", "sysVersion", "view/systemTools/version/version.vue", "版本管理", "version-kra", 4), child("monitor", "state", "state", "view/system/state.vue", "服务器状态", "server", 5), child("monitor", "dataAccessLog", "dataAccessLog", "view/superAdmin/dataAccessLog/dataAccessLog.vue", "数据权限审计", "warning", 6), child("monitor", "timedTask", "timedTask", "view/systemTools/timedTask/index.vue", "定时任务", "timer", 7), child("monitor", "logViewer", "logViewer", "view/systemTools/logViewer/index.vue", "文件日志", "document", 8),
- child("media", "upload", "upload", "view/media/upload.vue", "媒体库(上传下载)", "upload", 1), child("media", "chunkUpload", "chunkUpload", "view/media/chunkUpload.vue", "大文件上传", "folder-add", 2),
- child("extensions", "email", "email", "modules/email/view/index.vue", "邮件发送", "message", 4), child("extensions", "anInfo", "anInfo", "modules/announcement/view/info.vue", "公告管理", "bell", 5),
- }
-}
+func NewSystemRepo(data *Data) biz.SystemRepo { return &systemRepo{data: data} }
diff --git a/internal/data/system_init.go b/internal/data/system_init.go
new file mode 100644
index 0000000..d7c67e7
--- /dev/null
+++ b/internal/data/system_init.go
@@ -0,0 +1,192 @@
+package data
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "strings"
+ "time"
+
+ "kra/internal/biz"
+ "kra/internal/conf"
+
+ "github.com/google/uuid"
+ "golang.org/x/crypto/bcrypt"
+ "google.golang.org/protobuf/encoding/protojson"
+ "google.golang.org/protobuf/proto"
+ "gorm.io/gorm"
+)
+
+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)
+ 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)
+}
+func (r *systemRepo) ReloadConfig(ctx context.Context) error {
+ return r.data.reloadConfig(ctx)
+}
+
+func (r *systemRepo) IsInitialized(ctx context.Context) (bool, error) {
+ db := r.data.gormDB.WithContext(ctx)
+ if !db.Migrator().HasTable(&userPO{}) {
+ return false, nil
+ }
+ var count int64
+ if err := db.Model(&userPO{}).Where("username = ?", "admin").Count(&count).Error; err != nil {
+ return false, err
+ }
+ return count > 0, nil
+}
+
+func (r *systemRepo) Initialize(ctx context.Context, input *biz.DatabaseConfig) error {
+ config := &conf.Data_Database{Driver: input.Driver, Host: input.Host, Port: input.Port, User: input.User, Password: input.Password, Name: input.Name, Path: input.Path, Config: input.Config}
+ r.data.initMu.Lock()
+ defer r.data.initMu.Unlock()
+ initialized, err := r.IsInitialized(ctx)
+ if err != nil {
+ return err
+ }
+ if initialized {
+ return errors.New("数据库已初始化,无需重复初始化")
+ }
+ candidate, err := openDatabase(config, true, input.Template)
+ if err != nil {
+ return err
+ }
+ activated := false
+ defer func() {
+ if !activated {
+ if sqlDB, closeErr := candidate.DB(); closeErr == nil {
+ _ = sqlDB.Close()
+ }
+ }
+ }()
+ db := candidate.WithContext(ctx)
+ if err := migrateAll(db); err != nil {
+ return err
+ }
+ if err := db.Transaction(func(tx *gorm.DB) error {
+ authority := authorityPO{AuthorityID: 888, AuthorityName: "超级管理员", DataScope: 1, DefaultRouter: "dashboard"}
+ if err := tx.FirstOrCreate(&authority, authorityPO{AuthorityID: 888}).Error; err != nil {
+ return err
+ }
+ menus := defaultMenus()
+ for i := range menus {
+ if err := tx.Where("name = ?", menus[i].Name).FirstOrCreate(&menus[i]).Error; err != nil {
+ return err
+ }
+ }
+ var persisted []menuPO
+ if err := tx.Order("sort asc, id asc").Find(&persisted).Error; err != nil {
+ return err
+ }
+ nameID := make(map[string]uint, len(persisted))
+ for _, menu := range persisted {
+ nameID[menu.Name] = menu.ID
+ }
+ for i := range menus {
+ if menus[i].ActiveName == "" {
+ continue
+ }
+ if err := tx.Model(&menuPO{}).Where("name = ?", menus[i].Name).Updates(map[string]any{"parent_id": nameID[menus[i].ActiveName], "active_name": ""}).Error; err != nil {
+ return err
+ }
+ }
+ if err := tx.Where("sys_authority_authority_id = ?", 888).Delete(&authorityMenuPO{}).Error; err != nil {
+ return err
+ }
+ links := make([]authorityMenuPO, 0, len(persisted))
+ for _, menu := range persisted {
+ links = append(links, authorityMenuPO{SysAuthorityAuthorityID: 888, SysBaseMenuID: menu.ID})
+ }
+ if len(links) > 0 {
+ if err := tx.Create(&links).Error; err != nil {
+ return err
+ }
+ }
+ var count int64
+ if err := tx.Model(&userPO{}).Where("username = ?", "admin").Count(&count).Error; err != nil {
+ return err
+ }
+ if count == 0 {
+ hash, err := bcrypt.GenerateFromPassword([]byte(input.AdminPassword), bcrypt.DefaultCost)
+ if err != nil {
+ return err
+ }
+ now := time.Now()
+ user := userPO{UUID: uuid.NewString(), Username: "admin", Password: string(hash), NickName: "超级管理员", AuthorityID: 888, Enable: 1, PasswordUpdatedAt: &now}
+ if err := tx.Create(&user).Error; err != nil {
+ return err
+ }
+ if err := tx.Create(&userAuthorityPO{SysUserID: user.ID, SysAuthorityAuthorityID: 888}).Error; err != nil {
+ return err
+ }
+ }
+ department := departmentPO{Name: "总公司", ParentID: 0, Ancestors: "0", Sort: 0, Status: true}
+ if err := tx.Where("name = ?", department.Name).FirstOrCreate(&department).Error; err != nil {
+ return err
+ }
+ for _, position := range []positionPO{{Name: "总经理", Code: "CEO", Sort: 1, Status: true}, {Name: "普通员工", Code: "STAFF", Sort: 2, Status: true}} {
+ if err := tx.Where("code = ?", position.Code).FirstOrCreate(&position).Error; err != nil {
+ return err
+ }
+ }
+ security := defaultSecurityConfig()
+ if err := tx.FirstOrCreate(&security, securityConfigPO{ID: 1}).Error; err != nil {
+ return err
+ }
+ exportTemplate := exportTemplatePO{Name: "api", DBTableName: "sys_apis", TemplateID: "api", TemplateInfo: "{\n\"path\":\"路径\",\n\"method\":\"方法(大写)\",\n\"description\":\"方法介绍\",\n\"api_group\":\"方法分组\"\n}"}
+ if err := tx.Where("template_id = ?", exportTemplate.TemplateID).FirstOrCreate(&exportTemplate).Error; err != nil {
+ return err
+ }
+ for _, task := range []taskPO{{Name: "ClearDB", Description: "定时清理数据库过期日志(操作记录/JWT黑名单/定时任务执行日志)", Spec: "@daily", ExecutorType: "method", MethodName: "ClearDB", Enabled: true}, {Name: "CleanStaleUploads", Description: "定时清理过期大文件上传会话", Spec: "@hourly", ExecutorType: "method", MethodName: "CleanStaleUploads", Enabled: true}} {
+ if err := tx.Where("name = ?", task.Name).FirstOrCreate(&task).Error; err != nil {
+ return err
+ }
+ }
+ for _, item := range input.APIs {
+ if item == nil {
+ continue
+ }
+ po := apiPO{Path: item.Path, Method: strings.ToUpper(item.Method), Description: item.Description, APIGroup: item.APIGroup}
+ if err := tx.Where("path = ? AND method = ?", po.Path, po.Method).FirstOrCreate(&po).Error; err != nil {
+ return err
+ }
+ }
+ return nil
+ }); err != nil {
+ return err
+ }
+ nextData := proto.Clone(r.data.config).(*conf.Data)
+ nextData.Database = config
+ if err := r.data.persistConfigValues(nextData, r.data.admin); err != nil {
+ return fmt.Errorf("persist database configuration: %w", err)
+ }
+ r.data.activateDatabase(candidate, config)
+ activated = true
+ return nil
+}
+
+func defaultMenus() []menuPO {
+ root := func(path, name, title, icon string, sort int) menuPO {
+ return menuPO{Path: path, Name: name, Component: "view/routerHolder.vue", Title: title, Icon: icon, Sort: sort}
+ }
+ child := func(parent, path, name, component, title, icon string, sort int) menuPO {
+ return menuPO{MenuLevel: 1, Path: path, Name: name, Component: component, Title: title, Icon: icon, Sort: sort, ActiveName: parent}
+ }
+ return []menuPO{
+ {Path: "dashboard", Name: "dashboard", Component: "view/dashboard/index.vue", Title: "仪表盘", Icon: "odometer", Sort: 1},
+ root("permission", "permission", "权限管理", "perm-kra", 2), root("org", "org", "组织管理", "share", 3), root("systemConfig", "systemConfig", "系统设置", "config-kra", 4), root("monitor", "monitor", "运维监控", "monitor-kra", 5), root("media", "media", "媒体管理", "folder-opened", 6), root("extensions", "extensions", "扩展功能", "cherry", 10),
+ {Path: "person", Name: "person", Component: "view/person/person.vue", Title: "个人信息", Icon: "postcard", Hidden: true, Sort: 13},
+ child("permission", "authority", "authority", "view/superAdmin/authority/authority.vue", "角色管理", "role-kra", 1), child("permission", "menu", "menu", "view/superAdmin/menu/menu.vue", "菜单管理", "tickets", 2), child("permission", "api", "api", "view/superAdmin/api/api.vue", "api管理", "api-kra", 3), child("permission", "apiToken", "apiToken", "view/systemTools/apiToken/index.vue", "API Token", "key", 4),
+ child("org", "user", "user", "view/superAdmin/user/user.vue", "用户管理", "user", 1), child("org", "department", "department", "view/superAdmin/department/department.vue", "部门管理", "office-building", 2), child("org", "position", "position", "view/superAdmin/position/position.vue", "岗位管理", "postcard", 3),
+ child("systemConfig", "system", "system", "view/systemTools/system/system.vue", "配置文件", "config-file-kra", 1), child("systemConfig", "dictionary", "dictionary", "view/superAdmin/dictionary/sysDictionary.vue", "字典管理", "notebook", 2), child("systemConfig", "sysParams", "sysParams", "view/superAdmin/params/sysParams.vue", "参数管理", "set-up", 3), child("systemConfig", "security", "security", "view/system/security/index.vue", "安全配置", "security-kra", 4),
+ child("monitor", "operation", "operation", "view/superAdmin/operation/sysOperationRecord.vue", "操作历史", "document", 1), child("monitor", "loginLog", "loginLog", "view/systemTools/loginLog/index.vue", "登录日志", "clock", 2), child("monitor", "sysError", "sysError", "view/systemTools/sysError/sysError.vue", "错误日志", "error-kra", 3), child("monitor", "sysVersion", "sysVersion", "view/systemTools/version/version.vue", "版本管理", "version-kra", 4), child("monitor", "state", "state", "view/system/state.vue", "服务器状态", "server", 5), child("monitor", "dataAccessLog", "dataAccessLog", "view/superAdmin/dataAccessLog/dataAccessLog.vue", "数据权限审计", "warning", 6), child("monitor", "timedTask", "timedTask", "view/systemTools/timedTask/index.vue", "定时任务", "timer", 7), child("monitor", "logViewer", "logViewer", "view/systemTools/logViewer/index.vue", "文件日志", "document", 8),
+ child("media", "upload", "upload", "view/media/upload.vue", "媒体库(上传下载)", "upload", 1), child("media", "chunkUpload", "chunkUpload", "view/media/chunkUpload.vue", "大文件上传", "folder-add", 2),
+ child("extensions", "email", "email", "modules/email/view/index.vue", "邮件发送", "message", 4), child("extensions", "anInfo", "anInfo", "modules/announcement/view/info.vue", "公告管理", "bell", 5),
+ }
+}
diff --git a/internal/data/user.go b/internal/data/user.go
new file mode 100644
index 0000000..d21a893
--- /dev/null
+++ b/internal/data/user.go
@@ -0,0 +1,292 @@
+package data
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "time"
+
+ "kra/internal/biz"
+
+ "github.com/google/uuid"
+ "gorm.io/gorm"
+)
+
+func (r *systemRepo) FindUserByUsername(ctx context.Context, username string) (*biz.User, error) {
+ var po userPO
+ if err := r.data.gormDB.WithContext(ctx).Where("username = ?", username).First(&po).Error; err != nil {
+ if errors.Is(err, gorm.ErrRecordNotFound) {
+ return nil, biz.ErrUserNotFound
+ }
+ return nil, err
+ }
+ return r.toBizUser(ctx, &po)
+}
+
+func (r *systemRepo) FindUserByID(ctx context.Context, id uint) (*biz.User, error) {
+ var po userPO
+ if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil {
+ if errors.Is(err, gorm.ErrRecordNotFound) {
+ return nil, biz.ErrUserNotFound
+ }
+ return nil, err
+ }
+ return r.toBizUser(ctx, &po)
+}
+
+func (r *systemRepo) toBizUser(ctx context.Context, po *userPO) (*biz.User, error) {
+ var authority authorityPO
+ if err := r.data.gormDB.WithContext(ctx).First(&authority, "authority_id = ?", po.AuthorityID).Error; err != nil {
+ return nil, err
+ }
+ var authorityPOs []authorityPO
+ if err := r.data.gormDB.WithContext(ctx).Table("sys_authorities").Joins("JOIN sys_user_authority ON sys_user_authority.sys_authority_authority_id = sys_authorities.authority_id").Where("sys_user_authority.sys_user_id = ? AND sys_authorities.deleted_at IS NULL", po.ID).Find(&authorityPOs).Error; err != nil {
+ return nil, err
+ }
+ authorities := make([]biz.Authority, 0, len(authorityPOs))
+ for _, item := range authorityPOs {
+ authorities = append(authorities, toBizAuthority(item))
+ }
+ setting := map[string]any(nil)
+ if po.OriginSetting != "" {
+ _ = json.Unmarshal([]byte(po.OriginSetting), &setting)
+ }
+ var departmentPOs []departmentPO
+ if err := r.data.gormDB.WithContext(ctx).Table("sys_departments").Joins("JOIN sys_user_departments ON sys_user_departments.sys_department_id = sys_departments.id").Where("sys_user_departments.sys_user_id = ? AND sys_departments.deleted_at IS NULL", po.ID).Find(&departmentPOs).Error; err != nil {
+ return nil, err
+ }
+ departments := make([]biz.Department, 0, len(departmentPOs))
+ var primary *biz.Department
+ for _, item := range departmentPOs {
+ value := deptFromPO(item)
+ departments = append(departments, *value)
+ if item.ID == po.DeptID {
+ copy := *value
+ primary = ©
+ }
+ }
+ var positionPOs []positionPO
+ if err := r.data.gormDB.WithContext(ctx).Table("sys_positions").Joins("JOIN sys_user_positions ON sys_user_positions.sys_position_id = sys_positions.id").Where("sys_user_positions.sys_user_id = ? AND sys_positions.deleted_at IS NULL", po.ID).Find(&positionPOs).Error; err != nil {
+ return nil, err
+ }
+ positions := make([]biz.Position, 0, len(positionPOs))
+ for _, item := range positionPOs {
+ positions = append(positions, *posFromPO(item))
+ }
+ 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, Authority: toBizAuthority(authority), Authorities: authorities, DeptID: po.DeptID, Department: primary, Departments: departments, Positions: positions, Phone: po.Phone, Email: po.Email, Enable: po.Enable, OriginSetting: setting, MustChangePassword: po.MustChangePassword, PasswordUpdatedAt: po.PasswordUpdatedAt}, nil
+}
+
+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 (r *systemRepo) MenusByAuthority(ctx context.Context, authorityID uint) ([]*biz.Menu, error) {
+ var pos []menuPO
+ err := r.data.gormDB.WithContext(ctx).Table("sys_base_menus").
+ 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.deleted_at IS NULL", authorityID).
+ Order("sys_base_menus.sort asc, sys_base_menus.id asc").Scan(&pos).Error
+ if err != nil {
+ return nil, err
+ }
+ byID := make(map[uint]*biz.Menu, 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{}}
+ var parameters []menuParameterPO
+ if err := r.data.gormDB.WithContext(ctx).Where("sys_base_menu_id = ?", po.ID).Order("id").Find(¶meters).Error; err != nil {
+ return nil, err
+ }
+ for _, parameter := range parameters {
+ 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 {
+ return nil, err
+ }
+ for _, button := range buttons {
+ 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 {
+ menu := byID[po.ID]
+ if parent := byID[po.ParentID]; parent != nil {
+ parent.Children = append(parent.Children, menu)
+ } else {
+ roots = append(roots, menu)
+ }
+ }
+ return roots, nil
+}
+
+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 != "" {
+ db = db.Where("username LIKE ?", "%"+filter.Username+"%")
+ }
+ if filter.NickName != "" {
+ db = db.Where("nick_name LIKE ?", "%"+filter.NickName+"%")
+ }
+ if filter.Phone != "" {
+ db = db.Where("phone LIKE ?", "%"+filter.Phone+"%")
+ }
+ 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}
+ if allowed[filter.OrderKey] {
+ order = filter.OrderKey
+ if filter.Desc {
+ order += " desc"
+ }
+ }
+ }
+ var total int64
+ if err := db.Count(&total).Error; err != nil {
+ return nil, 0, err
+ }
+ var pos []userPO
+ if err := db.Order(order).Offset((page - 1) * pageSize).Limit(pageSize).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
+}
+
+func (r *systemRepo) CreateUser(ctx context.Context, user *biz.User) (*biz.User, error) {
+ if user.UUID == "" {
+ user.UUID = uuid.NewString()
+ }
+ if user.Enable == 0 {
+ user.Enable = 1
+ }
+ if user.AuthorityID == 0 {
+ user.AuthorityID = 888
+ }
+ if user.NickName == "" {
+ user.NickName = "系统用户"
+ }
+ 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 err := r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ if err := tx.Create(&po).Error; err != nil {
+ return err
+ }
+ return tx.Create(&userAuthorityPO{SysUserID: po.ID, SysAuthorityAuthorityID: po.AuthorityID}).Error
+ }); err != nil {
+ return nil, err
+ }
+ return r.FindUserByID(ctx, po.ID)
+}
+
+func (r *systemRepo) UpdateUser(ctx context.Context, 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
+}
+func (r *systemRepo) DeleteUser(ctx context.Context, id 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
+ }
+ if err := tx.Where("sys_user_id = ?", id).Delete(&userDepartmentPO{}).Error; err != nil {
+ return err
+ }
+ if err := tx.Where("sys_user_id = ?", id).Delete(&userPositionPO{}).Error; err != nil {
+ return err
+ }
+ return tx.Delete(&userPO{}, id).Error
+ })
+}
+func (r *systemRepo) UpdatePassword(ctx context.Context, id uint, password string, clearMustChange bool) error {
+ now := time.Now()
+ updates := map[string]any{"password": password, "password_updated_at": now}
+ if clearMustChange {
+ updates["must_change_password"] = false
+ }
+ return r.data.gormDB.WithContext(ctx).Model(&userPO{}).Where("id = ?", id).Updates(updates).Error
+}
+func (r *systemRepo) ListAuthorities(ctx context.Context) ([]*biz.Authority, error) {
+ var pos []authorityPO
+ if err := r.data.gormDB.WithContext(ctx).Order("authority_id").Find(&pos).Error; err != nil {
+ return nil, err
+ }
+ result := make([]*biz.Authority, 0, len(pos))
+ for _, po := range pos {
+ a := toBizAuthority(po)
+ result = append(result, &a)
+ }
+ return result, nil
+}
+
+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))
+ 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("角色不存在")
+ }
+ 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 {
+ 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 {
+ return err
+ }
+ if count == 0 {
+ return errors.New("该用户无此角色")
+ }
+ return r.data.gormDB.WithContext(ctx).Model(&userPO{}).Where("id = ?", id).Update("authority_id", authorityID).Error
+}
+
+func (r *systemRepo) SetUserSetting(ctx context.Context, id uint, setting map[string]any) error {
+ value, err := json.Marshal(setting)
+ if err != nil {
+ return err
+ }
+ return r.data.gormDB.WithContext(ctx).Model(&userPO{}).Where("id = ?", id).Update("origin_setting", string(value)).Error
+}
+
+// ActiveName temporarily carries the parent menu name during seeding. It is
+// cleared from responses by the service for these records.
diff --git a/internal/server/audit_middleware.go b/internal/server/audit_middleware.go
deleted file mode 100644
index 199f9bc..0000000
--- a/internal/server/audit_middleware.go
+++ /dev/null
@@ -1,101 +0,0 @@
-package server
-
-import (
- "bytes"
- "encoding/json"
- "github.com/gin-gonic/gin"
- "io"
- "kra/internal/biz"
- "kra/internal/service"
- "strings"
- "time"
-)
-
-type captureWriter struct {
- gin.ResponseWriter
- body bytes.Buffer
-}
-
-func (w *captureWriter) Write(data []byte) (int, error) {
- if w.body.Len() < 32768 {
- remaining := 32768 - w.body.Len()
- if len(data) > remaining {
- _, _ = w.body.Write(data[:remaining])
- } else {
- _, _ = w.body.Write(data)
- }
- }
- return w.ResponseWriter.Write(data)
-}
-func redactJSON(raw []byte) string {
- if len(raw) == 0 {
- return ""
- }
- if len(raw) > 32768 {
- raw = raw[:32768]
- }
- var value any
- if json.Unmarshal(raw, &value) != nil {
- return string(raw)
- }
- var clean func(any)
- clean = func(current any) {
- switch v := current.(type) {
- case map[string]any:
- for key, item := range v {
- lower := strings.ToLower(key)
- if strings.Contains(lower, "password") || strings.Contains(lower, "token") || strings.Contains(lower, "secret") {
- v[key] = "******"
- } else {
- clean(item)
- }
- }
- case []any:
- for _, item := range v {
- clean(item)
- }
- }
- }
- clean(value)
- encoded, _ := json.Marshal(value)
- return string(encoded)
-}
-func operationAudit(svc *service.AuditService) gin.HandlerFunc {
- return func(c *gin.Context) {
- if isBootstrapPath(c.Request.URL.Path) || c.Request.Method == "GET" || c.Request.Method == "HEAD" || c.Request.Method == "OPTIONS" {
- c.Next()
- return
- }
- path := c.Request.URL.Path
- if strings.Contains(path, "sysOperationRecord") || strings.Contains(path, "sysLoginLog") || strings.Contains(path, "dataAccessLog") {
- c.Next()
- return
- }
- var requestBody []byte
- if c.Request.Body != nil {
- requestBody, _ = io.ReadAll(io.LimitReader(c.Request.Body, 32769))
- c.Request.Body = io.NopCloser(bytes.NewReader(requestBody))
- }
- writer := &captureWriter{ResponseWriter: c.Writer}
- c.Writer = writer
- started := time.Now()
- c.Next()
- userID := uint(0)
- if claims := currentClaims(c); claims != nil {
- userID = claims.ID
- }
- requestID, _ := c.Get("request_id")
- status := c.Writer.Status()
- errorMessage := ""
- if status >= 400 {
- errorMessage = writer.body.String()
- }
- _ = svc.RecordOperation(c.Request.Context(), &biz.OperationRecord{IP: c.ClientIP(), Method: c.Request.Method, Path: path, Status: status, LatencyMS: time.Since(started).Milliseconds(), Agent: c.Request.UserAgent(), ErrorMessage: errorMessage, Body: redactJSON(requestBody), Response: redactJSON(writer.body.Bytes()), UserID: userID, RequestID: toString(requestID), TraceID: c.GetHeader("traceparent"), DeviceID: c.GetHeader("X-Device-Id")})
- }
-}
-func toString(value any) string {
- if text, ok := value.(string); ok {
- return text
- }
- return ""
-}
diff --git a/internal/server/audit_routes.go b/internal/server/audit_routes.go
deleted file mode 100644
index 2986bcd..0000000
--- a/internal/server/audit_routes.go
+++ /dev/null
@@ -1,263 +0,0 @@
-package server
-
-import (
- "github.com/gin-gonic/gin"
- "kra/internal/biz"
- "kra/internal/service"
- "strconv"
-)
-
-func idsFromQuery(c *gin.Context) []uint {
- values := c.QueryArray("IDs[]")
- if len(values) == 0 {
- values = c.QueryArray("ids[]")
- }
- ids := make([]uint, 0, len(values))
- for _, v := range values {
- id, _ := strconv.ParseUint(v, 10, 64)
- if id > 0 {
- ids = append(ids, uint(id))
- }
- }
- return ids
-}
-func registerAuditRoutes(group, public *gin.RouterGroup, svc *service.AuditService) {
- operations := group.Group("/sysOperationRecord")
- operations.GET("/getSysOperationRecordList", func(c *gin.Context) {
- page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
- size, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
- status, _ := strconv.Atoi(c.Query("status"))
- userID, _ := strconv.ParseUint(c.Query("user_id"), 10, 64)
- items, total, err := svc.Operations(c.Request.Context(), page, size, &biz.OperationRecord{Path: c.Query("path"), Method: c.Query("method"), Status: status, IP: c.Query("ip"), UserID: uint(userID)})
- if err != nil {
- fail(c, "获取失败")
- return
- }
- writeResult(c, codeSuccess, pageResult{List: items, Total: total, Page: page, PageSize: size}, "获取成功")
- })
- operations.GET("/findSysOperationRecord", func(c *gin.Context) {
- id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
- item, err := svc.Operation(c.Request.Context(), uint(id))
- if err != nil {
- fail(c, "查询失败")
- return
- }
- writeResult(c, codeSuccess, gin.H{"resysOperationRecord": item}, "查询成功")
- })
- operations.DELETE("/deleteSysOperationRecord", func(c *gin.Context) {
- var req struct {
- ID uint `json:"ID"`
- }
- if c.ShouldBindJSON(&req) != nil || req.ID == 0 {
- fail(c, "参数错误")
- return
- }
- if err := svc.DeleteOperations(c.Request.Context(), []uint{req.ID}); err != nil {
- fail(c, "删除失败")
- return
- }
- ok(c)
- })
- operations.DELETE("/deleteSysOperationRecordByIds", func(c *gin.Context) {
- var req struct {
- IDs []uint `json:"ids"`
- }
- if c.ShouldBindJSON(&req) != nil || len(req.IDs) == 0 {
- fail(c, "参数错误")
- return
- }
- if err := svc.DeleteOperations(c.Request.Context(), req.IDs); err != nil {
- fail(c, "删除失败")
- return
- }
- ok(c)
- })
- logins := group.Group("/sysLoginLog")
- logins.GET("/getLoginLogList", func(c *gin.Context) {
- page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
- size, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
- items, total, err := svc.Logins(c.Request.Context(), page, size, &biz.LoginLog{Username: c.Query("username"), IP: c.Query("ip")})
- if err != nil {
- fail(c, "获取失败")
- return
- }
- writeResult(c, codeSuccess, pageResult{List: items, Total: total, Page: page, PageSize: size}, "获取成功")
- })
- logins.GET("/findLoginLog", func(c *gin.Context) {
- id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
- item, err := svc.Login(c.Request.Context(), uint(id))
- if err != nil {
- fail(c, "查询失败")
- return
- }
- writeResult(c, codeSuccess, item, "查询成功")
- })
- logins.DELETE("/deleteLoginLog", func(c *gin.Context) {
- var req struct {
- ID uint `json:"ID"`
- }
- if c.ShouldBindJSON(&req) != nil || req.ID == 0 {
- fail(c, "参数错误")
- return
- }
- if err := svc.DeleteLogins(c.Request.Context(), []uint{req.ID}); err != nil {
- fail(c, "删除失败")
- return
- }
- ok(c)
- })
- logins.DELETE("/deleteLoginLogByIds", func(c *gin.Context) {
- var req struct {
- IDs []uint `json:"ids"`
- }
- if c.ShouldBindJSON(&req) != nil || len(req.IDs) == 0 {
- fail(c, "参数错误")
- return
- }
- if err := svc.DeleteLogins(c.Request.Context(), req.IDs); err != nil {
- fail(c, "删除失败")
- return
- }
- ok(c)
- })
- access := group.Group("/dataAccessLog")
- access.POST("/getDataAccessLogList", func(c *gin.Context) {
- var req struct {
- Page int `json:"page"`
- PageSize int `json:"pageSize"`
- EventType string `json:"eventType"`
- TargetTable string `json:"targetTable"`
- UserID uint `json:"userId"`
- }
- if c.ShouldBindJSON(&req) != nil {
- fail(c, "参数错误")
- return
- }
- items, total, err := svc.DataAccess(c.Request.Context(), req.Page, req.PageSize, &biz.DataAccessLog{EventType: req.EventType, TargetTable: req.TargetTable, UserID: req.UserID})
- if err != nil {
- fail(c, "获取失败")
- return
- }
- writeResult(c, codeSuccess, pageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功")
- })
- access.DELETE("/deleteDataAccessLogByIds", func(c *gin.Context) {
- var req struct {
- IDs []uint `json:"ids"`
- }
- if c.ShouldBindJSON(&req) != nil {
- fail(c, "参数错误")
- return
- }
- if err := svc.DeleteDataAccess(c.Request.Context(), req.IDs); err != nil {
- fail(c, "删除失败")
- return
- }
- ok(c)
- })
- viewer := group.Group("/logViewer")
- viewer.GET("/dates", func(c *gin.Context) {
- data, err := svc.LogDates(c.Request.Context(), c.Query("month"))
- if err != nil {
- fail(c, err.Error())
- return
- }
- writeResult(c, codeSuccess, data, "获取成功")
- })
- viewer.GET("/files", func(c *gin.Context) {
- data, err := svc.LogFiles(c.Request.Context(), c.Query("date"))
- if err != nil {
- fail(c, err.Error())
- return
- }
- writeResult(c, codeSuccess, data, "获取成功")
- })
- viewer.GET("/content", func(c *gin.Context) {
- var cursor *int64
- if raw := c.Query("cursor"); raw != "" {
- value, err := strconv.ParseInt(raw, 10, 64)
- if err != nil || value < 0 {
- fail(c, "参数错误")
- return
- }
- cursor = &value
- }
- data, err := svc.LogContent(c.Request.Context(), c.Query("date"), c.Query("path"), cursor)
- if err != nil {
- fail(c, err.Error())
- return
- }
- writeResult(c, codeSuccess, data, "获取成功")
- })
- errorsRoute := group.Group("/sysError")
- errorsRoute.DELETE("/deleteSysError", func(c *gin.Context) {
- id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
- if err := svc.DeleteErrors(c.Request.Context(), []uint{uint(id)}); err != nil {
- fail(c, "删除失败")
- return
- }
- ok(c)
- })
- errorsRoute.DELETE("/deleteSysErrorByIds", func(c *gin.Context) {
- if err := svc.DeleteErrors(c.Request.Context(), idsFromQuery(c)); err != nil {
- fail(c, "删除失败")
- return
- }
- ok(c)
- })
- errorsRoute.PUT("/updateSysError", func(c *gin.Context) {
- var req struct {
- ID uint `json:"ID"`
- Form string `json:"form"`
- Info string `json:"info"`
- Level string `json:"level"`
- Solution string `json:"solution"`
- Status string `json:"status"`
- }
- if c.ShouldBindJSON(&req) != nil {
- fail(c, "参数错误")
- return
- }
- if err := svc.UpdateError(c.Request.Context(), &biz.ErrorRecord{ID: req.ID, Form: req.Form, Info: req.Info, Level: req.Level, Solution: req.Solution, Status: req.Status}); err != nil {
- fail(c, "更新失败")
- return
- }
- ok(c)
- })
- errorsRoute.GET("/findSysError", func(c *gin.Context) {
- id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
- item, err := svc.Error(c.Request.Context(), uint(id))
- if err != nil {
- fail(c, "查询失败")
- return
- }
- writeResult(c, codeSuccess, gin.H{"resysError": item}, "查询成功")
- })
- errorsRoute.GET("/getSysErrorList", func(c *gin.Context) {
- page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
- size, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
- items, total, err := svc.Errors(c.Request.Context(), page, size, &biz.ErrorRecord{Form: c.Query("form"), Level: c.Query("level"), Status: c.Query("status")})
- if err != nil {
- fail(c, "获取失败")
- return
- }
- writeResult(c, codeSuccess, pageResult{List: items, Total: total, Page: page, PageSize: size}, "获取成功")
- })
- public.Group("/sysError").POST("/createSysError", func(c *gin.Context) {
- var req struct {
- Form string `json:"form"`
- Info string `json:"info"`
- Level string `json:"level"`
- RequestID string `json:"request_id"`
- TraceID string `json:"trace_id"`
- }
- if c.ShouldBindJSON(&req) != nil || req.Form == "" {
- fail(c, "参数错误")
- return
- }
- if err := svc.CreateError(c.Request.Context(), &biz.ErrorRecord{Form: req.Form, Info: req.Info, Level: req.Level, RequestID: req.RequestID, TraceID: req.TraceID}); err != nil {
- fail(c, "创建失败")
- return
- }
- ok(c)
- })
-}
diff --git a/internal/server/export_routes.go b/internal/server/export_routes.go
deleted file mode 100644
index 6c21f74..0000000
--- a/internal/server/export_routes.go
+++ /dev/null
@@ -1,224 +0,0 @@
-package server
-
-import (
- "encoding/json"
- "github.com/gin-gonic/gin"
- "github.com/google/uuid"
- "kra/internal/biz"
- "kra/internal/service"
- "net/http"
- "net/url"
- "strconv"
- "strings"
- "time"
-)
-
-type exportTemplateRequest struct {
- ID uint `json:"ID"`
- DBName string `json:"dbName"`
- Name string `json:"name"`
- TableName string `json:"tableName"`
- TemplateID string `json:"templateID"`
- TemplateInfo string `json:"templateInfo"`
- SQL string `json:"sql"`
- ImportSQL string `json:"importSql"`
- Limit *int `json:"limit"`
- Order string `json:"order"`
- Conditions []struct {
- From string `json:"from"`
- Column string `json:"column"`
- Operator string `json:"operator"`
- } `json:"conditions"`
- Joins []struct {
- Join string `json:"joins"`
- Table string `json:"table"`
- On string `json:"on"`
- } `json:"joinTemplate"`
-}
-
-func exportTemplateDomain(v exportTemplateRequest) *biz.ExportTemplate {
- out := &biz.ExportTemplate{ID: v.ID, 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}
- for _, x := range v.Conditions {
- out.Conditions = append(out.Conditions, biz.ExportCondition{From: x.From, Column: x.Column, Operator: x.Operator})
- }
- for _, x := range v.Joins {
- out.Joins = append(out.Joins, biz.ExportJoin{Join: x.Join, Table: x.Table, On: x.On})
- }
- return out
-}
-func exportParams(values url.Values) map[string]string {
- out := map[string]string{}
- nested, _ := url.ParseQuery(values.Get("params"))
- for key, items := range nested {
- if len(items) > 0 {
- out[key] = items[0]
- }
- }
- for key, items := range values {
- if key != "params" && len(items) > 0 {
- out[key] = items[0]
- }
- }
- return out
-}
-
-type exportToken struct {
- TemplateID string `json:"templateID"`
- Params map[string]string `json:"params"`
- Blank bool `json:"blank"`
-}
-
-func registerExportRoutes(group, public *gin.RouterGroup, system *service.SystemService, svc *service.ExportService) {
- router := group.Group("/sysExportTemplate")
- router.POST("/createSysExportTemplate", func(c *gin.Context) {
- var req exportTemplateRequest
- if c.ShouldBindJSON(&req) != nil || req.Name == "" || req.TemplateID == "" {
- fail(c, "参数错误")
- return
- }
- if err := svc.Create(c.Request.Context(), exportTemplateDomain(req)); err != nil {
- fail(c, "创建失败:"+err.Error())
- return
- }
- ok(c)
- })
- router.PUT("/updateSysExportTemplate", func(c *gin.Context) {
- var req exportTemplateRequest
- if c.ShouldBindJSON(&req) != nil || req.ID == 0 {
- fail(c, "参数错误")
- return
- }
- if err := svc.Update(c.Request.Context(), exportTemplateDomain(req)); err != nil {
- fail(c, "更新失败:"+err.Error())
- return
- }
- ok(c)
- })
- router.DELETE("/deleteSysExportTemplate", func(c *gin.Context) {
- var req struct {
- ID uint `json:"ID"`
- }
- if c.ShouldBindJSON(&req) != nil {
- fail(c, "参数错误")
- return
- }
- if err := svc.Delete(c.Request.Context(), []uint{req.ID}); err != nil {
- fail(c, "删除失败")
- return
- }
- ok(c)
- })
- router.DELETE("/deleteSysExportTemplateByIds", func(c *gin.Context) {
- var req struct {
- IDs []uint `json:"ids"`
- }
- if c.ShouldBindJSON(&req) != nil {
- fail(c, "参数错误")
- return
- }
- if err := svc.Delete(c.Request.Context(), req.IDs); err != nil {
- fail(c, "删除失败")
- return
- }
- ok(c)
- })
- router.GET("/findSysExportTemplate", func(c *gin.Context) {
- id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
- item, err := svc.Template(c.Request.Context(), uint(id), "")
- if err != nil {
- fail(c, "查询失败")
- return
- }
- writeResult(c, codeSuccess, gin.H{"resysExportTemplate": item}, "查询成功")
- })
- router.GET("/getSysExportTemplateList", func(c *gin.Context) {
- page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
- size, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
- items, total, err := svc.Templates(c.Request.Context(), page, size, &biz.ExportTemplate{Name: c.Query("name"), TableName: c.Query("tableName"), TemplateID: c.Query("templateID")})
- if err != nil {
- fail(c, "获取失败")
- return
- }
- writeResult(c, codeSuccess, pageResult{List: items, Total: total, Page: page, PageSize: size}, "获取成功")
- })
- router.GET("/previewSQL", func(c *gin.Context) {
- sql, err := svc.Preview(c.Request.Context(), c.Query("templateID"), exportParams(c.Request.URL.Query()))
- if err != nil {
- fail(c, "预览失败:"+err.Error())
- return
- }
- writeResult(c, codeSuccess, gin.H{"sql": sql}, "获取成功")
- })
- issue := func(blank bool) gin.HandlerFunc {
- return func(c *gin.Context) {
- tid := c.Query("templateID")
- if tid == "" {
- fail(c, "模板ID不能为空")
- return
- }
- token := strings.ReplaceAll(uuid.NewString(), "-", "")
- raw, _ := json.Marshal(exportToken{TemplateID: tid, Params: exportParams(c.Request.URL.Query()), Blank: blank})
- if err := system.CacheSet(c.Request.Context(), "export:"+token, string(raw), 5*time.Minute); err != nil {
- fail(c, "导出令牌创建失败")
- return
- }
- writeResult(c, codeSuccess, gin.H{"token": token}, "获取成功")
- }
- }
- router.GET("/exportExcel", issue(false))
- router.GET("/exportTemplate", issue(true))
- router.POST("/importExcel", func(c *gin.Context) {
- file, err := c.FormFile("file")
- if err != nil {
- fail(c, "请选择导入文件")
- return
- }
- opened, err := file.Open()
- if err != nil {
- fail(c, "读取文件失败")
- return
- }
- defer opened.Close()
- tid := c.PostForm("templateID")
- if tid == "" {
- tid = c.Query("templateID")
- }
- if err = svc.Import(c.Request.Context(), tid, opened); err != nil {
- fail(c, err.Error())
- return
- }
- ok(c)
- })
- pub := public.Group("/sysExportTemplate")
- download := func(expectBlank bool) gin.HandlerFunc {
- return func(c *gin.Context) {
- token := c.Query("token")
- raw, okValue, err := system.CacheGet(c.Request.Context(), "export:"+token)
- if err != nil || !okValue {
- fail(c, "导出令牌无效或已过期")
- return
- }
- _ = system.CacheDelete(c.Request.Context(), "export:"+token)
- var value exportToken
- if json.Unmarshal([]byte(raw), &value) != nil || value.Blank != expectBlank {
- fail(c, "导出令牌无效")
- return
- }
- var data []byte
- var name string
- if expectBlank {
- data, name, err = svc.ExportBlankTemplate(c.Request.Context(), value.TemplateID)
- } else {
- data, name, err = svc.Export(c.Request.Context(), value.TemplateID, value.Params)
- }
- if err != nil {
- fail(c, "导出失败:"+err.Error())
- return
- }
- c.Header("Content-Disposition", "attachment; filename*=UTF-8''"+url.QueryEscape(name))
- c.Data(http.StatusOK, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", data)
- }
- }
- pub.GET("/exportExcelByToken", download(false))
- pub.GET("/exportTemplateByToken", download(true))
-}
diff --git a/internal/server/gin.go b/internal/server/gin.go
index 2969222..01c85dd 100644
--- a/internal/server/gin.go
+++ b/internal/server/gin.go
@@ -7,21 +7,16 @@ import (
"net"
"net/http"
"os"
- "strconv"
"strings"
- "time"
- "kra/internal/biz"
"kra/internal/conf"
"kra/internal/server/handler"
+ "kra/internal/server/httpx"
servermiddleware "kra/internal/server/middleware"
serverrouter "kra/internal/server/router"
"kra/internal/service"
- "kra/internal/worker"
- "kra/pkg/adminauth"
"github.com/gin-gonic/gin"
- "github.com/mojocn/base64Captcha"
)
// GinServer adapts Gin to the Kratos transport.Server lifecycle.
@@ -33,34 +28,38 @@ type GinServer struct {
logger *slog.Logger
}
-func NewGinServer(c *conf.Server, config *conf.AdminBackend, svc *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, settings *service.SettingsService, versions *service.VersionService, exports *service.ExportService, audit *service.AuditService, media *service.MediaService, emails *service.EmailService, scheduler *worker.TaskScheduler, logger *slog.Logger) *GinServer {
+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 {
gin.SetMode(gin.ReleaseMode)
engine := gin.New()
- engine.Use(servermiddleware.RequestMeta(), emailErrorAlert(emails, logger), gin.Recovery(), securityRateLimit(svc, settings), operationAudit(audit))
+ engine.Use(servermiddleware.RequestMeta(), servermiddleware.EmailErrorAlert(emails, logger), gin.Recovery(), servermiddleware.SecurityRateLimit(system, settings), servermiddleware.OperationAudit(audit))
+
prefix := ""
if config != nil {
prefix = strings.TrimSuffix(config.RouterPrefix, "/")
}
public := engine.Group(prefix)
- public.GET("/health", func(c *gin.Context) {
- c.JSON(http.StatusOK, "ok")
- })
- registerPublicRoutes(public, engine, config, svc, settings, access, audit, scheduler)
+ public.GET("/health", func(c *gin.Context) { c.JSON(http.StatusOK, "ok") })
+ serverrouter.RegisterPublic(public, engine, publicHandler)
+
private := engine.Group(prefix)
private.Use(servermiddleware.Auth(config, settings), servermiddleware.MustChangePassword(), servermiddleware.AccessControl(access, audit))
- registerPrivateRoutes(private, svc, settings)
+ 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.RegisterPermission(private, permission)
serverrouter.RegisterOrganization(private, organization)
- registerSettingsRoutes(private, public, settings)
- registerSystemConfigRoutes(private, config, svc, settings, scheduler)
- registerVersionRoutes(private, versions)
- registerExportRoutes(private, public, svc, exports)
- registerAuditRoutes(private, public, audit)
+ serverrouter.RegisterDictionary(private, dictionary)
+ serverrouter.RegisterParameter(private, parameter)
+ serverrouter.RegisterAPIToken(private, apiToken)
+ serverrouter.RegisterSystemConfig(private, systemConfig)
+ serverrouter.RegisterVersion(private, version)
+ serverrouter.RegisterExport(private, public, export)
+ serverrouter.RegisterAudit(private, public, auditHandler)
serverrouter.RegisterTask(private, task)
- registerMediaRoutes(private, media)
+ serverrouter.RegisterMedia(private, media)
serverrouter.RegisterAnnouncement(private, public, announcement)
serverrouter.RegisterEmail(private, email)
@@ -68,11 +67,10 @@ func NewGinServer(c *conf.Server, config *conf.AdminBackend, svc *service.System
if serveLocalStorage(c, config) {
return
}
- fail(c, "请求的接口不存在")
+ httpx.Fail(c, "请求的接口不存在")
})
- network := "tcp"
- address := ":8000"
+ network, address := "tcp", ":8000"
if c != nil && c.Http != nil {
if c.Http.Network != "" {
network = c.Http.Network
@@ -81,13 +79,7 @@ func NewGinServer(c *conf.Server, config *conf.AdminBackend, svc *service.System
address = c.Http.Addr
}
}
-
- return &GinServer{
- network: network,
- address: address,
- server: &http.Server{Addr: address, Handler: engine},
- logger: logger,
- }
+ return &GinServer{network: network, address: address, server: &http.Server{Addr: address, Handler: engine}, logger: logger}
}
// serveLocalStorage resolves the local path for every request so a config
@@ -107,219 +99,6 @@ func serveLocalStorage(c *gin.Context, config *conf.AdminBackend) bool {
return true
}
-func currentClaims(c *gin.Context) *adminauth.Claims { return servermiddleware.Claims(c) }
-
-func registerPublicRoutes(group *gin.RouterGroup, engine *gin.Engine, config *conf.AdminBackend, svc *service.SystemService, settings *service.SettingsService, access *service.AccessService, audit *service.AuditService, scheduler *worker.TaskScheduler) {
- expiration := 3 * time.Minute
- keyLong, width, height := 6, 240, 80
- 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)
- }
- if config.Captcha.ImgWidth > 0 {
- width = int(config.Captcha.ImgWidth)
- }
- if config.Captcha.ImgHeight > 0 {
- height = int(config.Captcha.ImgHeight)
- }
- }
- store := &captchaStore{service: svc, expiration: expiration}
- base := group.Group("/base")
- base.POST("/captcha", func(c *gin.Context) {
- security, _ := 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(), svc, "login:fail:"+c.ClientIP())
- openCaptcha = failures >= security.CaptchaOpen
- }
- }
- if !openCaptcha {
- writeResult(c, codeSuccess, gin.H{"captchaId": "", "picPath": "", "captchaLength": keyLong, "openCaptcha": false}, "验证码获取成功")
- return
- }
- driver := base64Captcha.NewDriverDigit(height, width, keyLong, 0.7, 80)
- id, picture, _, err := base64Captcha.NewCaptcha(driver, store).Generate()
- if err != nil {
- fail(c, "验证码获取失败")
- return
- }
- writeResult(c, codeSuccess, gin.H{"captchaId": id, "picPath": picture, "captchaLength": keyLong, "openCaptcha": openCaptcha}, "验证码获取成功")
- })
- base.POST("/login", func(c *gin.Context) {
- var req struct {
- Username string `json:"username"`
- Password string `json:"password"`
- Captcha string `json:"captcha"`
- CaptchaID string `json:"captchaId"`
- }
- if err := c.ShouldBindJSON(&req); err != nil || req.Username == "" || req.Password == "" {
- fail(c, "请输入用户名和密码")
- return
- }
- security, _ := settings.CurrentSecurity(c.Request.Context())
- if security != nil && security.LockEnable {
- if _, locked, _ := svc.CacheGet(c.Request.Context(), "login:lock:"+req.Username); locked {
- fail(c, "账户已锁定,请稍后重试")
- return
- }
- }
- requireCaptcha := security == nil || security.CaptchaOpen == 0
- if security != nil && security.CaptchaOpen > 0 {
- failures, _ := cachedInt(c.Request.Context(), svc, "login:fail:"+c.ClientIP())
- requireCaptcha = failures >= security.CaptchaOpen
- }
- if requireCaptcha && (req.CaptchaID == "" || req.Captcha == "" || !store.Verify(req.CaptchaID, req.Captcha, true)) {
- fail(c, "验证码错误")
- return
- }
- result, err := svc.Login(c.Request.Context(), req.Username, req.Password)
- if err != nil {
- _ = audit.RecordLogin(c.Request.Context(), &biz.LoginLog{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
- }
- failures, _ := incrementCached(c.Request.Context(), svc, "login:fail:"+c.ClientIP(), ttl)
- if security != nil && security.LockEnable && failures >= security.LockThreshold {
- _ = svc.CacheSet(c.Request.Context(), "login:lock:"+req.Username, "1", time.Duration(security.LockDuration)*time.Minute)
- }
- fail(c, "用户名不存在或者密码错误")
- return
- }
- userID, _ := result.User["ID"].(uint)
- _ = audit.RecordLogin(c.Request.Context(), &biz.LoginLog{Username: req.Username, IP: c.ClientIP(), Status: true, Agent: c.Request.UserAgent(), UserID: userID})
- _ = svc.CacheDelete(c.Request.Context(), "login:fail:"+c.ClientIP())
- _ = svc.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})
- writeResult(c, codeSuccess, result, "登录成功")
- })
- initGroup := group.Group("/init")
- initGroup.POST("/checkdb", func(c *gin.Context) {
- initialized, err := svc.IsInitialized(c.Request.Context())
- if err != nil {
- fail(c, "数据库状态检查失败")
- return
- }
- message := "数据库无需初始化"
- if !initialized {
- message = "前往初始化数据库"
- }
- writeResult(c, codeSuccess, gin.H{"needInit": !initialized}, message)
- })
- initGroup.POST("/initdb", func(c *gin.Context) {
- initialized, err := svc.IsInitialized(c.Request.Context())
- if err != nil {
- fail(c, "数据库状态检查失败")
- return
- }
- if initialized {
- fail(c, "数据库已初始化,无需重复初始化")
- return
- }
- var input service.DatabaseInit
- if err := c.ShouldBindJSON(&input); err != nil {
- fail(c, "数据库初始化参数无效")
- return
- }
- routes := engine.Routes()
- apis := make([]*biz.API, 0, len(routes))
- for _, route := range routes {
- apis = append(apis, &biz.API{Path: route.Path, Method: route.Method, APIGroup: routeGroup(route.Path)})
- }
- if err := svc.Initialize(c.Request.Context(), &input, apis); err != nil {
- fail(c, "自动创建数据库失败: "+err.Error())
- return
- }
- if err := scheduler.Reload(c.Request.Context()); err != nil {
- fail(c, "数据库已初始化,但定时任务加载失败:"+err.Error())
- return
- }
- writeResult(c, codeSuccess, gin.H{}, "自动创建数据库成功")
- })
-}
-
-func routeGroup(path string) string {
- parts := strings.Split(strings.Trim(path, "/"), "/")
- if len(parts) > 0 && parts[0] != "" {
- return parts[0]
- }
- return "base"
-}
-
-func cachedInt(ctx context.Context, svc *service.SystemService, key string) (int, error) {
- value, ok, err := svc.CacheGet(ctx, key)
- if err != nil || !ok {
- return 0, err
- }
- result, err := strconv.Atoi(value)
- return result, err
-}
-func incrementCached(ctx context.Context, svc *service.SystemService, key string, ttl time.Duration) (int, error) {
- value, err := svc.CacheIncrement(ctx, key, ttl)
- return int(value), err
-}
-func securityRateLimit(svc *service.SystemService, settings *service.SettingsService) gin.HandlerFunc {
- return func(c *gin.Context) {
- if isBootstrapPath(c.Request.URL.Path) {
- c.Next()
- return
- }
- cfg, err := settings.CurrentSecurity(c.Request.Context())
- if err != nil || cfg == nil || !cfg.LimitEnable {
- c.Next()
- return
- }
- window := cfg.LimitWindow
- if window < 1 {
- window = 60
- }
- bucket := time.Now().Unix() / int64(window)
- key := "rate:" + c.ClientIP() + ":" + strconv.FormatInt(bucket, 10)
- count, cacheErr := incrementCached(c.Request.Context(), svc, key, time.Duration(window+1)*time.Second)
- if cacheErr == nil && count > cfg.LimitCount {
- c.AbortWithStatusJSON(http.StatusTooManyRequests, response{Code: codeError, Data: nil, Msg: "请求过于频繁"})
- return
- }
- c.Next()
- }
-}
-
-func isBootstrapPath(path string) bool {
- return strings.HasSuffix(path, "/health") || strings.Contains(path, "/init/")
-}
-
-type captchaStore struct {
- service *service.SystemService
- expiration time.Duration
-}
-
-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)
-}
-func (s *captchaStore) Get(id string, clear bool) string {
- value, ok, err := s.service.CacheGet(context.Background(), s.key(id))
- if err != nil || !ok {
- return ""
- }
- if clear {
- _ = s.service.CacheDelete(context.Background(), s.key(id))
- }
- return value
-}
-func (s *captchaStore) Verify(id, answer string, clear bool) bool {
- if id == "" || answer == "" {
- return false
- }
- return strings.EqualFold(s.Get(id, clear), answer)
-}
-
type filesOnly struct{ http.FileSystem }
func (f filesOnly) Open(name string) (http.File, error) {
@@ -339,235 +118,6 @@ func (f filesOnly) Open(name string) (http.File, error) {
return file, nil
}
-func registerPrivateRoutes(group *gin.RouterGroup, svc *service.SystemService, settings *service.SettingsService) {
- user := group.Group("/user")
- user.POST("/getUserList", func(c *gin.Context) {
- var req struct {
- Page int `json:"page"`
- PageSize int `json:"pageSize"`
- Username string `json:"username"`
- NickName string `json:"nickName"`
- Phone string `json:"phone"`
- Email string `json:"email"`
- OrderKey string `json:"orderKey"`
- Desc bool `json:"desc"`
- }
- if c.ShouldBindJSON(&req) != nil {
- fail(c, "参数错误")
- return
- }
- list, total, err := svc.ListUsers(c.Request.Context(), req.Page, req.PageSize, &biz.UserListFilter{Username: req.Username, NickName: req.NickName, Phone: req.Phone, Email: req.Email, OrderKey: req.OrderKey, Desc: req.Desc})
- if err != nil {
- fail(c, "获取失败")
- return
- }
- writeResult(c, codeSuccess, pageResult{List: list, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功")
- })
- user.POST("/admin_register", func(c *gin.Context) {
- var req struct {
- Username string `json:"userName"`
- Password string `json:"passWord"`
- NickName string `json:"nickName"`
- HeaderImg string `json:"headerImg"`
- AuthorityID uint `json:"authorityId"`
- AuthorityIDs []uint `json:"authorityIds"`
- Enable int `json:"enable"`
- Phone string `json:"phone"`
- Email string `json:"email"`
- }
- if c.ShouldBindJSON(&req) != nil || req.Username == "" || req.Password == "" {
- fail(c, "参数错误")
- return
- }
- result, err := svc.CreateUser(c.Request.Context(), service.UserInput{Username: req.Username, Password: req.Password, NickName: req.NickName, HeaderImg: req.HeaderImg, AuthorityID: req.AuthorityID, AuthorityIDs: req.AuthorityIDs, Enable: req.Enable, Phone: req.Phone, Email: req.Email})
- if err != nil {
- fail(c, "注册失败")
- return
- }
- writeResult(c, codeSuccess, gin.H{"user": result}, "注册成功")
- })
- user.PUT("/setUserInfo", func(c *gin.Context) {
- var req struct {
- ID uint `json:"ID"`
- NickName string `json:"nickName"`
- HeaderImg string `json:"headerImg"`
- AuthorityID uint `json:"authorityId"`
- AuthorityIDs []uint `json:"authorityIds"`
- Enable int `json:"enable"`
- Phone string `json:"phone"`
- Email string `json:"email"`
- }
- if c.ShouldBindJSON(&req) != nil {
- fail(c, "参数错误")
- return
- }
- if err := svc.UpdateUser(c.Request.Context(), service.UserInput{ID: req.ID, NickName: req.NickName, HeaderImg: req.HeaderImg, AuthorityID: req.AuthorityID, AuthorityIDs: req.AuthorityIDs, Enable: req.Enable, Phone: req.Phone, Email: req.Email}); err != nil {
- fail(c, "修改失败")
- return
- }
- ok(c)
- })
- user.PUT("/setSelfInfo", func(c *gin.Context) {
- claims := currentClaims(c)
- if claims == nil {
- noAuth(c, "未登录或非法访问")
- return
- }
- var req struct {
- NickName string `json:"nickName"`
- HeaderImg string `json:"headerImg"`
- Phone string `json:"phone"`
- Email string `json:"email"`
- }
- if c.ShouldBindJSON(&req) != nil {
- fail(c, "参数错误")
- return
- }
- if err := svc.UpdateUser(c.Request.Context(), service.UserInput{ID: claims.ID, NickName: req.NickName, HeaderImg: req.HeaderImg, Phone: req.Phone, Email: req.Email, Enable: 1}); err != nil {
- fail(c, "修改失败")
- return
- }
- ok(c)
- })
- user.DELETE("/deleteUser", func(c *gin.Context) {
- var req struct {
- ID uint `form:"id" json:"id"`
- }
- _ = c.ShouldBind(&req)
- if req.ID == 0 {
- var body struct {
- ID uint `json:"ID"`
- }
- _ = c.ShouldBindJSON(&body)
- req.ID = body.ID
- }
- if err := svc.DeleteUser(c.Request.Context(), req.ID); err != nil {
- fail(c, "删除失败")
- return
- }
- ok(c)
- })
- user.POST("/resetPassword", func(c *gin.Context) {
- var req struct {
- ID uint `json:"ID"`
- Password string `json:"password"`
- }
- if c.ShouldBindJSON(&req) != nil {
- fail(c, "参数错误")
- return
- }
- if err := svc.ResetPassword(c.Request.Context(), req.ID, req.Password); err != nil {
- fail(c, "重置失败")
- return
- }
- ok(c)
- })
- user.POST("/changePassword", func(c *gin.Context) {
- claims := currentClaims(c)
- var req struct {
- Password string `json:"password"`
- NewPassword string `json:"newPassword"`
- }
- if claims == nil || c.ShouldBindJSON(&req) != nil {
- fail(c, "参数错误")
- return
- }
- if err := svc.ChangePassword(c.Request.Context(), claims.ID, req.Password, req.NewPassword); err != nil {
- fail(c, "修改失败,原密码与当前账户不符")
- return
- }
- ok(c)
- })
- user.PUT("/setSelfSetting", func(c *gin.Context) {
- claims := currentClaims(c)
- var setting map[string]any
- if claims == nil || c.ShouldBindJSON(&setting) != nil {
- fail(c, "参数错误")
- return
- }
- if err := svc.SetUserSetting(c.Request.Context(), claims.ID, setting); err != nil {
- fail(c, "设置失败")
- return
- }
- ok(c)
- })
- user.POST("/setUserAuthorities", func(c *gin.Context) {
- var req struct {
- ID uint `json:"ID"`
- AuthorityIDs []uint `json:"authorityIds"`
- }
- if c.ShouldBindJSON(&req) != nil || req.ID == 0 || len(req.AuthorityIDs) == 0 {
- fail(c, "参数错误")
- return
- }
- if err := svc.SetUserAuthorities(c.Request.Context(), req.ID, req.AuthorityIDs); err != nil {
- fail(c, "修改失败")
- return
- }
- ok(c)
- })
- user.POST("/setUserAuthority", func(c *gin.Context) {
- claims := currentClaims(c)
- var req struct {
- AuthorityID uint `json:"authorityId"`
- }
- if claims == nil || c.ShouldBindJSON(&req) != nil || req.AuthorityID == 0 {
- fail(c, "参数错误")
- return
- }
- login, err := svc.SwitchAuthority(c.Request.Context(), claims.ID, req.AuthorityID)
- if err != nil {
- fail(c, err.Error())
- return
- }
- c.Header("new-token", login.Token)
- c.Header("new-expires-at", strconv.FormatInt(login.ExpiresAt/1000, 10))
- maxAge := int(time.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})
- ok(c)
- })
- user.GET("/getUserInfo", func(c *gin.Context) {
- claims := currentClaims(c)
- if claims == nil {
- noAuth(c, "未登录或非法访问")
- return
- }
- result, err := svc.User(c.Request.Context(), claims.ID)
- if err != nil {
- fail(c, "获取失败")
- return
- }
- writeResult(c, codeSuccess, gin.H{"userInfo": result}, "获取成功")
- })
- menu := group.Group("/menu")
- menu.POST("/getMenu", func(c *gin.Context) {
- claims := currentClaims(c)
- if claims == nil {
- noAuth(c, "未登录或非法访问")
- return
- }
- menus, err := svc.Menus(c.Request.Context(), claims.AuthorityID)
- if err != nil {
- fail(c, "获取失败")
- return
- }
- writeResult(c, codeSuccess, gin.H{"menus": menus}, "获取成功")
- })
- jwt := group.Group("/jwt")
- jwt.POST("/jsonInBlacklist", func(c *gin.Context) {
- token := c.GetHeader("x-token")
- if token == "" {
- token, _ = c.Cookie("x-token")
- }
- if err := settings.BlacklistToken(c.Request.Context(), token); err != nil {
- fail(c, "退出失败")
- return
- }
- http.SetCookie(c.Writer, &http.Cookie{Name: "x-token", Value: "", Path: "/", MaxAge: -1, HttpOnly: true})
- ok(c)
- })
-}
-
func (s *GinServer) Start(ctx context.Context) error {
listener, err := net.Listen(s.network, s.address)
if err != nil {
diff --git a/internal/server/handler/api_token.go b/internal/server/handler/api_token.go
new file mode 100644
index 0000000..daac263
--- /dev/null
+++ b/internal/server/handler/api_token.go
@@ -0,0 +1,58 @@
+package handler
+
+import (
+ "kra/internal/server/httpx"
+ "kra/internal/service"
+ "kra/internal/service/dto"
+
+ "github.com/gin-gonic/gin"
+)
+
+type APIToken struct {
+ service *service.SettingsService
+}
+
+func NewAPIToken(service *service.SettingsService) *APIToken {
+ return &APIToken{service: service}
+}
+
+func (h *APIToken) Create(c *gin.Context) {
+ var req dto.CreateAPITokenRequest
+ if c.ShouldBindJSON(&req) != nil {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ token, err := h.service.CreateAPIToken(c.Request.Context(), req.UserID, req.AuthorityID, req.Days, req.Remark)
+ if err != nil {
+ httpx.Fail(c, "签发失败:"+err.Error())
+ return
+ }
+ httpx.Write(c, httpx.CodeSuccess, gin.H{"token": token}, "签发成功")
+}
+
+func (h *APIToken) List(c *gin.Context) {
+ var req dto.APITokenListRequest
+ if c.ShouldBindJSON(&req) != nil {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ items, total, err := h.service.APITokens(c.Request.Context(), req.Page, req.PageSize, req.UserID, req.Status)
+ if err != nil {
+ httpx.Fail(c, "获取失败")
+ return
+ }
+ httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功")
+}
+
+func (h *APIToken) Delete(c *gin.Context) {
+ var req dto.IDRequest
+ if c.ShouldBindJSON(&req) != nil {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ if err := h.service.DisableAPIToken(c.Request.Context(), req.ID); err != nil {
+ httpx.Fail(c, "作废失败")
+ return
+ }
+ httpx.OK(c)
+}
diff --git a/internal/server/handler/audit.go b/internal/server/handler/audit.go
new file mode 100644
index 0000000..74b9e64
--- /dev/null
+++ b/internal/server/handler/audit.go
@@ -0,0 +1,241 @@
+package handler
+
+import (
+ "strconv"
+
+ "kra/internal/server/httpx"
+ "kra/internal/service"
+ "kra/internal/service/dto"
+
+ "github.com/gin-gonic/gin"
+)
+
+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"))
+ return value, size
+}
+func IDsFromQuery(c *gin.Context) []uint {
+ values := c.QueryArray("IDs[]")
+ if len(values) == 0 {
+ values = c.QueryArray("ids[]")
+ }
+ ids := make([]uint, 0, len(values))
+ for _, value := range values {
+ id, _ := strconv.ParseUint(value, 10, 64)
+ if id > 0 {
+ ids = append(ids, uint(id))
+ }
+ }
+ return ids
+}
+
+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))
+ if err != nil {
+ httpx.Fail(c, "获取失败")
+ return
+ }
+ httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: p, PageSize: size}, "获取成功")
+}
+func (h *Audit) Operation(c *gin.Context) {
+ id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
+ item, err := h.service.Operation(c.Request.Context(), uint(id))
+ if err != nil {
+ httpx.Fail(c, "查询失败")
+ return
+ }
+ httpx.Write(c, httpx.CodeSuccess, gin.H{"resysOperationRecord": item}, "查询成功")
+}
+func (h *Audit) DeleteOperation(c *gin.Context) {
+ var req dto.IDRequest
+ if c.ShouldBindJSON(&req) != nil || req.ID == 0 {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ if err := h.service.DeleteOperations(c.Request.Context(), []uint{req.ID}); err != nil {
+ httpx.Fail(c, "删除失败")
+ return
+ }
+ httpx.OK(c)
+}
+func (h *Audit) DeleteOperations(c *gin.Context) {
+ var req dto.IDsRequest
+ if c.ShouldBindJSON(&req) != nil || len(req.IDs) == 0 {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ if err := h.service.DeleteOperations(c.Request.Context(), req.IDs); err != nil {
+ httpx.Fail(c, "删除失败")
+ return
+ }
+ httpx.OK(c)
+}
+
+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"))
+ if err != nil {
+ httpx.Fail(c, "获取失败")
+ return
+ }
+ httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: p, PageSize: size}, "获取成功")
+}
+func (h *Audit) Login(c *gin.Context) {
+ id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
+ item, err := h.service.Login(c.Request.Context(), uint(id))
+ if err != nil {
+ httpx.Fail(c, "查询失败")
+ return
+ }
+ httpx.Write(c, httpx.CodeSuccess, item, "查询成功")
+}
+func (h *Audit) DeleteLogin(c *gin.Context) {
+ var req dto.IDRequest
+ if c.ShouldBindJSON(&req) != nil || req.ID == 0 {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ if err := h.service.DeleteLogins(c.Request.Context(), []uint{req.ID}); err != nil {
+ httpx.Fail(c, "删除失败")
+ return
+ }
+ httpx.OK(c)
+}
+func (h *Audit) DeleteLogins(c *gin.Context) {
+ var req dto.IDsRequest
+ if c.ShouldBindJSON(&req) != nil || len(req.IDs) == 0 {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ if err := h.service.DeleteLogins(c.Request.Context(), req.IDs); err != nil {
+ httpx.Fail(c, "删除失败")
+ return
+ }
+ httpx.OK(c)
+}
+
+func (h *Audit) DataAccess(c *gin.Context) {
+ var req dto.DataAccessListRequest
+ if c.ShouldBindJSON(&req) != nil {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ items, total, err := h.service.DataAccessRequest(c.Request.Context(), &req)
+ if err != nil {
+ httpx.Fail(c, "获取失败")
+ return
+ }
+ httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功")
+}
+func (h *Audit) DeleteDataAccess(c *gin.Context) {
+ var req dto.IDsRequest
+ if c.ShouldBindJSON(&req) != nil {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ if err := h.service.DeleteDataAccess(c.Request.Context(), req.IDs); err != nil {
+ httpx.Fail(c, "删除失败")
+ return
+ }
+ httpx.OK(c)
+}
+
+func (h *Audit) LogDates(c *gin.Context) {
+ data, err := h.service.LogDates(c.Request.Context(), c.Query("month"))
+ if err != nil {
+ httpx.Fail(c, err.Error())
+ return
+ }
+ httpx.Write(c, httpx.CodeSuccess, data, "获取成功")
+}
+func (h *Audit) LogFiles(c *gin.Context) {
+ data, err := h.service.LogFiles(c.Request.Context(), c.Query("date"))
+ if err != nil {
+ httpx.Fail(c, err.Error())
+ return
+ }
+ httpx.Write(c, httpx.CodeSuccess, data, "获取成功")
+}
+func (h *Audit) LogContent(c *gin.Context) {
+ var cursor *int64
+ if raw := c.Query("cursor"); raw != "" {
+ value, err := strconv.ParseInt(raw, 10, 64)
+ if err != nil || value < 0 {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ cursor = &value
+ }
+ data, err := h.service.LogContent(c.Request.Context(), c.Query("date"), c.Query("path"), cursor)
+ if err != nil {
+ httpx.Fail(c, err.Error())
+ return
+ }
+ httpx.Write(c, httpx.CodeSuccess, data, "获取成功")
+}
+
+func (h *Audit) DeleteError(c *gin.Context) {
+ id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
+ if err := h.service.DeleteErrors(c.Request.Context(), []uint{uint(id)}); err != nil {
+ httpx.Fail(c, "删除失败")
+ return
+ }
+ httpx.OK(c)
+}
+func (h *Audit) DeleteErrors(c *gin.Context) {
+ if err := h.service.DeleteErrors(c.Request.Context(), IDsFromQuery(c)); err != nil {
+ httpx.Fail(c, "删除失败")
+ return
+ }
+ httpx.OK(c)
+}
+func (h *Audit) UpdateError(c *gin.Context) {
+ var req dto.ErrorRecordRequest
+ if c.ShouldBindJSON(&req) != nil {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ if err := h.service.UpdateErrorRequest(c.Request.Context(), &req); err != nil {
+ httpx.Fail(c, "更新失败")
+ return
+ }
+ httpx.OK(c)
+}
+func (h *Audit) Error(c *gin.Context) {
+ id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
+ item, err := h.service.Error(c.Request.Context(), uint(id))
+ if err != nil {
+ httpx.Fail(c, "查询失败")
+ return
+ }
+ httpx.Write(c, httpx.CodeSuccess, gin.H{"resysError": 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"))
+ if err != nil {
+ httpx.Fail(c, "获取失败")
+ return
+ }
+ httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: p, PageSize: size}, "获取成功")
+}
+func (h *Audit) CreateError(c *gin.Context) {
+ var req dto.ErrorRecordRequest
+ if c.ShouldBindJSON(&req) != nil || req.Form == "" {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ if err := h.service.CreateErrorRequest(c.Request.Context(), &req); err != nil {
+ httpx.Fail(c, "创建失败")
+ return
+ }
+ httpx.OK(c)
+}
diff --git a/internal/server/handler/dictionary.go b/internal/server/handler/dictionary.go
new file mode 100644
index 0000000..c07bc5b
--- /dev/null
+++ b/internal/server/handler/dictionary.go
@@ -0,0 +1,189 @@
+package handler
+
+import (
+ "strconv"
+
+ "kra/internal/server/httpx"
+ "kra/internal/service"
+ "kra/internal/service/dto"
+
+ "github.com/gin-gonic/gin"
+)
+
+type Dictionary struct{ service *service.SettingsService }
+
+func NewDictionary(service *service.SettingsService) *Dictionary {
+ return &Dictionary{service: service}
+}
+
+func (h *Dictionary) Create(c *gin.Context) {
+ var req dto.DictionaryRequest
+ if c.ShouldBindJSON(&req) != nil || req.Name == "" || req.Type == "" {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ if err := h.service.CreateDictionaryRequest(c.Request.Context(), &req); err != nil {
+ httpx.Fail(c, "创建失败:"+err.Error())
+ return
+ }
+ httpx.OK(c)
+}
+func (h *Dictionary) Update(c *gin.Context) {
+ var req dto.DictionaryRequest
+ if c.ShouldBindJSON(&req) != nil || req.ID == 0 {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ if err := h.service.UpdateDictionaryRequest(c.Request.Context(), &req); err != nil {
+ httpx.Fail(c, "更新失败")
+ return
+ }
+ httpx.OK(c)
+}
+func (h *Dictionary) Delete(c *gin.Context) {
+ var req dto.DictionaryRequest
+ if c.ShouldBindJSON(&req) != nil || req.ID == 0 {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ if err := h.service.DeleteDictionary(c.Request.Context(), req.ID); err != nil {
+ httpx.Fail(c, err.Error())
+ return
+ }
+ httpx.OK(c)
+}
+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)
+ if err != nil {
+ httpx.Fail(c, "查询失败")
+ return
+ }
+ httpx.Write(c, httpx.CodeSuccess, gin.H{"reSysDictionary": item}, "查询成功")
+}
+func (h *Dictionary) List(details bool) gin.HandlerFunc {
+ return func(c *gin.Context) {
+ p, size := page(c)
+ items, total, err := h.service.Dictionaries(c.Request.Context(), p, size, c.Query("name"), c.Query("type"), details)
+ if err != nil {
+ httpx.Fail(c, "获取失败")
+ return
+ }
+ httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: p, PageSize: size}, "获取成功")
+ }
+}
+func (h *Dictionary) Export(c *gin.Context) {
+ id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
+ item, err := h.service.Dictionary(c.Request.Context(), uint(id), "", true)
+ if err != nil {
+ httpx.Fail(c, "导出失败")
+ return
+ }
+ delete(item, "ID")
+ delete(item, "CreatedAt")
+ delete(item, "UpdatedAt")
+ delete(item, "DeletedAt")
+ httpx.Write(c, httpx.CodeSuccess, item, "导出成功")
+}
+func (h *Dictionary) Import(c *gin.Context) {
+ var req dto.ImportDictionaryRequest
+ if c.ShouldBindJSON(&req) != nil {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ if err := h.service.ImportDictionaryJSON(c.Request.Context(), req.JSON); err != nil {
+ httpx.Fail(c, err.Error())
+ return
+ }
+ httpx.OK(c)
+}
+
+func (h *Dictionary) CreateDetail(c *gin.Context) {
+ var req dto.DictionaryDetailRequest
+ if c.ShouldBindJSON(&req) != nil || req.DictionaryID == 0 {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ if err := h.service.CreateDictionaryDetailRequest(c.Request.Context(), &req); err != nil {
+ httpx.Fail(c, "创建失败")
+ return
+ }
+ httpx.OK(c)
+}
+func (h *Dictionary) UpdateDetail(c *gin.Context) {
+ var req dto.DictionaryDetailRequest
+ if c.ShouldBindJSON(&req) != nil || req.ID == 0 {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ if err := h.service.UpdateDictionaryDetailRequest(c.Request.Context(), &req); err != nil {
+ httpx.Fail(c, err.Error())
+ return
+ }
+ httpx.OK(c)
+}
+func (h *Dictionary) DeleteDetail(c *gin.Context) {
+ var req dto.DictionaryDetailRequest
+ if c.ShouldBindJSON(&req) != nil || req.ID == 0 {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ if err := h.service.DeleteDictionaryDetail(c.Request.Context(), req.ID); err != nil {
+ httpx.Fail(c, err.Error())
+ return
+ }
+ httpx.OK(c)
+}
+func (h *Dictionary) FindDetail(c *gin.Context) {
+ id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
+ item, err := h.service.DictionaryDetail(c.Request.Context(), uint(id))
+ if err != nil {
+ httpx.Fail(c, "查询失败")
+ return
+ }
+ httpx.Write(c, httpx.CodeSuccess, gin.H{"reSysDictionaryDetail": item}, "查询成功")
+}
+func (h *Dictionary) Details(c *gin.Context) {
+ p, size := 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"))
+ if err != nil {
+ httpx.Fail(c, "获取失败")
+ return
+ }
+ httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: p, PageSize: size}, "获取成功")
+}
+func (h *Dictionary) Tree(byType bool) gin.HandlerFunc {
+ return func(c *gin.Context) {
+ id, _ := strconv.ParseUint(c.Query("sysDictionaryID"), 10, 64)
+ typ := ""
+ if byType {
+ typ = c.Query("type")
+ }
+ items, err := h.service.DictionaryTree(c.Request.Context(), uint(id), typ)
+ if err != nil {
+ httpx.Fail(c, "获取失败")
+ return
+ }
+ httpx.Write(c, httpx.CodeSuccess, items, "获取成功")
+ }
+}
+func (h *Dictionary) DetailsByParent(c *gin.Context) {
+ parentID, _ := strconv.ParseUint(c.Query("parentID"), 10, 64)
+ dictionaryID, _ := strconv.ParseUint(c.Query("sysDictionaryID"), 10, 64)
+ items, err := h.service.DictionaryDetailsByParent(c.Request.Context(), uint(dictionaryID), uint(parentID))
+ if err != nil {
+ httpx.Fail(c, "获取失败")
+ return
+ }
+ httpx.Write(c, httpx.CodeSuccess, items, "获取成功")
+}
+func (h *Dictionary) Path(c *gin.Context) {
+ id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
+ item, err := h.service.DictionaryDetail(c.Request.Context(), uint(id))
+ if err != nil {
+ httpx.Fail(c, "获取失败")
+ return
+ }
+ httpx.Write(c, httpx.CodeSuccess, gin.H{"path": item["path"]}, "获取成功")
+}
diff --git a/internal/server/handler/export.go b/internal/server/handler/export.go
new file mode 100644
index 0000000..78752ec
--- /dev/null
+++ b/internal/server/handler/export.go
@@ -0,0 +1,193 @@
+package handler
+
+import (
+ "encoding/json"
+ "net/http"
+ "net/url"
+ "strconv"
+ "strings"
+ "time"
+
+ "kra/internal/server/httpx"
+ "kra/internal/service"
+ "kra/internal/service/dto"
+
+ "github.com/gin-gonic/gin"
+ "github.com/google/uuid"
+)
+
+type Export struct {
+ system *service.SystemService
+ service *service.ExportService
+}
+
+func NewExport(system *service.SystemService, service *service.ExportService) *Export {
+ return &Export{system: system, service: service}
+}
+
+type exportToken struct {
+ TemplateID string `json:"templateID"`
+ Params map[string]string `json:"params"`
+ Blank bool `json:"blank"`
+}
+
+func exportParams(values url.Values) map[string]string {
+ out := map[string]string{}
+ nested, _ := url.ParseQuery(values.Get("params"))
+ for key, items := range nested {
+ if len(items) > 0 {
+ out[key] = items[0]
+ }
+ }
+ for key, items := range values {
+ if key != "params" && len(items) > 0 {
+ out[key] = items[0]
+ }
+ }
+ return out
+}
+
+func (h *Export) Create(c *gin.Context) {
+ var req dto.ExportTemplateRequest
+ if c.ShouldBindJSON(&req) != nil || req.Name == "" || req.TemplateID == "" {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ if err := h.service.CreateRequest(c.Request.Context(), &req); err != nil {
+ httpx.Fail(c, "创建失败:"+err.Error())
+ return
+ }
+ httpx.OK(c)
+}
+func (h *Export) Update(c *gin.Context) {
+ var req dto.ExportTemplateRequest
+ if c.ShouldBindJSON(&req) != nil || req.ID == 0 {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ if err := h.service.UpdateRequest(c.Request.Context(), &req); err != nil {
+ httpx.Fail(c, "更新失败:"+err.Error())
+ return
+ }
+ httpx.OK(c)
+}
+func (h *Export) Delete(c *gin.Context) {
+ var req dto.IDRequest
+ if c.ShouldBindJSON(&req) != nil {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ if err := h.service.Delete(c.Request.Context(), []uint{req.ID}); err != nil {
+ httpx.Fail(c, "删除失败")
+ return
+ }
+ httpx.OK(c)
+}
+func (h *Export) DeleteMany(c *gin.Context) {
+ var req dto.IDsRequest
+ if c.ShouldBindJSON(&req) != nil {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ if err := h.service.Delete(c.Request.Context(), req.IDs); err != nil {
+ httpx.Fail(c, "删除失败")
+ return
+ }
+ httpx.OK(c)
+}
+func (h *Export) Find(c *gin.Context) {
+ id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
+ item, err := h.service.Template(c.Request.Context(), uint(id), "")
+ if err != nil {
+ httpx.Fail(c, "查询失败")
+ return
+ }
+ httpx.Write(c, httpx.CodeSuccess, gin.H{"resysExportTemplate": item}, "查询成功")
+}
+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"))
+ if err != nil {
+ httpx.Fail(c, "获取失败")
+ return
+ }
+ httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: p, PageSize: size}, "获取成功")
+}
+func (h *Export) Preview(c *gin.Context) {
+ sql, err := h.service.Preview(c.Request.Context(), c.Query("templateID"), exportParams(c.Request.URL.Query()))
+ if err != nil {
+ httpx.Fail(c, "预览失败:"+err.Error())
+ return
+ }
+ httpx.Write(c, httpx.CodeSuccess, gin.H{"sql": sql}, "获取成功")
+}
+
+func (h *Export) Issue(blank bool) gin.HandlerFunc {
+ return func(c *gin.Context) {
+ templateID := c.Query("templateID")
+ if templateID == "" {
+ httpx.Fail(c, "模板ID不能为空")
+ return
+ }
+ token := strings.ReplaceAll(uuid.NewString(), "-", "")
+ raw, _ := json.Marshal(exportToken{TemplateID: templateID, Params: exportParams(c.Request.URL.Query()), Blank: blank})
+ if err := h.system.CacheSet(c.Request.Context(), "export:"+token, string(raw), 5*time.Minute); err != nil {
+ httpx.Fail(c, "导出令牌创建失败")
+ return
+ }
+ httpx.Write(c, httpx.CodeSuccess, gin.H{"token": token}, "获取成功")
+ }
+}
+
+func (h *Export) Import(c *gin.Context) {
+ file, err := c.FormFile("file")
+ if err != nil {
+ httpx.Fail(c, "请选择导入文件")
+ return
+ }
+ opened, err := file.Open()
+ if err != nil {
+ httpx.Fail(c, "读取文件失败")
+ return
+ }
+ defer opened.Close()
+ templateID := c.PostForm("templateID")
+ if templateID == "" {
+ templateID = c.Query("templateID")
+ }
+ if err = h.service.Import(c.Request.Context(), templateID, opened); err != nil {
+ httpx.Fail(c, err.Error())
+ return
+ }
+ httpx.OK(c)
+}
+
+func (h *Export) Download(expectBlank bool) gin.HandlerFunc {
+ return func(c *gin.Context) {
+ token := c.Query("token")
+ raw, ok, err := h.system.CacheGet(c.Request.Context(), "export:"+token)
+ if err != nil || !ok {
+ httpx.Fail(c, "导出令牌无效或已过期")
+ return
+ }
+ _ = h.system.CacheDelete(c.Request.Context(), "export:"+token)
+ var value exportToken
+ if json.Unmarshal([]byte(raw), &value) != nil || value.Blank != expectBlank {
+ httpx.Fail(c, "导出令牌无效")
+ return
+ }
+ var data []byte
+ var name string
+ if expectBlank {
+ data, name, err = h.service.ExportBlankTemplate(c.Request.Context(), value.TemplateID)
+ } else {
+ data, name, err = h.service.Export(c.Request.Context(), value.TemplateID, value.Params)
+ }
+ if err != nil {
+ httpx.Fail(c, "导出失败:"+err.Error())
+ return
+ }
+ c.Header("Content-Disposition", "attachment; filename*=UTF-8''"+url.QueryEscape(name))
+ c.Data(http.StatusOK, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", data)
+ }
+}
diff --git a/internal/server/handler/media.go b/internal/server/handler/media.go
new file mode 100644
index 0000000..ac914d7
--- /dev/null
+++ b/internal/server/handler/media.go
@@ -0,0 +1,230 @@
+package handler
+
+import (
+ "strconv"
+
+ "kra/internal/server/httpx"
+ servermiddleware "kra/internal/server/middleware"
+ "kra/internal/service"
+ "kra/internal/service/dto"
+
+ "github.com/gin-gonic/gin"
+)
+
+type Media struct{ service *service.MediaService }
+
+func NewMedia(service *service.MediaService) *Media { return &Media{service: service} }
+
+func (h *Media) Upload(c *gin.Context) {
+ claims := servermiddleware.Claims(c)
+ header, err := c.FormFile("file")
+ if claims == nil || err != nil {
+ httpx.Fail(c, "接收文件失败")
+ return
+ }
+ opened, err := header.Open()
+ if err != nil {
+ httpx.Fail(c, "读取文件失败")
+ return
+ }
+ defer opened.Close()
+ category, _ := strconv.Atoi(c.DefaultPostForm("classId", "0"))
+ save := c.DefaultQuery("noSave", "0") == "0"
+ item, err := h.service.Upload(c.Request.Context(), claims.ID, header.Filename, header.Header.Get("Content-Type"), category, opened, save)
+ if err != nil {
+ httpx.Fail(c, "上传失败:"+err.Error())
+ return
+ }
+ httpx.Write(c, httpx.CodeSuccess, gin.H{"file": item}, "上传成功")
+}
+
+func (h *Media) List(c *gin.Context) {
+ var req dto.MediaListRequest
+ if c.ShouldBindJSON(&req) != nil {
+ 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)
+ if err != nil {
+ httpx.Fail(c, "获取失败")
+ return
+ }
+ httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功")
+}
+func (h *Media) Delete(c *gin.Context) {
+ var req dto.IDRequest
+ if c.ShouldBindJSON(&req) != nil {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ if err := h.service.Delete(c.Request.Context(), req.ID); err != nil {
+ httpx.Fail(c, "删除失败:"+err.Error())
+ return
+ }
+ httpx.OK(c)
+}
+func (h *Media) DeleteMany(c *gin.Context) {
+ var req dto.IDsRequest
+ if c.ShouldBindJSON(&req) != nil {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ failed := []uint{}
+ for _, id := range req.IDs {
+ if h.service.Delete(c.Request.Context(), id) != nil {
+ failed = append(failed, id)
+ }
+ }
+ if len(failed) > 0 {
+ httpx.Write(c, httpx.CodeSuccess, gin.H{"failedIds": failed}, "部分文件删除失败")
+ return
+ }
+ httpx.OK(c)
+}
+func (h *Media) Find(c *gin.Context) {
+ id, _ := strconv.ParseUint(c.Query("id"), 10, 64)
+ item, err := h.service.Media(c.Request.Context(), uint(id))
+ if err != nil {
+ httpx.Fail(c, "查询失败")
+ return
+ }
+ httpx.Write(c, httpx.CodeSuccess, item, "获取成功")
+}
+func (h *Media) Rename(c *gin.Context) {
+ var req dto.RenameMediaRequest
+ if c.ShouldBindJSON(&req) != nil {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ if err := h.service.Rename(c.Request.Context(), req.ID, req.Name); err != nil {
+ httpx.Fail(c, "编辑失败")
+ return
+ }
+ httpx.OK(c)
+}
+func (h *Media) ImportURLs(c *gin.Context) {
+ var req []dto.ImportMediaRequest
+ if c.ShouldBindJSON(&req) != nil {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ claims := servermiddleware.Claims(c)
+ if claims == nil {
+ httpx.Fail(c, "未登录")
+ return
+ }
+ if err := h.service.ImportURLRequests(c.Request.Context(), claims.ID, req); err != nil {
+ httpx.Fail(c, "导入失败")
+ return
+ }
+ httpx.OK(c)
+}
+func (h *Media) Storage(c *gin.Context) {
+ var req dto.StorageListRequest
+ if c.ShouldBindJSON(&req) != nil {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ items, next, more, err := h.service.Storage(c.Request.Context(), req.Prefix, req.Cursor, req.Limit)
+ if err != nil {
+ httpx.Fail(c, "列举失败")
+ return
+ }
+ httpx.Write(c, httpx.CodeSuccess, gin.H{"list": items, "nextCursor": next, "hasMore": more}, "获取成功")
+}
+
+func (h *Media) Categories(c *gin.Context) {
+ items, err := h.service.Categories(c.Request.Context())
+ if err != nil {
+ httpx.Fail(c, "获取失败")
+ return
+ }
+ httpx.Write(c, httpx.CodeSuccess, items, "获取成功")
+}
+func (h *Media) SaveCategory(c *gin.Context) {
+ var req dto.CategoryRequest
+ if c.ShouldBindJSON(&req) != nil || req.Name == "" {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ if err := h.service.SaveCategoryRequest(c.Request.Context(), &req); err != nil {
+ httpx.Fail(c, err.Error())
+ return
+ }
+ httpx.OK(c)
+}
+func (h *Media) DeleteCategory(c *gin.Context) {
+ var req dto.IDRequest
+ if c.ShouldBindJSON(&req) != nil {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ if err := h.service.DeleteCategory(c.Request.Context(), req.ID); err != nil {
+ httpx.Fail(c, err.Error())
+ return
+ }
+ httpx.OK(c)
+}
+
+func (h *Media) InitUpload(c *gin.Context) {
+ claims := servermiddleware.Claims(c)
+ var req dto.InitUploadRequest
+ if claims == nil || c.ShouldBindJSON(&req) != nil {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ result, err := h.service.InitUpload(c.Request.Context(), claims.ID, req.FileName, req.FileHash, req.FileSize, req.ChunkSize, req.ChunkTotal)
+ if err != nil {
+ httpx.Fail(c, err.Error())
+ return
+ }
+ httpx.Write(c, httpx.CodeSuccess, result, "获取成功")
+}
+func (h *Media) SaveChunk(c *gin.Context) {
+ claims := servermiddleware.Claims(c)
+ uploadID, _ := strconv.ParseUint(c.PostForm("uploadId"), 10, 64)
+ index, _ := strconv.Atoi(c.PostForm("chunkIndex"))
+ header, err := c.FormFile("chunk")
+ if claims == nil || err != nil {
+ httpx.Fail(c, "接收分片失败")
+ return
+ }
+ opened, err := header.Open()
+ if err != nil {
+ httpx.Fail(c, "读取分片失败")
+ return
+ }
+ defer opened.Close()
+ if err = h.service.SaveChunk(c.Request.Context(), claims.ID, uint(uploadID), index, c.PostForm("chunkHash"), opened); err != nil {
+ httpx.Fail(c, err.Error())
+ return
+ }
+ httpx.OK(c)
+}
+func (h *Media) CompleteUpload(c *gin.Context) {
+ claims := servermiddleware.Claims(c)
+ var req dto.CompleteUploadRequest
+ if claims == nil || c.ShouldBindJSON(&req) != nil {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ item, err := h.service.CompleteUpload(c.Request.Context(), claims.ID, req.UploadID)
+ if err != nil {
+ httpx.Fail(c, err.Error())
+ return
+ }
+ httpx.Write(c, httpx.CodeSuccess, gin.H{"media": item}, "上传成功")
+}
+func (h *Media) CancelUpload(c *gin.Context) {
+ claims := servermiddleware.Claims(c)
+ id, _ := strconv.ParseUint(c.Param("uploadId"), 10, 64)
+ if claims == nil {
+ httpx.Fail(c, "未登录")
+ return
+ }
+ if err := h.service.CancelUpload(c.Request.Context(), claims.ID, uint(id)); err != nil {
+ httpx.Fail(c, err.Error())
+ return
+ }
+ httpx.OK(c)
+}
diff --git a/internal/server/handler/navigation.go b/internal/server/handler/navigation.go
new file mode 100644
index 0000000..0785837
--- /dev/null
+++ b/internal/server/handler/navigation.go
@@ -0,0 +1,26 @@
+package handler
+
+import (
+ "kra/internal/server/httpx"
+ "kra/internal/server/middleware"
+ "kra/internal/service"
+
+ "github.com/gin-gonic/gin"
+)
+
+type Navigation struct{ service *service.SystemService }
+
+func NewNavigation(service *service.SystemService) *Navigation { return &Navigation{service: service} }
+func (h *Navigation) Menu(c *gin.Context) {
+ claims := middleware.Claims(c)
+ if claims == nil {
+ httpx.NoAuth(c, "未登录或非法访问")
+ return
+ }
+ menus, err := h.service.Menus(c.Request.Context(), claims.AuthorityID)
+ if err != nil {
+ httpx.Fail(c, "获取失败")
+ return
+ }
+ httpx.Write(c, httpx.CodeSuccess, gin.H{"menus": menus}, "获取成功")
+}
diff --git a/internal/server/handler/parameter.go b/internal/server/handler/parameter.go
new file mode 100644
index 0000000..bbfdb56
--- /dev/null
+++ b/internal/server/handler/parameter.go
@@ -0,0 +1,100 @@
+package handler
+
+import (
+ "strconv"
+
+ "kra/internal/server/httpx"
+ "kra/internal/service"
+ "kra/internal/service/dto"
+
+ "github.com/gin-gonic/gin"
+)
+
+type Parameter struct {
+ service *service.SettingsService
+}
+
+func NewParameter(service *service.SettingsService) *Parameter {
+ return &Parameter{service: service}
+}
+
+func (h *Parameter) Create(c *gin.Context) {
+ var req dto.SystemParameterRequest
+ if c.ShouldBindJSON(&req) != nil || req.Key == "" {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ if err := h.service.CreateParameterRequest(c.Request.Context(), &req); err != nil {
+ httpx.Fail(c, "创建失败")
+ return
+ }
+ httpx.OK(c)
+}
+
+func (h *Parameter) Update(c *gin.Context) {
+ var req dto.SystemParameterRequest
+ if c.ShouldBindJSON(&req) != nil || req.ID == 0 {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ if err := h.service.UpdateParameterRequest(c.Request.Context(), &req); err != nil {
+ httpx.Fail(c, "更新失败")
+ return
+ }
+ httpx.OK(c)
+}
+
+func (h *Parameter) Delete(c *gin.Context) {
+ id, err := strconv.ParseUint(c.Query("ID"), 10, 64)
+ if err != nil || id == 0 {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ if err := h.service.DeleteParameters(c.Request.Context(), []uint{uint(id)}); err != nil {
+ httpx.Fail(c, "删除失败")
+ return
+ }
+ httpx.OK(c)
+}
+
+func (h *Parameter) DeleteMany(c *gin.Context) {
+ ids := IDsFromQuery(c)
+ if len(ids) == 0 {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ if err := h.service.DeleteParameters(c.Request.Context(), ids); err != nil {
+ httpx.Fail(c, "删除失败")
+ return
+ }
+ httpx.OK(c)
+}
+
+func (h *Parameter) Find(c *gin.Context) {
+ id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
+ item, err := h.service.Parameter(c.Request.Context(), uint(id), "")
+ if err != nil {
+ httpx.Fail(c, "查询失败")
+ return
+ }
+ httpx.Write(c, httpx.CodeSuccess, gin.H{"resysParams": item}, "查询成功")
+}
+
+func (h *Parameter) Get(c *gin.Context) {
+ item, err := h.service.Parameter(c.Request.Context(), 0, c.Query("key"))
+ if err != nil {
+ httpx.Fail(c, "查询失败")
+ return
+ }
+ httpx.Write(c, httpx.CodeSuccess, item, "查询成功")
+}
+
+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"))
+ if err != nil {
+ httpx.Fail(c, "获取失败")
+ return
+ }
+ httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: p, PageSize: size}, "获取成功")
+}
diff --git a/internal/server/handler/public.go b/internal/server/handler/public.go
new file mode 100644
index 0000000..b01755f
--- /dev/null
+++ b/internal/server/handler/public.go
@@ -0,0 +1,200 @@
+package handler
+
+import (
+ "context"
+ "net/http"
+ "strconv"
+ "strings"
+ "time"
+
+ "kra/internal/conf"
+ "kra/internal/server/httpx"
+ "kra/internal/service"
+ "kra/internal/service/dto"
+ "kra/internal/worker"
+
+ "github.com/gin-gonic/gin"
+ "github.com/mojocn/base64Captcha"
+)
+
+type Public struct {
+ system *service.SystemService
+ settings *service.SettingsService
+ audit *service.AuditService
+ scheduler *worker.TaskScheduler
+ store *captchaStore
+ keyLong int
+ imageWidth int
+ imageHeight int
+}
+
+func NewPublic(config *conf.AdminBackend, system *service.SystemService, settings *service.SettingsService, audit *service.AuditService, scheduler *worker.TaskScheduler) *Public {
+ expiration := 3 * time.Minute
+ keyLong, width, height := 6, 240, 80
+ 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)
+ }
+ if config.Captcha.ImgWidth > 0 {
+ width = int(config.Captcha.ImgWidth)
+ }
+ if config.Captcha.ImgHeight > 0 {
+ 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}
+}
+
+func (h *Public) Captcha(c *gin.Context) {
+ keyLong, width, height := h.keyLong, h.imageWidth, h.imageHeight
+ 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
+ }
+ driver := base64Captcha.NewDriverDigit(height, width, keyLong, 0.7, 80)
+ id, picture, _, err := base64Captcha.NewCaptcha(driver, h.store).Generate()
+ if err != nil {
+ httpx.Fail(c, "验证码获取失败")
+ return
+ }
+ httpx.Write(c, httpx.CodeSuccess, gin.H{"captchaId": id, "picPath": picture, "captchaLength": keyLong, "openCaptcha": openCaptcha}, "验证码获取成功")
+}
+
+func (h *Public) Login(c *gin.Context) {
+ var req dto.LoginRequest
+ if c.ShouldBindJSON(&req) != nil || req.Username == "" || req.Password == "" {
+ httpx.Fail(c, "请输入用户名和密码")
+ return
+ }
+ 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, "账户已锁定,请稍后重试")
+ 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
+ }
+ if requireCaptcha && (req.CaptchaID == "" || req.Captcha == "" || !h.store.Verify(req.CaptchaID, req.Captcha, true)) {
+ 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
+ }
+ 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)
+ }
+ 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: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.Write(c, httpx.CodeSuccess, result, "登录成功")
+}
+
+func (h *Public) CheckDatabase(c *gin.Context) {
+ initialized, err := h.system.IsInitialized(c.Request.Context())
+ if err != nil {
+ httpx.Fail(c, "数据库状态检查失败")
+ return
+ }
+ message := "数据库无需初始化"
+ if !initialized {
+ message = "前往初始化数据库"
+ }
+ httpx.Write(c, httpx.CodeSuccess, gin.H{"needInit": !initialized}, message)
+}
+
+func (h *Public) InitializeDatabase(engine *gin.Engine) gin.HandlerFunc {
+ return func(c *gin.Context) {
+ initialized, err := h.system.IsInitialized(c.Request.Context())
+ if err != nil {
+ httpx.Fail(c, "数据库状态检查失败")
+ return
+ }
+ if initialized {
+ httpx.Fail(c, "数据库已初始化,无需重复初始化")
+ return
+ }
+ var input service.DatabaseInit
+ if c.ShouldBindJSON(&input) != nil {
+ httpx.Fail(c, "数据库初始化参数无效")
+ return
+ }
+ routes := engine.Routes()
+ values := make([]dto.Route, 0, len(routes))
+ for _, route := range routes {
+ values = append(values, dto.Route{Path: route.Path, Method: route.Method})
+ }
+ if err := h.system.InitializeRoutes(c.Request.Context(), &input, values); err != nil {
+ httpx.Fail(c, "自动创建数据库失败: "+err.Error())
+ return
+ }
+ if err := h.scheduler.Reload(c.Request.Context()); err != nil {
+ httpx.Fail(c, "数据库已初始化,但定时任务加载失败:"+err.Error())
+ return
+ }
+ httpx.Write(c, httpx.CodeSuccess, gin.H{}, "自动创建数据库成功")
+ }
+}
+
+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
+}
+
+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)
+}
+func (s *captchaStore) Get(id string, clear bool) string {
+ value, ok, err := s.service.CacheGet(context.Background(), s.key(id))
+ if err != nil || !ok {
+ return ""
+ }
+ if clear {
+ _ = s.service.CacheDelete(context.Background(), s.key(id))
+ }
+ return value
+}
+func (s *captchaStore) Verify(id, answer string, clear bool) bool {
+ return id != "" && answer != "" && strings.EqualFold(s.Get(id, clear), answer)
+}
diff --git a/internal/server/handler/session.go b/internal/server/handler/session.go
new file mode 100644
index 0000000..2badae4
--- /dev/null
+++ b/internal/server/handler/session.go
@@ -0,0 +1,26 @@
+package handler
+
+import (
+ "net/http"
+
+ "kra/internal/server/httpx"
+ "kra/internal/service"
+
+ "github.com/gin-gonic/gin"
+)
+
+type Session struct{ settings *service.SettingsService }
+
+func NewSession(settings *service.SettingsService) *Session { return &Session{settings: settings} }
+func (h *Session) Logout(c *gin.Context) {
+ token := c.GetHeader("x-token")
+ if token == "" {
+ token, _ = c.Cookie("x-token")
+ }
+ if err := h.settings.BlacklistToken(c.Request.Context(), token); err != nil {
+ httpx.Fail(c, "退出失败")
+ return
+ }
+ http.SetCookie(c.Writer, &http.Cookie{Name: "x-token", Value: "", Path: "/", MaxAge: -1, HttpOnly: true})
+ httpx.OK(c)
+}
diff --git a/internal/server/handler/system_config.go b/internal/server/handler/system_config.go
new file mode 100644
index 0000000..849691f
--- /dev/null
+++ b/internal/server/handler/system_config.go
@@ -0,0 +1,101 @@
+package handler
+
+import (
+ "runtime"
+ "syscall"
+ "time"
+
+ "kra/internal/server/httpx"
+ "kra/internal/service"
+ "kra/internal/service/dto"
+ "kra/internal/worker"
+
+ "github.com/gin-gonic/gin"
+ "github.com/shirou/gopsutil/v4/cpu"
+ "github.com/shirou/gopsutil/v4/mem"
+)
+
+type SystemConfig struct {
+ system *service.SystemService
+ settings *service.SettingsService
+ scheduler *worker.TaskScheduler
+}
+
+func NewSystemConfig(system *service.SystemService, settings *service.SettingsService, scheduler *worker.TaskScheduler) *SystemConfig {
+ return &SystemConfig{system: system, settings: settings, scheduler: scheduler}
+}
+
+func (h *SystemConfig) GetSecurity(c *gin.Context) {
+ value, err := h.settings.Security(c.Request.Context())
+ if err != nil {
+ httpx.Fail(c, "获取安全配置失败")
+ return
+ }
+ httpx.Write(c, httpx.CodeSuccess, value, "获取成功")
+}
+
+func (h *SystemConfig) SetSecurity(c *gin.Context) {
+ var req dto.SecurityConfigRequest
+ if c.ShouldBindJSON(&req) != nil {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ result, err := h.settings.SaveSecurityRequest(c.Request.Context(), &req)
+ if err != nil {
+ httpx.Fail(c, err.Error())
+ return
+ }
+ httpx.Write(c, httpx.CodeSuccess, result, "设置成功")
+}
+
+func (h *SystemConfig) Get(c *gin.Context) {
+ httpx.Write(c, httpx.CodeSuccess, h.system.SystemConfig(), "获取成功")
+}
+
+func (h *SystemConfig) Set(c *gin.Context) {
+ var req dto.SetSystemConfigRequest
+ if c.ShouldBindJSON(&req) != nil {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ if err := h.system.SaveSystemConfig(c.Request.Context(), &req); err != nil {
+ httpx.Fail(c, "配置保存失败: "+err.Error())
+ return
+ }
+ httpx.Write(c, httpx.CodeSuccess, gin.H{}, "设置成功")
+}
+
+func (h *SystemConfig) Reload(c *gin.Context) {
+ if err := h.system.ReloadConfig(c.Request.Context()); err != nil {
+ httpx.Fail(c, "重载系统失败:"+err.Error())
+ return
+ }
+ if err := h.scheduler.Reload(c.Request.Context()); err != nil {
+ httpx.Fail(c, "系统配置已重载,但定时任务重载失败:"+err.Error())
+ return
+ }
+ httpx.Write(c, httpx.CodeSuccess, gin.H{}, "重载系统成功")
+}
+
+func (h *SystemConfig) ServerInfo(c *gin.Context) {
+ 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)
+ }
+ 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})
+ }
+ 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}
+ httpx.Write(c, httpx.CodeSuccess, gin.H{"server": server}, "获取成功")
+}
diff --git a/internal/server/handler/user.go b/internal/server/handler/user.go
new file mode 100644
index 0000000..4fd722a
--- /dev/null
+++ b/internal/server/handler/user.go
@@ -0,0 +1,178 @@
+package handler
+
+import (
+ "net/http"
+ "strconv"
+ "time"
+
+ "kra/internal/server/httpx"
+ "kra/internal/server/middleware"
+ "kra/internal/service"
+ "kra/internal/service/dto"
+
+ "github.com/gin-gonic/gin"
+)
+
+type User struct{ service *service.SystemService }
+
+func NewUser(service *service.SystemService) *User { return &User{service: service} }
+
+func (h *User) List(c *gin.Context) {
+ var req dto.UserListRequest
+ if c.ShouldBindJSON(&req) != nil {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ items, total, err := h.service.ListUsersRequest(c.Request.Context(), &req)
+ if err != nil {
+ httpx.Fail(c, "获取失败")
+ return
+ }
+ httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功")
+}
+
+func (h *User) Create(c *gin.Context) {
+ var req dto.UserRequest
+ if c.ShouldBindJSON(&req) != nil || req.Username == "" || req.Password == "" {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ result, err := h.service.CreateUserRequest(c.Request.Context(), &req)
+ if err != nil {
+ httpx.Fail(c, "注册失败")
+ return
+ }
+ httpx.Write(c, httpx.CodeSuccess, gin.H{"user": result}, "注册成功")
+}
+
+func (h *User) Update(c *gin.Context) {
+ var req dto.UserRequest
+ if c.ShouldBindJSON(&req) != nil {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ if err := h.service.UpdateUserRequest(c.Request.Context(), &req); err != nil {
+ httpx.Fail(c, "修改失败")
+ return
+ }
+ httpx.OK(c)
+}
+
+func (h *User) UpdateSelf(c *gin.Context) {
+ claims := middleware.Claims(c)
+ if claims == nil {
+ httpx.NoAuth(c, "未登录或非法访问")
+ return
+ }
+ var req dto.SelfUserRequest
+ if c.ShouldBindJSON(&req) != nil {
+ 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 {
+ httpx.Fail(c, "修改失败")
+ return
+ }
+ httpx.OK(c)
+}
+
+func (h *User) Delete(c *gin.Context) {
+ var req dto.IDRequest
+ if c.ShouldBind(&req) != nil || req.ID == 0 {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ if err := h.service.DeleteUser(c.Request.Context(), req.ID); err != nil {
+ httpx.Fail(c, "删除失败")
+ return
+ }
+ httpx.OK(c)
+}
+
+func (h *User) ResetPassword(c *gin.Context) {
+ var req dto.ResetPasswordRequest
+ if c.ShouldBindJSON(&req) != nil {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ if err := h.service.ResetPassword(c.Request.Context(), req.ID, req.Password); err != nil {
+ httpx.Fail(c, "重置失败")
+ return
+ }
+ httpx.OK(c)
+}
+
+func (h *User) ChangePassword(c *gin.Context) {
+ claims := middleware.Claims(c)
+ var req dto.ChangePasswordRequest
+ if claims == nil || c.ShouldBindJSON(&req) != nil {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ if err := h.service.ChangePassword(c.Request.Context(), claims.ID, req.Password, req.NewPassword); err != nil {
+ httpx.Fail(c, "修改失败,原密码与当前账户不符")
+ return
+ }
+ httpx.OK(c)
+}
+
+func (h *User) SetSelfSetting(c *gin.Context) {
+ claims := middleware.Claims(c)
+ var setting map[string]any
+ if claims == nil || c.ShouldBindJSON(&setting) != nil {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ if err := h.service.SetUserSetting(c.Request.Context(), claims.ID, setting); err != nil {
+ httpx.Fail(c, "设置失败")
+ return
+ }
+ httpx.OK(c)
+}
+
+func (h *User) SetAuthorities(c *gin.Context) {
+ var req dto.UserAuthoritiesRequest
+ if c.ShouldBindJSON(&req) != nil || req.ID == 0 || len(req.AuthorityIDs) == 0 {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ if err := h.service.SetUserAuthorities(c.Request.Context(), req.ID, req.AuthorityIDs); err != nil {
+ httpx.Fail(c, "修改失败")
+ return
+ }
+ httpx.OK(c)
+}
+
+func (h *User) SwitchAuthority(c *gin.Context) {
+ claims := middleware.Claims(c)
+ var req dto.SwitchAuthorityRequest
+ if claims == nil || c.ShouldBindJSON(&req) != nil || req.AuthorityID == 0 {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ login, err := h.service.SwitchAuthority(c.Request.Context(), claims.ID, req.AuthorityID)
+ if err != nil {
+ httpx.Fail(c, err.Error())
+ return
+ }
+ c.Header("new-token", login.Token)
+ c.Header("new-expires-at", strconv.FormatInt(login.ExpiresAt/1000, 10))
+ maxAge := int(time.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.OK(c)
+}
+
+func (h *User) Get(c *gin.Context) {
+ claims := middleware.Claims(c)
+ if claims == nil {
+ httpx.NoAuth(c, "未登录或非法访问")
+ return
+ }
+ result, err := h.service.User(c.Request.Context(), claims.ID)
+ if err != nil {
+ httpx.Fail(c, "获取失败")
+ return
+ }
+ httpx.Write(c, httpx.CodeSuccess, gin.H{"userInfo": result}, "获取成功")
+}
diff --git a/internal/server/handler/version.go b/internal/server/handler/version.go
new file mode 100644
index 0000000..825da53
--- /dev/null
+++ b/internal/server/handler/version.go
@@ -0,0 +1,93 @@
+package handler
+
+import (
+ "fmt"
+ "net/http"
+ "strconv"
+ "time"
+
+ "kra/internal/server/httpx"
+ "kra/internal/service"
+ "kra/internal/service/dto"
+
+ "github.com/gin-gonic/gin"
+)
+
+type Version struct{ service *service.VersionService }
+
+func NewVersion(service *service.VersionService) *Version { return &Version{service: service} }
+
+func (h *Version) Delete(c *gin.Context) {
+ id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
+ if err := h.service.DeleteVersions(c.Request.Context(), []uint{uint(id)}); err != nil {
+ httpx.Fail(c, "删除失败")
+ return
+ }
+ httpx.OK(c)
+}
+func (h *Version) DeleteMany(c *gin.Context) {
+ raw := c.QueryArray("IDs[]")
+ ids := make([]uint, 0, len(raw))
+ for _, value := range raw {
+ id, _ := strconv.ParseUint(value, 10, 64)
+ ids = append(ids, uint(id))
+ }
+ if err := h.service.DeleteVersions(c.Request.Context(), ids); err != nil {
+ httpx.Fail(c, "删除失败")
+ return
+ }
+ httpx.OK(c)
+}
+func (h *Version) Find(c *gin.Context) {
+ id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
+ item, err := h.service.Version(c.Request.Context(), uint(id))
+ if err != nil {
+ httpx.Fail(c, "查询失败")
+ return
+ }
+ httpx.Write(c, httpx.CodeSuccess, item, "查询成功")
+}
+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"))
+ if err != nil {
+ httpx.Fail(c, "获取失败")
+ return
+ }
+ httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: p, PageSize: size}, "获取成功")
+}
+func (h *Version) Export(c *gin.Context) {
+ var req dto.ExportVersionRequest
+ if c.ShouldBindJSON(&req) != nil || req.VersionName == "" || req.VersionCode == "" {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ if err := h.service.Export(c.Request.Context(), req.VersionName, req.VersionCode, req.Description, req.MenuIDs, req.APIIDs, req.DictIDs); err != nil {
+ httpx.Fail(c, "创建发版失败:"+err.Error())
+ return
+ }
+ httpx.OK(c)
+}
+func (h *Version) Download(c *gin.Context) {
+ id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
+ raw, code, err := h.service.VersionData(c.Request.Context(), uint(id))
+ if err != nil {
+ httpx.Fail(c, "下载失败")
+ return
+ }
+ name := fmt.Sprintf("version_%s_%s.json", code, time.Now().Format("20060102150405"))
+ c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", name))
+ c.Data(http.StatusOK, "application/json; charset=utf-8", []byte(raw))
+}
+func (h *Version) Import(c *gin.Context) {
+ var req dto.ImportVersionRequest
+ if c.ShouldBindJSON(&req) != nil {
+ httpx.Fail(c, "参数错误")
+ return
+ }
+ if err := h.service.ImportRequest(c.Request.Context(), &req); err != nil {
+ httpx.Fail(c, "导入失败:"+err.Error())
+ return
+ }
+ httpx.OK(c)
+}
diff --git a/internal/server/media_routes.go b/internal/server/media_routes.go
deleted file mode 100644
index 8ca8eef..0000000
--- a/internal/server/media_routes.go
+++ /dev/null
@@ -1,265 +0,0 @@
-package server
-
-import (
- "github.com/gin-gonic/gin"
- "kra/internal/biz"
- "kra/internal/service"
- "strconv"
-)
-
-func registerMediaRoutes(group *gin.RouterGroup, svc *service.MediaService) {
- files := group.Group("/fileUploadAndDownload")
- files.POST("/upload", func(c *gin.Context) {
- claims := currentClaims(c)
- header, err := c.FormFile("file")
- if claims == nil || err != nil {
- fail(c, "接收文件失败")
- return
- }
- opened, err := header.Open()
- if err != nil {
- fail(c, "读取文件失败")
- return
- }
- defer opened.Close()
- category, _ := strconv.Atoi(c.DefaultPostForm("classId", "0"))
- save := c.DefaultQuery("noSave", "0") == "0"
- item, err := svc.Upload(c.Request.Context(), claims.ID, header.Filename, header.Header.Get("Content-Type"), category, opened, save)
- if err != nil {
- fail(c, "上传失败:"+err.Error())
- return
- }
- writeResult(c, codeSuccess, gin.H{"file": item}, "上传成功")
- })
- files.POST("/getFileList", func(c *gin.Context) {
- var req 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"`
- }
- if c.ShouldBindJSON(&req) != nil {
- fail(c, "参数错误")
- return
- }
- items, total, err := svc.MediaList(c.Request.Context(), req.Page, req.PageSize, req.Keyword, req.ClassID, req.Tag, req.UserID)
- if err != nil {
- fail(c, "获取失败")
- return
- }
- writeResult(c, codeSuccess, pageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功")
- })
- files.POST("/deleteFile", func(c *gin.Context) {
- var req struct {
- ID uint `json:"ID"`
- }
- if c.ShouldBindJSON(&req) != nil {
- fail(c, "参数错误")
- return
- }
- if err := svc.Delete(c.Request.Context(), req.ID); err != nil {
- fail(c, "删除失败:"+err.Error())
- return
- }
- ok(c)
- })
- files.POST("/deleteFiles", func(c *gin.Context) {
- var req struct {
- IDs []uint `json:"ids"`
- }
- if c.ShouldBindJSON(&req) != nil {
- fail(c, "参数错误")
- return
- }
- failed := []uint{}
- for _, id := range req.IDs {
- if svc.Delete(c.Request.Context(), id) != nil {
- failed = append(failed, id)
- }
- }
- if len(failed) > 0 {
- writeResult(c, codeSuccess, gin.H{"failedIds": failed}, "部分文件删除失败")
- return
- }
- ok(c)
- })
- files.GET("/findFile", func(c *gin.Context) {
- id, _ := strconv.ParseUint(c.Query("id"), 10, 64)
- item, err := svc.Media(c.Request.Context(), uint(id))
- if err != nil {
- fail(c, "查询失败")
- return
- }
- writeResult(c, codeSuccess, item, "获取成功")
- })
- files.POST("/editFileName", func(c *gin.Context) {
- var req struct {
- ID uint `json:"ID"`
- Name string `json:"name"`
- }
- if c.ShouldBindJSON(&req) != nil {
- fail(c, "参数错误")
- return
- }
- if err := svc.Rename(c.Request.Context(), req.ID, req.Name); err != nil {
- fail(c, "编辑失败")
- return
- }
- ok(c)
- })
- files.POST("/importURL", func(c *gin.Context) {
- var req []struct {
- Name string `json:"name"`
- ClassID int `json:"classId"`
- URL string `json:"url"`
- Tag string `json:"tag"`
- Key string `json:"key"`
- Size int64 `json:"size"`
- Mime string `json:"mime"`
- MD5 string `json:"md5"`
- }
- if c.ShouldBindJSON(&req) != nil {
- fail(c, "参数错误")
- return
- }
- claims := currentClaims(c)
- items := make([]*biz.MediaFile, 0, len(req))
- for _, v := range req {
- items = append(items, &biz.MediaFile{Name: v.Name, CategoryID: v.ClassID, URL: v.URL, Tag: v.Tag, Key: v.Key, Size: v.Size, Mime: v.Mime, MD5: v.MD5, UserID: claims.ID})
- }
- if err := svc.ImportURLs(c.Request.Context(), items); err != nil {
- fail(c, "导入失败")
- return
- }
- ok(c)
- })
- files.POST("/listOssFiles", func(c *gin.Context) {
- var req struct {
- Prefix string `json:"prefix"`
- Cursor string `json:"cursor"`
- Limit int `json:"limit"`
- }
- if c.ShouldBindJSON(&req) != nil {
- fail(c, "参数错误")
- return
- }
- items, next, more, err := svc.Storage(c.Request.Context(), req.Prefix, req.Cursor, req.Limit)
- if err != nil {
- fail(c, "列举失败")
- return
- }
- writeResult(c, codeSuccess, gin.H{"list": items, "nextCursor": next, "hasMore": more}, "获取成功")
- })
- categories := group.Group("/attachmentCategory")
- categories.GET("/getCategoryList", func(c *gin.Context) {
- items, err := svc.Categories(c.Request.Context())
- if err != nil {
- fail(c, "获取失败")
- return
- }
- writeResult(c, codeSuccess, items, "获取成功")
- })
- categories.POST("/addCategory", func(c *gin.Context) {
- var req struct {
- ID uint `json:"ID"`
- Name string `json:"name"`
- PID uint `json:"pid"`
- }
- if c.ShouldBindJSON(&req) != nil || req.Name == "" {
- fail(c, "参数错误")
- return
- }
- if err := svc.SaveCategory(c.Request.Context(), &biz.AttachmentCategory{ID: req.ID, Name: req.Name, ParentID: req.PID}); err != nil {
- fail(c, err.Error())
- return
- }
- ok(c)
- })
- categories.POST("/deleteCategory", func(c *gin.Context) {
- var req struct {
- ID uint `json:"ID"`
- }
- if c.ShouldBindJSON(&req) != nil {
- fail(c, "参数错误")
- return
- }
- if err := svc.DeleteCategory(c.Request.Context(), req.ID); err != nil {
- fail(c, err.Error())
- return
- }
- ok(c)
- })
- upload := group.Group("/mediaUpload")
- upload.POST("/init", func(c *gin.Context) {
- claims := currentClaims(c)
- var req struct {
- FileName string `json:"fileName"`
- FileHash string `json:"fileHash"`
- FileSize int64 `json:"fileSize"`
- ChunkSize int64 `json:"chunkSize"`
- ChunkTotal int `json:"chunkTotal"`
- }
- if claims == nil || c.ShouldBindJSON(&req) != nil {
- fail(c, "参数错误")
- return
- }
- result, err := svc.InitUpload(c.Request.Context(), claims.ID, req.FileName, req.FileHash, req.FileSize, req.ChunkSize, req.ChunkTotal)
- if err != nil {
- fail(c, err.Error())
- return
- }
- writeResult(c, codeSuccess, result, "获取成功")
- })
- upload.POST("/chunk", func(c *gin.Context) {
- claims := currentClaims(c)
- uploadID, _ := strconv.ParseUint(c.PostForm("uploadId"), 10, 64)
- index, _ := strconv.Atoi(c.PostForm("chunkIndex"))
- header, err := c.FormFile("chunk")
- if claims == nil || err != nil {
- fail(c, "接收分片失败")
- return
- }
- opened, err := header.Open()
- if err != nil {
- fail(c, "读取分片失败")
- return
- }
- defer opened.Close()
- if err = svc.SaveChunk(c.Request.Context(), claims.ID, uint(uploadID), index, c.PostForm("chunkHash"), opened); err != nil {
- fail(c, err.Error())
- return
- }
- ok(c)
- })
- upload.POST("/complete", func(c *gin.Context) {
- claims := currentClaims(c)
- var req struct {
- UploadID uint `json:"uploadId"`
- }
- if claims == nil || c.ShouldBindJSON(&req) != nil {
- fail(c, "参数错误")
- return
- }
- item, err := svc.CompleteUpload(c.Request.Context(), claims.ID, req.UploadID)
- if err != nil {
- fail(c, err.Error())
- return
- }
- writeResult(c, codeSuccess, gin.H{"media": item}, "上传成功")
- })
- upload.DELETE("/:uploadId", func(c *gin.Context) {
- claims := currentClaims(c)
- id, _ := strconv.ParseUint(c.Param("uploadId"), 10, 64)
- if claims == nil {
- fail(c, "未登录")
- return
- }
- if err := svc.CancelUpload(c.Request.Context(), claims.ID, uint(id)); err != nil {
- fail(c, err.Error())
- return
- }
- ok(c)
- })
-}
diff --git a/internal/server/middleware/access.go b/internal/server/middleware/access.go
index 5718a27..cda5e74 100644
--- a/internal/server/middleware/access.go
+++ b/internal/server/middleware/access.go
@@ -3,9 +3,9 @@ package middleware
import (
"net/http"
- "kra/internal/biz"
"kra/internal/server/httpx"
"kra/internal/service"
+ "kra/internal/service/dto"
"github.com/gin-gonic/gin"
)
@@ -22,16 +22,16 @@ func AccessControl(access *service.AccessService, audit *service.AuditService) g
if err != nil || !allowed {
requestID, _ := c.Get("request_id")
requestIDText, _ := requestID.(string)
- _ = audit.RecordDataAccess(c.Request.Context(), &biz.DataAccessLog{EventType: "blocked_access", Operation: c.Request.Method, UserID: claims.ID, AuthorityID: claims.AuthorityID, RequestID: requestIDText, Method: c.Request.Method, Path: path, Detail: "Casbin policy denied the request"})
+ _ = audit.RecordDataAccessRequest(c.Request.Context(), &dto.DataAccessRecordRequest{EventType: "blocked_access", Operation: c.Request.Method, UserID: claims.ID, AuthorityID: claims.AuthorityID, RequestID: requestIDText, Method: c.Request.Method, Path: path, Detail: "Casbin policy denied the request"})
c.AbortWithStatusJSON(http.StatusForbidden, httpx.Response{Code: httpx.CodeError, Data: nil, Msg: "权限不足"})
return
}
- scope, err := access.ResolveDataScope(c.Request.Context(), claims.AuthorityID, claims.ID)
+ requestContext, err := access.ContextWithDataScope(c.Request.Context(), claims.AuthorityID, claims.ID)
if err != nil {
httpx.Fail(c, "数据权限加载失败")
return
}
- c.Request = c.Request.WithContext(biz.NewDataScopeContext(c.Request.Context(), scope))
+ c.Request = c.Request.WithContext(requestContext)
c.Next()
}
}
diff --git a/internal/server/middleware/audit.go b/internal/server/middleware/audit.go
new file mode 100644
index 0000000..ab0d559
--- /dev/null
+++ b/internal/server/middleware/audit.go
@@ -0,0 +1,46 @@
+package middleware
+
+import (
+ "bytes"
+ "io"
+ "strings"
+ "time"
+
+ "kra/internal/service"
+ "kra/internal/service/dto"
+
+ "github.com/gin-gonic/gin"
+)
+
+func OperationAudit(service *service.AuditService) gin.HandlerFunc {
+ return func(c *gin.Context) {
+ if isBootstrapPath(c.Request.URL.Path) || c.Request.Method == "GET" || c.Request.Method == "HEAD" || c.Request.Method == "OPTIONS" {
+ c.Next()
+ return
+ }
+ path := c.Request.URL.Path
+ if strings.Contains(path, "sysOperationRecord") || strings.Contains(path, "sysLoginLog") || strings.Contains(path, "dataAccessLog") {
+ c.Next()
+ return
+ }
+ var requestBody []byte
+ if c.Request.Body != nil {
+ requestBody, _ = io.ReadAll(io.LimitReader(c.Request.Body, 32769))
+ c.Request.Body = io.NopCloser(bytes.NewReader(requestBody))
+ }
+ writer := &captureWriter{ResponseWriter: c.Writer}
+ c.Writer = writer
+ started := time.Now()
+ c.Next()
+ userID := uint(0)
+ if claims := Claims(c); claims != nil {
+ userID = claims.ID
+ }
+ requestID, _ := c.Get("request_id")
+ status, errorMessage := c.Writer.Status(), ""
+ if status >= 400 {
+ errorMessage = writer.body.String()
+ }
+ _ = service.RecordOperationRequest(c.Request.Context(), &dto.OperationRecordRequest{IP: c.ClientIP(), Method: c.Request.Method, Path: path, Status: status, LatencyMS: time.Since(started).Milliseconds(), Agent: c.Request.UserAgent(), ErrorMessage: errorMessage, Body: redactJSON(requestBody), Response: redactJSON(writer.body.Bytes()), UserID: userID, RequestID: stringValue(requestID), TraceID: c.GetHeader("traceparent"), DeviceID: c.GetHeader("X-Device-Id")})
+ }
+}
diff --git a/internal/server/middleware/capture.go b/internal/server/middleware/capture.go
new file mode 100644
index 0000000..b796ac0
--- /dev/null
+++ b/internal/server/middleware/capture.go
@@ -0,0 +1,66 @@
+package middleware
+
+import (
+ "bytes"
+ "encoding/json"
+ "strings"
+
+ "github.com/gin-gonic/gin"
+)
+
+type captureWriter struct {
+ gin.ResponseWriter
+ body bytes.Buffer
+}
+
+func (w *captureWriter) Write(data []byte) (int, error) {
+ if w.body.Len() < 32768 {
+ remaining := 32768 - w.body.Len()
+ if len(data) > remaining {
+ _, _ = w.body.Write(data[:remaining])
+ } else {
+ _, _ = w.body.Write(data)
+ }
+ }
+ return w.ResponseWriter.Write(data)
+}
+
+func redactJSON(raw []byte) string {
+ if len(raw) == 0 {
+ return ""
+ }
+ if len(raw) > 32768 {
+ raw = raw[:32768]
+ }
+ var value any
+ if json.Unmarshal(raw, &value) != nil {
+ return string(raw)
+ }
+ var clean func(any)
+ clean = func(current any) {
+ switch v := current.(type) {
+ case map[string]any:
+ for key, item := range v {
+ lower := strings.ToLower(key)
+ if strings.Contains(lower, "password") || strings.Contains(lower, "token") || strings.Contains(lower, "secret") {
+ v[key] = "******"
+ } else {
+ clean(item)
+ }
+ }
+ case []any:
+ for _, item := range v {
+ clean(item)
+ }
+ }
+ }
+ clean(value)
+ encoded, _ := json.Marshal(value)
+ return string(encoded)
+}
+
+func stringValue(value any) string { text, _ := value.(string); return text }
+
+func isBootstrapPath(path string) bool {
+ return strings.HasSuffix(path, "/health") || strings.Contains(path, "/init/")
+}
diff --git a/internal/server/email_routes.go b/internal/server/middleware/email.go
similarity index 75%
rename from internal/server/email_routes.go
rename to internal/server/middleware/email.go
index 38775c4..c9e5a52 100644
--- a/internal/server/email_routes.go
+++ b/internal/server/middleware/email.go
@@ -1,4 +1,4 @@
-package server
+package middleware
import (
"bytes"
@@ -8,12 +8,13 @@ import (
"strings"
"time"
+ "kra/internal/server/httpx"
"kra/internal/service"
"github.com/gin-gonic/gin"
)
-func emailErrorAlert(svc *service.EmailService, logger *slog.Logger) gin.HandlerFunc {
+func EmailErrorAlert(service *service.EmailService, logger *slog.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
if strings.Contains(c.Request.URL.Path, "/email/") {
c.Next()
@@ -28,19 +29,19 @@ func emailErrorAlert(svc *service.EmailService, logger *slog.Logger) gin.Handler
var result struct {
Code int `json:"code"`
}
- failed = json.Unmarshal(writer.body.Bytes(), &result) == nil && result.Code != codeSuccess
+ failed = json.Unmarshal(writer.body.Bytes(), &result) == nil && result.Code != httpx.CodeSuccess
}
if !failed {
return
}
username := "Unknown"
- if claims := currentClaims(c); claims != nil && claims.Username != "" {
+ if claims := Claims(c); claims != nil && claims.Username != "" {
username = claims.Username
}
subject := fmt.Sprintf("%s %s 调用 %s 报错", username, c.ClientIP(), c.Request.URL.Path)
body := bytes.NewBuffer(nil)
fmt.Fprintf(body, "请求方式:%s
请求路径:%s
状态码:%d
耗时:%s
错误响应:
%s", c.Request.Method, c.Request.URL.Path, c.Writer.Status(), time.Since(started), redactJSON(writer.body.Bytes())) - if err := svc.Alert(c.Request.Context(), subject, body.String()); err != nil && logger != nil { + if err := service.Alert(c.Request.Context(), subject, body.String()); err != nil && logger != nil { logger.Error("send HTTP error email", "error", err) } } diff --git a/internal/server/middleware/rate_limit.go b/internal/server/middleware/rate_limit.go new file mode 100644 index 0000000..b45c69e --- /dev/null +++ b/internal/server/middleware/rate_limit.go @@ -0,0 +1,38 @@ +package middleware + +import ( + "net/http" + "strconv" + "time" + + "kra/internal/server/httpx" + "kra/internal/service" + + "github.com/gin-gonic/gin" +) + +func SecurityRateLimit(system *service.SystemService, settings *service.SettingsService) gin.HandlerFunc { + return func(c *gin.Context) { + if isBootstrapPath(c.Request.URL.Path) { + c.Next() + return + } + config, err := settings.CurrentSecurity(c.Request.Context()) + if err != nil || config == nil || !config.LimitEnable { + c.Next() + return + } + window := config.LimitWindow + 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) + if cacheErr == nil && int(count) > config.LimitCount { + c.AbortWithStatusJSON(http.StatusTooManyRequests, httpx.Response{Code: httpx.CodeError, Data: nil, Msg: "请求过于频繁"}) + return + } + c.Next() + } +} diff --git a/internal/server/response.go b/internal/server/response.go deleted file mode 100644 index cc5f924..0000000 --- a/internal/server/response.go +++ /dev/null @@ -1,29 +0,0 @@ -package server - -import ( - "kra/internal/server/httpx" - - "github.com/gin-gonic/gin" -) - -const ( - codeSuccess = httpx.CodeSuccess - codeError = httpx.CodeError - codePasswordChangeRequired = httpx.CodePasswordChangeRequired -) - -type response = httpx.Response - -type pageResult = httpx.PageResult - -func writeResult(c *gin.Context, code int, data any, message string) { - httpx.Write(c, code, data, message) -} - -func ok(c *gin.Context) { httpx.OK(c) } - -func okWithData(c *gin.Context, data any) { httpx.OKWithData(c, data) } - -func fail(c *gin.Context, message string) { httpx.Fail(c, message) } - -func noAuth(c *gin.Context, message string) { httpx.NoAuth(c, message) } diff --git a/internal/server/router/api_token.go b/internal/server/router/api_token.go new file mode 100644 index 0000000..4ad9d09 --- /dev/null +++ b/internal/server/router/api_token.go @@ -0,0 +1,14 @@ +package router + +import ( + "kra/internal/server/handler" + + "github.com/gin-gonic/gin" +) + +func RegisterAPIToken(group *gin.RouterGroup, handler *handler.APIToken) { + router := group.Group("/sysApiToken") + router.POST("/createApiToken", handler.Create) + router.POST("/getApiTokenList", handler.List) + router.POST("/deleteApiToken", handler.Delete) +} diff --git a/internal/server/router/audit.go b/internal/server/router/audit.go new file mode 100644 index 0000000..e526700 --- /dev/null +++ b/internal/server/router/audit.go @@ -0,0 +1,34 @@ +package router + +import ( + "kra/internal/server/handler" + + "github.com/gin-gonic/gin" +) + +func RegisterAudit(group, public *gin.RouterGroup, h *handler.Audit) { + operations := group.Group("/sysOperationRecord") + operations.GET("/getSysOperationRecordList", h.Operations) + operations.GET("/findSysOperationRecord", h.Operation) + operations.DELETE("/deleteSysOperationRecord", h.DeleteOperation) + operations.DELETE("/deleteSysOperationRecordByIds", h.DeleteOperations) + logins := group.Group("/sysLoginLog") + logins.GET("/getLoginLogList", h.Logins) + logins.GET("/findLoginLog", h.Login) + logins.DELETE("/deleteLoginLog", h.DeleteLogin) + logins.DELETE("/deleteLoginLogByIds", h.DeleteLogins) + access := group.Group("/dataAccessLog") + access.POST("/getDataAccessLogList", h.DataAccess) + access.DELETE("/deleteDataAccessLogByIds", h.DeleteDataAccess) + viewer := group.Group("/logViewer") + viewer.GET("/dates", h.LogDates) + viewer.GET("/files", h.LogFiles) + viewer.GET("/content", h.LogContent) + errors := group.Group("/sysError") + errors.DELETE("/deleteSysError", h.DeleteError) + errors.DELETE("/deleteSysErrorByIds", h.DeleteErrors) + errors.PUT("/updateSysError", h.UpdateError) + errors.GET("/findSysError", h.Error) + errors.GET("/getSysErrorList", h.Errors) + public.Group("/sysError").POST("/createSysError", h.CreateError) +} diff --git a/internal/server/router/dictionary.go b/internal/server/router/dictionary.go new file mode 100644 index 0000000..f4c5cd1 --- /dev/null +++ b/internal/server/router/dictionary.go @@ -0,0 +1,29 @@ +package router + +import ( + "kra/internal/server/handler" + + "github.com/gin-gonic/gin" +) + +func RegisterDictionary(group *gin.RouterGroup, h *handler.Dictionary) { + dictionaries := group.Group("/sysDictionary") + dictionaries.POST("/createSysDictionary", h.Create) + dictionaries.PUT("/updateSysDictionary", h.Update) + dictionaries.DELETE("/deleteSysDictionary", h.Delete) + dictionaries.GET("/findSysDictionary", h.Find) + dictionaries.GET("/getSysDictionaryList", h.List(false)) + dictionaries.GET("/getSysDictionaryListWithDetails", h.List(true)) + dictionaries.GET("/exportSysDictionary", h.Export) + dictionaries.POST("/importSysDictionary", h.Import) + details := group.Group("/sysDictionaryDetail") + details.POST("/createSysDictionaryDetail", h.CreateDetail) + details.PUT("/updateSysDictionaryDetail", h.UpdateDetail) + details.DELETE("/deleteSysDictionaryDetail", h.DeleteDetail) + details.GET("/findSysDictionaryDetail", h.FindDetail) + details.GET("/getSysDictionaryDetailList", h.Details) + details.GET("/getDictionaryTreeList", h.Tree(false)) + details.GET("/getDictionaryTreeListByType", h.Tree(true)) + details.GET("/getDictionaryDetailsByParent", h.DetailsByParent) + details.GET("/getDictionaryPath", h.Path) +} diff --git a/internal/server/router/export.go b/internal/server/router/export.go new file mode 100644 index 0000000..1d4c563 --- /dev/null +++ b/internal/server/router/export.go @@ -0,0 +1,24 @@ +package router + +import ( + "kra/internal/server/handler" + + "github.com/gin-gonic/gin" +) + +func RegisterExport(group, public *gin.RouterGroup, h *handler.Export) { + router := group.Group("/sysExportTemplate") + router.POST("/createSysExportTemplate", h.Create) + router.PUT("/updateSysExportTemplate", h.Update) + router.DELETE("/deleteSysExportTemplate", h.Delete) + router.DELETE("/deleteSysExportTemplateByIds", h.DeleteMany) + router.GET("/findSysExportTemplate", h.Find) + router.GET("/getSysExportTemplateList", h.List) + router.GET("/previewSQL", h.Preview) + router.GET("/exportExcel", h.Issue(false)) + router.GET("/exportTemplate", h.Issue(true)) + router.POST("/importExcel", h.Import) + pub := public.Group("/sysExportTemplate") + pub.GET("/exportExcelByToken", h.Download(false)) + pub.GET("/exportTemplateByToken", h.Download(true)) +} diff --git a/internal/server/router/media.go b/internal/server/router/media.go new file mode 100644 index 0000000..67cbf4a --- /dev/null +++ b/internal/server/router/media.go @@ -0,0 +1,28 @@ +package router + +import ( + "kra/internal/server/handler" + + "github.com/gin-gonic/gin" +) + +func RegisterMedia(group *gin.RouterGroup, h *handler.Media) { + files := group.Group("/fileUploadAndDownload") + files.POST("/upload", h.Upload) + files.POST("/getFileList", h.List) + files.POST("/deleteFile", h.Delete) + files.POST("/deleteFiles", h.DeleteMany) + files.GET("/findFile", h.Find) + files.POST("/editFileName", h.Rename) + files.POST("/importURL", h.ImportURLs) + files.POST("/listOssFiles", h.Storage) + categories := group.Group("/attachmentCategory") + categories.GET("/getCategoryList", h.Categories) + categories.POST("/addCategory", h.SaveCategory) + categories.POST("/deleteCategory", h.DeleteCategory) + upload := group.Group("/mediaUpload") + upload.POST("/init", h.InitUpload) + upload.POST("/chunk", h.SaveChunk) + upload.POST("/complete", h.CompleteUpload) + upload.DELETE("/:uploadId", h.CancelUpload) +} diff --git a/internal/server/router/parameter.go b/internal/server/router/parameter.go new file mode 100644 index 0000000..c117ba4 --- /dev/null +++ b/internal/server/router/parameter.go @@ -0,0 +1,18 @@ +package router + +import ( + "kra/internal/server/handler" + + "github.com/gin-gonic/gin" +) + +func RegisterParameter(group *gin.RouterGroup, handler *handler.Parameter) { + router := group.Group("/sysParams") + router.POST("/createSysParams", handler.Create) + router.PUT("/updateSysParams", handler.Update) + router.DELETE("/deleteSysParams", handler.Delete) + router.DELETE("/deleteSysParamsByIds", handler.DeleteMany) + router.GET("/findSysParams", handler.Find) + router.GET("/getSysParam", handler.Get) + router.GET("/getSysParamsList", handler.List) +} diff --git a/internal/server/router/public.go b/internal/server/router/public.go new file mode 100644 index 0000000..2e1be19 --- /dev/null +++ b/internal/server/router/public.go @@ -0,0 +1,16 @@ +package router + +import ( + "kra/internal/server/handler" + + "github.com/gin-gonic/gin" +) + +func RegisterPublic(group *gin.RouterGroup, engine *gin.Engine, handler *handler.Public) { + base := group.Group("/base") + base.POST("/captcha", handler.Captcha) + base.POST("/login", handler.Login) + init := group.Group("/init") + init.POST("/checkdb", handler.CheckDatabase) + init.POST("/initdb", handler.InitializeDatabase(engine)) +} diff --git a/internal/server/router/system_config.go b/internal/server/router/system_config.go new file mode 100644 index 0000000..0f49979 --- /dev/null +++ b/internal/server/router/system_config.go @@ -0,0 +1,19 @@ +package router + +import ( + "kra/internal/server/handler" + + "github.com/gin-gonic/gin" +) + +func RegisterSystemConfig(group *gin.RouterGroup, handler *handler.SystemConfig) { + security := group.Group("/securityConfig") + security.GET("/getSecurityConfig", handler.GetSecurity) + security.POST("/setSecurityConfig", handler.SetSecurity) + + system := group.Group("/system") + system.POST("/getSystemConfig", handler.Get) + system.POST("/setSystemConfig", handler.Set) + system.POST("/reloadSystem", handler.Reload) + system.POST("/getServerInfo", handler.ServerInfo) +} diff --git a/internal/server/router/user.go b/internal/server/router/user.go new file mode 100644 index 0000000..6b98ca8 --- /dev/null +++ b/internal/server/router/user.go @@ -0,0 +1,30 @@ +package router + +import ( + "kra/internal/server/handler" + + "github.com/gin-gonic/gin" +) + +func RegisterUser(group *gin.RouterGroup, handler *handler.User) { + user := group.Group("/user") + user.POST("/getUserList", handler.List) + user.POST("/admin_register", handler.Create) + user.PUT("/setUserInfo", handler.Update) + user.PUT("/setSelfInfo", handler.UpdateSelf) + user.DELETE("/deleteUser", handler.Delete) + user.POST("/resetPassword", handler.ResetPassword) + user.POST("/changePassword", handler.ChangePassword) + user.PUT("/setSelfSetting", handler.SetSelfSetting) + user.POST("/setUserAuthorities", handler.SetAuthorities) + user.POST("/setUserAuthority", handler.SwitchAuthority) + user.GET("/getUserInfo", handler.Get) +} + +func RegisterNavigation(group *gin.RouterGroup, handler *handler.Navigation) { + group.Group("/menu").POST("/getMenu", handler.Menu) +} + +func RegisterSession(group *gin.RouterGroup, handler *handler.Session) { + group.Group("/jwt").POST("/jsonInBlacklist", handler.Logout) +} diff --git a/internal/server/router/version.go b/internal/server/router/version.go new file mode 100644 index 0000000..50289cc --- /dev/null +++ b/internal/server/router/version.go @@ -0,0 +1,18 @@ +package router + +import ( + "kra/internal/server/handler" + + "github.com/gin-gonic/gin" +) + +func RegisterVersion(group *gin.RouterGroup, h *handler.Version) { + router := group.Group("/sysVersion") + router.DELETE("/deleteSysVersion", h.Delete) + router.DELETE("/deleteSysVersionByIds", h.DeleteMany) + router.GET("/findSysVersion", h.Find) + router.GET("/getSysVersionList", h.List) + router.POST("/exportVersion", h.Export) + router.GET("/downloadVersionJson", h.Download) + router.POST("/importVersion", h.Import) +} diff --git a/internal/server/server.go b/internal/server/server.go index bbe8739..ed7d9a5 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -8,4 +8,4 @@ import ( ) // ProviderSet is server providers. -var ProviderSet = wire.NewSet(NewGinServer, handler.NewAuthority, handler.NewMenu, handler.NewAPI, handler.NewPermission, handler.NewOrganization, handler.NewAnnouncement, handler.NewEmail, handler.NewTask, worker.NewTaskScheduler) +var ProviderSet = wire.NewSet(NewGinServer, handler.NewAuthority, handler.NewMenu, handler.NewAPI, handler.NewPermission, handler.NewOrganization, handler.NewAnnouncement, handler.NewEmail, handler.NewTask, handler.NewMedia, handler.NewAudit, handler.NewExport, handler.NewVersion, handler.NewDictionary, handler.NewParameter, handler.NewAPIToken, handler.NewSystemConfig, handler.NewPublic, handler.NewUser, handler.NewNavigation, handler.NewSession, worker.NewTaskScheduler) diff --git a/internal/server/settings_routes.go b/internal/server/settings_routes.go deleted file mode 100644 index 87eac9f..0000000 --- a/internal/server/settings_routes.go +++ /dev/null @@ -1,435 +0,0 @@ -package server - -import ( - "encoding/json" - "strconv" - - "kra/internal/biz" - "kra/internal/service" - - "github.com/gin-gonic/gin" -) - -type dictionaryRequest struct { - ID uint `json:"ID" form:"ID"` - Name string `json:"name" form:"name"` - Type string `json:"type" form:"type"` - Status *bool `json:"status" form:"status"` - Desc string `json:"desc" form:"desc"` - ParentID *uint `json:"parentID" form:"parentID"` -} - -func dictionaryDomain(v dictionaryRequest) *biz.Dictionary { - status := true - if v.Status != nil { - status = *v.Status - } - return &biz.Dictionary{ID: v.ID, Name: v.Name, Type: v.Type, Status: status, Desc: v.Desc, ParentID: v.ParentID} -} - -type detailRequest struct { - ID uint `json:"ID" form:"ID"` - Label string `json:"label" form:"label"` - Value string `json:"value" form:"value"` - Extend string `json:"extend" form:"extend"` - Status *bool `json:"status" form:"status"` - Sort int `json:"sort" form:"sort"` - DictionaryID uint `json:"sysDictionaryID" form:"sysDictionaryID"` - ParentID *uint `json:"parentID" form:"parentID"` - Level int `json:"level"` - Path string `json:"path"` -} - -func detailDomain(v detailRequest) *biz.DictionaryDetail { - status := true - if v.Status != nil { - status = *v.Status - } - return &biz.DictionaryDetail{ID: v.ID, Label: v.Label, Value: v.Value, Extend: v.Extend, Status: status, Sort: v.Sort, DictionaryID: v.DictionaryID, ParentID: v.ParentID, Level: v.Level, Path: v.Path} -} - -func registerSettingsRoutes(group *gin.RouterGroup, public *gin.RouterGroup, svc *service.SettingsService) { - registerDictionaryRoutes(group, svc) - registerParameterRoutes(group, svc) - registerAPITokenRoutes(group, svc) -} - -func registerDictionaryRoutes(group *gin.RouterGroup, svc *service.SettingsService) { - dictionaries := group.Group("/sysDictionary") - dictionaries.POST("/createSysDictionary", func(c *gin.Context) { - var req dictionaryRequest - if c.ShouldBindJSON(&req) != nil || req.Name == "" || req.Type == "" { - fail(c, "参数错误") - return - } - if err := svc.CreateDictionary(c.Request.Context(), dictionaryDomain(req)); err != nil { - fail(c, "创建失败:"+err.Error()) - return - } - ok(c) - }) - dictionaries.PUT("/updateSysDictionary", func(c *gin.Context) { - var req dictionaryRequest - if c.ShouldBindJSON(&req) != nil || req.ID == 0 { - fail(c, "参数错误") - return - } - if err := svc.UpdateDictionary(c.Request.Context(), dictionaryDomain(req)); err != nil { - fail(c, "更新失败") - return - } - ok(c) - }) - dictionaries.DELETE("/deleteSysDictionary", func(c *gin.Context) { - var req dictionaryRequest - if c.ShouldBindJSON(&req) != nil || req.ID == 0 { - fail(c, "参数错误") - return - } - if err := svc.DeleteDictionary(c.Request.Context(), req.ID); err != nil { - fail(c, err.Error()) - return - } - ok(c) - }) - dictionaries.GET("/findSysDictionary", func(c *gin.Context) { - id, _ := strconv.ParseUint(c.Query("ID"), 10, 64) - item, err := svc.Dictionary(c.Request.Context(), uint(id), c.Query("type"), true) - if err != nil { - fail(c, "查询失败") - return - } - writeResult(c, codeSuccess, gin.H{"reSysDictionary": item}, "查询成功") - }) - listHandler := func(details bool) gin.HandlerFunc { - return func(c *gin.Context) { - page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) - size, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10")) - items, total, err := svc.Dictionaries(c.Request.Context(), page, size, c.Query("name"), c.Query("type"), details) - if err != nil { - fail(c, "获取失败") - return - } - writeResult(c, codeSuccess, pageResult{List: items, Total: total, Page: page, PageSize: size}, "获取成功") - } - } - dictionaries.GET("/getSysDictionaryList", listHandler(false)) - dictionaries.GET("/getSysDictionaryListWithDetails", listHandler(true)) - dictionaries.GET("/exportSysDictionary", func(c *gin.Context) { - id, _ := strconv.ParseUint(c.Query("ID"), 10, 64) - item, err := svc.Dictionary(c.Request.Context(), uint(id), "", true) - if err != nil { - fail(c, "导出失败") - return - } - delete(item, "ID") - delete(item, "CreatedAt") - delete(item, "UpdatedAt") - delete(item, "DeletedAt") - writeResult(c, codeSuccess, item, "导出成功") - }) - dictionaries.POST("/importSysDictionary", func(c *gin.Context) { - var req struct { - JSON string `json:"json"` - } - if c.ShouldBindJSON(&req) != nil { - fail(c, "参数错误") - return - } - var payload struct { - Name string `json:"name"` - Type string `json:"type"` - Status *bool `json:"status"` - Desc string `json:"desc"` - Details []detailRequest `json:"sysDictionaryDetails"` - } - if json.Unmarshal([]byte(req.JSON), &payload) != nil || payload.Name == "" || payload.Type == "" { - fail(c, "JSON 格式错误") - return - } - dictionary := dictionaryDomain(dictionaryRequest{Name: payload.Name, Type: payload.Type, Status: payload.Status, Desc: payload.Desc}) - if err := svc.CreateDictionary(c.Request.Context(), dictionary); err != nil { - fail(c, "导入失败:"+err.Error()) - return - } - mapping := map[uint]uint{} - pending := append([]detailRequest(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 := svc.CreateDictionaryDetail(c.Request.Context(), value); err != nil { - fail(c, "导入详情失败") - return - } - if oldID != 0 { - mapping[oldID] = value.ID - } - progress = true - } - if !progress { - fail(c, "字典层级关系无效") - return - } - pending = next - } - ok(c) - }) - - details := group.Group("/sysDictionaryDetail") - details.POST("/createSysDictionaryDetail", func(c *gin.Context) { - var req detailRequest - if c.ShouldBindJSON(&req) != nil || req.DictionaryID == 0 { - fail(c, "参数错误") - return - } - if err := svc.CreateDictionaryDetail(c.Request.Context(), detailDomain(req)); err != nil { - fail(c, "创建失败") - return - } - ok(c) - }) - details.PUT("/updateSysDictionaryDetail", func(c *gin.Context) { - var req detailRequest - if c.ShouldBindJSON(&req) != nil || req.ID == 0 { - fail(c, "参数错误") - return - } - if err := svc.UpdateDictionaryDetail(c.Request.Context(), detailDomain(req)); err != nil { - fail(c, err.Error()) - return - } - ok(c) - }) - details.DELETE("/deleteSysDictionaryDetail", func(c *gin.Context) { - var req detailRequest - if c.ShouldBindJSON(&req) != nil || req.ID == 0 { - fail(c, "参数错误") - return - } - if err := svc.DeleteDictionaryDetail(c.Request.Context(), req.ID); err != nil { - fail(c, err.Error()) - return - } - ok(c) - }) - details.GET("/findSysDictionaryDetail", func(c *gin.Context) { - id, _ := strconv.ParseUint(c.Query("ID"), 10, 64) - item, err := svc.DictionaryDetail(c.Request.Context(), uint(id)) - if err != nil { - fail(c, "查询失败") - return - } - writeResult(c, codeSuccess, gin.H{"reSysDictionaryDetail": item}, "查询成功") - }) - details.GET("/getSysDictionaryDetailList", func(c *gin.Context) { - page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) - size, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10")) - did, _ := strconv.ParseUint(c.Query("sysDictionaryID"), 10, 64) - items, total, err := svc.DictionaryDetails(c.Request.Context(), page, size, uint(did), c.Query("label"), c.Query("value")) - if err != nil { - fail(c, "获取失败") - return - } - writeResult(c, codeSuccess, pageResult{List: items, Total: total, Page: page, PageSize: size}, "获取成功") - }) - treeHandler := func(byType bool) gin.HandlerFunc { - return func(c *gin.Context) { - id, _ := strconv.ParseUint(c.Query("sysDictionaryID"), 10, 64) - typ := "" - if byType { - typ = c.Query("type") - } - items, err := svc.DictionaryTree(c.Request.Context(), uint(id), typ) - if err != nil { - fail(c, "获取失败") - return - } - writeResult(c, codeSuccess, items, "获取成功") - } - } - details.GET("/getDictionaryTreeList", treeHandler(false)) - details.GET("/getDictionaryTreeListByType", treeHandler(true)) - details.GET("/getDictionaryDetailsByParent", func(c *gin.Context) { - id, _ := strconv.ParseUint(c.Query("parentID"), 10, 64) - did, _ := strconv.ParseUint(c.Query("sysDictionaryID"), 10, 64) - items, _, err := svc.DictionaryDetails(c.Request.Context(), 1, 10000, uint(did), "", "") - if err != nil { - fail(c, "获取失败") - return - } - out := []map[string]any{} - for _, item := range items { - parent, _ := item["parentID"].(*uint) - if (id == 0 && parent == nil) || (parent != nil && uint64(*parent) == id) { - out = append(out, item) - } - } - writeResult(c, codeSuccess, out, "获取成功") - }) - details.GET("/getDictionaryPath", func(c *gin.Context) { - id, _ := strconv.ParseUint(c.Query("ID"), 10, 64) - item, err := svc.DictionaryDetail(c.Request.Context(), uint(id)) - if err != nil { - fail(c, "获取失败") - return - } - writeResult(c, codeSuccess, gin.H{"path": item["path"]}, "获取成功") - }) -} - -type parameterRequest struct { - ID uint `json:"ID" form:"ID"` - Name string `json:"name" form:"name"` - Key string `json:"key" form:"key"` - Value string `json:"value" form:"value"` - Desc string `json:"desc" form:"desc"` -} - -func parameterDomain(v parameterRequest) *biz.SystemParameter { - return &biz.SystemParameter{ID: v.ID, Name: v.Name, Key: v.Key, Value: v.Value, Desc: v.Desc} -} -func registerParameterRoutes(group *gin.RouterGroup, svc *service.SettingsService) { - router := group.Group("/sysParams") - router.POST("/createSysParams", func(c *gin.Context) { - var req parameterRequest - if c.ShouldBindJSON(&req) != nil || req.Key == "" { - fail(c, "参数错误") - return - } - if err := svc.CreateParameter(c.Request.Context(), parameterDomain(req)); err != nil { - fail(c, "创建失败") - return - } - ok(c) - }) - router.PUT("/updateSysParams", func(c *gin.Context) { - var req parameterRequest - if c.ShouldBindJSON(&req) != nil || req.ID == 0 { - fail(c, "参数错误") - return - } - if err := svc.UpdateParameter(c.Request.Context(), parameterDomain(req)); err != nil { - fail(c, "更新失败") - return - } - ok(c) - }) - router.DELETE("/deleteSysParams", func(c *gin.Context) { - id, err := strconv.ParseUint(c.Query("ID"), 10, 64) - if err != nil || id == 0 { - fail(c, "参数错误") - return - } - if err := svc.DeleteParameters(c.Request.Context(), []uint{uint(id)}); err != nil { - fail(c, "删除失败") - return - } - ok(c) - }) - router.DELETE("/deleteSysParamsByIds", func(c *gin.Context) { - ids := idsFromQuery(c) - if len(ids) == 0 { - fail(c, "参数错误") - return - } - if err := svc.DeleteParameters(c.Request.Context(), ids); err != nil { - fail(c, "删除失败") - return - } - ok(c) - }) - router.GET("/findSysParams", func(c *gin.Context) { - id, _ := strconv.ParseUint(c.Query("ID"), 10, 64) - item, err := svc.Parameter(c.Request.Context(), uint(id), "") - if err != nil { - fail(c, "查询失败") - return - } - writeResult(c, codeSuccess, gin.H{"resysParams": item}, "查询成功") - }) - router.GET("/getSysParam", func(c *gin.Context) { - item, err := svc.Parameter(c.Request.Context(), 0, c.Query("key")) - if err != nil { - fail(c, "查询失败") - return - } - writeResult(c, codeSuccess, item, "查询成功") - }) - router.GET("/getSysParamsList", func(c *gin.Context) { - page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) - size, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10")) - items, total, err := svc.Parameters(c.Request.Context(), page, size, &biz.SystemParameter{Name: c.Query("name"), Key: c.Query("key")}) - if err != nil { - fail(c, "获取失败") - return - } - writeResult(c, codeSuccess, pageResult{List: items, Total: total, Page: page, PageSize: size}, "获取成功") - }) -} - -func registerAPITokenRoutes(group *gin.RouterGroup, svc *service.SettingsService) { - router := group.Group("/sysApiToken") - router.POST("/createApiToken", func(c *gin.Context) { - var req struct { - UserID uint `json:"userId"` - AuthorityID uint `json:"authorityId"` - Days int `json:"days"` - Remark string `json:"remark"` - } - if c.ShouldBindJSON(&req) != nil { - fail(c, "参数错误") - return - } - token, err := svc.CreateAPIToken(c.Request.Context(), req.UserID, req.AuthorityID, req.Days, req.Remark) - if err != nil { - fail(c, "签发失败:"+err.Error()) - return - } - writeResult(c, codeSuccess, gin.H{"token": token}, "签发成功") - }) - router.POST("/getApiTokenList", func(c *gin.Context) { - var req struct { - Page int `json:"page"` - PageSize int `json:"pageSize"` - UserID uint `json:"userId"` - Status *bool `json:"status"` - } - if c.ShouldBindJSON(&req) != nil { - fail(c, "参数错误") - return - } - items, total, err := svc.APITokens(c.Request.Context(), req.Page, req.PageSize, req.UserID, req.Status) - if err != nil { - fail(c, "获取失败") - return - } - writeResult(c, codeSuccess, pageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功") - }) - router.POST("/deleteApiToken", func(c *gin.Context) { - var req struct { - ID uint `json:"ID"` - } - if c.ShouldBindJSON(&req) != nil { - fail(c, "参数错误") - return - } - if err := svc.DisableAPIToken(c.Request.Context(), req.ID); err != nil { - fail(c, "作废失败") - return - } - ok(c) - }) -} diff --git a/internal/server/system_config_routes.go b/internal/server/system_config_routes.go deleted file mode 100644 index ace70a7..0000000 --- a/internal/server/system_config_routes.go +++ /dev/null @@ -1,215 +0,0 @@ -package server - -import ( - "runtime" - "syscall" - "time" - - "kra/internal/biz" - "kra/internal/conf" - "kra/internal/service" - "kra/internal/worker" - - "github.com/gin-gonic/gin" - "github.com/shirou/gopsutil/v4/cpu" - "github.com/shirou/gopsutil/v4/mem" - "google.golang.org/protobuf/proto" -) - -func registerSystemConfigRoutes(group *gin.RouterGroup, config *conf.AdminBackend, systemService *service.SystemService, settings *service.SettingsService, scheduler *worker.TaskScheduler) { - security := group.Group("/securityConfig") - security.GET("/getSecurityConfig", func(c *gin.Context) { - value, err := settings.Security(c.Request.Context()) - if err != nil { - fail(c, "获取安全配置失败") - return - } - writeResult(c, codeSuccess, value, "获取成功") - }) - security.POST("/setSecurityConfig", func(c *gin.Context) { - var value biz.SecurityConfig - if c.ShouldBindJSON(&value) != nil { - fail(c, "参数错误") - return - } - result, err := settings.SaveSecurity(c.Request.Context(), &value) - if err != nil { - fail(c, err.Error()) - return - } - writeResult(c, codeSuccess, result, "设置成功") - }) - - system := group.Group("/system") - system.POST("/getSystemConfig", func(c *gin.Context) { - admin := gin.H{"routerPrefix": ""} - email := gin.H{} - if config != nil { - admin["routerPrefix"] = config.RouterPrefix - if config.Jwt != nil { - admin["jwt"] = gin.H{"signingKey": "******", "expiresTime": config.Jwt.ExpiresTime.AsDuration().String(), "bufferTime": config.Jwt.BufferTime.AsDuration().String(), "issuer": config.Jwt.Issuer} - } - if config.Captcha != nil { - admin["captcha"] = gin.H{"keyLong": config.Captcha.KeyLong, "imgWidth": config.Captcha.ImgWidth, "imgHeight": config.Captcha.ImgHeight, "storeExpiration": config.Captcha.StoreExpiration.AsDuration().String()} - } - if config.Local != nil { - admin["local"] = gin.H{"storePath": config.Local.StorePath, "pathPrefix": config.Local.PathPrefix} - } - if config.Media != nil { - admin["media"] = gin.H{"sessionTtl": config.Media.SessionTtl} - } - if config.Email != nil { - email = gin.H{"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 - } - } - writeResult(c, codeSuccess, gin.H{"config": gin.H{"admin": admin, "email": email}}, "获取成功") - }) - system.POST("/setSystemConfig", func(c *gin.Context) { - var req struct { - Config struct { - Admin struct { - RouterPrefix string `json:"routerPrefix"` - JWT struct { - SigningKey string `json:"signingKey"` - Issuer string `json:"issuer"` - } `json:"jwt"` - Local struct { - StorePath string `json:"storePath"` - PathPrefix string `json:"pathPrefix"` - } `json:"local"` - Media struct { - SessionTTL int32 `json:"sessionTtl"` - } `json:"media"` - Storage *conf.AdminBackend_Storage `json:"storage"` - } `json:"admin"` - Email *struct { - To string `json:"to"` - From string `json:"from"` - Host string `json:"host"` - Secret string `json:"secret"` - Nickname string `json:"nickname"` - Port int32 `json:"port"` - IsSSL bool `json:"is-ssl"` - IsLoginAuth bool `json:"is-loginauth"` - } `json:"email"` - } `json:"config"` - } - if c.ShouldBindJSON(&req) != nil { - fail(c, "参数错误") - return - } - if config != nil { - next := proto.Clone(config).(*conf.AdminBackend) - next.RouterPrefix = req.Config.Admin.RouterPrefix - if next.Jwt != nil { - if req.Config.Admin.JWT.SigningKey != "" && req.Config.Admin.JWT.SigningKey != "******" { - next.Jwt.SigningKey = req.Config.Admin.JWT.SigningKey - } - if req.Config.Admin.JWT.Issuer != "" { - next.Jwt.Issuer = req.Config.Admin.JWT.Issuer - } - } - if next.Local != nil { - if req.Config.Admin.Local.StorePath != "" { - next.Local.StorePath = req.Config.Admin.Local.StorePath - } - if req.Config.Admin.Local.PathPrefix != "" { - next.Local.PathPrefix = req.Config.Admin.Local.PathPrefix - } - } - if next.Media != nil && req.Config.Admin.Media.SessionTTL > 0 { - next.Media.SessionTtl = req.Config.Admin.Media.SessionTTL - } - if req.Config.Admin.Storage != nil { - preserveStorageSecrets(req.Config.Admin.Storage, next.Storage) - next.Storage = req.Config.Admin.Storage - } - if next.Email != nil && req.Config.Email != nil { - next.Email.To = req.Config.Email.To - next.Email.From = req.Config.Email.From - next.Email.Host = req.Config.Email.Host - next.Email.Nickname = req.Config.Email.Nickname - next.Email.Port = req.Config.Email.Port - next.Email.IsSsl = req.Config.Email.IsSSL - next.Email.IsLoginAuth = req.Config.Email.IsLoginAuth - if req.Config.Email.Secret != "" && req.Config.Email.Secret != "******" { - next.Email.Secret = req.Config.Email.Secret - } - } - if err := systemService.PersistAdminConfig(c.Request.Context(), next); err != nil { - fail(c, "配置保存失败: "+err.Error()) - return - } - } - writeResult(c, codeSuccess, gin.H{}, "设置成功") - }) - system.POST("/reloadSystem", func(c *gin.Context) { - if err := systemService.ReloadConfig(c.Request.Context()); err != nil { - fail(c, "重载系统失败:"+err.Error()) - return - } - if err := scheduler.Reload(c.Request.Context()); err != nil { - fail(c, "系统配置已重载,但定时任务重载失败:"+err.Error()) - return - } - writeResult(c, codeSuccess, gin.H{}, "重载系统成功") - }) - system.POST("/getServerInfo", func(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) - } - 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}) - } - 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} - writeResult(c, codeSuccess, gin.H{"server": server}, "获取成功") - }) -} - -func objectStores(storage *conf.AdminBackend_Storage) []*conf.AdminBackend_ObjectStore { - if storage == nil { - return nil - } - return []*conf.AdminBackend_ObjectStore{storage.AliyunOss, storage.HuaweiObs, storage.TencentCos, storage.AwsS3, storage.CloudflareR2, storage.Minio} -} -func maskStorageSecrets(storage *conf.AdminBackend_Storage) { - if storage.Qiniu != nil { - storage.Qiniu.SecretKey = "******" - } - for _, item := range objectStores(storage) { - if item != nil { - item.SecretKey = "******" - } - } -} -func preserveStorageSecrets(next, current *conf.AdminBackend_Storage) { - if next == nil || current == nil { - return - } - if next.Qiniu != nil && current.Qiniu != nil && (next.Qiniu.SecretKey == "" || next.Qiniu.SecretKey == "******") { - next.Qiniu.SecretKey = current.Qiniu.SecretKey - } - nextItems, currentItems := objectStores(next), objectStores(current) - for i := range nextItems { - if nextItems[i] != nil && currentItems[i] != nil && (nextItems[i].SecretKey == "" || nextItems[i].SecretKey == "******") { - nextItems[i].SecretKey = currentItems[i].SecretKey - } - } -} diff --git a/internal/server/version_routes.go b/internal/server/version_routes.go deleted file mode 100644 index 90f4499..0000000 --- a/internal/server/version_routes.go +++ /dev/null @@ -1,165 +0,0 @@ -package server - -import ( - "fmt" - "net/http" - "strconv" - "time" - - "github.com/gin-gonic/gin" - "kra/internal/biz" - "kra/internal/service" -) - -type versionMenuRequest struct { - Path string `json:"path"` - Name string `json:"name"` - Hidden bool `json:"hidden"` - Component string `json:"component"` - Sort int `json:"sort"` - Meta struct { - ActiveName string `json:"activeName"` - KeepAlive bool `json:"keepAlive"` - DefaultMenu bool `json:"defaultMenu"` - Title string `json:"title"` - Icon string `json:"icon"` - CloseTab bool `json:"closeTab"` - TransitionType string `json:"transitionType"` - } `json:"meta"` - MenuButtons []struct { - Name string `json:"name"` - Desc string `json:"desc"` - } `json:"menuBtn"` - Children []versionMenuRequest `json:"children"` -} - -func versionMenuDomain(v versionMenuRequest) *biz.Menu { - m := &biz.Menu{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} - for _, b := range v.MenuButtons { - m.Buttons = append(m.Buttons, &biz.MenuButton{Name: b.Name, Description: b.Desc}) - } - for _, child := range v.Children { - m.Children = append(m.Children, versionMenuDomain(child)) - } - return m -} -func registerVersionRoutes(group *gin.RouterGroup, svc *service.VersionService) { - router := group.Group("/sysVersion") - router.DELETE("/deleteSysVersion", func(c *gin.Context) { - id, _ := strconv.ParseUint(c.Query("ID"), 10, 64) - if err := svc.DeleteVersions(c.Request.Context(), []uint{uint(id)}); err != nil { - fail(c, "删除失败") - return - } - ok(c) - }) - router.DELETE("/deleteSysVersionByIds", func(c *gin.Context) { - raw := c.QueryArray("IDs[]") - ids := make([]uint, 0, len(raw)) - for _, v := range raw { - id, _ := strconv.ParseUint(v, 10, 64) - ids = append(ids, uint(id)) - } - if err := svc.DeleteVersions(c.Request.Context(), ids); err != nil { - fail(c, "删除失败") - return - } - ok(c) - }) - router.GET("/findSysVersion", func(c *gin.Context) { - id, _ := strconv.ParseUint(c.Query("ID"), 10, 64) - item, err := svc.Version(c.Request.Context(), uint(id)) - if err != nil { - fail(c, "查询失败") - return - } - writeResult(c, codeSuccess, item, "查询成功") - }) - router.GET("/getSysVersionList", func(c *gin.Context) { - page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) - size, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10")) - items, total, err := svc.Versions(c.Request.Context(), page, size, c.Query("versionName"), c.Query("versionCode")) - if err != nil { - fail(c, "获取失败") - return - } - writeResult(c, codeSuccess, pageResult{List: items, Total: total, Page: page, PageSize: size}, "获取成功") - }) - router.POST("/exportVersion", func(c *gin.Context) { - var req struct { - VersionName string `json:"versionName"` - VersionCode string `json:"versionCode"` - Description string `json:"description"` - MenuIDs []uint `json:"menuIds"` - APIIDs []uint `json:"apiIds"` - DictIDs []uint `json:"dictIds"` - } - if c.ShouldBindJSON(&req) != nil || req.VersionName == "" || req.VersionCode == "" { - fail(c, "参数错误") - return - } - if err := svc.Export(c.Request.Context(), req.VersionName, req.VersionCode, req.Description, req.MenuIDs, req.APIIDs, req.DictIDs); err != nil { - fail(c, "创建发版失败:"+err.Error()) - return - } - ok(c) - }) - router.GET("/downloadVersionJson", func(c *gin.Context) { - id, _ := strconv.ParseUint(c.Query("ID"), 10, 64) - raw, code, err := svc.VersionData(c.Request.Context(), uint(id)) - if err != nil { - fail(c, "下载失败") - return - } - name := fmt.Sprintf("version_%s_%s.json", code, time.Now().Format("20060102150405")) - c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", name)) - c.Data(http.StatusOK, "application/json; charset=utf-8", []byte(raw)) - }) - router.POST("/importVersion", func(c *gin.Context) { - var req struct { - Version struct { - Name string `json:"name"` - Code string `json:"code"` - Description string `json:"description"` - ExportTime string `json:"exportTime"` - } `json:"version"` - Menus []versionMenuRequest `json:"menus"` - APIs []struct { - Path string `json:"path"` - Description string `json:"description"` - APIGroup string `json:"apiGroup"` - Method string `json:"method"` - } `json:"apis"` - Dictionaries []struct { - Name string `json:"name"` - Type string `json:"type"` - Status bool `json:"status"` - Desc string `json:"desc"` - Details []detailRequest `json:"sysDictionaryDetails"` - } `json:"dictionaries"` - } - if c.ShouldBindJSON(&req) != nil { - fail(c, "参数错误") - return - } - bundle := &biz.VersionBundle{Name: req.Version.Name, Code: req.Version.Code, Description: req.Version.Description, ExportTime: req.Version.ExportTime} - for _, m := range req.Menus { - bundle.Menus = append(bundle.Menus, versionMenuDomain(m)) - } - for _, a := range req.APIs { - bundle.APIs = append(bundle.APIs, &biz.API{Path: a.Path, Description: a.Description, APIGroup: a.APIGroup, Method: a.Method}) - } - for _, d := range req.Dictionaries { - dictionary := &biz.Dictionary{Name: d.Name, Type: d.Type, Status: d.Status, Desc: d.Desc} - for _, detail := range d.Details { - dictionary.Details = append(dictionary.Details, detailDomain(detail)) - } - bundle.Dictionaries = append(bundle.Dictionaries, dictionary) - } - if err := svc.Import(c.Request.Context(), bundle); err != nil { - fail(c, "导入失败:"+err.Error()) - return - } - ok(c) - }) -} diff --git a/internal/service/access.go b/internal/service/access.go index 4dc1b88..d0d3e01 100644 --- a/internal/service/access.go +++ b/internal/service/access.go @@ -106,6 +106,13 @@ func (s *AccessService) DataScopeDepartmentIDs(ctx context.Context, id uint) ([] func (s *AccessService) ResolveDataScope(ctx context.Context, authorityID, userID uint) (biz.DataScope, error) { return s.uc.Repo().ResolveDataScope(ctx, authorityID, userID) } +func (s *AccessService) ContextWithDataScope(ctx context.Context, authorityID, userID uint) (context.Context, error) { + scope, err := s.ResolveDataScope(ctx, authorityID, userID) + if err != nil { + return ctx, err + } + return biz.NewDataScopeContext(ctx, scope), nil +} func (s *AccessService) DeleteAPIs(ctx context.Context, ids []uint) error { return s.uc.Repo().DeleteAPIs(ctx, ids) diff --git a/internal/service/api_token.go b/internal/service/api_token.go new file mode 100644 index 0000000..a97286e --- /dev/null +++ b/internal/service/api_token.go @@ -0,0 +1,79 @@ +package service + +import ( + "context" + "errors" + "time" + + "kra/internal/biz" + "kra/pkg/adminauth" +) + +func tokenDTO(v *biz.APIToken) map[string]any { + var user any = nil + if v.User != nil { + user = convertUser(v.User) + } + return map[string]any{"ID": v.ID, "CreatedAt": v.CreatedAt, "UpdatedAt": v.UpdatedAt, "DeletedAt": nil, "userId": v.UserID, "authorityId": v.AuthorityID, "token": v.Token, "status": v.Status, "expiresAt": v.ExpiresAt, "remark": v.Remark, "user": user} +} + +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) + 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 + } + if s.config.Jwt.BufferTime != nil { + buffer = s.config.Jwt.BufferTime.AsDuration() + } + } + token, _, err := adminauth.Generate(secret, issuer, duration, buffer, user.ID, authorityID, user.UUID, user.Username, user.NickName, false) + if err != nil { + 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 { + 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) + if err != nil { + return nil, 0, err + } + out := make([]map[string]any, 0, len(items)) + for _, v := range items { + out = append(out, tokenDTO(v)) + } + 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) +} +func (s *SettingsService) BlacklistToken(ctx context.Context, token string) error { + return s.Repo().BlacklistToken(ctx, token) +} +func (s *SettingsService) IsTokenDisabled(ctx context.Context, token string) (bool, error) { + return s.Repo().IsTokenDisabled(ctx, token) +} diff --git a/internal/service/audit.go b/internal/service/audit.go index 478d4b0..20b06f4 100644 --- a/internal/service/audit.go +++ b/internal/service/audit.go @@ -3,6 +3,7 @@ package service import ( "context" "kra/internal/biz" + "kra/internal/service/dto" ) type AuditService struct{ uc *biz.AuditUsecase } @@ -11,12 +12,30 @@ func NewAuditService(uc *biz.AuditUsecase) *AuditService { return &AuditService{ func (s *AuditService) RecordOperation(ctx context.Context, v *biz.OperationRecord) error { return s.uc.Repo().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 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} } @@ -117,6 +136,18 @@ func errorDTO(v *biz.ErrorRecord) map[string]any { 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) } diff --git a/internal/service/authentication.go b/internal/service/authentication.go new file mode 100644 index 0000000..ff3a033 --- /dev/null +++ b/internal/service/authentication.go @@ -0,0 +1,62 @@ +package service + +import ( + "context" + "time" + + "kra/internal/biz" + "kra/pkg/adminauth" +) + +type LoginResult struct { + User map[string]any `json:"user"` + Token string `json:"token"` + ExpiresAt int64 `json:"expiresAt"` + NeedChangePassword bool `json:"needChangePassword"` +} + +func (s *SystemService) CacheGet(ctx context.Context, key string) (string, bool, error) { + return s.uc.CacheGet(ctx, key) +} +func (s *SystemService) CacheSet(ctx context.Context, key, value string, expiration time.Duration) error { + return s.uc.CacheSet(ctx, key, value, expiration) +} +func (s *SystemService) CacheDelete(ctx context.Context, key string) error { + return s.uc.CacheDelete(ctx, key) +} +func (s *SystemService) CacheIncrement(ctx context.Context, key string, expiration time.Duration) (int64, error) { + return s.uc.CacheIncrement(ctx, key, expiration) +} + +func (s *SystemService) Login(ctx context.Context, username, password string) (*LoginResult, error) { + u, err := s.uc.Login(ctx, username, password) + if err != nil { + return nil, err + } + if u.Enable != 1 { + return nil, biz.ErrInvalidCredentials + } + 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) +} + +func (s *SystemService) issueLogin(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() + } + if s.config.Jwt.BufferTime != nil { + buffer = s.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 + } + 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 new file mode 100644 index 0000000..f644f9f --- /dev/null +++ b/internal/service/dictionary.go @@ -0,0 +1,182 @@ +package service + +import ( + "context" + "encoding/json" + "errors" + + "kra/internal/biz" + "kra/internal/service/dto" +) + +func dictionaryDomain(value *dto.DictionaryRequest) *biz.Dictionary { + status := true + if value.Status != nil { + status = *value.Status + } + 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 { + children = append(children, dictionaryDTO(x)) + } + details := make([]map[string]any, 0, len(v.Details)) + for _, x := range v.Details { + details = append(details, detailDTO(x)) + } + 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) + if err != nil { + return nil, 0, err + } + out := make([]map[string]any, 0, len(items)) + for _, v := range items { + out = append(out, dictionaryDTO(v)) + } + 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) + 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) +} +func (s *SettingsService) UpdateDictionary(ctx context.Context, v *biz.Dictionary) error { + return s.Repo().UpdateDictionary(ctx, v) +} +func (s *SettingsService) DeleteDictionary(ctx context.Context, id uint) error { + return s.Repo().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) + if err != nil { + return nil, 0, err + } + out := make([]map[string]any, 0, len(items)) + for _, v := range items { + out = append(out, detailDTO(v)) + } + return out, total, nil +} +func (s *SettingsService) DictionaryDetail(ctx context.Context, id uint) (map[string]any, error) { + v, err := s.Repo().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) + if err != nil { + return nil, err + } + out := make([]map[string]any, 0, len(items)) + for _, v := range items { + out = append(out, detailDTO(v)) + } + return out, nil +} +func (s *SettingsService) CreateDictionaryDetail(ctx context.Context, v *biz.DictionaryDetail) error { + return s.Repo().CreateDictionaryDetail(ctx, v) +} +func (s *SettingsService) UpdateDictionaryDetail(ctx context.Context, v *biz.DictionaryDetail) error { + return s.Repo().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 +} diff --git a/internal/service/dto/audit.go b/internal/service/dto/audit.go new file mode 100644 index 0000000..c9d719a --- /dev/null +++ b/internal/service/dto/audit.go @@ -0,0 +1,46 @@ +package dto + +type OperationRecordRequest struct { + IP string + Method string + Path string + Status int + LatencyMS int64 + Agent string + ErrorMessage string + Body string + Response string + UserID uint + RequestID string + TraceID string + DeviceID string +} + +type DataAccessRecordRequest struct { + EventType string + Operation string + UserID uint + AuthorityID uint + RequestID string + Method string + Path string + Detail string +} + +type DataAccessListRequest struct { + Page int `json:"page"` + PageSize int `json:"pageSize"` + EventType string `json:"eventType"` + TargetTable string `json:"targetTable"` + UserID uint `json:"userId"` +} +type ErrorRecordRequest struct { + ID uint `json:"ID"` + Form string `json:"form"` + Info string `json:"info"` + Level string `json:"level"` + RequestID string `json:"request_id"` + TraceID string `json:"trace_id"` + Solution string `json:"solution"` + Status string `json:"status"` +} diff --git a/internal/service/dto/export.go b/internal/service/dto/export.go new file mode 100644 index 0000000..5b28e51 --- /dev/null +++ b/internal/service/dto/export.go @@ -0,0 +1,26 @@ +package dto + +type ExportConditionRequest struct { + From string `json:"from"` + Column string `json:"column"` + Operator string `json:"operator"` +} +type ExportJoinRequest struct { + Join string `json:"joins"` + Table string `json:"table"` + On string `json:"on"` +} +type ExportTemplateRequest struct { + ID uint `json:"ID"` + DBName string `json:"dbName"` + Name string `json:"name"` + TableName string `json:"tableName"` + TemplateID string `json:"templateID"` + TemplateInfo string `json:"templateInfo"` + SQL string `json:"sql"` + ImportSQL string `json:"importSql"` + Limit *int `json:"limit"` + Order string `json:"order"` + Conditions []ExportConditionRequest `json:"conditions"` + Joins []ExportJoinRequest `json:"joinTemplate"` +} diff --git a/internal/service/dto/media.go b/internal/service/dto/media.go new file mode 100644 index 0000000..6ac3d91 --- /dev/null +++ b/internal/service/dto/media.go @@ -0,0 +1,50 @@ +package dto + +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"` +} +type IDRequest struct { + ID uint `json:"ID" form:"id"` +} +type IDsRequest struct { + IDs []uint `json:"ids"` +} +type RenameMediaRequest struct { + ID uint `json:"ID"` + Name string `json:"name"` +} +type ImportMediaRequest struct { + Name string `json:"name"` + ClassID int `json:"classId"` + URL string `json:"url"` + Tag string `json:"tag"` + Key string `json:"key"` + Size int64 `json:"size"` + Mime string `json:"mime"` + MD5 string `json:"md5"` +} +type StorageListRequest struct { + Prefix string `json:"prefix"` + Cursor string `json:"cursor"` + Limit int `json:"limit"` +} +type CategoryRequest struct { + ID uint `json:"ID"` + Name string `json:"name"` + PID uint `json:"pid"` +} +type InitUploadRequest struct { + FileName string `json:"fileName"` + FileHash string `json:"fileHash"` + FileSize int64 `json:"fileSize"` + ChunkSize int64 `json:"chunkSize"` + ChunkTotal int `json:"chunkTotal"` +} +type CompleteUploadRequest struct { + UploadID uint `json:"uploadId"` +} diff --git a/internal/service/dto/settings.go b/internal/service/dto/settings.go new file mode 100644 index 0000000..1870e22 --- /dev/null +++ b/internal/service/dto/settings.go @@ -0,0 +1,44 @@ +package dto + +type DictionaryRequest struct { + ID uint `json:"ID" form:"ID"` + Name string `json:"name" form:"name"` + Type string `json:"type" form:"type"` + Status *bool `json:"status" form:"status"` + Description string `json:"desc" form:"desc"` + ParentID *uint `json:"parentID" form:"parentID"` +} +type DictionaryDetailRequest struct { + ID uint `json:"ID" form:"ID"` + Label string `json:"label" form:"label"` + Value string `json:"value" form:"value"` + Extend string `json:"extend" form:"extend"` + Status *bool `json:"status" form:"status"` + Sort int `json:"sort" form:"sort"` + DictionaryID uint `json:"sysDictionaryID" form:"sysDictionaryID"` + ParentID *uint `json:"parentID" form:"parentID"` + Level int `json:"level"` + Path string `json:"path"` +} +type ImportDictionaryRequest struct { + JSON string `json:"json"` +} +type SystemParameterRequest struct { + ID uint `json:"ID" form:"ID"` + Name string `json:"name" form:"name"` + Key string `json:"key" form:"key"` + Value string `json:"value" form:"value"` + Description string `json:"desc" form:"desc"` +} +type CreateAPITokenRequest struct { + UserID uint `json:"userId"` + AuthorityID uint `json:"authorityId"` + Days int `json:"days"` + Remark string `json:"remark"` +} +type APITokenListRequest struct { + Page int `json:"page"` + PageSize int `json:"pageSize"` + UserID uint `json:"userId"` + Status *bool `json:"status"` +} diff --git a/internal/service/dto/system.go b/internal/service/dto/system.go new file mode 100644 index 0000000..a059903 --- /dev/null +++ b/internal/service/dto/system.go @@ -0,0 +1,72 @@ +package dto + +type LoginRequest struct { + Username string `json:"username"` + Password string `json:"password"` + Captcha string `json:"captcha"` + CaptchaID string `json:"captchaId"` +} + +type Route struct { + Path string + Method string +} + +type UserListRequest struct { + Page int `json:"page"` + PageSize int `json:"pageSize"` + Username string `json:"username"` + NickName string `json:"nickName"` + Phone string `json:"phone"` + Email string `json:"email"` + OrderKey string `json:"orderKey"` + Desc bool `json:"desc"` +} + +type UserRequest struct { + ID uint `json:"ID" form:"id"` + Username string `json:"userName"` + Password string `json:"passWord"` + NickName string `json:"nickName"` + HeaderImg string `json:"headerImg"` + AuthorityID uint `json:"authorityId"` + AuthorityIDs []uint `json:"authorityIds"` + Enable int `json:"enable"` + Phone string `json:"phone"` + Email string `json:"email"` +} + +type SelfUserRequest struct { + NickName string `json:"nickName"` + HeaderImg string `json:"headerImg"` + Phone string `json:"phone"` + Email string `json:"email"` +} + +type ResetPasswordRequest struct { + ID uint `json:"ID"` + Password string `json:"password"` +} + +type ChangePasswordRequest struct { + Password string `json:"password"` + NewPassword string `json:"newPassword"` +} + +type UserAuthoritiesRequest struct { + ID uint `json:"ID"` + AuthorityIDs []uint `json:"authorityIds"` +} + +type SwitchAuthorityRequest struct { + AuthorityID uint `json:"authorityId"` +} + +type LoginLogRequest struct { + Username string + IP string + Status bool + ErrorMessage string + Agent string + UserID uint +} diff --git a/internal/service/dto/system_config.go b/internal/service/dto/system_config.go new file mode 100644 index 0000000..5cdffec --- /dev/null +++ b/internal/service/dto/system_config.go @@ -0,0 +1,56 @@ +package dto + +import "kra/internal/conf" + +type SecurityConfigRequest struct { + ID uint `json:"ID"` + CaptchaOpen int `json:"captchaOpen"` + CaptchaTimeout int `json:"captchaTimeout"` + KeyLong int `json:"keyLong"` + ImgWidth int `json:"imgWidth"` + ImgHeight int `json:"imgHeight"` + PwdMinLength int `json:"pwdMinLength"` + PwdRequireUpper bool `json:"pwdRequireUpper"` + PwdRequireLower bool `json:"pwdRequireLower"` + PwdRequireDigit bool `json:"pwdRequireDigit"` + PwdRequireSpecial bool `json:"pwdRequireSpecial"` + LimitEnable bool `json:"limitEnable"` + LimitWindow int `json:"limitWindow"` + LimitCount int `json:"limitCount"` + LockEnable bool `json:"lockEnable"` + LockThreshold int `json:"lockThreshold"` + LockDuration int `json:"lockDuration"` + PwdExpireEnable bool `json:"pwdExpireEnable"` + PwdExpireDays int `json:"pwdExpireDays"` + ForceNewUserChangePassword bool `json:"forceNewUserChangePassword"` +} + +type SetSystemConfigRequest struct { + Config struct { + Admin struct { + RouterPrefix string `json:"routerPrefix"` + JWT struct { + SigningKey string `json:"signingKey"` + Issuer string `json:"issuer"` + } `json:"jwt"` + Local struct { + StorePath string `json:"storePath"` + PathPrefix string `json:"pathPrefix"` + } `json:"local"` + Media struct { + SessionTTL int32 `json:"sessionTtl"` + } `json:"media"` + Storage *conf.AdminBackend_Storage `json:"storage"` + } `json:"admin"` + Email *struct { + To string `json:"to"` + From string `json:"from"` + Host string `json:"host"` + Secret string `json:"secret"` + Nickname string `json:"nickname"` + Port int32 `json:"port"` + IsSSL bool `json:"is-ssl"` + IsLoginAuth bool `json:"is-loginauth"` + } `json:"email"` + } `json:"config"` +} diff --git a/internal/service/dto/version.go b/internal/service/dto/version.go new file mode 100644 index 0000000..d5b6ffd --- /dev/null +++ b/internal/service/dto/version.go @@ -0,0 +1,67 @@ +package dto + +type VersionMenuMetaRequest struct { + ActiveName string `json:"activeName"` + KeepAlive bool `json:"keepAlive"` + DefaultMenu bool `json:"defaultMenu"` + Title string `json:"title"` + Icon string `json:"icon"` + CloseTab bool `json:"closeTab"` + TransitionType string `json:"transitionType"` +} +type VersionMenuButtonRequest struct { + Name string `json:"name"` + Description string `json:"desc"` +} +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"` +} +type VersionAPIRequest struct { + Path string `json:"path"` + Description string `json:"description"` + APIGroup string `json:"apiGroup"` + Method string `json:"method"` +} +type VersionDictionaryDetailRequest struct { + Label string `json:"label"` + Value string `json:"value"` + Extend string `json:"extend"` + Status *bool `json:"status"` + Sort int `json:"sort"` + ParentID *uint `json:"parentID"` + Level int `json:"level"` + Path string `json:"path"` +} +type VersionDictionaryRequest struct { + Name string `json:"name"` + Type string `json:"type"` + Status bool `json:"status"` + Description string `json:"desc"` + Details []VersionDictionaryDetailRequest `json:"sysDictionaryDetails"` +} +type ImportVersionRequest struct { + Version struct { + Name string `json:"name"` + Code string `json:"code"` + Description string `json:"description"` + ExportTime string `json:"exportTime"` + } `json:"version"` + Menus []VersionMenuRequest `json:"menus"` + APIs []VersionAPIRequest `json:"apis"` + Dictionaries []VersionDictionaryRequest `json:"dictionaries"` +} +type ExportVersionRequest struct { + VersionName string `json:"versionName"` + VersionCode string `json:"versionCode"` + Description string `json:"description"` + MenuIDs []uint `json:"menuIds"` + APIIDs []uint `json:"apiIds"` + DictIDs []uint `json:"dictIds"` +} diff --git a/internal/service/export.go b/internal/service/export.go index 2c13e03..11ec55f 100644 --- a/internal/service/export.go +++ b/internal/service/export.go @@ -8,12 +8,32 @@ import ( "github.com/xuri/excelize/v2" "io" "kra/internal/biz" + "kra/internal/service/dto" "sort" ) type ExportService struct{ uc *biz.ExportUsecase } func NewExportService(uc *biz.ExportUsecase) *ExportService { return &ExportService{uc: uc} } +func exportTemplateDomain(value *dto.ExportTemplateRequest) *biz.ExportTemplate { + out := &biz.ExportTemplate{ID: value.ID, DBName: value.DBName, Name: value.Name, TableName: value.TableName, TemplateID: value.TemplateID, TemplateInfo: value.TemplateInfo, SQL: value.SQL, ImportSQL: value.ImportSQL, Limit: value.Limit, Order: value.Order} + for _, item := range value.Conditions { + out.Conditions = append(out.Conditions, biz.ExportCondition{From: item.From, Column: item.Column, Operator: item.Operator}) + } + for _, item := range value.Joins { + out.Joins = append(out.Joins, biz.ExportJoin{Join: item.Join, Table: item.Table, On: item.On}) + } + return out +} +func (s *ExportService) CreateRequest(ctx context.Context, req *dto.ExportTemplateRequest) error { + return s.Create(ctx, exportTemplateDomain(req)) +} +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 exportDTO(v *biz.ExportTemplate) map[string]any { conditions := make([]map[string]any, 0, len(v.Conditions)) for _, x := range v.Conditions { diff --git a/internal/service/media.go b/internal/service/media.go index 1f0138e..8bc8186 100644 --- a/internal/service/media.go +++ b/internal/service/media.go @@ -3,7 +3,9 @@ package service import ( "context" "io" + "kra/internal/biz" + "kra/internal/service/dto" ) type MediaService struct{ uc *biz.MediaUsecase } @@ -51,6 +53,13 @@ func (s *MediaService) Rename(ctx context.Context, id uint, name string) error { func (s *MediaService) ImportURLs(ctx context.Context, items []*biz.MediaFile) error { return s.uc.Repo().CreateMediaBatch(ctx, items) } +func (s *MediaService) ImportURLRequests(ctx context.Context, userID uint, 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}) + } + return s.ImportURLs(ctx, items) +} func (s *MediaService) Storage(ctx context.Context, prefix, cursor string, limit int) ([]map[string]any, string, bool, error) { items, next, more, err := s.uc.ListStorage(ctx, prefix, cursor, limit) if err != nil { @@ -76,29 +85,9 @@ func (s *MediaService) Categories(ctx context.Context) ([]map[string]any, error) func (s *MediaService) SaveCategory(ctx context.Context, v *biz.AttachmentCategory) error { return s.uc.Repo().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) } -func (s *MediaService) InitUpload(ctx context.Context, userID uint, name, hash string, size, chunkSize int64, total int) (map[string]any, error) { - session, media, chunks, err := s.uc.InitUpload(ctx, userID, name, hash, size, chunkSize, total) - if err != nil { - return nil, err - } - if media != nil { - return map[string]any{"instant": true, "media": mediaDTO(media), "uploadedChunks": []int{}}, nil - } - return map[string]any{"instant": false, "uploadId": session.ID, "uploadedChunks": chunks, "media": nil}, nil -} -func (s *MediaService) SaveChunk(ctx context.Context, userID, uploadID uint, index int, hash string, reader io.Reader) error { - return s.uc.SaveChunk(ctx, userID, uploadID, index, hash, reader) -} -func (s *MediaService) CompleteUpload(ctx context.Context, userID, uploadID uint) (map[string]any, error) { - v, err := s.uc.CompleteUpload(ctx, userID, uploadID, "") - if err != nil { - return nil, err - } - return mediaDTO(v), nil -} -func (s *MediaService) CancelUpload(ctx context.Context, userID, uploadID uint) error { - return s.uc.CancelUpload(ctx, userID, uploadID) -} diff --git a/internal/service/media_upload.go b/internal/service/media_upload.go new file mode 100644 index 0000000..e42bfa0 --- /dev/null +++ b/internal/service/media_upload.go @@ -0,0 +1,30 @@ +package service + +import ( + "context" + "io" +) + +func (s *MediaService) InitUpload(ctx context.Context, userID uint, name, hash string, size, chunkSize int64, total int) (map[string]any, error) { + session, media, chunks, err := s.uc.InitUpload(ctx, userID, name, hash, size, chunkSize, total) + if err != nil { + return nil, err + } + if media != nil { + return map[string]any{"instant": true, "media": mediaDTO(media), "uploadedChunks": []int{}}, nil + } + return map[string]any{"instant": false, "uploadId": session.ID, "uploadedChunks": chunks, "media": nil}, nil +} +func (s *MediaService) SaveChunk(ctx context.Context, userID, uploadID uint, index int, hash string, reader io.Reader) error { + return s.uc.SaveChunk(ctx, userID, uploadID, index, hash, reader) +} +func (s *MediaService) CompleteUpload(ctx context.Context, userID, uploadID uint) (map[string]any, error) { + v, err := s.uc.CompleteUpload(ctx, userID, uploadID, "") + if err != nil { + return nil, err + } + return mediaDTO(v), nil +} +func (s *MediaService) CancelUpload(ctx context.Context, userID, uploadID uint) error { + return s.uc.CancelUpload(ctx, userID, uploadID) +} diff --git a/internal/service/parameter.go b/internal/service/parameter.go new file mode 100644 index 0000000..b25f094 --- /dev/null +++ b/internal/service/parameter.go @@ -0,0 +1,51 @@ +package service + +import ( + "context" + + "kra/internal/biz" + "kra/internal/service/dto" +) + +func parameterDomain(value *dto.SystemParameterRequest) *biz.SystemParameter { + return &biz.SystemParameter{ID: value.ID, Name: value.Name, Key: value.Key, Value: value.Value, Desc: value.Description} +} +func (s *SettingsService) CreateParameterRequest(ctx context.Context, req *dto.SystemParameterRequest) error { + return s.CreateParameter(ctx, parameterDomain(req)) +} +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 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) + if err != nil { + return nil, 0, err + } + out := make([]map[string]any, 0, len(items)) + for _, v := range items { + out = append(out, parameterDTO(v)) + } + 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) + 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) +} +func (s *SettingsService) UpdateParameter(ctx context.Context, v *biz.SystemParameter) error { + return s.Repo().UpdateParameter(ctx, v) +} +func (s *SettingsService) DeleteParameters(ctx context.Context, ids []uint) error { + return s.Repo().DeleteParameters(ctx, ids) +} diff --git a/internal/service/security.go b/internal/service/security.go new file mode 100644 index 0000000..7986124 --- /dev/null +++ b/internal/service/security.go @@ -0,0 +1,98 @@ +package service + +import ( + "context" + "errors" + + "kra/internal/biz" + "kra/internal/service/dto" +) + +func securityDTO(v *biz.SecurityConfig) map[string]any { + return map[string]any{"ID": v.ID, "captchaOpen": v.CaptchaOpen, "captchaTimeout": v.CaptchaTimeout, "keyLong": v.KeyLong, "imgWidth": v.ImgWidth, "imgHeight": v.ImgHeight, "pwdMinLength": v.PwdMinLength, "pwdRequireUpper": v.PwdRequireUpper, "pwdRequireLower": v.PwdRequireLower, "pwdRequireDigit": v.PwdRequireDigit, "pwdRequireSpecial": v.PwdRequireSpecial, "limitEnable": v.LimitEnable, "limitWindow": v.LimitWindow, "limitCount": v.LimitCount, "lockEnable": v.LockEnable, "lockThreshold": v.LockThreshold, "lockDuration": v.LockDuration, "pwdExpireEnable": v.PwdExpireEnable, "pwdExpireDays": v.PwdExpireDays, "forceNewUserChangePassword": v.ForceNewUserChangePassword} +} +func (s *SettingsService) CurrentSecurity(ctx context.Context) (*biz.SecurityConfig, error) { + s.securityMu.RLock() + if s.securityCache != nil { + copy := *s.securityCache + s.securityMu.RUnlock() + return ©, nil + } + s.securityMu.RUnlock() + value, err := s.Repo().SecurityConfig(ctx) + if err != nil { + return nil, err + } + s.securityMu.Lock() + s.securityCache = value + copy := *value + s.securityMu.Unlock() + return ©, nil +} +func (s *SettingsService) Security(ctx context.Context) (map[string]any, error) { + value, err := s.CurrentSecurity(ctx) + if err != nil { + return nil, err + } + 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 { + return nil, err + } + s.securityMu.Lock() + s.securityCache = value + s.securityMu.Unlock() + return securityDTO(value), nil +} +func (s *SettingsService) SaveSecurityRequest(ctx context.Context, value *dto.SecurityConfigRequest) (map[string]any, error) { + return s.SaveSecurity(ctx, &biz.SecurityConfig{ + ID: value.ID, CaptchaOpen: value.CaptchaOpen, CaptchaTimeout: value.CaptchaTimeout, + KeyLong: value.KeyLong, ImgWidth: value.ImgWidth, ImgHeight: value.ImgHeight, + PwdMinLength: value.PwdMinLength, PwdRequireUpper: value.PwdRequireUpper, + PwdRequireLower: value.PwdRequireLower, PwdRequireDigit: value.PwdRequireDigit, + PwdRequireSpecial: value.PwdRequireSpecial, LimitEnable: value.LimitEnable, + LimitWindow: value.LimitWindow, LimitCount: value.LimitCount, LockEnable: value.LockEnable, + LockThreshold: value.LockThreshold, LockDuration: value.LockDuration, + PwdExpireEnable: value.PwdExpireEnable, PwdExpireDays: value.PwdExpireDays, + ForceNewUserChangePassword: value.ForceNewUserChangePassword, + }) +} +func (s *SettingsService) ValidatePassword(ctx context.Context, password string) error { + cfg, err := s.CurrentSecurity(ctx) + 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 +} diff --git a/internal/service/settings.go b/internal/service/settings.go index 15ae104..baefc3e 100644 --- a/internal/service/settings.go +++ b/internal/service/settings.go @@ -1,14 +1,10 @@ package service import ( - "context" - "errors" "sync" - "time" "kra/internal/biz" "kra/internal/conf" - "kra/pkg/adminauth" ) type SettingsService struct { @@ -22,263 +18,3 @@ func NewSettingsService(uc *biz.SettingsUsecase, config *conf.AdminBackend) *Set return &SettingsService{uc: uc, config: config} } func (s *SettingsService) Repo() biz.SettingsRepo { return s.uc.Repo() } - -func dictionaryDTO(v *biz.Dictionary) map[string]any { - children := make([]map[string]any, 0, len(v.Children)) - for _, x := range v.Children { - children = append(children, dictionaryDTO(x)) - } - details := make([]map[string]any, 0, len(v.Details)) - for _, x := range v.Details { - details = append(details, detailDTO(x)) - } - 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 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 tokenDTO(v *biz.APIToken) map[string]any { - var user any = nil - if v.User != nil { - user = convertUser(v.User) - } - return map[string]any{"ID": v.ID, "CreatedAt": v.CreatedAt, "UpdatedAt": v.UpdatedAt, "DeletedAt": nil, "userId": v.UserID, "authorityId": v.AuthorityID, "token": v.Token, "status": v.Status, "expiresAt": v.ExpiresAt, "remark": v.Remark, "user": user} -} - -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) - if err != nil { - return nil, 0, err - } - out := make([]map[string]any, 0, len(items)) - for _, v := range items { - out = append(out, dictionaryDTO(v)) - } - 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) - 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) -} -func (s *SettingsService) UpdateDictionary(ctx context.Context, v *biz.Dictionary) error { - return s.Repo().UpdateDictionary(ctx, v) -} -func (s *SettingsService) DeleteDictionary(ctx context.Context, id uint) error { - return s.Repo().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) - if err != nil { - return nil, 0, err - } - out := make([]map[string]any, 0, len(items)) - for _, v := range items { - out = append(out, detailDTO(v)) - } - return out, total, nil -} -func (s *SettingsService) DictionaryDetail(ctx context.Context, id uint) (map[string]any, error) { - v, err := s.Repo().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) - if err != nil { - return nil, err - } - out := make([]map[string]any, 0, len(items)) - for _, v := range items { - out = append(out, detailDTO(v)) - } - return out, nil -} -func (s *SettingsService) CreateDictionaryDetail(ctx context.Context, v *biz.DictionaryDetail) error { - return s.Repo().CreateDictionaryDetail(ctx, v) -} -func (s *SettingsService) UpdateDictionaryDetail(ctx context.Context, v *biz.DictionaryDetail) error { - return s.Repo().UpdateDictionaryDetail(ctx, v) -} -func (s *SettingsService) DeleteDictionaryDetail(ctx context.Context, id uint) error { - return s.Repo().DeleteDictionaryDetail(ctx, id) -} - -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) - if err != nil { - return nil, 0, err - } - out := make([]map[string]any, 0, len(items)) - for _, v := range items { - out = append(out, parameterDTO(v)) - } - 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) - 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) -} -func (s *SettingsService) UpdateParameter(ctx context.Context, v *biz.SystemParameter) error { - return s.Repo().UpdateParameter(ctx, v) -} -func (s *SettingsService) DeleteParameters(ctx context.Context, ids []uint) error { - return s.Repo().DeleteParameters(ctx, ids) -} - -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) - 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 - } - if s.config.Jwt.BufferTime != nil { - buffer = s.config.Jwt.BufferTime.AsDuration() - } - } - token, _, err := adminauth.Generate(secret, issuer, duration, buffer, user.ID, authorityID, user.UUID, user.Username, user.NickName, false) - if err != nil { - 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 { - 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) - if err != nil { - return nil, 0, err - } - out := make([]map[string]any, 0, len(items)) - for _, v := range items { - out = append(out, tokenDTO(v)) - } - 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) -} -func (s *SettingsService) BlacklistToken(ctx context.Context, token string) error { - return s.Repo().BlacklistToken(ctx, token) -} -func (s *SettingsService) IsTokenDisabled(ctx context.Context, token string) (bool, error) { - return s.Repo().IsTokenDisabled(ctx, token) -} - -func securityDTO(v *biz.SecurityConfig) map[string]any { - return map[string]any{"ID": v.ID, "captchaOpen": v.CaptchaOpen, "captchaTimeout": v.CaptchaTimeout, "keyLong": v.KeyLong, "imgWidth": v.ImgWidth, "imgHeight": v.ImgHeight, "pwdMinLength": v.PwdMinLength, "pwdRequireUpper": v.PwdRequireUpper, "pwdRequireLower": v.PwdRequireLower, "pwdRequireDigit": v.PwdRequireDigit, "pwdRequireSpecial": v.PwdRequireSpecial, "limitEnable": v.LimitEnable, "limitWindow": v.LimitWindow, "limitCount": v.LimitCount, "lockEnable": v.LockEnable, "lockThreshold": v.LockThreshold, "lockDuration": v.LockDuration, "pwdExpireEnable": v.PwdExpireEnable, "pwdExpireDays": v.PwdExpireDays, "forceNewUserChangePassword": v.ForceNewUserChangePassword} -} -func (s *SettingsService) CurrentSecurity(ctx context.Context) (*biz.SecurityConfig, error) { - s.securityMu.RLock() - if s.securityCache != nil { - copy := *s.securityCache - s.securityMu.RUnlock() - return ©, nil - } - s.securityMu.RUnlock() - value, err := s.Repo().SecurityConfig(ctx) - if err != nil { - return nil, err - } - s.securityMu.Lock() - s.securityCache = value - copy := *value - s.securityMu.Unlock() - return ©, nil -} -func (s *SettingsService) Security(ctx context.Context) (map[string]any, error) { - value, err := s.CurrentSecurity(ctx) - if err != nil { - return nil, err - } - 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 { - return nil, err - } - s.securityMu.Lock() - s.securityCache = value - s.securityMu.Unlock() - return securityDTO(value), nil -} -func (s *SettingsService) ValidatePassword(ctx context.Context, password string) error { - cfg, err := s.CurrentSecurity(ctx) - 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 -} diff --git a/internal/service/system.go b/internal/service/system.go index e6fa628..5094931 100644 --- a/internal/service/system.go +++ b/internal/service/system.go @@ -2,12 +2,9 @@ package service import ( "context" - "time" - "google.golang.org/protobuf/encoding/protojson" "kra/internal/biz" "kra/internal/conf" - "kra/pkg/adminauth" ) type SystemService struct { @@ -16,13 +13,6 @@ type SystemService struct { settings *SettingsService } -type LoginResult struct { - User map[string]any `json:"user"` - Token string `json:"token"` - ExpiresAt int64 `json:"expiresAt"` - NeedChangePassword bool `json:"needChangePassword"` -} - func NewSystemService(uc *biz.SystemUsecase, config *conf.AdminBackend, settings *SettingsService) *SystemService { return &SystemService{uc: uc, config: config, settings: settings} } @@ -30,264 +20,3 @@ func NewSystemService(uc *biz.SystemUsecase, config *conf.AdminBackend, settings func (s *SystemService) IsInitialized(ctx context.Context) (bool, error) { return s.uc.IsInitialized(ctx) } -func (s *SystemService) PersistConfig(ctx context.Context) error { return s.uc.PersistConfig(ctx) } -func (s *SystemService) PersistAdminConfig(ctx context.Context, value *conf.AdminBackend) error { - raw, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(value) - if err != nil { - return err - } - return s.uc.PersistAdminConfig(ctx, raw) -} -func (s *SystemService) ReloadConfig(ctx context.Context) error { return s.uc.ReloadConfig(ctx) } - -type DatabaseInit struct { - DBType string `json:"dbType"` - Host string `json:"host"` - Port string `json:"port"` - UserName string `json:"userName"` - Password string `json:"password"` - DBName string `json:"dbName"` - DBPath string `json:"dbPath"` - Template string `json:"template"` - AdminPassword string `json:"adminPassword"` -} - -func (s *SystemService) Initialize(ctx context.Context, input *DatabaseInit, apis []*biz.API) error { - config := "" - switch input.DBType { - case "mysql": - config = "timeout=5s&parseTime=True&loc=Local&charset=utf8mb4" - case "pgsql": - config = "sslmode=disable TimeZone=Local" - } - return s.uc.Initialize(ctx, &biz.DatabaseConfig{Driver: input.DBType, Host: input.Host, Port: input.Port, User: input.UserName, Password: input.Password, Name: input.DBName, Path: input.DBPath, Config: config, Template: input.Template, AdminPassword: input.AdminPassword, APIs: apis}) -} - -func (s *SystemService) CacheGet(ctx context.Context, key string) (string, bool, error) { - return s.uc.CacheGet(ctx, key) -} -func (s *SystemService) CacheSet(ctx context.Context, key, value string, expiration time.Duration) error { - return s.uc.CacheSet(ctx, key, value, expiration) -} -func (s *SystemService) CacheDelete(ctx context.Context, key string) error { - return s.uc.CacheDelete(ctx, key) -} -func (s *SystemService) CacheIncrement(ctx context.Context, key string, expiration time.Duration) (int64, error) { - return s.uc.CacheIncrement(ctx, key, expiration) -} - -func (s *SystemService) Login(ctx context.Context, username, password string) (*LoginResult, error) { - u, err := s.uc.Login(ctx, username, password) - if err != nil { - return nil, err - } - if u.Enable != 1 { - return nil, biz.ErrInvalidCredentials - } - 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 - } - 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() - } - if s.config.Jwt.BufferTime != nil { - buffer = s.config.Jwt.BufferTime.AsDuration() - } - } - token, claims, err := adminauth.Generate(secret, issuer, expires, buffer, u.ID, u.AuthorityID, u.UUID, u.Username, u.NickName, u.MustChangePassword) - if err != nil { - return nil, err - } - return &LoginResult{User: convertUser(u), Token: token, ExpiresAt: claims.ExpiresAt.UnixMilli(), NeedChangePassword: u.MustChangePassword}, nil -} - -func (s *SystemService) User(ctx context.Context, id uint) (map[string]any, error) { - u, err := s.uc.User(ctx, id) - if err != nil { - return nil, err - } - return convertUser(u), 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 - AuthorityID uint - Enable int - AuthorityIDs []uint -} - -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 { - return nil, 0, err - } - result := make([]map[string]any, 0, len(users)) - for _, u := range users { - result = append(result, convertUser(u)) - } - return result, total, nil -} -func (s *SystemService) CreateUser(ctx context.Context, in UserInput) (map[string]any, error) { - if err := s.settings.ValidatePassword(ctx, in.Password); err != nil { - return nil, err - } - security, _ := s.settings.CurrentSecurity(ctx) - mustChange := security != nil && security.ForceNewUserChangePassword - if in.AuthorityID == 0 && len(in.AuthorityIDs) > 0 { - in.AuthorityID = in.AuthorityIDs[0] - } - in.AuthorityIDs = includeAuthority(in.AuthorityIDs, in.AuthorityID) - u, err := s.uc.CreateUser(ctx, &biz.User{Username: in.Username, Password: in.Password, NickName: in.NickName, HeaderImg: in.HeaderImg, AuthorityID: in.AuthorityID, Phone: in.Phone, Email: in.Email, Enable: in.Enable, MustChangePassword: mustChange}) - if err != nil { - return nil, err - } - if len(in.AuthorityIDs) > 0 { - if err = s.uc.SetUserAuthorities(ctx, u.ID, in.AuthorityIDs); err != nil { - return nil, err - } - u, err = s.uc.User(ctx, u.ID) - if err != nil { - return nil, err - } - } - return convertUser(u), nil -} -func (s *SystemService) UpdateUser(ctx context.Context, in UserInput) error { - in.AuthorityIDs = includeAuthority(in.AuthorityIDs, in.AuthorityID) - if err := s.uc.UpdateUser(ctx, &biz.User{ID: in.ID, NickName: in.NickName, HeaderImg: in.HeaderImg, AuthorityID: in.AuthorityID, Phone: in.Phone, Email: in.Email, Enable: in.Enable}); err != nil { - return err - } - if len(in.AuthorityIDs) > 0 { - return s.uc.SetUserAuthorities(ctx, in.ID, in.AuthorityIDs) - } - return nil -} -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) DeleteUser(ctx context.Context, id uint) error { - return s.uc.DeleteUser(ctx, id) -} -func (s *SystemService) ResetPassword(ctx context.Context, id uint, password string) error { - if err := s.settings.ValidatePassword(ctx, password); err != nil { - return err - } - return s.uc.ResetPassword(ctx, id, password) -} -func (s *SystemService) ChangePassword(ctx context.Context, id uint, oldPassword, newPassword string) error { - if err := s.settings.ValidatePassword(ctx, newPassword); err != nil { - return err - } - return s.uc.ChangePassword(ctx, id, oldPassword, newPassword) -} -func (s *SystemService) Authorities(ctx context.Context) ([]map[string]any, error) { - authorities, err := s.uc.Authorities(ctx) - if err != nil { - return nil, err - } - result := make([]map[string]any, 0, len(authorities)) - for _, a := range authorities { - result = append(result, convertAuthority(*a)) - } - return result, nil -} -func (s *SystemService) SetUserAuthorities(ctx context.Context, id uint, authorityIDs []uint) error { - return s.uc.SetUserAuthorities(ctx, id, authorityIDs) -} -func (s *SystemService) SwitchAuthority(ctx context.Context, id, authorityID uint) (*LoginResult, error) { - if err := s.uc.SetUserAuthority(ctx, id, authorityID); err != nil { - return nil, err - } - u, err := s.uc.User(ctx, id) - if err != nil { - return nil, err - } - 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() - } - if s.config.Jwt.BufferTime != nil { - buffer = s.config.Jwt.BufferTime.AsDuration() - } - } - token, claims, err := adminauth.Generate(secret, issuer, expires, buffer, u.ID, authorityID, u.UUID, u.Username, u.NickName, u.MustChangePassword) - if err != nil { - return nil, err - } - return &LoginResult{User: convertUser(u), Token: token, ExpiresAt: claims.ExpiresAt.UnixMilli(), NeedChangePassword: u.MustChangePassword}, nil -} -func (s *SystemService) SetUserSetting(ctx context.Context, id uint, setting map[string]any) error { - return s.uc.SetUserSetting(ctx, id, setting) -} - -func convertAuthority(a biz.Authority) map[string]any { - return map[string]any{"authorityId": a.AuthorityID, "authorityName": a.AuthorityName, "parentId": a.ParentID, "children": []map[string]any{}, "menus": []any{}, "dataScope": a.DataScope, "defaultRouter": a.DefaultRouter} -} - -func convertUser(u *biz.User) map[string]any { - authorities := make([]map[string]any, 0, len(u.Authorities)) - for _, a := range u.Authorities { - authorities = append(authorities, convertAuthority(a)) - } - departments := make([]map[string]any, 0, len(u.Departments)) - for i := range u.Departments { - departments = append(departments, departmentDTO(&u.Departments[i])) - } - positions := make([]map[string]any, 0, len(u.Positions)) - for i := range u.Positions { - positions = append(positions, positionDTO(&u.Positions[i])) - } - dept := map[string]any{} - if u.Department != nil { - dept = departmentDTO(u.Department) - } - return map[string]any{"ID": u.ID, "CreatedAt": u.CreatedAt, "UpdatedAt": u.UpdatedAt, "DeletedAt": nil, "uuid": u.UUID, "userName": u.Username, "nickName": u.NickName, "headerImg": u.HeaderImg, "authorityId": u.AuthorityID, "authority": convertAuthority(u.Authority), "authorities": authorities, "deptId": u.DeptID, "dept": dept, "departments": departments, "positions": positions, "phone": u.Phone, "email": u.Email, "enable": u.Enable, "originSetting": u.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/system_config.go b/internal/service/system_config.go new file mode 100644 index 0000000..eae0cb9 --- /dev/null +++ b/internal/service/system_config.go @@ -0,0 +1,126 @@ +package service + +import ( + "context" + + "kra/internal/conf" + "kra/internal/service/dto" + + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" +) + +func (s *SystemService) PersistConfig(ctx context.Context) error { return s.uc.PersistConfig(ctx) } + +func (s *SystemService) PersistAdminConfig(ctx context.Context, value *conf.AdminBackend) error { + raw, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(value) + if err != nil { + return err + } + return s.uc.PersistAdminConfig(ctx, raw) +} + +func (s *SystemService) ReloadConfig(ctx context.Context) error { return s.uc.ReloadConfig(ctx) } + +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}} + } + 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} + } + 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 s.config.Local != nil { + admin["local"] = map[string]any{"storePath": s.config.Local.StorePath, "pathPrefix": s.config.Local.PathPrefix} + } + if s.config.Media != nil { + admin["media"] = map[string]any{"sessionTtl": s.config.Media.SessionTtl} + } + 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 s.config.Storage != nil { + storage := proto.Clone(s.config.Storage).(*conf.AdminBackend_Storage) + maskStorageSecrets(storage) + admin["storage"] = storage + } + return map[string]any{"config": map[string]any{"admin": admin, "email": email}} +} + +func (s *SystemService) SaveSystemConfig(ctx context.Context, req *dto.SetSystemConfigRequest) error { + if s.config == nil { + return nil + } + next := proto.Clone(s.config).(*conf.AdminBackend) + next.RouterPrefix = req.Config.Admin.RouterPrefix + if next.Jwt != nil { + if req.Config.Admin.JWT.SigningKey != "" && req.Config.Admin.JWT.SigningKey != "******" { + next.Jwt.SigningKey = req.Config.Admin.JWT.SigningKey + } + if req.Config.Admin.JWT.Issuer != "" { + next.Jwt.Issuer = req.Config.Admin.JWT.Issuer + } + } + if next.Local != nil { + if req.Config.Admin.Local.StorePath != "" { + next.Local.StorePath = req.Config.Admin.Local.StorePath + } + if req.Config.Admin.Local.PathPrefix != "" { + next.Local.PathPrefix = req.Config.Admin.Local.PathPrefix + } + } + if next.Media != nil && req.Config.Admin.Media.SessionTTL > 0 { + next.Media.SessionTtl = req.Config.Admin.Media.SessionTTL + } + if req.Config.Admin.Storage != nil { + preserveStorageSecrets(req.Config.Admin.Storage, next.Storage) + next.Storage = req.Config.Admin.Storage + } + if next.Email != nil && req.Config.Email != nil { + next.Email.To, next.Email.From, next.Email.Host = req.Config.Email.To, req.Config.Email.From, req.Config.Email.Host + next.Email.Nickname, next.Email.Port = req.Config.Email.Nickname, req.Config.Email.Port + next.Email.IsSsl, next.Email.IsLoginAuth = req.Config.Email.IsSSL, req.Config.Email.IsLoginAuth + if req.Config.Email.Secret != "" && req.Config.Email.Secret != "******" { + next.Email.Secret = req.Config.Email.Secret + } + } + return s.PersistAdminConfig(ctx, next) +} + +func objectStores(storage *conf.AdminBackend_Storage) []*conf.AdminBackend_ObjectStore { + if storage == nil { + return nil + } + return []*conf.AdminBackend_ObjectStore{storage.AliyunOss, storage.HuaweiObs, storage.TencentCos, storage.AwsS3, storage.CloudflareR2, storage.Minio} +} + +func maskStorageSecrets(storage *conf.AdminBackend_Storage) { + if storage.Qiniu != nil { + storage.Qiniu.SecretKey = "******" + } + for _, item := range objectStores(storage) { + if item != nil { + item.SecretKey = "******" + } + } +} + +func preserveStorageSecrets(next, current *conf.AdminBackend_Storage) { + if next == nil || current == nil { + return + } + if next.Qiniu != nil && current.Qiniu != nil && (next.Qiniu.SecretKey == "" || next.Qiniu.SecretKey == "******") { + next.Qiniu.SecretKey = current.Qiniu.SecretKey + } + nextItems, currentItems := objectStores(next), objectStores(current) + for i := range nextItems { + if nextItems[i] != nil && currentItems[i] != nil && (nextItems[i].SecretKey == "" || nextItems[i].SecretKey == "******") { + nextItems[i].SecretKey = currentItems[i].SecretKey + } + } +} diff --git a/internal/service/system_init.go b/internal/service/system_init.go new file mode 100644 index 0000000..b65d256 --- /dev/null +++ b/internal/service/system_init.go @@ -0,0 +1,48 @@ +package service + +import ( + "context" + "strings" + + "kra/internal/biz" + "kra/internal/service/dto" +) + +type DatabaseInit struct { + DBType string `json:"dbType"` + Host string `json:"host"` + Port string `json:"port"` + UserName string `json:"userName"` + Password string `json:"password"` + DBName string `json:"dbName"` + DBPath string `json:"dbPath"` + Template string `json:"template"` + AdminPassword string `json:"adminPassword"` +} + +func (s *SystemService) Initialize(ctx context.Context, input *DatabaseInit, apis []*biz.API) error { + config := "" + switch input.DBType { + case "mysql": + config = "timeout=5s&parseTime=True&loc=Local&charset=utf8mb4" + case "pgsql": + config = "sslmode=disable TimeZone=Local" + } + return s.uc.Initialize(ctx, &biz.DatabaseConfig{Driver: input.DBType, Host: input.Host, Port: input.Port, User: input.UserName, Password: input.Password, Name: input.DBName, Path: input.DBPath, Config: config, Template: input.Template, AdminPassword: input.AdminPassword, APIs: apis}) +} + +func (s *SystemService) InitializeRoutes(ctx context.Context, input *DatabaseInit, routes []dto.Route) error { + apis := make([]*biz.API, 0, len(routes)) + for _, route := range routes { + apis = append(apis, &biz.API{Path: route.Path, Method: route.Method, APIGroup: routeGroup(route.Path)}) + } + return s.Initialize(ctx, input, apis) +} + +func routeGroup(path string) string { + parts := strings.Split(strings.Trim(path, "/"), "/") + if len(parts) > 0 && parts[0] != "" { + return parts[0] + } + return "base" +} diff --git a/internal/service/user.go b/internal/service/user.go new file mode 100644 index 0000000..32632ff --- /dev/null +++ b/internal/service/user.go @@ -0,0 +1,191 @@ +package service + +import ( + "context" + + "kra/internal/biz" + "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 + AuthorityID uint + Enable int + AuthorityIDs []uint +} + +func userInput(value *dto.UserRequest) UserInput { + return UserInput{ID: value.ID, Username: value.Username, Password: value.Password, NickName: value.NickName, HeaderImg: value.HeaderImg, AuthorityID: value.AuthorityID, AuthorityIDs: value.AuthorityIDs, Enable: value.Enable, Phone: value.Phone, Email: value.Email} +} +func (s *SystemService) ListUsersRequest(ctx context.Context, value *dto.UserListRequest) ([]map[string]any, int64, error) { + return s.ListUsers(ctx, value.Page, value.PageSize, &biz.UserListFilter{Username: value.Username, NickName: value.NickName, Phone: value.Phone, Email: value.Email, OrderKey: value.OrderKey, Desc: value.Desc}) +} +func (s *SystemService) CreateUserRequest(ctx context.Context, value *dto.UserRequest) (map[string]any, error) { + return s.CreateUser(ctx, userInput(value)) +} +func (s *SystemService) UpdateUserRequest(ctx context.Context, value *dto.UserRequest) error { + return s.UpdateUser(ctx, userInput(value)) +} + +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 { + return nil, 0, err + } + result := make([]map[string]any, 0, len(users)) + for _, user := range users { + result = append(result, convertUser(user)) + } + return result, total, 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}) + 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 +} +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) DeleteUser(ctx context.Context, id uint) error { + return s.uc.DeleteUser(ctx, id) +} +func (s *SystemService) ResetPassword(ctx context.Context, id uint, password string) error { + if err := s.settings.ValidatePassword(ctx, password); err != nil { + return err + } + return s.uc.ResetPassword(ctx, id, password) +} +func (s *SystemService) ChangePassword(ctx context.Context, id uint, oldPassword, newPassword string) error { + if err := s.settings.ValidatePassword(ctx, newPassword); err != nil { + return err + } + 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) +} +func (s *SystemService) SwitchAuthority(ctx context.Context, id, authorityID uint) (*LoginResult, error) { + if err := s.uc.SetUserAuthority(ctx, id, authorityID); err != nil { + return nil, err + } + user, err := s.uc.User(ctx, id) + if err != nil { + return nil, err + } + return s.issueLogin(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/version.go b/internal/service/version.go index e90850e..2af80fc 100644 --- a/internal/service/version.go +++ b/internal/service/version.go @@ -6,6 +6,7 @@ import ( "time" "kra/internal/biz" + "kra/internal/service/dto" ) type VersionService struct{ uc *biz.VersionUsecase } @@ -73,3 +74,34 @@ func (s *VersionService) Export(ctx context.Context, name, code, description str func (s *VersionService) Import(ctx context.Context, bundle *biz.VersionBundle) error { return s.uc.Repo().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 _, child := range value.Children { + menu.Children = append(menu.Children, versionMenuDomain(child)) + } + return menu +} +func (s *VersionService) ImportRequest(ctx context.Context, req *dto.ImportVersionRequest) error { + 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)) + } + for _, api := range req.APIs { + bundle.APIs = append(bundle.APIs, &biz.API{Path: api.Path, Description: api.Description, APIGroup: api.APIGroup, Method: api.Method}) + } + for _, dictionary := range req.Dictionaries { + item := &biz.Dictionary{Name: dictionary.Name, Type: dictionary.Type, Status: dictionary.Status, Desc: dictionary.Description} + for _, detail := range dictionary.Details { + status := true + if detail.Status != nil { + status = *detail.Status + } + item.Details = append(item.Details, &biz.DictionaryDetail{Label: detail.Label, Value: detail.Value, Extend: detail.Extend, Status: status, Sort: detail.Sort, ParentID: detail.ParentID, Level: detail.Level, Path: detail.Path}) + } + bundle.Dictionaries = append(bundle.Dictionaries, item) + } + return s.Import(ctx, bundle) +}