暂存一下,防止丢失
This commit is contained in:
104
internal/infrastructure/approval/decision_delivery.go
Normal file
104
internal/infrastructure/approval/decision_delivery.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package approval
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
approvalapp "github.com/break/junhong_cmp_fiber/internal/application/approval"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
)
|
||||
|
||||
// DecisionDeliveryStore 持久化标准决策消费事实并管理处理租约。
|
||||
type DecisionDeliveryStore struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewDecisionDeliveryStore 创建标准决策投递 Store。
|
||||
func NewDecisionDeliveryStore(db *gorm.DB) *DecisionDeliveryStore {
|
||||
return &DecisionDeliveryStore{db: db}
|
||||
}
|
||||
|
||||
// Create 在审批状态事务中创建唯一标准决策投递事实。
|
||||
func (s *DecisionDeliveryStore) Create(ctx context.Context, tx *gorm.DB, event approvalapp.TerminalDecisionEvent) error {
|
||||
if tx == nil {
|
||||
return errors.New(errors.CodeInternalError, "审批决策投递必须使用审批状态事务")
|
||||
}
|
||||
record := model.ApprovalDecisionDelivery{
|
||||
InstanceID: event.InstanceID, Decision: event.Decision, EventID: event.EventID,
|
||||
Status: constants.ApprovalDecisionDeliveryPending, CreatedAt: event.OccurredAt, UpdatedAt: event.OccurredAt,
|
||||
}
|
||||
if err := tx.WithContext(ctx).Create(&record).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建审批决策投递事实失败")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Claim 领取待处理、失败或租约已过期的标准决策。
|
||||
func (s *DecisionDeliveryStore) Claim(
|
||||
ctx context.Context,
|
||||
eventID string,
|
||||
owner string,
|
||||
now time.Time,
|
||||
duration time.Duration,
|
||||
) (bool, error) {
|
||||
if s == nil || s.db == nil || eventID == "" || owner == "" || duration <= 0 {
|
||||
return false, errors.New(errors.CodeInternalError, "审批决策处理租约 Store 未完整配置")
|
||||
}
|
||||
result := s.db.WithContext(ctx).Model(&model.ApprovalDecisionDelivery{}).
|
||||
Where("event_id = ? AND (status IN ? OR (status = ? AND lease_expires_at <= ?))",
|
||||
eventID,
|
||||
[]int{constants.ApprovalDecisionDeliveryPending, constants.ApprovalDecisionDeliveryFailed},
|
||||
constants.ApprovalDecisionDeliveryProcessing, now).
|
||||
Updates(map[string]any{
|
||||
"status": constants.ApprovalDecisionDeliveryProcessing,
|
||||
"lease_owner": owner, "lease_expires_at": now.Add(duration), "updated_at": now,
|
||||
})
|
||||
if result.Error != nil {
|
||||
return false, errors.Wrap(errors.CodeDatabaseError, result.Error, "领取审批决策处理租约失败")
|
||||
}
|
||||
return result.RowsAffected == 1, nil
|
||||
}
|
||||
|
||||
// MarkSucceeded 标记当前租约的业务消费者已幂等处理成功。
|
||||
func (s *DecisionDeliveryStore) MarkSucceeded(ctx context.Context, eventID string, owner string, now time.Time) (bool, error) {
|
||||
if s == nil || s.db == nil {
|
||||
return false, errors.New(errors.CodeInternalError, "审批决策处理租约 Store 未配置")
|
||||
}
|
||||
result := s.db.WithContext(ctx).Model(&model.ApprovalDecisionDelivery{}).
|
||||
Where("event_id = ? AND status = ? AND lease_owner = ?", eventID, constants.ApprovalDecisionDeliveryProcessing, owner).
|
||||
Updates(map[string]any{
|
||||
"status": constants.ApprovalDecisionDeliverySucceeded, "processed_at": now,
|
||||
"lease_owner": nil, "lease_expires_at": nil, "last_error": "", "updated_at": now,
|
||||
})
|
||||
if result.Error != nil {
|
||||
return false, errors.Wrap(errors.CodeDatabaseError, result.Error, "完成审批决策业务处理失败")
|
||||
}
|
||||
return result.RowsAffected == 1, nil
|
||||
}
|
||||
|
||||
// MarkFailed 记录当前租约的安全失败摘要并释放租约等待重试。
|
||||
func (s *DecisionDeliveryStore) MarkFailed(
|
||||
ctx context.Context,
|
||||
eventID string,
|
||||
owner string,
|
||||
now time.Time,
|
||||
errorSummary string,
|
||||
) (bool, error) {
|
||||
if s == nil || s.db == nil {
|
||||
return false, errors.New(errors.CodeInternalError, "审批决策处理租约 Store 未配置")
|
||||
}
|
||||
result := s.db.WithContext(ctx).Model(&model.ApprovalDecisionDelivery{}).
|
||||
Where("event_id = ? AND status = ? AND lease_owner = ?", eventID, constants.ApprovalDecisionDeliveryProcessing, owner).
|
||||
Updates(map[string]any{
|
||||
"status": constants.ApprovalDecisionDeliveryFailed, "retry_count": gorm.Expr("retry_count + 1"),
|
||||
"lease_owner": nil, "lease_expires_at": nil, "last_error": errorSummary, "updated_at": now,
|
||||
})
|
||||
if result.Error != nil {
|
||||
return false, errors.Wrap(errors.CodeDatabaseError, result.Error, "记录审批决策业务处理失败")
|
||||
}
|
||||
return result.RowsAffected == 1, nil
|
||||
}
|
||||
112
internal/infrastructure/approval/repository.go
Normal file
112
internal/infrastructure/approval/repository.go
Normal file
@@ -0,0 +1,112 @@
|
||||
// Package approval 实现通用审批实例的 PostgreSQL 持久化 Adapter。
|
||||
package approval
|
||||
|
||||
import (
|
||||
"context"
|
||||
stderrors "errors"
|
||||
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
|
||||
approvaldomain "github.com/break/junhong_cmp_fiber/internal/domain/approval"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
)
|
||||
|
||||
// Repository 实现通用审批实例写侧仓储。
|
||||
type Repository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// RepositoryProvider 为 Application 用例创建事务作用域 Repository。
|
||||
type RepositoryProvider struct{}
|
||||
|
||||
// NewRepositoryProvider 创建通用审批 Repository Provider。
|
||||
func NewRepositoryProvider() *RepositoryProvider {
|
||||
return &RepositoryProvider{}
|
||||
}
|
||||
|
||||
// ForDB 使用指定数据库会话创建领域 Repository。
|
||||
func (p *RepositoryProvider) ForDB(db *gorm.DB) approvaldomain.Repository {
|
||||
return NewRepository(db)
|
||||
}
|
||||
|
||||
// GetForUpdate 在当前事务中锁定并读取通用审批实例。
|
||||
func (r *Repository) GetForUpdate(ctx context.Context, instanceID uint) (*approvaldomain.Instance, error) {
|
||||
if r == nil || r.db == nil {
|
||||
return nil, errors.New(errors.CodeInternalError, "通用审批实例 Repository 未配置数据库会话")
|
||||
}
|
||||
var record model.ApprovalInstance
|
||||
err := r.db.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ?", instanceID).First(&record).Error
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "读取通用审批实例失败")
|
||||
}
|
||||
return instanceFromModel(record), nil
|
||||
}
|
||||
|
||||
// SaveDecision 以旧状态和旧版本为条件保存标准决策,防止并发重复终态。
|
||||
func (r *Repository) SaveDecision(
|
||||
ctx context.Context,
|
||||
instance *approvaldomain.Instance,
|
||||
expectedStatus int,
|
||||
expectedVersion int,
|
||||
) (bool, error) {
|
||||
if r == nil || r.db == nil || instance == nil {
|
||||
return false, errors.New(errors.CodeInternalError, "通用审批实例 Repository 未完整配置")
|
||||
}
|
||||
result := r.db.WithContext(ctx).Model(&model.ApprovalInstance{}).
|
||||
Where("id = ? AND status = ? AND version = ?", instance.ID, expectedStatus, expectedVersion).
|
||||
Updates(map[string]any{
|
||||
"status": instance.Status, "decision_snapshot": datatypes.JSON(instance.DecisionSnapshot),
|
||||
"status_changed_at": instance.StatusChangedAt, "version": instance.Version, "updated_at": instance.UpdatedAt,
|
||||
})
|
||||
if result.Error != nil {
|
||||
return false, errors.Wrap(errors.CodeDatabaseError, result.Error, "保存通用审批决策失败")
|
||||
}
|
||||
return result.RowsAffected == 1, nil
|
||||
}
|
||||
|
||||
func instanceFromModel(record model.ApprovalInstance) *approvaldomain.Instance {
|
||||
return &approvaldomain.Instance{
|
||||
ID: record.ID, BusinessType: record.BusinessType, BusinessID: record.BusinessID,
|
||||
SubmitterAccountID: record.SubmitterAccountID, SubmitterSnapshot: append([]byte(nil), record.SubmitterSnapshot...),
|
||||
Provider: record.Provider, ExternalRef: record.ExternalRef, Status: record.Status,
|
||||
RequestSnapshot: append([]byte(nil), record.RequestSnapshot...),
|
||||
DecisionSnapshot: append([]byte(nil), record.DecisionSnapshot...), CorrelationID: record.CorrelationID,
|
||||
Version: record.Version, StatusChangedAt: record.StatusChangedAt,
|
||||
CreatedAt: record.CreatedAt, UpdatedAt: record.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// NewRepository 创建通用审批实例 PostgreSQL Repository。
|
||||
func NewRepository(db *gorm.DB) *Repository {
|
||||
return &Repository{db: db}
|
||||
}
|
||||
|
||||
// Create 使用 Repository 持有的数据库会话创建一条唯一通用审批实例。
|
||||
// 需要原子写入业务事实时,调用方必须以当前 GORM 事务创建 Repository。
|
||||
func (r *Repository) Create(ctx context.Context, instance *approvaldomain.Instance) error {
|
||||
if r == nil || r.db == nil {
|
||||
return stderrors.New("通用审批实例 Repository 未配置数据库会话")
|
||||
}
|
||||
if instance == nil {
|
||||
return stderrors.New("通用审批实例不能为空")
|
||||
}
|
||||
record := model.ApprovalInstance{
|
||||
BusinessType: instance.BusinessType, BusinessID: instance.BusinessID,
|
||||
SubmitterAccountID: instance.SubmitterAccountID, SubmitterSnapshot: datatypes.JSON(instance.SubmitterSnapshot),
|
||||
Provider: instance.Provider, ExternalRef: instance.ExternalRef, Status: instance.Status,
|
||||
RequestSnapshot: datatypes.JSON(instance.RequestSnapshot), DecisionSnapshot: datatypes.JSON(instance.DecisionSnapshot),
|
||||
CorrelationID: instance.CorrelationID, Version: instance.Version, StatusChangedAt: instance.StatusChangedAt,
|
||||
CreatedAt: instance.CreatedAt, UpdatedAt: instance.UpdatedAt,
|
||||
}
|
||||
if err := r.db.WithContext(ctx).Create(&record).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
instance.ID = record.ID
|
||||
return nil
|
||||
}
|
||||
38
internal/infrastructure/approval/submission_event.go
Normal file
38
internal/infrastructure/approval/submission_event.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package approval
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
approvalapp "github.com/break/junhong_cmp_fiber/internal/application/approval"
|
||||
"github.com/break/junhong_cmp_fiber/internal/infrastructure/messaging/outbox"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
)
|
||||
|
||||
// SubmissionEventWriter 将通用审批申请写入公共 Outbox。
|
||||
type SubmissionEventWriter struct {
|
||||
outbox *outbox.Repository
|
||||
}
|
||||
|
||||
// NewSubmissionEventWriter 创建通用审批申请 Outbox Writer。
|
||||
func NewSubmissionEventWriter(repository *outbox.Repository) *SubmissionEventWriter {
|
||||
return &SubmissionEventWriter{outbox: repository}
|
||||
}
|
||||
|
||||
// Append 在调用方业务事务中追加渠道提交事件。
|
||||
func (w *SubmissionEventWriter) Append(ctx context.Context, tx *gorm.DB, event approvalapp.SubmissionRequestedEvent) error {
|
||||
if w == nil || w.outbox == nil {
|
||||
return errors.New(errors.CodeInternalError, "审批申请 Outbox Writer 未配置")
|
||||
}
|
||||
_, err := w.outbox.Append(ctx, tx, outbox.Envelope{
|
||||
EventID: event.EventID, EventType: constants.OutboxEventTypeApprovalSubmissionRequested,
|
||||
PayloadVersion: constants.ApprovalSubmissionPayloadVersionV1,
|
||||
AggregateType: "approval", AggregateID: strconv.FormatUint(uint64(event.InstanceID), 10),
|
||||
ResourceType: event.BusinessType, ResourceID: strconv.FormatUint(uint64(event.BusinessID), 10),
|
||||
BusinessKey: event.EventID, CorrelationID: event.CorrelationID, Payload: event,
|
||||
})
|
||||
return err
|
||||
}
|
||||
73
internal/infrastructure/approval/terminal_event.go
Normal file
73
internal/infrastructure/approval/terminal_event.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package approval
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"gorm.io/gorm"
|
||||
|
||||
approvalapp "github.com/break/junhong_cmp_fiber/internal/application/approval"
|
||||
approvaldomain "github.com/break/junhong_cmp_fiber/internal/domain/approval"
|
||||
"github.com/break/junhong_cmp_fiber/internal/infrastructure/messaging/outbox"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
)
|
||||
|
||||
// TerminalEventWriter 将通用审批标准决策写入公共 Outbox。
|
||||
type TerminalEventWriter struct {
|
||||
outbox *outbox.Repository
|
||||
}
|
||||
|
||||
// NewTerminalEventWriter 创建通用审批标准决策 Outbox Writer。
|
||||
func NewTerminalEventWriter(repository *outbox.Repository) *TerminalEventWriter {
|
||||
return &TerminalEventWriter{outbox: repository}
|
||||
}
|
||||
|
||||
// Append 在审批状态事务中追加结构化标准决策事件。
|
||||
func (w *TerminalEventWriter) Append(ctx context.Context, tx *gorm.DB, event approvalapp.TerminalDecisionEvent) error {
|
||||
if w == nil || w.outbox == nil {
|
||||
return errors.New(errors.CodeInternalError, "审批标准决策 Outbox Writer 未配置")
|
||||
}
|
||||
_, err := w.outbox.Append(ctx, tx, outbox.Envelope{
|
||||
EventID: event.EventID, EventType: constants.OutboxEventTypeApprovalTerminalDecision,
|
||||
PayloadVersion: constants.ApprovalTerminalDecisionPayloadVersionV1,
|
||||
AggregateType: "approval", AggregateID: strconv.FormatUint(uint64(event.InstanceID), 10),
|
||||
ResourceType: event.BusinessType, ResourceID: strconv.FormatUint(uint64(event.BusinessID), 10),
|
||||
BusinessKey: event.EventID, CorrelationID: event.CorrelationID, Payload: event,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// TerminalDecisionConsumer 将公共 Outbox 信封转换为渠道无关业务决策。
|
||||
type TerminalDecisionConsumer struct {
|
||||
dispatcher *approvalapp.DecisionDispatcher
|
||||
}
|
||||
|
||||
// NewTerminalDecisionConsumer 创建审批标准决策消费者 Adapter。
|
||||
func NewTerminalDecisionConsumer(dispatcher *approvalapp.DecisionDispatcher) *TerminalDecisionConsumer {
|
||||
return &TerminalDecisionConsumer{dispatcher: dispatcher}
|
||||
}
|
||||
|
||||
// Consume 校验事件类型、版本和稳定身份后交给业务分发器。
|
||||
func (c *TerminalDecisionConsumer) Consume(ctx context.Context, envelope outbox.DeliveryEnvelope) error {
|
||||
if c == nil || c.dispatcher == nil {
|
||||
return errors.New(errors.CodeInternalError, "审批标准决策消费者未配置")
|
||||
}
|
||||
if envelope.EventType != constants.OutboxEventTypeApprovalTerminalDecision ||
|
||||
envelope.PayloadVersion != constants.ApprovalTerminalDecisionPayloadVersionV1 {
|
||||
return errors.New(errors.CodeInvalidParam, "审批标准决策事件类型或版本不受支持")
|
||||
}
|
||||
var event approvalapp.TerminalDecisionEvent
|
||||
if err := sonic.Unmarshal(envelope.Payload, &event); err != nil {
|
||||
return errors.Wrap(errors.CodeInvalidParam, err, "审批标准决策事件载荷格式错误")
|
||||
}
|
||||
if event.EventID == "" || event.EventID != envelope.EventID || event.InstanceID == 0 ||
|
||||
event.BusinessType == "" || event.BusinessID == 0 || event.CorrelationID == "" {
|
||||
return errors.New(errors.CodeInvalidParam, "审批标准决策事件载荷不完整")
|
||||
}
|
||||
if _, err := approvaldomain.StatusForDecision(event.Decision); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.dispatcher.Consume(ctx, event)
|
||||
}
|
||||
Reference in New Issue
Block a user