128 lines
5.3 KiB
Go
128 lines
5.3 KiB
Go
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, "当前环境没有可用审批渠道")
|
||
}
|