收口审计治理与套餐任务进展
Constraint: 在线热修前必须保存当前迭代分支全部有效代码进展 Confidence: medium Scope-risk: broad Directive: 后续修改需保持审计事件与业务事务边界一致 Tested: git diff --cached --check Not-tested: 未运行全量测试,提交用于切换分支前保存既有工作
This commit is contained in:
40
internal/application/approval/audit.go
Normal file
40
internal/application/approval/audit.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package approval
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// AuditChange 描述通用审批链路一次实际状态变化。
|
||||
type AuditChange struct {
|
||||
EventID string
|
||||
ActionCode string
|
||||
Summary string
|
||||
InstanceID uint
|
||||
BusinessType string
|
||||
BusinessID uint
|
||||
SubmitterAccountID uint
|
||||
SubmitterSnapshot []byte
|
||||
Provider string
|
||||
BeforeExternalRef string
|
||||
AfterExternalRef string
|
||||
CorrelationID string
|
||||
ParentEventID string
|
||||
BeforeStatus *int
|
||||
AfterStatus *int
|
||||
ActorKind string
|
||||
ActorID string
|
||||
ActorName string
|
||||
Source string
|
||||
Result string
|
||||
ErrorSummary string
|
||||
Decision string
|
||||
IntegrationIDs []string
|
||||
OutboxEventID string
|
||||
}
|
||||
|
||||
// AuditWriter 在审批事实事务中追加统一 Audit Event。
|
||||
type AuditWriter interface {
|
||||
WriteApproval(ctx context.Context, tx *gorm.DB, change AuditChange) error
|
||||
}
|
||||
@@ -18,6 +18,7 @@ type CreationService struct {
|
||||
providers ProviderPort
|
||||
repositories RepositoryProvider
|
||||
eventWriter SubmissionEventWriter
|
||||
audit AuditWriter
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
@@ -34,6 +35,11 @@ func NewCreationService(
|
||||
return &CreationService{providers: providers, repositories: repositories, eventWriter: eventWriter, now: now}
|
||||
}
|
||||
|
||||
// SetAuditWriter 注入通用审批统一审计 Writer。
|
||||
func (s *CreationService) SetAuditWriter(writer AuditWriter) {
|
||||
s.audit = writer
|
||||
}
|
||||
|
||||
// Prepare 在任何业务事实写入前确认 Adapter、场景和真实发起身份可用。
|
||||
func (s *CreationService) Prepare(ctx context.Context, request PrepareRequest) (Preparation, error) {
|
||||
if s == nil || s.providers == nil || s.repositories == nil || s.eventWriter == nil {
|
||||
@@ -62,7 +68,7 @@ func (s *CreationService) Prepare(ctx context.Context, request PrepareRequest) (
|
||||
|
||||
// CreateInTx 使用调用方业务事务原子创建通用实例、渠道上下文和提交 Outbox。
|
||||
func (s *CreationService) CreateInTx(ctx context.Context, tx *gorm.DB, request CreateRequest) (Reference, error) {
|
||||
if s == nil || tx == nil || s.repositories == nil || s.providers == nil || s.eventWriter == nil {
|
||||
if s == nil || tx == nil || s.repositories == nil || s.providers == nil || s.eventWriter == nil || s.audit == nil {
|
||||
return Reference{}, errors.New(errors.CodeInternalError, "通用审批创建用例未完整配置")
|
||||
}
|
||||
now := s.now().UTC()
|
||||
@@ -97,6 +103,18 @@ func (s *CreationService) CreateInTx(ctx context.Context, tx *gorm.DB, request C
|
||||
if err := s.eventWriter.Append(ctx, tx, event); err != nil {
|
||||
return Reference{}, err
|
||||
}
|
||||
afterStatus := instance.Status
|
||||
if err := s.audit.WriteApproval(ctx, tx, AuditChange{
|
||||
EventID: "approval:" + strconv.FormatUint(uint64(instance.ID), 10) + ":audit:requested",
|
||||
ActionCode: constants.AuditActionApprovalRequested, Summary: "提交通用审批申请",
|
||||
InstanceID: instance.ID, BusinessType: instance.BusinessType, BusinessID: instance.BusinessID,
|
||||
SubmitterAccountID: instance.SubmitterAccountID, SubmitterSnapshot: instance.SubmitterSnapshot,
|
||||
Provider: instance.Provider, CorrelationID: instance.CorrelationID, AfterStatus: &afterStatus,
|
||||
ActorKind: constants.AuditActorAccount, ActorID: strconv.FormatUint(uint64(instance.SubmitterAccountID), 10),
|
||||
Source: constants.AuditSourceAdminAPI, Result: constants.AuditResultSuccess, OutboxEventID: event.EventID,
|
||||
}); err != nil {
|
||||
return Reference{}, err
|
||||
}
|
||||
return Reference{InstanceID: instance.ID, Status: instance.Status}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ type SyncDecisionCommand struct {
|
||||
Decision string
|
||||
DecisionSnapshot []byte
|
||||
Source string
|
||||
IntegrationIDs []string
|
||||
}
|
||||
|
||||
// SyncDecisionResult 返回本次是否首次记录该标准终态。
|
||||
@@ -60,6 +61,7 @@ type SyncDecisionService struct {
|
||||
repositories RepositoryProvider
|
||||
eventWriter TerminalEventWriter
|
||||
deliveryWriter DecisionDeliveryWriter
|
||||
audit AuditWriter
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
@@ -79,9 +81,14 @@ func NewSyncDecisionService(
|
||||
}
|
||||
}
|
||||
|
||||
// SetAuditWriter 注入通用审批统一审计 Writer。
|
||||
func (s *SyncDecisionService) SetAuditWriter(writer AuditWriter) {
|
||||
s.audit = writer
|
||||
}
|
||||
|
||||
// Execute 将回调或轮询取得的权威渠道状态原子转换为通用审批终态和可靠业务事件。
|
||||
func (s *SyncDecisionService) Execute(ctx context.Context, command SyncDecisionCommand) (*SyncDecisionResult, error) {
|
||||
if s == nil || s.db == nil || s.repositories == nil || s.eventWriter == nil || s.deliveryWriter == nil {
|
||||
if s == nil || s.db == nil || s.repositories == nil || s.eventWriter == nil || s.deliveryWriter == nil || s.audit == nil {
|
||||
return nil, errors.New(errors.CodeInternalError, "通用审批决策同步用例未完整配置")
|
||||
}
|
||||
if command.InstanceID == 0 || !isSupportedSyncSource(command.Source) {
|
||||
@@ -98,6 +105,8 @@ func (s *SyncDecisionService) Execute(ctx context.Context, command SyncDecisionC
|
||||
return err
|
||||
}
|
||||
expectedStatus, expectedVersion := instance.Status, instance.Version
|
||||
beforeStatus := instance.Status
|
||||
beforeExternalRef := instance.ExternalRef
|
||||
changed, err := instance.ApplyDecision(command.Decision, command.DecisionSnapshot, s.now().UTC())
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -125,6 +134,20 @@ func (s *SyncDecisionService) Execute(ctx context.Context, command SyncDecisionC
|
||||
if err := s.eventWriter.Append(ctx, tx, event); err != nil {
|
||||
return err
|
||||
}
|
||||
actorKind, actorID, source := approvalSyncAuditOrigin(command.Source)
|
||||
afterStatus := instance.Status
|
||||
if err := s.audit.WriteApproval(ctx, tx, AuditChange{
|
||||
EventID: event.EventID + ":audit", ActionCode: constants.AuditActionApprovalDecisionSynced,
|
||||
Summary: "同步审批权威终态", InstanceID: instance.ID,
|
||||
BusinessType: instance.BusinessType, BusinessID: instance.BusinessID,
|
||||
SubmitterAccountID: instance.SubmitterAccountID, SubmitterSnapshot: instance.SubmitterSnapshot,
|
||||
Provider: instance.Provider, BeforeExternalRef: beforeExternalRef, AfterExternalRef: instance.ExternalRef,
|
||||
CorrelationID: instance.CorrelationID, BeforeStatus: &beforeStatus, AfterStatus: &afterStatus,
|
||||
ActorKind: actorKind, ActorID: actorID, Source: source, Result: constants.AuditResultSuccess,
|
||||
Decision: command.Decision, IntegrationIDs: command.IntegrationIDs, OutboxEventID: event.EventID,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
result.FirstTerminal = true
|
||||
return nil
|
||||
})
|
||||
@@ -134,6 +157,17 @@ func (s *SyncDecisionService) Execute(ctx context.Context, command SyncDecisionC
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func approvalSyncAuditOrigin(source string) (string, string, string) {
|
||||
switch source {
|
||||
case constants.ApprovalSyncSourceCallback:
|
||||
return constants.AuditActorExternalSystem, constants.ApprovalAuditActorWeCom, constants.AuditSourceCallback
|
||||
case constants.ApprovalSyncSourcePolling:
|
||||
return constants.AuditActorScheduledJob, constants.ApprovalAuditActorRecoveryJob, constants.AuditSourceScheduler
|
||||
default:
|
||||
return constants.AuditActorAccount, "", constants.AuditSourceAdminAPI
|
||||
}
|
||||
}
|
||||
|
||||
func terminalDecisionEventID(instanceID uint, decision string) string {
|
||||
return "approval:" + strconv.FormatUint(uint64(instanceID), 10) + ":" + decision
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user