暂存一下,防止丢失

This commit is contained in:
2026-07-24 16:07:18 +08:00
parent 5d6e23f1a5
commit a18ed8bc8d
180 changed files with 13597 additions and 1986 deletions

View 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
}

View 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
}

View 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
}

View 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)
}