// Package retention 定义 Orbit 历史数据保留策略的通用模型。 // // 策略只描述候选数据的最短保留窗口,不直接删除任务、事件或审计记录。 // 归档执行器必须在独立流程中先完成核查、备份和归档证明,再按策略处理。 package retention import ( "context" "errors" "fmt" "strconv" "strings" "time" "unicode/utf8" core "git.yuntuo.hk/k2safe/orbit/model" "git.yuntuo.hk/k2safe/orbit/platform/auth" ) const ( // DefaultIdempotencyDays 是请求幂等记录的默认保留天数。 DefaultIdempotencyDays uint32 = 90 // DefaultTaskHistoryDays 是终态任务历史的默认保留天数。 DefaultTaskHistoryDays uint32 = 365 // DefaultWorkflowHistoryDays 是终态流程历史的默认保留天数。 DefaultWorkflowHistoryDays uint32 = 365 // DefaultEventLogDays 是状态事件和结果事件的默认保留天数。 DefaultEventLogDays uint32 = 365 // DefaultDeliveryHistoryDays 是已确认交付历史的默认保留天数。 DefaultDeliveryHistoryDays uint32 = 90 // DefaultInboxHistoryDays 是已处理流程 Inbox 历史的默认保留天数。 DefaultInboxHistoryDays uint32 = 90 // DefaultAuditLogDays 是管理审计的默认保留天数,约七年。 DefaultAuditLogDays uint32 = 2555 minRetentionDays uint32 = 1 maxRetentionDays uint32 = 3650 ) // Policy 是按命名空间保存的历史数据保留策略。 // Enabled 默认关闭;即使打开,也允许只读归档导出使用满足窗口且已核验的数据。 // 当前版本不会因为配置变更自动删除任何数据。 type Policy struct { // NamespaceID 是策略所属命名空间,不能由控制台请求替换。 NamespaceID core.ID `json:"namespace_id"` // Revision 是乐观并发版本;默认策略未落库时为0,首次保存后从1开始。 Revision uint64 `json:"revision,string"` // Enabled 表示是否允许归档执行器使用该策略;默认关闭以避免误清理。 Enabled bool `json:"enabled"` // IdempotencyDays 是请求幂等记录至少保留的天数。 IdempotencyDays uint32 `json:"idempotency_days"` // TaskHistoryDays 是终态任务及执行证据至少保留的天数。 TaskHistoryDays uint32 `json:"task_history_days"` // WorkflowHistoryDays 是终态流程、步骤、等待和流程事件至少保留的天数。 WorkflowHistoryDays uint32 `json:"workflow_history_days"` // EventLogDays 是状态事件和结果事件至少保留的天数。 EventLogDays uint32 `json:"event_log_days"` // DeliveryHistoryDays 是已确认结果交付至少保留的天数;未确认交付不得按窗口清理。 DeliveryHistoryDays uint32 `json:"delivery_history_days"` // InboxHistoryDays 是已处理流程 Inbox 事件至少保留的天数;未处理记录不得清理。 InboxHistoryDays uint32 `json:"inbox_history_days"` // AuditLogDays 是管理审计至少保留的天数;审计记录不参与普通清理。 AuditLogDays uint32 `json:"audit_log_days"` // UpdatedAt 是策略最后修改时间,统一使用 UTC。 UpdatedAt time.Time `json:"updated_at"` // Source 表示策略来自默认值还是数据库配置,便于控制台明确当前状态。 Source string `json:"source"` } // UpdateInput 是控制台保存策略所需的完整替换输入。 // 所有窗口必须显式提交,避免遗漏字段被解释成缩短保留期。 type UpdateInput struct { // ExpectedRevision 是页面读取到的版本;首次保存必须为0。 ExpectedRevision uint64 `json:"expected_revision,string" required:"true"` // Enabled 是否允许归档执行器读取本策略。 Enabled bool `json:"enabled" required:"true"` // IdempotencyDays 请求幂等记录保留天数。 IdempotencyDays uint32 `json:"idempotency_days" required:"true"` // TaskHistoryDays 终态任务历史保留天数。 TaskHistoryDays uint32 `json:"task_history_days" required:"true"` // WorkflowHistoryDays 终态流程历史保留天数。 WorkflowHistoryDays uint32 `json:"workflow_history_days" required:"true"` // EventLogDays 状态和结果事件保留天数。 EventLogDays uint32 `json:"event_log_days" required:"true"` // DeliveryHistoryDays 已确认交付历史保留天数。 DeliveryHistoryDays uint32 `json:"delivery_history_days" required:"true"` // InboxHistoryDays 已处理 Inbox 历史保留天数。 InboxHistoryDays uint32 `json:"inbox_history_days" required:"true"` // AuditLogDays 管理审计保留天数。 AuditLogDays uint32 `json:"audit_log_days" required:"true"` // Reason 是本次策略变更原因,会进入管理审计。 Reason string `json:"reason" required:"true"` } // Store 是控制台和归档执行器共享的最小策略存储边界。 // 实现必须保证 Update 的版本核对、幂等和审计与策略写入处于同一事务。 type Store interface { GetRetentionPolicy(context.Context, core.ID) (Policy, error) UpdateRetentionPolicy(context.Context, auth.Token, string, UpdateInput) (Policy, error) } // Defaults 返回不会触发任何归档执行的安全默认策略。 func Defaults(namespace core.ID) Policy { return Policy{ NamespaceID: namespace, Enabled: false, IdempotencyDays: DefaultIdempotencyDays, TaskHistoryDays: DefaultTaskHistoryDays, WorkflowHistoryDays: DefaultWorkflowHistoryDays, EventLogDays: DefaultEventLogDays, DeliveryHistoryDays: DefaultDeliveryHistoryDays, InboxHistoryDays: DefaultInboxHistoryDays, AuditLogDays: DefaultAuditLogDays, Source: "default", } } // Validate 检查策略身份、窗口和审计原因。 func (p Policy) Validate() error { if p.NamespaceID.Validate() != nil { return errors.New("retention namespace is invalid") } for name, days := range map[string]uint32{ "idempotency_days": p.IdempotencyDays, "task_history_days": p.TaskHistoryDays, "workflow_history_days": p.WorkflowHistoryDays, "event_log_days": p.EventLogDays, "delivery_history_days": p.DeliveryHistoryDays, "inbox_history_days": p.InboxHistoryDays, "audit_log_days": p.AuditLogDays, } { if days < minRetentionDays || days > maxRetentionDays { return fmt.Errorf("%s must be between %d and %d days", name, minRetentionDays, maxRetentionDays) } } if p.IdempotencyDays < DefaultIdempotencyDays { return errors.New("idempotency_days cannot be less than 90 days") } return nil } // ToPolicy 将完整输入转为待保存策略,版本由事务内分配。 func (in UpdateInput) ToPolicy(namespace core.ID) (Policy, error) { if strings.TrimSpace(in.Reason) == "" || !utf8.ValidString(in.Reason) || len(in.Reason) > 1024 || strings.ContainsAny(in.Reason, "\r\n") { return Policy{}, errors.New("retention reason must be 1-1024 UTF-8 bytes without newlines") } p := Policy{ NamespaceID: namespace, Enabled: in.Enabled, IdempotencyDays: in.IdempotencyDays, TaskHistoryDays: in.TaskHistoryDays, WorkflowHistoryDays: in.WorkflowHistoryDays, EventLogDays: in.EventLogDays, DeliveryHistoryDays: in.DeliveryHistoryDays, InboxHistoryDays: in.InboxHistoryDays, AuditLogDays: in.AuditLogDays, } if err := p.Validate(); err != nil { return Policy{}, err } return p, nil } // FromEnv 读取部署默认值;数据库策略保存后优先于部署默认值。 // 所有节点应注入同一组默认值,保证尚未保存策略的展示一致。 func FromEnv(get func(string) string) (Policy, error) { p := Defaults("") for _, f := range []struct { name string dst *uint32 }{ {"ORBIT_RETENTION_IDEMPOTENCY_DAYS", &p.IdempotencyDays}, {"ORBIT_RETENTION_TASK_DAYS", &p.TaskHistoryDays}, {"ORBIT_RETENTION_WORKFLOW_DAYS", &p.WorkflowHistoryDays}, {"ORBIT_RETENTION_EVENT_DAYS", &p.EventLogDays}, {"ORBIT_RETENTION_DELIVERY_DAYS", &p.DeliveryHistoryDays}, {"ORBIT_RETENTION_INBOX_DAYS", &p.InboxHistoryDays}, {"ORBIT_RETENTION_AUDIT_DAYS", &p.AuditLogDays}, } { if raw := get(f.name); raw != "" { n, err := strconv.ParseUint(raw, 10, 32) if err != nil || n < 1 || n > 3650 { return Policy{}, fmt.Errorf("%s must be 1-3650 days", f.name) } *f.dst = uint32(n) } } if p.IdempotencyDays < 90 { return Policy{}, errors.New("ORBIT_RETENTION_IDEMPOTENCY_DAYS cannot be less than 90") } return p, nil }