优化
This commit is contained in:
+2
-2
@@ -84,7 +84,7 @@ data:
|
||||
- host: ""
|
||||
port: ""
|
||||
admin:
|
||||
# GVA system.router-prefix maps to this transport prefix.
|
||||
# The administration system router prefix maps to this transport prefix.
|
||||
router_prefix: ""
|
||||
jwt:
|
||||
# Production deployments must override this value with a private secret.
|
||||
@@ -104,7 +104,7 @@ admin:
|
||||
# Upload chunks are stored below .chunks in the selected storage backend.
|
||||
session_ttl: 24
|
||||
max_file_size: 0
|
||||
chunk_dir: .chunks
|
||||
chunk_dir: uploads/chunks
|
||||
system:
|
||||
use_redis: false
|
||||
use_multipoint: false
|
||||
|
||||
@@ -12,9 +12,6 @@ func NewAccessControlUsecase(authorities AuthorityAccessRepo, apis APIRepo) *Acc
|
||||
}
|
||||
|
||||
func (uc *AccessControlUsecase) Authorize(ctx context.Context, authorityID uint, path, method string) (bool, error) {
|
||||
if authorityID == 888 {
|
||||
return true, nil
|
||||
}
|
||||
return uc.apis.Authorize(ctx, authorityID, path, method)
|
||||
}
|
||||
|
||||
|
||||
+21
-4
@@ -2,7 +2,6 @@ package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -39,6 +38,17 @@ type APIUsecase struct{ APIRepo }
|
||||
|
||||
func NewAPIUsecase(repo APIRepo) *APIUsecase { return &APIUsecase{APIRepo: repo} }
|
||||
|
||||
// DeleteAPI preserves the single-delete contract used by the legacy admin:
|
||||
// the target is looked up first, so deleting a missing API returns the
|
||||
// repository's not-found error instead of silently succeeding on an empty
|
||||
// UPDATE/DELETE. Bulk deletion intentionally keeps separate semantics.
|
||||
func (uc *APIUsecase) DeleteAPI(ctx context.Context, id uint) error {
|
||||
if _, err := uc.FindAPI(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
return uc.DeleteAPIs(ctx, []uint{id})
|
||||
}
|
||||
|
||||
type APISyncDiff struct {
|
||||
Added []*API
|
||||
Deleted []*API
|
||||
@@ -54,7 +64,7 @@ func (uc *APIUsecase) SyncAPIs(ctx context.Context, routes []*API) (*APISyncDiff
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key := func(value *API) string { return strings.ToUpper(value.Method) + " " + value.Path }
|
||||
key := func(value *API) string { return value.Method + " " + value.Path }
|
||||
ignoreSet := make(map[string]bool, len(ignored))
|
||||
for _, item := range ignored {
|
||||
ignoreSet[key(item)] = true
|
||||
@@ -70,12 +80,19 @@ func (uc *APIUsecase) SyncAPIs(ctx context.Context, routes []*API) (*APISyncDiff
|
||||
storedSet[key(item)] = item
|
||||
}
|
||||
diff := &APISyncDiff{Ignored: ignored}
|
||||
for routeKey, item := range routeSet {
|
||||
added := make(map[string]bool)
|
||||
for _, item := range routes {
|
||||
routeKey := key(item)
|
||||
if ignoreSet[routeKey] || added[routeKey] {
|
||||
continue
|
||||
}
|
||||
if storedSet[routeKey] == nil {
|
||||
diff.Added = append(diff.Added, item)
|
||||
added[routeKey] = true
|
||||
}
|
||||
}
|
||||
for storedKey, item := range storedSet {
|
||||
for _, item := range stored {
|
||||
storedKey := key(item)
|
||||
if routeSet[storedKey] == nil && !ignoreSet[storedKey] {
|
||||
diff.Deleted = append(diff.Deleted, item)
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ var (
|
||||
type OperationRecord struct {
|
||||
ID uint
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
IP, Method, Path string
|
||||
Status int
|
||||
LatencyMS int64
|
||||
@@ -30,6 +31,7 @@ type OperationRecord struct {
|
||||
type LoginLog struct {
|
||||
ID uint
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
Username, IP string
|
||||
Status bool
|
||||
ErrorMessage, Agent string
|
||||
@@ -41,6 +43,7 @@ type LoginLog struct {
|
||||
type DataAccessLog struct {
|
||||
ID uint
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
EventType, TargetTable, Operation string
|
||||
UserID, AuthorityID uint
|
||||
Scope int
|
||||
@@ -79,7 +82,7 @@ func NewLogViewerUsecase(repo LogFileRepo) *LogViewerUsecase {
|
||||
|
||||
type ErrorRecord struct {
|
||||
ID uint
|
||||
CreatedAt time.Time
|
||||
CreatedAt, UpdatedAt time.Time
|
||||
Form, Info, Level, RequestID, TraceID, Solution, Status string
|
||||
CreatedAtRange []time.Time
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ func (uc *AuthenticationUsecase) Login(ctx context.Context, attempt *LoginAttemp
|
||||
}
|
||||
|
||||
uc.security.ClearLoginState(ctx, attempt.Username)
|
||||
if config != nil && config.PwdExpireEnable && user.PasswordUpdatedAt != nil && time.Since(*user.PasswordUpdatedAt) > time.Duration(config.PwdExpireDays)*24*time.Hour {
|
||||
if config != nil && config.PwdExpireEnable && config.PwdExpireDays > 0 && user.PasswordUpdatedAt != nil && time.Now().After((*user.PasswordUpdatedAt).AddDate(0, 0, config.PwdExpireDays)) {
|
||||
user.MustChangePassword = true
|
||||
}
|
||||
issued, err := uc.issuer.IssueToken(user, user.AuthorityID, user.MustChangePassword, 0)
|
||||
@@ -127,42 +127,45 @@ func (uc *AuthenticationUsecase) Login(ctx context.Context, attempt *LoginAttemp
|
||||
return &AuthenticationResult{User: user, Token: issued.Value, ExpiresAt: issued.ExpiresAt, NeedChangePassword: user.MustChangePassword}, nil
|
||||
}
|
||||
|
||||
func (uc *AuthenticationUsecase) SwitchAuthority(ctx context.Context, userID, authorityID uint) (*AuthenticationResult, error) {
|
||||
if err := uc.users.SetUserAuthority(ctx, userID, authorityID); err != nil {
|
||||
// SwitchAuthority changes the current user's active role and re-signs the
|
||||
// current claims. Role switching keeps the original JWT expiry; it
|
||||
// does not treat a role switch as a fresh login or rotate the multipoint
|
||||
// session. Keeping that distinction is important to the admin UI, which
|
||||
// replaces the token returned in the response but expects its original TTL.
|
||||
func (uc *AuthenticationUsecase) SwitchAuthority(ctx context.Context, claims *AuthClaims, authorityID uint) (*AuthenticationResult, error) {
|
||||
if claims == nil {
|
||||
return nil, ErrInvalidCredentials
|
||||
}
|
||||
if err := uc.users.SetUserAuthority(ctx, claims.ID, authorityID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user, err := uc.users.User(ctx, userID)
|
||||
user, err := uc.users.User(ctx, claims.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
issued, err := uc.issuer.IssueToken(user, authorityID, user.MustChangePassword, 0)
|
||||
// IssueToken receives the remaining lifetime rather than zero so the new
|
||||
// token expires at the same instant as the token it replaced.
|
||||
remaining := time.Until(claims.ExpiresAt)
|
||||
issued, err := uc.issuer.IssueToken(user, authorityID, claims.MustChangePwd, remaining)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if uc.security.UseMultipoint() {
|
||||
oldToken, cacheErr := uc.security.ActiveToken(ctx, user.Username)
|
||||
if cacheErr != nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrLoginState, cacheErr)
|
||||
}
|
||||
if cacheErr = uc.security.RotateActiveToken(ctx, user.Username, oldToken, issued.Value, issued.TTL); cacheErr != nil {
|
||||
if errors.Is(cacheErr, ErrTokenRevoke) {
|
||||
return nil, cacheErr
|
||||
}
|
||||
return nil, fmt.Errorf("%w: %v", ErrLoginState, cacheErr)
|
||||
}
|
||||
}
|
||||
return &AuthenticationResult{User: user, Token: issued.Value, ExpiresAt: issued.ExpiresAt, NeedChangePassword: user.MustChangePassword}, nil
|
||||
return &AuthenticationResult{User: user, Token: issued.Value, ExpiresAt: issued.ExpiresAt, NeedChangePassword: claims.MustChangePwd}, nil
|
||||
}
|
||||
|
||||
func (uc *AuthenticationUsecase) AuthenticateToken(ctx context.Context, token string) (*TokenAuthentication, error) {
|
||||
claims, err := uc.issuer.ParseToken(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// The blacklist is checked before parsing the JWT. Besides avoiding work,
|
||||
// this makes a revoked-but-expired token report the revocation reason (and
|
||||
// not the generic expiry message), which the frontend uses to decide whether
|
||||
// to clear a session.
|
||||
disabled, err := uc.security.tokens.IsTokenDisabled(ctx, token)
|
||||
if err != nil || disabled {
|
||||
return nil, ErrTokenDisabled
|
||||
}
|
||||
claims, err := uc.issuer.ParseToken(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &TokenAuthentication{Claims: claims}
|
||||
if claims.BufferTime <= 0 || time.Until(claims.ExpiresAt) >= claims.BufferTime {
|
||||
return result, nil
|
||||
@@ -172,7 +175,10 @@ func (uc *AuthenticationUsecase) AuthenticateToken(ctx context.Context, token st
|
||||
if err != nil {
|
||||
return result, nil
|
||||
}
|
||||
if err = uc.security.RotateActiveToken(ctx, claims.Username, token, issued.Value, issued.TTL); err != nil {
|
||||
// Refreshing updates the active Redis token but does not blacklist the old JWT;
|
||||
// both tokens remain valid until their own expiry. Passing an empty old token
|
||||
// preserves that behavior while still updating the multipoint session.
|
||||
if err = uc.security.RotateActiveToken(ctx, claims.Username, "", issued.Value, issued.TTL); err != nil {
|
||||
return result, nil
|
||||
}
|
||||
result.Refreshed = issued
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package biz
|
||||
|
||||
import "context"
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
type AuthorityAccessRepo interface {
|
||||
CreateAuthority(context.Context, *Authority) error
|
||||
@@ -15,6 +18,21 @@ type AuthorityAccessRepo interface {
|
||||
ResolveDataScope(context.Context, uint, uint) (DataScope, error)
|
||||
}
|
||||
|
||||
// Authority is the domain representation of an administration role. Keep the
|
||||
// audit timestamps because exports and API clients use them even though the
|
||||
// administration screens do not display them.
|
||||
type Authority struct {
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time
|
||||
AuthorityID uint
|
||||
AuthorityName string
|
||||
ParentID *uint
|
||||
DataScope int
|
||||
DefaultRouter string
|
||||
Children []*Authority
|
||||
}
|
||||
|
||||
type AuthorityUsecase struct{ AuthorityAccessRepo }
|
||||
|
||||
func NewAuthorityUsecase(repo AuthorityAccessRepo) *AuthorityUsecase {
|
||||
@@ -36,11 +54,11 @@ func (uc *AuthorityUsecase) AuthorityTree(ctx context.Context) ([]*Authority, er
|
||||
if item.ParentID != nil && *item.ParentID != 0 && byID[*item.ParentID] != nil {
|
||||
parent := byID[*item.ParentID]
|
||||
parent.Children = append(parent.Children, item)
|
||||
} else if item.ParentID == nil || *item.ParentID == 0 {
|
||||
} else if item.ParentID != nil && *item.ParentID == 0 {
|
||||
roots = append(roots, item)
|
||||
}
|
||||
}
|
||||
// In GVA strict-authority mode a non-root actor receives its direct
|
||||
// In strict-authority mode a non-root actor receives its direct
|
||||
// children as the top-level result even though their parent is omitted.
|
||||
if len(roots) == 0 {
|
||||
if actor, ok := ActorFromContext(ctx); ok {
|
||||
|
||||
@@ -12,7 +12,7 @@ type DictionaryDetail struct {
|
||||
Label string
|
||||
Value string
|
||||
Extend string
|
||||
Status bool
|
||||
Status *bool
|
||||
Sort int
|
||||
DictionaryID uint
|
||||
ParentID *uint
|
||||
@@ -45,7 +45,7 @@ type Dictionary struct {
|
||||
UpdatedAt time.Time
|
||||
Name string
|
||||
Type string
|
||||
Status bool
|
||||
Status *bool
|
||||
Desc string
|
||||
ParentID *uint
|
||||
Children []*Dictionary
|
||||
|
||||
+11
-3
@@ -32,9 +32,17 @@ func (uc *EmailUsecase) Alert(ctx context.Context, subject, body string) error {
|
||||
if !uc.repo.Enabled() {
|
||||
return nil
|
||||
}
|
||||
recipients := uc.repo.DefaultRecipients()
|
||||
if len(recipients) == 0 {
|
||||
return nil
|
||||
// ErrorToEmail splits the configured comma-separated recipient
|
||||
// string, while its explicit test endpoint deliberately sends the whole
|
||||
// value as one recipient. Keep those two contracts separate.
|
||||
configured := uc.repo.DefaultRecipients()
|
||||
recipients := make([]string, 0, len(configured))
|
||||
for _, value := range configured {
|
||||
parts := strings.Split(value, ",")
|
||||
if len(parts) > 1 && parts[len(parts)-1] == "" {
|
||||
parts = parts[:len(parts)-1]
|
||||
}
|
||||
recipients = append(recipients, parts...)
|
||||
}
|
||||
return uc.repo.Send(ctx, recipients, subject, body)
|
||||
}
|
||||
|
||||
+13
-2
@@ -5,8 +5,19 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type ExportCondition struct{ From, Column, Operator string }
|
||||
type ExportJoin struct{ Join, Table, On string }
|
||||
type ExportCondition struct {
|
||||
ID uint
|
||||
CreatedAt, UpdatedAt time.Time
|
||||
TemplateID string
|
||||
From, Column string
|
||||
Operator string
|
||||
}
|
||||
type ExportJoin struct {
|
||||
ID uint
|
||||
CreatedAt, UpdatedAt time.Time
|
||||
TemplateID string
|
||||
Join, Table, On string
|
||||
}
|
||||
type ExportTemplate struct {
|
||||
ID uint
|
||||
CreatedAt time.Time
|
||||
|
||||
+73
-5
@@ -48,14 +48,25 @@ func validateMediaName(name string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (uc *MediaUsecase) Upload(ctx context.Context, userID uint, name, mime string, categoryID int, reader io.Reader, save bool) (*MediaFile, error) {
|
||||
func (uc *MediaUsecase) Upload(ctx context.Context, userID uint, name, suppliedMIME string, categoryID int, reader io.Reader, save bool) (*MediaFile, error) {
|
||||
if err := validateMediaName(name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ext := strings.ToLower(filepath.Ext(name))
|
||||
buffered := bufio.NewReader(reader)
|
||||
if header, _ := buffered.Peek(512); len(header) > 0 {
|
||||
mime = http.DetectContentType(header)
|
||||
// Keep the multipart Content-Type when the client supplied one.
|
||||
// Only infer it when the header is empty: first by extension, then by
|
||||
// sniffing the first 512 bytes. Overwriting a valid client MIME with
|
||||
// http.DetectContentType changes the media record for common uploads
|
||||
// (for example SVG/JSON), and diverges from the compatible DetectMIME helper.
|
||||
if strings.TrimSpace(suppliedMIME) == "" {
|
||||
if detected := mediaMIMEByExtension(ext); detected != "" {
|
||||
suppliedMIME = detected
|
||||
} else if header, _ := buffered.Peek(512); len(header) > 0 {
|
||||
suppliedMIME = http.DetectContentType(header)
|
||||
} else {
|
||||
suppliedMIME = "application/octet-stream"
|
||||
}
|
||||
}
|
||||
key := time.Now().Format("20060102") + "/" + uuid.NewString() + ext
|
||||
hash := md5.New()
|
||||
@@ -63,7 +74,7 @@ func (uc *MediaUsecase) Upload(ctx context.Context, userID uint, name, mime stri
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
media := &MediaFile{Name: name, CategoryID: categoryID, URL: stored.URL, Tag: strings.TrimPrefix(ext, "."), Key: key, Size: stored.Size, Mime: mime, MD5: hex.EncodeToString(hash.Sum(nil)), UserID: userID}
|
||||
media := &MediaFile{Name: name, CategoryID: categoryID, URL: stored.URL, Tag: strings.TrimPrefix(filepath.Ext(name), "."), Key: key, Size: stored.Size, Mime: suppliedMIME, MD5: hex.EncodeToString(hash.Sum(nil)), UserID: userID}
|
||||
if save {
|
||||
if err = uc.CreateMedia(ctx, media); err != nil {
|
||||
_ = uc.files.Delete(ctx, key)
|
||||
@@ -72,6 +83,30 @@ func (uc *MediaUsecase) Upload(ctx context.Context, userID uint, name, mime stri
|
||||
}
|
||||
return media, nil
|
||||
}
|
||||
|
||||
// mediaMIMEByExtension mirrors the small extension fallback table. It is
|
||||
// intentionally narrower than mime.TypeByExtension: all other types are sniffed.
|
||||
// types from the first 512 bytes.
|
||||
func mediaMIMEByExtension(ext string) string {
|
||||
switch ext {
|
||||
case ".json":
|
||||
return "application/json"
|
||||
case ".xml":
|
||||
return "application/xml"
|
||||
case ".pdf":
|
||||
return "application/pdf"
|
||||
case ".zip":
|
||||
return "application/zip"
|
||||
case ".mp4":
|
||||
return "video/mp4"
|
||||
case ".mp3":
|
||||
return "audio/mpeg"
|
||||
case ".csv":
|
||||
return "text/csv"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
func (uc *MediaUsecase) Delete(ctx context.Context, id uint) error {
|
||||
media, err := uc.FindMedia(ctx, id)
|
||||
if err != nil {
|
||||
@@ -89,5 +124,38 @@ func (uc *MediaUsecase) Delete(ctx context.Context, id uint) error {
|
||||
return uc.DeleteMedia(ctx, id)
|
||||
}
|
||||
func (uc *MediaUsecase) ListStorage(ctx context.Context, prefix, cursor string, limit int) ([]*StoredFile, string, bool, error) {
|
||||
return uc.files.List(ctx, prefix, cursor, limit)
|
||||
if limit <= 0 {
|
||||
limit = 100
|
||||
}
|
||||
// Multipart chunks are staged on the local filesystem, so listOssFiles
|
||||
// never exposes them. Kra's storage abstraction may stage them through
|
||||
// the selected backend; hide the internal prefix to preserve the API.
|
||||
chunkRoot := ".chunks"
|
||||
if uc.settings != nil {
|
||||
if configured := strings.Trim(uc.settings.MediaSettings().ChunkDir, "/\\ "); configured != "" {
|
||||
chunkRoot = configured
|
||||
}
|
||||
}
|
||||
chunkRoot = strings.Trim(chunkRoot, "/") + "/"
|
||||
result := make([]*StoredFile, 0, limit)
|
||||
nextCursor := cursor
|
||||
for {
|
||||
items, next, more, err := uc.files.List(ctx, prefix, nextCursor, limit)
|
||||
if err != nil {
|
||||
return nil, "", false, err
|
||||
}
|
||||
for _, item := range items {
|
||||
if item == nil || strings.HasPrefix(strings.TrimPrefix(item.Path, "/"), chunkRoot) {
|
||||
continue
|
||||
}
|
||||
result = append(result, item)
|
||||
if len(result) == limit {
|
||||
return result, next, more, nil
|
||||
}
|
||||
}
|
||||
if !more || next == "" || next == nextCursor {
|
||||
return result, next, more, nil
|
||||
}
|
||||
nextCursor = next
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
@@ -17,7 +18,7 @@ import (
|
||||
)
|
||||
|
||||
func (uc *MediaUsecase) chunkPrefix(uploadID uint) string {
|
||||
directory := ".chunks"
|
||||
directory := "uploads/chunks"
|
||||
if uc.settings != nil {
|
||||
if configured := strings.Trim(uc.settings.MediaSettings().ChunkDir, "/\\ "); configured != "" {
|
||||
directory = configured
|
||||
@@ -34,9 +35,6 @@ func (uc *MediaUsecase) InitUpload(ctx context.Context, userID uint, name, hash
|
||||
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 completed, err := uc.FindCompletedSession(ctx, userID, hash); err == nil && completed.MediaID != 0 {
|
||||
if media, findErr := uc.FindMedia(ctx, completed.MediaID); findErr == nil {
|
||||
copy := &MediaFile{Name: name, URL: media.URL, Tag: media.Tag, Key: media.Key}
|
||||
@@ -65,29 +63,50 @@ func (uc *MediaUsecase) InitUpload(ctx context.Context, userID uint, name, hash
|
||||
}
|
||||
func (uc *MediaUsecase) SaveChunk(ctx context.Context, userID, uploadID uint, index int, expected string, reader io.Reader) error {
|
||||
session, err := uc.FindUploadSession(ctx, uploadID)
|
||||
if err != nil || session.UserID != userID {
|
||||
return errors.New("上传会话不存在或无权操作")
|
||||
if err != nil {
|
||||
return errors.New("上传会话不存在")
|
||||
}
|
||||
if session.UserID != userID {
|
||||
return errors.New("无权操作该上传")
|
||||
}
|
||||
if session.Status != "uploading" {
|
||||
return errors.New("上传会话状态不允许收片")
|
||||
}
|
||||
hash := md5.New()
|
||||
key := uc.chunkKey(uploadID, index)
|
||||
stored, err := uc.files.Put(ctx, key, io.TeeReader(reader, hash))
|
||||
temporary, err := os.CreateTemp("", "kra-upload-chunk-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
temporaryName := temporary.Name()
|
||||
defer os.Remove(temporaryName)
|
||||
defer temporary.Close()
|
||||
if _, err = io.Copy(io.MultiWriter(temporary, hash), reader); err != nil {
|
||||
return err
|
||||
}
|
||||
actual := hex.EncodeToString(hash.Sum(nil))
|
||||
if actual != expected {
|
||||
_ = uc.files.Delete(ctx, key)
|
||||
return fmt.Errorf("分片 %d 校验失败", index)
|
||||
}
|
||||
if _, err = temporary.Seek(0, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
stored, err := uc.files.Put(ctx, key, temporary)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return uc.UpsertChunk(ctx, uploadID, &UploadChunk{Index: index, Hash: actual, Size: stored.Size})
|
||||
}
|
||||
func (uc *MediaUsecase) CompleteUpload(ctx context.Context, userID, uploadID uint, mime string) (*MediaFile, error) {
|
||||
session, err := uc.FindUploadSession(ctx, uploadID)
|
||||
if err != nil || session.UserID != userID {
|
||||
return nil, errors.New("上传会话不存在或无权操作")
|
||||
if err != nil {
|
||||
return nil, errors.New("上传会话不存在")
|
||||
}
|
||||
if session.UserID != userID {
|
||||
return nil, errors.New("无权操作该上传")
|
||||
}
|
||||
if err = validateMediaName(session.FileName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
claimed, err := uc.ClaimUploadSession(ctx, uploadID)
|
||||
if err != nil {
|
||||
@@ -122,7 +141,10 @@ func (uc *MediaUsecase) CompleteUpload(ctx context.Context, userID, uploadID uin
|
||||
_ = uc.files.Delete(ctx, key)
|
||||
return fail(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}
|
||||
// Keep the size declared when the upload session was created. The full-file
|
||||
// MD5 above remains the integrity check even if storage reports another
|
||||
// byte count.
|
||||
media := &MediaFile{Name: session.FileName, URL: stored.URL, Tag: strings.TrimPrefix(filepath.Ext(session.FileName), "."), Key: key, Size: session.FileSize, Mime: mime, MD5: hash, UserID: userID}
|
||||
if err = uc.CreateMedia(ctx, media); err != nil {
|
||||
_ = uc.files.Delete(ctx, key)
|
||||
return fail(err)
|
||||
@@ -134,8 +156,11 @@ func (uc *MediaUsecase) CompleteUpload(ctx context.Context, userID, uploadID uin
|
||||
}
|
||||
func (uc *MediaUsecase) CancelUpload(ctx context.Context, userID, uploadID uint) error {
|
||||
session, err := uc.FindUploadSession(ctx, uploadID)
|
||||
if err != nil || session.UserID != userID {
|
||||
return errors.New("上传会话不存在或无权操作")
|
||||
if err != nil {
|
||||
return errors.New("上传会话不存在")
|
||||
}
|
||||
if session.UserID != userID {
|
||||
return errors.New("无权操作该上传")
|
||||
}
|
||||
_ = uc.DeleteChunks(ctx, uploadID)
|
||||
_ = uc.files.DeletePrefix(ctx, uc.chunkPrefix(uploadID))
|
||||
|
||||
+23
-8
@@ -1,8 +1,14 @@
|
||||
package biz
|
||||
|
||||
import "context"
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
type MenuButton struct {
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time
|
||||
ID uint
|
||||
Name string
|
||||
Description string
|
||||
@@ -11,15 +17,22 @@ type MenuButton struct {
|
||||
}
|
||||
|
||||
type MenuParameter struct {
|
||||
ID uint
|
||||
MenuID uint
|
||||
Type string
|
||||
Key string
|
||||
Value string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time
|
||||
ID uint
|
||||
MenuID uint
|
||||
Type string
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
|
||||
type Menu struct {
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time
|
||||
ID uint
|
||||
MenuID uint
|
||||
ParentID uint
|
||||
Path string
|
||||
Name string
|
||||
@@ -73,7 +86,7 @@ func (uc *MenuUsecase) Tree(ctx context.Context) ([]*Menu, error) {
|
||||
for _, item := range items {
|
||||
if parent := byID[item.ParentID]; parent != nil {
|
||||
parent.Children = append(parent.Children, item)
|
||||
} else {
|
||||
} else if item.ParentID == 0 {
|
||||
roots = append(roots, item)
|
||||
}
|
||||
}
|
||||
@@ -118,8 +131,10 @@ func (uc *MenuUsecase) AuthorityMenus(ctx context.Context, authorityID uint) ([]
|
||||
if _, ok := selected[item.ID]; ok {
|
||||
// The authority-menu endpoint returns menu relations only. Keep its
|
||||
// existing contract: button details are loaded by the dedicated
|
||||
// authority-button endpoints.
|
||||
// authority-button endpoints. The compatible query does not preload either
|
||||
// relation here, so parameters must remain absent as well.
|
||||
item.Buttons = nil
|
||||
item.Parameters = nil
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
|
||||
type SecurityConfig struct {
|
||||
ID uint
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
CaptchaOpen int
|
||||
CaptchaTimeout int
|
||||
KeyLong int
|
||||
@@ -128,21 +130,21 @@ func (uc *SecurityUsecase) ValidateCurrentPassword(ctx context.Context, password
|
||||
}
|
||||
|
||||
func (uc *SecurityUsecase) LoginLocked(ctx context.Context, username string) (bool, error) {
|
||||
_, locked, err := uc.cache.Get(ctx, "login:lock:"+username)
|
||||
_, locked, err := uc.cache.Get(ctx, loginLockKey(username))
|
||||
return locked, err
|
||||
}
|
||||
|
||||
func (uc *SecurityUsecase) IncrementLoginFailure(ctx context.Context, username string, expiration time.Duration) (int64, error) {
|
||||
return uc.cache.Increment(ctx, "login:fail:"+username, expiration)
|
||||
return uc.cache.Increment(ctx, loginFailureKey(username), expiration)
|
||||
}
|
||||
|
||||
func (uc *SecurityUsecase) LockLogin(ctx context.Context, username string, expiration time.Duration) error {
|
||||
return uc.cache.Set(ctx, "login:lock:"+username, "1", expiration)
|
||||
return uc.cache.Set(ctx, loginLockKey(username), "1", expiration)
|
||||
}
|
||||
|
||||
func (uc *SecurityUsecase) ClearLoginState(ctx context.Context, username string) {
|
||||
_ = uc.cache.Delete(ctx, "login:fail:"+username)
|
||||
_ = uc.cache.Delete(ctx, "login:lock:"+username)
|
||||
_ = uc.cache.Delete(ctx, loginFailureKey(username))
|
||||
_ = uc.cache.Delete(ctx, loginLockKey(username))
|
||||
}
|
||||
|
||||
func (uc *SecurityUsecase) EnsureLoginIPCounter(ctx context.Context, ip string, expiration time.Duration) (int, error) {
|
||||
@@ -171,15 +173,15 @@ func (uc *SecurityUsecase) IncrementRateLimit(ctx context.Context, key string, e
|
||||
}
|
||||
|
||||
func (uc *SecurityUsecase) SetCaptcha(ctx context.Context, id, value string, expiration time.Duration) error {
|
||||
return uc.cache.Set(ctx, "captcha:"+id, value, expiration)
|
||||
return uc.cache.Set(ctx, "CAPTCHA_"+id, value, expiration)
|
||||
}
|
||||
|
||||
func (uc *SecurityUsecase) GetCaptcha(ctx context.Context, id string) (string, bool, error) {
|
||||
return uc.cache.Get(ctx, "captcha:"+id)
|
||||
return uc.cache.Get(ctx, "CAPTCHA_"+id)
|
||||
}
|
||||
|
||||
func (uc *SecurityUsecase) DeleteCaptcha(ctx context.Context, id string) error {
|
||||
return uc.cache.Delete(ctx, "captcha:"+id)
|
||||
return uc.cache.Delete(ctx, "CAPTCHA_"+id)
|
||||
}
|
||||
|
||||
func (uc *SecurityUsecase) VerifyCaptcha(ctx context.Context, id, answer string, clear bool) bool {
|
||||
@@ -196,7 +198,13 @@ func (uc *SecurityUsecase) VerifyCaptcha(ctx context.Context, id, answer string,
|
||||
return strings.EqualFold(value, answer)
|
||||
}
|
||||
|
||||
func activeTokenKey(username string) string { return "jwt:active:" + username }
|
||||
// The currently active multipoint token is stored under the username itself.
|
||||
// Keep the same key so a rolling deployment can revoke sessions issued by the
|
||||
// other implementation instead of silently creating a second active session.
|
||||
func activeTokenKey(username string) string { return username }
|
||||
|
||||
func loginFailureKey(username string) string { return "login_fail:" + username }
|
||||
func loginLockKey(username string) string { return "login_lock:" + username }
|
||||
|
||||
func (uc *SecurityUsecase) ActiveToken(ctx context.Context, username string) (string, error) {
|
||||
value, _, err := uc.cache.Get(ctx, activeTokenKey(username))
|
||||
|
||||
@@ -43,8 +43,8 @@ func (uc *SystemConfigUsecase) IsInitialized(ctx context.Context) (bool, error)
|
||||
return uc.repo.IsInitialized(ctx)
|
||||
}
|
||||
func (uc *SystemConfigUsecase) Initialize(ctx context.Context, config *DatabaseConfig) error {
|
||||
if config == nil || len(config.AdminPassword) < 6 {
|
||||
return ErrInvalidCredentials
|
||||
if config == nil {
|
||||
return errors.New("数据库初始化参数无效")
|
||||
}
|
||||
if err := uc.repo.Initialize(ctx, config); err != nil {
|
||||
return err
|
||||
|
||||
+2
-25
@@ -27,7 +27,7 @@ type TimedTask struct {
|
||||
}
|
||||
type TimedTaskLog struct {
|
||||
ID uint
|
||||
CreatedAt time.Time
|
||||
CreatedAt, UpdatedAt time.Time
|
||||
TaskID uint
|
||||
TaskName, TriggerType string
|
||||
StartedAt, FinishedAt time.Time
|
||||
@@ -49,29 +49,6 @@ type TaskScheduleError struct{ Err error }
|
||||
func (e *TaskScheduleError) Error() string { return e.Err.Error() }
|
||||
func (e *TaskScheduleError) Is(target error) bool { return target == ErrTaskSchedule }
|
||||
|
||||
type TaskMethod struct {
|
||||
Name string
|
||||
Description string
|
||||
}
|
||||
|
||||
var registeredTaskMethods = []TaskMethod{
|
||||
{Name: TaskMethodClearDB, Description: "清理数据库过期日志(操作记录、JWT黑名单、定时任务日志)"},
|
||||
{Name: TaskMethodUploads, Description: "清理过期大文件上传会话"},
|
||||
}
|
||||
|
||||
func RegisteredTaskMethods() []TaskMethod {
|
||||
return append([]TaskMethod(nil), registeredTaskMethods...)
|
||||
}
|
||||
|
||||
func registeredTaskMethod(name string) bool {
|
||||
for _, method := range registeredTaskMethods {
|
||||
if method.Name == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type TaskRepo interface {
|
||||
CreateTask(context.Context, *TimedTask) error
|
||||
UpdateTask(context.Context, *TimedTask) error
|
||||
@@ -122,7 +99,7 @@ func (uc *TaskUsecase) Validate(value *TimedTask) error {
|
||||
}
|
||||
case TaskExecutorHTTP:
|
||||
parsed, parseErr := url.Parse(value.HTTPURL)
|
||||
if parseErr != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.Hostname() == "" {
|
||||
if parseErr != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.Host == "" {
|
||||
return errors.New("httpUrl 必须是合法的 http/https 地址")
|
||||
}
|
||||
if len(value.HTTPHeader) > 0 {
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// TaskMethodFunc is the execution contract for a registered in-process task.
|
||||
// Implementations are responsible for decoding params and honoring ctx cancellation.
|
||||
type TaskMethodFunc func(ctx context.Context, params json.RawMessage) error
|
||||
|
||||
type TaskMethod struct {
|
||||
Name string
|
||||
Description string
|
||||
}
|
||||
|
||||
type taskMethodEntry struct {
|
||||
meta TaskMethod
|
||||
fn TaskMethodFunc
|
||||
}
|
||||
|
||||
var taskMethodRegistry = struct {
|
||||
sync.RWMutex
|
||||
methods map[string]taskMethodEntry
|
||||
}{methods: make(map[string]taskMethodEntry)}
|
||||
|
||||
// RegisterTaskMethod registers an in-process task. Re-registering the same name
|
||||
// replaces the previous implementation so startup registration remains idempotent.
|
||||
func RegisterTaskMethod(name, description string, fn TaskMethodFunc) {
|
||||
taskMethodRegistry.Lock()
|
||||
defer taskMethodRegistry.Unlock()
|
||||
taskMethodRegistry.methods[name] = taskMethodEntry{
|
||||
meta: TaskMethod{Name: name, Description: description},
|
||||
fn: fn,
|
||||
}
|
||||
}
|
||||
|
||||
// TaskMethodByName returns the registered function for name.
|
||||
func TaskMethodByName(name string) (TaskMethodFunc, bool) {
|
||||
taskMethodRegistry.RLock()
|
||||
defer taskMethodRegistry.RUnlock()
|
||||
method, ok := taskMethodRegistry.methods[name]
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
return method.fn, true
|
||||
}
|
||||
|
||||
// RegisteredTaskMethods returns task metadata in a stable name order.
|
||||
func RegisteredTaskMethods() []TaskMethod {
|
||||
taskMethodRegistry.RLock()
|
||||
defer taskMethodRegistry.RUnlock()
|
||||
methods := make([]TaskMethod, 0, len(taskMethodRegistry.methods))
|
||||
for _, method := range taskMethodRegistry.methods {
|
||||
methods = append(methods, method.meta)
|
||||
}
|
||||
sort.Slice(methods, func(i, j int) bool { return methods[i].Name < methods[j].Name })
|
||||
return methods
|
||||
}
|
||||
|
||||
func registeredTaskMethod(name string) bool {
|
||||
_, ok := TaskMethodByName(name)
|
||||
return ok
|
||||
}
|
||||
+10
-11
@@ -35,15 +35,6 @@ type UserListFilter struct {
|
||||
Desc bool
|
||||
}
|
||||
|
||||
type Authority struct {
|
||||
AuthorityID uint
|
||||
AuthorityName string
|
||||
ParentID *uint
|
||||
DataScope int
|
||||
DefaultRouter string
|
||||
Children []*Authority
|
||||
}
|
||||
|
||||
type UserRepo interface {
|
||||
FindUserByUsername(context.Context, string) (*User, error)
|
||||
FindUserByID(context.Context, uint) (*User, error)
|
||||
@@ -125,8 +116,16 @@ func (uc *UserUsecase) CreateUser(ctx context.Context, user *User, authorityIDs
|
||||
return uc.repo.CreateUserWithAuthorities(ctx, user, authorityIDs)
|
||||
}
|
||||
func (uc *UserUsecase) UpdateUser(ctx context.Context, user *User, authorityIDs []uint) error {
|
||||
authorityIDs = includeAuthority(authorityIDs, user.AuthorityID)
|
||||
return uc.repo.UpdateUserWithAuthorities(ctx, user, authorityIDs)
|
||||
// Role associations are updated first only when authorityIds was supplied,
|
||||
// and then updates the basic user fields as a separate operation. Preserve
|
||||
// that ordering and partial-failure behavior instead of coupling both writes
|
||||
// into the data-layer transaction used by lower-level callers.
|
||||
if len(authorityIDs) > 0 {
|
||||
if err := uc.repo.SetUserAuthorities(ctx, user.ID, authorityIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return uc.repo.UpdateUser(ctx, user)
|
||||
}
|
||||
|
||||
func (uc *UserUsecase) UpdateSelfUser(ctx context.Context, user *User) error {
|
||||
|
||||
@@ -8,6 +8,24 @@ import (
|
||||
|
||||
var ErrInvalidVersion = errors.New("版本信息格式错误")
|
||||
|
||||
type VersionStage string
|
||||
|
||||
const (
|
||||
VersionStageMenus VersionStage = "menus"
|
||||
VersionStageAPIs VersionStage = "apis"
|
||||
VersionStageDictionaries VersionStage = "dictionaries"
|
||||
VersionStageJSON VersionStage = "json"
|
||||
VersionStageSave VersionStage = "save"
|
||||
)
|
||||
|
||||
type VersionStageError struct {
|
||||
Stage VersionStage
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *VersionStageError) Error() string { return e.Err.Error() }
|
||||
func (e *VersionStageError) Unwrap() error { return e.Err }
|
||||
|
||||
type Version struct {
|
||||
ID uint
|
||||
CreatedAt time.Time
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"google.golang.org/protobuf/types/known/durationpb"
|
||||
)
|
||||
|
||||
func gvaDatabase(value *conf.Data_Database) map[string]any {
|
||||
func compatibleDatabase(value *conf.Data_Database) map[string]any {
|
||||
if value == nil {
|
||||
return map[string]any{}
|
||||
}
|
||||
@@ -28,14 +28,14 @@ func gvaDatabase(value *conf.Data_Database) map[string]any {
|
||||
}
|
||||
}
|
||||
|
||||
func gvaRedis(value *conf.Data_Redis) map[string]any {
|
||||
func compatibleRedis(value *conf.Data_Redis) map[string]any {
|
||||
if value == nil {
|
||||
return map[string]any{}
|
||||
}
|
||||
return map[string]any{"name": value.Name, "addr": value.Addr, "password": value.Password, "db": value.Db, "useCluster": value.UseCluster, "clusterAddrs": value.ClusterAddrs}
|
||||
}
|
||||
|
||||
func gvaObjectStore(value *conf.AdminBackend_ObjectStore) map[string]any {
|
||||
func compatibleObjectStore(value *conf.AdminBackend_ObjectStore) map[string]any {
|
||||
if value == nil {
|
||||
return map[string]any{}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ func gvaObjectStore(value *conf.AdminBackend_ObjectStore) map[string]any {
|
||||
}
|
||||
}
|
||||
|
||||
func gvaObjectStores(storage *conf.AdminBackend_Storage) map[string]map[string]any {
|
||||
func compatibleObjectStores(storage *conf.AdminBackend_Storage) map[string]map[string]any {
|
||||
if storage == nil {
|
||||
storage = &conf.AdminBackend_Storage{}
|
||||
}
|
||||
@@ -82,7 +82,7 @@ func gvaObjectStores(storage *conf.AdminBackend_Storage) map[string]map[string]a
|
||||
}
|
||||
}
|
||||
|
||||
func gvaConfiguration(data *conf.Data, admin *conf.AdminBackend) map[string]any {
|
||||
func compatibleConfiguration(data *conf.Data, admin *conf.AdminBackend) map[string]any {
|
||||
result := map[string]any{}
|
||||
if data == nil {
|
||||
data = &conf.Data{}
|
||||
@@ -107,10 +107,10 @@ func gvaConfiguration(data *conf.Data, admin *conf.AdminBackend) map[string]any
|
||||
"access-req-headers": zap.AccessReqHeaders, "access-log-max-bytes": zap.AccessLogMaxBytes,
|
||||
"file-only-modules": zap.FileOnlyModules,
|
||||
}
|
||||
result["redis"] = gvaRedis(data.Redis)
|
||||
result["redis"] = compatibleRedis(data.Redis)
|
||||
redisList := make([]map[string]any, 0, len(data.RedisList))
|
||||
for _, item := range data.RedisList {
|
||||
redisList = append(redisList, gvaRedis(item))
|
||||
redisList = append(redisList, compatibleRedis(item))
|
||||
}
|
||||
result["redis-list"] = redisList
|
||||
mongo := data.Mongo
|
||||
@@ -146,11 +146,11 @@ func gvaConfiguration(data *conf.Data, admin *conf.AdminBackend) map[string]any
|
||||
result[name] = map[string]any{}
|
||||
}
|
||||
if driver != "" {
|
||||
result[driver] = gvaDatabase(data.Database)
|
||||
result[driver] = compatibleDatabase(data.Database)
|
||||
}
|
||||
dbList := make([]map[string]any, 0, len(data.DatabaseList))
|
||||
for _, item := range data.DatabaseList {
|
||||
row := gvaDatabase(item)
|
||||
row := compatibleDatabase(item)
|
||||
if item != nil {
|
||||
row["type"], row["alias-name"], row["disable"] = item.Driver, item.AliasName, item.Disable
|
||||
}
|
||||
@@ -166,7 +166,11 @@ func gvaConfiguration(data *conf.Data, admin *conf.AdminBackend) map[string]any
|
||||
if media == nil {
|
||||
media = &conf.AdminBackend_Media{}
|
||||
}
|
||||
result["media"] = map[string]any{"session-ttl": media.SessionTtl, "max-file-size": media.MaxFileSize, "chunk-dir": media.ChunkDir}
|
||||
chunkDir := media.ChunkDir
|
||||
if chunkDir == "" {
|
||||
chunkDir = "uploads/chunks"
|
||||
}
|
||||
result["media"] = map[string]any{"session-ttl": media.SessionTtl, "max-file-size": media.MaxFileSize, "chunk-dir": chunkDir}
|
||||
if admin.Storage == nil {
|
||||
admin.Storage = &conf.AdminBackend_Storage{}
|
||||
}
|
||||
@@ -175,7 +179,7 @@ func gvaConfiguration(data *conf.Data, admin *conf.AdminBackend) map[string]any
|
||||
qiniu = &conf.AdminBackend_Qiniu{}
|
||||
}
|
||||
result["qiniu"] = map[string]any{"zone": qiniu.Zone, "bucket": qiniu.Bucket, "img-path": qiniu.BaseUrl, "access-key": qiniu.AccessKey, "secret-key": qiniu.SecretKey, "use-https": qiniu.UseHttps, "use-cdn-domains": qiniu.UseCdnDomains}
|
||||
for key, value := range gvaObjectStores(admin.Storage) {
|
||||
for key, value := range compatibleObjectStores(admin.Storage) {
|
||||
result[key] = value
|
||||
}
|
||||
disks := make([]map[string]any, 0, len(admin.DiskList))
|
||||
@@ -240,7 +244,7 @@ func stringSliceValue(values map[string]any, key string, fallback []string) []st
|
||||
return result
|
||||
}
|
||||
|
||||
func applyGVARedis(target *conf.Data_Redis, values map[string]any) {
|
||||
func applyCompatibleRedis(target *conf.Data_Redis, values map[string]any) {
|
||||
if target == nil || values == nil {
|
||||
return
|
||||
}
|
||||
@@ -257,14 +261,14 @@ func durationValue(raw string, fallback *durationpb.Duration) *durationpb.Durati
|
||||
if raw == "" {
|
||||
return fallback
|
||||
}
|
||||
value, err := parseGVADuration(raw)
|
||||
value, err := parseCompatibleDuration(raw)
|
||||
if err != nil {
|
||||
return fallback
|
||||
}
|
||||
return durationpb.New(value)
|
||||
}
|
||||
|
||||
func parseGVADuration(raw string) (time.Duration, error) {
|
||||
func parseCompatibleDuration(raw string) (time.Duration, error) {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if value, err := time.ParseDuration(raw); err == nil {
|
||||
return value, nil
|
||||
@@ -282,7 +286,7 @@ func parseGVADuration(raw string) (time.Duration, error) {
|
||||
return time.Duration(nanoseconds), err
|
||||
}
|
||||
|
||||
func applyGVADatabase(target *conf.Data_Database, values map[string]any, driver string) {
|
||||
func applyCompatibleDatabase(target *conf.Data_Database, values map[string]any, driver string) {
|
||||
if target == nil || values == nil {
|
||||
return
|
||||
}
|
||||
@@ -304,7 +308,7 @@ func applyGVADatabase(target *conf.Data_Database, values map[string]any, driver
|
||||
target.Singular = boolValue(values, "singular", target.Singular)
|
||||
}
|
||||
|
||||
func applyGVAObjectStore(target *conf.AdminBackend_ObjectStore, values map[string]any) {
|
||||
func applyCompatibleObjectStore(target *conf.AdminBackend_ObjectStore, values map[string]any) {
|
||||
if target == nil || values == nil {
|
||||
return
|
||||
}
|
||||
@@ -345,12 +349,12 @@ func applyGVAObjectStore(target *conf.AdminBackend_ObjectStore, values map[strin
|
||||
target.AccountId = stringValue(values, "account-id")
|
||||
}
|
||||
|
||||
func applyGVAConfiguration(raw json.RawMessage, data *conf.Data, admin *conf.AdminBackend) error {
|
||||
func applyCompatibleConfiguration(raw json.RawMessage, data *conf.Data, admin *conf.AdminBackend) error {
|
||||
var values map[string]any
|
||||
if err := json.Unmarshal(raw, &values); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, isGVA := values["system"]; !isGVA {
|
||||
if _, compatibleShape := values["system"]; !compatibleShape {
|
||||
return nil
|
||||
}
|
||||
if data.Database == nil {
|
||||
@@ -367,7 +371,7 @@ func applyGVAConfiguration(raw json.RawMessage, data *conf.Data, admin *conf.Adm
|
||||
if driver == "" {
|
||||
driver = data.Database.Driver
|
||||
}
|
||||
applyGVADatabase(data.Database, objectMap(values[driver]), driver)
|
||||
applyCompatibleDatabase(data.Database, objectMap(values[driver]), driver)
|
||||
admin.Storage.Type = stringValue(system, "oss-type")
|
||||
admin.RouterPrefix = stringValue(system, "router-prefix")
|
||||
admin.System.UseMultipoint = boolValue(system, "use-multipoint", admin.System.UseMultipoint)
|
||||
@@ -411,7 +415,7 @@ func applyGVAConfiguration(raw json.RawMessage, data *conf.Data, admin *conf.Adm
|
||||
if data.Redis == nil {
|
||||
data.Redis = &conf.Data_Redis{}
|
||||
}
|
||||
applyGVARedis(data.Redis, objectMap(values["redis"]))
|
||||
applyCompatibleRedis(data.Redis, objectMap(values["redis"]))
|
||||
if items, ok := values["redis-list"].([]any); ok {
|
||||
current := make(map[string]*conf.Data_Redis, len(data.RedisList))
|
||||
for _, item := range data.RedisList {
|
||||
@@ -426,7 +430,7 @@ func applyGVAConfiguration(raw json.RawMessage, data *conf.Data, admin *conf.Adm
|
||||
if target == nil {
|
||||
target = &conf.Data_Redis{}
|
||||
}
|
||||
applyGVARedis(target, values)
|
||||
applyCompatibleRedis(target, values)
|
||||
data.RedisList = append(data.RedisList, target)
|
||||
}
|
||||
}
|
||||
@@ -467,7 +471,7 @@ func applyGVAConfiguration(raw json.RawMessage, data *conf.Data, admin *conf.Adm
|
||||
if target == nil {
|
||||
target = &conf.Data_Database{}
|
||||
}
|
||||
applyGVADatabase(target, row, stringValue(row, "type"))
|
||||
applyCompatibleDatabase(target, row, stringValue(row, "type"))
|
||||
target.AliasName, target.Disable = alias, boolValue(row, "disable", target.Disable)
|
||||
data.DatabaseList = append(data.DatabaseList, target)
|
||||
}
|
||||
@@ -513,7 +517,7 @@ func applyGVAConfiguration(raw json.RawMessage, data *conf.Data, admin *conf.Adm
|
||||
if *store.target == nil {
|
||||
*store.target = &conf.AdminBackend_ObjectStore{}
|
||||
}
|
||||
applyGVAObjectStore(*store.target, objectMap(values[store.key]))
|
||||
applyCompatibleObjectStore(*store.target, objectMap(values[store.key]))
|
||||
}
|
||||
if disks, ok := values["disk-list"].([]any); ok {
|
||||
admin.DiskList = make([]*conf.AdminBackend_Disk, 0, len(disks))
|
||||
@@ -3,7 +3,6 @@ package data
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"kra/internal/biz"
|
||||
@@ -94,10 +93,9 @@ func (r *announcementRepo) UserOptions(ctx context.Context) ([]biz.UserOption, e
|
||||
Label string
|
||||
Value uint
|
||||
}
|
||||
err := r.data.gormDB.WithContext(ctx).Table("sys_users").Select("nick_name AS label, id AS value").Scan(&rows).Error
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
// The generated data-source endpoint is best effort: return collected
|
||||
// options even when the underlying scan reports an error.
|
||||
_ = r.data.gormDB.WithContext(ctx).Table("sys_users").Select("nick_name AS label, id AS value").Scan(&rows).Error
|
||||
items := make([]biz.UserOption, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
items = append(items, biz.UserOption{Label: row.Label, Value: row.Value})
|
||||
|
||||
+102
-56
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"kra/internal/biz"
|
||||
@@ -32,8 +31,12 @@ type apiPO struct {
|
||||
func (apiPO) TableName() string { return "sys_apis" }
|
||||
|
||||
type ignoredAPIPO struct {
|
||||
Path string `gorm:"primaryKey"`
|
||||
Method string `gorm:"primaryKey"`
|
||||
ID uint `gorm:"primaryKey"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||
Path string
|
||||
Method string `gorm:"default:POST"`
|
||||
}
|
||||
|
||||
func (ignoredAPIPO) TableName() string { return "sys_ignore_apis" }
|
||||
@@ -49,7 +52,7 @@ 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 *apiRepo) CreateAPI(ctx context.Context, v *biz.API) error {
|
||||
po := apiPO{Path: v.Path, Description: v.Description, APIGroup: v.APIGroup, Method: strings.ToUpper(v.Method)}
|
||||
po := apiPO{Path: v.Path, Description: v.Description, APIGroup: v.APIGroup, Method: v.Method}
|
||||
var count int64
|
||||
if err := r.data.gormDB.WithContext(ctx).Model(&apiPO{}).Where("path = ? AND method = ?", po.Path, po.Method).Count(&count).Error; err != nil {
|
||||
return err
|
||||
@@ -69,7 +72,7 @@ func (r *apiRepo) UpdateAPI(ctx context.Context, v *biz.API) error {
|
||||
if err := db.First(&old, v.ID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
method := strings.ToUpper(v.Method)
|
||||
method := v.Method
|
||||
if old.Path != v.Path || old.Method != method {
|
||||
var count int64
|
||||
if err := db.Model(&apiPO{}).Where("id <> ? AND path = ? AND method = ?", v.ID, v.Path, method).Count(&count).Error; err != nil {
|
||||
@@ -79,10 +82,28 @@ func (r *apiRepo) UpdateAPI(ctx context.Context, v *biz.API) error {
|
||||
return errors.New("存在相同api路径")
|
||||
}
|
||||
}
|
||||
// Authorization is stored independently in casbin_rule and updates every
|
||||
// matching policy when an API path or method changes.
|
||||
if err := db.Model(&casbinRulePO{}).
|
||||
Where("ptype = ? AND v1 = ? AND v2 = ?", "p", old.Path, old.Method).
|
||||
Updates(map[string]any{"v1": v.Path, "v2": method}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return db.Model(&old).Updates(map[string]any{"path": v.Path, "description": v.Description, "api_group": v.APIGroup, "method": method}).Error
|
||||
}
|
||||
func (r *apiRepo) DeleteAPIs(ctx context.Context, ids []uint) error {
|
||||
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var apis []apiPO
|
||||
if err := tx.Where("id IN ?", ids).Find(&apis).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, api := range apis {
|
||||
if err := deletePoliciesForPath(tx, api.Path, api.Method); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Keep the legacy relation table clean for upgraded installations. It is
|
||||
// no longer the authorization source.
|
||||
if err := tx.Where("api_id IN ?", ids).Delete(&authorityAPIPO{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -106,7 +127,13 @@ func (r *apiRepo) ListAPIs(ctx context.Context, page, size int, q *biz.API) ([]*
|
||||
return nil, 0, err
|
||||
}
|
||||
if authority.ParentID != nil && *authority.ParentID != 0 {
|
||||
db = db.Where("id IN (?)", r.data.gormDB.WithContext(ctx).Model(&authorityAPIPO{}).Select("api_id").Where("authority_id = ?", actor.AuthorityID))
|
||||
// Strict mode filters API metadata by the actor's persisted
|
||||
// Casbin policies, matching on path/method rather than an API FK.
|
||||
db = db.Where(`EXISTS (
|
||||
SELECT 1 FROM casbin_rule cr
|
||||
WHERE cr.ptype = 'p' AND cr.v0 = ?
|
||||
AND cr.v1 = sys_apis.path AND cr.v2 = sys_apis.method
|
||||
)`, strconv.FormatUint(uint64(actor.AuthorityID), 10))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -121,7 +148,7 @@ func (r *apiRepo) ListAPIs(ctx context.Context, page, size int, q *biz.API) ([]*
|
||||
db = db.Where("api_group = ?", q.APIGroup)
|
||||
}
|
||||
if q.Method != "" {
|
||||
db = db.Where("method = ?", strings.ToUpper(q.Method))
|
||||
db = db.Where("method = ?", q.Method)
|
||||
}
|
||||
}
|
||||
var total int64
|
||||
@@ -154,34 +181,42 @@ func (r *apiRepo) ListAPIs(ctx context.Context, page, size int, q *biz.API) ([]*
|
||||
return out, total, nil
|
||||
}
|
||||
func (r *apiRepo) APIRoleIDs(ctx context.Context, path, method string) ([]uint, error) {
|
||||
var ids []uint
|
||||
err := r.data.gormDB.WithContext(ctx).Table("sys_authority_apis").
|
||||
Joins("JOIN sys_apis ON sys_apis.id = sys_authority_apis.api_id").
|
||||
Where("sys_apis.path = ? AND sys_apis.method = ? AND sys_apis.deleted_at IS NULL", path, strings.ToUpper(method)).
|
||||
Pluck("sys_authority_apis.authority_id", &ids).Error
|
||||
return ids, err
|
||||
rows := make([]casbinRulePO, 0)
|
||||
err := policyScope(r.data.gormDB.WithContext(ctx)).
|
||||
Where("v1 = ? AND v2 = ?", path, method).
|
||||
Find(&rows).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids := make([]uint, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
id, err := strconv.ParseUint(row.V0, 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
ids = append(ids, uint(id))
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
func (r *apiRepo) SetAPIRoles(ctx context.Context, path, method string, ids []uint) error {
|
||||
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var api apiPO
|
||||
if err := tx.Where("path = ? AND method = ?", path, strings.ToUpper(method)).First(&api).Error; err != nil {
|
||||
// A path/method need not be present in sys_apis when replacing
|
||||
// all matching policy rows. Do not validate or deduplicate authority IDs.
|
||||
if err := deletePoliciesForPath(tx, path, method); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("api_id = ?", api.ID).Delete(&authorityAPIPO{}).Error; err != nil {
|
||||
return err
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
links := make([]authorityAPIPO, 0, len(ids))
|
||||
rules := make([]casbinRulePO, 0, len(ids))
|
||||
for _, aid := range ids {
|
||||
links = append(links, authorityAPIPO{AuthorityID: aid, APIID: api.ID})
|
||||
rules = append(rules, newPolicyRule(aid, path, method))
|
||||
}
|
||||
if len(links) > 0 {
|
||||
return tx.Create(&links).Error
|
||||
}
|
||||
return nil
|
||||
return tx.Create(&rules).Error
|
||||
})
|
||||
}
|
||||
func (r *apiRepo) Authorize(ctx context.Context, aid uint, path, method string) (bool, error) {
|
||||
policies, err := r.PolicyPaths(ctx, aid)
|
||||
rows, err := policyRowsForAuthority(r.data.gormDB.WithContext(ctx), aid)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -192,7 +227,7 @@ 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)`)
|
||||
m = r.sub == p.sub && keyMatch2(r.obj, p.obj) && r.act == p.act`)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -201,22 +236,21 @@ m = r.sub == p.sub && keyMatch2(r.obj, p.obj) && regexMatch(r.act, p.act)`)
|
||||
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 {
|
||||
for _, policy := range rows {
|
||||
if _, err := enforcer.AddPolicy(subject, policy.V1, policy.V2); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
return enforcer.Enforce(subject, path, strings.ToUpper(method))
|
||||
return enforcer.Enforce(subject, path, method)
|
||||
}
|
||||
func (r *apiRepo) 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
|
||||
rows, err := policyRowsForAuthority(r.data.gormDB.WithContext(ctx), aid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]*biz.API, 0, len(pos))
|
||||
for _, po := range pos {
|
||||
out = append(out, apiFromPO(po))
|
||||
out := make([]*biz.API, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
out = append(out, &biz.API{Path: row.V1, Method: row.V2})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -237,35 +271,36 @@ func (r *apiRepo) SetPolicyPaths(ctx context.Context, aid uint, paths []*biz.API
|
||||
}
|
||||
allowedSet := make(map[string]bool, len(allowed))
|
||||
for _, item := range allowed {
|
||||
allowedSet[item.Path+"\x00"+strings.ToUpper(item.Method)] = true
|
||||
allowedSet[item.Path+"\x00"+item.Method] = true
|
||||
}
|
||||
for _, item := range paths {
|
||||
if !allowedSet[item.Path+"\x00"+strings.ToUpper(item.Method)] {
|
||||
if !allowedSet[item.Path+"\x00"+item.Method] {
|
||||
return errors.New("存在api不在权限列表中")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := deletePoliciesForAuthority(tx, aid); err != nil {
|
||||
return err
|
||||
}
|
||||
// Keep the legacy relation table clean for upgraded installations. It
|
||||
// is not consulted for authorization anymore.
|
||||
if err := tx.Where("authority_id = ?", aid).Delete(&authorityAPIPO{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
links := make([]authorityAPIPO, 0, len(paths))
|
||||
rules := make([]casbinRulePO, 0, len(paths))
|
||||
seen := make(map[string]struct{}, 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
|
||||
key := path.Path + "\x00" + path.Method
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
links = append(links, authorityAPIPO{AuthorityID: aid, APIID: po.ID})
|
||||
seen[key] = struct{}{}
|
||||
rules = append(rules, newPolicyRule(aid, path.Path, path.Method))
|
||||
}
|
||||
if len(links) > 0 {
|
||||
return tx.Create(&links).Error
|
||||
if len(rules) > 0 {
|
||||
return tx.Create(&rules).Error
|
||||
}
|
||||
return nil
|
||||
})
|
||||
@@ -273,7 +308,7 @@ func (r *apiRepo) SetPolicyPaths(ctx context.Context, aid uint, paths []*biz.API
|
||||
|
||||
func (r *apiRepo) 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 {
|
||||
if err := r.data.gormDB.WithContext(ctx).Find(&pos).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]*biz.API, 0, len(pos))
|
||||
@@ -283,26 +318,37 @@ func (r *apiRepo) IgnoredAPIs(ctx context.Context) ([]*biz.API, error) {
|
||||
return out, nil
|
||||
}
|
||||
func (r *apiRepo) SetAPIIgnored(ctx context.Context, path, method string, ignored bool) error {
|
||||
po := ignoredAPIPO{Path: path, Method: strings.ToUpper(method)}
|
||||
po := ignoredAPIPO{Path: path, Method: method}
|
||||
if ignored {
|
||||
return r.data.gormDB.WithContext(ctx).FirstOrCreate(&po, po).Error
|
||||
// The compatible endpoint creates an ignore row on every request (the table has no
|
||||
// path/method uniqueness constraint); retain that observable behavior.
|
||||
return r.data.gormDB.WithContext(ctx).Create(&po).Error
|
||||
}
|
||||
return r.data.gormDB.WithContext(ctx).Where("path = ? AND method = ?", po.Path, po.Method).Delete(&ignoredAPIPO{}).Error
|
||||
return r.data.gormDB.WithContext(ctx).Unscoped().Where("path = ? AND method = ?", po.Path, po.Method).Delete(&ignoredAPIPO{}).Error
|
||||
}
|
||||
func (r *apiRepo) 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 {
|
||||
if len(added) > 0 {
|
||||
pos := make([]apiPO, 0, len(added))
|
||||
for _, item := range added {
|
||||
pos = append(pos, apiPO{Path: item.Path, Method: item.Method, Description: item.Description, APIGroup: item.APIGroup})
|
||||
}
|
||||
// Insert the submitted newApis slice as-is. Do not silently
|
||||
// collapse repeated/existing path+method pairs here; sys_apis itself
|
||||
// intentionally has no uniqueness constraint.
|
||||
if err := tx.Create(&pos).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 {
|
||||
if err := tx.Model(&apiPO{}).Where("path = ? AND method = ?", item.Path, item.Method).Pluck("id", &ids).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if len(ids) > 0 {
|
||||
if err := deletePoliciesForPath(tx, item.Path, item.Method); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("api_id IN ?", ids).Delete(&authorityAPIPO{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+25
-27
@@ -42,10 +42,16 @@ func (jwtBlacklistPO) TableName() string { return "jwt_blacklists" }
|
||||
func (r *apiTokenRepo) 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 {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, false, errors.New("用户不存在")
|
||||
}
|
||||
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
|
||||
err := r.data.gormDB.WithContext(ctx).Model(&userAuthorityPO{}).
|
||||
Joins("JOIN sys_authorities ON sys_authorities.authority_id = sys_user_authority.sys_authority_authority_id").
|
||||
Where("sys_user_authority.sys_user_id = ? AND sys_user_authority.sys_authority_authority_id = ? AND sys_authorities.deleted_at IS NULL", userID, authorityID).
|
||||
Count(&count).Error
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
@@ -106,22 +112,20 @@ func (r *apiTokenRepo) DisableAPIToken(ctx context.Context, id uint) (string, er
|
||||
return po.Token, r.data.gormDB.WithContext(ctx).Model(&po).Update("status", false).Error
|
||||
}
|
||||
func (r *apiTokenRepo) DisableAndBlacklistAPIToken(ctx context.Context, id uint) error {
|
||||
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var po apiTokenPO
|
||||
if err := tx.First(&po, id).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
// Blacklist the token before disabling it while making both writes atomic.
|
||||
if err := tx.Create(&jwtBlacklistPO{JWT: po.Token}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Model(&po).Update("status", false).Error
|
||||
})
|
||||
// Persist the blacklist row first, then mark the
|
||||
// API-token record disabled. The two writes are intentionally separate in
|
||||
// the reference implementation, so a storage error has the same observable
|
||||
// result here.
|
||||
var po apiTokenPO
|
||||
if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.BlacklistToken(ctx, po.Token); err != nil {
|
||||
return err
|
||||
}
|
||||
return r.data.gormDB.WithContext(ctx).Model(&po).Update("status", false).Error
|
||||
}
|
||||
func (r *apiTokenRepo) 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 *apiTokenRepo) IsTokenDisabled(ctx context.Context, token string) (bool, error) {
|
||||
@@ -129,16 +133,10 @@ func (r *apiTokenRepo) IsTokenDisabled(ctx context.Context, token string) (bool,
|
||||
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
|
||||
// The JWT middleware consults only jwt_blacklists and lets JWT parsing
|
||||
// report expiry. API-token deletion writes this row before changing the
|
||||
// token status, so consulting status/ExpiresAt here would produce a
|
||||
// different error message and would make manually-disabled tokens behave
|
||||
// unlike tokens issued through the regular login flow.
|
||||
return blacklistCount > 0, nil
|
||||
}
|
||||
|
||||
+63
-29
@@ -3,8 +3,6 @@ package data
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"kra/internal/biz"
|
||||
|
||||
@@ -89,9 +87,11 @@ func (r *authorityAccessRepo) CreateAuthority(ctx context.Context, value *biz.Au
|
||||
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 {
|
||||
po := authorityPO{AuthorityID: value.AuthorityID, AuthorityName: value.AuthorityName, ParentID: value.ParentID, DataScope: value.DataScope, DefaultRouter: value.DefaultRouter}
|
||||
if err := tx.Create(&po).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
value.CreatedAt, value.UpdatedAt = po.CreatedAt, po.UpdatedAt
|
||||
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 {
|
||||
@@ -99,13 +99,12 @@ func (r *authorityAccessRepo) CreateAuthority(ctx context.Context, value *biz.Au
|
||||
}
|
||||
}
|
||||
defaults := []struct{ path, method string }{{"/menu/getMenu", "POST"}, {"/jwt/jsonInBlacklist", "POST"}, {"/base/login", "POST"}, {"/user/changePassword", "POST"}, {"/user/setUserAuthority", "POST"}, {"/user/getUserInfo", "GET"}, {"/user/setSelfInfo", "PUT"}, {"/fileUploadAndDownload/upload", "POST"}, {"/sysDictionary/findSysDictionary", "GET"}}
|
||||
rules := make([]casbinRulePO, 0, len(defaults))
|
||||
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
|
||||
}
|
||||
}
|
||||
rules = append(rules, newPolicyRule(value.AuthorityID, item.path, item.method))
|
||||
}
|
||||
if err := tx.Create(&rules).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
@@ -114,13 +113,18 @@ func (r *authorityAccessRepo) CopyAuthority(ctx context.Context, sourceID uint,
|
||||
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
config := r.data.runtime.Admin()
|
||||
actor, hasActor := biz.ActorFromContext(ctx)
|
||||
strict := hasActor && actor.AuthorityID != 888 && config != nil && config.System != nil && config.System.UseStrictAuth
|
||||
// Strict-authority checks apply to every actor. A root actor is
|
||||
// allowed to create/copy a role directly below itself; non-root actors
|
||||
// may target themselves' descendants. Do not special-case 888 here:
|
||||
// the configured strict-auth switch is the source of truth.
|
||||
strict := hasActor && config != nil && config.System != nil && config.System.UseStrictAuth
|
||||
if strict {
|
||||
allowed, err := r.strictAuthorityIDs(ctx, actor.AuthorityID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if value.ParentID == nil || !allowed[*value.ParentID] {
|
||||
parentAllowed := value.ParentID != nil && (allowed[*value.ParentID] || *value.ParentID == actor.AuthorityID)
|
||||
if !parentAllowed {
|
||||
return errors.New("严格角色模式下只能在自己的角色树中复制角色")
|
||||
}
|
||||
}
|
||||
@@ -131,9 +135,11 @@ func (r *authorityAccessRepo) CopyAuthority(ctx context.Context, sourceID uint,
|
||||
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 {
|
||||
po := authorityPO{AuthorityID: value.AuthorityID, AuthorityName: value.AuthorityName, ParentID: value.ParentID, DataScope: value.DataScope, DefaultRouter: value.DefaultRouter}
|
||||
if err := tx.Create(&po).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
value.CreatedAt, value.UpdatedAt = po.CreatedAt, po.UpdatedAt
|
||||
copyLinks := func(table string, destination any, columns map[string]any) error {
|
||||
return tx.Table(table).Where(columns).Find(destination).Error
|
||||
}
|
||||
@@ -149,24 +155,31 @@ func (r *authorityAccessRepo) CopyAuthority(ctx context.Context, sourceID uint,
|
||||
return err
|
||||
}
|
||||
}
|
||||
var apis []authorityAPIPO
|
||||
if err := copyLinks("sys_authority_apis", &apis, map[string]any{"authority_id": sourceID}); err != nil {
|
||||
apis, err := policyRowsForAuthority(tx, sourceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := range apis {
|
||||
copiedRules := make([]casbinRulePO, 0, len(apis))
|
||||
seenPolicies := make(map[string]struct{}, len(apis))
|
||||
for _, api := range apis {
|
||||
if strict {
|
||||
var allowedCount int64
|
||||
if err := tx.Model(&authorityAPIPO{}).Where("authority_id = ? AND api_id = ?", actor.AuthorityID, apis[i].APIID).Count(&allowedCount).Error; err != nil {
|
||||
allowed, err := policyExists(tx, actor.AuthorityID, api.V1, api.V2)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if allowedCount == 0 {
|
||||
if !allowed {
|
||||
return errors.New("严格角色模式下不能复制当前角色未拥有的 API")
|
||||
}
|
||||
}
|
||||
apis[i].AuthorityID = value.AuthorityID
|
||||
key := api.V1 + "\x00" + api.V2
|
||||
if _, ok := seenPolicies[key]; ok {
|
||||
continue
|
||||
}
|
||||
seenPolicies[key] = struct{}{}
|
||||
copiedRules = append(copiedRules, newPolicyRule(value.AuthorityID, api.V1, api.V2))
|
||||
}
|
||||
if len(apis) > 0 {
|
||||
if err := tx.Create(&apis).Error; err != nil {
|
||||
if len(copiedRules) > 0 {
|
||||
if err := tx.Create(&copiedRules).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -225,13 +238,18 @@ func (r *authorityAccessRepo) DeleteAuthority(ctx context.Context, id uint) erro
|
||||
if err := tx.Where("sys_authority_authority_id = ?", id).Delete(&authorityMenuPO{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := deletePoliciesForAuthority(tx, id); 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
|
||||
}
|
||||
return tx.Delete(&authorityPO{}, "authority_id = ?", id).Error
|
||||
// The compatible delete removes the role row permanently with Unscoped().Delete,
|
||||
// so reusing an authority ID after deletion behaves the same way.
|
||||
return tx.Unscoped().Delete(&authorityPO{}, "authority_id = ?", id).Error
|
||||
})
|
||||
}
|
||||
func (r *authorityAccessRepo) ListAuthorities(ctx context.Context) ([]*biz.Authority, error) {
|
||||
@@ -359,19 +377,35 @@ func (r *authorityAccessRepo) ResolveDataScope(ctx context.Context, authorityID,
|
||||
ids = append(ids, id)
|
||||
}
|
||||
if identity.Scope == 2 && len(ids) > 0 {
|
||||
// Match the administration subtree-union semantics: derive the visible set from
|
||||
// parent_id edges at read time, rather than trusting the denormalized
|
||||
// ancestors string (which may be stale after a department move).
|
||||
var departments []departmentPO
|
||||
_ = r.data.gormDB.WithContext(ctx).Find(&departments).Error
|
||||
_ = r.data.gormDB.WithContext(ctx).Select("id", "parent_id").Find(&departments).Error
|
||||
children := make(map[uint][]uint, len(departments))
|
||||
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
|
||||
children[department.ParentID] = append(children[department.ParentID], department.ID)
|
||||
}
|
||||
queue := make([]uint, 0, len(selected))
|
||||
for id := range selected {
|
||||
queue = append(queue, id)
|
||||
}
|
||||
visited := make(map[uint]struct{}, len(selected))
|
||||
for len(queue) > 0 {
|
||||
current := queue[0]
|
||||
queue = queue[1:]
|
||||
if _, ok := visited[current]; ok {
|
||||
continue
|
||||
}
|
||||
visited[current] = struct{}{}
|
||||
for _, child := range children[current] {
|
||||
if _, ok := visited[child]; !ok {
|
||||
queue = append(queue, child)
|
||||
}
|
||||
}
|
||||
}
|
||||
ids = ids[:0]
|
||||
for id := range selected {
|
||||
for id := range visited {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
} else if identity.Scope == 5 {
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// casbinRulePO mirrors the gorm-adapter CasbinRule storage shape. API
|
||||
// metadata (sys_apis) and authorization policy are intentionally independent:
|
||||
// a policy may refer to a route that has not been registered in sys_apis yet.
|
||||
// Keep this type private so the storage representation does not leak above
|
||||
// the data layer.
|
||||
type casbinRulePO struct {
|
||||
ID uint `gorm:"primaryKey;autoIncrement"`
|
||||
Ptype string `gorm:"size:100"`
|
||||
V0 string `gorm:"size:100"`
|
||||
V1 string `gorm:"size:100"`
|
||||
V2 string `gorm:"size:100"`
|
||||
V3 string `gorm:"size:100"`
|
||||
V4 string `gorm:"size:100"`
|
||||
V5 string `gorm:"size:100"`
|
||||
}
|
||||
|
||||
func (casbinRulePO) TableName() string { return "casbin_rule" }
|
||||
|
||||
func newPolicyRule(authorityID uint, path, method string) casbinRulePO {
|
||||
return casbinRulePO{
|
||||
Ptype: "p",
|
||||
V0: strconv.FormatUint(uint64(authorityID), 10),
|
||||
V1: path,
|
||||
V2: method,
|
||||
}
|
||||
}
|
||||
|
||||
func policyScope(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("ptype = ?", "p")
|
||||
}
|
||||
|
||||
func deletePoliciesForAuthority(db *gorm.DB, authorityID uint) error {
|
||||
return policyScope(db).Where("v0 = ?", strconv.FormatUint(uint64(authorityID), 10)).Delete(&casbinRulePO{}).Error
|
||||
}
|
||||
|
||||
func deletePoliciesForPath(db *gorm.DB, path, method string) error {
|
||||
return policyScope(db).Where("v1 = ? AND v2 = ?", path, method).Delete(&casbinRulePO{}).Error
|
||||
}
|
||||
|
||||
func policyRowsForAuthority(db *gorm.DB, authorityID uint) ([]casbinRulePO, error) {
|
||||
var rows []casbinRulePO
|
||||
err := policyScope(db).Where("v0 = ?", strconv.FormatUint(uint64(authorityID), 10)).Find(&rows).Error
|
||||
return rows, err
|
||||
}
|
||||
|
||||
func policyExists(db *gorm.DB, authorityID uint, path, method string) (bool, error) {
|
||||
var count int64
|
||||
err := policyScope(db).
|
||||
Where("v0 = ? AND v1 = ? AND v2 = ?", strconv.FormatUint(uint64(authorityID), 10), path, method).
|
||||
Count(&count).Error
|
||||
return count > 0, err
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package data
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"kra/internal/conf"
|
||||
|
||||
@@ -33,7 +34,16 @@ func (r *initializationRepo) ConfigurationJSON() (json.RawMessage, error) {
|
||||
if adminConfig != nil {
|
||||
admin["routerPrefix"] = adminConfig.RouterPrefix
|
||||
if adminConfig.System != nil {
|
||||
admin["system"] = map[string]any{"useRedis": adminConfig.System.UseRedis, "useMultipoint": adminConfig.System.UseMultipoint, "useStrictAuth": adminConfig.System.UseStrictAuth, "disableAutoMigrate": adminConfig.System.DisableAutoMigrate, "useMongo": adminConfig.System.UseMongo}
|
||||
admin["system"] = map[string]any{
|
||||
"useRedis": adminConfig.System.UseRedis,
|
||||
"useMultipoint": adminConfig.System.UseMultipoint,
|
||||
"useStrictAuth": adminConfig.System.UseStrictAuth,
|
||||
"disableAutoMigrate": adminConfig.System.DisableAutoMigrate,
|
||||
"useMongo": adminConfig.System.UseMongo,
|
||||
"addr": adminConfig.System.Addr,
|
||||
"iplimitCount": adminConfig.System.IplimitCount,
|
||||
"iplimitTime": adminConfig.System.IplimitTime,
|
||||
}
|
||||
}
|
||||
if adminConfig.Jwt != nil {
|
||||
admin["jwt"] = map[string]any{"signingKey": "******", "expiresTime": durationString(adminConfig.Jwt.ExpiresTime), "bufferTime": durationString(adminConfig.Jwt.BufferTime), "issuer": adminConfig.Jwt.Issuer}
|
||||
@@ -45,7 +55,11 @@ func (r *initializationRepo) ConfigurationJSON() (json.RawMessage, error) {
|
||||
admin["local"] = map[string]any{"storePath": adminConfig.Local.StorePath, "pathPrefix": adminConfig.Local.PathPrefix}
|
||||
}
|
||||
if adminConfig.Media != nil {
|
||||
admin["media"] = map[string]any{"sessionTtl": adminConfig.Media.SessionTtl, "maxFileSize": adminConfig.Media.MaxFileSize}
|
||||
chunkDir := adminConfig.Media.ChunkDir
|
||||
if chunkDir == "" {
|
||||
chunkDir = "uploads/chunks"
|
||||
}
|
||||
admin["media"] = map[string]any{"sessionTtl": adminConfig.Media.SessionTtl, "maxFileSize": adminConfig.Media.MaxFileSize, "chunkDir": chunkDir}
|
||||
}
|
||||
if adminConfig.Email != nil {
|
||||
email = map[string]any{"to": adminConfig.Email.To, "from": adminConfig.Email.From, "host": adminConfig.Email.Host, "secret": "******", "nickname": adminConfig.Email.Nickname, "port": adminConfig.Email.Port, "is-ssl": adminConfig.Email.IsSsl, "is-loginauth": adminConfig.Email.IsLoginAuth}
|
||||
@@ -55,6 +69,13 @@ func (r *initializationRepo) ConfigurationJSON() (json.RawMessage, error) {
|
||||
maskStorageSecrets(storage)
|
||||
admin["storage"] = storage
|
||||
}
|
||||
disks := make([]map[string]any, 0, len(adminConfig.DiskList))
|
||||
for _, disk := range adminConfig.DiskList {
|
||||
if disk != nil {
|
||||
disks = append(disks, map[string]any{"mount_point": disk.MountPoint})
|
||||
}
|
||||
}
|
||||
admin["disk_list"] = disks
|
||||
if adminConfig.Zap != nil {
|
||||
admin["zap"] = adminConfig.Zap
|
||||
}
|
||||
@@ -65,7 +86,12 @@ func (r *initializationRepo) ConfigurationJSON() (json.RawMessage, error) {
|
||||
admin["app"] = adminConfig.App
|
||||
}
|
||||
}
|
||||
maskDataSecrets(dataConfig)
|
||||
// Never mask secrets on the live runtime object. ConfigurationJSON is a
|
||||
// read-only operation; mutating dataConfig here would replace the actual
|
||||
// database/Redis/Mongo credentials with "******" and make the next reload
|
||||
// fail. Work on a clone, just as we already do for adminConfig.
|
||||
safeData := cloneDataConfig(dataConfig)
|
||||
maskDataSecrets(safeData)
|
||||
safeAdmin := cloneAdminConfig(adminConfig)
|
||||
if safeAdmin != nil {
|
||||
if safeAdmin.Jwt != nil {
|
||||
@@ -77,8 +103,8 @@ func (r *initializationRepo) ConfigurationJSON() (json.RawMessage, error) {
|
||||
maskStorageSecrets(safeAdmin.Storage)
|
||||
}
|
||||
dataMap := map[string]any{}
|
||||
if dataConfig != nil {
|
||||
raw, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(dataConfig)
|
||||
if safeData != nil {
|
||||
raw, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(safeData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -86,7 +112,7 @@ func (r *initializationRepo) ConfigurationJSON() (json.RawMessage, error) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
config := gvaConfiguration(dataConfig, safeAdmin)
|
||||
config := compatibleConfiguration(safeData, safeAdmin)
|
||||
config["admin"], config["email"], config["data"] = admin, email, dataMap
|
||||
return json.Marshal(map[string]any{"config": config})
|
||||
}
|
||||
@@ -98,7 +124,7 @@ func (r *initializationRepo) SaveConfigurationJSON(ctx context.Context, raw json
|
||||
}
|
||||
nextData := cloneDataConfig(currentData)
|
||||
nextAdmin := proto.Clone(currentAdmin).(*conf.AdminBackend)
|
||||
if err := applyGVAConfiguration(raw, nextData, nextAdmin); err != nil {
|
||||
if err := applyCompatibleConfiguration(raw, nextData, nextAdmin); err != nil {
|
||||
return err
|
||||
}
|
||||
var value configurationEnvelope
|
||||
@@ -107,18 +133,14 @@ func (r *initializationRepo) SaveConfigurationJSON(ctx context.Context, raw json
|
||||
}
|
||||
options := protojson.UnmarshalOptions{DiscardUnknown: true}
|
||||
if len(value.Data) > 0 && string(value.Data) != "null" {
|
||||
patch := &conf.Data{}
|
||||
if err := options.Unmarshal(value.Data, patch); err != nil {
|
||||
if err := mergeConfigurationMessage(nextData, value.Data, options); err != nil {
|
||||
return err
|
||||
}
|
||||
applyDataPatch(nextData, patch)
|
||||
}
|
||||
if len(value.Admin) > 0 && string(value.Admin) != "null" {
|
||||
patch := &conf.AdminBackend{}
|
||||
if err := options.Unmarshal(value.Admin, patch); err != nil {
|
||||
if err := mergeConfigurationMessage(nextAdmin, value.Admin, options); err != nil {
|
||||
return err
|
||||
}
|
||||
applyAdminPatch(nextAdmin, patch)
|
||||
}
|
||||
preserveDataSecrets(nextData, currentData)
|
||||
preserveAdminSecrets(nextAdmin, currentAdmin)
|
||||
@@ -216,6 +238,80 @@ func refreshDatabaseSource(database *conf.Data_Database) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// mergeConfigurationMessage applies a JSON object as a partial update while
|
||||
// retaining fields that were not sent by the caller. protojson.Unmarshal is
|
||||
// intentionally not used directly here because its reset semantics would
|
||||
// clear unrelated configuration sections when the management page submits a
|
||||
// partial object.
|
||||
func mergeConfigurationMessage(target proto.Message, patch json.RawMessage, options protojson.UnmarshalOptions) error {
|
||||
currentRaw, err := protojson.MarshalOptions{UseProtoNames: false}.Marshal(target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var current map[string]any
|
||||
if err = json.Unmarshal(currentRaw, ¤t); err != nil {
|
||||
return err
|
||||
}
|
||||
var incoming map[string]any
|
||||
if err = json.Unmarshal(patch, &incoming); err != nil {
|
||||
return err
|
||||
}
|
||||
incoming = normalizeJSONKeys(incoming).(map[string]any)
|
||||
mergeJSONObjects(current, incoming)
|
||||
merged, err := json.Marshal(current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return options.Unmarshal(merged, target)
|
||||
}
|
||||
|
||||
func normalizeJSONKeys(value any) any {
|
||||
switch item := value.(type) {
|
||||
case map[string]any:
|
||||
result := make(map[string]any, len(item))
|
||||
for key, nested := range item {
|
||||
result[snakeToLowerCamel(key)] = normalizeJSONKeys(nested)
|
||||
}
|
||||
return result
|
||||
case []any:
|
||||
result := make([]any, len(item))
|
||||
for index, nested := range item {
|
||||
result[index] = normalizeJSONKeys(nested)
|
||||
}
|
||||
return result
|
||||
default:
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
func snakeToLowerCamel(value string) string {
|
||||
if !strings.Contains(value, "_") {
|
||||
return value
|
||||
}
|
||||
parts := strings.Split(value, "_")
|
||||
result := parts[0]
|
||||
for _, part := range parts[1:] {
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
result += strings.ToUpper(part[:1]) + part[1:]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func mergeJSONObjects(target, patch map[string]any) {
|
||||
for key, value := range patch {
|
||||
incoming, isObject := value.(map[string]any)
|
||||
if isObject {
|
||||
if existing, ok := target[key].(map[string]any); ok {
|
||||
mergeJSONObjects(existing, incoming)
|
||||
continue
|
||||
}
|
||||
}
|
||||
target[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
func durationString(value *durationpb.Duration) string {
|
||||
if value == nil {
|
||||
return "0s"
|
||||
@@ -223,69 +319,17 @@ func durationString(value *durationpb.Duration) string {
|
||||
return value.AsDuration().String()
|
||||
}
|
||||
|
||||
func applyDataPatch(target, patch *conf.Data) {
|
||||
if patch.Database != nil {
|
||||
target.Database = patch.Database
|
||||
}
|
||||
if patch.Redis != nil {
|
||||
target.Redis = patch.Redis
|
||||
}
|
||||
if patch.Mongo != nil {
|
||||
target.Mongo = patch.Mongo
|
||||
}
|
||||
if patch.DatabaseList != nil {
|
||||
target.DatabaseList = patch.DatabaseList
|
||||
}
|
||||
if patch.RedisList != nil {
|
||||
target.RedisList = patch.RedisList
|
||||
}
|
||||
}
|
||||
|
||||
func applyAdminPatch(target, patch *conf.AdminBackend) {
|
||||
target.RouterPrefix = patch.RouterPrefix
|
||||
if patch.System != nil {
|
||||
if target.System != nil {
|
||||
patch.System.Addr = target.System.Addr
|
||||
patch.System.IplimitCount = target.System.IplimitCount
|
||||
patch.System.IplimitTime = target.System.IplimitTime
|
||||
}
|
||||
target.System = patch.System
|
||||
}
|
||||
if patch.Jwt != nil {
|
||||
target.Jwt = patch.Jwt
|
||||
}
|
||||
if patch.Captcha != nil {
|
||||
target.Captcha = patch.Captcha
|
||||
}
|
||||
if patch.Local != nil {
|
||||
target.Local = patch.Local
|
||||
}
|
||||
if patch.Media != nil {
|
||||
if patch.Media.ChunkDir == "" && target.Media != nil {
|
||||
patch.Media.ChunkDir = target.Media.ChunkDir
|
||||
}
|
||||
target.Media = patch.Media
|
||||
}
|
||||
if patch.Storage != nil {
|
||||
target.Storage = patch.Storage
|
||||
}
|
||||
if patch.Zap != nil {
|
||||
target.Zap = patch.Zap
|
||||
}
|
||||
if patch.Cors != nil {
|
||||
target.Cors = patch.Cors
|
||||
}
|
||||
if patch.App != nil {
|
||||
target.App = patch.App
|
||||
}
|
||||
}
|
||||
|
||||
func maskDataSecrets(value *conf.Data) {
|
||||
if value == nil {
|
||||
return
|
||||
}
|
||||
if value.Database != nil {
|
||||
value.Database.Password = "******"
|
||||
// Source may contain an embedded password (for example a MySQL DSN).
|
||||
// The management response must never expose it. Structured database
|
||||
// fields are retained and refreshDatabaseSources rebuilds Source when
|
||||
// the configuration is saved.
|
||||
value.Database.Source = ""
|
||||
}
|
||||
if value.Redis != nil {
|
||||
value.Redis.Password = "******"
|
||||
@@ -296,6 +340,7 @@ func maskDataSecrets(value *conf.Data) {
|
||||
for _, item := range value.DatabaseList {
|
||||
if item != nil {
|
||||
item.Password = "******"
|
||||
item.Source = ""
|
||||
}
|
||||
}
|
||||
for _, item := range value.RedisList {
|
||||
@@ -312,20 +357,54 @@ func preserveDataSecrets(next, current *conf.Data) {
|
||||
if next.Database != nil && current.Database != nil && maskedSecret(next.Database.Password) {
|
||||
next.Database.Password = current.Database.Password
|
||||
}
|
||||
if next.Database != nil && current.Database != nil && next.Database.Source == "" {
|
||||
next.Database.Source = current.Database.Source
|
||||
}
|
||||
if next.Redis != nil && current.Redis != nil && maskedSecret(next.Redis.Password) {
|
||||
next.Redis.Password = current.Redis.Password
|
||||
}
|
||||
if next.Mongo != nil && current.Mongo != nil && maskedSecret(next.Mongo.Password) {
|
||||
next.Mongo.Password = current.Mongo.Password
|
||||
}
|
||||
currentDatabases := make(map[string]*conf.Data_Database, len(current.DatabaseList))
|
||||
for _, item := range current.DatabaseList {
|
||||
if item != nil && item.AliasName != "" {
|
||||
currentDatabases[item.AliasName] = item
|
||||
}
|
||||
}
|
||||
for i, item := range next.DatabaseList {
|
||||
if item != nil && i < len(current.DatabaseList) && current.DatabaseList[i] != nil && maskedSecret(item.Password) {
|
||||
item.Password = current.DatabaseList[i].Password
|
||||
var previous *conf.Data_Database
|
||||
if item != nil && item.AliasName != "" {
|
||||
previous = currentDatabases[item.AliasName]
|
||||
}
|
||||
if previous == nil && i < len(current.DatabaseList) {
|
||||
previous = current.DatabaseList[i]
|
||||
}
|
||||
if item != nil && previous != nil {
|
||||
if maskedSecret(item.Password) {
|
||||
item.Password = previous.Password
|
||||
}
|
||||
if item.Source == "" {
|
||||
item.Source = previous.Source
|
||||
}
|
||||
}
|
||||
}
|
||||
currentRedis := make(map[string]*conf.Data_Redis, len(current.RedisList))
|
||||
for _, item := range current.RedisList {
|
||||
if item != nil && item.Name != "" {
|
||||
currentRedis[item.Name] = item
|
||||
}
|
||||
}
|
||||
for i, item := range next.RedisList {
|
||||
if item != nil && i < len(current.RedisList) && current.RedisList[i] != nil && maskedSecret(item.Password) {
|
||||
item.Password = current.RedisList[i].Password
|
||||
var previous *conf.Data_Redis
|
||||
if item != nil && item.Name != "" {
|
||||
previous = currentRedis[item.Name]
|
||||
}
|
||||
if previous == nil && i < len(current.RedisList) {
|
||||
previous = current.RedisList[i]
|
||||
}
|
||||
if item != nil && previous != nil && maskedSecret(item.Password) {
|
||||
item.Password = previous.Password
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -350,6 +429,9 @@ func objectStores(storage *conf.AdminBackend_Storage) []*conf.AdminBackend_Objec
|
||||
}
|
||||
|
||||
func maskStorageSecrets(storage *conf.AdminBackend_Storage) {
|
||||
if storage == nil {
|
||||
return
|
||||
}
|
||||
if storage.Qiniu != nil {
|
||||
storage.Qiniu.SecretKey = "******"
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ func (r *auditRecorderRepo) RecordDataAccess(ctx context.Context, v *biz.DataAcc
|
||||
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}
|
||||
return &biz.DataAccessLog{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, 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 *auditQueryRepo) ListDataAccess(ctx context.Context, page, size int, q *biz.DataAccessLog) ([]*biz.DataAccessLog, int64, error) {
|
||||
db := r.data.gormDB.WithContext(ctx).Model(&dataAccessLogPO{})
|
||||
@@ -37,9 +37,6 @@ func (r *auditQueryRepo) ListDataAccess(ctx context.Context, page, size int, q *
|
||||
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 {
|
||||
@@ -56,5 +53,5 @@ func (r *auditQueryRepo) ListDataAccess(ctx context.Context, page, size int, q *
|
||||
return out, total, nil
|
||||
}
|
||||
func (r *auditQueryRepo) DeleteDataAccess(ctx context.Context, ids []uint) error {
|
||||
return r.data.gormDB.WithContext(ctx).Delete(&dataAccessLogPO{}, ids).Error
|
||||
return r.data.gormDB.WithContext(ctx).Delete(&dataAccessLogPO{}, "id IN ?", ids).Error
|
||||
}
|
||||
|
||||
@@ -77,6 +77,9 @@ func (r *departmentRepo) CreateDepartment(ctx context.Context, v *biz.Department
|
||||
if v.ParentID != 0 {
|
||||
var parent departmentPO
|
||||
if err := r.data.gormDB.WithContext(ctx).First(&parent, v.ParentID).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New("父部门不存在")
|
||||
}
|
||||
return err
|
||||
}
|
||||
v.Ancestors = strings.Trim(parent.Ancestors+","+strconv.FormatUint(uint64(parent.ID), 10), ",")
|
||||
@@ -91,6 +94,9 @@ func (r *departmentRepo) UpdateDepartment(ctx context.Context, v *biz.Department
|
||||
if v.ParentID != 0 {
|
||||
var parent departmentPO
|
||||
if err := r.data.gormDB.WithContext(ctx).First(&parent, v.ParentID).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New("父部门不存在")
|
||||
}
|
||||
return err
|
||||
}
|
||||
v.Ancestors = strings.Trim(parent.Ancestors+","+strconv.FormatUint(uint64(parent.ID), 10), ",")
|
||||
@@ -108,6 +114,12 @@ func (r *departmentRepo) DeleteDepartment(ctx context.Context, id uint) error {
|
||||
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
|
||||
}
|
||||
|
||||
+80
-15
@@ -23,7 +23,7 @@ type dictionaryPO struct {
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||
Name string
|
||||
Type string
|
||||
Status bool
|
||||
Status *bool
|
||||
Desc string
|
||||
ParentID *uint
|
||||
}
|
||||
@@ -38,7 +38,7 @@ type dictionaryDetailPO struct {
|
||||
Label string
|
||||
Value string
|
||||
Extend string
|
||||
Status bool
|
||||
Status *bool
|
||||
Sort int
|
||||
DictionaryID uint `gorm:"column:sys_dictionary_id;index"`
|
||||
ParentID *uint
|
||||
@@ -59,11 +59,18 @@ func parameterFromPO(po parameterPO) *biz.SystemParameter {
|
||||
}
|
||||
|
||||
func (r *dictionaryRepo) CreateDictionary(ctx context.Context, v *biz.Dictionary) error {
|
||||
var existing dictionaryPO
|
||||
if err := r.data.gormDB.WithContext(ctx).Where("type = ?", v.Type).First(&existing).Error; err == nil {
|
||||
return errors.New("存在相同的type,不允许创建")
|
||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
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
|
||||
v.CreatedAt, v.UpdatedAt = po.CreatedAt, po.UpdatedAt
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -108,7 +115,46 @@ func (r *dictionaryRepo) ImportDictionary(ctx context.Context, dictionary *biz.D
|
||||
})
|
||||
}
|
||||
func (r *dictionaryRepo) 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
|
||||
db := r.data.gormDB.WithContext(ctx)
|
||||
var current dictionaryPO
|
||||
if err := db.Where("id = ?", v.ID).First(¤t).Error; err != nil {
|
||||
return errors.New("查询字典数据失败")
|
||||
}
|
||||
if current.Type != v.Type {
|
||||
var duplicate dictionaryPO
|
||||
if err := db.Where("type = ?", v.Type).First(&duplicate).Error; err == nil {
|
||||
return errors.New("存在相同的type,不允许创建")
|
||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if v.ParentID != nil && *v.ParentID != 0 {
|
||||
if err := r.checkDictionaryCircularReference(ctx, v.ID, *v.ParentID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return db.Model(¤t).Updates(map[string]any{"name": v.Name, "type": v.Type, "status": v.Status, "desc": v.Desc, "parent_id": v.ParentID}).Error
|
||||
}
|
||||
|
||||
// checkDictionaryCircularReference mirrors the compatible recursive parent-chain
|
||||
// check. A missing parent is allowed (the reference implementation permits a
|
||||
// dangling parent ID); only a self-reference or an ancestor cycle is rejected.
|
||||
func (r *dictionaryRepo) checkDictionaryCircularReference(ctx context.Context, currentID, parentID uint) error {
|
||||
if currentID == parentID {
|
||||
return errors.New("不能将字典设置为自己的父级")
|
||||
}
|
||||
var parent dictionaryPO
|
||||
err := r.data.gormDB.WithContext(ctx).Where("id = ?", parentID).First(&parent).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if parent.ParentID != nil && *parent.ParentID != 0 {
|
||||
return r.checkDictionaryCircularReference(ctx, currentID, *parent.ParentID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (r *dictionaryRepo) DeleteDictionary(ctx context.Context, id uint) error {
|
||||
db := r.data.gormDB.WithContext(ctx)
|
||||
@@ -141,7 +187,7 @@ func (r *dictionaryRepo) FindDictionary(ctx context.Context, id uint, typ string
|
||||
if err == nil {
|
||||
filtered := out.Details[:0]
|
||||
for _, item := range out.Details {
|
||||
if item.Status {
|
||||
if item.Status != nil && *item.Status {
|
||||
filtered = append(filtered, item)
|
||||
}
|
||||
}
|
||||
@@ -226,7 +272,7 @@ func (r *dictionaryRepo) CreateDictionaryDetail(ctx context.Context, v *biz.Dict
|
||||
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 {
|
||||
if v.ParentID != nil {
|
||||
var parent dictionaryDetailPO
|
||||
if err := r.data.gormDB.WithContext(ctx).First(&parent, *v.ParentID).Error; err != nil {
|
||||
return err
|
||||
@@ -244,40 +290,59 @@ func (r *dictionaryRepo) CreateDictionaryDetail(ctx context.Context, v *biz.Dict
|
||||
}
|
||||
func (r *dictionaryRepo) 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 {
|
||||
db := r.data.gormDB.WithContext(ctx)
|
||||
if err := db.First(&po, v.ID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
level, path := 0, ""
|
||||
if v.ParentID != nil && *v.ParentID != 0 {
|
||||
if v.ParentID != nil {
|
||||
if *v.ParentID == v.ID {
|
||||
return errors.New("不能将自身设为父级")
|
||||
return errors.New("不能将字典详情设置为自己或其子项的父级")
|
||||
}
|
||||
var parent dictionaryDetailPO
|
||||
if err := r.data.gormDB.WithContext(ctx).First(&parent, *v.ParentID).Error; err != nil {
|
||||
if err := db.First(&parent, *v.ParentID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.Contains(","+parent.Path+",", fmt.Sprintf(",%d,", v.ID)) {
|
||||
return errors.New("不能移动到自己的子级")
|
||||
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 {
|
||||
return db.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
|
||||
return r.updateDictionaryDetailChildren(tx, v.ID, level, path)
|
||||
})
|
||||
}
|
||||
|
||||
// updateDictionaryDetailChildren recomputes descendants after a move, as the
|
||||
// The service does this recursively. Updating level as well as path matters when a
|
||||
// subtree is moved beneath a different depth.
|
||||
func (r *dictionaryRepo) updateDictionaryDetailChildren(tx *gorm.DB, parentID uint, parentLevel int, parentPath string) error {
|
||||
var children []dictionaryDetailPO
|
||||
if err := tx.Where("parent_id = ?", parentID).Order("sort,id").Find(&children).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, child := range children {
|
||||
childPath := strings.Trim(strings.Join([]string{parentPath, fmt.Sprint(parentID)}, ","), ",")
|
||||
if err := tx.Model(&dictionaryDetailPO{}).Where("id = ?", child.ID).Updates(map[string]any{"level": parentLevel + 1, "path": childPath}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.updateDictionaryDetailChildren(tx, child.ID, parentLevel+1, childPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (r *dictionaryRepo) 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 errors.New("该字典详情下还有子项,无法删除")
|
||||
}
|
||||
return r.data.gormDB.WithContext(ctx).Delete(&dictionaryDetailPO{}, id).Error
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ type errorRecordPO struct {
|
||||
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}
|
||||
return &biz.ErrorRecord{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, Form: v.Form, Info: v.Info, Level: v.Level, RequestID: v.RequestID, TraceID: v.TraceID, Solution: v.Solution, Status: v.Status}
|
||||
}
|
||||
func (r *auditRecorderRepo) CreateError(ctx context.Context, v *biz.ErrorRecord) error {
|
||||
if v.Status == "" {
|
||||
|
||||
+51
-15
@@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
"kra/internal/biz"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -31,8 +32,11 @@ type exportTemplatePO struct {
|
||||
func (exportTemplatePO) TableName() string { return "sys_export_templates" }
|
||||
|
||||
type exportConditionPO struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
TemplateID string `gorm:"index"`
|
||||
ID uint `gorm:"primaryKey"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||
TemplateID string `gorm:"index"`
|
||||
From string
|
||||
Column string
|
||||
Operator string
|
||||
@@ -41,9 +45,12 @@ type exportConditionPO struct {
|
||||
func (exportConditionPO) TableName() string { return "sys_export_template_condition" }
|
||||
|
||||
type exportJoinPO struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
TemplateID string `gorm:"index"`
|
||||
Join string `gorm:"column:joins"`
|
||||
ID uint `gorm:"primaryKey"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||
TemplateID string `gorm:"index"`
|
||||
Join string `gorm:"column:joins"`
|
||||
Table string
|
||||
On string `gorm:"column:on"`
|
||||
}
|
||||
@@ -56,17 +63,17 @@ func NewExportRepo(data *Data) biz.ExportRepo { return &exportRepo{data: data} }
|
||||
func exportFromPO(po exportTemplatePO, conditions []exportConditionPO, joins []exportJoinPO) *biz.ExportTemplate {
|
||||
v := &biz.ExportTemplate{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, DBName: po.DBName, Name: po.Name, TableName: po.DBTableName, TemplateID: po.TemplateID, TemplateInfo: po.TemplateInfo, SQL: po.SQL, ImportSQL: po.ImportSQL, Limit: po.Limit, Order: po.Order}
|
||||
for _, x := range conditions {
|
||||
v.Conditions = append(v.Conditions, biz.ExportCondition{From: x.From, Column: x.Column, Operator: x.Operator})
|
||||
v.Conditions = append(v.Conditions, biz.ExportCondition{ID: x.ID, CreatedAt: x.CreatedAt, UpdatedAt: x.UpdatedAt, TemplateID: x.TemplateID, From: x.From, Column: x.Column, Operator: x.Operator})
|
||||
}
|
||||
for _, x := range joins {
|
||||
v.Joins = append(v.Joins, biz.ExportJoin{Join: x.Join, Table: x.Table, On: x.On})
|
||||
v.Joins = append(v.Joins, biz.ExportJoin{ID: x.ID, CreatedAt: x.CreatedAt, UpdatedAt: x.UpdatedAt, TemplateID: x.TemplateID, Join: x.Join, Table: x.Table, On: x.On})
|
||||
}
|
||||
return v
|
||||
}
|
||||
func exportToPO(v *biz.ExportTemplate) exportTemplatePO {
|
||||
return exportTemplatePO{ID: v.ID, DBName: v.DBName, Name: v.Name, DBTableName: v.TableName, TemplateID: v.TemplateID, TemplateInfo: v.TemplateInfo, SQL: v.SQL, ImportSQL: v.ImportSQL, Limit: v.Limit, Order: v.Order}
|
||||
}
|
||||
func (r *exportRepo) saveRelations(tx *gorm.DB, v *biz.ExportTemplate) error {
|
||||
func (r *exportRepo) saveRelations(tx *gorm.DB, v *biz.ExportTemplate, resetIDs bool) error {
|
||||
if err := tx.Where("template_id = ?", v.TemplateID).Delete(&exportConditionPO{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -75,19 +82,45 @@ func (r *exportRepo) saveRelations(tx *gorm.DB, v *biz.ExportTemplate) error {
|
||||
}
|
||||
conditions := make([]exportConditionPO, 0, len(v.Conditions))
|
||||
for _, x := range v.Conditions {
|
||||
conditions = append(conditions, exportConditionPO{TemplateID: v.TemplateID, From: x.From, Column: x.Column, Operator: x.Operator})
|
||||
id := x.ID
|
||||
if resetIDs {
|
||||
id = 0
|
||||
}
|
||||
templateID := x.TemplateID
|
||||
if templateID == "" {
|
||||
templateID = v.TemplateID
|
||||
}
|
||||
conditions = append(conditions, exportConditionPO{ID: id, CreatedAt: x.CreatedAt, UpdatedAt: x.UpdatedAt, TemplateID: templateID, From: x.From, Column: x.Column, Operator: x.Operator})
|
||||
}
|
||||
if len(conditions) > 0 {
|
||||
if err := tx.Create(&conditions).Error; err != nil {
|
||||
create := tx
|
||||
if !resetIDs {
|
||||
// GORM saves Create associations with ON CONFLICT DO NOTHING.
|
||||
// Preserve that behavior for copied templates carrying relation IDs.
|
||||
create = create.Clauses(clause.OnConflict{DoNothing: true})
|
||||
}
|
||||
if err := create.Create(&conditions).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
joins := make([]exportJoinPO, 0, len(v.Joins))
|
||||
for _, x := range v.Joins {
|
||||
joins = append(joins, exportJoinPO{TemplateID: v.TemplateID, Join: x.Join, Table: x.Table, On: x.On})
|
||||
id := x.ID
|
||||
if resetIDs {
|
||||
id = 0
|
||||
}
|
||||
templateID := x.TemplateID
|
||||
if templateID == "" {
|
||||
templateID = v.TemplateID
|
||||
}
|
||||
joins = append(joins, exportJoinPO{ID: id, CreatedAt: x.CreatedAt, UpdatedAt: x.UpdatedAt, TemplateID: templateID, Join: x.Join, Table: x.Table, On: x.On})
|
||||
}
|
||||
if len(joins) > 0 {
|
||||
return tx.Create(&joins).Error
|
||||
create := tx
|
||||
if !resetIDs {
|
||||
create = create.Clauses(clause.OnConflict{DoNothing: true})
|
||||
}
|
||||
return create.Create(&joins).Error
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -98,7 +131,7 @@ func (r *exportRepo) CreateExportTemplate(ctx context.Context, v *biz.ExportTemp
|
||||
return err
|
||||
}
|
||||
v.ID = po.ID
|
||||
return r.saveRelations(tx, v)
|
||||
return r.saveRelations(tx, v, false)
|
||||
})
|
||||
}
|
||||
func (r *exportRepo) UpdateExportTemplate(ctx context.Context, v *biz.ExportTemplate) error {
|
||||
@@ -107,7 +140,7 @@ func (r *exportRepo) UpdateExportTemplate(ctx context.Context, v *biz.ExportTemp
|
||||
if err := tx.Model(&exportTemplatePO{}).Where("id = ?", v.ID).Updates(&po).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return r.saveRelations(tx, v)
|
||||
return r.saveRelations(tx, v, true)
|
||||
})
|
||||
}
|
||||
func (r *exportRepo) DeleteExportTemplates(ctx context.Context, ids []uint) error {
|
||||
@@ -387,7 +420,10 @@ func parseTemplateColumns(raw string) map[string]string {
|
||||
return out
|
||||
}
|
||||
func (r *exportRepo) ImportExportRows(ctx context.Context, t *biz.ExportTemplate, rows []map[string]any) error {
|
||||
sql := strings.TrimSpace(t.ImportSQL)
|
||||
// ImportSQL is checked verbatim. In particular, a whitespace-only value
|
||||
// is still treated as custom SQL and is allowed to return the driver's
|
||||
// native error instead of silently falling back to GORM insertion.
|
||||
sql := t.ImportSQL
|
||||
selected, err := r.data.database(t.DBName)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -26,7 +26,7 @@ func (r *auditRecorderRepo) RecordLogin(ctx context.Context, v *biz.LoginLog) er
|
||||
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}
|
||||
return &biz.LoginLog{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, Username: v.Username, IP: v.IP, Status: v.Status, ErrorMessage: v.ErrorMessage, Agent: v.Agent, UserID: v.UserID}
|
||||
}
|
||||
func (r *auditQueryRepo) ListLogins(ctx context.Context, page, size int, q *biz.LoginLog) ([]*biz.LoginLog, int64, error) {
|
||||
db := r.data.gormDB.WithContext(ctx).Model(&loginLogPO{})
|
||||
@@ -69,5 +69,5 @@ func (r *auditQueryRepo) FindLogin(ctx context.Context, id uint) (*biz.LoginLog,
|
||||
return loginFromPO(po), nil
|
||||
}
|
||||
func (r *auditQueryRepo) DeleteLogins(ctx context.Context, ids []uint) error {
|
||||
return r.data.gormDB.WithContext(ctx).Delete(&loginLogPO{}, ids).Error
|
||||
return r.data.gormDB.WithContext(ctx).Delete(&loginLogPO{}, "id IN ?", ids).Error
|
||||
}
|
||||
|
||||
+13
-3
@@ -55,6 +55,7 @@ func (r *mediaRepo) CreateMedia(ctx context.Context, v *biz.MediaFile) error {
|
||||
}
|
||||
v.ID = po.ID
|
||||
v.CreatedAt = po.CreatedAt
|
||||
v.UpdatedAt = po.UpdatedAt
|
||||
return nil
|
||||
}
|
||||
func (r *mediaRepo) FindMedia(ctx context.Context, id uint) (*biz.MediaFile, error) {
|
||||
@@ -119,7 +120,13 @@ func (r *mediaRepo) ListMedia(ctx context.Context, filter biz.MediaFilter) ([]*b
|
||||
return out, total, nil
|
||||
}
|
||||
func (r *mediaRepo) UpdateMediaName(ctx context.Context, id uint, name string) error {
|
||||
return r.data.gormDB.WithContext(ctx).Model(&mediaPO{}).Where("id = ?", id).Update("name", name).Error
|
||||
// The compatible flow performs a First before Update, so a missing record returns a
|
||||
// not-found error instead of silently succeeding with RowsAffected=0.
|
||||
var po mediaPO
|
||||
if err := r.data.gormDB.WithContext(ctx).Where("id = ?", id).First(&po).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return r.data.gormDB.WithContext(ctx).Model(&po).Update("name", name).Error
|
||||
}
|
||||
func (r *mediaRepo) DeleteMedia(ctx context.Context, id uint) error {
|
||||
return r.data.gormDB.WithContext(ctx).Unscoped().Delete(&mediaPO{}, id).Error
|
||||
@@ -176,14 +183,17 @@ func (r *mediaRepo) ListCategories(ctx context.Context) ([]*biz.AttachmentCatego
|
||||
}
|
||||
byID := map[uint]*biz.AttachmentCategory{}
|
||||
for _, po := range pos {
|
||||
byID[po.ID] = &biz.AttachmentCategory{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Name: po.Name, ParentID: po.ParentID, Children: []*biz.AttachmentCategory{}}
|
||||
// Children remains nil for leaf nodes, which serializes as null.
|
||||
byID[po.ID] = &biz.AttachmentCategory{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Name: po.Name, ParentID: po.ParentID}
|
||||
}
|
||||
roots := []*biz.AttachmentCategory{}
|
||||
for _, po := range pos {
|
||||
v := byID[po.ID]
|
||||
if parent := byID[po.ParentID]; parent != nil {
|
||||
parent.Children = append(parent.Children, v)
|
||||
} else {
|
||||
} else if po.ParentID == 0 {
|
||||
// The tree starts at parent id 0; non-zero orphan nodes are
|
||||
// intentionally omitted instead of being promoted to roots.
|
||||
roots = append(roots, v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +84,9 @@ func (r *mediaRepo) CompleteUploadSession(ctx context.Context, id uint, key stri
|
||||
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
|
||||
// The compatible flow uses GORM's normal Delete here, retaining the soft-deleted session
|
||||
// for audit/recovery rather than physically removing it.
|
||||
return r.data.gormDB.WithContext(ctx).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}
|
||||
@@ -102,7 +104,7 @@ func (r *mediaRepo) ListChunks(ctx context.Context, uploadID uint) ([]*biz.Uploa
|
||||
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
|
||||
return r.data.gormDB.WithContext(ctx).Where("upload_id = ?", uploadID).Delete(&uploadChunkPO{}).Error
|
||||
}
|
||||
func (r *mediaRepo) StaleUploadSessionIDs(ctx context.Context, before time.Time) ([]uint, error) {
|
||||
var ids []uint
|
||||
@@ -111,9 +113,9 @@ func (r *mediaRepo) StaleUploadSessionIDs(ctx context.Context, before time.Time)
|
||||
}
|
||||
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 {
|
||||
if err := tx.Where("upload_id = ?", uploadID).Delete(&uploadChunkPO{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Unscoped().Delete(&uploadSessionPO{}, uploadID).Error
|
||||
return tx.Delete(&uploadSessionPO{}, uploadID).Error
|
||||
})
|
||||
}
|
||||
|
||||
+35
-16
@@ -3,6 +3,7 @@ package data
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"kra/internal/biz"
|
||||
|
||||
@@ -14,7 +15,12 @@ type menuRepo struct{ data *Data }
|
||||
func NewMenuRepo(data *Data) biz.MenuRepo { return &menuRepo{data: data} }
|
||||
|
||||
func menuFromPO(po menuPO) *biz.Menu {
|
||||
return &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}
|
||||
var deletedAt *time.Time
|
||||
if po.DeletedAt.Valid {
|
||||
value := po.DeletedAt.Time
|
||||
deletedAt = &value
|
||||
}
|
||||
return &biz.Menu{CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, DeletedAt: deletedAt, 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}
|
||||
}
|
||||
|
||||
func menuToPO(v *biz.Menu) menuPO {
|
||||
@@ -22,19 +28,14 @@ func menuToPO(v *biz.Menu) menuPO {
|
||||
}
|
||||
|
||||
func replaceMenuRelations(tx *gorm.DB, menu *biz.Menu) error {
|
||||
var oldButtonIDs []uint
|
||||
if err := tx.Model(&menuButtonPO{}).Where("sys_base_menu_id = ?", menu.ID).Pluck("id", &oldButtonIDs).Error; err != nil {
|
||||
// The compatible update rebuilds menu-button rows but does not remove the
|
||||
// existing authority-button rows that point at the old button IDs. Keep
|
||||
// that observable behavior here; menu deletion itself still cleans those
|
||||
// rows in Delete below, matching the administration endpoint behavior.
|
||||
if err := tx.Unscoped().Where("sys_base_menu_id = ?", menu.ID).Delete(&menuButtonPO{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if len(oldButtonIDs) > 0 {
|
||||
if err := tx.Where("sys_base_menu_btn_id IN ?", oldButtonIDs).Delete(&authorityButtonPO{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := tx.Where("sys_base_menu_id = ?", menu.ID).Delete(&menuButtonPO{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("sys_base_menu_id = ?", menu.ID).Delete(&menuParameterPO{}).Error; err != nil {
|
||||
if err := tx.Unscoped().Where("sys_base_menu_id = ?", menu.ID).Delete(&menuParameterPO{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
buttons := make([]menuButtonPO, 0, len(menu.Buttons))
|
||||
@@ -166,7 +167,7 @@ func (r *menuRepo) loadRelations(ctx context.Context, menu *biz.Menu) error {
|
||||
}
|
||||
menu.Parameters = make([]*biz.MenuParameter, 0, len(parameters))
|
||||
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})
|
||||
menu.Parameters = append(menu.Parameters, menuParameterFromPO(parameter))
|
||||
}
|
||||
var buttons []menuButtonPO
|
||||
if err := r.data.gormDB.WithContext(ctx).Where("sys_base_menu_id = ?", menu.ID).Order("id").Find(&buttons).Error; err != nil {
|
||||
@@ -174,7 +175,7 @@ func (r *menuRepo) loadRelations(ctx context.Context, menu *biz.Menu) error {
|
||||
}
|
||||
menu.Buttons = make([]*biz.MenuButton, 0, len(buttons))
|
||||
for _, button := range buttons {
|
||||
menu.Buttons = append(menu.Buttons, &biz.MenuButton{ID: button.ID, Name: button.Name, Description: button.Description, MenuID: button.MenuID})
|
||||
menu.Buttons = append(menu.Buttons, menuButtonFromPO(button))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -228,7 +229,7 @@ func (r *menuRepo) List(ctx context.Context) ([]*biz.Menu, error) {
|
||||
}
|
||||
for _, parameter := range parameters {
|
||||
menu := byID[parameter.MenuID]
|
||||
menu.Parameters = append(menu.Parameters, &biz.MenuParameter{ID: parameter.ID, MenuID: parameter.MenuID, Type: parameter.Type, Key: parameter.Key, Value: parameter.Value})
|
||||
menu.Parameters = append(menu.Parameters, menuParameterFromPO(parameter))
|
||||
}
|
||||
var buttons []menuButtonPO
|
||||
if err := r.data.gormDB.WithContext(ctx).Where("sys_base_menu_id IN ?", ids).Order("id").Find(&buttons).Error; err != nil {
|
||||
@@ -236,11 +237,29 @@ func (r *menuRepo) List(ctx context.Context) ([]*biz.Menu, error) {
|
||||
}
|
||||
for _, button := range buttons {
|
||||
menu := byID[button.MenuID]
|
||||
menu.Buttons = append(menu.Buttons, &biz.MenuButton{ID: button.ID, Name: button.Name, Description: button.Description, MenuID: button.MenuID})
|
||||
menu.Buttons = append(menu.Buttons, menuButtonFromPO(button))
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func menuParameterFromPO(po menuParameterPO) *biz.MenuParameter {
|
||||
var deletedAt *time.Time
|
||||
if po.DeletedAt.Valid {
|
||||
value := po.DeletedAt.Time
|
||||
deletedAt = &value
|
||||
}
|
||||
return &biz.MenuParameter{CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, DeletedAt: deletedAt, ID: po.ID, MenuID: po.MenuID, Type: po.Type, Key: po.Key, Value: po.Value}
|
||||
}
|
||||
|
||||
func menuButtonFromPO(po menuButtonPO) *biz.MenuButton {
|
||||
var deletedAt *time.Time
|
||||
if po.DeletedAt.Valid {
|
||||
value := po.DeletedAt.Time
|
||||
deletedAt = &value
|
||||
}
|
||||
return &biz.MenuButton{CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, DeletedAt: deletedAt, ID: po.ID, Name: po.Name, Description: po.Description, MenuID: po.MenuID}
|
||||
}
|
||||
|
||||
func (r *menuRepo) SetAuthorityMenus(ctx context.Context, id uint, ids []uint) error {
|
||||
if err := (&authorityAccessRepo{data: r.data}).checkAuthorityIDAuth(ctx, id); err != nil {
|
||||
return err
|
||||
|
||||
+136
-1
@@ -2,14 +2,18 @@ package data
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func migrateAll(db *gorm.DB) error {
|
||||
if err := migrateLegacyIgnoreAPITable(db); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := db.AutoMigrate(
|
||||
&userPO{}, &authorityPO{}, &menuPO{}, &userAuthorityPO{}, &authorityMenuPO{}, &menuParameterPO{},
|
||||
&apiPO{}, &ignoredAPIPO{}, &authorityAPIPO{}, &menuButtonPO{}, &authorityButtonPO{},
|
||||
&apiPO{}, &ignoredAPIPO{}, &authorityAPIPO{}, &casbinRulePO{}, &menuButtonPO{}, &authorityButtonPO{},
|
||||
&departmentPO{}, &positionPO{}, &userDepartmentPO{}, &userPositionPO{}, &authorityDepartmentPO{},
|
||||
&dictionaryPO{}, &dictionaryDetailPO{}, ¶meterPO{}, &apiTokenPO{}, &jwtBlacklistPO{}, &securityConfigPO{},
|
||||
&versionPO{}, &exportTemplatePO{}, &exportConditionPO{}, &exportJoinPO{},
|
||||
@@ -19,9 +23,140 @@ func migrateAll(db *gorm.DB) error {
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := migrateLegacyAuthorityAPIsToCasbinRules(db); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := normalizeErrorRecordStatuses(db); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := reconcileRootAuthorityAPIs(db); err != nil {
|
||||
return err
|
||||
}
|
||||
return reconcileReferenceIndexes(db)
|
||||
}
|
||||
|
||||
// Older Kra builds used a status label that is not part of the GVA error-log
|
||||
// contract and is therefore rendered as an unknown state by the compatible
|
||||
// administration page.
|
||||
func normalizeErrorRecordStatuses(db *gorm.DB) error {
|
||||
return db.Model(&errorRecordPO{}).Where("status = ?", "未解决").Update("status", "未处理").Error
|
||||
}
|
||||
|
||||
// migrateLegacyAuthorityAPIsToCasbinRules upgrades the early Kra join-table
|
||||
// representation to the independent Casbin policy table. Keep the legacy
|
||||
// table in place for backwards compatibility, but make casbin_rule the sole
|
||||
// live policy source. Existing policy rows are not duplicated.
|
||||
func migrateLegacyAuthorityAPIsToCasbinRules(db *gorm.DB) error {
|
||||
if !db.Migrator().HasTable(&authorityAPIPO{}) || !db.Migrator().HasTable(&casbinRulePO{}) {
|
||||
return nil
|
||||
}
|
||||
type legacyPolicy struct {
|
||||
AuthorityID uint
|
||||
Path string
|
||||
Method string
|
||||
}
|
||||
var rows []legacyPolicy
|
||||
if err := db.Table("sys_authority_apis sa").
|
||||
Select("sa.authority_id, a.path, a.method").
|
||||
Joins("JOIN sys_apis a ON a.id = sa.api_id").
|
||||
Where("a.deleted_at IS NULL").Find(&rows).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, row := range rows {
|
||||
exists, err := policyExists(db, row.AuthorityID, row.Path, row.Method)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
continue
|
||||
}
|
||||
if err := db.Create(&casbinRulePO{Ptype: "p", V0: fmt.Sprint(row.AuthorityID), V1: row.Path, V2: row.Method}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// migrateLegacyIgnoreAPITable upgrades the early Kra-only composite-key
|
||||
// shape (path, method) to the compatible model shape (ID/timestamps/soft
|
||||
// delete). AutoMigrate can add columns but cannot replace an existing
|
||||
// composite primary key portably, so rebuild the small table once while
|
||||
// preserving every existing ignore rule.
|
||||
func migrateLegacyIgnoreAPITable(db *gorm.DB) error {
|
||||
if !db.Migrator().HasTable(&ignoredAPIPO{}) || db.Migrator().HasColumn(&ignoredAPIPO{}, "id") {
|
||||
return nil
|
||||
}
|
||||
legacyTable := fmt.Sprintf("sys_ignore_apis_legacy_%d", time.Now().UnixNano())
|
||||
type legacyIgnoredAPI struct {
|
||||
Path string
|
||||
Method string
|
||||
}
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Migrator().RenameTable(ignoredAPIPO{}.TableName(), legacyTable); err != nil {
|
||||
return fmt.Errorf("rename legacy ignore API table: %w", err)
|
||||
}
|
||||
if err := tx.AutoMigrate(&ignoredAPIPO{}); err != nil {
|
||||
return fmt.Errorf("create compatible ignore API table: %w", err)
|
||||
}
|
||||
var rows []legacyIgnoredAPI
|
||||
if err := tx.Table(legacyTable).Find(&rows).Error; err != nil {
|
||||
return fmt.Errorf("read legacy ignore API rows: %w", err)
|
||||
}
|
||||
if len(rows) > 0 {
|
||||
items := make([]ignoredAPIPO, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
items = append(items, ignoredAPIPO{Path: row.Path, Method: row.Method})
|
||||
}
|
||||
if err := tx.Create(&items).Error; err != nil {
|
||||
return fmt.Errorf("copy legacy ignore API rows: %w", err)
|
||||
}
|
||||
}
|
||||
if err := tx.Migrator().DropTable(legacyTable); err != nil {
|
||||
return fmt.Errorf("drop legacy ignore API table: %w", err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// reconcileRootAuthorityAPIs is a one-time upgrade path from the former Kra
|
||||
// implementation where authority 888 bypassed policy storage entirely. The compatible behavior
|
||||
// grants its root role through persisted Casbin policies, so when a legacy
|
||||
// database has the root role but no stored API links, materialize the same
|
||||
// policy set and let normal authorization read it thereafter.
|
||||
func reconcileRootAuthorityAPIs(db *gorm.DB) error {
|
||||
var authorityCount int64
|
||||
if err := db.Model(&authorityPO{}).Where("authority_id = ?", 888).Count(&authorityCount).Error; err != nil || authorityCount == 0 {
|
||||
return err
|
||||
}
|
||||
var policyCount int64
|
||||
if err := policyScope(db).Where("v0 = ?", "888").Count(&policyCount).Error; err != nil || policyCount != 0 {
|
||||
return err
|
||||
}
|
||||
var ignored []ignoredAPIPO
|
||||
if err := db.Find(&ignored).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
ignoreSet := make(map[string]struct{}, len(ignored))
|
||||
for _, item := range ignored {
|
||||
ignoreSet[item.Method+"\x00"+item.Path] = struct{}{}
|
||||
}
|
||||
var apis []apiPO
|
||||
if err := db.Find(&apis).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
rules := make([]casbinRulePO, 0, len(apis))
|
||||
for _, api := range apis {
|
||||
if _, ok := ignoreSet[api.Method+"\x00"+api.Path]; ok {
|
||||
continue
|
||||
}
|
||||
rules = append(rules, newPolicyRule(888, api.Path, api.Method))
|
||||
}
|
||||
if len(rules) == 0 {
|
||||
return nil
|
||||
}
|
||||
return db.Create(&rules).Error
|
||||
}
|
||||
|
||||
// reconcileReferenceIndexes removes constraints created by older Kra builds
|
||||
// that are not part of the administration data model. Business services own
|
||||
// duplicate checks and their user-facing error messages.
|
||||
|
||||
@@ -33,7 +33,7 @@ func (r *auditRecorderRepo) RecordOperation(ctx context.Context, v *biz.Operatio
|
||||
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}
|
||||
return &biz.OperationRecord{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, 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 *auditQueryRepo) ListOperations(ctx context.Context, page, size int, q *biz.OperationRecord) ([]*biz.OperationRecord, int64, error) {
|
||||
db := r.data.gormDB.WithContext(ctx).Model(&operationPO{})
|
||||
@@ -98,5 +98,5 @@ func (r *auditQueryRepo) FindOperation(ctx context.Context, id uint) (*biz.Opera
|
||||
return opFromPO(po), nil
|
||||
}
|
||||
func (r *auditQueryRepo) DeleteOperations(ctx context.Context, ids []uint) error {
|
||||
return r.data.gormDB.WithContext(ctx).Delete(&operationPO{}, ids).Error
|
||||
return r.data.gormDB.WithContext(ctx).Delete(&operationPO{}, "id IN ?", ids).Error
|
||||
}
|
||||
|
||||
@@ -36,7 +36,9 @@ func (r *parameterRepo) CreateParameter(ctx context.Context, v *biz.SystemParame
|
||||
return nil
|
||||
}
|
||||
func (r *parameterRepo) 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
|
||||
// The compatible update uses a struct, so zero-value optional fields (notably desc)
|
||||
// are intentionally ignored rather than clearing an existing value.
|
||||
return r.data.gormDB.WithContext(ctx).Model(¶meterPO{}).Where("id = ?", v.ID).Updates(¶meterPO{Name: v.Name, Key: v.Key, Value: v.Value, Desc: v.Desc}).Error
|
||||
}
|
||||
func (r *parameterRepo) DeleteParameters(ctx context.Context, ids []uint) error {
|
||||
return r.data.gormDB.WithContext(ctx).Delete(¶meterPO{}, ids).Error
|
||||
|
||||
@@ -2,6 +2,7 @@ package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"kra/internal/biz"
|
||||
|
||||
@@ -14,6 +15,9 @@ func NewPermissionRepo(data *Data) biz.PermissionRepo { return &permissionRepo{d
|
||||
|
||||
type menuButtonPO struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||
Name string
|
||||
Description string `gorm:"column:desc"`
|
||||
MenuID uint `gorm:"column:sys_base_menu_id"`
|
||||
@@ -36,7 +40,7 @@ func (r *permissionRepo) Buttons(ctx context.Context, menuID uint) ([]*biz.MenuB
|
||||
}
|
||||
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})
|
||||
out = append(out, menuButtonFromPO(po))
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@ func NewSecurityRepo(data *Data) biz.SecurityRepo { return &securityRepo{data: d
|
||||
|
||||
type securityConfigPO struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||
CaptchaOpen int
|
||||
CaptchaTimeout int
|
||||
KeyLong int
|
||||
@@ -43,10 +46,10 @@ 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}
|
||||
return &biz.SecurityConfig{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, 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}
|
||||
return securityConfigPO{ID: 1, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, 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 *securityRepo) SecurityConfig(ctx context.Context) (*biz.SecurityConfig, error) {
|
||||
var po securityConfigPO
|
||||
@@ -66,6 +69,7 @@ func (r *securityRepo) SaveSecurityConfig(ctx context.Context, v *biz.SecurityCo
|
||||
return err
|
||||
}
|
||||
po := securityToPO(v)
|
||||
po.CreatedAt, po.UpdatedAt = previous.CreatedAt, previous.UpdatedAt
|
||||
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Save(&po).Error; err != nil {
|
||||
return err
|
||||
@@ -73,6 +77,7 @@ func (r *securityRepo) SaveSecurityConfig(ctx context.Context, v *biz.SecurityCo
|
||||
if v.PwdExpireEnable && !previous.PwdExpireEnable {
|
||||
return tx.Model(&userPO{}).Where("password_updated_at IS NULL").Update("password_updated_at", time.Now()).Error
|
||||
}
|
||||
v.ID, v.CreatedAt, v.UpdatedAt = po.ID, po.CreatedAt, po.UpdatedAt
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ func (s *aliyunStorage) DeletePrefix(ctx context.Context, prefix string) error {
|
||||
}
|
||||
}
|
||||
func (s *aliyunStorage) List(_ context.Context, prefix, cursor string, limit int) ([]*biz.StoredFile, string, bool, error) {
|
||||
if limit < 1 || limit > 1000 {
|
||||
if limit <= 0 {
|
||||
limit = 100
|
||||
}
|
||||
result, err := s.bucket.ListObjects(oss.Prefix(s.key(prefix)), oss.Marker(cursor), oss.MaxKeys(limit))
|
||||
|
||||
@@ -121,7 +121,7 @@ func (s *awsStorage) DeletePrefix(ctx context.Context, prefix string) error {
|
||||
}
|
||||
}
|
||||
func (s *awsStorage) List(ctx context.Context, prefix, cursor string, limit int) ([]*biz.StoredFile, string, bool, error) {
|
||||
if limit < 1 || limit > 1000 {
|
||||
if limit <= 0 {
|
||||
limit = 100
|
||||
}
|
||||
input := &s3.ListObjectsV2Input{Bucket: aws.String(s.bucket), Prefix: aws.String(s.key(prefix)), MaxKeys: aws.Int32(int32(limit))}
|
||||
|
||||
@@ -85,7 +85,7 @@ func (s *huaweiStorage) DeletePrefix(ctx context.Context, prefix string) error {
|
||||
}
|
||||
}
|
||||
func (s *huaweiStorage) List(_ context.Context, prefix, cursor string, limit int) ([]*biz.StoredFile, string, bool, error) {
|
||||
if limit < 1 || limit > 1000 {
|
||||
if limit <= 0 {
|
||||
limit = 100
|
||||
}
|
||||
result, err := s.client.ListObjects(&obs.ListObjectsInput{ListObjsInput: obs.ListObjsInput{Prefix: s.key(prefix), MaxKeys: limit}, Bucket: s.bucket, Marker: cursor})
|
||||
|
||||
@@ -103,6 +103,9 @@ func composeFiles(ctx context.Context, storage biz.FileStorage, names []string,
|
||||
}
|
||||
|
||||
func (s *fileStorage) resolve(name string) (string, error) {
|
||||
if name == "" {
|
||||
return "", fmt.Errorf("key不能为空")
|
||||
}
|
||||
clean := filepath.Clean(strings.TrimPrefix(name, "/"))
|
||||
if clean == "." || filepath.IsAbs(clean) || clean == ".." || strings.HasPrefix(clean, ".."+string(filepath.Separator)) {
|
||||
return "", fmt.Errorf("invalid storage path")
|
||||
@@ -146,7 +149,12 @@ func (s *fileStorage) Delete(ctx context.Context, name string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
|
||||
// Match the compatible Local.DeleteFile behavior: a missing object is an error and the
|
||||
// media DB record is retained when the physical file cannot be removed.
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return fmt.Errorf("文件不存在")
|
||||
}
|
||||
if err := os.Remove(path); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -170,7 +178,7 @@ func (s *fileStorage) DeletePrefix(ctx context.Context, prefix string) error {
|
||||
return nil
|
||||
}
|
||||
func (s *fileStorage) List(ctx context.Context, prefix, cursor string, limit int) ([]*biz.StoredFile, string, bool, error) {
|
||||
if limit < 1 || limit > 1000 {
|
||||
if limit <= 0 {
|
||||
limit = 100
|
||||
}
|
||||
root, err := s.resolve(strings.TrimSuffix(prefix, "/") + "/placeholder")
|
||||
|
||||
@@ -111,7 +111,7 @@ func (s *qiniuStorage) DeletePrefix(ctx context.Context, prefix string) error {
|
||||
}
|
||||
}
|
||||
func (s *qiniuStorage) List(ctx context.Context, prefix, cursor string, limit int) ([]*biz.StoredFile, string, bool, error) {
|
||||
if limit < 1 || limit > 1000 {
|
||||
if limit <= 0 {
|
||||
limit = 100
|
||||
}
|
||||
entries, _, marker, more, err := s.manager.ListFiles(s.config.Bucket, prefix, "", cursor, limit)
|
||||
|
||||
@@ -109,7 +109,7 @@ func (s *s3Storage) DeletePrefix(ctx context.Context, prefix string) error {
|
||||
return nil
|
||||
}
|
||||
func (s *s3Storage) List(ctx context.Context, prefix, cursor string, limit int) ([]*biz.StoredFile, string, bool, error) {
|
||||
if limit < 1 || limit > 1000 {
|
||||
if limit <= 0 {
|
||||
limit = 100
|
||||
}
|
||||
items := s.client.ListObjects(ctx, s.bucket, minio.ListObjectsOptions{Prefix: s.key(prefix), Recursive: true, StartAfter: s.key(cursor)})
|
||||
|
||||
@@ -99,7 +99,7 @@ func (s *tencentStorage) DeletePrefix(ctx context.Context, prefix string) error
|
||||
}
|
||||
}
|
||||
func (s *tencentStorage) List(ctx context.Context, prefix, cursor string, limit int) ([]*biz.StoredFile, string, bool, error) {
|
||||
if limit < 1 || limit > 1000 {
|
||||
if limit <= 0 {
|
||||
limit = 100
|
||||
}
|
||||
result, _, err := s.client.Bucket.Get(ctx, &cos.BucketGetOptions{Prefix: s.key(prefix), Marker: cursor, MaxKeys: limit})
|
||||
|
||||
@@ -81,11 +81,14 @@ type authorityMenuPO struct {
|
||||
func (authorityMenuPO) TableName() string { return "sys_authority_menus" }
|
||||
|
||||
type menuParameterPO struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
MenuID uint `gorm:"column:sys_base_menu_id;index"`
|
||||
Type string
|
||||
Key string
|
||||
Value string
|
||||
ID uint `gorm:"primaryKey"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||
MenuID uint `gorm:"column:sys_base_menu_id;index"`
|
||||
Type string
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
|
||||
func (menuParameterPO) TableName() string { return "sys_base_menu_parameters" }
|
||||
|
||||
@@ -112,10 +112,16 @@ func (r *initializationRepo) Initialize(ctx context.Context, input *biz.Database
|
||||
return err
|
||||
}
|
||||
if err := db.Transaction(func(tx *gorm.DB) error {
|
||||
authority := authorityPO{AuthorityID: 888, AuthorityName: "超级管理员", DataScope: 1, DefaultRouter: "dashboard"}
|
||||
rootParentID := uint(0)
|
||||
authority := authorityPO{AuthorityID: 888, AuthorityName: "超级管理员", ParentID: &rootParentID, DataScope: 1, DefaultRouter: "dashboard"}
|
||||
if err := tx.FirstOrCreate(&authority, authorityPO{AuthorityID: 888}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
// The root role is seeded with parent_id=0. Older Kra databases used
|
||||
// NULL, which makes the role disappear from the same root-only queries.
|
||||
if err := tx.Model(&authorityPO{}).Where("authority_id = ? AND parent_id IS NULL", 888).Update("parent_id", 0).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 {
|
||||
@@ -199,17 +205,36 @@ func (r *initializationRepo) Initialize(ctx context.Context, input *biz.Database
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, ignored := range []ignoredAPIPO{
|
||||
{Method: "GET", Path: "/api/freshCasbin"}, {Method: "GET", Path: "/health"},
|
||||
{Method: "POST", Path: "/system/reloadSystem"}, {Method: "POST", Path: "/base/login"},
|
||||
{Method: "POST", Path: "/base/captcha"}, {Method: "POST", Path: "/init/initdb"},
|
||||
{Method: "POST", Path: "/init/checkdb"}, {Method: "GET", Path: "/info/getInfoDataSource"},
|
||||
{Method: "GET", Path: "/info/getInfoPublic"},
|
||||
} {
|
||||
ignoredAPIs := defaultIgnoredAPIs()
|
||||
for _, ignored := range ignoredAPIs {
|
||||
if err := tx.FirstOrCreate(&ignored, ignored).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
ignoreSet := make(map[string]struct{}, len(ignoredAPIs))
|
||||
for _, ignored := range ignoredAPIs {
|
||||
ignoreSet[ignored.Method+"\x00"+ignored.Path] = struct{}{}
|
||||
}
|
||||
var apiRows []apiPO
|
||||
if err := tx.Find(&apiRows).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, api := range apiRows {
|
||||
if _, ignored := ignoreSet[api.Method+"\x00"+api.Path]; ignored {
|
||||
continue
|
||||
}
|
||||
exists, err := policyExists(tx, 888, api.Path, api.Method)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
continue
|
||||
}
|
||||
rule := newPolicyRule(888, api.Path, api.Method)
|
||||
if err := tx.Create(&rule).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
@@ -222,6 +247,16 @@ func (r *initializationRepo) Initialize(ctx context.Context, input *biz.Database
|
||||
return nil
|
||||
}
|
||||
|
||||
func defaultIgnoredAPIs() []ignoredAPIPO {
|
||||
return []ignoredAPIPO{
|
||||
{Method: "GET", Path: "/api/freshCasbin"}, {Method: "GET", Path: "/health"},
|
||||
{Method: "POST", Path: "/system/reloadSystem"}, {Method: "POST", Path: "/base/login"},
|
||||
{Method: "POST", Path: "/base/captcha"}, {Method: "POST", Path: "/init/initdb"},
|
||||
{Method: "POST", Path: "/init/checkdb"}, {Method: "GET", Path: "/info/getInfoDataSource"},
|
||||
{Method: "GET", Path: "/info/getInfoPublic"},
|
||||
}
|
||||
}
|
||||
|
||||
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}
|
||||
|
||||
@@ -116,7 +116,7 @@ func (r *taskRepo) RecordTaskLog(ctx context.Context, v *biz.TimedTaskLog) error
|
||||
return r.data.gormDB.WithContext(ctx).Create(&taskLogPO{TaskID: v.TaskID, TaskName: v.TaskName, TriggerType: v.TriggerType, StartedAt: v.StartedAt, FinishedAt: v.FinishedAt, DurationMS: v.DurationMS, Status: v.Status, ErrorMsg: v.ErrorMsg, Output: v.Output}).Error
|
||||
}
|
||||
func taskLogFromPO(v taskLogPO) *biz.TimedTaskLog {
|
||||
return &biz.TimedTaskLog{ID: v.ID, CreatedAt: v.CreatedAt, TaskID: v.TaskID, TaskName: v.TaskName, TriggerType: v.TriggerType, StartedAt: v.StartedAt, FinishedAt: v.FinishedAt, DurationMS: v.DurationMS, Status: v.Status, ErrorMsg: v.ErrorMsg, Output: v.Output}
|
||||
return &biz.TimedTaskLog{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, TaskID: v.TaskID, TaskName: v.TaskName, TriggerType: v.TriggerType, StartedAt: v.StartedAt, FinishedAt: v.FinishedAt, DurationMS: v.DurationMS, Status: v.Status, ErrorMsg: v.ErrorMsg, Output: v.Output}
|
||||
}
|
||||
func (r *taskRepo) ListTaskLogs(ctx context.Context, page, size int, taskID uint, status string) ([]*biz.TimedTaskLog, int64, error) {
|
||||
db := r.data.gormDB.WithContext(ctx).Model(&taskLogPO{})
|
||||
|
||||
@@ -68,8 +68,9 @@ func TestDictionaryImportKeepsHierarchyInOneTransaction(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repo := &dictionaryRepo{data: data}
|
||||
parentID := uint(10)
|
||||
dictionary := &biz.Dictionary{Name: "status", Type: "status", Status: true}
|
||||
details := []*biz.DictionaryDetail{{ID: 10, Label: "parent", Value: "1", Status: true}, {ID: 11, Label: "child", Value: "2", ParentID: &parentID, Status: true}}
|
||||
active := true
|
||||
dictionary := &biz.Dictionary{Name: "status", Type: "status", Status: &active}
|
||||
details := []*biz.DictionaryDetail{{ID: 10, Label: "parent", Value: "1", Status: &active}, {ID: 11, Label: "child", Value: "2", ParentID: &parentID, Status: &active}}
|
||||
if err := repo.ImportDictionary(ctx, dictionary, details); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
+24
-5
@@ -79,7 +79,12 @@ func (r *userRepo) loadUser(ctx context.Context, po *userPO) (*biz.User, error)
|
||||
}
|
||||
|
||||
func toBizAuthority(po authorityPO) biz.Authority {
|
||||
return biz.Authority{AuthorityID: po.AuthorityID, AuthorityName: po.AuthorityName, ParentID: po.ParentID, DataScope: po.DataScope, DefaultRouter: po.DefaultRouter}
|
||||
var deletedAt *time.Time
|
||||
if po.DeletedAt.Valid {
|
||||
value := po.DeletedAt.Time
|
||||
deletedAt = &value
|
||||
}
|
||||
return biz.Authority{CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, DeletedAt: deletedAt, AuthorityID: po.AuthorityID, AuthorityName: po.AuthorityName, ParentID: po.ParentID, DataScope: po.DataScope, DefaultRouter: po.DefaultRouter}
|
||||
}
|
||||
|
||||
func baseBizUser(po *userPO) *biz.User {
|
||||
@@ -207,7 +212,9 @@ func (r *userRepo) MenusByAuthority(ctx context.Context, authorityID uint) ([]*b
|
||||
byID := make(map[uint]*biz.Menu, len(pos))
|
||||
menuIDs := make([]uint, 0, len(pos))
|
||||
for _, po := range pos {
|
||||
menu := &biz.Menu{ID: po.ID, ParentID: po.ParentID, Path: po.Path, Name: po.Name, Hidden: po.Hidden, Component: po.Component, Sort: po.Sort, ActiveName: po.ActiveName, KeepAlive: po.KeepAlive, DefaultMenu: po.DefaultMenu, Title: po.Title, Icon: po.Icon, CloseTab: po.CloseTab, TransitionType: po.TransitionType, Children: []*biz.Menu{}}
|
||||
menu := menuFromPO(po)
|
||||
menu.MenuID = po.ID
|
||||
menu.Children = []*biz.Menu{}
|
||||
menu.Parameters = []*biz.MenuParameter{}
|
||||
menu.Buttons = []*biz.MenuButton{}
|
||||
byID[po.ID] = menu
|
||||
@@ -220,7 +227,7 @@ func (r *userRepo) MenusByAuthority(ctx context.Context, authorityID uint) ([]*b
|
||||
}
|
||||
for _, parameter := range parameters {
|
||||
menu := byID[parameter.MenuID]
|
||||
menu.Parameters = append(menu.Parameters, &biz.MenuParameter{ID: parameter.ID, MenuID: parameter.MenuID, Type: parameter.Type, Key: parameter.Key, Value: parameter.Value})
|
||||
menu.Parameters = append(menu.Parameters, menuParameterFromPO(parameter))
|
||||
}
|
||||
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 IN ?", authorityID, menuIDs).Find(&buttons).Error; err != nil {
|
||||
@@ -228,7 +235,9 @@ func (r *userRepo) MenusByAuthority(ctx context.Context, authorityID uint) ([]*b
|
||||
}
|
||||
for _, button := range buttons {
|
||||
menu := byID[button.MenuID]
|
||||
menu.Buttons = append(menu.Buttons, &biz.MenuButton{ID: button.ID, Name: button.Name, Description: button.Description, MenuID: button.MenuID, AuthorityID: authorityID})
|
||||
value := menuButtonFromPO(button)
|
||||
value.AuthorityID = authorityID
|
||||
menu.Buttons = append(menu.Buttons, value)
|
||||
}
|
||||
}
|
||||
roots := make([]*biz.Menu, 0)
|
||||
@@ -236,7 +245,7 @@ func (r *userRepo) MenusByAuthority(ctx context.Context, authorityID uint) ([]*b
|
||||
menu := byID[po.ID]
|
||||
if parent := byID[po.ParentID]; parent != nil {
|
||||
parent.Children = append(parent.Children, menu)
|
||||
} else {
|
||||
} else if po.ParentID == 0 {
|
||||
roots = append(roots, menu)
|
||||
}
|
||||
}
|
||||
@@ -302,6 +311,16 @@ func (r *userRepo) CreateUserWithAuthorities(ctx context.Context, user *biz.User
|
||||
authorityIDs = []uint{user.AuthorityID}
|
||||
}
|
||||
if err := r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
// The compatible registration rejects an existing non-deleted username before creating the
|
||||
// account. The username column is intentionally not unique for legacy
|
||||
// schema compatibility, so keep the behavioral check here rather than
|
||||
// relying on a database constraint.
|
||||
var existing userPO
|
||||
if err := tx.Where("username = ?", user.Username).First(&existing).Error; err == nil {
|
||||
return errors.New("用户名已注册")
|
||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
if err := validateAuthorities(tx, authorityIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+97
-68
@@ -2,8 +2,7 @@ package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
@@ -38,7 +37,7 @@ func (r *versionRepo) CreateVersion(ctx context.Context, v *biz.Version) error {
|
||||
return nil
|
||||
}
|
||||
func (r *versionRepo) DeleteVersions(ctx context.Context, ids []uint) error {
|
||||
return r.data.gormDB.WithContext(ctx).Delete(&versionPO{}, ids).Error
|
||||
return r.data.gormDB.WithContext(ctx).Where("id IN ?", ids).Delete(&versionPO{}).Error
|
||||
}
|
||||
func (r *versionRepo) FindVersion(ctx context.Context, id uint) (*biz.Version, error) {
|
||||
var po versionPO
|
||||
@@ -77,24 +76,27 @@ func (r *versionRepo) BuildVersionBundle(ctx context.Context, menuIDs, apiIDs, d
|
||||
var menus []menuPO
|
||||
if len(menuIDs) > 0 {
|
||||
if err := r.data.gormDB.WithContext(ctx).Where("id IN ?", menuIDs).Order("sort,id").Find(&menus).Error; err != nil {
|
||||
return nil, err
|
||||
return nil, &biz.VersionStageError{Stage: biz.VersionStageMenus, Err: err}
|
||||
}
|
||||
}
|
||||
byID := map[uint]*biz.Menu{}
|
||||
for _, po := range menus {
|
||||
v := menuFromPO(po)
|
||||
v.Children = []*biz.Menu{}
|
||||
var btns []menuButtonPO
|
||||
_ = r.data.gormDB.WithContext(ctx).Where("sys_base_menu_id = ?", po.ID).Find(&btns).Error
|
||||
for _, btn := range btns {
|
||||
v.Buttons = append(v.Buttons, &biz.MenuButton{Name: btn.Name, Description: btn.Description})
|
||||
}
|
||||
byID[po.ID] = v
|
||||
}
|
||||
if len(menuIDs) > 0 {
|
||||
var buttons []menuButtonPO
|
||||
if err := r.data.gormDB.WithContext(ctx).Where("sys_base_menu_id IN ?", menuIDs).Find(&buttons).Error; err != nil {
|
||||
return nil, &biz.VersionStageError{Stage: biz.VersionStageMenus, Err: err}
|
||||
}
|
||||
for _, button := range buttons {
|
||||
if menu := byID[button.MenuID]; menu != nil {
|
||||
menu.Buttons = append(menu.Buttons, &biz.MenuButton{Name: button.Name, Description: button.Description})
|
||||
}
|
||||
}
|
||||
var parameters []menuParameterPO
|
||||
if err := r.data.gormDB.WithContext(ctx).Where("sys_base_menu_id IN ?", menuIDs).Find(¶meters).Error; err != nil {
|
||||
return nil, err
|
||||
return nil, &biz.VersionStageError{Stage: biz.VersionStageMenus, Err: err}
|
||||
}
|
||||
for _, parameter := range parameters {
|
||||
if menu := byID[parameter.MenuID]; menu != nil {
|
||||
@@ -110,6 +112,14 @@ func (r *versionRepo) BuildVersionBundle(ctx context.Context, menuIDs, apiIDs, d
|
||||
bundle.Menus = append(bundle.Menus, v)
|
||||
}
|
||||
}
|
||||
var sortMenus func([]*biz.Menu)
|
||||
sortMenus = func(items []*biz.Menu) {
|
||||
sort.Slice(items, func(i, j int) bool { return items[i].Sort < items[j].Sort })
|
||||
for _, item := range items {
|
||||
sortMenus(item.Children)
|
||||
}
|
||||
}
|
||||
sortMenus(bundle.Menus)
|
||||
var cleanMenu func(*biz.Menu)
|
||||
cleanMenu = func(menu *biz.Menu) {
|
||||
menu.ID = 0
|
||||
@@ -133,7 +143,7 @@ func (r *versionRepo) BuildVersionBundle(ctx context.Context, menuIDs, apiIDs, d
|
||||
var apis []apiPO
|
||||
if len(apiIDs) > 0 {
|
||||
if err := r.data.gormDB.WithContext(ctx).Where("id IN ?", apiIDs).Find(&apis).Error; err != nil {
|
||||
return nil, err
|
||||
return nil, &biz.VersionStageError{Stage: biz.VersionStageAPIs, Err: err}
|
||||
}
|
||||
}
|
||||
for _, po := range apis {
|
||||
@@ -144,7 +154,7 @@ func (r *versionRepo) BuildVersionBundle(ctx context.Context, menuIDs, apiIDs, d
|
||||
var dictionaries []dictionaryPO
|
||||
if len(dictIDs) > 0 {
|
||||
if err := r.data.gormDB.WithContext(ctx).Where("id IN ?", dictIDs).Find(&dictionaries).Error; err != nil {
|
||||
return nil, err
|
||||
return nil, &biz.VersionStageError{Stage: biz.VersionStageDictionaries, Err: err}
|
||||
}
|
||||
}
|
||||
dictionariesRepo := &dictionaryRepo{data: r.data}
|
||||
@@ -153,7 +163,7 @@ func (r *versionRepo) BuildVersionBundle(ctx context.Context, menuIDs, apiIDs, d
|
||||
v.ID = 0
|
||||
details, _, err := dictionariesRepo.ListDictionaryDetails(ctx, 0, 0, biz.DictionaryDetailFilter{DictionaryID: po.ID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, &biz.VersionStageError{Stage: biz.VersionStageDictionaries, Err: err}
|
||||
}
|
||||
for _, d := range details {
|
||||
d.ID = 0
|
||||
@@ -168,67 +178,86 @@ func (r *versionRepo) BuildVersionBundle(ctx context.Context, menuIDs, apiIDs, d
|
||||
return bundle, nil
|
||||
}
|
||||
func (r *versionRepo) ImportVersionBundle(ctx context.Context, b *biz.VersionBundle) error {
|
||||
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var addMenus func([]*biz.Menu, uint) error
|
||||
addMenus = func(items []*biz.Menu, parent uint) error {
|
||||
for _, item := range items {
|
||||
var existing menuPO
|
||||
err := tx.Where("name = ? AND path = ?", item.Name, item.Path).First(&existing).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
po := menuToPO(item)
|
||||
po.ID = 0
|
||||
po.ParentID = parent
|
||||
if err = tx.Create(&po).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
existing = po
|
||||
for _, button := range item.Buttons {
|
||||
if err = tx.Create(&menuButtonPO{Name: button.Name, Description: button.Description, MenuID: po.ID}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, parameter := range item.Parameters {
|
||||
if err = tx.Create(&menuParameterPO{MenuID: po.ID, Type: parameter.Type, Key: parameter.Key, Value: parameter.Value}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = addMenus(item.Children, existing.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
db := r.data.gormDB.WithContext(ctx)
|
||||
if len(b.Menus) > 0 {
|
||||
if err := db.Transaction(func(tx *gorm.DB) error { return importVersionMenus(tx, b.Menus, 0) }); err != nil {
|
||||
return &biz.VersionStageError{Stage: biz.VersionStageMenus, Err: err}
|
||||
}
|
||||
if err := addMenus(b.Menus, 0); err != nil {
|
||||
}
|
||||
if len(b.APIs) > 0 {
|
||||
if err := db.Transaction(func(tx *gorm.DB) error { return importVersionAPIs(tx, b.APIs) }); err != nil {
|
||||
return &biz.VersionStageError{Stage: biz.VersionStageAPIs, Err: err}
|
||||
}
|
||||
}
|
||||
if len(b.Dictionaries) > 0 {
|
||||
if err := db.Transaction(func(tx *gorm.DB) error { return importVersionDictionaries(tx, b.Dictionaries) }); err != nil {
|
||||
return &biz.VersionStageError{Stage: biz.VersionStageDictionaries, Err: err}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func importVersionMenus(tx *gorm.DB, items []*biz.Menu, parentID uint) error {
|
||||
for _, item := range items {
|
||||
var existing menuPO
|
||||
if err := tx.Where("name = ? AND path = ?", item.Name, item.Path).First(&existing).Error; err == nil {
|
||||
if err = importVersionMenus(tx, item.Children, existing.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
po := menuToPO(item)
|
||||
po.ID = 0
|
||||
po.ParentID = parentID
|
||||
if err := tx.Create(&po).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, item := range b.APIs {
|
||||
po := apiPO{Path: item.Path, Description: item.Description, APIGroup: item.APIGroup, Method: strings.ToUpper(item.Method)}
|
||||
if err := tx.Where("path = ? AND method = ?", po.Path, po.Method).FirstOrCreate(&po).Error; err != nil {
|
||||
for _, parameter := range item.Parameters {
|
||||
if err := tx.Create(&menuParameterPO{MenuID: po.ID, Type: parameter.Type, Key: parameter.Key, Value: parameter.Value}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, item := range b.Dictionaries {
|
||||
var count int64
|
||||
if err := tx.Model(&dictionaryPO{}).Where("type = ?", item.Type).Count(&count).Error; err != nil {
|
||||
for _, button := range item.Buttons {
|
||||
if err := tx.Create(&menuButtonPO{Name: button.Name, Description: button.Description, MenuID: po.ID}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
continue
|
||||
}
|
||||
po := dictionaryPO{Name: item.Name, Type: item.Type, Status: item.Status, Desc: item.Desc}
|
||||
if err := tx.Create(&po).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, detail := range item.Details {
|
||||
d := dictionaryDetailPO{Label: detail.Label, Value: detail.Value, Extend: detail.Extend, Status: detail.Status, Sort: detail.Sort, DictionaryID: po.ID}
|
||||
if err := tx.Create(&d).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err := importVersionMenus(tx, item.Children, po.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func importVersionAPIs(tx *gorm.DB, items []*biz.API) error {
|
||||
for _, item := range items {
|
||||
var existing apiPO
|
||||
if err := tx.Where("path = ? AND method = ?", item.Path, item.Method).First(&existing).Error; err == nil {
|
||||
continue
|
||||
}
|
||||
if err := tx.Create(&apiPO{Path: item.Path, Description: item.Description, APIGroup: item.APIGroup, Method: item.Method}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func importVersionDictionaries(tx *gorm.DB, items []*biz.Dictionary) error {
|
||||
for _, item := range items {
|
||||
var existing dictionaryPO
|
||||
if err := tx.Where("type = ?", item.Type).First(&existing).Error; err == nil {
|
||||
continue
|
||||
}
|
||||
po := dictionaryPO{Name: item.Name, Type: item.Type, Status: item.Status, Desc: item.Desc}
|
||||
if err := tx.Create(&po).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, detail := range item.Details {
|
||||
value := dictionaryDetailPO{Label: detail.Label, Value: detail.Value, Extend: detail.Extend, Status: detail.Status, Sort: detail.Sort, DictionaryID: po.ID}
|
||||
if err := tx.Create(&value).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
func NewGinEngine(runtime *conf.Runtime, access *service.AccessControlService, handlers *handler.Set, auth *service.AuthService, security *service.SecurityService, audit *service.AuditRecorder, logger *slog.Logger) *gin.Engine {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
engine := gin.New()
|
||||
engine.Use(servermiddleware.RequestMeta(), servermiddleware.Recovery(audit, logger), servermiddleware.AccessLog(runtime, logger), servermiddleware.ErrorAudit(audit), servermiddleware.SecurityRateLimit(security), servermiddleware.OperationAudit(runtime, audit))
|
||||
engine.Use(servermiddleware.RequestMeta(), servermiddleware.Recovery(audit, logger), servermiddleware.AccessLog(runtime, logger), servermiddleware.ErrorAudit(audit), servermiddleware.SecurityRateLimit(security))
|
||||
|
||||
prefix := ""
|
||||
config := runtime.Admin()
|
||||
@@ -35,7 +35,11 @@ func NewGinEngine(runtime *conf.Runtime, access *service.AccessControlService, h
|
||||
serverrouter.RegisterPublic(public, engine, handlers.Public)
|
||||
|
||||
private := engine.Group(prefix)
|
||||
private.Use(servermiddleware.Auth(auth), servermiddleware.MustChangePassword(), servermiddleware.AccessControl(runtime, access, audit))
|
||||
// The reference administration behavior installs operation recording after JWT,
|
||||
// password and Casbin/data-scope middleware. Keeping the recorder after the
|
||||
// equivalent Kra guards avoids persisting rejected unauthenticated or
|
||||
// unauthorized requests as successful business operations.
|
||||
private.Use(servermiddleware.Auth(auth), servermiddleware.MustChangePassword(), servermiddleware.AccessControl(runtime, access), servermiddleware.OperationAudit(runtime, audit))
|
||||
serverrouter.RegisterUser(private, handlers.User)
|
||||
serverrouter.RegisterNavigation(private, handlers.Navigation)
|
||||
serverrouter.RegisterSession(private, handlers.Session)
|
||||
|
||||
@@ -35,8 +35,16 @@ func (h *Announcement) Create(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (h *Announcement) Delete(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
|
||||
if err := h.service.Delete(c.Request.Context(), uint(id)); err != nil {
|
||||
id, present, err := queryUintValue(c, "ID")
|
||||
if err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if !present {
|
||||
// Preserve the legacy endpoint behavior for an omitted ID.
|
||||
id = 0
|
||||
}
|
||||
if err := h.service.Delete(c.Request.Context(), id); err != nil {
|
||||
httpx.Fail(c, "删除失败")
|
||||
return
|
||||
}
|
||||
@@ -78,8 +86,12 @@ func (h *Announcement) Update(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (h *Announcement) Find(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
|
||||
item, err := h.service.Find(c.Request.Context(), uint(id))
|
||||
id, _, err := queryUintValue(c, "ID")
|
||||
if err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
item, err := h.service.Find(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "查询失败")
|
||||
return
|
||||
@@ -88,20 +100,17 @@ func (h *Announcement) Find(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (h *Announcement) List(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.Query("page"))
|
||||
pageSize, _ := strconv.Atoi(c.Query("pageSize"))
|
||||
start, startErr := parseTime(c.Query("startCreatedAt"))
|
||||
end, endErr := parseTime(c.Query("endCreatedAt"))
|
||||
if startErr != nil || endErr != nil || (start == nil) != (end == nil) {
|
||||
httpx.Fail(c, "创建日期范围不合法")
|
||||
var req dto.AnnouncementSearchRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
items, total, err := h.service.List(c.Request.Context(), page, pageSize, start, end)
|
||||
items, total, err := h.service.List(c.Request.Context(), req.Page, req.PageSize, req.StartCreatedAt, req.EndCreatedAt)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: page, PageSize: pageSize}, "获取成功")
|
||||
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功")
|
||||
}
|
||||
|
||||
func (h *Announcement) DataSource(c *gin.Context) {
|
||||
|
||||
@@ -18,6 +18,14 @@ func (h *API) List(c *gin.Context) {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.Page == 0 {
|
||||
httpx.Fail(c, "Page值不能为空")
|
||||
return
|
||||
}
|
||||
if req.PageSize == 0 {
|
||||
httpx.Fail(c, "PageSize值不能为空")
|
||||
return
|
||||
}
|
||||
items, total, err := h.service.ListAPI(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败")
|
||||
@@ -39,6 +47,10 @@ func (h *API) Create(c *gin.Context) {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if message := validateAPIRequest(&req); message != "" {
|
||||
httpx.Fail(c, message)
|
||||
return
|
||||
}
|
||||
created, err := h.service.CreateAPIRequest(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "创建失败")
|
||||
@@ -52,6 +64,10 @@ func (h *API) Update(c *gin.Context) {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if message := validateAPIRequest(&req); message != "" {
|
||||
httpx.Fail(c, message)
|
||||
return
|
||||
}
|
||||
if err := h.service.UpdateAPIRequest(c.Request.Context(), &req); err != nil {
|
||||
httpx.Fail(c, "修改失败")
|
||||
return
|
||||
@@ -64,7 +80,11 @@ func (h *API) Delete(c *gin.Context) {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.DeleteAPIs(c.Request.Context(), []uint{req.ID}); err != nil {
|
||||
if req.ID == 0 {
|
||||
httpx.Fail(c, "ID值不能为空")
|
||||
return
|
||||
}
|
||||
if err := h.service.DeleteAPI(c.Request.Context(), req.ID); err != nil {
|
||||
httpx.Fail(c, "删除失败")
|
||||
return
|
||||
}
|
||||
@@ -88,6 +108,10 @@ func (h *API) Find(c *gin.Context) {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.ID == 0 {
|
||||
httpx.Fail(c, "ID值不能为空")
|
||||
return
|
||||
}
|
||||
item, err := h.service.FindAPIResponse(c.Request.Context(), req.ID)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败")
|
||||
@@ -156,10 +180,6 @@ func (h *API) Ignore(c *gin.Context) {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.Path == "" || req.Method == "" {
|
||||
httpx.Fail(c, "参数错误")
|
||||
return
|
||||
}
|
||||
if err := h.service.SetAPIIgnored(c.Request.Context(), req.Path, req.Method, req.Flag); err != nil {
|
||||
httpx.Fail(c, "忽略失败")
|
||||
return
|
||||
@@ -173,12 +193,15 @@ func (h *API) ApplySync(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
if err := h.service.ApplyAPISyncRequest(c.Request.Context(), &req); err != nil {
|
||||
httpx.Fail(c, "同步失败")
|
||||
httpx.Fail(c, "忽略失败")
|
||||
return
|
||||
}
|
||||
httpx.OK(c)
|
||||
}
|
||||
func (h *API) FreshCasbin(c *gin.Context) {
|
||||
// Policies are read from casbin_rule on every authorization decision, so
|
||||
// there is no in-memory enforcer cache to reload. Keep the compatible
|
||||
// endpoint and success response.
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "刷新成功")
|
||||
}
|
||||
|
||||
@@ -188,6 +211,10 @@ func (h *API) SetPolicyPaths(c *gin.Context) {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.AuthorityID == 0 {
|
||||
httpx.Fail(c, "AuthorityId值不能为空")
|
||||
return
|
||||
}
|
||||
if err := h.service.SetPolicyPathsRequest(c.Request.Context(), &req); err != nil {
|
||||
httpx.Fail(c, "更新失败")
|
||||
return
|
||||
@@ -200,6 +227,10 @@ func (h *API) PolicyPaths(c *gin.Context) {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.AuthorityID == 0 {
|
||||
httpx.Fail(c, "AuthorityId值不能为空")
|
||||
return
|
||||
}
|
||||
paths, err := h.service.PolicyPathResponses(c.Request.Context(), req.AuthorityID)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败")
|
||||
@@ -207,3 +238,18 @@ func (h *API) PolicyPaths(c *gin.Context) {
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{"paths": paths}, "获取成功")
|
||||
}
|
||||
|
||||
func validateAPIRequest(req *dto.APIRequest) string {
|
||||
switch {
|
||||
case req.Path == "":
|
||||
return "Path值不能为空"
|
||||
case req.Description == "":
|
||||
return "Description值不能为空"
|
||||
case req.APIGroup == "":
|
||||
return "ApiGroup值不能为空"
|
||||
case req.Method == "":
|
||||
return "Method值不能为空"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
@@ -28,6 +29,15 @@ func page(c *gin.Context) (int, int) {
|
||||
size, _ := strconv.Atoi(c.Query("pageSize"))
|
||||
return value, size
|
||||
}
|
||||
|
||||
func queryUintValue(c *gin.Context, key string) (uint, bool, error) {
|
||||
raw, ok := c.GetQuery(key)
|
||||
if !ok || raw == "" {
|
||||
return 0, false, nil
|
||||
}
|
||||
value, err := strconv.ParseUint(raw, 10, 0)
|
||||
return uint(value), true, err
|
||||
}
|
||||
func IDsFromQuery(c *gin.Context) []uint {
|
||||
values := c.QueryArray("IDs[]")
|
||||
if len(values) == 0 {
|
||||
@@ -43,19 +53,57 @@ func IDsFromQuery(c *gin.Context) []uint {
|
||||
return ids
|
||||
}
|
||||
|
||||
// auditID accepts both forms used by the GVA web client over time: DELETE
|
||||
// requests may carry the identifier in the query string or in a JSON body.
|
||||
func auditID(c *gin.Context) (uint, error) {
|
||||
if raw := c.Query("ID"); raw != "" {
|
||||
value, err := strconv.ParseUint(raw, 10, 64)
|
||||
return uint(value), err
|
||||
}
|
||||
var request dto.IDRequest
|
||||
if err := c.ShouldBindJSON(&request); err != nil && !errors.Is(err, io.EOF) {
|
||||
return 0, err
|
||||
}
|
||||
return request.ID, nil
|
||||
}
|
||||
|
||||
// auditIDs mirrors auditID for bulk deletes and supports both query-array and
|
||||
// JSON-body encodings used by compatible GVA pages.
|
||||
func auditIDs(c *gin.Context) ([]uint, error) {
|
||||
if ids := IDsFromQuery(c); len(ids) > 0 {
|
||||
return ids, nil
|
||||
}
|
||||
var request dto.IDsRequest
|
||||
if err := c.ShouldBindJSON(&request); err != nil && !errors.Is(err, io.EOF) {
|
||||
return nil, err
|
||||
}
|
||||
return request.IDs, nil
|
||||
}
|
||||
|
||||
func (h *Audit) Operations(c *gin.Context) {
|
||||
p, size := page(c)
|
||||
status, _ := strconv.Atoi(c.Query("status"))
|
||||
items, total, err := h.service.OperationsFilter(c.Request.Context(), p, size, c.Query("path"), c.Query("method"), status)
|
||||
var req dto.OperationRecordSearchRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
items, total, err := h.service.OperationsFilter(c.Request.Context(), req.Page, req.PageSize, req.Path, req.Method, req.Status)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: p, PageSize: size}, "获取成功")
|
||||
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功")
|
||||
}
|
||||
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))
|
||||
var req dto.AuditIDQuery
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.ID == 0 {
|
||||
httpx.Fail(c, "ID值不能为空")
|
||||
return
|
||||
}
|
||||
item, err := h.service.Operation(c.Request.Context(), req.ID)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "查询失败")
|
||||
return
|
||||
@@ -63,24 +111,28 @@ func (h *Audit) Operation(c *gin.Context) {
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{"reSysOperationRecord": item}, "查询成功")
|
||||
}
|
||||
func (h *Audit) DeleteOperation(c *gin.Context) {
|
||||
var req dto.IDRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
id, err := auditID(c)
|
||||
if err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.DeleteOperations(c.Request.Context(), []uint{req.ID}); err != nil {
|
||||
if id == 0 {
|
||||
httpx.Fail(c, "删除失败")
|
||||
return
|
||||
}
|
||||
if err := h.service.DeleteOperations(c.Request.Context(), []uint{id}); err != nil {
|
||||
httpx.Fail(c, "删除失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
|
||||
}
|
||||
func (h *Audit) DeleteOperations(c *gin.Context) {
|
||||
var req dto.IDsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
ids, err := auditIDs(c)
|
||||
if err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.DeleteOperations(c.Request.Context(), req.IDs); err != nil {
|
||||
if err := h.service.DeleteOperations(c.Request.Context(), ids); err != nil {
|
||||
httpx.Fail(c, "批量删除失败")
|
||||
return
|
||||
}
|
||||
@@ -88,18 +140,25 @@ func (h *Audit) DeleteOperations(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (h *Audit) Logins(c *gin.Context) {
|
||||
p, size := page(c)
|
||||
status, _ := strconv.ParseBool(c.Query("status"))
|
||||
items, total, err := h.service.LoginsFilter(c.Request.Context(), p, size, c.Query("username"), status)
|
||||
var req dto.LoginLogSearchRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
items, total, err := h.service.LoginsFilter(c.Request.Context(), req.Page, req.PageSize, req.Username, req.Status)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: p, PageSize: size}, "获取成功")
|
||||
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功")
|
||||
}
|
||||
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))
|
||||
var req dto.AuditIDQuery
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
item, err := h.service.Login(c.Request.Context(), req.ID)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "查询失败")
|
||||
return
|
||||
@@ -107,24 +166,28 @@ func (h *Audit) Login(c *gin.Context) {
|
||||
httpx.Write(c, httpx.CodeSuccess, item, "查询成功")
|
||||
}
|
||||
func (h *Audit) DeleteLogin(c *gin.Context) {
|
||||
var req dto.IDRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
id, err := auditID(c)
|
||||
if err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.DeleteLogins(c.Request.Context(), []uint{req.ID}); err != nil {
|
||||
if id == 0 {
|
||||
httpx.Fail(c, "删除失败")
|
||||
return
|
||||
}
|
||||
if err := h.service.DeleteLogins(c.Request.Context(), []uint{id}); err != nil {
|
||||
httpx.Fail(c, "删除失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
|
||||
}
|
||||
func (h *Audit) DeleteLogins(c *gin.Context) {
|
||||
var req dto.IDsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
ids, err := auditIDs(c)
|
||||
if err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.DeleteLogins(c.Request.Context(), req.IDs); err != nil {
|
||||
if err := h.service.DeleteLogins(c.Request.Context(), ids); err != nil {
|
||||
httpx.Fail(c, "批量删除失败")
|
||||
return
|
||||
}
|
||||
@@ -133,8 +196,8 @@ func (h *Audit) DeleteLogins(c *gin.Context) {
|
||||
|
||||
func (h *Audit) DataAccess(c *gin.Context) {
|
||||
var req dto.DataAccessListRequest
|
||||
if c.ShouldBindJSON(&req) != nil {
|
||||
httpx.Fail(c, "参数错误")
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
items, total, err := h.service.DataAccessRequest(c.Request.Context(), &req)
|
||||
@@ -146,8 +209,8 @@ func (h *Audit) DataAccess(c *gin.Context) {
|
||||
}
|
||||
func (h *Audit) DeleteDataAccess(c *gin.Context) {
|
||||
var req dto.IDsRequest
|
||||
if c.ShouldBindJSON(&req) != nil {
|
||||
httpx.Fail(c, "参数错误")
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.DeleteDataAccess(c.Request.Context(), req.IDs); err != nil {
|
||||
@@ -158,7 +221,14 @@ func (h *Audit) DeleteDataAccess(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (h *Audit) LogDates(c *gin.Context) {
|
||||
data, err := h.logs.LogDates(c.Request.Context(), c.Query("month"))
|
||||
month := c.Query("month")
|
||||
// Month uses `binding:"required"`; an omitted or empty query is a
|
||||
// generic parameter error, not the service's format-specific error.
|
||||
if month == "" {
|
||||
httpx.Fail(c, "参数错误")
|
||||
return
|
||||
}
|
||||
data, err := h.logs.LogDates(c.Request.Context(), month)
|
||||
if err != nil {
|
||||
failLogViewer(c, err)
|
||||
return
|
||||
@@ -166,7 +236,12 @@ func (h *Audit) LogDates(c *gin.Context) {
|
||||
httpx.Write(c, httpx.CodeSuccess, data, "获取成功")
|
||||
}
|
||||
func (h *Audit) LogFiles(c *gin.Context) {
|
||||
data, err := h.logs.LogFiles(c.Request.Context(), c.Query("date"))
|
||||
date := c.Query("date")
|
||||
if date == "" {
|
||||
httpx.Fail(c, "参数错误")
|
||||
return
|
||||
}
|
||||
data, err := h.logs.LogFiles(c.Request.Context(), date)
|
||||
if err != nil {
|
||||
failLogViewer(c, err)
|
||||
return
|
||||
@@ -174,16 +249,23 @@ func (h *Audit) LogFiles(c *gin.Context) {
|
||||
httpx.Write(c, httpx.CodeSuccess, data, "获取成功")
|
||||
}
|
||||
func (h *Audit) LogContent(c *gin.Context) {
|
||||
date, path := c.Query("date"), c.Query("path")
|
||||
if date == "" || path == "" {
|
||||
httpx.Fail(c, "参数错误")
|
||||
return
|
||||
}
|
||||
var cursor *int64
|
||||
if raw := c.Query("cursor"); raw != "" {
|
||||
value, err := strconv.ParseInt(raw, 10, 64)
|
||||
if err != nil || value < 0 {
|
||||
if err != nil {
|
||||
httpx.Fail(c, "参数错误")
|
||||
return
|
||||
}
|
||||
cursor = &value
|
||||
}
|
||||
data, err := h.logs.LogContent(c.Request.Context(), c.Query("date"), c.Query("path"), cursor)
|
||||
// Preserve negative cursors for the usecase to classify with the compatible
|
||||
// "日志文件路径不合法" error rather than rejecting them as binding errors.
|
||||
data, err := h.logs.LogContent(c.Request.Context(), date, path, cursor)
|
||||
if err != nil {
|
||||
failLogViewer(c, err)
|
||||
return
|
||||
@@ -211,15 +293,24 @@ func failLogViewer(c *gin.Context, err error) {
|
||||
}
|
||||
|
||||
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 {
|
||||
id, err := auditID(c)
|
||||
if err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.DeleteErrors(c.Request.Context(), []uint{id}); err != nil {
|
||||
httpx.Fail(c, "删除失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
|
||||
}
|
||||
func (h *Audit) DeleteErrors(c *gin.Context) {
|
||||
if err := h.service.DeleteErrors(c.Request.Context(), IDsFromQuery(c)); err != nil {
|
||||
ids, err := auditIDs(c)
|
||||
if err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.DeleteErrors(c.Request.Context(), ids); err != nil {
|
||||
httpx.Fail(c, "批量删除失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
@@ -227,8 +318,8 @@ func (h *Audit) DeleteErrors(c *gin.Context) {
|
||||
}
|
||||
func (h *Audit) UpdateError(c *gin.Context) {
|
||||
var req dto.ErrorRecordRequest
|
||||
if c.ShouldBindJSON(&req) != nil {
|
||||
httpx.Fail(c, "参数错误")
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.UpdateErrorRequest(c.Request.Context(), &req); err != nil {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"kra/internal/server/httpx"
|
||||
"kra/internal/service"
|
||||
"kra/internal/service/dto"
|
||||
@@ -17,15 +15,23 @@ func NewAuthority(service *service.AuthorityService) *Authority { return &Author
|
||||
func (h *Authority) List(c *gin.Context) {
|
||||
items, err := h.service.Authorities(c.Request.Context())
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败")
|
||||
httpx.Fail(c, "获取失败"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, items, "获取成功")
|
||||
}
|
||||
func (h *Authority) Create(c *gin.Context) {
|
||||
var req dto.AuthorityRequest
|
||||
if c.ShouldBindJSON(&req) != nil || req.AuthorityID == 0 {
|
||||
httpx.Fail(c, "参数错误")
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.AuthorityID == 0 {
|
||||
httpx.Fail(c, "AuthorityId值不能为空")
|
||||
return
|
||||
}
|
||||
if req.AuthorityName == "" {
|
||||
httpx.Fail(c, "AuthorityName值不能为空")
|
||||
return
|
||||
}
|
||||
value, err := h.service.CreateAuthorityRequest(c.Request.Context(), &req)
|
||||
@@ -37,8 +43,20 @@ func (h *Authority) Create(c *gin.Context) {
|
||||
}
|
||||
func (h *Authority) Copy(c *gin.Context) {
|
||||
var req dto.CopyAuthorityRequest
|
||||
if c.ShouldBindJSON(&req) != nil || req.Authority.AuthorityID == 0 || req.OldAuthorityID == 0 {
|
||||
httpx.Fail(c, "参数错误")
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.OldAuthorityID == 0 {
|
||||
httpx.Fail(c, "OldAuthorityId值不能为空")
|
||||
return
|
||||
}
|
||||
if req.Authority.AuthorityID == 0 {
|
||||
httpx.Fail(c, "AuthorityId值不能为空")
|
||||
return
|
||||
}
|
||||
if req.Authority.AuthorityName == "" {
|
||||
httpx.Fail(c, "AuthorityName值不能为空")
|
||||
return
|
||||
}
|
||||
value, err := h.service.CopyAuthorityRequest(c.Request.Context(), &req)
|
||||
@@ -50,8 +68,16 @@ func (h *Authority) Copy(c *gin.Context) {
|
||||
}
|
||||
func (h *Authority) Update(c *gin.Context) {
|
||||
var req dto.AuthorityRequest
|
||||
if c.ShouldBindJSON(&req) != nil {
|
||||
httpx.Fail(c, "参数错误")
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.AuthorityID == 0 {
|
||||
httpx.Fail(c, "AuthorityId值不能为空")
|
||||
return
|
||||
}
|
||||
if req.AuthorityName == "" {
|
||||
httpx.Fail(c, "AuthorityName值不能为空")
|
||||
return
|
||||
}
|
||||
value, err := h.service.UpdateAuthorityRequest(c.Request.Context(), &req)
|
||||
@@ -63,8 +89,12 @@ func (h *Authority) Update(c *gin.Context) {
|
||||
}
|
||||
func (h *Authority) Delete(c *gin.Context) {
|
||||
var req dto.DeleteAuthorityRequest
|
||||
if c.ShouldBindJSON(&req) != nil {
|
||||
httpx.Fail(c, "参数错误")
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.AuthorityID == 0 {
|
||||
httpx.Fail(c, "AuthorityId值不能为空")
|
||||
return
|
||||
}
|
||||
if err := h.service.DeleteAuthority(c.Request.Context(), req.AuthorityID); err != nil {
|
||||
@@ -75,8 +105,12 @@ func (h *Authority) Delete(c *gin.Context) {
|
||||
}
|
||||
func (h *Authority) SetUsers(c *gin.Context) {
|
||||
var req dto.SetRoleUsersRequest
|
||||
if c.ShouldBindJSON(&req) != nil {
|
||||
httpx.Fail(c, "参数错误")
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.AuthorityID == 0 {
|
||||
httpx.Fail(c, "角色ID不能为空")
|
||||
return
|
||||
}
|
||||
if err := h.service.SetAuthorityUsers(c.Request.Context(), req.AuthorityID, req.UserIDs); err != nil {
|
||||
@@ -86,8 +120,12 @@ func (h *Authority) SetUsers(c *gin.Context) {
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "设置成功")
|
||||
}
|
||||
func (h *Authority) Users(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Query("authorityId"), 10, 64)
|
||||
ids, err := h.service.AuthorityUserIDs(c.Request.Context(), uint(id))
|
||||
var req dto.SetRoleUsersRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
ids, err := h.service.AuthorityUserIDs(c.Request.Context(), req.AuthorityID)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败"+err.Error())
|
||||
return
|
||||
@@ -99,8 +137,12 @@ func (h *Authority) Users(c *gin.Context) {
|
||||
}
|
||||
func (h *Authority) SetDataScope(c *gin.Context) {
|
||||
var req dto.SetDataScopeRequest
|
||||
if c.ShouldBindJSON(&req) != nil {
|
||||
httpx.Fail(c, "参数错误")
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.AuthorityID == 0 {
|
||||
httpx.Fail(c, "角色ID不能为空")
|
||||
return
|
||||
}
|
||||
if err := h.service.SetDataScope(c.Request.Context(), req.AuthorityID, req.DataScope, req.DepartmentIDs); err != nil {
|
||||
@@ -110,8 +152,12 @@ func (h *Authority) SetDataScope(c *gin.Context) {
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "设置成功")
|
||||
}
|
||||
func (h *Authority) DataScopeDepartments(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Query("authorityId"), 10, 64)
|
||||
ids, err := h.service.DataScopeDepartmentIDs(c.Request.Context(), uint(id))
|
||||
var req dto.SetDataScopeRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
ids, err := h.service.DataScopeDepartmentIDs(c.Request.Context(), req.AuthorityID)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败"+err.Error())
|
||||
return
|
||||
|
||||
@@ -25,7 +25,9 @@ func (h *Media) Upload(c *gin.Context) {
|
||||
}
|
||||
opened, err := header.Open()
|
||||
if err != nil {
|
||||
httpx.Fail(c, "读取文件失败")
|
||||
// Let the upload service surface this as the generic upload
|
||||
// failure; keep the endpoint message consistent.
|
||||
httpx.Fail(c, "上传文件失败")
|
||||
return
|
||||
}
|
||||
defer opened.Close()
|
||||
@@ -41,8 +43,8 @@ func (h *Media) Upload(c *gin.Context) {
|
||||
|
||||
func (h *Media) List(c *gin.Context) {
|
||||
var req dto.MediaListRequest
|
||||
if c.ShouldBindJSON(&req) != nil {
|
||||
httpx.Fail(c, "参数错误")
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
items, total, err := h.service.MediaList(c.Request.Context(), biz.MediaFilter{Page: req.Page, PageSize: req.PageSize, Keyword: req.Keyword, CategoryID: req.ClassID, Tag: req.Tag, UserID: req.UserID, StartCreatedAt: req.StartCreatedAt, EndCreatedAt: req.EndCreatedAt, OrderKey: req.OrderKey, Desc: req.Desc})
|
||||
@@ -54,8 +56,8 @@ func (h *Media) List(c *gin.Context) {
|
||||
}
|
||||
func (h *Media) Delete(c *gin.Context) {
|
||||
var req dto.IDRequest
|
||||
if c.ShouldBindJSON(&req) != nil {
|
||||
httpx.Fail(c, "参数错误")
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.Delete(c.Request.Context(), req.ID); err != nil {
|
||||
@@ -66,8 +68,8 @@ func (h *Media) Delete(c *gin.Context) {
|
||||
}
|
||||
func (h *Media) DeleteMany(c *gin.Context) {
|
||||
var req dto.IDsRequest
|
||||
if c.ShouldBindJSON(&req) != nil {
|
||||
httpx.Fail(c, "参数错误")
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
failed := []uint{}
|
||||
@@ -97,8 +99,8 @@ func (h *Media) Find(c *gin.Context) {
|
||||
}
|
||||
func (h *Media) Rename(c *gin.Context) {
|
||||
var req dto.RenameMediaRequest
|
||||
if c.ShouldBindJSON(&req) != nil {
|
||||
httpx.Fail(c, "参数错误")
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.Rename(c.Request.Context(), req.ID, req.Name); err != nil {
|
||||
@@ -109,8 +111,8 @@ func (h *Media) Rename(c *gin.Context) {
|
||||
}
|
||||
func (h *Media) ImportURLs(c *gin.Context) {
|
||||
var req []dto.ImportMediaRequest
|
||||
if c.ShouldBindJSON(&req) != nil {
|
||||
httpx.Fail(c, "参数错误")
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.ImportURLRequests(c.Request.Context(), req); err != nil {
|
||||
@@ -121,8 +123,8 @@ func (h *Media) ImportURLs(c *gin.Context) {
|
||||
}
|
||||
func (h *Media) Storage(c *gin.Context) {
|
||||
var req dto.StorageListRequest
|
||||
if c.ShouldBindJSON(&req) != nil {
|
||||
httpx.Fail(c, "参数错误")
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
items, next, more, err := h.service.Storage(c.Request.Context(), req.Prefix, req.Cursor, req.Limit)
|
||||
@@ -143,7 +145,7 @@ func (h *Media) Categories(c *gin.Context) {
|
||||
}
|
||||
func (h *Media) SaveCategory(c *gin.Context) {
|
||||
var req dto.CategoryRequest
|
||||
if c.ShouldBindJSON(&req) != nil {
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, "参数错误")
|
||||
return
|
||||
}
|
||||
@@ -169,10 +171,14 @@ func (h *Media) DeleteCategory(c *gin.Context) {
|
||||
func (h *Media) InitUpload(c *gin.Context) {
|
||||
claims := servermiddleware.Claims(c)
|
||||
var req dto.InitUploadRequest
|
||||
if claims == nil || c.ShouldBindJSON(&req) != nil {
|
||||
if claims == nil {
|
||||
httpx.Fail(c, "参数错误")
|
||||
return
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if config := h.service.MediaConfig(); config.MaxFileSize > 0 && req.FileSize > config.MaxFileSize {
|
||||
httpx.Fail(c, "文件超过大小上限")
|
||||
return
|
||||
@@ -195,7 +201,7 @@ func (h *Media) SaveChunk(c *gin.Context) {
|
||||
}
|
||||
opened, err := header.Open()
|
||||
if err != nil {
|
||||
httpx.Fail(c, "读取分片失败")
|
||||
httpx.Fail(c, "分片读取失败")
|
||||
return
|
||||
}
|
||||
defer opened.Close()
|
||||
@@ -208,10 +214,14 @@ func (h *Media) SaveChunk(c *gin.Context) {
|
||||
func (h *Media) CompleteUpload(c *gin.Context) {
|
||||
claims := servermiddleware.Claims(c)
|
||||
var req dto.CompleteUploadRequest
|
||||
if claims == nil || c.ShouldBindJSON(&req) != nil {
|
||||
if claims == nil {
|
||||
httpx.Fail(c, "参数错误")
|
||||
return
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
item, err := h.service.CompleteUpload(c.Request.Context(), claims.ID, req.UploadID)
|
||||
if err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"kra/internal/server/httpx"
|
||||
"kra/internal/service"
|
||||
"kra/internal/service/dto"
|
||||
@@ -34,8 +32,28 @@ func (h *Menu) Tree(c *gin.Context) {
|
||||
|
||||
func (h *Menu) Create(c *gin.Context) {
|
||||
var req dto.MenuRequest
|
||||
if c.ShouldBindJSON(&req) != nil {
|
||||
httpx.Fail(c, "参数错误")
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.Path == "" {
|
||||
httpx.Fail(c, "Path值不能为空")
|
||||
return
|
||||
}
|
||||
if req.Name == "" {
|
||||
httpx.Fail(c, "Name值不能为空")
|
||||
return
|
||||
}
|
||||
if req.Component == "" {
|
||||
httpx.Fail(c, "Component值不能为空")
|
||||
return
|
||||
}
|
||||
if req.Sort < 0 {
|
||||
httpx.Fail(c, "Sort长度或值不在合法范围,ge=0")
|
||||
return
|
||||
}
|
||||
if req.Meta.Title == "" {
|
||||
httpx.Fail(c, "Title值不能为空")
|
||||
return
|
||||
}
|
||||
if err := h.service.Create(c.Request.Context(), &req); err != nil {
|
||||
@@ -47,8 +65,28 @@ func (h *Menu) Create(c *gin.Context) {
|
||||
|
||||
func (h *Menu) Update(c *gin.Context) {
|
||||
var req dto.MenuRequest
|
||||
if c.ShouldBindJSON(&req) != nil {
|
||||
httpx.Fail(c, "参数错误")
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.Path == "" {
|
||||
httpx.Fail(c, "Path值不能为空")
|
||||
return
|
||||
}
|
||||
if req.Name == "" {
|
||||
httpx.Fail(c, "Name值不能为空")
|
||||
return
|
||||
}
|
||||
if req.Component == "" {
|
||||
httpx.Fail(c, "Component值不能为空")
|
||||
return
|
||||
}
|
||||
if req.Sort < 0 {
|
||||
httpx.Fail(c, "Sort长度或值不在合法范围,ge=0")
|
||||
return
|
||||
}
|
||||
if req.Meta.Title == "" {
|
||||
httpx.Fail(c, "Title值不能为空")
|
||||
return
|
||||
}
|
||||
if err := h.service.Update(c.Request.Context(), &req); err != nil {
|
||||
@@ -60,8 +98,12 @@ func (h *Menu) Update(c *gin.Context) {
|
||||
|
||||
func (h *Menu) Delete(c *gin.Context) {
|
||||
var req dto.DeleteMenuRequest
|
||||
if c.ShouldBindJSON(&req) != nil {
|
||||
httpx.Fail(c, "参数错误")
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.ID == 0 {
|
||||
httpx.Fail(c, "ID值不能为空")
|
||||
return
|
||||
}
|
||||
if err := h.service.Delete(c.Request.Context(), req.ID); err != nil {
|
||||
@@ -73,8 +115,12 @@ func (h *Menu) Delete(c *gin.Context) {
|
||||
|
||||
func (h *Menu) Find(c *gin.Context) {
|
||||
var req dto.GetMenuRequest
|
||||
if c.ShouldBindJSON(&req) != nil {
|
||||
httpx.Fail(c, "参数错误")
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.ID == 0 {
|
||||
httpx.Fail(c, "ID值不能为空")
|
||||
return
|
||||
}
|
||||
item, err := h.service.Find(c.Request.Context(), req.ID)
|
||||
@@ -87,8 +133,12 @@ func (h *Menu) Find(c *gin.Context) {
|
||||
|
||||
func (h *Menu) SetAuthorityMenus(c *gin.Context) {
|
||||
var req dto.SetAuthorityMenusRequest
|
||||
if c.ShouldBindJSON(&req) != nil {
|
||||
httpx.Fail(c, "参数错误")
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.AuthorityID == 0 {
|
||||
httpx.Fail(c, "AuthorityId值不能为空")
|
||||
return
|
||||
}
|
||||
if err := h.service.SetAuthorityMenus(c.Request.Context(), &req); err != nil {
|
||||
@@ -100,8 +150,12 @@ func (h *Menu) SetAuthorityMenus(c *gin.Context) {
|
||||
|
||||
func (h *Menu) AuthorityMenus(c *gin.Context) {
|
||||
var req dto.GetAuthorityMenusRequest
|
||||
if c.ShouldBindJSON(&req) != nil {
|
||||
httpx.Fail(c, "参数错误")
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.AuthorityID == 0 {
|
||||
httpx.Fail(c, "AuthorityId值不能为空")
|
||||
return
|
||||
}
|
||||
menus, err := h.service.AuthorityMenus(c.Request.Context(), req.AuthorityID)
|
||||
@@ -113,18 +167,26 @@ func (h *Menu) AuthorityMenus(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (h *Menu) RoleIDs(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Query("menuId"), 10, 64)
|
||||
ids, err := h.service.RoleIDs(c.Request.Context(), uint(id))
|
||||
var req dto.SetMenuRolesRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.MenuID == 0 {
|
||||
httpx.Fail(c, "菜单ID不能为空")
|
||||
return
|
||||
}
|
||||
ids, err := h.service.RoleIDs(c.Request.Context(), req.MenuID)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败")
|
||||
httpx.Fail(c, "获取失败"+err.Error())
|
||||
return
|
||||
}
|
||||
if ids == nil {
|
||||
ids = []uint{}
|
||||
}
|
||||
defaultIDs, err := h.service.DefaultRouterRoleIDs(c.Request.Context(), uint(id))
|
||||
defaultIDs, err := h.service.DefaultRouterRoleIDs(c.Request.Context(), req.MenuID)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败")
|
||||
httpx.Fail(c, "获取失败"+err.Error())
|
||||
return
|
||||
}
|
||||
if defaultIDs == nil {
|
||||
@@ -135,12 +197,16 @@ func (h *Menu) RoleIDs(c *gin.Context) {
|
||||
|
||||
func (h *Menu) SetRoles(c *gin.Context) {
|
||||
var req dto.SetMenuRolesRequest
|
||||
if c.ShouldBindJSON(&req) != nil {
|
||||
httpx.Fail(c, "参数错误")
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.MenuID == 0 {
|
||||
httpx.Fail(c, "菜单ID不能为空")
|
||||
return
|
||||
}
|
||||
if err := h.service.SetRoles(c.Request.Context(), &req); err != nil {
|
||||
httpx.Fail(c, "设置失败")
|
||||
httpx.Fail(c, "设置失败"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "设置成功")
|
||||
|
||||
@@ -41,7 +41,7 @@ func (h *Organization) CreateDepartment(c *gin.Context) {
|
||||
httpx.Fail(c, "创建失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, nil, "创建成功")
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "创建成功")
|
||||
}
|
||||
func (h *Organization) UpdateDepartment(c *gin.Context) {
|
||||
var req dto.DepartmentRequest
|
||||
@@ -57,7 +57,7 @@ func (h *Organization) UpdateDepartment(c *gin.Context) {
|
||||
httpx.Fail(c, "更新失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, nil, "更新成功")
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "更新成功")
|
||||
}
|
||||
func (h *Organization) DeleteDepartment(c *gin.Context) {
|
||||
var req dto.DeleteDepartmentRequest
|
||||
@@ -69,7 +69,7 @@ func (h *Organization) DeleteDepartment(c *gin.Context) {
|
||||
httpx.Fail(c, "删除失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, nil, "删除成功")
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
|
||||
}
|
||||
func (h *Organization) FindDepartment(c *gin.Context) {
|
||||
var req dto.DeleteDepartmentRequest
|
||||
@@ -122,8 +122,12 @@ func (h *Organization) SetUserDepartments(c *gin.Context) {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.ID == 0 {
|
||||
httpx.Fail(c, "用户ID不能为空")
|
||||
return
|
||||
}
|
||||
if err := h.departments.SetUserDepartments(c.Request.Context(), &req); err != nil {
|
||||
httpx.Fail(c, "设置失败")
|
||||
httpx.Fail(c, "设置失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "设置成功")
|
||||
@@ -156,7 +160,7 @@ func (h *Organization) CreatePosition(c *gin.Context) {
|
||||
httpx.Fail(c, "创建失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, nil, "创建成功")
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "创建成功")
|
||||
}
|
||||
func (h *Organization) UpdatePosition(c *gin.Context) {
|
||||
var req dto.PositionRequest
|
||||
@@ -172,7 +176,7 @@ func (h *Organization) UpdatePosition(c *gin.Context) {
|
||||
httpx.Fail(c, "更新失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, nil, "更新成功")
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "更新成功")
|
||||
}
|
||||
func (h *Organization) DeletePosition(c *gin.Context) {
|
||||
var req dto.DeletePositionRequest
|
||||
@@ -184,7 +188,7 @@ func (h *Organization) DeletePosition(c *gin.Context) {
|
||||
httpx.Fail(c, "删除失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, nil, "删除成功")
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
|
||||
}
|
||||
func (h *Organization) FindPosition(c *gin.Context) {
|
||||
var req dto.DeletePositionRequest
|
||||
@@ -237,8 +241,12 @@ func (h *Organization) SetUserPositions(c *gin.Context) {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.ID == 0 {
|
||||
httpx.Fail(c, "用户ID不能为空")
|
||||
return
|
||||
}
|
||||
if err := h.positions.SetUserPositions(c.Request.Context(), &req); err != nil {
|
||||
httpx.Fail(c, "设置失败")
|
||||
httpx.Fail(c, "设置失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "设置成功")
|
||||
|
||||
@@ -82,17 +82,15 @@ func (h *Parameter) Get(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (h *Parameter) List(c *gin.Context) {
|
||||
p, size := page(c)
|
||||
start, startErr := parseTime(c.Query("startCreatedAt"))
|
||||
end, endErr := parseTime(c.Query("endCreatedAt"))
|
||||
if startErr != nil || endErr != nil {
|
||||
httpx.Fail(c, "参数错误")
|
||||
var req dto.SystemParameterSearchRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
items, total, err := h.service.ParametersFilter(c.Request.Context(), p, size, c.Query("name"), c.Query("key"), start, end)
|
||||
items, total, err := h.service.ParametersFilter(c.Request.Context(), req.Page, req.PageSize, req.Name, req.Key, req.StartCreatedAt, req.EndCreatedAt)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: p, PageSize: size}, "获取成功")
|
||||
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功")
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@ func NewPermission(service *service.PermissionService) *Permission {
|
||||
|
||||
func (h *Permission) Buttons(c *gin.Context) {
|
||||
var req dto.GetAuthorityButtonsRequest
|
||||
if c.ShouldBindJSON(&req) != nil {
|
||||
httpx.Fail(c, "参数错误")
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
selected, err := h.service.SelectedButtons(c.Request.Context(), req.AuthorityID, req.MenuID)
|
||||
@@ -32,8 +32,8 @@ func (h *Permission) Buttons(c *gin.Context) {
|
||||
|
||||
func (h *Permission) SetButtons(c *gin.Context) {
|
||||
var req dto.SetAuthorityButtonsRequest
|
||||
if c.ShouldBindJSON(&req) != nil {
|
||||
httpx.Fail(c, "参数错误")
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.SetSelectedButtons(c.Request.Context(), req.AuthorityID, req.MenuID, req.Selected); err != nil {
|
||||
@@ -47,7 +47,7 @@ func (h *Permission) CanRemoveButton(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Query("id"), 10, 64)
|
||||
allowed, err := h.service.CanRemoveButton(c.Request.Context(), uint(id))
|
||||
if err != nil {
|
||||
httpx.Fail(c, "检查失败")
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if !allowed {
|
||||
|
||||
@@ -53,8 +53,16 @@ func (h *Public) Captcha(c *gin.Context) {
|
||||
|
||||
func (h *Public) Login(c *gin.Context) {
|
||||
var req dto.LoginRequest
|
||||
if c.ShouldBindJSON(&req) != nil || req.Username == "" || req.Password == "" {
|
||||
httpx.Fail(c, "请输入用户名和密码")
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.Username == "" {
|
||||
httpx.Fail(c, "Username值不能为空")
|
||||
return
|
||||
}
|
||||
if req.Password == "" {
|
||||
httpx.Fail(c, "Password值不能为空")
|
||||
return
|
||||
}
|
||||
result, err := h.auth.Login(c.Request.Context(), &req, c.ClientIP(), c.Request.UserAgent())
|
||||
@@ -109,12 +117,12 @@ func (h *Public) InitializeDatabase(engine *gin.Engine) gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
if initialized {
|
||||
httpx.Fail(c, "数据库已初始化,无需重复初始化")
|
||||
httpx.Fail(c, "已存在数据库配置")
|
||||
return
|
||||
}
|
||||
var input service.DatabaseInit
|
||||
if c.ShouldBindJSON(&input) != nil {
|
||||
httpx.Fail(c, "数据库初始化参数无效")
|
||||
httpx.Fail(c, "参数校验不通过")
|
||||
return
|
||||
}
|
||||
routes := engine.Routes()
|
||||
|
||||
@@ -3,7 +3,6 @@ package handler
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"kra/internal/server/httpx"
|
||||
@@ -24,8 +23,8 @@ func NewTask(service *service.TaskService) *Task {
|
||||
|
||||
func (h *Task) Create(c *gin.Context) {
|
||||
var req dto.TaskRequest
|
||||
if c.ShouldBindJSON(&req) != nil {
|
||||
httpx.Fail(c, "参数错误")
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
_, err := h.service.CreateRequest(c.Request.Context(), &req)
|
||||
@@ -38,8 +37,8 @@ func (h *Task) Create(c *gin.Context) {
|
||||
|
||||
func (h *Task) Update(c *gin.Context) {
|
||||
var req dto.TaskRequest
|
||||
if c.ShouldBindJSON(&req) != nil {
|
||||
httpx.Fail(c, "参数错误")
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.UpdateRequest(c.Request.Context(), &req); err != nil {
|
||||
@@ -51,8 +50,8 @@ func (h *Task) Update(c *gin.Context) {
|
||||
|
||||
func (h *Task) Delete(c *gin.Context) {
|
||||
var req dto.DeleteTaskRequest
|
||||
if c.ShouldBindJSON(&req) != nil || req.ID == 0 {
|
||||
httpx.Fail(c, "参数错误")
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.Delete(c.Request.Context(), req.ID); err != nil {
|
||||
@@ -64,8 +63,8 @@ func (h *Task) Delete(c *gin.Context) {
|
||||
|
||||
func (h *Task) Toggle(c *gin.Context) {
|
||||
var req dto.ToggleTaskRequest
|
||||
if c.ShouldBindJSON(&req) != nil {
|
||||
httpx.Fail(c, "参数错误")
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.Toggle(c.Request.Context(), req.ID, req.Enabled); err != nil {
|
||||
@@ -77,8 +76,8 @@ func (h *Task) Toggle(c *gin.Context) {
|
||||
|
||||
func (h *Task) Trigger(c *gin.Context) {
|
||||
var req dto.DeleteTaskRequest
|
||||
if c.ShouldBindJSON(&req) != nil {
|
||||
httpx.Fail(c, "参数错误")
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.Trigger(c.Request.Context(), req.ID); err != nil {
|
||||
@@ -89,31 +88,31 @@ func (h *Task) Trigger(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (h *Task) List(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.Query("page"))
|
||||
size, _ := strconv.Atoi(c.Query("pageSize"))
|
||||
var enabled *bool
|
||||
if raw := c.Query("enabled"); raw != "" {
|
||||
value, _ := strconv.ParseBool(raw)
|
||||
enabled = &value
|
||||
var req dto.TaskListRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
items, total, err := h.service.ListRequest(c.Request.Context(), page, size, c.Query("name"), c.Query("executorType"), enabled)
|
||||
items, total, err := h.service.ListRequest(c.Request.Context(), req.Page, req.PageSize, req.Name, req.ExecutorType, req.Enabled)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: page, PageSize: size}, "获取成功")
|
||||
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功")
|
||||
}
|
||||
|
||||
func (h *Task) Logs(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.Query("page"))
|
||||
size, _ := strconv.Atoi(c.Query("pageSize"))
|
||||
taskID, _ := strconv.ParseUint(c.Query("taskId"), 10, 64)
|
||||
items, total, err := h.service.Logs(c.Request.Context(), page, size, uint(taskID), c.Query("status"))
|
||||
var req dto.TaskLogListRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
items, total, err := h.service.Logs(c.Request.Context(), req.Page, req.PageSize, req.TaskID, req.Status)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: page, PageSize: size}, "获取成功")
|
||||
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功")
|
||||
}
|
||||
|
||||
func (h *Task) Methods(c *gin.Context) {
|
||||
|
||||
@@ -23,8 +23,16 @@ func NewUser(user *service.UserService, auth *service.AuthService) *User {
|
||||
|
||||
func (h *User) List(c *gin.Context) {
|
||||
var req dto.UserListRequest
|
||||
if c.ShouldBindJSON(&req) != nil {
|
||||
httpx.Fail(c, "参数错误")
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.Page == 0 {
|
||||
httpx.Fail(c, "Page值不能为空")
|
||||
return
|
||||
}
|
||||
if req.PageSize == 0 {
|
||||
httpx.Fail(c, "PageSize值不能为空")
|
||||
return
|
||||
}
|
||||
items, total, err := h.service.ListUsersRequest(c.Request.Context(), &req)
|
||||
@@ -37,8 +45,24 @@ func (h *User) List(c *gin.Context) {
|
||||
|
||||
func (h *User) Create(c *gin.Context) {
|
||||
var req dto.UserRequest
|
||||
if c.ShouldBindJSON(&req) != nil || req.Username == "" || req.Password == "" {
|
||||
httpx.Fail(c, "参数错误")
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.Username == "" {
|
||||
httpx.Fail(c, "Username值不能为空")
|
||||
return
|
||||
}
|
||||
if req.Password == "" {
|
||||
httpx.Fail(c, "Password值不能为空")
|
||||
return
|
||||
}
|
||||
if req.NickName == "" {
|
||||
httpx.Fail(c, "NickName值不能为空")
|
||||
return
|
||||
}
|
||||
if req.AuthorityID == 0 {
|
||||
httpx.Fail(c, "AuthorityId值不能为空")
|
||||
return
|
||||
}
|
||||
result, err := h.service.CreateUserRequest(c.Request.Context(), &req)
|
||||
@@ -51,8 +75,12 @@ func (h *User) Create(c *gin.Context) {
|
||||
|
||||
func (h *User) Update(c *gin.Context) {
|
||||
var req dto.UserRequest
|
||||
if c.ShouldBindJSON(&req) != nil {
|
||||
httpx.Fail(c, "参数错误")
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.ID == 0 {
|
||||
httpx.Fail(c, "ID值不能为空")
|
||||
return
|
||||
}
|
||||
if err := h.service.UpdateUserRequest(c.Request.Context(), &req); err != nil {
|
||||
@@ -69,8 +97,8 @@ func (h *User) UpdateSelf(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
var req dto.SelfUserRequest
|
||||
if c.ShouldBindJSON(&req) != nil {
|
||||
httpx.Fail(c, "参数错误")
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
value := service.UserInput{ID: claims.ID, NickName: req.NickName, HeaderImg: req.HeaderImg, Phone: req.Phone, Email: req.Email, Enable: req.Enable}
|
||||
@@ -83,8 +111,12 @@ func (h *User) UpdateSelf(c *gin.Context) {
|
||||
|
||||
func (h *User) Delete(c *gin.Context) {
|
||||
var req dto.IDRequest
|
||||
if c.ShouldBind(&req) != nil || req.ID == 0 {
|
||||
httpx.Fail(c, "参数错误")
|
||||
if err := c.ShouldBind(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.ID == 0 {
|
||||
httpx.Fail(c, "ID值不能为空")
|
||||
return
|
||||
}
|
||||
claims := middleware.Claims(c)
|
||||
@@ -101,8 +133,8 @@ func (h *User) Delete(c *gin.Context) {
|
||||
|
||||
func (h *User) ResetPassword(c *gin.Context) {
|
||||
var req dto.ResetPasswordRequest
|
||||
if c.ShouldBindJSON(&req) != nil {
|
||||
httpx.Fail(c, "参数错误")
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.ResetPassword(c.Request.Context(), req.ID, req.Password); err != nil {
|
||||
@@ -115,8 +147,20 @@ func (h *User) ResetPassword(c *gin.Context) {
|
||||
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, "参数错误")
|
||||
if claims == nil {
|
||||
httpx.NoAuth(c, "未登录或非法访问")
|
||||
return
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.Password == "" {
|
||||
httpx.Fail(c, "Password值不能为空")
|
||||
return
|
||||
}
|
||||
if req.NewPassword == "" {
|
||||
httpx.Fail(c, "NewPassword值不能为空")
|
||||
return
|
||||
}
|
||||
if err := h.service.ChangePassword(c.Request.Context(), claims.ID, req.Password, req.NewPassword); err != nil {
|
||||
@@ -129,8 +173,12 @@ func (h *User) ChangePassword(c *gin.Context) {
|
||||
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, "参数错误")
|
||||
if claims == nil {
|
||||
httpx.NoAuth(c, "未登录或非法访问")
|
||||
return
|
||||
}
|
||||
if err := c.ShouldBindJSON(&setting); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.SetUserSetting(c.Request.Context(), claims.ID, setting); err != nil {
|
||||
@@ -142,8 +190,8 @@ func (h *User) SetSelfSetting(c *gin.Context) {
|
||||
|
||||
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, "参数错误")
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.SetUserAuthorities(c.Request.Context(), req.ID, req.AuthorityIDs); err != nil {
|
||||
@@ -156,11 +204,19 @@ func (h *User) SetAuthorities(c *gin.Context) {
|
||||
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, "参数错误")
|
||||
if claims == nil {
|
||||
httpx.NoAuth(c, "未登录或非法访问")
|
||||
return
|
||||
}
|
||||
login, err := h.auth.SwitchAuthority(c.Request.Context(), claims.ID, req.AuthorityID)
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.AuthorityID == 0 {
|
||||
httpx.Fail(c, "AuthorityId值不能为空")
|
||||
return
|
||||
}
|
||||
login, err := h.auth.SwitchAuthority(c.Request.Context(), claims, req.AuthorityID)
|
||||
if err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"kra/internal/biz"
|
||||
"kra/internal/server/httpx"
|
||||
"kra/internal/service"
|
||||
"kra/internal/service/dto"
|
||||
@@ -17,10 +19,18 @@ type Version struct{ service *service.VersionService }
|
||||
|
||||
func NewVersion(service *service.VersionService) *Version { return &Version{service: service} }
|
||||
|
||||
func versionStage(err error) (biz.VersionStage, bool) {
|
||||
var stageErr *biz.VersionStageError
|
||||
if !errors.As(err, &stageErr) {
|
||||
return "", false
|
||||
}
|
||||
return stageErr.Stage, true
|
||||
}
|
||||
|
||||
func (h *Version) Delete(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
|
||||
if err := h.service.DeleteVersions(c.Request.Context(), []uint{uint(id)}); err != nil {
|
||||
httpx.Fail(c, "删除失败")
|
||||
httpx.Fail(c, "删除失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
|
||||
@@ -33,7 +43,7 @@ func (h *Version) DeleteMany(c *gin.Context) {
|
||||
ids = append(ids, uint(id))
|
||||
}
|
||||
if err := h.service.DeleteVersions(c.Request.Context(), ids); err != nil {
|
||||
httpx.Fail(c, "删除失败")
|
||||
httpx.Fail(c, "批量删除失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "批量删除成功")
|
||||
@@ -42,60 +52,99 @@ 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, "查询失败")
|
||||
httpx.Fail(c, "查询失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.OKWithData(c, item)
|
||||
}
|
||||
func (h *Version) List(c *gin.Context) {
|
||||
p, size := page(c)
|
||||
createdAtRange := make([]*time.Time, 0, 2)
|
||||
for _, raw := range c.QueryArray("createdAtRange[]") {
|
||||
value, parseErr := parseTime(raw)
|
||||
if parseErr != nil {
|
||||
httpx.Fail(c, parseErr.Error())
|
||||
return
|
||||
}
|
||||
createdAtRange = append(createdAtRange, value)
|
||||
}
|
||||
items, total, err := h.service.Versions(c.Request.Context(), p, size, c.Query("versionName"), c.Query("versionCode"), createdAtRange)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败")
|
||||
var req dto.VersionSearchRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: p, PageSize: size}, "获取成功")
|
||||
name, code := "", ""
|
||||
if req.VersionName != nil {
|
||||
name = *req.VersionName
|
||||
}
|
||||
if req.VersionCode != nil {
|
||||
code = *req.VersionCode
|
||||
}
|
||||
createdAtRange := make([]*time.Time, 0, len(req.CreatedAtRange))
|
||||
for i := range req.CreatedAtRange {
|
||||
createdAtRange = append(createdAtRange, &req.CreatedAtRange[i])
|
||||
}
|
||||
items, total, err := h.service.Versions(c.Request.Context(), req.Page, req.PageSize, name, code, createdAtRange)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功")
|
||||
}
|
||||
func (h *Version) Export(c *gin.Context) {
|
||||
var req dto.ExportVersionRequest
|
||||
if c.ShouldBindJSON(&req) != nil || req.VersionName == "" || req.VersionCode == "" {
|
||||
httpx.Fail(c, "参数错误")
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.Export(c.Request.Context(), req.VersionName, req.VersionCode, req.Description, req.MenuIDs, req.APIIDs, req.DictIDs); err != nil {
|
||||
httpx.Fail(c, "创建发版失败:"+err.Error())
|
||||
switch stage, _ := versionStage(err); stage {
|
||||
case biz.VersionStageMenus:
|
||||
httpx.Fail(c, "获取菜单数据失败:"+err.Error())
|
||||
case biz.VersionStageAPIs:
|
||||
httpx.Fail(c, "获取API数据失败:"+err.Error())
|
||||
case biz.VersionStageDictionaries:
|
||||
httpx.Fail(c, "获取字典数据失败:"+err.Error())
|
||||
case biz.VersionStageJSON:
|
||||
httpx.Fail(c, "JSON序列化失败:"+err.Error())
|
||||
case biz.VersionStageSave:
|
||||
httpx.Fail(c, "保存版本记录失败:"+err.Error())
|
||||
default:
|
||||
httpx.Fail(c, "创建发版失败:"+err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "创建发版成功")
|
||||
}
|
||||
func (h *Version) Download(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
|
||||
rawID := c.Query("ID")
|
||||
if rawID == "" {
|
||||
httpx.Fail(c, "版本ID不能为空")
|
||||
return
|
||||
}
|
||||
id, _ := strconv.ParseUint(rawID, 10, 64)
|
||||
raw, code, err := h.service.VersionData(c.Request.Context(), uint(id))
|
||||
if err != nil {
|
||||
httpx.Fail(c, "下载失败")
|
||||
httpx.Fail(c, "获取版本记录失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
name := fmt.Sprintf("version_%s_%s.json", code, time.Now().Format("20060102150405"))
|
||||
c.Header("Content-Type", "application/json")
|
||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", name))
|
||||
c.Header("Content-Length", strconv.Itoa(len(raw)))
|
||||
c.Data(http.StatusOK, "application/json; charset=utf-8", []byte(raw))
|
||||
}
|
||||
func (h *Version) Import(c *gin.Context) {
|
||||
var req dto.ImportVersionRequest
|
||||
if c.ShouldBindJSON(&req) != nil {
|
||||
httpx.Fail(c, "参数错误")
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, "解析JSON数据失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.ImportRequest(c.Request.Context(), &req); err != nil {
|
||||
httpx.Fail(c, "导入失败:"+err.Error())
|
||||
if errors.Is(err, biz.ErrInvalidVersion) {
|
||||
httpx.Fail(c, "版本信息格式错误")
|
||||
return
|
||||
}
|
||||
switch stage, _ := versionStage(err); stage {
|
||||
case biz.VersionStageMenus:
|
||||
httpx.Fail(c, "导入菜单失败: "+err.Error())
|
||||
case biz.VersionStageAPIs:
|
||||
httpx.Fail(c, "导入API失败: "+err.Error())
|
||||
case biz.VersionStageDictionaries:
|
||||
httpx.Fail(c, "导入字典失败: "+err.Error())
|
||||
default:
|
||||
httpx.Fail(c, "导入失败:"+err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "导入成功")
|
||||
|
||||
@@ -7,12 +7,11 @@ import (
|
||||
"kra/internal/conf"
|
||||
"kra/internal/server/httpx"
|
||||
"kra/internal/service"
|
||||
"kra/internal/service/dto"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func AccessControl(runtime *conf.Runtime, access *service.AccessControlService, audit *service.AuditRecorder) gin.HandlerFunc {
|
||||
func AccessControl(runtime *conf.Runtime, access *service.AccessControlService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
claims := Claims(c)
|
||||
if claims == nil {
|
||||
@@ -29,9 +28,6 @@ func AccessControl(runtime *conf.Runtime, access *service.AccessControlService,
|
||||
}
|
||||
allowed, err := access.Authorize(c.Request.Context(), claims.AuthorityID, policyPath, c.Request.Method)
|
||||
if err != nil || !allowed {
|
||||
requestID, _ := c.Get("request_id")
|
||||
requestIDText, _ := requestID.(string)
|
||||
_ = 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"})
|
||||
httpx.Write(c, httpx.CodeError, gin.H{}, "权限不足")
|
||||
c.Abort()
|
||||
return
|
||||
|
||||
@@ -2,7 +2,11 @@ package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -21,11 +25,13 @@ func OperationAudit(runtime *conf.Runtime, service *service.AuditRecorder) gin.H
|
||||
return
|
||||
}
|
||||
var requestBody []byte
|
||||
maxBytes := 32768
|
||||
maxBytes := 1024
|
||||
if config := runtime.Admin(); config != nil && config.Zap != nil && config.Zap.AccessLogMaxBytes > 0 {
|
||||
maxBytes = int(config.Zap.AccessLogMaxBytes)
|
||||
}
|
||||
if c.Request.Body != nil {
|
||||
if c.Request.Method == http.MethodGet {
|
||||
requestBody = operationQueryBody(c.Request.URL.RawQuery)
|
||||
} else if c.Request.Body != nil {
|
||||
if strings.Contains(c.GetHeader("Content-Type"), "multipart/form-data") {
|
||||
requestBody = []byte("[文件]")
|
||||
} else {
|
||||
@@ -33,23 +39,95 @@ func OperationAudit(runtime *conf.Runtime, service *service.AuditRecorder) gin.H
|
||||
c.Request.Body = io.NopCloser(bytes.NewReader(requestBody))
|
||||
}
|
||||
}
|
||||
writer := &captureWriter{ResponseWriter: c.Writer, maxBytes: maxBytes}
|
||||
// The reference middleware captures up to the global 1 MiB response
|
||||
// safety limit and only applies the configured operation-log limit when a
|
||||
// download response is recorded. Using maxBytes here would silently
|
||||
// truncate ordinary JSON responses before that decision is possible.
|
||||
writer := &captureWriter{ResponseWriter: c.Writer, maxBytes: 1 << 20}
|
||||
c.Writer = writer
|
||||
started := time.Now()
|
||||
c.Next()
|
||||
userID := uint(0)
|
||||
if claims := Claims(c); claims != nil {
|
||||
userID = claims.ID
|
||||
} else if value, err := strconv.ParseUint(c.GetHeader("x-user-id"), 10, 64); err == nil {
|
||||
userID = uint(value)
|
||||
}
|
||||
requestID, _ := c.Get("request_id")
|
||||
status, errorMessage := c.Writer.Status(), ""
|
||||
if status >= 400 {
|
||||
errorMessage = writer.body.String()
|
||||
status := c.Writer.Status()
|
||||
responseBody := writer.body.String()
|
||||
if isDownloadResponse(c) && len(responseBody) > maxBytes {
|
||||
responseBody = "[超出记录长度]"
|
||||
}
|
||||
_ = service.RecordOperationRequest(c.Request.Context(), &dto.OperationRecordRequest{IP: c.ClientIP(), Method: c.Request.Method, Path: path, Status: status, LatencyMS: time.Since(started).Milliseconds(), Agent: c.Request.UserAgent(), ErrorMessage: errorMessage, Body: redactJSONLimit(requestBody, maxBytes), Response: writer.body.String(), UserID: userID, RequestID: stringValue(requestID), TraceID: stringValueFromContext(c, "trace_id"), DeviceID: c.GetHeader("X-Device-Id")})
|
||||
errorMessage := c.Errors.ByType(gin.ErrorTypePrivate).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: operationRequestBody(requestBody, c.GetHeader("Content-Type"), maxBytes), Response: responseBody, UserID: userID, RequestID: stringValue(requestID), TraceID: stringValueFromContext(c, "trace_id"), DeviceID: c.GetHeader("X-Device-Id")})
|
||||
}
|
||||
}
|
||||
|
||||
func operationQueryBody(raw string) []byte {
|
||||
query, _ := url.QueryUnescape(raw)
|
||||
values := make(map[string]string)
|
||||
for _, item := range strings.Split(query, "&") {
|
||||
parts := strings.Split(item, "=")
|
||||
if len(parts) == 2 {
|
||||
values[parts[0]] = parts[1]
|
||||
}
|
||||
}
|
||||
body, _ := json.Marshal(&values)
|
||||
return body
|
||||
}
|
||||
|
||||
func operationRequestBody(raw []byte, contentType string, limit int) string {
|
||||
if strings.Contains(contentType, "multipart/form-data") {
|
||||
return "[文件]"
|
||||
}
|
||||
text := string(raw)
|
||||
if strings.Contains(strings.ToLower(contentType), "json") && text != "" {
|
||||
var value any
|
||||
if json.Unmarshal(raw, &value) == nil {
|
||||
maskOperationBody(value)
|
||||
if encoded, err := json.Marshal(value); err == nil {
|
||||
text = string(encoded)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(text) > limit {
|
||||
return "[超出记录长度]"
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
func maskOperationBody(value any) {
|
||||
switch current := value.(type) {
|
||||
case map[string]any:
|
||||
for key, item := range current {
|
||||
normalized := strings.ToLower(strings.ReplaceAll(strings.ReplaceAll(key, "_", ""), "-", ""))
|
||||
if normalized == "password" || normalized == "newpassword" || normalized == "oldpassword" || normalized == "confirmpassword" || normalized == "passwd" || normalized == "pwd" || normalized == "token" || normalized == "accesstoken" || normalized == "refreshtoken" || normalized == "secret" || normalized == "clientsecret" || normalized == "apikey" || normalized == "privatekey" || normalized == "idcard" {
|
||||
current[key] = "***"
|
||||
continue
|
||||
}
|
||||
maskOperationBody(item)
|
||||
}
|
||||
case []any:
|
||||
for _, item := range current {
|
||||
maskOperationBody(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isDownloadResponse(c *gin.Context) bool {
|
||||
header := c.Writer.Header()
|
||||
return strings.Contains(header.Get("Pragma"), "public") ||
|
||||
strings.Contains(header.Get("Expires"), "0") ||
|
||||
strings.Contains(header.Get("Cache-Control"), "must-revalidate, post-check=0, pre-check=0") ||
|
||||
strings.Contains(header.Get("Content-Type"), "application/force-download") ||
|
||||
strings.Contains(header.Get("Content-Type"), "application/octet-stream") ||
|
||||
strings.Contains(header.Get("Content-Type"), "application/vnd.ms-excel") ||
|
||||
strings.Contains(header.Get("Content-Type"), "application/download") ||
|
||||
strings.Contains(header.Get("Content-Disposition"), "attachment") ||
|
||||
strings.Contains(header.Get("Content-Transfer-Encoding"), "binary")
|
||||
}
|
||||
|
||||
// recordsOperation mirrors the routes on which operation records are enabled.
|
||||
// Matching by suffix keeps the behavior stable when router-prefix is configured.
|
||||
func recordsOperation(method, path string) bool {
|
||||
@@ -82,7 +160,7 @@ var operationRoutes = func() map[string]struct{} {
|
||||
"DELETE /sysVersion/deleteSysVersion", "DELETE /sysVersion/deleteSysVersionByIds", "POST /sysVersion/exportVersion", "POST /sysVersion/importVersion",
|
||||
"POST /sysExportTemplate/createSysExportTemplate", "DELETE /sysExportTemplate/deleteSysExportTemplate", "DELETE /sysExportTemplate/deleteSysExportTemplateByIds", "PUT /sysExportTemplate/updateSysExportTemplate", "POST /sysExportTemplate/importExcel",
|
||||
"DELETE /sysError/deleteSysError", "DELETE /sysError/deleteSysErrorByIds", "PUT /sysError/updateSysError",
|
||||
"DELETE /sysLoginLog/deleteLoginLog", "DELETE /sysLoginLog/deleteLoginLogByIds", "DELETE /sysOperationRecord/deleteSysOperationRecord", "DELETE /sysOperationRecord/deleteSysOperationRecordByIds", "DELETE /dataAccessLog/deleteDataAccessLogByIds",
|
||||
"DELETE /sysLoginLog/deleteLoginLog", "DELETE /sysLoginLog/deleteLoginLogByIds", "DELETE /dataAccessLog/deleteDataAccessLogByIds",
|
||||
"POST /timedTask/createTimedTask", "PUT /timedTask/updateTimedTask", "DELETE /timedTask/deleteTimedTask", "POST /timedTask/toggleTimedTask", "POST /timedTask/triggerTimedTask",
|
||||
"POST /info/createInfo", "DELETE /info/deleteInfo", "DELETE /info/deleteInfoByIds", "PUT /info/updateInfo", "POST /email/emailTest", "POST /email/sendEmail",
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ func ErrorAudit(audit *service.AuditRecorder) gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
requestID, _ := c.Get("request_id")
|
||||
_ = audit.CreateErrorRequest(c.Request.Context(), &dto.ErrorRecordRequest{Form: c.Request.URL.Path, Info: response.Msg, Level: "error", RequestID: stringValue(requestID), TraceID: stringValueFromContext(c, "trace_id"), Status: "未解决"})
|
||||
_ = audit.CreateErrorRequest(c.Request.Context(), &dto.ErrorRecordRequest{Form: c.Request.URL.Path, Info: response.Msg, Level: "error", RequestID: stringValue(requestID), TraceID: stringValueFromContext(c, "trace_id")})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ func Recovery(audit *service.AuditRecorder, logger *slog.Logger) gin.HandlerFunc
|
||||
logger.ErrorContext(c.Request.Context(), "recovery from panic", "error", panicValue, "request", string(request), "stack", string(debug.Stack()))
|
||||
}
|
||||
requestID, _ := c.Get("request_id")
|
||||
_ = audit.CreateErrorRequest(c.Request.Context(), &dto.ErrorRecordRequest{Form: c.Request.URL.Path, Info: info, Level: "error", RequestID: stringValue(requestID), TraceID: stringValueFromContext(c, "trace_id"), Status: "未解决"})
|
||||
_ = audit.CreateErrorRequest(c.Request.Context(), &dto.ErrorRecordRequest{Form: c.Request.URL.Path, Info: info, Level: "error", RequestID: stringValue(requestID), TraceID: stringValueFromContext(c, "trace_id")})
|
||||
if brokenPipe {
|
||||
if err, ok := panicValue.(error); ok {
|
||||
_ = c.Error(err)
|
||||
|
||||
@@ -24,11 +24,7 @@ func NewAnnouncementService(uc *biz.AnnouncementUsecase) *AnnouncementService {
|
||||
}
|
||||
|
||||
func announcementDTO(item *biz.Announcement) *dto.AnnouncementResponse {
|
||||
attachments := any([]any{})
|
||||
if len(item.Attachments) > 0 {
|
||||
_ = json.Unmarshal(item.Attachments, &attachments)
|
||||
}
|
||||
return &dto.AnnouncementResponse{ID: item.ID, CreatedAt: item.CreatedAt, UpdatedAt: item.UpdatedAt, DeletedAt: nil, Title: item.Title, Content: item.Content, UserID: item.UserID, Attachments: attachments}
|
||||
return &dto.AnnouncementResponse{ID: item.ID, CreatedAt: item.CreatedAt, UpdatedAt: item.UpdatedAt, DeletedAt: nil, Title: item.Title, Content: item.Content, UserID: item.UserID, Attachments: json.RawMessage(item.Attachments)}
|
||||
}
|
||||
|
||||
func announcementDO(in AnnouncementInput) *biz.Announcement {
|
||||
|
||||
+10
-10
@@ -63,7 +63,10 @@ func (s *APIService) AllAPI(ctx context.Context, strict bool) ([]*dto.APIRespons
|
||||
}
|
||||
|
||||
func (s *APIService) Groups(ctx context.Context) ([]string, map[string]string, error) {
|
||||
items, err := s.AllAPI(ctx, false)
|
||||
// The compatible group endpoint uses an unqualified Find, whose natural order is
|
||||
// the primary-key ascending order. GetAllApis intentionally uses id DESC,
|
||||
// so do not reuse it here or the group/dropdown order changes.
|
||||
items, _, err := s.uc.ListAPIs(ctx, 0, 0, &biz.API{OrderKey: "id"})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -133,15 +136,9 @@ func (s *APIService) SyncAPIResponses(ctx context.Context, routes []dto.APIReque
|
||||
items := make([]*biz.API, 0, len(routes))
|
||||
for i := range routes {
|
||||
routes[i].Path = s.NormalizeRoutePath(routes[i].Path)
|
||||
if routes[i].APIGroup == "" || routes[i].Description == "" {
|
||||
group, description := routeMetadata(routes[i].Method, routes[i].Path)
|
||||
if routes[i].APIGroup == "" {
|
||||
routes[i].APIGroup = group
|
||||
}
|
||||
if routes[i].Description == "" {
|
||||
routes[i].Description = description
|
||||
}
|
||||
}
|
||||
// The compatible sync endpoint compares Gin's route table directly, so newly
|
||||
// discovered routes carry only path and method. Group/description are
|
||||
// intentionally left empty for the operator to fill in the sync dialog.
|
||||
items = append(items, apiDomain(&routes[i]))
|
||||
}
|
||||
return s.SyncAPIs(ctx, items)
|
||||
@@ -150,6 +147,9 @@ func (s *APIService) SyncAPIResponses(ctx context.Context, routes []dto.APIReque
|
||||
func (s *APIService) DeleteAPIs(ctx context.Context, ids []uint) error {
|
||||
return s.uc.DeleteAPIs(ctx, ids)
|
||||
}
|
||||
func (s *APIService) DeleteAPI(ctx context.Context, id uint) error {
|
||||
return s.uc.DeleteAPI(ctx, id)
|
||||
}
|
||||
func (s *APIService) APIRoleIDs(ctx context.Context, path, method string) ([]uint, error) {
|
||||
return s.uc.APIRoleIDs(ctx, path, method)
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ func opDTO(v *biz.OperationRecord) *dto.OperationRecordResponse {
|
||||
if v.User != nil {
|
||||
user = convertUser(v.User)
|
||||
}
|
||||
return &dto.OperationRecordResponse{ID: v.ID, CreatedAt: v.CreatedAt, DeletedAt: nil, 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, User: user}
|
||||
return &dto.OperationRecordResponse{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, DeletedAt: nil, 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, User: user}
|
||||
}
|
||||
func (s *AuditService) Operations(ctx context.Context, page, size int, q *biz.OperationRecord) ([]*dto.OperationRecordResponse, int64, error) {
|
||||
items, total, err := s.uc.ListOperations(ctx, page, size, q)
|
||||
@@ -67,7 +67,7 @@ func loginDTO(v *biz.LoginLog) *dto.LoginLogResponse {
|
||||
if v.User != nil {
|
||||
user = convertUser(v.User)
|
||||
}
|
||||
return &dto.LoginLogResponse{ID: v.ID, CreatedAt: v.CreatedAt, DeletedAt: nil, Username: v.Username, IP: v.IP, Status: v.Status, ErrorMessage: v.ErrorMessage, Agent: v.Agent, UserID: v.UserID, User: user}
|
||||
return &dto.LoginLogResponse{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, DeletedAt: nil, Username: v.Username, IP: v.IP, Status: v.Status, ErrorMessage: v.ErrorMessage, Agent: v.Agent, UserID: v.UserID, User: user}
|
||||
}
|
||||
func (s *AuditService) Logins(ctx context.Context, page, size int, q *biz.LoginLog) ([]*dto.LoginLogResponse, int64, error) {
|
||||
items, total, err := s.uc.ListLogins(ctx, page, size, q)
|
||||
@@ -98,10 +98,10 @@ func (s *AuditRecorder) RecordDataAccessRequest(ctx context.Context, value *dto.
|
||||
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) ([]*dto.DataAccessLogResponse, int64, error) {
|
||||
return s.DataAccess(ctx, req.Page, req.PageSize, &biz.DataAccessLog{EventType: req.EventType, TargetTable: req.TargetTable, UserID: req.UserID})
|
||||
return s.DataAccess(ctx, req.Page, req.PageSize, &biz.DataAccessLog{EventType: req.EventType, TargetTable: req.TargetTable})
|
||||
}
|
||||
func dataAccessDTO(v *biz.DataAccessLog) *dto.DataAccessLogResponse {
|
||||
return &dto.DataAccessLogResponse{ID: v.ID, CreatedAt: v.CreatedAt, DeletedAt: nil, EventType: v.EventType, TargetTable: v.TargetTable, Operation: v.Operation, UserID: v.UserID, AuthorityID: v.AuthorityID, Scope: v.Scope, RequestID: v.RequestID, Method: v.Method, Path: v.Path, Detail: v.Detail}
|
||||
return &dto.DataAccessLogResponse{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, DeletedAt: nil, EventType: v.EventType, TargetTable: v.TargetTable, Operation: v.Operation, UserID: v.UserID, AuthorityID: v.AuthorityID, Scope: v.Scope, RequestID: v.RequestID, Method: v.Method, Path: v.Path, Detail: v.Detail}
|
||||
}
|
||||
func (s *AuditService) DataAccess(ctx context.Context, page, size int, q *biz.DataAccessLog) ([]*dto.DataAccessLogResponse, int64, error) {
|
||||
items, total, err := s.uc.ListDataAccess(ctx, page, size, q)
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
)
|
||||
|
||||
func errorDTO(v *biz.ErrorRecord) *dto.ErrorRecordResponse {
|
||||
return &dto.ErrorRecordResponse{ID: v.ID, CreatedAt: v.CreatedAt, DeletedAt: nil, Form: v.Form, Info: v.Info, Level: v.Level, RequestID: v.RequestID, TraceID: v.TraceID, Solution: v.Solution, Status: v.Status}
|
||||
return &dto.ErrorRecordResponse{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, DeletedAt: nil, Form: v.Form, Info: v.Info, Level: v.Level, RequestID: v.RequestID, TraceID: v.TraceID, Solution: v.Solution, Status: v.Status}
|
||||
}
|
||||
func (s *AuditRecorder) CreateError(ctx context.Context, v *biz.ErrorRecord) error {
|
||||
return s.uc.CreateError(ctx, v)
|
||||
|
||||
@@ -34,8 +34,8 @@ func (s *AuthService) Login(ctx context.Context, req *dto.LoginRequest, ip, agen
|
||||
return loginResult(value), nil
|
||||
}
|
||||
|
||||
func (s *AuthService) SwitchAuthority(ctx context.Context, id, authorityID uint) (*LoginResult, error) {
|
||||
value, err := s.uc.SwitchAuthority(ctx, id, authorityID)
|
||||
func (s *AuthService) SwitchAuthority(ctx context.Context, claims *biz.AuthClaims, authorityID uint) (*LoginResult, error) {
|
||||
value, err := s.uc.SwitchAuthority(ctx, claims, authorityID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ func authorityDomain(req *dto.AuthorityRequest) *biz.Authority {
|
||||
}
|
||||
|
||||
func authorityResponse(value *biz.Authority) *dto.AuthorityResponse {
|
||||
return &dto.AuthorityResponse{AuthorityID: value.AuthorityID, AuthorityName: value.AuthorityName, ParentID: value.ParentID, DataScope: value.DataScope, DefaultRouter: value.DefaultRouter}
|
||||
return &dto.AuthorityResponse{CreatedAt: value.CreatedAt, UpdatedAt: value.UpdatedAt, DeletedAt: value.DeletedAt, AuthorityID: value.AuthorityID, AuthorityName: value.AuthorityName, ParentID: value.ParentID, Children: []*dto.AuthorityResponse{}, Menus: []any{}, DataScope: value.DataScope, DefaultRouter: value.DefaultRouter}
|
||||
}
|
||||
|
||||
func (s *AuthorityService) Authorities(ctx context.Context) ([]*dto.AuthorityResponse, error) {
|
||||
|
||||
@@ -14,11 +14,7 @@ func NewDictionaryService(uc *biz.DictionaryUsecase) *DictionaryService {
|
||||
}
|
||||
|
||||
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}
|
||||
return &biz.Dictionary{ID: value.ID, Name: value.Name, Type: value.Type, Status: value.Status, Desc: value.Description, ParentID: value.ParentID}
|
||||
}
|
||||
func (s *DictionaryService) CreateDictionaryRequest(ctx context.Context, req *dto.DictionaryRequest) (*dto.DictionaryResponse, error) {
|
||||
value := dictionaryDomain(req)
|
||||
@@ -31,13 +27,19 @@ func (s *DictionaryService) UpdateDictionaryRequest(ctx context.Context, req *dt
|
||||
return s.UpdateDictionary(ctx, dictionaryDomain(req))
|
||||
}
|
||||
func dictionaryDTO(v *biz.Dictionary) *dto.DictionaryResponse {
|
||||
children := make([]*dto.DictionaryResponse, 0, len(v.Children))
|
||||
for _, x := range v.Children {
|
||||
children = append(children, dictionaryDTO(x))
|
||||
var children []*dto.DictionaryResponse
|
||||
if v.Children != nil {
|
||||
children = make([]*dto.DictionaryResponse, 0, len(v.Children))
|
||||
for _, x := range v.Children {
|
||||
children = append(children, dictionaryDTO(x))
|
||||
}
|
||||
}
|
||||
details := make([]*dto.DictionaryDetailResponse, 0, len(v.Details))
|
||||
for _, x := range v.Details {
|
||||
details = append(details, detailDTO(x))
|
||||
var details []*dto.DictionaryDetailResponse
|
||||
if v.Details != nil {
|
||||
details = make([]*dto.DictionaryDetailResponse, 0, len(v.Details))
|
||||
for _, x := range v.Details {
|
||||
details = append(details, detailDTO(x))
|
||||
}
|
||||
}
|
||||
return &dto.DictionaryResponse{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}
|
||||
}
|
||||
@@ -87,11 +89,7 @@ func (s *DictionaryService) DeleteDictionary(ctx context.Context, id uint) error
|
||||
}
|
||||
|
||||
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}
|
||||
return &biz.DictionaryDetail{ID: value.ID, Label: value.Label, Value: value.Value, Extend: value.Extend, Status: value.Status, Sort: value.Sort, DictionaryID: value.DictionaryID, ParentID: value.ParentID, Level: value.Level, Path: value.Path}
|
||||
}
|
||||
func (s *DictionaryService) CreateDictionaryDetailRequest(ctx context.Context, req *dto.DictionaryDetailRequest) error {
|
||||
return s.CreateDictionaryDetail(ctx, detailDomain(req))
|
||||
@@ -100,11 +98,15 @@ func (s *DictionaryService) UpdateDictionaryDetailRequest(ctx context.Context, r
|
||||
return s.UpdateDictionaryDetail(ctx, detailDomain(req))
|
||||
}
|
||||
func detailDTO(v *biz.DictionaryDetail) *dto.DictionaryDetailResponse {
|
||||
children := make([]*dto.DictionaryDetailResponse, 0, len(v.Children))
|
||||
for _, x := range v.Children {
|
||||
children = append(children, detailDTO(x))
|
||||
var children []*dto.DictionaryDetailResponse
|
||||
if v.Children != nil {
|
||||
children = make([]*dto.DictionaryDetailResponse, 0, len(v.Children))
|
||||
for _, x := range v.Children {
|
||||
children = append(children, detailDTO(x))
|
||||
}
|
||||
}
|
||||
return &dto.DictionaryDetailResponse{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, DictionaryID: v.DictionaryID, ParentID: v.ParentID, Level: v.Level, Path: v.Path, Disabled: !v.Status, Children: children}
|
||||
disabled := v.Status != nil && !*v.Status
|
||||
return &dto.DictionaryDetailResponse{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, DictionaryID: v.DictionaryID, ParentID: v.ParentID, Level: v.Level, Path: v.Path, Disabled: disabled, Children: children}
|
||||
}
|
||||
func (s *DictionaryService) DictionaryDetails(ctx context.Context, page, size int, filter biz.DictionaryDetailFilter) ([]*dto.DictionaryDetailResponse, int64, error) {
|
||||
items, total, err := s.uc.ListDictionaryDetails(ctx, page, size, filter)
|
||||
|
||||
@@ -13,15 +13,24 @@ type AnnouncementRequest struct {
|
||||
Attachments json.RawMessage `json:"attachments"`
|
||||
}
|
||||
|
||||
type AnnouncementSearchRequest struct {
|
||||
Page int `form:"page"`
|
||||
PageSize int `form:"pageSize"`
|
||||
StartCreatedAt *time.Time `form:"startCreatedAt"`
|
||||
EndCreatedAt *time.Time `form:"endCreatedAt"`
|
||||
}
|
||||
|
||||
type AnnouncementResponse struct {
|
||||
ID uint `json:"ID"`
|
||||
CreatedAt time.Time `json:"CreatedAt"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt"`
|
||||
DeletedAt any `json:"DeletedAt"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
UserID *uint `json:"userID"`
|
||||
Attachments any `json:"attachments"`
|
||||
ID uint `json:"ID"`
|
||||
CreatedAt time.Time `json:"CreatedAt"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt"`
|
||||
// Keep the soft-delete slot in the transport shape while omitting it from
|
||||
// JSON responses.
|
||||
DeletedAt any `json:"-"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
UserID *uint `json:"userID"`
|
||||
Attachments any `json:"attachments"`
|
||||
}
|
||||
|
||||
type SelectOptionResponse struct {
|
||||
|
||||
@@ -25,7 +25,7 @@ type APIResponse struct {
|
||||
ID uint `json:"ID"`
|
||||
CreatedAt time.Time `json:"CreatedAt"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt"`
|
||||
DeletedAt any `json:"DeletedAt"`
|
||||
DeletedAt any `json:"-"`
|
||||
Path string `json:"path"`
|
||||
Description string `json:"description"`
|
||||
APIGroup string `json:"apiGroup"`
|
||||
|
||||
@@ -34,11 +34,30 @@ type DataAccessListRequest struct {
|
||||
PageSize int `json:"pageSize"`
|
||||
EventType string `json:"eventType"`
|
||||
TargetTable string `json:"targetTable"`
|
||||
UserID uint `json:"userId"`
|
||||
}
|
||||
|
||||
type OperationRecordSearchRequest struct {
|
||||
Page int `form:"page"`
|
||||
PageSize int `form:"pageSize"`
|
||||
Path string `form:"path"`
|
||||
Method string `form:"method"`
|
||||
Status int `form:"status"`
|
||||
}
|
||||
|
||||
type LoginLogSearchRequest struct {
|
||||
Page int `form:"page"`
|
||||
PageSize int `form:"pageSize"`
|
||||
Username string `form:"username"`
|
||||
Status bool `form:"status"`
|
||||
}
|
||||
|
||||
type AuditIDQuery struct {
|
||||
ID uint `form:"ID"`
|
||||
}
|
||||
|
||||
type ErrorRecordRequest struct {
|
||||
ID uint `json:"ID"`
|
||||
Form string `json:"form"`
|
||||
Form string `json:"form" binding:"required"`
|
||||
Info string `json:"info"`
|
||||
Level string `json:"level"`
|
||||
RequestID string `json:"request_id"`
|
||||
@@ -50,7 +69,8 @@ type ErrorRecordRequest struct {
|
||||
type OperationRecordResponse struct {
|
||||
ID uint `json:"ID"`
|
||||
CreatedAt time.Time `json:"CreatedAt"`
|
||||
DeletedAt any `json:"DeletedAt"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt"`
|
||||
DeletedAt any `json:"-"`
|
||||
IP string `json:"ip"`
|
||||
Method string `json:"method"`
|
||||
Path string `json:"path"`
|
||||
@@ -70,7 +90,8 @@ type OperationRecordResponse struct {
|
||||
type LoginLogResponse struct {
|
||||
ID uint `json:"ID"`
|
||||
CreatedAt time.Time `json:"CreatedAt"`
|
||||
DeletedAt any `json:"DeletedAt"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt"`
|
||||
DeletedAt any `json:"-"`
|
||||
Username string `json:"username"`
|
||||
IP string `json:"ip"`
|
||||
Status bool `json:"status"`
|
||||
@@ -83,7 +104,8 @@ type LoginLogResponse struct {
|
||||
type DataAccessLogResponse struct {
|
||||
ID uint `json:"ID"`
|
||||
CreatedAt time.Time `json:"CreatedAt"`
|
||||
DeletedAt any `json:"DeletedAt"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt"`
|
||||
DeletedAt any `json:"-"`
|
||||
EventType string `json:"eventType"`
|
||||
TargetTable string `json:"targetTable"`
|
||||
Operation string `json:"operation"`
|
||||
@@ -99,7 +121,8 @@ type DataAccessLogResponse struct {
|
||||
type ErrorRecordResponse struct {
|
||||
ID uint `json:"ID"`
|
||||
CreatedAt time.Time `json:"CreatedAt"`
|
||||
DeletedAt any `json:"DeletedAt"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt"`
|
||||
DeletedAt any `json:"-"`
|
||||
Form string `json:"form"`
|
||||
Info string `json:"info"`
|
||||
Level string `json:"level"`
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package dto
|
||||
|
||||
import "time"
|
||||
|
||||
type AuthorityRequest struct {
|
||||
AuthorityID uint `json:"authorityId"`
|
||||
AuthorityName string `json:"authorityName"`
|
||||
@@ -17,16 +19,19 @@ type DeleteAuthorityRequest struct {
|
||||
AuthorityID uint `json:"authorityId"`
|
||||
}
|
||||
type SetRoleUsersRequest struct {
|
||||
AuthorityID uint `json:"authorityId"`
|
||||
AuthorityID uint `json:"authorityId" form:"authorityId"`
|
||||
UserIDs []uint `json:"userIds"`
|
||||
}
|
||||
type SetDataScopeRequest struct {
|
||||
AuthorityID uint `json:"authorityId"`
|
||||
AuthorityID uint `json:"authorityId" form:"authorityId"`
|
||||
DataScope int `json:"dataScope"`
|
||||
DepartmentIDs []uint `json:"deptIds"`
|
||||
}
|
||||
|
||||
type AuthorityResponse struct {
|
||||
CreatedAt time.Time `json:"CreatedAt"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt"`
|
||||
DeletedAt *time.Time `json:"DeletedAt"`
|
||||
AuthorityID uint `json:"authorityId"`
|
||||
AuthorityName string `json:"authorityName"`
|
||||
ParentID *uint `json:"parentId"`
|
||||
|
||||
@@ -3,14 +3,22 @@ package dto
|
||||
import "time"
|
||||
|
||||
type ExportConditionRequest struct {
|
||||
From string `json:"from"`
|
||||
Column string `json:"column"`
|
||||
Operator string `json:"operator"`
|
||||
ID uint `json:"ID"`
|
||||
CreatedAt time.Time `json:"CreatedAt"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt"`
|
||||
TemplateID string `json:"templateID"`
|
||||
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"`
|
||||
ID uint `json:"ID"`
|
||||
CreatedAt time.Time `json:"CreatedAt"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt"`
|
||||
TemplateID string `json:"templateID"`
|
||||
Join string `json:"joins"`
|
||||
Table string `json:"table"`
|
||||
On string `json:"on"`
|
||||
}
|
||||
type ExportTemplateRequest struct {
|
||||
ID uint `json:"ID"`
|
||||
@@ -28,22 +36,31 @@ type ExportTemplateRequest struct {
|
||||
}
|
||||
|
||||
type ExportConditionResponse struct {
|
||||
From string `json:"from"`
|
||||
Column string `json:"column"`
|
||||
Operator string `json:"operator"`
|
||||
ID uint `json:"ID"`
|
||||
CreatedAt time.Time `json:"CreatedAt"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt"`
|
||||
TemplateID string `json:"templateID"`
|
||||
From string `json:"from"`
|
||||
Column string `json:"column"`
|
||||
Operator string `json:"operator"`
|
||||
}
|
||||
|
||||
type ExportJoinResponse struct {
|
||||
Join string `json:"joins"`
|
||||
Table string `json:"table"`
|
||||
On string `json:"on"`
|
||||
ID uint `json:"ID"`
|
||||
CreatedAt time.Time `json:"CreatedAt"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt"`
|
||||
TemplateID string `json:"templateID"`
|
||||
Join string `json:"joins"`
|
||||
Table string `json:"table"`
|
||||
On string `json:"on"`
|
||||
}
|
||||
|
||||
type ExportTemplateResponse struct {
|
||||
ID uint `json:"ID"`
|
||||
CreatedAt time.Time `json:"CreatedAt"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt"`
|
||||
DeletedAt any `json:"DeletedAt"`
|
||||
ID uint `json:"ID"`
|
||||
CreatedAt time.Time `json:"CreatedAt"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt"`
|
||||
// The compatible embedded gorm.DeletedAt field is json:"-".
|
||||
DeletedAt any `json:"-"`
|
||||
DBName string `json:"dbName"`
|
||||
Name string `json:"name"`
|
||||
TableName string `json:"tableName"`
|
||||
|
||||
@@ -6,23 +6,24 @@ type MediaResponse struct {
|
||||
ID uint `json:"ID"`
|
||||
CreatedAt time.Time `json:"CreatedAt"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt"`
|
||||
DeletedAt any `json:"DeletedAt"`
|
||||
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"`
|
||||
UserID uint `json:"userId"`
|
||||
// The compatible model embeds gorm.DeletedAt with json:"-", so this field is omitted.
|
||||
DeletedAt any `json:"-"`
|
||||
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"`
|
||||
UserID uint `json:"userId"`
|
||||
}
|
||||
|
||||
type MediaCategoryResponse struct {
|
||||
ID uint `json:"ID"`
|
||||
CreatedAt time.Time `json:"CreatedAt"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt"`
|
||||
DeletedAt any `json:"DeletedAt"`
|
||||
DeletedAt any `json:"-"`
|
||||
Name string `json:"name"`
|
||||
PID uint `json:"pid"`
|
||||
Children []*MediaCategoryResponse `json:"children"`
|
||||
@@ -37,9 +38,9 @@ type StorageObjectResponse struct {
|
||||
|
||||
type InitUploadResponse struct {
|
||||
Instant bool `json:"instant"`
|
||||
UploadID uint `json:"uploadId,omitempty"`
|
||||
UploadID uint `json:"uploadId"`
|
||||
UploadedChunks []int `json:"uploadedChunks"`
|
||||
Media *MediaResponse `json:"media"`
|
||||
Media *MediaResponse `json:"media,omitempty"`
|
||||
}
|
||||
|
||||
type MediaListRequest struct {
|
||||
@@ -86,13 +87,13 @@ type CategoryRequest struct {
|
||||
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"`
|
||||
FileName string `json:"fileName" binding:"required"`
|
||||
FileHash string `json:"fileHash" binding:"required"`
|
||||
FileSize int64 `json:"fileSize" binding:"required"`
|
||||
ChunkSize int64 `json:"chunkSize" binding:"required"`
|
||||
ChunkTotal int `json:"chunkTotal" binding:"required"`
|
||||
Mime string `json:"mime"`
|
||||
}
|
||||
type CompleteUploadRequest struct {
|
||||
UploadID uint `json:"uploadId"`
|
||||
UploadID uint `json:"uploadId" binding:"required"`
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package dto
|
||||
|
||||
import "time"
|
||||
|
||||
type MenuMetaRequest struct {
|
||||
ActiveName string `json:"activeName"`
|
||||
KeepAlive bool `json:"keepAlive"`
|
||||
@@ -58,7 +60,7 @@ type GetAuthorityMenusRequest struct {
|
||||
}
|
||||
|
||||
type SetMenuRolesRequest struct {
|
||||
MenuID uint `json:"menuId"`
|
||||
MenuID uint `json:"menuId" form:"menuId"`
|
||||
AuthorityIDs []uint `json:"authorityIds"`
|
||||
}
|
||||
|
||||
@@ -73,22 +75,29 @@ type MenuMetaResponse struct {
|
||||
}
|
||||
|
||||
type MenuButtonResponse struct {
|
||||
ID uint `json:"ID"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"desc"`
|
||||
SysBaseMenuID uint `json:"sysBaseMenuID"`
|
||||
CreatedAt time.Time `json:"CreatedAt"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt"`
|
||||
ID uint `json:"ID"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"desc"`
|
||||
SysBaseMenuID uint `json:"sysBaseMenuID"`
|
||||
}
|
||||
|
||||
type MenuParameterResponse struct {
|
||||
ID uint `json:"ID"`
|
||||
SysBaseMenuID uint `json:"SysBaseMenuID"`
|
||||
Type string `json:"type"`
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
CreatedAt time.Time `json:"CreatedAt"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt"`
|
||||
ID uint `json:"ID"`
|
||||
SysBaseMenuID uint `json:"SysBaseMenuID"`
|
||||
Type string `json:"type"`
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type MenuResponse struct {
|
||||
CreatedAt time.Time `json:"CreatedAt"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt"`
|
||||
ID uint `json:"ID"`
|
||||
MenuID uint `json:"menuId,omitempty"`
|
||||
ParentID uint `json:"parentId"`
|
||||
Path string `json:"path"`
|
||||
Name string `json:"name"`
|
||||
@@ -103,8 +112,10 @@ type MenuResponse struct {
|
||||
}
|
||||
|
||||
type AuthorityMenuResponse struct {
|
||||
MenuID uint `json:"menuId"`
|
||||
AuthorityID uint `json:"authorityId"`
|
||||
SysBaseMenu *MenuResponse `json:"sysBaseMenu"`
|
||||
ParentID uint `json:"parentId"`
|
||||
// The administration response embeds the base-menu fields, so menu fields are
|
||||
// flat in the response (there is no sysBaseMenu envelope). Keep the
|
||||
// authority ID internal, matching the compatible json:"-" field.
|
||||
*MenuResponse
|
||||
MenuID uint `json:"menuId"`
|
||||
AuthorityID uint `json:"-"`
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ type DepartmentResponse struct {
|
||||
ID uint `json:"ID"`
|
||||
CreatedAt time.Time `json:"CreatedAt"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt"`
|
||||
DeletedAt any `json:"DeletedAt"`
|
||||
DeletedAt any `json:"-"`
|
||||
Name string `json:"name"`
|
||||
ParentID uint `json:"parentId"`
|
||||
Ancestors string `json:"ancestors"`
|
||||
@@ -75,7 +75,7 @@ type PositionResponse struct {
|
||||
ID uint `json:"ID"`
|
||||
CreatedAt time.Time `json:"CreatedAt"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt"`
|
||||
DeletedAt any `json:"DeletedAt"`
|
||||
DeletedAt any `json:"-"`
|
||||
Name string `json:"name"`
|
||||
Code string `json:"code"`
|
||||
Sort int `json:"sort"`
|
||||
|
||||
@@ -32,11 +32,20 @@ type DictionaryDetailsByParentRequest struct {
|
||||
}
|
||||
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"`
|
||||
Name string `json:"name" form:"name" binding:"required"`
|
||||
Key string `json:"key" form:"key" binding:"required"`
|
||||
Value string `json:"value" form:"value" binding:"required"`
|
||||
Description string `json:"desc" form:"desc"`
|
||||
}
|
||||
|
||||
type SystemParameterSearchRequest struct {
|
||||
Page int `json:"page" form:"page"`
|
||||
PageSize int `json:"pageSize" form:"pageSize"`
|
||||
StartCreatedAt *time.Time `json:"startCreatedAt" form:"startCreatedAt"`
|
||||
EndCreatedAt *time.Time `json:"endCreatedAt" form:"endCreatedAt"`
|
||||
Name string `json:"name" form:"name"`
|
||||
Key string `json:"key" form:"key"`
|
||||
}
|
||||
type CreateAPITokenRequest struct {
|
||||
UserID uint `json:"userId"`
|
||||
AuthorityID uint `json:"authorityId"`
|
||||
@@ -54,7 +63,7 @@ type APITokenResponse struct {
|
||||
ID uint `json:"ID"`
|
||||
CreatedAt time.Time `json:"CreatedAt"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt"`
|
||||
DeletedAt any `json:"DeletedAt"`
|
||||
DeletedAt any `json:"-"`
|
||||
UserID uint `json:"userId"`
|
||||
AuthorityID uint `json:"authorityId"`
|
||||
Token string `json:"token"`
|
||||
@@ -65,33 +74,36 @@ type APITokenResponse struct {
|
||||
}
|
||||
|
||||
type SecurityConfigResponse 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"`
|
||||
ID uint `json:"ID"`
|
||||
CreatedAt time.Time `json:"CreatedAt"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt"`
|
||||
DeletedAt any `json:"-"`
|
||||
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 SystemParameterResponse struct {
|
||||
ID uint `json:"ID"`
|
||||
CreatedAt time.Time `json:"CreatedAt"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt"`
|
||||
DeletedAt any `json:"DeletedAt"`
|
||||
DeletedAt any `json:"-"`
|
||||
Name string `json:"name"`
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
@@ -102,10 +114,10 @@ type DictionaryResponse struct {
|
||||
ID uint `json:"ID"`
|
||||
CreatedAt time.Time `json:"CreatedAt"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt"`
|
||||
DeletedAt any `json:"DeletedAt"`
|
||||
DeletedAt any `json:"-"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Status bool `json:"status"`
|
||||
Status *bool `json:"status"`
|
||||
Desc string `json:"desc"`
|
||||
ParentID *uint `json:"parentID"`
|
||||
Children []*DictionaryResponse `json:"children"`
|
||||
@@ -116,11 +128,11 @@ type DictionaryDetailResponse struct {
|
||||
ID uint `json:"ID"`
|
||||
CreatedAt time.Time `json:"CreatedAt"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt"`
|
||||
DeletedAt any `json:"DeletedAt"`
|
||||
DeletedAt any `json:"-"`
|
||||
Label string `json:"label"`
|
||||
Value string `json:"value"`
|
||||
Extend string `json:"extend"`
|
||||
Status bool `json:"status"`
|
||||
Status *bool `json:"status"`
|
||||
Sort int `json:"sort"`
|
||||
DictionaryID uint `json:"sysDictionaryID"`
|
||||
ParentID *uint `json:"parentID"`
|
||||
@@ -133,7 +145,7 @@ type DictionaryDetailResponse struct {
|
||||
type DictionaryExportResponse struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Status bool `json:"status"`
|
||||
Status *bool `json:"status"`
|
||||
Desc string `json:"desc"`
|
||||
SysDictionaryDetails []*DictionaryDetailExportResponse `json:"sysDictionaryDetails"`
|
||||
}
|
||||
@@ -142,7 +154,7 @@ type DictionaryDetailExportResponse struct {
|
||||
Label string `json:"label"`
|
||||
Value string `json:"value"`
|
||||
Extend string `json:"extend"`
|
||||
Status bool `json:"status"`
|
||||
Status *bool `json:"status"`
|
||||
Sort int `json:"sort"`
|
||||
Level int `json:"level"`
|
||||
Path string `json:"path"`
|
||||
|
||||
@@ -78,7 +78,7 @@ type UserResponse struct {
|
||||
ID uint `json:"ID"`
|
||||
CreatedAt time.Time `json:"CreatedAt"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt"`
|
||||
DeletedAt any `json:"DeletedAt"`
|
||||
DeletedAt any `json:"-"`
|
||||
UUID string `json:"uuid"`
|
||||
Username string `json:"userName"`
|
||||
NickName string `json:"nickName"`
|
||||
|
||||
@@ -23,18 +23,33 @@ type TaskRequest struct {
|
||||
}
|
||||
|
||||
type DeleteTaskRequest struct {
|
||||
ID uint `json:"ID"`
|
||||
ID uint `json:"ID" binding:"required"`
|
||||
}
|
||||
type ToggleTaskRequest struct {
|
||||
ID uint `json:"ID"`
|
||||
ID uint `json:"ID" binding:"required"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
type TaskListRequest struct {
|
||||
Page int `form:"page"`
|
||||
PageSize int `form:"pageSize"`
|
||||
Name string `form:"name"`
|
||||
ExecutorType string `form:"executorType"`
|
||||
Enabled *bool `form:"enabled"`
|
||||
}
|
||||
|
||||
type TaskLogListRequest struct {
|
||||
Page int `form:"page"`
|
||||
PageSize int `form:"pageSize"`
|
||||
TaskID uint `form:"taskId"`
|
||||
Status string `form:"status"`
|
||||
}
|
||||
|
||||
type TaskResponse struct {
|
||||
ID uint `json:"ID"`
|
||||
CreatedAt time.Time `json:"CreatedAt"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt"`
|
||||
DeletedAt any `json:"DeletedAt"`
|
||||
DeletedAt any `json:"-"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Spec string `json:"spec"`
|
||||
@@ -54,7 +69,8 @@ type TaskResponse struct {
|
||||
type TaskLogResponse struct {
|
||||
ID uint `json:"ID"`
|
||||
CreatedAt time.Time `json:"CreatedAt"`
|
||||
DeletedAt any `json:"DeletedAt"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt"`
|
||||
DeletedAt any `json:"-"`
|
||||
TaskID uint `json:"taskId"`
|
||||
TaskName string `json:"taskName"`
|
||||
TriggerType string `json:"triggerType"`
|
||||
|
||||
@@ -6,13 +6,21 @@ type VersionResponse struct {
|
||||
ID uint `json:"ID"`
|
||||
CreatedAt time.Time `json:"CreatedAt"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt"`
|
||||
DeletedAt any `json:"DeletedAt"`
|
||||
DeletedAt any `json:"-"`
|
||||
VersionName string `json:"versionName"`
|
||||
VersionCode string `json:"versionCode"`
|
||||
Description string `json:"description"`
|
||||
VersionData string `json:"versionData"`
|
||||
}
|
||||
|
||||
type VersionSearchRequest struct {
|
||||
CreatedAtRange []time.Time `json:"createdAtRange" form:"createdAtRange[]"`
|
||||
VersionName *string `json:"versionName" form:"versionName"`
|
||||
VersionCode *string `json:"versionCode" form:"versionCode"`
|
||||
Page int `json:"page" form:"page"`
|
||||
PageSize int `json:"pageSize" form:"pageSize"`
|
||||
}
|
||||
|
||||
type VersionMenuMetaRequest struct {
|
||||
ActiveName string `json:"activeName"`
|
||||
KeepAlive bool `json:"keepAlive"`
|
||||
@@ -61,24 +69,24 @@ type VersionDictionaryDetailRequest struct {
|
||||
type VersionDictionaryRequest struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Status bool `json:"status"`
|
||||
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"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
Code string `json:"code" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
ExportTime string `json:"exportTime"`
|
||||
} `json:"version"`
|
||||
} `json:"version" binding:"required"`
|
||||
Menus []VersionMenuRequest `json:"menus"`
|
||||
APIs []VersionAPIRequest `json:"apis"`
|
||||
Dictionaries []VersionDictionaryRequest `json:"dictionaries"`
|
||||
}
|
||||
type ExportVersionRequest struct {
|
||||
VersionName string `json:"versionName"`
|
||||
VersionCode string `json:"versionCode"`
|
||||
VersionName string `json:"versionName" binding:"required"`
|
||||
VersionCode string `json:"versionCode" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
MenuIDs []uint `json:"menuIds"`
|
||||
APIIDs []uint `json:"apiIds"`
|
||||
|
||||
@@ -70,10 +70,10 @@ func (s *ExportService) ConsumeToken(ctx context.Context, token string, expectBl
|
||||
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})
|
||||
out.Conditions = append(out.Conditions, biz.ExportCondition{ID: item.ID, CreatedAt: item.CreatedAt, UpdatedAt: item.UpdatedAt, TemplateID: item.TemplateID, 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})
|
||||
out.Joins = append(out.Joins, biz.ExportJoin{ID: item.ID, CreatedAt: item.CreatedAt, UpdatedAt: item.UpdatedAt, TemplateID: item.TemplateID, Join: item.Join, Table: item.Table, On: item.On})
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -91,14 +91,14 @@ func exportDTO(v *biz.ExportTemplate) *dto.ExportTemplateResponse {
|
||||
if v.Conditions != nil {
|
||||
conditions = make([]dto.ExportConditionResponse, 0, len(v.Conditions))
|
||||
for _, x := range v.Conditions {
|
||||
conditions = append(conditions, dto.ExportConditionResponse{From: x.From, Column: x.Column, Operator: x.Operator})
|
||||
conditions = append(conditions, dto.ExportConditionResponse{ID: x.ID, CreatedAt: x.CreatedAt, UpdatedAt: x.UpdatedAt, TemplateID: x.TemplateID, From: x.From, Column: x.Column, Operator: x.Operator})
|
||||
}
|
||||
}
|
||||
var joins []dto.ExportJoinResponse
|
||||
if v.Joins != nil {
|
||||
joins = make([]dto.ExportJoinResponse, 0, len(v.Joins))
|
||||
for _, x := range v.Joins {
|
||||
joins = append(joins, dto.ExportJoinResponse{Join: x.Join, Table: x.Table, On: x.On})
|
||||
joins = append(joins, dto.ExportJoinResponse{ID: x.ID, CreatedAt: x.CreatedAt, UpdatedAt: x.UpdatedAt, TemplateID: x.TemplateID, Join: x.Join, Table: x.Table, On: x.On})
|
||||
}
|
||||
}
|
||||
return &dto.ExportTemplateResponse{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, DeletedAt: nil, DBName: v.DBName, Name: v.Name, TableName: v.TableName, TemplateID: v.TemplateID, TemplateInfo: v.TemplateInfo, SQL: v.SQL, ImportSQL: v.ImportSQL, Limit: v.Limit, Order: v.Order, Conditions: conditions, Joins: joins}
|
||||
|
||||
@@ -23,9 +23,12 @@ func mediaDTO(v *biz.MediaFile) *dto.MediaResponse {
|
||||
return &dto.MediaResponse{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, DeletedAt: nil, Name: v.Name, ClassID: v.CategoryID, URL: v.URL, Tag: v.Tag, Key: v.Key, Size: v.Size, Mime: v.Mime, MD5: v.MD5, UserID: v.UserID}
|
||||
}
|
||||
func categoryDTO(v *biz.AttachmentCategory) *dto.MediaCategoryResponse {
|
||||
children := make([]*dto.MediaCategoryResponse, 0, len(v.Children))
|
||||
for _, x := range v.Children {
|
||||
children = append(children, categoryDTO(x))
|
||||
var children []*dto.MediaCategoryResponse
|
||||
if v.Children != nil {
|
||||
children = make([]*dto.MediaCategoryResponse, 0, len(v.Children))
|
||||
for _, x := range v.Children {
|
||||
children = append(children, categoryDTO(x))
|
||||
}
|
||||
}
|
||||
return &dto.MediaCategoryResponse{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, DeletedAt: nil, Name: v.Name, PID: v.ParentID, Children: children}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,9 @@ func (s *MediaService) InitUpload(ctx context.Context, userID uint, name, hash s
|
||||
return nil, err
|
||||
}
|
||||
if media != nil {
|
||||
return &dto.InitUploadResponse{Instant: true, Media: mediaDTO(media), UploadedChunks: []int{}}, nil
|
||||
// The compatible response leaves UploadedChunks nil on an instant-upload hit, producing
|
||||
// JSON null while still returning uploadId: 0.
|
||||
return &dto.InitUploadResponse{Instant: true, Media: mediaDTO(media)}, nil
|
||||
}
|
||||
return &dto.InitUploadResponse{Instant: false, UploadID: session.ID, UploadedChunks: chunks, Media: nil}, nil
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ func (s *MenuService) AuthorityMenus(ctx context.Context, authorityID uint) ([]*
|
||||
}
|
||||
out := make([]*dto.AuthorityMenuResponse, 0, len(items))
|
||||
for _, item := range items {
|
||||
out = append(out, &dto.AuthorityMenuResponse{MenuID: item.ID, AuthorityID: authorityID, SysBaseMenu: menuResponse(item), ParentID: item.ParentID})
|
||||
out = append(out, &dto.AuthorityMenuResponse{MenuResponse: menuResponse(item), MenuID: item.ID, AuthorityID: authorityID})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -94,16 +94,16 @@ func menuResponse(menu *biz.Menu) *dto.MenuResponse {
|
||||
buttons := make([]dto.MenuButtonResponse, 0, len(menu.Buttons))
|
||||
buttonAuthorities := make(map[string]uint, len(menu.Buttons))
|
||||
for _, button := range menu.Buttons {
|
||||
buttons = append(buttons, dto.MenuButtonResponse{ID: button.ID, Name: button.Name, Description: button.Description, SysBaseMenuID: button.MenuID})
|
||||
buttons = append(buttons, dto.MenuButtonResponse{CreatedAt: button.CreatedAt, UpdatedAt: button.UpdatedAt, ID: button.ID, Name: button.Name, Description: button.Description, SysBaseMenuID: button.MenuID})
|
||||
if button.AuthorityID != 0 {
|
||||
buttonAuthorities[button.Name] = button.AuthorityID
|
||||
}
|
||||
}
|
||||
parameters := make([]dto.MenuParameterResponse, 0, len(menu.Parameters))
|
||||
for _, parameter := range menu.Parameters {
|
||||
parameters = append(parameters, dto.MenuParameterResponse{ID: parameter.ID, SysBaseMenuID: parameter.MenuID, Type: parameter.Type, Key: parameter.Key, Value: parameter.Value})
|
||||
parameters = append(parameters, dto.MenuParameterResponse{CreatedAt: parameter.CreatedAt, UpdatedAt: parameter.UpdatedAt, ID: parameter.ID, SysBaseMenuID: parameter.MenuID, Type: parameter.Type, Key: parameter.Key, Value: parameter.Value})
|
||||
}
|
||||
return &dto.MenuResponse{ID: menu.ID, ParentID: menu.ParentID, Path: menu.Path, Name: menu.Name, Hidden: menu.Hidden, Component: menu.Component, Sort: menu.Sort, Meta: dto.MenuMetaResponse{ActiveName: menu.ActiveName, KeepAlive: menu.KeepAlive, DefaultMenu: menu.DefaultMenu, Title: menu.Title, Icon: menu.Icon, CloseTab: menu.CloseTab, TransitionType: menu.TransitionType}, Children: children, Parameters: parameters, MenuButtons: buttons, Buttons: buttonAuthorities}
|
||||
return &dto.MenuResponse{CreatedAt: menu.CreatedAt, UpdatedAt: menu.UpdatedAt, ID: menu.ID, MenuID: menu.MenuID, ParentID: menu.ParentID, Path: menu.Path, Name: menu.Name, Hidden: menu.Hidden, Component: menu.Component, Sort: menu.Sort, Meta: dto.MenuMetaResponse{ActiveName: menu.ActiveName, KeepAlive: menu.KeepAlive, DefaultMenu: menu.DefaultMenu, Title: menu.Title, Icon: menu.Icon, CloseTab: menu.CloseTab, TransitionType: menu.TransitionType}, Children: children, Parameters: parameters, MenuButtons: buttons, Buttons: buttonAuthorities}
|
||||
}
|
||||
|
||||
func menuResponses(items []*biz.Menu) []*dto.MenuResponse {
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
func securityDTO(v *biz.SecurityConfig) *dto.SecurityConfigResponse {
|
||||
return &dto.SecurityConfigResponse{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}
|
||||
return &dto.SecurityConfigResponse{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, DeletedAt: nil, 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 *SecurityService) CurrentSecurity(ctx context.Context) (*biz.SecurityConfig, error) {
|
||||
return s.uc.Current(ctx)
|
||||
|
||||
@@ -21,14 +21,20 @@ type DatabaseInit struct {
|
||||
}
|
||||
|
||||
func (s *SystemConfigService) Initialize(ctx context.Context, input *DatabaseInit, apis []*biz.API) error {
|
||||
config := ""
|
||||
switch input.DBType {
|
||||
driver, config := input.DBType, ""
|
||||
switch driver {
|
||||
case "mysql":
|
||||
config = "timeout=5s&parseTime=True&loc=Local&charset=utf8mb4"
|
||||
config = "charset=utf8mb4&parseTime=True&loc=Local"
|
||||
case "pgsql":
|
||||
config = "sslmode=disable TimeZone=Local"
|
||||
config = "sslmode=disable TimeZone=Asia/Shanghai"
|
||||
case "sqlite", "mssql":
|
||||
default:
|
||||
// The compatible initialization flow treats an omitted or unknown dbType as MySQL during
|
||||
// initialization rather than rejecting it as an unsupported driver.
|
||||
driver = "mysql"
|
||||
config = "charset=utf8mb4&parseTime=True&loc=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})
|
||||
return s.uc.Initialize(ctx, &biz.DatabaseConfig{Driver: driver, 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 *SystemConfigService) InitializeRoutes(ctx context.Context, input *DatabaseInit, routes []dto.Route) error {
|
||||
|
||||
@@ -71,7 +71,7 @@ func (s *TaskService) Logs(ctx context.Context, page, size int, taskID uint, sta
|
||||
}
|
||||
out := make([]*dto.TaskLogResponse, 0, len(items))
|
||||
for _, v := range items {
|
||||
out = append(out, &dto.TaskLogResponse{ID: v.ID, CreatedAt: v.CreatedAt, DeletedAt: nil, TaskID: v.TaskID, TaskName: v.TaskName, TriggerType: v.TriggerType, StartedAt: v.StartedAt, FinishedAt: v.FinishedAt, DurationMS: v.DurationMS, Status: v.Status, ErrorMsg: v.ErrorMsg, Output: v.Output})
|
||||
out = append(out, &dto.TaskLogResponse{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, DeletedAt: nil, TaskID: v.TaskID, TaskName: v.TaskName, TriggerType: v.TriggerType, StartedAt: v.StartedAt, FinishedAt: v.FinishedAt, DurationMS: v.DurationMS, Status: v.Status, ErrorMsg: v.ErrorMsg, Output: v.Output})
|
||||
}
|
||||
return out, total, nil
|
||||
}
|
||||
|
||||
@@ -90,7 +90,9 @@ func (s *UserService) CreateUser(ctx context.Context, input UserInput) (*dto.Use
|
||||
return convertUser(user), nil
|
||||
}
|
||||
func (s *UserService) UpdateUser(ctx context.Context, input UserInput) error {
|
||||
return s.uc.UpdateUser(ctx, &biz.User{ID: input.ID, NickName: input.NickName, HeaderImg: input.HeaderImg, AuthorityID: input.AuthorityID, Phone: input.Phone, Email: input.Email, Enable: input.Enable}, input.AuthorityIDs)
|
||||
// The compatible ChangeUserInfo payload uses authorityIds for role assignment; the
|
||||
// standalone authorityId field is not applied by setUserInfo.
|
||||
return s.uc.UpdateUser(ctx, &biz.User{ID: input.ID, NickName: input.NickName, HeaderImg: input.HeaderImg, Phone: input.Phone, Email: input.Email, Enable: input.Enable}, input.AuthorityIDs)
|
||||
}
|
||||
func (s *UserService) UpdateSelfUser(ctx context.Context, input UserInput) error {
|
||||
return s.uc.UpdateSelfUser(ctx, &biz.User{ID: input.ID, NickName: input.NickName, HeaderImg: input.HeaderImg, Phone: input.Phone, Email: input.Email, Enable: input.Enable})
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
)
|
||||
|
||||
func convertAuthority(value biz.Authority) *dto.AuthorityResponse {
|
||||
return &dto.AuthorityResponse{AuthorityID: value.AuthorityID, AuthorityName: value.AuthorityName, ParentID: value.ParentID, Children: []*dto.AuthorityResponse{}, Menus: []any{}, DataScope: value.DataScope, DefaultRouter: value.DefaultRouter}
|
||||
return &dto.AuthorityResponse{CreatedAt: value.CreatedAt, UpdatedAt: value.UpdatedAt, DeletedAt: value.DeletedAt, AuthorityID: value.AuthorityID, AuthorityName: value.AuthorityName, ParentID: value.ParentID, Children: []*dto.AuthorityResponse{}, Menus: []any{}, DataScope: value.DataScope, DefaultRouter: value.DefaultRouter}
|
||||
}
|
||||
func convertUser(user *biz.User) *dto.UserResponse {
|
||||
authorities := make([]*dto.AuthorityResponse, 0, len(user.Authorities))
|
||||
|
||||
+153
-18
@@ -12,6 +12,123 @@ import (
|
||||
type VersionService struct{ uc *biz.VersionUsecase }
|
||||
|
||||
func NewVersionService(uc *biz.VersionUsecase) *VersionService { return &VersionService{uc: uc} }
|
||||
|
||||
type versionExportInfo struct {
|
||||
Name string `json:"name"`
|
||||
Code string `json:"code"`
|
||||
Description string `json:"description"`
|
||||
ExportTime string `json:"exportTime"`
|
||||
}
|
||||
|
||||
type versionExportMenuParameter struct {
|
||||
ID uint `json:"ID"`
|
||||
CreatedAt time.Time `json:"CreatedAt"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt"`
|
||||
SysBaseMenuID uint `json:"SysBaseMenuID"`
|
||||
Type string `json:"type"`
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type versionExportMenuButton struct {
|
||||
ID uint `json:"ID"`
|
||||
CreatedAt time.Time `json:"CreatedAt"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"desc"`
|
||||
SysBaseMenuID uint `json:"sysBaseMenuID"`
|
||||
}
|
||||
|
||||
type versionExportMenu struct {
|
||||
ID uint `json:"ID"`
|
||||
CreatedAt time.Time `json:"CreatedAt"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt"`
|
||||
ParentID uint `json:"parentId"`
|
||||
Path string `json:"path"`
|
||||
Name string `json:"name"`
|
||||
Hidden bool `json:"hidden"`
|
||||
Component string `json:"component"`
|
||||
Sort int `json:"sort"`
|
||||
Meta dto.VersionMenuMetaRequest `json:"meta"`
|
||||
Authorities []any `json:"authoritys"`
|
||||
Children []*versionExportMenu `json:"children"`
|
||||
Parameters []versionExportMenuParameter `json:"parameters"`
|
||||
MenuButtons []versionExportMenuButton `json:"menuBtn"`
|
||||
}
|
||||
|
||||
type versionExportAPI struct {
|
||||
ID uint `json:"ID"`
|
||||
CreatedAt time.Time `json:"CreatedAt"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt"`
|
||||
Path string `json:"path"`
|
||||
Description string `json:"description"`
|
||||
APIGroup string `json:"apiGroup"`
|
||||
Method string `json:"method"`
|
||||
}
|
||||
|
||||
type versionExportDictionaryDetail struct {
|
||||
ID uint `json:"ID"`
|
||||
CreatedAt time.Time `json:"CreatedAt"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt"`
|
||||
Label string `json:"label"`
|
||||
Value string `json:"value"`
|
||||
Extend string `json:"extend"`
|
||||
Status *bool `json:"status"`
|
||||
Sort int `json:"sort"`
|
||||
DictionaryID int `json:"sysDictionaryID"`
|
||||
ParentID *uint `json:"parentID"`
|
||||
Children []versionExportDictionaryDetail `json:"children"`
|
||||
Level int `json:"level"`
|
||||
Path string `json:"path"`
|
||||
Disabled bool `json:"disabled"`
|
||||
}
|
||||
|
||||
type versionExportDictionary struct {
|
||||
ID uint `json:"ID"`
|
||||
CreatedAt time.Time `json:"CreatedAt"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Status *bool `json:"status"`
|
||||
Desc string `json:"desc"`
|
||||
ParentID *uint `json:"parentID"`
|
||||
Children []versionExportDictionary `json:"children"`
|
||||
Details []versionExportDictionaryDetail `json:"sysDictionaryDetails"`
|
||||
}
|
||||
|
||||
type versionExportDocument struct {
|
||||
Version versionExportInfo `json:"version"`
|
||||
Menus []*versionExportMenu `json:"menus"`
|
||||
APIs []versionExportAPI `json:"apis"`
|
||||
Dictionaries []versionExportDictionary `json:"dictionaries"`
|
||||
}
|
||||
|
||||
func exportMenu(value *biz.Menu) *versionExportMenu {
|
||||
menu := &versionExportMenu{
|
||||
Path: value.Path, Name: value.Name, Hidden: value.Hidden, Component: value.Component, Sort: value.Sort,
|
||||
Meta: dto.VersionMenuMetaRequest{ActiveName: value.ActiveName, KeepAlive: value.KeepAlive, DefaultMenu: value.DefaultMenu, Title: value.Title, Icon: value.Icon, CloseTab: value.CloseTab, TransitionType: value.TransitionType},
|
||||
}
|
||||
if len(value.Parameters) > 0 {
|
||||
menu.Parameters = make([]versionExportMenuParameter, 0, len(value.Parameters))
|
||||
for _, parameter := range value.Parameters {
|
||||
menu.Parameters = append(menu.Parameters, versionExportMenuParameter{Type: parameter.Type, Key: parameter.Key, Value: parameter.Value})
|
||||
}
|
||||
}
|
||||
if len(value.Buttons) > 0 {
|
||||
menu.MenuButtons = make([]versionExportMenuButton, 0, len(value.Buttons))
|
||||
for _, button := range value.Buttons {
|
||||
menu.MenuButtons = append(menu.MenuButtons, versionExportMenuButton{Name: button.Name, Description: button.Description})
|
||||
}
|
||||
}
|
||||
if len(value.Children) > 0 {
|
||||
menu.Children = make([]*versionExportMenu, 0, len(value.Children))
|
||||
for _, child := range value.Children {
|
||||
menu.Children = append(menu.Children, exportMenu(child))
|
||||
}
|
||||
}
|
||||
return menu
|
||||
}
|
||||
|
||||
func versionDTO(v *biz.Version) *dto.VersionResponse {
|
||||
return &dto.VersionResponse{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, DeletedAt: nil, VersionName: v.Name, VersionCode: v.Code, Description: v.Description, VersionData: v.Data}
|
||||
}
|
||||
@@ -38,7 +155,19 @@ func (s *VersionService) VersionData(ctx context.Context, id uint) (string, stri
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return v.Data, v.Code, nil
|
||||
if v.Data != "" {
|
||||
return v.Data, v.Code, nil
|
||||
}
|
||||
payload := versionExportDocument{
|
||||
Version: versionExportInfo{Name: v.Name, Code: v.Code, Description: v.Description, ExportTime: v.CreatedAt.Format("2006-01-02 15:04:05")},
|
||||
Menus: []*versionExportMenu{},
|
||||
APIs: []versionExportAPI{},
|
||||
}
|
||||
raw, marshalErr := json.MarshalIndent(payload, "", " ")
|
||||
if marshalErr != nil {
|
||||
return "", "", marshalErr
|
||||
}
|
||||
return string(raw), v.Code, nil
|
||||
}
|
||||
func (s *VersionService) DeleteVersions(ctx context.Context, ids []uint) error {
|
||||
return s.uc.DeleteVersions(ctx, ids)
|
||||
@@ -53,24 +182,34 @@ func (s *VersionService) Export(ctx context.Context, name, code, description str
|
||||
bundle.Code = code
|
||||
bundle.Description = description
|
||||
bundle.ExportTime = time.Now().Format("2006-01-02 15:04:05")
|
||||
menus := make([]*dto.MenuResponse, 0, len(bundle.Menus))
|
||||
for _, v := range bundle.Menus {
|
||||
menus = append(menus, menuResponse(v))
|
||||
var menus []*versionExportMenu
|
||||
if bundle.Menus != nil {
|
||||
menus = make([]*versionExportMenu, 0, len(bundle.Menus))
|
||||
for _, value := range bundle.Menus {
|
||||
menus = append(menus, exportMenu(value))
|
||||
}
|
||||
}
|
||||
apis := make([]*dto.APIResponse, 0, len(bundle.APIs))
|
||||
for _, v := range bundle.APIs {
|
||||
apis = append(apis, apiResponse(v))
|
||||
apis := make([]versionExportAPI, 0, len(bundle.APIs))
|
||||
for _, value := range bundle.APIs {
|
||||
apis = append(apis, versionExportAPI{Path: value.Path, Description: value.Description, APIGroup: value.APIGroup, Method: value.Method})
|
||||
}
|
||||
dicts := make([]*dto.DictionaryResponse, 0, len(bundle.Dictionaries))
|
||||
for _, v := range bundle.Dictionaries {
|
||||
dicts = append(dicts, dictionaryDTO(v))
|
||||
dictionaries := make([]versionExportDictionary, 0, len(bundle.Dictionaries))
|
||||
for _, value := range bundle.Dictionaries {
|
||||
details := make([]versionExportDictionaryDetail, 0, len(value.Details))
|
||||
for _, detail := range value.Details {
|
||||
details = append(details, versionExportDictionaryDetail{Label: detail.Label, Value: detail.Value, Extend: detail.Extend, Status: detail.Status, Sort: detail.Sort})
|
||||
}
|
||||
dictionaries = append(dictionaries, versionExportDictionary{Name: value.Name, Type: value.Type, Status: value.Status, Desc: value.Desc, Details: details})
|
||||
}
|
||||
payload := map[string]any{"version": map[string]any{"name": name, "code": code, "description": description, "exportTime": bundle.ExportTime}, "menus": menus, "apis": apis, "dictionaries": dicts}
|
||||
payload := versionExportDocument{Version: versionExportInfo{Name: name, Code: code, Description: description, ExportTime: bundle.ExportTime}, Menus: menus, APIs: apis, Dictionaries: dictionaries}
|
||||
raw, err := json.MarshalIndent(payload, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
return &biz.VersionStageError{Stage: biz.VersionStageJSON, Err: err}
|
||||
}
|
||||
return s.uc.CreateVersion(ctx, &biz.Version{Name: name, Code: code, Description: description, Data: string(raw)})
|
||||
if err = s.uc.CreateVersion(ctx, &biz.Version{Name: name, Code: code, Description: description, Data: string(raw)}); err != nil {
|
||||
return &biz.VersionStageError{Stage: biz.VersionStageSave, Err: err}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (s *VersionService) Import(ctx context.Context, bundle *biz.VersionBundle) error {
|
||||
return s.uc.ImportVersionBundle(ctx, bundle)
|
||||
@@ -102,11 +241,7 @@ func (s *VersionService) ImportRequest(ctx context.Context, req *dto.ImportVersi
|
||||
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})
|
||||
item.Details = append(item.Details, &biz.DictionaryDetail{Label: detail.Label, Value: detail.Value, Extend: detail.Extend, Status: detail.Status, Sort: detail.Sort, ParentID: detail.ParentID, Level: detail.Level, Path: detail.Path})
|
||||
}
|
||||
bundle.Dictionaries = append(bundle.Dictionaries, item)
|
||||
}
|
||||
|
||||
@@ -25,7 +25,9 @@ type TaskExecutor struct {
|
||||
}
|
||||
|
||||
func NewTaskExecutor(tasks *biz.TaskUsecase, media *biz.MediaUsecase, runtime *conf.Runtime) *TaskExecutor {
|
||||
return &TaskExecutor{tasks: tasks, media: media, runtime: runtime}
|
||||
executor := &TaskExecutor{tasks: tasks, media: media, runtime: runtime}
|
||||
registerTaskMethods(executor)
|
||||
return executor
|
||||
}
|
||||
|
||||
func privateIP(ip net.IP) bool {
|
||||
@@ -61,7 +63,7 @@ func (e *TaskExecutor) runHTTP(ctx context.Context, task *biz.TimedTask) (string
|
||||
if parsed.Scheme != "http" && parsed.Scheme != "https" {
|
||||
return "", fmt.Errorf("仅允许 http/https, 实际为 %q", parsed.Scheme)
|
||||
}
|
||||
method := strings.ToUpper(task.HTTPMethod)
|
||||
method := strings.ToUpper(strings.TrimSpace(task.HTTPMethod))
|
||||
if method == "" {
|
||||
method = http.MethodGet
|
||||
}
|
||||
@@ -102,6 +104,10 @@ func truncateTaskText(value string) string {
|
||||
}
|
||||
|
||||
func (e *TaskExecutor) runMethod(task *biz.TimedTask) error {
|
||||
method, ok := biz.TaskMethodByName(task.MethodName)
|
||||
if !ok {
|
||||
return fmt.Errorf("方法 %s 未注册(需在 internal/worker/task_registry.go 经 biz.RegisterTaskMethod 注册)", task.MethodName)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
done := make(chan error, 1)
|
||||
@@ -111,21 +117,7 @@ func (e *TaskExecutor) runMethod(task *biz.TimedTask) error {
|
||||
done <- fmt.Errorf("panic: %v", recovered)
|
||||
}
|
||||
}()
|
||||
var err error
|
||||
switch task.MethodName {
|
||||
case biz.TaskMethodClearDB:
|
||||
err = e.tasks.CleanupLogs(ctx)
|
||||
case biz.TaskMethodUploads:
|
||||
ttl := 24
|
||||
config := e.runtime.Admin()
|
||||
if config != nil && config.Media != nil && config.Media.SessionTtl > 0 {
|
||||
ttl = int(config.Media.SessionTtl)
|
||||
}
|
||||
err = e.media.CleanupStale(ctx, ttl)
|
||||
default:
|
||||
err = fmt.Errorf("方法 %s 未注册(需在 initialize/timer.go 经 task.Register 注册)", task.MethodName)
|
||||
}
|
||||
done <- err
|
||||
done <- method(ctx, json.RawMessage(task.Params))
|
||||
}()
|
||||
select {
|
||||
case err := <-done:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user