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