每日清理
This commit is contained in:
@@ -57,26 +57,9 @@ func (s *Service) ArchiveIntegrationDate(ctx context.Context, archiveDate time.T
|
||||
return s.archiveIntegrationDate(ctx, archiveDate, false)
|
||||
}
|
||||
|
||||
// FinalizePreviousIntegrationMonth 复核并终结上一个完整自然月的 Integration Log 归档。
|
||||
func (s *Service) FinalizePreviousIntegrationMonth(ctx context.Context) error {
|
||||
now := time.Now().In(s.location)
|
||||
return s.FinalizeIntegrationMonth(ctx, now.AddDate(0, -1, 0))
|
||||
}
|
||||
|
||||
// FinalizeIntegrationMonth 逐日复核指定完整自然月,并为变化内容创建最终 revision。
|
||||
func (s *Service) FinalizeIntegrationMonth(ctx context.Context, month time.Time) error {
|
||||
monthStart := time.Date(month.In(s.location).Year(), month.In(s.location).Month(), 1, 0, 0, 0, 0, s.location)
|
||||
currentMonth := time.Now().In(s.location)
|
||||
currentMonthStart := time.Date(currentMonth.Year(), currentMonth.Month(), 1, 0, 0, 0, 0, s.location)
|
||||
if !monthStart.Before(currentMonthStart) {
|
||||
return fmt.Errorf("只能终结已经结束的 Integration Log 完整自然月")
|
||||
}
|
||||
for date := monthStart; date.Before(monthStart.AddDate(0, 1, 0)); date = date.AddDate(0, 0, 1) {
|
||||
if err := s.archiveIntegrationDate(ctx, date, true); err != nil {
|
||||
return fmt.Errorf("终结 %s Integration Log 归档失败: %w", date.Format(time.DateOnly), err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
// FinalizeIntegrationDate 为指定已结束自然日形成 Integration Log 最终归档版本。
|
||||
func (s *Service) FinalizeIntegrationDate(ctx context.Context, archiveDate time.Time) error {
|
||||
return s.archiveIntegrationDate(ctx, archiveDate, true)
|
||||
}
|
||||
|
||||
func (s *Service) archiveIntegrationDate(ctx context.Context, archiveDate time.Time, final bool) error {
|
||||
@@ -105,8 +88,6 @@ func (s *Service) archiveIntegrationDate(ctx context.Context, archiveDate time.T
|
||||
return pendingErr
|
||||
}
|
||||
if pending > 0 {
|
||||
_ = s.db.WithContext(ctx).Model(&model.LogArchiveRun{}).Where("id = ?", run.ID).
|
||||
Updates(map[string]any{"is_final": false, "error_summary": "存在 pending Integration Log,无法形成最终归档", "updated_at": time.Now()}).Error
|
||||
return fmt.Errorf("仍有 %d 条 pending Integration Log,无法形成最终归档", pending)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
@@ -19,25 +18,18 @@ import (
|
||||
|
||||
const maxManifestBytes = 1024 * 1024
|
||||
|
||||
// RetentionAudit 描述月度物理清理的统一审计事实。
|
||||
// RetentionAudit 保留系统审计 Writer 的输入兼容类型。
|
||||
type RetentionAudit struct {
|
||||
EventID string
|
||||
Month string
|
||||
Summary string
|
||||
Result string
|
||||
ErrorSummary string
|
||||
RangeStart time.Time
|
||||
RangeEnd time.Time
|
||||
EventCount int64
|
||||
ResourceCount int64
|
||||
IntegrationCount int64
|
||||
ManifestKeys []string
|
||||
DurationMS int64
|
||||
EventID, Month, Summary, Result, ErrorSummary string
|
||||
RangeStart, RangeEnd time.Time
|
||||
EventCount, ResourceCount, IntegrationCount int64
|
||||
ManifestKeys []string
|
||||
DurationMS int64
|
||||
}
|
||||
|
||||
// RetentionResult 是月度留存清理的结构化执行结果。
|
||||
// RetentionResult 是单日留存处理结果。
|
||||
type RetentionResult struct {
|
||||
Month string
|
||||
ArchiveDate string
|
||||
EventCount int64
|
||||
ResourceCount int64
|
||||
IntegrationCount int64
|
||||
@@ -46,193 +38,143 @@ type RetentionResult struct {
|
||||
Duration time.Duration
|
||||
}
|
||||
|
||||
// RetentionBlockedError 描述阻断日期推进的安全上下文。
|
||||
type RetentionBlockedError struct {
|
||||
ArchiveDate string
|
||||
Source string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *RetentionBlockedError) Error() string {
|
||||
return e.ArchiveDate + " " + e.Source + ": " + e.Err.Error()
|
||||
}
|
||||
func (e *RetentionBlockedError) Unwrap() error { return e.Err }
|
||||
|
||||
type retentionRuns struct {
|
||||
audit []*model.LogArchiveRun
|
||||
integration []*model.LogArchiveRun
|
||||
audit *model.LogArchiveRun
|
||||
integration *model.LogArchiveRun
|
||||
}
|
||||
|
||||
// CleanupPreviousMonth 校验并物理清理上一个完整自然月的在线审计日志。
|
||||
func (s *Service) CleanupPreviousMonth(ctx context.Context) (RetentionResult, error) {
|
||||
now := time.Now().In(s.location)
|
||||
return s.CleanupMonth(ctx, now.AddDate(0, -1, 0))
|
||||
}
|
||||
|
||||
// ValidatePreviousMonth 只读校验上一个完整自然月的归档与清理门禁。
|
||||
func (s *Service) ValidatePreviousMonth(ctx context.Context) (RetentionResult, error) {
|
||||
now := time.Now().In(s.location)
|
||||
return s.ValidateMonth(ctx, now.AddDate(0, -1, 0))
|
||||
}
|
||||
|
||||
// ValidateMonth 只读校验指定完整自然月,不写清理断点且不删除在线数据。
|
||||
func (s *Service) ValidateMonth(ctx context.Context, month time.Time) (result RetentionResult, err error) {
|
||||
// RetainPendingDays 从最早仍在线的日期连续处理至昨天。cleanup 为 false 时只读校验。
|
||||
func (s *Service) RetainPendingDays(ctx context.Context, cleanup bool) ([]RetentionResult, error) {
|
||||
if s.db == nil || s.store == nil {
|
||||
return result, fmt.Errorf("日志留存演练数据库或对象存储未配置")
|
||||
return nil, fmt.Errorf("日志日留存数据库或对象存储未配置")
|
||||
}
|
||||
start, end, err := s.retentionMonthRange(month)
|
||||
start, end, err := s.pendingRetentionRange(ctx)
|
||||
if err != nil {
|
||||
return result, err
|
||||
return nil, err
|
||||
}
|
||||
results := make([]RetentionResult, 0)
|
||||
for date := start; date.Before(end); date = date.AddDate(0, 0, 1) {
|
||||
result, err := s.retainDate(ctx, date, cleanup)
|
||||
if err != nil {
|
||||
source := "retention"
|
||||
if strings.HasPrefix(err.Error(), "Audit") {
|
||||
source = constants.AuditArchiveSource
|
||||
}
|
||||
if strings.HasPrefix(err.Error(), "Integration") {
|
||||
source = constants.IntegrationArchiveSource
|
||||
}
|
||||
return results, &RetentionBlockedError{ArchiveDate: date.Format(time.DateOnly), Source: source, Err: err}
|
||||
}
|
||||
results = append(results, result)
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// RetainDate 校验并按需清理指定已结束自然日,供受控演练使用。
|
||||
func (s *Service) RetainDate(ctx context.Context, date time.Time, cleanup bool) (RetentionResult, error) {
|
||||
return s.retainDate(ctx, date, cleanup)
|
||||
}
|
||||
|
||||
func (s *Service) pendingRetentionRange(ctx context.Context) (time.Time, time.Time, error) {
|
||||
today := time.Now().In(s.location)
|
||||
end := time.Date(today.Year(), today.Month(), today.Day(), 0, 0, 0, 0, s.location)
|
||||
var earliest *time.Time
|
||||
for _, item := range []struct{ table, column string }{{"tb_audit_event", "created_at"}, {"tb_integration_log", "created_at"}} {
|
||||
var value *time.Time
|
||||
if err := s.db.WithContext(ctx).Table(item.table).Select("MIN(" + item.column + ")").Scan(&value).Error; err != nil {
|
||||
return time.Time{}, time.Time{}, fmt.Errorf("查询日留存起点失败: %w", err)
|
||||
}
|
||||
if value != nil && (earliest == nil || value.Before(*earliest)) {
|
||||
local := value.In(s.location)
|
||||
earliest = &local
|
||||
}
|
||||
}
|
||||
if earliest == nil {
|
||||
return end, end, nil
|
||||
}
|
||||
start := time.Date(earliest.Year(), earliest.Month(), earliest.Day(), 0, 0, 0, 0, s.location)
|
||||
return start, end, nil
|
||||
}
|
||||
|
||||
func (s *Service) retainDate(ctx context.Context, date time.Time, cleanup bool) (RetentionResult, error) {
|
||||
startedAt := time.Now()
|
||||
result.Month = start.Format("2006-01")
|
||||
runs, err := s.loadRetentionRuns(ctx, start, end)
|
||||
var existing []model.LogArchiveRun
|
||||
if err := s.db.WithContext(ctx).Where("source IN ? AND archive_date = ? AND instance_id = ?", []string{constants.AuditArchiveSource, constants.IntegrationArchiveSource}, date.Format(time.DateOnly), s.instanceID).Find(&existing).Error; err != nil {
|
||||
return RetentionResult{}, fmt.Errorf("读取日归档账本失败: %w", err)
|
||||
}
|
||||
statuses := map[string]string{}
|
||||
for _, run := range existing {
|
||||
statuses[run.Source] = run.Status
|
||||
}
|
||||
if statuses[constants.AuditArchiveSource] != constants.ArchiveStatusSuccess {
|
||||
if err := s.ArchiveDate(ctx, date); err != nil {
|
||||
return RetentionResult{}, fmt.Errorf("Audit 归档失败: %w", err)
|
||||
}
|
||||
}
|
||||
if statuses[constants.IntegrationArchiveSource] != constants.ArchiveStatusSuccess {
|
||||
if err := s.ArchiveIntegrationDate(ctx, date); err != nil {
|
||||
return RetentionResult{}, fmt.Errorf("Integration Log 归档失败: %w", err)
|
||||
}
|
||||
}
|
||||
if err := s.FinalizeIntegrationDate(ctx, date); err != nil {
|
||||
return RetentionResult{}, fmt.Errorf("Integration Log 最终归档失败: %w", err)
|
||||
}
|
||||
runs, err := s.loadRetentionRuns(ctx, date)
|
||||
if err != nil {
|
||||
return result, err
|
||||
return RetentionResult{}, err
|
||||
}
|
||||
if err := s.validateRetentionRuns(ctx, start, end, runs); err != nil {
|
||||
return result, err
|
||||
if err := s.validateAuditRetentionDay(ctx, date, runs.audit); err != nil {
|
||||
return RetentionResult{}, fmt.Errorf("Audit 完整性校验失败: %w", err)
|
||||
}
|
||||
summarizeRetentionRuns(runs, &result)
|
||||
if err := s.validateIntegrationRetentionDay(ctx, date, runs.integration); err != nil {
|
||||
return RetentionResult{}, fmt.Errorf("Integration Log 完整性校验失败: %w", err)
|
||||
}
|
||||
result := RetentionResult{ArchiveDate: date.Format(time.DateOnly), EventCount: runs.audit.EventCount, ResourceCount: runs.audit.ResourceCount, IntegrationCount: runs.integration.RecordCount, ManifestKeys: []string{runs.audit.ManifestKey, runs.integration.ManifestKey}}
|
||||
result.EstimatedBatches = estimatedRetentionBatches(result)
|
||||
if cleanup {
|
||||
if err := s.cleanupAuditDate(ctx, date, runs.audit); err != nil {
|
||||
return result, err
|
||||
}
|
||||
if err := s.cleanupIntegrationDate(ctx, date, runs.integration); err != nil {
|
||||
return result, err
|
||||
}
|
||||
}
|
||||
result.Duration = time.Since(startedAt)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// CleanupMonth 校验归档硬门禁后按固定顺序物理清理指定完整自然月。
|
||||
func (s *Service) CleanupMonth(ctx context.Context, month time.Time) (result RetentionResult, cleanupErr error) {
|
||||
if s.db == nil || s.store == nil || s.audit == nil {
|
||||
return result, fmt.Errorf("日志留存清理数据库、对象存储或审计 Writer 未配置")
|
||||
}
|
||||
start, end, err := s.retentionMonthRange(month)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
startedAt := time.Now()
|
||||
result.Month = start.Format("2006-01")
|
||||
cleanupErr = s.executeRetention(ctx, start, end, &result)
|
||||
result.Duration = time.Since(startedAt)
|
||||
if auditErr := s.recordRetentionAudit(ctx, start, end, result, cleanupErr); auditErr != nil {
|
||||
if cleanupErr != nil {
|
||||
return result, fmt.Errorf("%w;记录留存清理失败审计失败: %v", cleanupErr, auditErr)
|
||||
}
|
||||
return result, fmt.Errorf("记录留存清理成功审计失败: %w", auditErr)
|
||||
}
|
||||
return result, cleanupErr
|
||||
}
|
||||
|
||||
func (s *Service) retentionMonthRange(month time.Time) (time.Time, time.Time, error) {
|
||||
start := time.Date(month.In(s.location).Year(), month.In(s.location).Month(), 1, 0, 0, 0, 0, s.location)
|
||||
end := start.AddDate(0, 1, 0)
|
||||
now := time.Now().In(s.location)
|
||||
currentMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, s.location)
|
||||
if !end.Before(currentMonth) && !end.Equal(currentMonth) {
|
||||
return time.Time{}, time.Time{}, fmt.Errorf("只能清理已经结束的完整自然月")
|
||||
}
|
||||
return start, end, nil
|
||||
}
|
||||
|
||||
func (s *Service) executeRetention(ctx context.Context, start, end time.Time, result *RetentionResult) error {
|
||||
started, err := s.retentionCleanupStarted(ctx, start, end)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !started {
|
||||
lastDay := end.AddDate(0, 0, -1)
|
||||
if err := s.ArchiveDate(ctx, lastDay); err != nil {
|
||||
return fmt.Errorf("完成上月最后一天 Audit 归档失败: %w", err)
|
||||
}
|
||||
if err := s.ArchiveIntegrationDate(ctx, lastDay); err != nil {
|
||||
return fmt.Errorf("完成上月最后一天 Integration Log 归档失败: %w", err)
|
||||
}
|
||||
if err := s.FinalizeIntegrationMonth(ctx, start); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
runs, err := s.loadRetentionRuns(ctx, start, end)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.validateRetentionRuns(ctx, start, end, runs); err != nil {
|
||||
return err
|
||||
}
|
||||
summarizeRetentionRuns(runs, result)
|
||||
if err := s.cleanupAuditMonth(ctx, start, end, runs.audit); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.cleanupIntegrationMonth(ctx, start, end, runs.integration)
|
||||
}
|
||||
|
||||
func (s *Service) retentionCleanupStarted(ctx context.Context, start, end time.Time) (bool, error) {
|
||||
var count int64
|
||||
err := s.db.WithContext(ctx).Model(&model.LogArchiveRun{}).
|
||||
Where("archive_date >= ? AND archive_date < ? AND instance_id = ? AND cleanup_started_at IS NOT NULL",
|
||||
start.Format(time.DateOnly), end.Format(time.DateOnly), s.instanceID).
|
||||
Count(&count).Error
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("读取月度清理断点失败: %w", err)
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func (s *Service) loadRetentionRuns(ctx context.Context, start, end time.Time) (retentionRuns, error) {
|
||||
func (s *Service) loadRetentionRuns(ctx context.Context, date time.Time) (retentionRuns, error) {
|
||||
var rows []model.LogArchiveRun
|
||||
err := s.db.WithContext(ctx).Where(
|
||||
"source IN ? AND archive_date >= ? AND archive_date < ? AND instance_id = ?",
|
||||
[]string{constants.AuditArchiveSource, constants.IntegrationArchiveSource}, start.Format(time.DateOnly), end.Format(time.DateOnly), s.instanceID,
|
||||
).Order("archive_date ASC, source ASC").Find(&rows).Error
|
||||
if err != nil {
|
||||
return retentionRuns{}, fmt.Errorf("读取月度归档账本失败: %w", err)
|
||||
if err := s.db.WithContext(ctx).Where("source IN ? AND archive_date = ? AND instance_id = ?", []string{constants.AuditArchiveSource, constants.IntegrationArchiveSource}, date.Format(time.DateOnly), s.instanceID).Find(&rows).Error; err != nil {
|
||||
return retentionRuns{}, fmt.Errorf("读取日归档账本失败: %w", err)
|
||||
}
|
||||
days := int(end.Sub(start).Hours() / 24)
|
||||
if len(rows) != days*2 {
|
||||
return retentionRuns{}, fmt.Errorf("月度归档账本缺日:期望 %d 条,实际 %d 条", days*2, len(rows))
|
||||
}
|
||||
runs := retentionRuns{audit: make([]*model.LogArchiveRun, 0, days), integration: make([]*model.LogArchiveRun, 0, days)}
|
||||
runs := retentionRuns{}
|
||||
for index := range rows {
|
||||
run := &rows[index]
|
||||
switch run.Source {
|
||||
switch rows[index].Source {
|
||||
case constants.AuditArchiveSource:
|
||||
runs.audit = append(runs.audit, run)
|
||||
runs.audit = &rows[index]
|
||||
case constants.IntegrationArchiveSource:
|
||||
runs.integration = append(runs.integration, run)
|
||||
runs.integration = &rows[index]
|
||||
}
|
||||
}
|
||||
if len(runs.audit) != days || len(runs.integration) != days {
|
||||
return retentionRuns{}, fmt.Errorf("月度 Audit 或 Integration 归档账本不完整")
|
||||
if runs.audit == nil || runs.integration == nil {
|
||||
return retentionRuns{}, fmt.Errorf("Audit 或 Integration Log 归档账本缺失")
|
||||
}
|
||||
return runs, nil
|
||||
}
|
||||
|
||||
func (s *Service) validateRetentionRuns(ctx context.Context, start, end time.Time, runs retentionRuns) error {
|
||||
if err := validateCleanupLedgerState(runs.audit); err != nil {
|
||||
return fmt.Errorf("Audit 清理断点非法: %w", err)
|
||||
}
|
||||
if err := validateCleanupLedgerState(runs.integration); err != nil {
|
||||
return fmt.Errorf("Integration 清理断点非法: %w", err)
|
||||
}
|
||||
for index := range runs.audit {
|
||||
date := start.AddDate(0, 0, index)
|
||||
if err := s.validateAuditRetentionDay(ctx, date, runs.audit[index]); err != nil {
|
||||
return fmt.Errorf("%s Audit 清理门禁失败: %w", date.Format(time.DateOnly), err)
|
||||
}
|
||||
if err := s.validateIntegrationRetentionDay(ctx, date, runs.integration[index]); err != nil {
|
||||
return fmt.Errorf("%s Integration 清理门禁失败: %w", date.Format(time.DateOnly), err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateCleanupLedgerState(runs []*model.LogArchiveRun) error {
|
||||
started, cleaned := 0, 0
|
||||
for _, run := range runs {
|
||||
if run.CleanupStartedAt != nil {
|
||||
started++
|
||||
}
|
||||
if run.CleanedAt != nil {
|
||||
cleaned++
|
||||
}
|
||||
}
|
||||
if started != 0 && started != len(runs) {
|
||||
return fmt.Errorf("清理开始断点不是整月原子状态")
|
||||
}
|
||||
if cleaned != 0 && cleaned != len(runs) {
|
||||
return fmt.Errorf("清理完成断点不是整月原子状态")
|
||||
}
|
||||
if cleaned > 0 && started == 0 {
|
||||
return fmt.Errorf("清理完成但缺少开始断点")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) validateAuditRetentionDay(ctx context.Context, date time.Time, run *model.LogArchiveRun) error {
|
||||
if err := validateRunBase(run, date, constants.AuditArchiveSchemaVersion, false); err != nil {
|
||||
return err
|
||||
@@ -258,25 +200,21 @@ func (s *Service) validateIntegrationRetentionDay(ctx context.Context, date time
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if run.CleanedAt != nil {
|
||||
if count != 0 {
|
||||
return fmt.Errorf("已标记清理完成但数据库仍有 %d 条记录", count)
|
||||
if run.CleanedAt != nil && count != 0 {
|
||||
return fmt.Errorf("已标记清理完成但数据库仍有 %d 条记录", count)
|
||||
}
|
||||
if run.CleanupStartedAt != nil && count > run.RecordCount {
|
||||
return fmt.Errorf("续跑窗口记录数超过最终归档数量")
|
||||
}
|
||||
if run.CleanupStartedAt == nil {
|
||||
file, err := s.buildIntegrationArchiveFile(ctx, run.RangeStart, run.RangeEnd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if run.CleanupStartedAt != nil {
|
||||
if count > run.RecordCount {
|
||||
return fmt.Errorf("续跑窗口记录数超过最终归档数量")
|
||||
defer os.Remove(file.path)
|
||||
if file.recordCount != run.RecordCount || file.sha256 != run.SHA256 {
|
||||
return fmt.Errorf("数据库当前 Integration 内容与最终 revision 不一致")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
file, err := s.buildIntegrationArchiveFile(ctx, run.RangeStart, run.RangeEnd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(file.path)
|
||||
if file.recordCount != run.RecordCount || file.sha256 != run.SHA256 {
|
||||
return fmt.Errorf("数据库当前 Integration 内容与最终 revision 不一致")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -285,8 +223,7 @@ func validateRunBase(run *model.LogArchiveRun, date time.Time, schema string, fi
|
||||
if run.Status != constants.ArchiveStatusSuccess || run.SchemaVersion != schema {
|
||||
return fmt.Errorf("归档状态或 schema version 不符合清理要求")
|
||||
}
|
||||
if run.ArchiveDate.Format(time.DateOnly) != date.Format(time.DateOnly) ||
|
||||
!run.RangeStart.Equal(date) || !run.RangeEnd.Equal(date.AddDate(0, 0, 1)) {
|
||||
if run.ArchiveDate.Format(time.DateOnly) != date.Format(time.DateOnly) || !run.RangeStart.Equal(date) || !run.RangeEnd.Equal(date.AddDate(0, 0, 1)) {
|
||||
return fmt.Errorf("归档日期或半开时间范围不一致")
|
||||
}
|
||||
if final && !run.IsFinal {
|
||||
@@ -297,21 +234,14 @@ func validateRunBase(run *model.LogArchiveRun, date time.Time, schema string, fi
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateRemainingCounts(run *model.LogArchiveRun, events, resources int64) error {
|
||||
if run.CleanedAt != nil {
|
||||
if events != 0 || resources != 0 {
|
||||
return fmt.Errorf("已标记清理完成但数据库仍有事件或资源")
|
||||
}
|
||||
return nil
|
||||
if run.CleanedAt != nil && (events != 0 || resources != 0) {
|
||||
return fmt.Errorf("已标记清理完成但数据库仍有事件或资源")
|
||||
}
|
||||
if run.CleanupStartedAt != nil {
|
||||
if events > run.EventCount || resources > run.ResourceCount {
|
||||
return fmt.Errorf("续跑窗口数量超过已归档数量")
|
||||
}
|
||||
return nil
|
||||
if run.CleanupStartedAt != nil && (events > run.EventCount || resources > run.ResourceCount) {
|
||||
return fmt.Errorf("续跑窗口数量超过已归档数量")
|
||||
}
|
||||
if events != run.EventCount || resources != run.ResourceCount {
|
||||
if run.CleanupStartedAt == nil && (events != run.EventCount || resources != run.ResourceCount) {
|
||||
return fmt.Errorf("数据库事件或资源数量与 manifest 不一致")
|
||||
}
|
||||
return nil
|
||||
@@ -322,43 +252,27 @@ func (s *Service) validateAuditManifest(ctx context.Context, run *model.LogArchi
|
||||
if err := s.readManifest(ctx, run.ManifestKey, &manifest); err != nil {
|
||||
return err
|
||||
}
|
||||
if manifest.Source != run.Source || manifest.SchemaVersion != run.SchemaVersion || manifest.Status != constants.ArchiveStatusSuccess ||
|
||||
manifest.ArchiveDate != run.ArchiveDate.Format(time.DateOnly) || manifest.Timezone != constants.AuditArchiveTimezone ||
|
||||
manifest.InstanceID != run.InstanceID || !manifest.RangeStart.Equal(run.RangeStart) || !manifest.RangeEnd.Equal(run.RangeEnd) ||
|
||||
manifest.EventCount != run.EventCount || manifest.ResourceCount != run.ResourceCount ||
|
||||
manifest.CompressedBytes != run.CompressedBytes || manifest.ObjectKey != run.ObjectKey ||
|
||||
manifest.SHA256 != run.SHA256 || manifest.Revision != run.Revision {
|
||||
if manifest.Source != run.Source || manifest.SchemaVersion != run.SchemaVersion || manifest.Status != constants.ArchiveStatusSuccess || manifest.ArchiveDate != run.ArchiveDate.Format(time.DateOnly) || manifest.Timezone != constants.AuditArchiveTimezone || manifest.InstanceID != run.InstanceID || !manifest.RangeStart.Equal(run.RangeStart) || !manifest.RangeEnd.Equal(run.RangeEnd) || manifest.EventCount != run.EventCount || manifest.ResourceCount != run.ResourceCount || manifest.CompressedBytes != run.CompressedBytes || manifest.ObjectKey != run.ObjectKey || manifest.SHA256 != run.SHA256 || manifest.Revision != run.Revision {
|
||||
return fmt.Errorf("Audit manifest 与 ledger 不一致")
|
||||
}
|
||||
if err := s.verifyObject(ctx, run.ManifestKey, -1, map[string]string{
|
||||
"source": constants.AuditArchiveSource, "data-sha256": run.SHA256, "revision": strconv.Itoa(run.Revision),
|
||||
}); err != nil {
|
||||
if err := s.verifyObject(ctx, run.ManifestKey, -1, map[string]string{"source": constants.AuditArchiveSource, "data-sha256": run.SHA256, "revision": strconv.Itoa(run.Revision)}); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.verifyRetentionObject(ctx, run, false)
|
||||
}
|
||||
|
||||
func (s *Service) validateIntegrationManifest(ctx context.Context, run *model.LogArchiveRun) error {
|
||||
var manifest integrationArchiveManifest
|
||||
if err := s.readManifest(ctx, run.ManifestKey, &manifest); err != nil {
|
||||
return err
|
||||
}
|
||||
if manifest.Source != run.Source || manifest.SchemaVersion != run.SchemaVersion || manifest.Status != constants.ArchiveStatusSuccess || !manifest.Final ||
|
||||
manifest.ArchiveDate != run.ArchiveDate.Format(time.DateOnly) || manifest.Timezone != constants.AuditArchiveTimezone ||
|
||||
manifest.InstanceID != run.InstanceID || !manifest.RangeStart.Equal(run.RangeStart) || !manifest.RangeEnd.Equal(run.RangeEnd) ||
|
||||
manifest.RecordCount != run.RecordCount || manifest.CompressedBytes != run.CompressedBytes ||
|
||||
manifest.ObjectKey != run.ObjectKey || manifest.SHA256 != run.SHA256 || manifest.Revision != run.Revision {
|
||||
if manifest.Source != run.Source || manifest.SchemaVersion != run.SchemaVersion || manifest.Status != constants.ArchiveStatusSuccess || !manifest.Final || manifest.ArchiveDate != run.ArchiveDate.Format(time.DateOnly) || manifest.Timezone != constants.AuditArchiveTimezone || manifest.InstanceID != run.InstanceID || !manifest.RangeStart.Equal(run.RangeStart) || !manifest.RangeEnd.Equal(run.RangeEnd) || manifest.RecordCount != run.RecordCount || manifest.CompressedBytes != run.CompressedBytes || manifest.ObjectKey != run.ObjectKey || manifest.SHA256 != run.SHA256 || manifest.Revision != run.Revision {
|
||||
return fmt.Errorf("Integration manifest 与最终 ledger 不一致")
|
||||
}
|
||||
if err := s.verifyObject(ctx, run.ManifestKey, -1, map[string]string{
|
||||
"source": constants.IntegrationArchiveSource, "data-sha256": run.SHA256,
|
||||
"revision": strconv.Itoa(run.Revision), "final": "true",
|
||||
}); err != nil {
|
||||
if err := s.verifyObject(ctx, run.ManifestKey, -1, map[string]string{"source": constants.IntegrationArchiveSource, "data-sha256": run.SHA256, "revision": strconv.Itoa(run.Revision), "final": "true"}); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.verifyRetentionObject(ctx, run, true)
|
||||
}
|
||||
|
||||
func (s *Service) readManifest(ctx context.Context, key string, target any) error {
|
||||
object, err := s.store.Stat(ctx, key)
|
||||
if err != nil {
|
||||
@@ -384,13 +298,8 @@ func (s *Service) readManifest(ctx context.Context, key string, target any) erro
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) verifyRetentionObject(ctx context.Context, run *model.LogArchiveRun, final bool) error {
|
||||
metadata := map[string]string{
|
||||
"schema-version": run.SchemaVersion, "source": run.Source,
|
||||
"archive-date": run.RangeStart.Format(time.DateOnly), "timezone": constants.AuditArchiveTimezone,
|
||||
"sha256": run.SHA256, "revision": strconv.Itoa(run.Revision),
|
||||
}
|
||||
metadata := map[string]string{"schema-version": run.SchemaVersion, "source": run.Source, "archive-date": run.RangeStart.Format(time.DateOnly), "timezone": constants.AuditArchiveTimezone, "sha256": run.SHA256, "revision": strconv.Itoa(run.Revision)}
|
||||
if run.Source == constants.AuditArchiveSource {
|
||||
metadata["event-count"] = strconv.FormatInt(run.EventCount, 10)
|
||||
metadata["resource-count"] = strconv.FormatInt(run.ResourceCount, 10)
|
||||
@@ -419,53 +328,35 @@ func (s *Service) verifyRetentionObject(ctx context.Context, run *model.LogArchi
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func summarizeRetentionRuns(runs retentionRuns, result *RetentionResult) {
|
||||
result.ManifestKeys = make([]string, 0, len(runs.audit)+len(runs.integration))
|
||||
for _, run := range runs.audit {
|
||||
result.EventCount += run.EventCount
|
||||
result.ResourceCount += run.ResourceCount
|
||||
result.ManifestKeys = append(result.ManifestKeys, run.ManifestKey)
|
||||
}
|
||||
for _, run := range runs.integration {
|
||||
result.IntegrationCount += run.RecordCount
|
||||
result.ManifestKeys = append(result.ManifestKeys, run.ManifestKey)
|
||||
}
|
||||
}
|
||||
|
||||
func estimatedRetentionBatches(result RetentionResult) int64 {
|
||||
batchSize := int64(constants.AuditRetentionDeleteBatchSize)
|
||||
return (result.EventCount+batchSize-1)/batchSize +
|
||||
(result.ResourceCount+batchSize-1)/batchSize +
|
||||
(result.IntegrationCount+batchSize-1)/batchSize
|
||||
batch := int64(constants.AuditRetentionDeleteBatchSize)
|
||||
return (result.EventCount+batch-1)/batch + (result.ResourceCount+batch-1)/batch + (result.IntegrationCount+batch-1)/batch
|
||||
}
|
||||
|
||||
func (s *Service) cleanupAuditMonth(ctx context.Context, start, end time.Time, runs []*model.LogArchiveRun) error {
|
||||
if allRunsCleaned(runs) {
|
||||
func (s *Service) cleanupAuditDate(ctx context.Context, date time.Time, run *model.LogArchiveRun) error {
|
||||
if run.CleanedAt != nil {
|
||||
return nil
|
||||
}
|
||||
if err := s.markCleanupStarted(ctx, constants.AuditArchiveSource, start, end); err != nil {
|
||||
if err := s.markCleanupStarted(ctx, constants.AuditArchiveSource, date); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.deleteAuditResources(ctx, start, end); err != nil {
|
||||
if err := s.deleteAuditResources(ctx, date, date.AddDate(0, 0, 1)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.deleteAuditEvents(ctx, start, end); err != nil {
|
||||
if err := s.deleteAuditEvents(ctx, date, date.AddDate(0, 0, 1)); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.markCleaned(ctx, constants.AuditArchiveSource, start, end)
|
||||
return s.markCleaned(ctx, constants.AuditArchiveSource, date)
|
||||
}
|
||||
|
||||
func (s *Service) cleanupIntegrationMonth(ctx context.Context, start, end time.Time, runs []*model.LogArchiveRun) error {
|
||||
if allRunsCleaned(runs) {
|
||||
func (s *Service) cleanupIntegrationDate(ctx context.Context, date time.Time, run *model.LogArchiveRun) error {
|
||||
if run.CleanedAt != nil {
|
||||
return nil
|
||||
}
|
||||
if err := s.markCleanupStarted(ctx, constants.IntegrationArchiveSource, start, end); err != nil {
|
||||
if err := s.markCleanupStarted(ctx, constants.IntegrationArchiveSource, date); err != nil {
|
||||
return err
|
||||
}
|
||||
end := date.AddDate(0, 0, 1)
|
||||
for {
|
||||
subquery := s.db.Model(&model.IntegrationLog{}).Select("id").
|
||||
Where("created_at >= ? AND created_at < ?", start, end).Order("id ASC").Limit(constants.AuditRetentionDeleteBatchSize)
|
||||
subquery := s.db.Model(&model.IntegrationLog{}).Select("id").Where("created_at >= ? AND created_at < ?", date, end).Order("id ASC").Limit(constants.AuditRetentionDeleteBatchSize)
|
||||
deleted := s.db.WithContext(ctx).Where("id IN (?)", subquery).Delete(&model.IntegrationLog{})
|
||||
if deleted.Error != nil {
|
||||
return fmt.Errorf("分批物理删除 Integration Log 失败: %w", deleted.Error)
|
||||
@@ -474,15 +365,11 @@ func (s *Service) cleanupIntegrationMonth(ctx context.Context, start, end time.T
|
||||
break
|
||||
}
|
||||
}
|
||||
return s.markCleaned(ctx, constants.IntegrationArchiveSource, start, end)
|
||||
return s.markCleaned(ctx, constants.IntegrationArchiveSource, date)
|
||||
}
|
||||
|
||||
func (s *Service) deleteAuditResources(ctx context.Context, start, end time.Time) error {
|
||||
for {
|
||||
subquery := s.db.Model(&model.AuditEventResource{}).Select("tb_audit_event_resource.id").
|
||||
Joins("JOIN tb_audit_event ON tb_audit_event.id = tb_audit_event_resource.audit_event_id").
|
||||
Where("tb_audit_event.created_at >= ? AND tb_audit_event.created_at < ?", start, end).
|
||||
Order("tb_audit_event_resource.id ASC").Limit(constants.AuditRetentionDeleteBatchSize)
|
||||
subquery := s.db.Model(&model.AuditEventResource{}).Select("tb_audit_event_resource.id").Joins("JOIN tb_audit_event ON tb_audit_event.id = tb_audit_event_resource.audit_event_id").Where("tb_audit_event.created_at >= ? AND tb_audit_event.created_at < ?", start, end).Order("tb_audit_event_resource.id ASC").Limit(constants.AuditRetentionDeleteBatchSize)
|
||||
deleted := s.db.WithContext(ctx).Where("id IN (?)", subquery).Delete(&model.AuditEventResource{})
|
||||
if deleted.Error != nil {
|
||||
return fmt.Errorf("分批物理删除 Audit Event Resource 失败: %w", deleted.Error)
|
||||
@@ -492,11 +379,9 @@ func (s *Service) deleteAuditResources(ctx context.Context, start, end time.Time
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) deleteAuditEvents(ctx context.Context, start, end time.Time) error {
|
||||
for {
|
||||
subquery := s.db.Model(&model.AuditEvent{}).Select("id").
|
||||
Where("created_at >= ? AND created_at < ?", start, end).Order("id ASC").Limit(constants.AuditRetentionDeleteBatchSize)
|
||||
subquery := s.db.Model(&model.AuditEvent{}).Select("id").Where("created_at >= ? AND created_at < ?", start, end).Order("id ASC").Limit(constants.AuditRetentionDeleteBatchSize)
|
||||
deleted := s.db.WithContext(ctx).Where("id IN (?)", subquery).Delete(&model.AuditEvent{})
|
||||
if deleted.Error != nil {
|
||||
return fmt.Errorf("分批物理删除 Audit Event 失败: %w", deleted.Error)
|
||||
@@ -506,75 +391,19 @@ func (s *Service) deleteAuditEvents(ctx context.Context, start, end time.Time) e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) markCleanupStarted(ctx context.Context, source string, start, end time.Time) error {
|
||||
func (s *Service) markCleanupStarted(ctx context.Context, source string, date time.Time) error {
|
||||
now := time.Now()
|
||||
result := s.db.WithContext(ctx).Model(&model.LogArchiveRun{}).
|
||||
Where("source = ? AND archive_date >= ? AND archive_date < ? AND instance_id = ? AND cleanup_started_at IS NULL",
|
||||
source, start.Format(time.DateOnly), end.Format(time.DateOnly), s.instanceID).
|
||||
Updates(map[string]any{"cleanup_started_at": now, "updated_at": now})
|
||||
result := s.db.WithContext(ctx).Model(&model.LogArchiveRun{}).Where("source = ? AND archive_date = ? AND instance_id = ? AND cleanup_started_at IS NULL", source, date.Format(time.DateOnly), s.instanceID).Updates(map[string]any{"cleanup_started_at": now, "updated_at": now})
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("记录月度清理开始断点失败: %w", result.Error)
|
||||
}
|
||||
return s.validateCleanupMarkerCount(ctx, source, start, end, "cleanup_started_at IS NOT NULL", "开始")
|
||||
}
|
||||
|
||||
func (s *Service) markCleaned(ctx context.Context, source string, start, end time.Time) error {
|
||||
now := time.Now()
|
||||
result := s.db.WithContext(ctx).Model(&model.LogArchiveRun{}).
|
||||
Where("source = ? AND archive_date >= ? AND archive_date < ? AND instance_id = ? AND cleanup_started_at IS NOT NULL",
|
||||
source, start.Format(time.DateOnly), end.Format(time.DateOnly), s.instanceID).
|
||||
Updates(map[string]any{"cleaned_at": now, "updated_at": now})
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("记录月度清理完成断点失败: %w", result.Error)
|
||||
}
|
||||
return s.validateCleanupMarkerCount(ctx, source, start, end, "cleaned_at IS NOT NULL", "完成")
|
||||
}
|
||||
|
||||
func (s *Service) validateCleanupMarkerCount(ctx context.Context, source string, start, end time.Time, marker, label string) error {
|
||||
var count int64
|
||||
err := s.db.WithContext(ctx).Model(&model.LogArchiveRun{}).
|
||||
Where("source = ? AND archive_date >= ? AND archive_date < ? AND instance_id = ? AND "+marker,
|
||||
source, start.Format(time.DateOnly), end.Format(time.DateOnly), s.instanceID).
|
||||
Count(&count).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("复核月度清理%s断点失败: %w", label, err)
|
||||
}
|
||||
expected := int64(end.Sub(start).Hours() / 24)
|
||||
if count != expected {
|
||||
return fmt.Errorf("月度清理%s断点不完整:期望 %d 条,实际 %d 条", label, expected, count)
|
||||
return fmt.Errorf("记录日清理开始断点失败: %w", result.Error)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func allRunsCleaned(runs []*model.LogArchiveRun) bool {
|
||||
return len(runs) > 0 && runs[0].CleanedAt != nil
|
||||
}
|
||||
|
||||
func (s *Service) recordRetentionAudit(ctx context.Context, start, end time.Time, result RetentionResult, cleanupErr error) error {
|
||||
audit := RetentionAudit{
|
||||
Month: result.Month, RangeStart: start, RangeEnd: end,
|
||||
EventCount: result.EventCount, ResourceCount: result.ResourceCount,
|
||||
IntegrationCount: result.IntegrationCount, ManifestKeys: result.ManifestKeys,
|
||||
DurationMS: result.Duration.Milliseconds(), Result: constants.AuditResultSuccess,
|
||||
Summary: "完成已归档在线日志月度物理清理",
|
||||
EventID: "evt_retention_" + strings.ReplaceAll(result.Month, "-", "_"),
|
||||
func (s *Service) markCleaned(ctx context.Context, source string, date time.Time) error {
|
||||
now := time.Now()
|
||||
result := s.db.WithContext(ctx).Model(&model.LogArchiveRun{}).Where("source = ? AND archive_date = ? AND instance_id = ? AND cleanup_started_at IS NOT NULL", source, date.Format(time.DateOnly), s.instanceID).Updates(map[string]any{"cleaned_at": now, "updated_at": now})
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("记录日清理完成断点失败: %w", result.Error)
|
||||
}
|
||||
if cleanupErr != nil {
|
||||
audit.EventID = ""
|
||||
audit.Result = constants.AuditResultFailed
|
||||
audit.Summary = "已归档在线日志月度物理清理失败"
|
||||
audit.ErrorSummary = truncateRetentionError(cleanupErr)
|
||||
}
|
||||
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
return s.audit.WriteRetentionCleanup(ctx, tx, audit)
|
||||
})
|
||||
}
|
||||
|
||||
func truncateRetentionError(err error) string {
|
||||
value := []rune(err.Error())
|
||||
if len(value) > 500 {
|
||||
value = value[:500]
|
||||
}
|
||||
return string(value)
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user