fix: align admin behavior and Kratos integrations
This commit is contained in:
@@ -1 +1,3 @@
|
||||
# Biz
|
||||
|
||||
本目录只包含领域对象、用例和仓储接口,不依赖 HTTP Handler、数据库驱动或对象存储 SDK。跨多表、必须原子完成的业务动作通过具有业务含义的仓储方法表达。
|
||||
|
||||
+98
-19
@@ -2,6 +2,7 @@ package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -13,9 +14,28 @@ type API struct {
|
||||
Description string
|
||||
APIGroup string
|
||||
Method string
|
||||
OrderKey string
|
||||
Desc bool
|
||||
StrictAll bool
|
||||
}
|
||||
|
||||
type AccessRepo interface {
|
||||
type APIRepo interface {
|
||||
CreateAPI(context.Context, *API) error
|
||||
UpdateAPI(context.Context, *API) error
|
||||
DeleteAPIs(context.Context, []uint) error
|
||||
FindAPI(context.Context, uint) (*API, error)
|
||||
ListAPIs(context.Context, int, int, *API) ([]*API, int64, error)
|
||||
APIRoleIDs(context.Context, string, string) ([]uint, error)
|
||||
SetAPIRoles(context.Context, string, string, []uint) error
|
||||
Authorize(context.Context, uint, string, string) (bool, error)
|
||||
PolicyPaths(context.Context, uint) ([]*API, error)
|
||||
SetPolicyPaths(context.Context, uint, []*API) error
|
||||
IgnoredAPIs(context.Context) ([]*API, error)
|
||||
SetAPIIgnored(context.Context, string, string, bool) error
|
||||
ApplyAPISync(context.Context, []*API, []*API) error
|
||||
}
|
||||
|
||||
type AuthorityAccessRepo interface {
|
||||
CreateAuthority(context.Context, *Authority) error
|
||||
CopyAuthority(context.Context, uint, *Authority) error
|
||||
UpdateAuthority(context.Context, *Authority) error
|
||||
@@ -26,21 +46,9 @@ type AccessRepo interface {
|
||||
SetDataScope(context.Context, uint, int, []uint) error
|
||||
DataScopeDepartmentIDs(context.Context, uint) ([]uint, error)
|
||||
ResolveDataScope(context.Context, uint, uint) (DataScope, error)
|
||||
}
|
||||
|
||||
CreateAPI(context.Context, *API) error
|
||||
UpdateAPI(context.Context, *API) error
|
||||
DeleteAPIs(context.Context, []uint) error
|
||||
FindAPI(context.Context, uint) (*API, error)
|
||||
ListAPIs(context.Context, int, int, *API) ([]*API, int64, error)
|
||||
APIRoleIDs(context.Context, uint) ([]uint, error)
|
||||
SetAPIRoles(context.Context, uint, []uint) error
|
||||
Authorize(context.Context, uint, string, string) (bool, error)
|
||||
PolicyPaths(context.Context, uint) ([]*API, error)
|
||||
SetPolicyPaths(context.Context, uint, []*API) error
|
||||
IgnoredAPIs(context.Context) ([]*API, error)
|
||||
SetAPIIgnored(context.Context, string, string, bool) error
|
||||
ApplyAPISync(context.Context, []*API, []*API) error
|
||||
|
||||
type PermissionRepo interface {
|
||||
Buttons(context.Context, uint) ([]*MenuButton, error)
|
||||
SetAuthorityButtons(context.Context, uint, map[uint][]uint) error
|
||||
AuthorityButtonIDs(context.Context, uint) ([]uint, error)
|
||||
@@ -49,14 +57,85 @@ type AccessRepo interface {
|
||||
CanRemoveButton(context.Context, uint) (bool, error)
|
||||
}
|
||||
|
||||
type AccessUsecase struct{ repo AccessRepo }
|
||||
type AccessRepo interface {
|
||||
AuthorityAccessRepo
|
||||
APIRepo
|
||||
PermissionRepo
|
||||
}
|
||||
|
||||
func NewAccessUsecase(repo AccessRepo) *AccessUsecase { return &AccessUsecase{repo: repo} }
|
||||
type AccessUsecase struct{ AccessRepo }
|
||||
|
||||
func NewAccessUsecase(repo AccessRepo) *AccessUsecase { return &AccessUsecase{AccessRepo: repo} }
|
||||
|
||||
func (uc *AccessUsecase) Repo() AccessRepo { return uc.repo }
|
||||
func (uc *AccessUsecase) Authorize(ctx context.Context, authorityID uint, path, method string) (bool, error) {
|
||||
if authorityID == 888 {
|
||||
return true, nil
|
||||
}
|
||||
return uc.repo.Authorize(ctx, authorityID, path, method)
|
||||
return uc.AccessRepo.Authorize(ctx, authorityID, path, method)
|
||||
}
|
||||
|
||||
type APISyncDiff struct {
|
||||
Added []*API
|
||||
Deleted []*API
|
||||
Ignored []*API
|
||||
}
|
||||
|
||||
func (uc *AccessUsecase) SyncAPIs(ctx context.Context, routes []*API) (*APISyncDiff, error) {
|
||||
stored, _, err := uc.ListAPIs(ctx, 0, 0, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ignored, err := uc.IgnoredAPIs(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key := func(value *API) string { return strings.ToUpper(value.Method) + " " + value.Path }
|
||||
ignoreSet := make(map[string]bool, len(ignored))
|
||||
for _, item := range ignored {
|
||||
ignoreSet[key(item)] = true
|
||||
}
|
||||
routeSet := make(map[string]*API, len(routes))
|
||||
for _, item := range routes {
|
||||
if !ignoreSet[key(item)] {
|
||||
routeSet[key(item)] = item
|
||||
}
|
||||
}
|
||||
storedSet := make(map[string]*API, len(stored))
|
||||
for _, item := range stored {
|
||||
storedSet[key(item)] = item
|
||||
}
|
||||
diff := &APISyncDiff{Ignored: ignored}
|
||||
for routeKey, item := range routeSet {
|
||||
if storedSet[routeKey] == nil {
|
||||
diff.Added = append(diff.Added, item)
|
||||
}
|
||||
}
|
||||
for storedKey, item := range storedSet {
|
||||
if routeSet[storedKey] == nil && !ignoreSet[storedKey] {
|
||||
diff.Deleted = append(diff.Deleted, item)
|
||||
}
|
||||
}
|
||||
return diff, nil
|
||||
}
|
||||
|
||||
func (uc *AccessUsecase) AuthorityTree(ctx context.Context) ([]*Authority, error) {
|
||||
items, err := uc.ListAuthorities(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
byID := make(map[uint]*Authority, len(items))
|
||||
for _, item := range items {
|
||||
item.Children = nil
|
||||
byID[item.AuthorityID] = item
|
||||
}
|
||||
roots := make([]*Authority, 0)
|
||||
for _, item := range items {
|
||||
if item.ParentID != nil && *item.ParentID != 0 && byID[*item.ParentID] != nil {
|
||||
parent := byID[*item.ParentID]
|
||||
parent.Children = append(parent.Children, item)
|
||||
} else {
|
||||
roots = append(roots, item)
|
||||
}
|
||||
}
|
||||
return roots, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package biz
|
||||
|
||||
import "context"
|
||||
|
||||
type actorContextKey struct{}
|
||||
|
||||
type Actor struct {
|
||||
UserID uint
|
||||
AuthorityID uint
|
||||
}
|
||||
|
||||
func NewActorContext(ctx context.Context, actor Actor) context.Context {
|
||||
return context.WithValue(ctx, actorContextKey{}, actor)
|
||||
}
|
||||
|
||||
func ActorFromContext(ctx context.Context) (Actor, bool) {
|
||||
actor, ok := ctx.Value(actorContextKey{}).(Actor)
|
||||
return actor, ok
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -23,6 +24,29 @@ type APITokenRepo interface {
|
||||
CreateAPIToken(context.Context, *APIToken) error
|
||||
ListAPITokens(context.Context, int, int, uint, *bool) ([]*APIToken, int64, error)
|
||||
DisableAPIToken(context.Context, uint) (string, error)
|
||||
DisableAndBlacklistAPIToken(context.Context, uint) error
|
||||
BlacklistToken(context.Context, string) error
|
||||
IsTokenDisabled(context.Context, string) (bool, error)
|
||||
}
|
||||
|
||||
func (uc *SettingsUsecase) PrepareAPIToken(ctx context.Context, userID, authorityID uint, days int) (*User, time.Duration, error) {
|
||||
user, allowed, err := uc.UserHasAuthority(ctx, userID, authorityID)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if !allowed {
|
||||
return nil, 0, errors.New("用户不具备该角色权限")
|
||||
}
|
||||
duration := time.Duration(days) * 24 * time.Hour
|
||||
if days == -1 {
|
||||
duration = 100 * 365 * 24 * time.Hour
|
||||
}
|
||||
if duration <= 0 {
|
||||
return nil, 0, errors.New("有效天数必须大于0或为-1")
|
||||
}
|
||||
return user, duration, nil
|
||||
}
|
||||
|
||||
func (uc *SettingsUsecase) DisableToken(ctx context.Context, id uint) error {
|
||||
return uc.SettingsRepo.DisableAndBlacklistAPIToken(ctx, id)
|
||||
}
|
||||
|
||||
+47
-18
@@ -14,7 +14,16 @@ type OperationRecord struct {
|
||||
Agent, ErrorMessage, Body, Response string
|
||||
UserID uint
|
||||
RequestID, TraceID, DeviceID string
|
||||
User *User
|
||||
}
|
||||
|
||||
type OperationLogRepo interface {
|
||||
RecordOperation(context.Context, *OperationRecord) error
|
||||
ListOperations(context.Context, int, int, *OperationRecord) ([]*OperationRecord, int64, error)
|
||||
FindOperation(context.Context, uint) (*OperationRecord, error)
|
||||
DeleteOperations(context.Context, []uint) error
|
||||
}
|
||||
|
||||
type LoginLog struct {
|
||||
ID uint
|
||||
CreatedAt time.Time
|
||||
@@ -22,7 +31,17 @@ type LoginLog struct {
|
||||
Status bool
|
||||
ErrorMessage, Agent string
|
||||
UserID uint
|
||||
User *User
|
||||
FilterByStatus bool
|
||||
}
|
||||
|
||||
type LoginLogRepo interface {
|
||||
RecordLogin(context.Context, *LoginLog) error
|
||||
ListLogins(context.Context, int, int, *LoginLog) ([]*LoginLog, int64, error)
|
||||
FindLogin(context.Context, uint) (*LoginLog, error)
|
||||
DeleteLogins(context.Context, []uint) error
|
||||
}
|
||||
|
||||
type DataAccessLog struct {
|
||||
ID uint
|
||||
CreatedAt time.Time
|
||||
@@ -31,6 +50,13 @@ type DataAccessLog struct {
|
||||
Scope int
|
||||
RequestID, Method, Path, Detail string
|
||||
}
|
||||
|
||||
type DataAccessLogRepo interface {
|
||||
RecordDataAccess(context.Context, *DataAccessLog) error
|
||||
ListDataAccess(context.Context, int, int, *DataAccessLog) ([]*DataAccessLog, int64, error)
|
||||
DeleteDataAccess(context.Context, []uint) error
|
||||
}
|
||||
|
||||
type LogDate struct {
|
||||
Date string
|
||||
FileCount int
|
||||
@@ -48,33 +74,36 @@ type LogContent struct {
|
||||
Size int64
|
||||
ModifiedAt time.Time
|
||||
}
|
||||
|
||||
type LogFileRepo interface {
|
||||
LogDates(context.Context, string) ([]LogDate, error)
|
||||
LogFiles(context.Context, string) ([]LogFile, error)
|
||||
LogContent(context.Context, string, string, *int64) (*LogContent, error)
|
||||
}
|
||||
|
||||
type ErrorRecord struct {
|
||||
ID uint
|
||||
CreatedAt time.Time
|
||||
Form, Info, Level, RequestID, TraceID, Solution, Status string
|
||||
CreatedAtRange []time.Time
|
||||
}
|
||||
type AuditRepo interface {
|
||||
RecordOperation(context.Context, *OperationRecord) error
|
||||
ListOperations(context.Context, int, int, *OperationRecord) ([]*OperationRecord, int64, error)
|
||||
FindOperation(context.Context, uint) (*OperationRecord, error)
|
||||
DeleteOperations(context.Context, []uint) error
|
||||
RecordLogin(context.Context, *LoginLog) error
|
||||
ListLogins(context.Context, int, int, *LoginLog) ([]*LoginLog, int64, error)
|
||||
FindLogin(context.Context, uint) (*LoginLog, error)
|
||||
DeleteLogins(context.Context, []uint) error
|
||||
RecordDataAccess(context.Context, *DataAccessLog) error
|
||||
ListDataAccess(context.Context, int, int, *DataAccessLog) ([]*DataAccessLog, int64, error)
|
||||
DeleteDataAccess(context.Context, []uint) error
|
||||
LogDates(context.Context, string) ([]LogDate, error)
|
||||
LogFiles(context.Context, string) ([]LogFile, error)
|
||||
LogContent(context.Context, string, string, *int64) (*LogContent, error)
|
||||
|
||||
type ErrorRecordRepo interface {
|
||||
CreateError(context.Context, *ErrorRecord) error
|
||||
UpdateError(context.Context, *ErrorRecord) error
|
||||
DeleteErrors(context.Context, []uint) error
|
||||
FindError(context.Context, uint) (*ErrorRecord, error)
|
||||
ListErrors(context.Context, int, int, *ErrorRecord) ([]*ErrorRecord, int64, error)
|
||||
}
|
||||
type AuditUsecase struct{ repo AuditRepo }
|
||||
|
||||
func NewAuditUsecase(repo AuditRepo) *AuditUsecase { return &AuditUsecase{repo: repo} }
|
||||
func (uc *AuditUsecase) Repo() AuditRepo { return uc.repo }
|
||||
type AuditRepo interface {
|
||||
OperationLogRepo
|
||||
LoginLogRepo
|
||||
DataAccessLogRepo
|
||||
LogFileRepo
|
||||
ErrorRecordRepo
|
||||
}
|
||||
|
||||
type AuditUsecase struct{ AuditRepo }
|
||||
|
||||
func NewAuditUsecase(repo AuditRepo) *AuditUsecase { return &AuditUsecase{AuditRepo: repo} }
|
||||
|
||||
+57
-20
@@ -5,19 +5,6 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type Dictionary struct {
|
||||
ID uint
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
Name string
|
||||
Type string
|
||||
Status bool
|
||||
Desc string
|
||||
ParentID *uint
|
||||
Children []*Dictionary
|
||||
Details []*DictionaryDetail
|
||||
}
|
||||
|
||||
type DictionaryDetail struct {
|
||||
ID uint
|
||||
CreatedAt time.Time
|
||||
@@ -34,16 +21,66 @@ type DictionaryDetail struct {
|
||||
Children []*DictionaryDetail
|
||||
}
|
||||
|
||||
type DictionaryRepo interface {
|
||||
CreateDictionary(context.Context, *Dictionary) error
|
||||
UpdateDictionary(context.Context, *Dictionary) error
|
||||
DeleteDictionary(context.Context, uint) error
|
||||
FindDictionary(context.Context, uint, string, bool) (*Dictionary, error)
|
||||
ListDictionaries(context.Context, int, int, string, string, bool) ([]*Dictionary, int64, error)
|
||||
type DictionaryDetailFilter struct {
|
||||
DictionaryID uint
|
||||
Label string
|
||||
Value string
|
||||
Status *bool
|
||||
ParentID *uint
|
||||
Level *int
|
||||
}
|
||||
|
||||
type DictionaryDetailRepo interface {
|
||||
CreateDictionaryDetail(context.Context, *DictionaryDetail) error
|
||||
UpdateDictionaryDetail(context.Context, *DictionaryDetail) error
|
||||
DeleteDictionaryDetail(context.Context, uint) error
|
||||
FindDictionaryDetail(context.Context, uint) (*DictionaryDetail, error)
|
||||
ListDictionaryDetails(context.Context, int, int, uint, string, string) ([]*DictionaryDetail, int64, error)
|
||||
ListDictionaryDetails(context.Context, int, int, DictionaryDetailFilter) ([]*DictionaryDetail, int64, error)
|
||||
DictionaryDetailTree(context.Context, uint, string) ([]*DictionaryDetail, error)
|
||||
}
|
||||
|
||||
type Dictionary struct {
|
||||
ID uint
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
Name string
|
||||
Type string
|
||||
Status bool
|
||||
Desc string
|
||||
ParentID *uint
|
||||
Children []*Dictionary
|
||||
Details []*DictionaryDetail
|
||||
}
|
||||
|
||||
type DictionaryRepo interface {
|
||||
DictionaryMetadataRepo
|
||||
DictionaryDetailRepo
|
||||
}
|
||||
|
||||
type DictionaryMetadataRepo interface {
|
||||
CreateDictionary(context.Context, *Dictionary) error
|
||||
ImportDictionary(context.Context, *Dictionary, []*DictionaryDetail) error
|
||||
UpdateDictionary(context.Context, *Dictionary) error
|
||||
DeleteDictionary(context.Context, uint) error
|
||||
FindDictionary(context.Context, uint, string, *bool, bool) (*Dictionary, error)
|
||||
ExportDictionary(context.Context, uint) (*Dictionary, error)
|
||||
ListDictionaries(context.Context, int, int, string, string, bool) ([]*Dictionary, int64, error)
|
||||
}
|
||||
|
||||
func (uc *SettingsUsecase) ImportDictionary(ctx context.Context, dictionary *Dictionary, details []*DictionaryDetail) error {
|
||||
return uc.SettingsRepo.ImportDictionary(ctx, dictionary, details)
|
||||
}
|
||||
|
||||
func (uc *SettingsUsecase) DictionaryDetailsByParent(ctx context.Context, dictionaryID, parentID uint) ([]*DictionaryDetail, error) {
|
||||
items, _, err := uc.ListDictionaryDetails(ctx, 0, 0, DictionaryDetailFilter{DictionaryID: dictionaryID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]*DictionaryDetail, 0)
|
||||
for _, item := range items {
|
||||
if (parentID == 0 && item.ParentID == nil) || (item.ParentID != nil && *item.ParentID == parentID) {
|
||||
out = append(out, item)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
@@ -9,4 +9,5 @@ var (
|
||||
// ErrInvalidCredentials is returned for any failed login, regardless of
|
||||
// whether the user exists, to avoid leaking which accounts are registered.
|
||||
ErrInvalidCredentials = errors.Unauthorized("INVALID_CREDENTIALS", "invalid credentials")
|
||||
ErrUserDisabled = errors.Forbidden("USER_DISABLED", "user disabled")
|
||||
)
|
||||
|
||||
@@ -16,6 +16,8 @@ type ExportTemplate struct {
|
||||
Order string
|
||||
Conditions []ExportCondition
|
||||
Joins []ExportJoin
|
||||
StartCreatedAt *time.Time
|
||||
EndCreatedAt *time.Time
|
||||
}
|
||||
type ExportRepo interface {
|
||||
CreateExportTemplate(context.Context, *ExportTemplate) error
|
||||
@@ -24,9 +26,9 @@ type ExportRepo interface {
|
||||
FindExportTemplate(context.Context, uint, string) (*ExportTemplate, error)
|
||||
ListExportTemplates(context.Context, int, int, *ExportTemplate) ([]*ExportTemplate, int64, error)
|
||||
QueryExport(context.Context, *ExportTemplate, map[string]string) ([]map[string]any, string, error)
|
||||
PreviewExport(context.Context, *ExportTemplate, map[string]string) (string, error)
|
||||
ImportExportRows(context.Context, *ExportTemplate, []map[string]any) error
|
||||
}
|
||||
type ExportUsecase struct{ repo ExportRepo }
|
||||
type ExportUsecase struct{ ExportRepo }
|
||||
|
||||
func NewExportUsecase(repo ExportRepo) *ExportUsecase { return &ExportUsecase{repo: repo} }
|
||||
func (uc *ExportUsecase) Repo() ExportRepo { return uc.repo }
|
||||
func NewExportUsecase(repo ExportRepo) *ExportUsecase { return &ExportUsecase{ExportRepo: repo} }
|
||||
|
||||
+18
-45
@@ -13,62 +13,35 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type MediaFile struct {
|
||||
ID uint
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
Name string
|
||||
CategoryID int
|
||||
URL, Tag, Key string
|
||||
Size int64
|
||||
Mime, MD5 string
|
||||
UserID uint
|
||||
}
|
||||
type AttachmentCategory struct {
|
||||
ID uint
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
Name string
|
||||
ParentID uint
|
||||
Children []*AttachmentCategory
|
||||
}
|
||||
type MediaMetadataRepo interface {
|
||||
CreateMedia(context.Context, *MediaFile) error
|
||||
FindMedia(context.Context, uint) (*MediaFile, error)
|
||||
FindMediaByHash(context.Context, uint, string) (*MediaFile, error)
|
||||
ListMedia(context.Context, int, int, string, int, string, uint) ([]*MediaFile, int64, error)
|
||||
UpdateMediaName(context.Context, uint, string) error
|
||||
DeleteMedia(context.Context, uint) error
|
||||
MediaKeyReferences(context.Context, string) (int64, error)
|
||||
CreateMediaBatch(context.Context, []*MediaFile) error
|
||||
SaveCategory(context.Context, *AttachmentCategory) error
|
||||
DeleteCategory(context.Context, uint) error
|
||||
ListCategories(context.Context) ([]*AttachmentCategory, error)
|
||||
}
|
||||
|
||||
type MediaRepo interface {
|
||||
MediaMetadataRepo
|
||||
UploadRepo
|
||||
}
|
||||
|
||||
type MediaUsecase struct {
|
||||
repo MediaRepo
|
||||
MediaRepo
|
||||
files FileStorage
|
||||
}
|
||||
|
||||
func NewMediaUsecase(repo MediaRepo, files FileStorage) *MediaUsecase {
|
||||
return &MediaUsecase{repo: repo, files: files}
|
||||
return &MediaUsecase{MediaRepo: repo, files: files}
|
||||
}
|
||||
func (uc *MediaUsecase) Repo() MediaRepo { return uc.repo }
|
||||
|
||||
var blockedExtensions = map[string]bool{".exe": true, ".dll": true, ".bat": true, ".cmd": true, ".sh": true, ".php": true, ".jsp": true, ".asp": true, ".aspx": true, ".html": true, ".htm": true, ".svg": true}
|
||||
var allowedMediaExtensions = map[string]bool{
|
||||
".jpg": true, ".jpeg": true, ".png": true, ".gif": true, ".webp": true, ".bmp": true, ".ico": true, ".avif": true,
|
||||
".mp3": true, ".wav": true, ".ogg": true, ".m4a": true, ".flac": true, ".aac": true,
|
||||
".mp4": true, ".webm": true, ".mov": true, ".avi": true, ".mkv": true,
|
||||
".txt": true, ".md": true, ".csv": true, ".json": true, ".log": true, ".pdf": true,
|
||||
".doc": true, ".docx": true, ".xls": true, ".xlsx": true, ".ppt": true, ".pptx": true,
|
||||
".zip": true, ".rar": true, ".7z": true, ".tar": true, ".gz": true, ".tgz": true, ".bin": true,
|
||||
}
|
||||
|
||||
func validateMediaName(name string) error {
|
||||
if name == "" || filepath.Base(name) != name {
|
||||
return errors.New("文件名不合法")
|
||||
if name == "" || strings.TrimSpace(name) != name || strings.ContainsAny(name, `/\`) {
|
||||
return errors.New("file extension is not allowed")
|
||||
}
|
||||
if blockedExtensions[strings.ToLower(filepath.Ext(name))] {
|
||||
return errors.New("文件类型不允许上传")
|
||||
if !allowedMediaExtensions[strings.ToLower(filepath.Ext(name))] {
|
||||
return errors.New("file extension is not allowed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -85,7 +58,7 @@ func (uc *MediaUsecase) Upload(ctx context.Context, userID uint, name, mime stri
|
||||
}
|
||||
media := &MediaFile{Name: name, CategoryID: categoryID, URL: stored.URL, Tag: strings.TrimPrefix(ext, "."), Key: key, Size: stored.Size, Mime: mime, MD5: hex.EncodeToString(hash.Sum(nil)), UserID: userID}
|
||||
if save {
|
||||
if err = uc.repo.CreateMedia(ctx, media); err != nil {
|
||||
if err = uc.CreateMedia(ctx, media); err != nil {
|
||||
_ = uc.files.Delete(ctx, key)
|
||||
return nil, err
|
||||
}
|
||||
@@ -93,11 +66,11 @@ func (uc *MediaUsecase) Upload(ctx context.Context, userID uint, name, mime stri
|
||||
return media, nil
|
||||
}
|
||||
func (uc *MediaUsecase) Delete(ctx context.Context, id uint) error {
|
||||
media, err := uc.repo.FindMedia(ctx, id)
|
||||
media, err := uc.FindMedia(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
count, err := uc.repo.MediaKeyReferences(ctx, media.Key)
|
||||
count, err := uc.MediaKeyReferences(ctx, media.Key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -106,7 +79,7 @@ func (uc *MediaUsecase) Delete(ctx context.Context, id uint) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return uc.repo.DeleteMedia(ctx, id)
|
||||
return uc.DeleteMedia(ctx, id)
|
||||
}
|
||||
func (uc *MediaUsecase) ListStorage(ctx context.Context, prefix, cursor string, limit int) ([]*StoredFile, string, bool, error) {
|
||||
return uc.files.List(ctx, prefix, cursor, limit)
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
type MediaFile struct {
|
||||
ID uint
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
Name string
|
||||
CategoryID int
|
||||
URL, Tag, Key string
|
||||
Size int64
|
||||
Mime, MD5 string
|
||||
UserID uint
|
||||
}
|
||||
|
||||
type MediaFilter struct {
|
||||
Page, PageSize int
|
||||
Keyword string
|
||||
CategoryID int
|
||||
Tag string
|
||||
UserID uint
|
||||
StartCreatedAt time.Time
|
||||
EndCreatedAt time.Time
|
||||
OrderKey string
|
||||
Desc bool
|
||||
}
|
||||
|
||||
type AttachmentCategory struct {
|
||||
ID uint
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
Name string
|
||||
ParentID uint
|
||||
Children []*AttachmentCategory
|
||||
}
|
||||
|
||||
type MediaMetadataRepo interface {
|
||||
CreateMedia(context.Context, *MediaFile) error
|
||||
FindMedia(context.Context, uint) (*MediaFile, error)
|
||||
FindMediaByHash(context.Context, uint, string) (*MediaFile, error)
|
||||
ListMedia(context.Context, MediaFilter) ([]*MediaFile, int64, error)
|
||||
UpdateMediaName(context.Context, uint, string) error
|
||||
DeleteMedia(context.Context, uint) error
|
||||
MediaKeyReferences(context.Context, string) (int64, error)
|
||||
CreateMediaBatch(context.Context, []*MediaFile) error
|
||||
SaveCategory(context.Context, *AttachmentCategory) error
|
||||
DeleteCategory(context.Context, uint) error
|
||||
ListCategories(context.Context) ([]*AttachmentCategory, error)
|
||||
}
|
||||
@@ -15,33 +15,6 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type UploadSession struct {
|
||||
ID uint
|
||||
UserID uint
|
||||
FileName, FileHash string
|
||||
FileSize, ChunkSize int64
|
||||
ChunkTotal int
|
||||
Status, StorageKey string
|
||||
MediaID uint
|
||||
}
|
||||
type UploadChunk struct {
|
||||
Index int
|
||||
Hash string
|
||||
Size int64
|
||||
}
|
||||
type UploadRepo interface {
|
||||
FindUploadingSession(context.Context, uint, string) (*UploadSession, error)
|
||||
CreateUploadSession(context.Context, *UploadSession) error
|
||||
FindUploadSession(context.Context, uint) (*UploadSession, error)
|
||||
CompleteUploadSession(context.Context, uint, string, uint) error
|
||||
DeleteUploadSession(context.Context, uint) error
|
||||
UpsertChunk(context.Context, uint, *UploadChunk) error
|
||||
ListChunks(context.Context, uint) ([]*UploadChunk, error)
|
||||
DeleteChunks(context.Context, uint) error
|
||||
StaleUploadSessionIDs(context.Context, time.Time) ([]uint, error)
|
||||
DeleteUploadData(context.Context, uint) error
|
||||
}
|
||||
|
||||
func (uc *MediaUsecase) InitUpload(ctx context.Context, userID uint, name, hash string, size, chunkSize int64, total int) (*UploadSession, *MediaFile, []int, error) {
|
||||
if err := validateMediaName(name); err != nil {
|
||||
return nil, nil, nil, err
|
||||
@@ -49,22 +22,22 @@ func (uc *MediaUsecase) InitUpload(ctx context.Context, userID uint, name, hash
|
||||
if size <= 0 || chunkSize <= 0 || total <= 0 {
|
||||
return nil, nil, nil, errors.New("上传参数不合法")
|
||||
}
|
||||
if media, err := uc.repo.FindMediaByHash(ctx, userID, hash); err == nil {
|
||||
if media, err := uc.FindMediaByHash(ctx, userID, hash); err == nil {
|
||||
copy := *media
|
||||
copy.ID = 0
|
||||
copy.Name = name
|
||||
if err = uc.repo.CreateMedia(ctx, ©); err == nil {
|
||||
if err = uc.CreateMedia(ctx, ©); err == nil {
|
||||
return nil, ©, nil, nil
|
||||
}
|
||||
}
|
||||
session, err := uc.repo.FindUploadingSession(ctx, userID, hash)
|
||||
session, err := uc.FindUploadingSession(ctx, userID, hash)
|
||||
if err != nil {
|
||||
session = &UploadSession{UserID: userID, FileName: name, FileHash: hash, FileSize: size, ChunkSize: chunkSize, ChunkTotal: total, Status: "uploading"}
|
||||
if err = uc.repo.CreateUploadSession(ctx, session); err != nil {
|
||||
if err = uc.CreateUploadSession(ctx, session); err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
}
|
||||
chunks, err := uc.repo.ListChunks(ctx, session.ID)
|
||||
chunks, err := uc.ListChunks(ctx, session.ID)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
@@ -76,7 +49,7 @@ func (uc *MediaUsecase) InitUpload(ctx context.Context, userID uint, name, hash
|
||||
return session, nil, indices, nil
|
||||
}
|
||||
func (uc *MediaUsecase) SaveChunk(ctx context.Context, userID, uploadID uint, index int, expected string, reader io.Reader) error {
|
||||
session, err := uc.repo.FindUploadSession(ctx, uploadID)
|
||||
session, err := uc.FindUploadSession(ctx, uploadID)
|
||||
if err != nil || session.UserID != userID {
|
||||
return errors.New("上传会话不存在或无权操作")
|
||||
}
|
||||
@@ -94,14 +67,14 @@ func (uc *MediaUsecase) SaveChunk(ctx context.Context, userID, uploadID uint, in
|
||||
_ = uc.files.Delete(ctx, key)
|
||||
return errors.New("分片校验失败")
|
||||
}
|
||||
return uc.repo.UpsertChunk(ctx, uploadID, &UploadChunk{Index: index, Hash: actual, Size: stored.Size})
|
||||
return uc.UpsertChunk(ctx, uploadID, &UploadChunk{Index: index, Hash: actual, Size: stored.Size})
|
||||
}
|
||||
func (uc *MediaUsecase) CompleteUpload(ctx context.Context, userID, uploadID uint, mime string) (*MediaFile, error) {
|
||||
session, err := uc.repo.FindUploadSession(ctx, uploadID)
|
||||
session, err := uc.FindUploadSession(ctx, uploadID)
|
||||
if err != nil || session.UserID != userID {
|
||||
return nil, errors.New("上传会话不存在或无权操作")
|
||||
}
|
||||
chunks, err := uc.repo.ListChunks(ctx, uploadID)
|
||||
chunks, err := uc.ListChunks(ctx, uploadID)
|
||||
if err != nil || len(chunks) != session.ChunkTotal {
|
||||
return nil, errors.New("分片不完整")
|
||||
}
|
||||
@@ -124,34 +97,34 @@ func (uc *MediaUsecase) CompleteUpload(ctx context.Context, userID, uploadID uin
|
||||
return nil, errors.New("整文件校验失败")
|
||||
}
|
||||
media := &MediaFile{Name: session.FileName, URL: stored.URL, Tag: strings.TrimPrefix(ext, "."), Key: key, Size: stored.Size, Mime: mime, MD5: hash, UserID: userID}
|
||||
if err = uc.repo.CreateMedia(ctx, media); err != nil {
|
||||
if err = uc.CreateMedia(ctx, media); err != nil {
|
||||
_ = uc.files.Delete(ctx, key)
|
||||
return nil, err
|
||||
}
|
||||
_ = uc.repo.CompleteUploadSession(ctx, uploadID, key, media.ID)
|
||||
_ = uc.repo.DeleteChunks(ctx, uploadID)
|
||||
_ = uc.CompleteUploadSession(ctx, uploadID, key, media.ID)
|
||||
_ = uc.DeleteChunks(ctx, uploadID)
|
||||
_ = uc.files.DeletePrefix(ctx, fmt.Sprintf(".chunks/%d", uploadID))
|
||||
return media, nil
|
||||
}
|
||||
func (uc *MediaUsecase) CancelUpload(ctx context.Context, userID, uploadID uint) error {
|
||||
session, err := uc.repo.FindUploadSession(ctx, uploadID)
|
||||
session, err := uc.FindUploadSession(ctx, uploadID)
|
||||
if err != nil || session.UserID != userID {
|
||||
return errors.New("上传会话不存在或无权操作")
|
||||
}
|
||||
_ = uc.repo.DeleteChunks(ctx, uploadID)
|
||||
_ = uc.DeleteChunks(ctx, uploadID)
|
||||
_ = uc.files.DeletePrefix(ctx, fmt.Sprintf(".chunks/%d", uploadID))
|
||||
return uc.repo.DeleteUploadSession(ctx, uploadID)
|
||||
return uc.DeleteUploadSession(ctx, uploadID)
|
||||
}
|
||||
func (uc *MediaUsecase) CleanupStale(ctx context.Context, ttlHours int) error {
|
||||
if ttlHours <= 0 {
|
||||
ttlHours = 24
|
||||
}
|
||||
ids, err := uc.repo.StaleUploadSessionIDs(ctx, time.Now().Add(-time.Duration(ttlHours)*time.Hour))
|
||||
ids, err := uc.StaleUploadSessionIDs(ctx, time.Now().Add(-time.Duration(ttlHours)*time.Hour))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, id := range ids {
|
||||
if err = uc.repo.DeleteUploadData(ctx, id); err != nil {
|
||||
if err = uc.DeleteUploadData(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = uc.files.DeletePrefix(ctx, fmt.Sprintf(".chunks/%d", id)); err != nil {
|
||||
|
||||
@@ -47,6 +47,7 @@ type MenuRepo interface {
|
||||
SetAuthorityMenus(context.Context, uint, []uint) error
|
||||
AuthorityMenuIDs(context.Context, uint) ([]uint, error)
|
||||
MenuRoleIDs(context.Context, uint) ([]uint, error)
|
||||
DefaultRouterRoleIDs(context.Context, uint) ([]uint, error)
|
||||
SetMenuRoles(context.Context, uint, []uint) error
|
||||
}
|
||||
|
||||
@@ -128,6 +129,9 @@ func (uc *MenuUsecase) AuthorityMenus(ctx context.Context, authorityID uint) ([]
|
||||
func (uc *MenuUsecase) RoleIDs(ctx context.Context, menuID uint) ([]uint, error) {
|
||||
return uc.repo.MenuRoleIDs(ctx, menuID)
|
||||
}
|
||||
func (uc *MenuUsecase) DefaultRouterRoleIDs(ctx context.Context, menuID uint) ([]uint, error) {
|
||||
return uc.repo.DefaultRouterRoleIDs(ctx, menuID)
|
||||
}
|
||||
|
||||
func (uc *MenuUsecase) SetRoles(ctx context.Context, menuID uint, ids []uint) error {
|
||||
return uc.repo.SetMenuRoles(ctx, menuID, ids)
|
||||
|
||||
@@ -14,8 +14,28 @@ type Department struct {
|
||||
Ancestors string
|
||||
Sort int
|
||||
LeaderID uint
|
||||
Leader *User
|
||||
Status bool
|
||||
Children []*Department
|
||||
NamePath string
|
||||
}
|
||||
|
||||
type DepartmentRepo interface {
|
||||
CreateDepartment(context.Context, *Department) error
|
||||
UpdateDepartment(context.Context, *Department) error
|
||||
DeleteDepartment(context.Context, uint) error
|
||||
FindDepartment(context.Context, uint) (*Department, error)
|
||||
ListDepartments(context.Context, string) ([]*Department, error)
|
||||
DepartmentUserIDs(context.Context, uint) ([]uint, error)
|
||||
SetDepartmentUsers(context.Context, uint, []uint) error
|
||||
SetUserDepartments(context.Context, uint, []uint, uint) error
|
||||
}
|
||||
|
||||
func (uc *OrganizationUsecase) Departments(ctx context.Context, name string) ([]*Department, error) {
|
||||
return uc.ListDepartments(ctx, name)
|
||||
}
|
||||
func (uc *OrganizationUsecase) Department(ctx context.Context, id uint) (*Department, error) {
|
||||
return uc.FindDepartment(ctx, id)
|
||||
}
|
||||
|
||||
type Position struct {
|
||||
@@ -29,76 +49,37 @@ type Position struct {
|
||||
Remark string
|
||||
}
|
||||
|
||||
type OrganizationRepo interface {
|
||||
CreateDepartment(context.Context, *Department) error
|
||||
UpdateDepartment(context.Context, *Department) error
|
||||
DeleteDepartment(context.Context, uint) error
|
||||
FindDepartment(context.Context, uint) (*Department, error)
|
||||
ListDepartments(context.Context, string) ([]*Department, error)
|
||||
DepartmentUserIDs(context.Context, uint) ([]uint, error)
|
||||
SetDepartmentUsers(context.Context, uint, []uint) error
|
||||
SetUserDepartments(context.Context, uint, []uint, uint) error
|
||||
type PositionListFilter struct {
|
||||
Name string
|
||||
Code string
|
||||
Status *bool
|
||||
}
|
||||
|
||||
type PositionRepo interface {
|
||||
CreatePosition(context.Context, *Position) error
|
||||
UpdatePosition(context.Context, *Position) error
|
||||
DeletePosition(context.Context, uint) error
|
||||
FindPosition(context.Context, uint) (*Position, error)
|
||||
ListPositions(context.Context, int, int, *Position) ([]*Position, int64, error)
|
||||
ListPositions(context.Context, int, int, *PositionListFilter) ([]*Position, int64, error)
|
||||
PositionUserIDs(context.Context, uint) ([]uint, error)
|
||||
SetPositionUsers(context.Context, uint, []uint) error
|
||||
SetUserPositions(context.Context, uint, []uint) error
|
||||
}
|
||||
|
||||
type OrganizationUsecase struct{ repo OrganizationRepo }
|
||||
|
||||
func NewOrganizationUsecase(repo OrganizationRepo) *OrganizationUsecase {
|
||||
return &OrganizationUsecase{repo: repo}
|
||||
}
|
||||
|
||||
func (uc *OrganizationUsecase) Departments(ctx context.Context, name string) ([]*Department, error) {
|
||||
return uc.repo.ListDepartments(ctx, name)
|
||||
}
|
||||
func (uc *OrganizationUsecase) CreateDepartment(ctx context.Context, value *Department) error {
|
||||
return uc.repo.CreateDepartment(ctx, value)
|
||||
}
|
||||
func (uc *OrganizationUsecase) UpdateDepartment(ctx context.Context, value *Department) error {
|
||||
return uc.repo.UpdateDepartment(ctx, value)
|
||||
}
|
||||
func (uc *OrganizationUsecase) DeleteDepartment(ctx context.Context, id uint) error {
|
||||
return uc.repo.DeleteDepartment(ctx, id)
|
||||
}
|
||||
func (uc *OrganizationUsecase) Department(ctx context.Context, id uint) (*Department, error) {
|
||||
return uc.repo.FindDepartment(ctx, id)
|
||||
}
|
||||
func (uc *OrganizationUsecase) DepartmentUserIDs(ctx context.Context, id uint) ([]uint, error) {
|
||||
return uc.repo.DepartmentUserIDs(ctx, id)
|
||||
}
|
||||
func (uc *OrganizationUsecase) SetDepartmentUsers(ctx context.Context, id uint, ids []uint) error {
|
||||
return uc.repo.SetDepartmentUsers(ctx, id, ids)
|
||||
}
|
||||
func (uc *OrganizationUsecase) SetUserDepartments(ctx context.Context, id uint, ids []uint, primary uint) error {
|
||||
return uc.repo.SetUserDepartments(ctx, id, ids, primary)
|
||||
}
|
||||
func (uc *OrganizationUsecase) Positions(ctx context.Context, page, size int, filter *Position) ([]*Position, int64, error) {
|
||||
return uc.repo.ListPositions(ctx, page, size, filter)
|
||||
}
|
||||
func (uc *OrganizationUsecase) CreatePosition(ctx context.Context, value *Position) error {
|
||||
return uc.repo.CreatePosition(ctx, value)
|
||||
}
|
||||
func (uc *OrganizationUsecase) UpdatePosition(ctx context.Context, value *Position) error {
|
||||
return uc.repo.UpdatePosition(ctx, value)
|
||||
}
|
||||
func (uc *OrganizationUsecase) DeletePosition(ctx context.Context, id uint) error {
|
||||
return uc.repo.DeletePosition(ctx, id)
|
||||
func (uc *OrganizationUsecase) Positions(ctx context.Context, page, size int, filter *PositionListFilter) ([]*Position, int64, error) {
|
||||
return uc.ListPositions(ctx, page, size, filter)
|
||||
}
|
||||
func (uc *OrganizationUsecase) Position(ctx context.Context, id uint) (*Position, error) {
|
||||
return uc.repo.FindPosition(ctx, id)
|
||||
return uc.FindPosition(ctx, id)
|
||||
}
|
||||
func (uc *OrganizationUsecase) PositionUserIDs(ctx context.Context, id uint) ([]uint, error) {
|
||||
return uc.repo.PositionUserIDs(ctx, id)
|
||||
|
||||
type OrganizationRepo interface {
|
||||
DepartmentRepo
|
||||
PositionRepo
|
||||
}
|
||||
func (uc *OrganizationUsecase) SetPositionUsers(ctx context.Context, id uint, ids []uint) error {
|
||||
return uc.repo.SetPositionUsers(ctx, id, ids)
|
||||
}
|
||||
func (uc *OrganizationUsecase) SetUserPositions(ctx context.Context, id uint, ids []uint) error {
|
||||
return uc.repo.SetUserPositions(ctx, id, ids)
|
||||
|
||||
type OrganizationUsecase struct{ OrganizationRepo }
|
||||
|
||||
func NewOrganizationUsecase(repo OrganizationRepo) *OrganizationUsecase {
|
||||
return &OrganizationUsecase{OrganizationRepo: repo}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,15 @@ import (
|
||||
)
|
||||
|
||||
type SystemParameter struct {
|
||||
ID uint
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
Name string
|
||||
Key string
|
||||
Value string
|
||||
Desc string
|
||||
ID uint
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
Name string
|
||||
Key string
|
||||
Value string
|
||||
Desc string
|
||||
StartCreatedAt *time.Time
|
||||
EndCreatedAt *time.Time
|
||||
}
|
||||
|
||||
type ParameterRepo interface {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package biz
|
||||
|
||||
import "context"
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type SecurityConfig struct {
|
||||
ID uint
|
||||
@@ -29,3 +32,42 @@ type SecurityRepo interface {
|
||||
SecurityConfig(context.Context) (*SecurityConfig, error)
|
||||
SaveSecurityConfig(context.Context, *SecurityConfig) error
|
||||
}
|
||||
|
||||
func (uc *SettingsUsecase) UpdateSecurity(ctx context.Context, value *SecurityConfig) error {
|
||||
if value.KeyLong < 1 || value.ImgWidth < 1 || value.ImgHeight < 1 || value.PwdMinLength < 1 || value.LimitWindow < 1 || value.LimitCount < 1 || value.LockThreshold < 1 || value.LockDuration < 1 || value.PwdExpireDays < 1 {
|
||||
return errors.New("安全配置数值必须大于0")
|
||||
}
|
||||
return uc.SaveSecurityConfig(ctx, value)
|
||||
}
|
||||
|
||||
func (uc *SettingsUsecase) ValidatePassword(value *SecurityConfig, password string) error {
|
||||
if len([]rune(password)) < value.PwdMinLength {
|
||||
return errors.New("密码长度不足")
|
||||
}
|
||||
hasUpper, hasLower, hasDigit, hasSpecial := false, false, false, false
|
||||
for _, ch := range password {
|
||||
switch {
|
||||
case ch >= 'A' && ch <= 'Z':
|
||||
hasUpper = true
|
||||
case ch >= 'a' && ch <= 'z':
|
||||
hasLower = true
|
||||
case ch >= '0' && ch <= '9':
|
||||
hasDigit = true
|
||||
default:
|
||||
hasSpecial = true
|
||||
}
|
||||
}
|
||||
if value.PwdRequireUpper && !hasUpper {
|
||||
return errors.New("密码必须包含大写字母")
|
||||
}
|
||||
if value.PwdRequireLower && !hasLower {
|
||||
return errors.New("密码必须包含小写字母")
|
||||
}
|
||||
if value.PwdRequireDigit && !hasDigit {
|
||||
return errors.New("密码必须包含数字")
|
||||
}
|
||||
if value.PwdRequireSpecial && !hasSpecial {
|
||||
return errors.New("密码必须包含特殊字符")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ type SettingsRepo interface {
|
||||
SecurityRepo
|
||||
}
|
||||
|
||||
type SettingsUsecase struct{ repo SettingsRepo }
|
||||
type SettingsUsecase struct{ SettingsRepo }
|
||||
|
||||
func NewSettingsUsecase(repo SettingsRepo) *SettingsUsecase { return &SettingsUsecase{repo: repo} }
|
||||
func (uc *SettingsUsecase) Repo() SettingsRepo { return uc.repo }
|
||||
func NewSettingsUsecase(repo SettingsRepo) *SettingsUsecase {
|
||||
return &SettingsUsecase{SettingsRepo: repo}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ type InitializationRepo interface {
|
||||
Initialize(context.Context, *DatabaseConfig) error
|
||||
PersistConfig(context.Context) error
|
||||
PersistAdminConfig(context.Context, []byte) error
|
||||
PersistRuntimeConfig(context.Context, []byte, []byte) error
|
||||
ReloadConfig(context.Context) error
|
||||
}
|
||||
|
||||
@@ -28,4 +29,7 @@ func (uc *SystemUsecase) PersistConfig(ctx context.Context) error { return uc.re
|
||||
func (uc *SystemUsecase) PersistAdminConfig(ctx context.Context, value []byte) error {
|
||||
return uc.repo.PersistAdminConfig(ctx, value)
|
||||
}
|
||||
func (uc *SystemUsecase) PersistRuntimeConfig(ctx context.Context, data, admin []byte) error {
|
||||
return uc.repo.PersistRuntimeConfig(ctx, data, admin)
|
||||
}
|
||||
func (uc *SystemUsecase) ReloadConfig(ctx context.Context) error { return uc.repo.ReloadConfig(ctx) }
|
||||
|
||||
+108
-3
@@ -2,7 +2,13 @@ package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
type TimedTask struct {
|
||||
@@ -28,6 +34,37 @@ type TimedTaskLog struct {
|
||||
DurationMS int64
|
||||
Status, ErrorMsg, Output string
|
||||
}
|
||||
|
||||
const (
|
||||
TaskExecutorMethod = "method"
|
||||
TaskExecutorHTTP = "http"
|
||||
TaskMethodClearDB = "ClearDB"
|
||||
TaskMethodUploads = "CleanStaleUploads"
|
||||
)
|
||||
|
||||
type TaskMethod struct {
|
||||
Name string
|
||||
Description string
|
||||
}
|
||||
|
||||
var registeredTaskMethods = []TaskMethod{
|
||||
{Name: TaskMethodClearDB, Description: "清理数据库过期日志(操作记录、JWT黑名单、定时任务日志)"},
|
||||
{Name: TaskMethodUploads, Description: "清理过期大文件上传会话"},
|
||||
}
|
||||
|
||||
func RegisteredTaskMethods() []TaskMethod {
|
||||
return append([]TaskMethod(nil), registeredTaskMethods...)
|
||||
}
|
||||
|
||||
func registeredTaskMethod(name string) bool {
|
||||
for _, method := range registeredTaskMethods {
|
||||
if method.Name == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type TaskRepo interface {
|
||||
CreateTask(context.Context, *TimedTask) error
|
||||
UpdateTask(context.Context, *TimedTask) error
|
||||
@@ -40,7 +77,75 @@ type TaskRepo interface {
|
||||
CleanupLogs(context.Context) error
|
||||
TaskNameExists(context.Context, string, uint) (bool, error)
|
||||
}
|
||||
type TaskUsecase struct{ repo TaskRepo }
|
||||
type TaskUsecase struct{ TaskRepo }
|
||||
|
||||
func NewTaskUsecase(repo TaskRepo) *TaskUsecase { return &TaskUsecase{repo: repo} }
|
||||
func (uc *TaskUsecase) Repo() TaskRepo { return uc.repo }
|
||||
func NewTaskUsecase(repo TaskRepo) *TaskUsecase { return &TaskUsecase{TaskRepo: repo} }
|
||||
|
||||
func (uc *TaskUsecase) Validate(value *TimedTask) error {
|
||||
if value.Name == "" {
|
||||
return errors.New("任务名不能为空")
|
||||
}
|
||||
var err error
|
||||
if value.WithSeconds {
|
||||
_, err = cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor).Parse(value.Spec)
|
||||
} else {
|
||||
_, err = cron.ParseStandard(value.Spec)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("cron 表达式非法: %w", err)
|
||||
}
|
||||
switch value.ExecutorType {
|
||||
case TaskExecutorMethod:
|
||||
if !registeredTaskMethod(value.MethodName) {
|
||||
return errors.New("方法未注册")
|
||||
}
|
||||
if len(value.Params) > 0 && !json.Valid(value.Params) {
|
||||
return errors.New("params 必须是合法 JSON")
|
||||
}
|
||||
case TaskExecutorHTTP:
|
||||
parsed, parseErr := url.Parse(value.HTTPURL)
|
||||
if parseErr != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.Hostname() == "" {
|
||||
return errors.New("httpUrl 必须是合法的 http/https 地址")
|
||||
}
|
||||
if len(value.HTTPHeader) > 0 {
|
||||
headers := map[string]string{}
|
||||
if json.Unmarshal(value.HTTPHeader, &headers) != nil {
|
||||
return errors.New("httpHeader 必须是 JSON 对象")
|
||||
}
|
||||
}
|
||||
default:
|
||||
return errors.New("executorType 必须为 method 或 http")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (uc *TaskUsecase) Create(ctx context.Context, value *TimedTask) error {
|
||||
if err := uc.Validate(value); err != nil {
|
||||
return err
|
||||
}
|
||||
exists, err := uc.TaskNameExists(ctx, value.Name, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
return fmt.Errorf("任务名 %s 已存在", value.Name)
|
||||
}
|
||||
return uc.CreateTask(ctx, value)
|
||||
}
|
||||
|
||||
func (uc *TaskUsecase) Update(ctx context.Context, value *TimedTask) error {
|
||||
if value.ID == 0 {
|
||||
return errors.New("缺少任务 ID")
|
||||
}
|
||||
if err := uc.Validate(value); err != nil {
|
||||
return err
|
||||
}
|
||||
exists, err := uc.TaskNameExists(ctx, value.Name, value.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
return fmt.Errorf("任务名 %s 已存在", value.Name)
|
||||
}
|
||||
return uc.UpdateTask(ctx, value)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
type UploadSession struct {
|
||||
ID uint
|
||||
UserID uint
|
||||
FileName, FileHash string
|
||||
FileSize, ChunkSize int64
|
||||
ChunkTotal int
|
||||
Status, StorageKey string
|
||||
MediaID uint
|
||||
}
|
||||
|
||||
type UploadChunk struct {
|
||||
Index int
|
||||
Hash string
|
||||
Size int64
|
||||
}
|
||||
|
||||
type UploadRepo interface {
|
||||
FindUploadingSession(context.Context, uint, string) (*UploadSession, error)
|
||||
CreateUploadSession(context.Context, *UploadSession) error
|
||||
FindUploadSession(context.Context, uint) (*UploadSession, error)
|
||||
CompleteUploadSession(context.Context, uint, string, uint) error
|
||||
DeleteUploadSession(context.Context, uint) error
|
||||
UpsertChunk(context.Context, uint, *UploadChunk) error
|
||||
ListChunks(context.Context, uint) ([]*UploadChunk, error)
|
||||
DeleteChunks(context.Context, uint) error
|
||||
StaleUploadSessionIDs(context.Context, time.Time) ([]uint, error)
|
||||
DeleteUploadData(context.Context, uint) error
|
||||
}
|
||||
+57
-5
@@ -41,6 +41,7 @@ type Authority struct {
|
||||
ParentID *uint
|
||||
DataScope int
|
||||
DefaultRouter string
|
||||
Children []*Authority
|
||||
}
|
||||
|
||||
type UserRepo interface {
|
||||
@@ -49,13 +50,17 @@ type UserRepo interface {
|
||||
MenusByAuthority(context.Context, uint) ([]*Menu, error)
|
||||
ListUsers(context.Context, int, int, *UserListFilter) ([]*User, int64, error)
|
||||
CreateUser(context.Context, *User) (*User, error)
|
||||
CreateUserWithAuthorities(context.Context, *User, []uint) (*User, error)
|
||||
UpdateUser(context.Context, *User) error
|
||||
UpdateSelfUser(context.Context, *User) error
|
||||
UpdateUserWithAuthorities(context.Context, *User, []uint) error
|
||||
DeleteUser(context.Context, uint) error
|
||||
UpdatePassword(context.Context, uint, string, bool) error
|
||||
SetUserAuthorities(context.Context, uint, []uint) error
|
||||
SetUserAuthority(context.Context, uint, uint) error
|
||||
SetUserSetting(context.Context, uint, map[string]any) error
|
||||
ListAuthorities(context.Context) ([]*Authority, error)
|
||||
FillDepartmentNamePaths(context.Context, *User) error
|
||||
}
|
||||
|
||||
func (uc *SystemUsecase) Login(ctx context.Context, username, password string) (*User, error) {
|
||||
@@ -63,11 +68,37 @@ func (uc *SystemUsecase) Login(ctx context.Context, username, password string) (
|
||||
if err != nil || bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password)) != nil {
|
||||
return nil, ErrInvalidCredentials
|
||||
}
|
||||
uc.fallbackDefaultRouter(ctx, u)
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func (uc *SystemUsecase) User(ctx context.Context, id uint) (*User, error) {
|
||||
return uc.repo.FindUserByID(ctx, id)
|
||||
user, err := uc.repo.FindUserByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = uc.repo.FillDepartmentNamePaths(ctx, user)
|
||||
uc.fallbackDefaultRouter(ctx, user)
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (uc *SystemUsecase) fallbackDefaultRouter(ctx context.Context, user *User) {
|
||||
if user == nil || user.Authority.DefaultRouter == "" {
|
||||
return
|
||||
}
|
||||
menus, err := uc.repo.MenusByAuthority(ctx, user.AuthorityID)
|
||||
if err != nil || !menuNameExists(menus, user.Authority.DefaultRouter) {
|
||||
user.Authority.DefaultRouter = "404"
|
||||
}
|
||||
}
|
||||
|
||||
func menuNameExists(menus []*Menu, name string) bool {
|
||||
for _, menu := range menus {
|
||||
if menu.Name == name || menuNameExists(menu.Children, name) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (uc *SystemUsecase) Menus(ctx context.Context, authorityID uint) ([]*Menu, error) {
|
||||
@@ -77,16 +108,37 @@ func (uc *SystemUsecase) Menus(ctx context.Context, authorityID uint) ([]*Menu,
|
||||
func (uc *SystemUsecase) ListUsers(ctx context.Context, page, pageSize int, filter *UserListFilter) ([]*User, int64, error) {
|
||||
return uc.repo.ListUsers(ctx, page, pageSize, filter)
|
||||
}
|
||||
func (uc *SystemUsecase) CreateUser(ctx context.Context, user *User) (*User, error) {
|
||||
func (uc *SystemUsecase) CreateUser(ctx context.Context, user *User, authorityIDs []uint) (*User, error) {
|
||||
if user.AuthorityID == 0 && len(authorityIDs) > 0 {
|
||||
user.AuthorityID = authorityIDs[0]
|
||||
}
|
||||
authorityIDs = includeAuthority(authorityIDs, user.AuthorityID)
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user.Password = string(hash)
|
||||
return uc.repo.CreateUser(ctx, user)
|
||||
return uc.repo.CreateUserWithAuthorities(ctx, user, authorityIDs)
|
||||
}
|
||||
func (uc *SystemUsecase) UpdateUser(ctx context.Context, user *User) error {
|
||||
return uc.repo.UpdateUser(ctx, user)
|
||||
func (uc *SystemUsecase) UpdateUser(ctx context.Context, user *User, authorityIDs []uint) error {
|
||||
authorityIDs = includeAuthority(authorityIDs, user.AuthorityID)
|
||||
return uc.repo.UpdateUserWithAuthorities(ctx, user, authorityIDs)
|
||||
}
|
||||
|
||||
func (uc *SystemUsecase) UpdateSelfUser(ctx context.Context, user *User) error {
|
||||
return uc.repo.UpdateSelfUser(ctx, user)
|
||||
}
|
||||
|
||||
func includeAuthority(ids []uint, primary uint) []uint {
|
||||
if primary == 0 {
|
||||
return ids
|
||||
}
|
||||
for _, id := range ids {
|
||||
if id == primary {
|
||||
return ids
|
||||
}
|
||||
}
|
||||
return append(ids, primary)
|
||||
}
|
||||
func (uc *SystemUsecase) DeleteUser(ctx context.Context, id uint) error {
|
||||
return uc.repo.DeleteUser(ctx, id)
|
||||
|
||||
@@ -2,9 +2,12 @@ package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
var ErrInvalidVersion = errors.New("版本信息格式错误")
|
||||
|
||||
type Version struct {
|
||||
ID uint
|
||||
CreatedAt time.Time
|
||||
@@ -27,11 +30,10 @@ type VersionRepo interface {
|
||||
CreateVersion(context.Context, *Version) error
|
||||
DeleteVersions(context.Context, []uint) error
|
||||
FindVersion(context.Context, uint) (*Version, error)
|
||||
ListVersions(context.Context, int, int, string, string) ([]*Version, int64, error)
|
||||
ListVersions(context.Context, int, int, string, string, []*time.Time) ([]*Version, int64, error)
|
||||
BuildVersionBundle(context.Context, []uint, []uint, []uint) (*VersionBundle, error)
|
||||
ImportVersionBundle(context.Context, *VersionBundle) error
|
||||
}
|
||||
type VersionUsecase struct{ repo VersionRepo }
|
||||
type VersionUsecase struct{ VersionRepo }
|
||||
|
||||
func NewVersionUsecase(repo VersionRepo) *VersionUsecase { return &VersionUsecase{repo: repo} }
|
||||
func (uc *VersionUsecase) Repo() VersionRepo { return uc.repo }
|
||||
func NewVersionUsecase(repo VersionRepo) *VersionUsecase { return &VersionUsecase{VersionRepo: repo} }
|
||||
|
||||
Reference in New Issue
Block a user