This commit is contained in:
parent
1945d34894
commit
560ae46b82
|
|
@ -83,7 +83,10 @@ func NewLogViewerUsecase(repo LogFileRepo) *LogViewerUsecase {
|
||||||
type ErrorRecord struct {
|
type ErrorRecord struct {
|
||||||
ID uint
|
ID uint
|
||||||
CreatedAt, UpdatedAt time.Time
|
CreatedAt, UpdatedAt time.Time
|
||||||
Form, Info, Level, RequestID, TraceID, Solution, Status string
|
Form, Info, Solution *string
|
||||||
|
Level string
|
||||||
|
RequestID, TraceID string
|
||||||
|
Status string
|
||||||
CreatedAtRange []time.Time
|
CreatedAtRange []time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -173,17 +173,13 @@ func (uc *MediaUsecase) CleanupStale(ctx context.Context, ttlHours int) error {
|
||||||
if ttlHours <= 0 {
|
if ttlHours <= 0 {
|
||||||
ttlHours = 24
|
ttlHours = 24
|
||||||
}
|
}
|
||||||
ids, err := uc.StaleUploadSessionIDs(ctx, time.Now().Add(-time.Duration(ttlHours)*time.Hour))
|
// The reference cleanup job is best-effort: a stale-session query or an
|
||||||
if err != nil {
|
// individual storage/database cleanup failure is not propagated to the
|
||||||
return err
|
// scheduler. Keep the endpoint-independent background behavior compatible.
|
||||||
}
|
ids, _ := uc.StaleUploadSessionIDs(ctx, time.Now().Add(-time.Duration(ttlHours)*time.Hour))
|
||||||
for _, id := range ids {
|
for _, id := range ids {
|
||||||
if err = uc.DeleteUploadData(ctx, id); err != nil {
|
_ = uc.DeleteUploadData(ctx, id)
|
||||||
return err
|
_ = uc.files.DeletePrefix(ctx, uc.chunkPrefix(id))
|
||||||
}
|
|
||||||
if err = uc.files.DeletePrefix(ctx, uc.chunkPrefix(id)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ type SecurityConfig struct {
|
||||||
type SecurityRepo interface {
|
type SecurityRepo interface {
|
||||||
SecurityConfig(context.Context) (*SecurityConfig, error)
|
SecurityConfig(context.Context) (*SecurityConfig, error)
|
||||||
SaveSecurityConfig(context.Context, *SecurityConfig) error
|
SaveSecurityConfig(context.Context, *SecurityConfig) error
|
||||||
|
BackfillPasswordUpdatedAt(context.Context, time.Time) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type SecurityUsecase struct {
|
type SecurityUsecase struct {
|
||||||
|
|
@ -63,6 +64,13 @@ func NewSecurityUsecase(repo SecurityRepo, cache Cache, settings RuntimeSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
func (uc *SecurityUsecase) UpdateSecurity(ctx context.Context, value *SecurityConfig) error {
|
func (uc *SecurityUsecase) UpdateSecurity(ctx context.Context, value *SecurityConfig) error {
|
||||||
|
previous, err := uc.repo.SecurityConfig(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
value.ID = previous.ID
|
||||||
|
value.CreatedAt = previous.CreatedAt
|
||||||
|
value.UpdatedAt = previous.UpdatedAt
|
||||||
if err := uc.repo.SaveSecurityConfig(ctx, value); err != nil {
|
if err := uc.repo.SaveSecurityConfig(ctx, value); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -70,6 +78,14 @@ func (uc *SecurityUsecase) UpdateSecurity(ctx context.Context, value *SecurityCo
|
||||||
copy := *value
|
copy := *value
|
||||||
uc.cachedConfig = ©
|
uc.cachedConfig = ©
|
||||||
uc.mu.Unlock()
|
uc.mu.Unlock()
|
||||||
|
// Keep the same observable ordering as the administration backend:
|
||||||
|
// persist and activate the new configuration first, then backfill legacy
|
||||||
|
// users when password expiration changes from disabled to enabled. A
|
||||||
|
// backfill failure is returned to the caller without rolling back the
|
||||||
|
// already effective configuration.
|
||||||
|
if value.PwdExpireEnable && !previous.PwdExpireEnable {
|
||||||
|
return uc.repo.BackfillPasswordUpdatedAt(ctx, time.Now())
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -83,6 +99,13 @@ func (uc *SecurityUsecase) Current(ctx context.Context) (*SecurityConfig, error)
|
||||||
uc.mu.RUnlock()
|
uc.mu.RUnlock()
|
||||||
value, err := uc.repo.SecurityConfig(ctx)
|
value, err := uc.repo.SecurityConfig(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
// GVA returns its default value together with the database-not-ready
|
||||||
|
// error. Callers such as login/runtime policy consumers intentionally
|
||||||
|
// ignore the error and continue with that default, while the HTTP
|
||||||
|
// settings endpoint still reports the failure.
|
||||||
|
if value != nil {
|
||||||
|
return value, err
|
||||||
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
uc.mu.Lock()
|
uc.mu.Lock()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
package biz
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type securityUpdateRepo struct {
|
||||||
|
current *SecurityConfig
|
||||||
|
backfillErr error
|
||||||
|
callOrder []string
|
||||||
|
backfillAt time.Time
|
||||||
|
persistedCopy *SecurityConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *securityUpdateRepo) SecurityConfig(context.Context) (*SecurityConfig, error) {
|
||||||
|
r.callOrder = append(r.callOrder, "get")
|
||||||
|
copy := *r.current
|
||||||
|
return ©, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *securityUpdateRepo) SaveSecurityConfig(_ context.Context, value *SecurityConfig) error {
|
||||||
|
r.callOrder = append(r.callOrder, "save")
|
||||||
|
value.UpdatedAt = value.UpdatedAt.Add(time.Second)
|
||||||
|
copy := *value
|
||||||
|
r.persistedCopy = ©
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *securityUpdateRepo) BackfillPasswordUpdatedAt(_ context.Context, at time.Time) error {
|
||||||
|
r.callOrder = append(r.callOrder, "backfill")
|
||||||
|
r.backfillAt = at
|
||||||
|
return r.backfillErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateSecurityKeepsSavedConfigWhenPasswordBackfillFails(t *testing.T) {
|
||||||
|
createdAt := time.Date(2026, time.August, 16, 12, 0, 0, 0, time.Local)
|
||||||
|
backfillErr := errors.New("backfill failed")
|
||||||
|
repo := &securityUpdateRepo{
|
||||||
|
current: &SecurityConfig{ID: 1, CreatedAt: createdAt, UpdatedAt: createdAt, PwdExpireEnable: false},
|
||||||
|
backfillErr: backfillErr,
|
||||||
|
}
|
||||||
|
uc := NewSecurityUsecase(repo, nil, nil, nil)
|
||||||
|
next := &SecurityConfig{ID: 99, PwdExpireEnable: true, PwdExpireDays: 30}
|
||||||
|
|
||||||
|
err := uc.UpdateSecurity(context.Background(), next)
|
||||||
|
if !errors.Is(err, backfillErr) {
|
||||||
|
t.Fatalf("UpdateSecurity() error = %v, want %v", err, backfillErr)
|
||||||
|
}
|
||||||
|
if got := repo.callOrder; len(got) != 3 || got[0] != "get" || got[1] != "save" || got[2] != "backfill" {
|
||||||
|
t.Fatalf("call order = %v, want [get save backfill]", got)
|
||||||
|
}
|
||||||
|
if repo.persistedCopy == nil || repo.persistedCopy.ID != 1 || !repo.persistedCopy.PwdExpireEnable {
|
||||||
|
t.Fatalf("persisted config = %+v", repo.persistedCopy)
|
||||||
|
}
|
||||||
|
if repo.backfillAt.IsZero() {
|
||||||
|
t.Fatal("password timestamp backfill was not attempted")
|
||||||
|
}
|
||||||
|
cached, currentErr := uc.Current(context.Background())
|
||||||
|
if currentErr != nil {
|
||||||
|
t.Fatalf("Current() error = %v", currentErr)
|
||||||
|
}
|
||||||
|
if !cached.PwdExpireEnable || cached.ID != 1 {
|
||||||
|
t.Fatalf("effective config = %+v, want newly saved config", cached)
|
||||||
|
}
|
||||||
|
if got := repo.callOrder; len(got) != 3 {
|
||||||
|
t.Fatalf("Current() unexpectedly reloaded repository: %v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateSecurityDoesNotBackfillWithoutDisabledToEnabledTransition(t *testing.T) {
|
||||||
|
repo := &securityUpdateRepo{current: &SecurityConfig{ID: 1, PwdExpireEnable: true}}
|
||||||
|
uc := NewSecurityUsecase(repo, nil, nil, nil)
|
||||||
|
|
||||||
|
if err := uc.UpdateSecurity(context.Background(), &SecurityConfig{PwdExpireEnable: true}); err != nil {
|
||||||
|
t.Fatalf("UpdateSecurity() error = %v", err)
|
||||||
|
}
|
||||||
|
if got := repo.callOrder; len(got) != 2 || got[0] != "get" || got[1] != "save" {
|
||||||
|
t.Fatalf("call order = %v, want [get save]", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,6 +14,7 @@ import (
|
||||||
"google.golang.org/protobuf/encoding/protojson"
|
"google.golang.org/protobuf/encoding/protojson"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
func protoMap(message proto.Message) (map[string]any, error) {
|
func protoMap(message proto.Message) (map[string]any, error) {
|
||||||
|
|
@ -260,10 +261,19 @@ func (d *Data) reloadConfig(ctx context.Context) error {
|
||||||
return fmt.Errorf("data.database and admin configuration are required")
|
return fmt.Errorf("data.database and admin configuration are required")
|
||||||
}
|
}
|
||||||
next.Admin.ConfigPath = configPath
|
next.Admin.ConfigPath = configPath
|
||||||
candidateDB, err := openDatabase(next.Data.Database, false, "", d.logger())
|
databaseReady := databaseConnectionConfigured(next.Data.Database)
|
||||||
|
var candidateDB *gorm.DB
|
||||||
|
if databaseReady {
|
||||||
|
candidateDB, err = openDatabase(next.Data.Database, false, "", d.logger())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("reload database: %w", err)
|
return fmt.Errorf("reload database: %w", err)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
candidateDB, err = openFallbackDatabase(d.logger())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reload bootstrap database: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
closeCandidate := true
|
closeCandidate := true
|
||||||
defer func() {
|
defer func() {
|
||||||
if closeCandidate {
|
if closeCandidate {
|
||||||
|
|
@ -272,12 +282,14 @@ func (d *Data) reloadConfig(ctx context.Context) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
if databaseReady {
|
||||||
if sqlDB, dbErr := candidateDB.DB(); dbErr != nil {
|
if sqlDB, dbErr := candidateDB.DB(); dbErr != nil {
|
||||||
return dbErr
|
return dbErr
|
||||||
} else if err = sqlDB.PingContext(ctx); err != nil {
|
} else if err = sqlDB.PingContext(ctx); err != nil {
|
||||||
return fmt.Errorf("reload database: %w", err)
|
return fmt.Errorf("reload database: %w", err)
|
||||||
}
|
}
|
||||||
if next.Admin.System == nil || !next.Admin.System.DisableAutoMigrate {
|
}
|
||||||
|
if databaseReady && (next.Admin.System == nil || !next.Admin.System.DisableAutoMigrate) {
|
||||||
if err = migrateAll(candidateDB.WithContext(ctx)); err != nil {
|
if err = migrateAll(candidateDB.WithContext(ctx)); err != nil {
|
||||||
return fmt.Errorf("reload database migrations: %w", err)
|
return fmt.Errorf("reload database migrations: %w", err)
|
||||||
}
|
}
|
||||||
|
|
@ -305,6 +317,7 @@ func (d *Data) reloadConfig(ctx context.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
d.gormDB.replace(candidateDB, d.enqueueDataScopeAudit)
|
d.gormDB.replace(candidateDB, d.enqueueDataScopeAudit)
|
||||||
|
d.databaseReady.Store(databaseReady)
|
||||||
for _, item := range candidateDBList {
|
for _, item := range candidateDBList {
|
||||||
registerDataScopeCallbacks(item, d.enqueueDataScopeAudit)
|
registerDataScopeCallbacks(item, d.enqueueDataScopeAudit)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/wire"
|
"github.com/google/wire"
|
||||||
|
|
@ -18,6 +19,7 @@ var ProviderSet = wire.NewSet(NewData, NewRuntimeSettings, NewTokenIssuer, NewUs
|
||||||
type Data struct {
|
type Data struct {
|
||||||
initMu sync.Mutex
|
initMu sync.Mutex
|
||||||
configMu sync.Mutex
|
configMu sync.Mutex
|
||||||
|
databaseReady atomic.Bool
|
||||||
gormDB *reloadableDB
|
gormDB *reloadableDB
|
||||||
redis *reloadableRedis
|
redis *reloadableRedis
|
||||||
mongo *reloadableMongo
|
mongo *reloadableMongo
|
||||||
|
|
@ -90,22 +92,35 @@ func NewData(runtime *conf.Runtime, appLogger *slog.Logger) (*Data, func(), erro
|
||||||
appLogger = slog.Default()
|
appLogger = slog.Default()
|
||||||
}
|
}
|
||||||
c := runtime.Data()
|
c := runtime.Data()
|
||||||
if c == nil || c.Database == nil {
|
if c == nil {
|
||||||
return nil, nil, fmt.Errorf("database configuration is required")
|
c = &conf.Data{}
|
||||||
|
}
|
||||||
|
if c.Database == nil {
|
||||||
|
// An empty database block is the Kratos equivalent of GVA's empty
|
||||||
|
// Dbname: the service starts on the bootstrap database so /init/checkdb
|
||||||
|
// and /init/initdb remain available.
|
||||||
|
c.Database = &conf.Data_Database{}
|
||||||
}
|
}
|
||||||
d := &Data{runtime: runtime, appLogger: appLogger}
|
d := &Data{runtime: runtime, appLogger: appLogger}
|
||||||
db, err := openDatabase(c.Database, false, "", appLogger)
|
usingFallback := !databaseConnectionConfigured(c.Database)
|
||||||
usingFallback := false
|
var db *gorm.DB
|
||||||
if err != nil {
|
var err error
|
||||||
|
if !usingFallback {
|
||||||
|
db, err = openDatabase(c.Database, false, "", appLogger)
|
||||||
|
}
|
||||||
|
if usingFallback || err != nil {
|
||||||
// The initialization endpoint must remain available when the configured
|
// The initialization endpoint must remain available when the configured
|
||||||
// target database has not been created yet.
|
// target database has not been created yet.
|
||||||
|
if err != nil {
|
||||||
appLogger.Warn("configured database unavailable before initialization", "mod", "system", "error", err)
|
appLogger.Warn("configured database unavailable before initialization", "mod", "system", "error", err)
|
||||||
|
}
|
||||||
db, err = openFallbackDatabase(appLogger)
|
db, err = openFallbackDatabase(appLogger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("open bootstrap database: %w", err)
|
return nil, nil, fmt.Errorf("open bootstrap database: %w", err)
|
||||||
}
|
}
|
||||||
usingFallback = true
|
usingFallback = true
|
||||||
}
|
}
|
||||||
|
d.databaseReady.Store(!usingFallback)
|
||||||
d.gormDB = newReloadableDB(db, d.enqueueDataScopeAudit)
|
d.gormDB = newReloadableDB(db, d.enqueueDataScopeAudit)
|
||||||
d.auditLog = newDataScopeAuditWriter(d, appLogger)
|
d.auditLog = newDataScopeAuditWriter(d, appLogger)
|
||||||
d.dbList, err = openDatabaseList(c.DatabaseList, appLogger)
|
d.dbList, err = openDatabaseList(c.DatabaseList, appLogger)
|
||||||
|
|
@ -183,4 +198,5 @@ func openRedis(config *conf.Data_Redis, enabled bool, appLogger ...*slog.Logger)
|
||||||
func (d *Data) activateDatabase(db *gorm.DB, config *conf.Data_Database) {
|
func (d *Data) activateDatabase(db *gorm.DB, config *conf.Data_Database) {
|
||||||
d.gormDB.replace(db, d.enqueueDataScopeAudit)
|
d.gormDB.replace(db, d.enqueueDataScopeAudit)
|
||||||
d.runtime.UpdateDatabase(config)
|
d.runtime.UpdateDatabase(config)
|
||||||
|
d.databaseReady.Store(true)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,20 @@ import (
|
||||||
|
|
||||||
var databaseNamePattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_$-]*$`)
|
var databaseNamePattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_$-]*$`)
|
||||||
|
|
||||||
|
// databaseConnectionConfigured mirrors the initialization contract of the
|
||||||
|
// administration backend: an explicitly selected database (or a standalone
|
||||||
|
// DSN) means a real database connection is expected to exist. The internal
|
||||||
|
// bootstrap database is deliberately excluded from this state.
|
||||||
|
func databaseConnectionConfigured(c *conf.Data_Database) bool {
|
||||||
|
if c == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(c.Name) != "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(c.Source) != "" && strings.TrimSpace(c.Host) == "" && strings.TrimSpace(c.Path) == ""
|
||||||
|
}
|
||||||
|
|
||||||
func normalizedDriver(driver string) string {
|
func normalizedDriver(driver string) string {
|
||||||
switch strings.ToLower(strings.TrimSpace(driver)) {
|
switch strings.ToLower(strings.TrimSpace(driver)) {
|
||||||
case "postgres", "postgresql", "pgsql":
|
case "postgres", "postgresql", "pgsql":
|
||||||
|
|
|
||||||
|
|
@ -79,13 +79,6 @@ func (r *emailRepo) Send(ctx context.Context, to []string, subject, body string)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer client.Close()
|
defer client.Close()
|
||||||
if !config.IsSsl {
|
|
||||||
if supported, _ := client.Extension("STARTTLS"); supported {
|
|
||||||
if err = client.StartTLS(&tls.Config{ServerName: config.Host, MinVersion: tls.VersionTLS12}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var auth smtp.Auth
|
var auth smtp.Auth
|
||||||
if config.IsLoginAuth {
|
if config.IsLoginAuth {
|
||||||
auth = &loginAuth{username: config.From, password: config.Secret}
|
auth = &loginAuth{username: config.From, password: config.Secret}
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,12 @@ type errorRecordPO struct {
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||||
Form string `gorm:"type:text"`
|
Form *string `gorm:"type:text"`
|
||||||
Info string `gorm:"type:text"`
|
Info *string `gorm:"type:text"`
|
||||||
Level string
|
Level string
|
||||||
RequestID string `gorm:"index"`
|
RequestID string `gorm:"index"`
|
||||||
TraceID string `gorm:"index"`
|
TraceID string `gorm:"index"`
|
||||||
Solution string `gorm:"type:text"`
|
Solution *string `gorm:"type:text"`
|
||||||
Status string `gorm:"default:未处理"`
|
Status string `gorm:"default:未处理"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -29,23 +29,39 @@ func errorFromPO(v errorRecordPO) *biz.ErrorRecord {
|
||||||
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}
|
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 {
|
func (r *auditRecorderRepo) CreateError(ctx context.Context, v *biz.ErrorRecord) error {
|
||||||
|
if !r.data.databaseReady.Load() {
|
||||||
|
// GVA silently ignores error records before GVA_DB is initialized.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if v.Status == "" {
|
if v.Status == "" {
|
||||||
v.Status = "未处理"
|
v.Status = "未处理"
|
||||||
}
|
}
|
||||||
return r.data.gormDB.WithContext(ctx).Create(&errorRecordPO{Form: v.Form, Info: v.Info, Level: v.Level, RequestID: v.RequestID, TraceID: v.TraceID, Solution: v.Solution, Status: v.Status}).Error
|
return r.data.gormDB.WithContext(ctx).Create(&errorRecordPO{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}).Error
|
||||||
}
|
}
|
||||||
func (r *auditQueryRepo) UpdateError(ctx context.Context, v *biz.ErrorRecord) error {
|
func (r *auditQueryRepo) UpdateError(ctx context.Context, v *biz.ErrorRecord) error {
|
||||||
updates := make(map[string]any, 5)
|
updates := make(map[string]any, 9)
|
||||||
if v.Form != "" {
|
if v.ID != 0 {
|
||||||
|
updates["id"] = v.ID
|
||||||
|
}
|
||||||
|
if !v.CreatedAt.IsZero() {
|
||||||
|
updates["created_at"] = v.CreatedAt
|
||||||
|
}
|
||||||
|
if v.Form != nil {
|
||||||
updates["form"] = v.Form
|
updates["form"] = v.Form
|
||||||
}
|
}
|
||||||
if v.Info != "" {
|
if v.Info != nil {
|
||||||
updates["info"] = v.Info
|
updates["info"] = v.Info
|
||||||
}
|
}
|
||||||
if v.Level != "" {
|
if v.Level != "" {
|
||||||
updates["level"] = v.Level
|
updates["level"] = v.Level
|
||||||
}
|
}
|
||||||
if v.Solution != "" {
|
if v.RequestID != "" {
|
||||||
|
updates["request_id"] = v.RequestID
|
||||||
|
}
|
||||||
|
if v.TraceID != "" {
|
||||||
|
updates["trace_id"] = v.TraceID
|
||||||
|
}
|
||||||
|
if v.Solution != nil {
|
||||||
updates["solution"] = v.Solution
|
updates["solution"] = v.Solution
|
||||||
}
|
}
|
||||||
if v.Status != "" {
|
if v.Status != "" {
|
||||||
|
|
@ -69,11 +85,11 @@ func (r *auditQueryRepo) ListErrors(ctx context.Context, page, size int, q *biz.
|
||||||
if len(q.CreatedAtRange) == 2 {
|
if len(q.CreatedAtRange) == 2 {
|
||||||
db = db.Where("created_at BETWEEN ? AND ?", q.CreatedAtRange[0], q.CreatedAtRange[1])
|
db = db.Where("created_at BETWEEN ? AND ?", q.CreatedAtRange[0], q.CreatedAtRange[1])
|
||||||
}
|
}
|
||||||
if q.Form != "" {
|
if q.Form != nil && *q.Form != "" {
|
||||||
db = db.Where("form = ?", q.Form)
|
db = db.Where("form = ?", *q.Form)
|
||||||
}
|
}
|
||||||
if q.Info != "" {
|
if q.Info != nil && *q.Info != "" {
|
||||||
db = db.Where("info LIKE ?", "%"+q.Info+"%")
|
db = db.Where("info LIKE ?", "%"+*q.Info+"%")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var total int64
|
var total int64
|
||||||
|
|
|
||||||
|
|
@ -86,11 +86,7 @@ func (r *exportRepo) saveRelations(tx *gorm.DB, v *biz.ExportTemplate, resetIDs
|
||||||
if resetIDs {
|
if resetIDs {
|
||||||
id = 0
|
id = 0
|
||||||
}
|
}
|
||||||
templateID := x.TemplateID
|
conditions = append(conditions, exportConditionPO{ID: id, CreatedAt: x.CreatedAt, UpdatedAt: x.UpdatedAt, TemplateID: x.TemplateID, From: x.From, Column: x.Column, Operator: x.Operator})
|
||||||
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 len(conditions) > 0 {
|
||||||
create := tx
|
create := tx
|
||||||
|
|
@ -109,11 +105,7 @@ func (r *exportRepo) saveRelations(tx *gorm.DB, v *biz.ExportTemplate, resetIDs
|
||||||
if resetIDs {
|
if resetIDs {
|
||||||
id = 0
|
id = 0
|
||||||
}
|
}
|
||||||
templateID := x.TemplateID
|
joins = append(joins, exportJoinPO{ID: id, CreatedAt: x.CreatedAt, UpdatedAt: x.UpdatedAt, TemplateID: x.TemplateID, Join: x.Join, Table: x.Table, On: x.On})
|
||||||
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 {
|
if len(joins) > 0 {
|
||||||
create := tx
|
create := tx
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,11 @@ type loginLogPO struct {
|
||||||
func (loginLogPO) TableName() string { return "sys_login_logs" }
|
func (loginLogPO) TableName() string { return "sys_login_logs" }
|
||||||
|
|
||||||
func (r *auditRecorderRepo) RecordLogin(ctx context.Context, v *biz.LoginLog) error {
|
func (r *auditRecorderRepo) RecordLogin(ctx context.Context, v *biz.LoginLog) error {
|
||||||
|
if !r.data.databaseReady.Load() {
|
||||||
|
// The login endpoint remains reachable before database initialization;
|
||||||
|
// GVA skips this audit write while GVA_DB is nil.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
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
|
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 {
|
func loginFromPO(v loginLogPO) *biz.LoginLog {
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,10 @@ func securityToPO(v *biz.SecurityConfig) securityConfigPO {
|
||||||
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}
|
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) {
|
func (r *securityRepo) SecurityConfig(ctx context.Context) (*biz.SecurityConfig, error) {
|
||||||
|
if !r.data.databaseReady.Load() {
|
||||||
|
po := defaultSecurityConfig()
|
||||||
|
return securityFromPO(po), errors.New("数据库未初始化")
|
||||||
|
}
|
||||||
var po securityConfigPO
|
var po securityConfigPO
|
||||||
err := r.data.gormDB.WithContext(ctx).First(&po, 1).Error
|
err := r.data.gormDB.WithContext(ctx).First(&po, 1).Error
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
|
@ -64,20 +68,16 @@ func (r *securityRepo) SecurityConfig(ctx context.Context) (*biz.SecurityConfig,
|
||||||
return securityFromPO(po), nil
|
return securityFromPO(po), nil
|
||||||
}
|
}
|
||||||
func (r *securityRepo) SaveSecurityConfig(ctx context.Context, v *biz.SecurityConfig) error {
|
func (r *securityRepo) SaveSecurityConfig(ctx context.Context, v *biz.SecurityConfig) error {
|
||||||
previous, err := r.SecurityConfig(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
po := securityToPO(v)
|
po := securityToPO(v)
|
||||||
po.CreatedAt, po.UpdatedAt = previous.CreatedAt, previous.UpdatedAt
|
if err := r.data.gormDB.WithContext(ctx).Save(&po).Error; err != nil {
|
||||||
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
||||||
if err := tx.Save(&po).Error; err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
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
|
v.ID, v.CreatedAt, v.UpdatedAt = po.ID, po.CreatedAt, po.UpdatedAt
|
||||||
return nil
|
return nil
|
||||||
})
|
}
|
||||||
|
|
||||||
|
func (r *securityRepo) BackfillPasswordUpdatedAt(ctx context.Context, at time.Time) error {
|
||||||
|
return r.data.gormDB.WithContext(ctx).Model(&userPO{}).
|
||||||
|
Where("password_updated_at IS NULL").
|
||||||
|
Update("password_updated_at", at).Error
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,15 +56,7 @@ func (r *initializationRepo) ReloadConfig(ctx context.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *initializationRepo) IsInitialized(ctx context.Context) (bool, error) {
|
func (r *initializationRepo) IsInitialized(ctx context.Context) (bool, error) {
|
||||||
db := r.data.gormDB.WithContext(ctx)
|
return r.data.databaseReady.Load(), nil
|
||||||
if !db.Migrator().HasTable(&userPO{}) {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
var count int64
|
|
||||||
if err := db.Model(&userPO{}).Where("username = ?", "admin").Count(&count).Error; err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
return count > 0, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *initializationRepo) Initialize(ctx context.Context, input *biz.DatabaseConfig) error {
|
func (r *initializationRepo) Initialize(ctx context.Context, input *biz.DatabaseConfig) error {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
package data
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"kra/internal/conf"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDatabaseConnectionConfigured(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
config *conf.Data_Database
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{name: "missing config", config: nil, want: false},
|
||||||
|
{name: "empty database", config: &conf.Data_Database{Driver: "mysql"}, want: false},
|
||||||
|
{name: "named database", config: &conf.Data_Database{Driver: "mysql", Name: "kra"}, want: true},
|
||||||
|
{name: "standalone DSN", config: &conf.Data_Database{Driver: "sqlite", Source: "file:kra.db"}, want: true},
|
||||||
|
{name: "generated DSN still requires a name", config: &conf.Data_Database{Driver: "mysql", Source: "root@tcp(localhost)/", Host: "localhost"}, want: false},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
if got := databaseConnectionConfigured(test.config); got != test.want {
|
||||||
|
t.Fatalf("databaseConnectionConfigured() = %v, want %v", got, test.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInitializationStateTracksRealDatabaseConnection(t *testing.T) {
|
||||||
|
data := &Data{}
|
||||||
|
repo := &initializationRepo{data: data}
|
||||||
|
|
||||||
|
initialized, err := repo.IsInitialized(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("IsInitialized() error = %v", err)
|
||||||
|
}
|
||||||
|
if initialized {
|
||||||
|
t.Fatal("bootstrap state must require initialization")
|
||||||
|
}
|
||||||
|
|
||||||
|
data.databaseReady.Store(true)
|
||||||
|
initialized, err = repo.IsInitialized(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("IsInitialized() error = %v", err)
|
||||||
|
}
|
||||||
|
if !initialized {
|
||||||
|
t.Fatal("an active configured database must not require initialization")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -322,7 +322,7 @@ func (h *Audit) DeleteErrors(c *gin.Context) {
|
||||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "批量删除成功")
|
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "批量删除成功")
|
||||||
}
|
}
|
||||||
func (h *Audit) UpdateError(c *gin.Context) {
|
func (h *Audit) UpdateError(c *gin.Context) {
|
||||||
var req dto.ErrorRecordRequest
|
var req dto.ErrorRecordMutationRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
httpx.Fail(c, err.Error())
|
httpx.Fail(c, err.Error())
|
||||||
return
|
return
|
||||||
|
|
@ -361,12 +361,12 @@ func (h *Audit) Errors(c *gin.Context) {
|
||||||
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: p, PageSize: size}, "获取成功")
|
||||||
}
|
}
|
||||||
func (h *Audit) CreateError(c *gin.Context) {
|
func (h *Audit) CreateError(c *gin.Context) {
|
||||||
var req dto.ErrorRecordRequest
|
var req dto.ErrorRecordMutationRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
httpx.Fail(c, err.Error())
|
httpx.Fail(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := h.recorder.CreateErrorRequest(c.Request.Context(), &req); err != nil {
|
if err := h.recorder.CreateErrorMutationRequest(c.Request.Context(), &req); err != nil {
|
||||||
httpx.Fail(c, "创建失败:"+err.Error())
|
httpx.Fail(c, "创建失败:"+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,9 @@ func (rateLimitSecurityRepo) SecurityConfig(context.Context) (*biz.SecurityConfi
|
||||||
func (rateLimitSecurityRepo) SaveSecurityConfig(context.Context, *biz.SecurityConfig) error {
|
func (rateLimitSecurityRepo) SaveSecurityConfig(context.Context, *biz.SecurityConfig) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
func (rateLimitSecurityRepo) BackfillPasswordUpdatedAt(context.Context, time.Time) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type rateLimitCache struct{}
|
type rateLimitCache struct{}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,17 +14,36 @@ func errorDTO(v *biz.ErrorRecord) *dto.ErrorRecordResponse {
|
||||||
func (s *AuditRecorder) CreateError(ctx context.Context, v *biz.ErrorRecord) error {
|
func (s *AuditRecorder) CreateError(ctx context.Context, v *biz.ErrorRecord) error {
|
||||||
return s.uc.CreateError(ctx, v)
|
return s.uc.CreateError(ctx, v)
|
||||||
}
|
}
|
||||||
func errorDomain(value *dto.ErrorRecordRequest) *biz.ErrorRecord {
|
func recordedErrorDomain(value *dto.ErrorRecordRequest) *biz.ErrorRecord {
|
||||||
return &biz.ErrorRecord{ID: value.ID, Form: value.Form, Info: value.Info, Level: value.Level, RequestID: value.RequestID, TraceID: value.TraceID, Solution: value.Solution, Status: value.Status}
|
form, info := value.Form, value.Info
|
||||||
|
result := &biz.ErrorRecord{Form: &form, Info: &info, Level: value.Level, RequestID: value.RequestID, TraceID: value.TraceID, Status: value.Status}
|
||||||
|
if value.Solution != "" {
|
||||||
|
solution := value.Solution
|
||||||
|
result.Solution = &solution
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
func mutatedErrorDomain(value *dto.ErrorRecordMutationRequest) *biz.ErrorRecord {
|
||||||
|
return &biz.ErrorRecord{ID: value.ID, CreatedAt: value.CreatedAt, UpdatedAt: value.UpdatedAt, Form: value.Form, Info: value.Info, Level: value.Level, RequestID: value.RequestID, TraceID: value.TraceID, Solution: value.Solution, Status: value.Status}
|
||||||
}
|
}
|
||||||
func (s *AuditRecorder) CreateErrorRequest(ctx context.Context, req *dto.ErrorRecordRequest) error {
|
func (s *AuditRecorder) CreateErrorRequest(ctx context.Context, req *dto.ErrorRecordRequest) error {
|
||||||
return s.CreateError(ctx, errorDomain(req))
|
return s.CreateError(ctx, recordedErrorDomain(req))
|
||||||
}
|
}
|
||||||
func (s *AuditService) UpdateErrorRequest(ctx context.Context, req *dto.ErrorRecordRequest) error {
|
func (s *AuditRecorder) CreateErrorMutationRequest(ctx context.Context, req *dto.ErrorRecordMutationRequest) error {
|
||||||
return s.UpdateError(ctx, errorDomain(req))
|
return s.CreateError(ctx, mutatedErrorDomain(req))
|
||||||
|
}
|
||||||
|
func (s *AuditService) UpdateErrorRequest(ctx context.Context, req *dto.ErrorRecordMutationRequest) error {
|
||||||
|
return s.UpdateError(ctx, mutatedErrorDomain(req))
|
||||||
}
|
}
|
||||||
func (s *AuditService) ErrorsFilter(ctx context.Context, page, size int, form, info string, createdAtRange []time.Time) ([]*dto.ErrorRecordResponse, int64, error) {
|
func (s *AuditService) ErrorsFilter(ctx context.Context, page, size int, form, info string, createdAtRange []time.Time) ([]*dto.ErrorRecordResponse, int64, error) {
|
||||||
return s.Errors(ctx, page, size, &biz.ErrorRecord{Form: form, Info: info, CreatedAtRange: createdAtRange})
|
query := &biz.ErrorRecord{CreatedAtRange: createdAtRange}
|
||||||
|
if form != "" {
|
||||||
|
query.Form = &form
|
||||||
|
}
|
||||||
|
if info != "" {
|
||||||
|
query.Info = &info
|
||||||
|
}
|
||||||
|
return s.Errors(ctx, page, size, query)
|
||||||
}
|
}
|
||||||
func (s *AuditService) UpdateError(ctx context.Context, v *biz.ErrorRecord) error {
|
func (s *AuditService) UpdateError(ctx context.Context, v *biz.ErrorRecord) error {
|
||||||
return s.uc.UpdateError(ctx, v)
|
return s.uc.UpdateError(ctx, v)
|
||||||
|
|
|
||||||
|
|
@ -56,8 +56,7 @@ type AuditIDQuery struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ErrorRecordRequest struct {
|
type ErrorRecordRequest struct {
|
||||||
ID uint `json:"ID"`
|
Form string `json:"form"`
|
||||||
Form string `json:"form" binding:"required"`
|
|
||||||
Info string `json:"info"`
|
Info string `json:"info"`
|
||||||
Level string `json:"level"`
|
Level string `json:"level"`
|
||||||
RequestID string `json:"request_id"`
|
RequestID string `json:"request_id"`
|
||||||
|
|
@ -66,6 +65,22 @@ type ErrorRecordRequest struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ErrorRecordMutationRequest mirrors the public error-record model. Pointer
|
||||||
|
// fields preserve the distinction between an omitted value and an explicitly
|
||||||
|
// supplied empty string, which is required by the update contract.
|
||||||
|
type ErrorRecordMutationRequest struct {
|
||||||
|
ID uint `json:"ID"`
|
||||||
|
CreatedAt time.Time `json:"CreatedAt"`
|
||||||
|
UpdatedAt time.Time `json:"UpdatedAt"`
|
||||||
|
Form *string `json:"form" binding:"required"`
|
||||||
|
Info *string `json:"info"`
|
||||||
|
Level string `json:"level"`
|
||||||
|
RequestID string `json:"request_id"`
|
||||||
|
TraceID string `json:"trace_id"`
|
||||||
|
Solution *string `json:"solution"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
type OperationRecordResponse struct {
|
type OperationRecordResponse struct {
|
||||||
ID uint `json:"ID"`
|
ID uint `json:"ID"`
|
||||||
CreatedAt time.Time `json:"CreatedAt"`
|
CreatedAt time.Time `json:"CreatedAt"`
|
||||||
|
|
@ -123,12 +138,12 @@ type ErrorRecordResponse struct {
|
||||||
CreatedAt time.Time `json:"CreatedAt"`
|
CreatedAt time.Time `json:"CreatedAt"`
|
||||||
UpdatedAt time.Time `json:"UpdatedAt"`
|
UpdatedAt time.Time `json:"UpdatedAt"`
|
||||||
DeletedAt any `json:"-"`
|
DeletedAt any `json:"-"`
|
||||||
Form string `json:"form"`
|
Form *string `json:"form"`
|
||||||
Info string `json:"info"`
|
Info *string `json:"info"`
|
||||||
Level string `json:"level"`
|
Level string `json:"level"`
|
||||||
RequestID string `json:"request_id"`
|
RequestID string `json:"request_id"`
|
||||||
TraceID string `json:"trace_id"`
|
TraceID string `json:"trace_id"`
|
||||||
Solution string `json:"solution"`
|
Solution *string `json:"solution"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,17 +48,16 @@ func (s *TaskScheduler) Start(ctx context.Context) error {
|
||||||
s.seconds.Start()
|
s.seconds.Start()
|
||||||
items, _, err := s.tasks.ListTasks(ctx, 0, 0, nil)
|
items, _, err := s.tasks.ListTasks(ctx, 0, 0, nil)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
loaded := 0
|
|
||||||
for _, task := range items {
|
for _, task := range items {
|
||||||
if task.Enabled {
|
if task.Enabled {
|
||||||
if scheduleErr := s.Schedule(task); scheduleErr != nil {
|
if scheduleErr := s.Schedule(task); scheduleErr != nil {
|
||||||
s.logger.ErrorContext(ctx, "restore timed task failed", "id", task.ID, "error", scheduleErr)
|
s.logger.ErrorContext(ctx, "restore timed task failed", "id", task.ID, "error", scheduleErr)
|
||||||
} else {
|
|
||||||
loaded++
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s.logger.InfoContext(ctx, "定时任务加载完成", "task_count", loaded)
|
// GVA reports the number of rows read from the database, not only
|
||||||
|
// the enabled rows that were scheduled successfully.
|
||||||
|
s.logger.InfoContext(ctx, "定时任务加载完成", "task_count", len(items))
|
||||||
} else {
|
} else {
|
||||||
s.logger.WarnContext(ctx, "timed task table is not ready", "error", err)
|
s.logger.WarnContext(ctx, "timed task table is not ready", "error", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue