优化结构

This commit is contained in:
Yvan
2026-08-24 13:25:16 +08:00
parent 7a9a738c6a
commit 0b6b8ddf1a
72 changed files with 2475 additions and 1443 deletions
+3 -40
View File
@@ -13,6 +13,7 @@ import (
"time"
"github.com/google/uuid"
"kra/internal/utils/uploadpolicy"
)
type MediaRepo interface {
@@ -39,23 +40,8 @@ func (uc *MediaUsecase) maxMediaFileSize() int64 {
return uc.settings.MediaSettings().EffectiveMaxFileSize()
}
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 == "" || strings.TrimSpace(name) != name || strings.ContainsAny(name, `/\`) {
return errors.New("file extension is not allowed")
}
if !allowedMediaExtensions[strings.ToLower(filepath.Ext(name))] {
return errors.New("file extension is not allowed")
}
return nil
return uploadpolicy.ValidateFileName(name)
}
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 {
@@ -69,7 +55,7 @@ func (uc *MediaUsecase) Upload(ctx context.Context, userID uint, name, suppliedM
// 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 != "" {
if detected := uploadpolicy.MIMEByExtension(ext); detected != "" {
suppliedMIME = detected
} else if header, _ := buffered.Peek(512); len(header) > 0 {
suppliedMIME = http.DetectContentType(header)
@@ -113,29 +99,6 @@ func (uc *MediaUsecase) Upload(ctx context.Context, userID uint, name, suppliedM
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 {
+4 -7
View File
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"errors"
"kra/internal/config"
)
var ErrTaskRuntimeReload = errors.New("task runtime reload failed")
@@ -22,8 +23,7 @@ type InitializationRepo interface {
IsInitialized(context.Context) (bool, error)
Initialize(context.Context, *DatabaseConfig) error
PersistConfig(context.Context) error
PersistAdminConfig(context.Context, []byte) error
PersistRuntimeConfig(context.Context, []byte, []byte) error
PersistRuntimeConfig(context.Context, *config.Config) error
ReloadConfig(context.Context) error
ConfigurationJSON() (json.RawMessage, error)
SaveConfigurationJSON(context.Context, json.RawMessage) error
@@ -63,11 +63,8 @@ func (uc *SystemConfigUsecase) Initialize(ctx context.Context, config *DatabaseC
func (uc *SystemConfigUsecase) PersistConfig(ctx context.Context) error {
return uc.repo.PersistConfig(ctx)
}
func (uc *SystemConfigUsecase) PersistAdminConfig(ctx context.Context, value []byte) error {
return uc.repo.PersistAdminConfig(ctx, value)
}
func (uc *SystemConfigUsecase) PersistRuntimeConfig(ctx context.Context, data, admin []byte) error {
return uc.repo.PersistRuntimeConfig(ctx, data, admin)
func (uc *SystemConfigUsecase) PersistRuntimeConfig(ctx context.Context, value *config.Config) error {
return uc.repo.PersistRuntimeConfig(ctx, value)
}
func (uc *SystemConfigUsecase) ReloadConfig(ctx context.Context) error {
if err := uc.repo.ReloadConfig(ctx); err != nil {
+2 -5
View File
@@ -9,6 +9,7 @@ import (
"time"
"github.com/robfig/cron/v3"
platformtask "kra/pkg/task"
)
type TimedTask struct {
@@ -77,13 +78,9 @@ type TaskUsecase struct {
methods TaskMethodRegistry
}
func NewTaskUsecase(repo TaskRepo) *TaskUsecase {
return NewTaskUsecaseWithRegistry(repo, DefaultTaskMethodRegistry())
}
func NewTaskUsecaseWithRegistry(repo TaskRepo, methods TaskMethodRegistry) *TaskUsecase {
if methods == nil {
methods = DefaultTaskMethodRegistry()
methods = platformtask.NewRegistry()
}
return &TaskUsecase{TaskRepo: repo, methods: methods}
}
-17
View File
@@ -6,23 +6,6 @@ type TaskMethodFunc = platformtask.MethodFunc
type TaskMethod = platformtask.Method
type TaskMethodRegistry interface {
Register(platformtask.Method)
Lookup(string) (TaskMethodFunc, bool)
List() []TaskMethod
}
var defaultTaskMethods = platformtask.NewRegistry()
// RegisterTaskMethod remains as a compatibility helper for tests and callers
// that have not moved to constructor injection yet.
func RegisterTaskMethod(name, description string, fn TaskMethodFunc) {
defaultTaskMethods.Register(platformtask.Method{Name: name, Description: description, Run: fn})
}
func TaskMethodByName(name string) (TaskMethodFunc, bool) {
return defaultTaskMethods.Lookup(name)
}
func RegisteredTaskMethods() []TaskMethod { return defaultTaskMethods.List() }
func DefaultTaskMethodRegistry() TaskMethodRegistry { return defaultTaskMethods }
+9 -6
View File
@@ -4,11 +4,14 @@ import (
"context"
"encoding/json"
"errors"
platformtask "kra/pkg/task"
"sync"
"testing"
"time"
)
var testTaskMethods = platformtask.NewRegistry()
type applicationTaskRepo struct {
mu sync.Mutex
deleted bool
@@ -112,14 +115,14 @@ func (r *applicationTaskRuntime) Unsubscribe(uint, chan []byte) {}
func validApplicationTask(id uint) *TimedTask {
const methodName = "biz-test-application-task"
RegisterTaskMethod(methodName, "test", func(context.Context, json.RawMessage) error { return nil })
testTaskMethods.Register(platformtask.Method{Name: methodName, Description: "test", Run: func(context.Context, json.RawMessage) error { return nil }})
return &TimedTask{ID: id, Name: "test", Spec: "0 0 * * *", ExecutorType: TaskExecutorMethod, MethodName: methodName, Enabled: true}
}
func TestTaskApplicationRepairsRuntimeAfterScheduleFailure(t *testing.T) {
repo := &applicationTaskRepo{createdID: 41}
runtime := &applicationTaskRuntime{scheduleErr: errors.New("schedule failed")}
uc := NewTaskApplicationUsecase(NewTaskUsecase(repo), runtime)
uc := NewTaskApplicationUsecase(NewTaskUsecaseWithRegistry(repo, testTaskMethods), runtime)
ctx, cancel := context.WithCancel(context.Background())
cancel()
@@ -137,7 +140,7 @@ func TestTaskApplicationRepairsRuntimeAfterScheduleFailure(t *testing.T) {
func TestTaskApplicationReturnsScheduleErrorWhenRepairFails(t *testing.T) {
repo := &applicationTaskRepo{createdID: 42}
runtime := &applicationTaskRuntime{scheduleErr: errors.New("schedule failed"), reloadErr: errors.New("reload failed")}
uc := NewTaskApplicationUsecase(NewTaskUsecase(repo), runtime)
uc := NewTaskApplicationUsecase(NewTaskUsecaseWithRegistry(repo, testTaskMethods), runtime)
err := uc.Create(context.Background(), validApplicationTask(0))
if !errors.Is(err, ErrTaskSchedule) {
@@ -158,7 +161,7 @@ func TestTaskApplicationUpdateAndToggleUseRuntimeRepair(t *testing.T) {
t.Run(test.name, func(t *testing.T) {
repo := &applicationTaskRepo{}
runtime := &applicationTaskRuntime{scheduleErr: errors.New("schedule failed")}
uc := NewTaskApplicationUsecase(NewTaskUsecase(repo), runtime)
uc := NewTaskApplicationUsecase(NewTaskUsecaseWithRegistry(repo, testTaskMethods), runtime)
if err := test.run(uc); err != nil {
t.Fatalf("operation error = %v", err)
}
@@ -172,7 +175,7 @@ func TestTaskApplicationUpdateAndToggleUseRuntimeRepair(t *testing.T) {
func TestTaskApplicationDeleteRemovesRuntimeAfterDatabase(t *testing.T) {
repo := &applicationTaskRepo{}
runtime := &applicationTaskRuntime{repo: repo}
uc := NewTaskApplicationUsecase(NewTaskUsecase(repo), runtime)
uc := NewTaskApplicationUsecase(NewTaskUsecaseWithRegistry(repo, testTaskMethods), runtime)
if err := uc.Delete(context.Background(), 8); err != nil {
t.Fatalf("Delete() error = %v", err)
}
@@ -187,7 +190,7 @@ func TestTaskApplicationDeleteRemovesRuntimeAfterDatabase(t *testing.T) {
func TestTaskApplicationDeleteKeepsRuntimeWhenDatabaseFails(t *testing.T) {
repo := &applicationTaskRepo{deleteErr: errors.New("delete failed")}
runtime := &applicationTaskRuntime{repo: repo}
uc := NewTaskApplicationUsecase(NewTaskUsecase(repo), runtime)
uc := NewTaskApplicationUsecase(NewTaskUsecaseWithRegistry(repo, testTaskMethods), runtime)
if err := uc.Delete(context.Background(), 8); err == nil {
t.Fatal("Delete() error = nil, want database error")
}