// Package asynctask 提供业务自有任务表复用的 PostgreSQL 条件更新 Adapter。 package asynctask import ( "context" stderrors "errors" "regexp" "time" "gorm.io/gorm" contract "github.com/break/junhong_cmp_fiber/pkg/asynctask" ) var identifierPattern = regexp.MustCompile(`^[a-z][a-z0-9_]*$`) // Definition 描述业务自有任务表如何映射统一字段。 type Definition struct { Table string IDColumn string StatusColumn string LeaseOwnerColumn string LeaseExpiresColumn string TotalColumn string SuccessColumn string FailedColumn string ProgressColumn string ErrorCodeColumn string ErrorSummaryColumn string StartedAtColumn string CompletedAtColumn string UpdatedAtColumn string } // Store 对一个已注册的业务任务表执行统一条件更新。 type Store struct { db *gorm.DB definition Definition } // NewStore 创建业务任务表 Adapter,不创建或迁移任何万能任务表。 func NewStore(db *gorm.DB, definition Definition) (*Store, error) { columns := []string{ definition.Table, definition.IDColumn, definition.StatusColumn, definition.LeaseOwnerColumn, definition.LeaseExpiresColumn, definition.TotalColumn, definition.SuccessColumn, definition.FailedColumn, definition.ProgressColumn, definition.ErrorCodeColumn, definition.ErrorSummaryColumn, definition.StartedAtColumn, definition.CompletedAtColumn, definition.UpdatedAtColumn, } for _, identifier := range columns { if !identifierPattern.MatchString(identifier) { return nil, stderrors.New("异步任务表或字段标识不合法") } } return &Store{db: db, definition: definition}, nil } // Claim 使用预期状态和过期租约条件领取任务。 func (s *Store) Claim(ctx context.Context, id any, owner string, now time.Time, duration time.Duration) (bool, error) { if owner == "" || duration <= 0 { return false, stderrors.New("任务租约所有者和时长不能为空") } d := s.definition result := s.db.WithContext(ctx).Table(d.Table). Where(d.IDColumn+" = ? AND ("+d.StatusColumn+" = ? OR ("+d.StatusColumn+" = ? AND "+d.LeaseExpiresColumn+" <= ?))", id, contract.StatusPending, contract.StatusProcessing, now). Updates(map[string]any{ d.StatusColumn: contract.StatusProcessing, d.LeaseOwnerColumn: owner, d.LeaseExpiresColumn: now.Add(duration), d.StartedAtColumn: gorm.Expr("COALESCE("+d.StartedAtColumn+", ?)", now), d.UpdatedAtColumn: now, }) return result.RowsAffected == 1, result.Error } // Renew 只允许当前所有者在租约有效时续期处理中任务。 func (s *Store) Renew(ctx context.Context, id any, owner string, now time.Time, duration time.Duration) (bool, error) { if owner == "" || duration <= 0 { return false, stderrors.New("任务租约所有者和时长不能为空") } d := s.definition updated := s.db.WithContext(ctx).Table(d.Table). Where(d.IDColumn+" = ? AND "+d.StatusColumn+" = ? AND "+d.LeaseOwnerColumn+" = ? AND "+d.LeaseExpiresColumn+" > ?", id, contract.StatusProcessing, owner, now). Updates(map[string]any{d.LeaseExpiresColumn: now.Add(duration), d.UpdatedAtColumn: now}) return updated.RowsAffected == 1, updated.Error } // Finish 只允许有效租约所有者把处理中任务推进到统一终态。 func (s *Store) Finish(ctx context.Context, id any, owner string, result contract.TerminalResult, now time.Time) (bool, error) { projection, err := contract.NewTerminalProjection(result) if err != nil { return false, err } d := s.definition updated := s.db.WithContext(ctx).Table(d.Table). Where(d.IDColumn+" = ? AND "+d.StatusColumn+" = ? AND "+d.LeaseOwnerColumn+" = ? AND "+d.LeaseExpiresColumn+" > ?", id, contract.StatusProcessing, owner, now). Updates(map[string]any{ d.StatusColumn: projection.Status, d.TotalColumn: projection.TotalCount, d.SuccessColumn: projection.SuccessCount, d.FailedColumn: projection.FailedCount, d.ProgressColumn: projection.Progress, d.ErrorCodeColumn: projection.ErrorCode, d.ErrorSummaryColumn: projection.ErrorSummary, d.CompletedAtColumn: now, d.LeaseOwnerColumn: nil, d.LeaseExpiresColumn: nil, d.UpdatedAtColumn: now, }) return updated.RowsAffected == 1, updated.Error } // Cancel 只允许业务明确支持时从待处理或处理中进入已取消。 func (s *Store) Cancel(ctx context.Context, id any, now time.Time) (bool, error) { d := s.definition updated := s.db.WithContext(ctx).Table(d.Table). Where(d.IDColumn+" = ? AND "+d.StatusColumn+" IN ?", id, []int{contract.StatusPending, contract.StatusProcessing}). Updates(map[string]any{ d.StatusColumn: contract.StatusCancelled, d.ProgressColumn: 100, d.CompletedAtColumn: now, d.LeaseOwnerColumn: nil, d.LeaseExpiresColumn: nil, d.UpdatedAtColumn: now, }) return updated.RowsAffected == 1, updated.Error }