Constraint: 在线热修前必须保存当前迭代分支全部有效代码进展 Confidence: medium Scope-risk: broad Directive: 后续修改需保持审计事件与业务事务边界一致 Tested: git diff --cached --check Not-tested: 未运行全量测试,提交用于切换分支前保存既有工作
180 lines
6.5 KiB
Go
180 lines
6.5 KiB
Go
package approval
|
|
|
|
import (
|
|
"context"
|
|
"strconv"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
|
|
approvaldomain "github.com/break/junhong_cmp_fiber/internal/domain/approval"
|
|
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
|
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
|
)
|
|
|
|
// RepositoryProvider 为当前 GORM 事务提供纯领域 Repository。
|
|
type RepositoryProvider interface {
|
|
ForDB(db *gorm.DB) approvaldomain.Repository
|
|
}
|
|
|
|
// TerminalDecisionEvent 是业务消费者接收的渠道无关标准决策事件。
|
|
type TerminalDecisionEvent struct {
|
|
EventID string `json:"event_id"`
|
|
InstanceID uint `json:"instance_id"`
|
|
BusinessType string `json:"business_type"`
|
|
BusinessID uint `json:"business_id"`
|
|
SubmitterAccountID uint `json:"submitter_account_id"`
|
|
Decision string `json:"decision"`
|
|
Source string `json:"source"`
|
|
CorrelationID string `json:"correlation_id"`
|
|
OccurredAt time.Time `json:"occurred_at"`
|
|
}
|
|
|
|
// TerminalEventWriter 在审批状态事务中追加标准决策 Outbox。
|
|
type TerminalEventWriter interface {
|
|
Append(ctx context.Context, tx *gorm.DB, event TerminalDecisionEvent) error
|
|
}
|
|
|
|
// DecisionDeliveryWriter 在审批状态事务中创建业务消费租约事实。
|
|
type DecisionDeliveryWriter interface {
|
|
Create(ctx context.Context, tx *gorm.DB, event TerminalDecisionEvent) error
|
|
}
|
|
|
|
// SyncDecisionCommand 是回调、兜底轮询和受控人工同步共用的标准决策命令。
|
|
type SyncDecisionCommand struct {
|
|
InstanceID uint
|
|
Decision string
|
|
DecisionSnapshot []byte
|
|
Source string
|
|
IntegrationIDs []string
|
|
}
|
|
|
|
// SyncDecisionResult 返回本次是否首次记录该标准终态。
|
|
type SyncDecisionResult struct {
|
|
Status int
|
|
FirstTerminal bool
|
|
}
|
|
|
|
// SyncDecisionService 统一处理各审批渠道回传的标准决策。
|
|
type SyncDecisionService struct {
|
|
db *gorm.DB
|
|
repositories RepositoryProvider
|
|
eventWriter TerminalEventWriter
|
|
deliveryWriter DecisionDeliveryWriter
|
|
audit AuditWriter
|
|
now func() time.Time
|
|
}
|
|
|
|
// NewSyncDecisionService 创建标准决策同步用例。
|
|
func NewSyncDecisionService(
|
|
db *gorm.DB,
|
|
repositories RepositoryProvider,
|
|
eventWriter TerminalEventWriter,
|
|
deliveryWriter DecisionDeliveryWriter,
|
|
now func() time.Time,
|
|
) *SyncDecisionService {
|
|
if now == nil {
|
|
now = time.Now
|
|
}
|
|
return &SyncDecisionService{
|
|
db: db, repositories: repositories, eventWriter: eventWriter, deliveryWriter: deliveryWriter, now: now,
|
|
}
|
|
}
|
|
|
|
// 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 || s.audit == nil {
|
|
return nil, errors.New(errors.CodeInternalError, "通用审批决策同步用例未完整配置")
|
|
}
|
|
if command.InstanceID == 0 || !isSupportedSyncSource(command.Source) {
|
|
return nil, errors.New(errors.CodeInvalidParam)
|
|
}
|
|
result := &SyncDecisionResult{}
|
|
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
repository := s.repositories.ForDB(tx)
|
|
if repository == nil {
|
|
return errors.New(errors.CodeInternalError, "通用审批 Repository 未配置")
|
|
}
|
|
instance, err := repository.GetForUpdate(ctx, command.InstanceID)
|
|
if err != nil {
|
|
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
|
|
}
|
|
result.Status = instance.Status
|
|
if !changed {
|
|
return nil
|
|
}
|
|
saved, err := repository.SaveDecision(ctx, instance, expectedStatus, expectedVersion)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !saved {
|
|
return errors.New(errors.CodeConflict, "审批状态已被其他同步任务更新")
|
|
}
|
|
event := TerminalDecisionEvent{
|
|
EventID: terminalDecisionEventID(instance.ID, command.Decision),
|
|
InstanceID: instance.ID, BusinessType: instance.BusinessType, BusinessID: instance.BusinessID,
|
|
SubmitterAccountID: instance.SubmitterAccountID, Decision: command.Decision, Source: command.Source,
|
|
CorrelationID: instance.CorrelationID, OccurredAt: instance.StatusChangedAt,
|
|
}
|
|
if err := s.deliveryWriter.Create(ctx, tx, event); err != nil {
|
|
return err
|
|
}
|
|
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
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
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
|
|
}
|
|
|
|
func isSupportedSyncSource(source string) bool {
|
|
return source == constants.ApprovalSyncSourceCallback ||
|
|
source == constants.ApprovalSyncSourcePolling ||
|
|
source == constants.ApprovalSyncSourceManual
|
|
}
|