@@ -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 . r etentionMonth Range( month )
start , end , err := s . pendingR etentionRange( 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 . loadRetentionRun s( ctx , start , end )
var existing [ ] model . LogArchiveRun
if err := s . db . WithContext ( ctx ) . Where ( "source IN ? AND archive_date = ? AND instance_id = ?" , [ ] string { constant s. 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 . validateAudit RetentionDay ( ctx , date , runs . audit ) ; err != nil {
return RetentionResult { } , fmt . Errorf ( "Audit 完整性校验失败: %w" , err )
}
summarizeRetentionR uns( runs , & result )
if err := s . validateIntegrationRetentionDay ( ctx , date , r uns. 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 )
}
day s := 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 ) }
run s := retentionRuns { }
for index := range rows {
run := & rows [ index ]
switch run . Source {
switch rows [ index ] . Source {
case constants . AuditArchiveSource :
runs . audit = append ( run s . audit , run )
runs . audit = & row s [ index ]
case constants . IntegrationArchiveSource :
runs . integration = append ( run s . integration , run )
runs . integration = & row s [ 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
}