feat(tasks): implement atomic claims and leases

This commit is contained in:
QiuSW
2026-07-26 16:16:36 +08:00
parent 49db5b8305
commit ce875af889
50 changed files with 7589 additions and 207 deletions
+14 -11
View File
@@ -32,17 +32,20 @@ type User struct {
}
type Device struct {
ID string
Name string
TokenHash string
BoundUserID *string
AppVersion *string
AndroidVersion *string
PDDVersion *string
LastSeenAt *time.Time
IsEnabled bool
CreatedAt time.Time
UpdatedAt time.Time
ID string
Name string
TokenHash string
BoundUserID *string
AppVersion *string
AndroidVersion *string
PDDVersion *string
LastSeenAt *time.Time
ReadinessAt *time.Time
AccessibilityEnabled bool
PDDInstalled bool
IsEnabled bool
CreatedAt time.Time
UpdatedAt time.Time
}
type AdminSession struct {
@@ -0,0 +1,45 @@
package domain
import "testing"
func TestLifecycleStatusPredicates(t *testing.T) {
statuses := []TaskStatus{
TaskStatusPending,
TaskStatusClaimed,
TaskStatusRunning,
TaskStatusWaitingConfirmation,
TaskStatusSucceeded,
TaskStatusFailed,
TaskStatusCanceled,
}
for _, status := range statuses {
t.Run(string(status), func(t *testing.T) {
active := status == TaskStatusClaimed ||
status == TaskStatusRunning ||
status == TaskStatusWaitingConfirmation
if IsActiveTaskStatus(status) != active {
t.Fatalf("IsActiveTaskStatus(%s) = %v", status, !active)
}
if CanStart(status) != (status == TaskStatusClaimed) {
t.Fatalf("CanStart(%s) mismatch", status)
}
heartbeat := status == TaskStatusRunning ||
status == TaskStatusWaitingConfirmation
if CanHeartbeat(status) != heartbeat {
t.Fatalf("CanHeartbeat(%s) mismatch", status)
}
if CanRelease(status) != (status == TaskStatusClaimed) {
t.Fatalf("CanRelease(%s) mismatch", status)
}
immediateCancel := status == TaskStatusPending ||
status == TaskStatusClaimed
if CanAdminCancelImmediately(status) != immediateCancel {
t.Fatalf("CanAdminCancelImmediately(%s) mismatch", status)
}
if CanRequestCancel(status) != heartbeat ||
CanAcknowledgeCancel(status) != heartbeat {
t.Fatalf("cancel predicates for %s mismatch", status)
}
})
}
}
+89 -27
View File
@@ -33,38 +33,62 @@ const (
)
type PurchaseTask struct {
ID string
CreatorSubject string
CreatedByUserID *string
SourceRef *string
Title string
Description string
SKU string
ImageAssetID string
Quantity int
MaxBudgetCents *int64
Currency string
Status TaskStatus
Version int64
CancelReason *string
CanceledAt *time.Time
CreatedAt time.Time
UpdatedAt time.Time
ID string
CreatorSubject string
CreatedByUserID *string
SourceRef *string
Title string
Description string
SKU string
ImageAssetID string
Quantity int
MaxBudgetCents *int64
Currency string
Status TaskStatus
Version int64
ClaimedByUserID *string
ClaimedByDeviceID *string
ClaimGeneration int64
ClaimTokenHash *string
ClaimIssuedAt *time.Time
ClaimExpiresAt *time.Time
CancelReason *string
CancelRequestedAt *time.Time
CancelRequestedByUserID *string
CanceledAt *time.Time
CreatedAt time.Time
UpdatedAt time.Time
}
type TaskEvent struct {
ID string
TaskID string
ActorUserID *string
Type string
Message string
OccurredAt time.Time
ID string
TaskID string
ActorUserID *string
ActorDeviceID *string
Type string
Message string
OccurredAt time.Time
}
type TaskExecution struct {
ID string
TaskID string
AttemptNo int64
ClaimGeneration int64
UserID string
DeviceID string
CurrentStep string
OrderSubmitted bool
StartedAt time.Time
LastHeartbeatAt *time.Time
FinishedAt *time.Time
}
type TaskDetail struct {
Task PurchaseTask
Asset Asset
Events []TaskEvent
Task PurchaseTask
Asset Asset
Execution *TaskExecution
Events []TaskEvent
}
type TaskValidationError struct {
@@ -192,7 +216,45 @@ func FormatOptionalCNY(cents *int64) *string {
}
func CanCancel(status TaskStatus) bool {
return status == TaskStatusPending
return CanAdminCancelImmediately(status) || CanRequestCancel(status)
}
func IsActiveTaskStatus(status TaskStatus) bool {
switch status {
case TaskStatusClaimed,
TaskStatusRunning,
TaskStatusWaitingConfirmation:
return true
default:
return false
}
}
func CanStart(status TaskStatus) bool {
return status == TaskStatusClaimed
}
func CanHeartbeat(status TaskStatus) bool {
return status == TaskStatusRunning ||
status == TaskStatusWaitingConfirmation
}
func CanRelease(status TaskStatus) bool {
return status == TaskStatusClaimed
}
func CanAdminCancelImmediately(status TaskStatus) bool {
return status == TaskStatusPending ||
status == TaskStatusClaimed
}
func CanRequestCancel(status TaskStatus) bool {
return status == TaskStatusRunning ||
status == TaskStatusWaitingConfirmation
}
func CanAcknowledgeCancel(status TaskStatus) bool {
return CanRequestCancel(status)
}
func IsValidTaskStatus(status TaskStatus) bool {
+10 -6
View File
@@ -107,11 +107,8 @@ func TestParseOptionalCNYRejectsInvalidAndOverflow(t *testing.T) {
}
}
func TestCanCancelOnlyPending(t *testing.T) {
func TestCanCancelOnlyNonTerminalTask(t *testing.T) {
for _, status := range []TaskStatus{
TaskStatusClaimed,
TaskStatusRunning,
TaskStatusWaitingConfirmation,
TaskStatusSucceeded,
TaskStatusFailed,
TaskStatusCanceled,
@@ -120,8 +117,15 @@ func TestCanCancelOnlyPending(t *testing.T) {
t.Fatalf("CanCancel(%s) = true", status)
}
}
if !CanCancel(TaskStatusPending) {
t.Fatal("CanCancel(PENDING) = false")
for _, status := range []TaskStatus{
TaskStatusPending,
TaskStatusClaimed,
TaskStatusRunning,
TaskStatusWaitingConfirmation,
} {
if !CanCancel(status) {
t.Fatalf("CanCancel(%s) = false", status)
}
}
}