实现七月迭代公共技术基础
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m20s

This commit is contained in:
2026-07-23 17:52:48 +09:00
parent f7c42252c0
commit 17782d5f8e
76 changed files with 5246 additions and 158 deletions

View File

@@ -0,0 +1,117 @@
// 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
}