package approval import ( "context" "strconv" "strings" "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" ) // CreationService 实现业务侧 Approval Port,并保持渠道前置检查与业务事务分离。 type CreationService struct { providers ProviderPort repositories RepositoryProvider eventWriter SubmissionEventWriter now func() time.Time } // NewCreationService 创建通用审批申请创建用例。 func NewCreationService( providers ProviderPort, repositories RepositoryProvider, eventWriter SubmissionEventWriter, now func() time.Time, ) *CreationService { if now == nil { now = time.Now } return &CreationService{providers: providers, repositories: repositories, eventWriter: eventWriter, now: now} } // 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 { return Preparation{}, errors.New(errors.CodeServiceUnavailable, "审批能力尚未配置") } request.BusinessType = strings.TrimSpace(request.BusinessType) request.CorrelationID = strings.TrimSpace(request.CorrelationID) if request.BusinessType == "" || request.SubmitterAccountID == 0 || request.CorrelationID == "" { return Preparation{}, errors.New(errors.CodeInvalidParam) } providerContext, err := s.providers.Prepare(ctx, request) if err != nil { return Preparation{}, err } providerContext.Provider = strings.TrimSpace(providerContext.Provider) if providerContext.Provider == "" { return Preparation{}, errors.New(errors.CodeServiceUnavailable, "审批渠道未返回有效 provider") } return Preparation{ provider: providerContext.Provider, businessType: request.BusinessType, submitterAccountID: request.SubmitterAccountID, correlationID: request.CorrelationID, expiresAt: s.now().UTC().Add(constants.ApprovalPreparationTTL), issuer: s, providerContext: providerContext, }, nil } // 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 { return Reference{}, errors.New(errors.CodeInternalError, "通用审批创建用例未完整配置") } now := s.now().UTC() if err := s.validatePreparation(request, now); err != nil { return Reference{}, err } instance, err := approvaldomain.NewInstance(approvaldomain.NewInstanceParams{ BusinessType: request.BusinessType, BusinessID: request.BusinessID, SubmitterAccountID: request.SubmitterAccountID, SubmitterSnapshot: request.SubmitterSnapshot, Provider: request.Preparation.provider, RequestSnapshot: request.RequestSnapshot, CorrelationID: request.CorrelationID, }, now) if err != nil { return Reference{}, err } repository := s.repositories.ForDB(tx) if repository == nil { return Reference{}, errors.New(errors.CodeInternalError, "通用审批 Repository 未配置") } if err := repository.Create(ctx, instance); err != nil { return Reference{}, err } if err := s.providers.CreateContextInTx(ctx, tx, request.Preparation.providerContext, instance.ID); err != nil { return Reference{}, err } event := SubmissionRequestedEvent{ EventID: "approval:" + strconv.FormatUint(uint64(instance.ID), 10) + ":submission", InstanceID: instance.ID, BusinessType: instance.BusinessType, BusinessID: instance.BusinessID, SubmitterAccountID: instance.SubmitterAccountID, Provider: instance.Provider, CorrelationID: instance.CorrelationID, OccurredAt: instance.CreatedAt, } if err := s.eventWriter.Append(ctx, tx, event); err != nil { return Reference{}, err } return Reference{InstanceID: instance.ID, Status: instance.Status}, nil } func (s *CreationService) validatePreparation(request CreateRequest, now time.Time) error { preparation := request.Preparation if preparation.issuer != s || preparation.expiresAt.IsZero() || !preparation.expiresAt.After(now) { return errors.New(errors.CodeServiceUnavailable, "审批可用性检查已失效,请重新提交") } if request.BusinessType != preparation.businessType || request.SubmitterAccountID != preparation.submitterAccountID || request.CorrelationID != preparation.correlationID { return errors.New(errors.CodeInvalidParam, "审批准备结果与业务申请不匹配") } return nil } // UnavailableProviderPort 在当前环境未装配有效审批 Adapter 时失败关闭。 type UnavailableProviderPort struct{} // Prepare 拒绝在缺少有效 Adapter、场景或发起身份时创建业务审批。 func (UnavailableProviderPort) Prepare(_ context.Context, _ PrepareRequest) (ProviderPreparation, error) { return ProviderPreparation{}, errors.New(errors.CodeServiceUnavailable, "当前环境没有可用审批渠道") } // CreateContextInTx 防止未配置渠道上下文时误写审批事实。 func (UnavailableProviderPort) CreateContextInTx(_ context.Context, _ *gorm.DB, _ ProviderPreparation, _ uint) error { return errors.New(errors.CodeServiceUnavailable, "当前环境没有可用审批渠道") }