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) +}