2026-07-26 14:03:32 +08:00
|
|
|
package domain
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
"unicode/utf8"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type TaskStatus string
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
TaskStatusPending TaskStatus = "PENDING"
|
|
|
|
|
TaskStatusClaimed TaskStatus = "CLAIMED"
|
|
|
|
|
TaskStatusRunning TaskStatus = "RUNNING"
|
|
|
|
|
TaskStatusWaitingConfirmation TaskStatus = "WAITING_CONFIRMATION"
|
|
|
|
|
TaskStatusSucceeded TaskStatus = "SUCCEEDED"
|
|
|
|
|
TaskStatusFailed TaskStatus = "FAILED"
|
|
|
|
|
TaskStatusCanceled TaskStatus = "CANCELED"
|
|
|
|
|
|
|
|
|
|
CurrencyCNY = "CNY"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
MaxTitleRunes = 120
|
|
|
|
|
MaxTitleBytes = 2048
|
|
|
|
|
MaxSKUBytes = 512
|
|
|
|
|
MaxDescriptionBytes = 8192
|
|
|
|
|
MaxSourceRefBytes = 256
|
|
|
|
|
MaxCancelReasonBytes = 500
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type PurchaseTask struct {
|
2026-07-26 16:16:36 +08:00
|
|
|
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
|
2026-07-26 14:03:32 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type TaskEvent struct {
|
2026-07-26 16:16:36 +08:00
|
|
|
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
|
2026-07-26 14:03:32 +08:00
|
|
|
}
|
|
|
|
|
|
2026-07-27 12:23:05 +08:00
|
|
|
type ExecutionEvent struct {
|
|
|
|
|
ID string
|
|
|
|
|
TaskID string
|
|
|
|
|
ExecutionID string
|
|
|
|
|
Step string
|
|
|
|
|
Type string
|
|
|
|
|
Message string
|
|
|
|
|
OccurredAt time.Time
|
|
|
|
|
ReceivedAt time.Time
|
|
|
|
|
ReceivedAfterExecutionExpiry bool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type ExecutionEvidenceAsset struct {
|
|
|
|
|
ID string
|
|
|
|
|
TaskID string
|
|
|
|
|
ExecutionID string
|
|
|
|
|
MediaType string
|
|
|
|
|
SizeBytes int64
|
|
|
|
|
SHA256 string
|
|
|
|
|
StorageKey string
|
|
|
|
|
CreatedAt time.Time
|
|
|
|
|
ReceivedAfterExecutionExpiry bool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type ExecutionCandidateBatch struct {
|
|
|
|
|
TaskID string
|
|
|
|
|
ExecutionID string
|
|
|
|
|
TaskContentSHA256 string
|
|
|
|
|
ExecutionMode string
|
|
|
|
|
SearchQuery string
|
|
|
|
|
ProvenanceJSON *string
|
|
|
|
|
CandidatesJSON string
|
|
|
|
|
RecommendationJSON *string
|
2026-07-28 12:50:42 +08:00
|
|
|
ReadyEvent *TaskEvent
|
2026-07-27 12:23:05 +08:00
|
|
|
ReceivedAt time.Time
|
|
|
|
|
ReceivedAfterExecutionExpiry bool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type ExecutionOutcome struct {
|
|
|
|
|
TaskID string
|
|
|
|
|
ExecutionID string
|
|
|
|
|
ResultType string
|
|
|
|
|
ExecutionMode *string
|
|
|
|
|
TaskContentSHA256 *string
|
|
|
|
|
Outcome *string
|
|
|
|
|
OperatorReason *string
|
|
|
|
|
SelectedCandidateJSON *string
|
|
|
|
|
EvidenceAssetIDsJSON *string
|
|
|
|
|
ErrorCode *string
|
|
|
|
|
ErrorMessage *string
|
|
|
|
|
ErrorStep *string
|
|
|
|
|
Retryable *bool
|
|
|
|
|
OrderSubmitted bool
|
|
|
|
|
ReceivedAt time.Time
|
|
|
|
|
ReceivedAfterExecutionExpiry bool
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-28 11:57:25 +08:00
|
|
|
type CandidateSearchRun struct {
|
|
|
|
|
TaskID string
|
|
|
|
|
ExecutionID string
|
|
|
|
|
TaskContentSHA256 string
|
|
|
|
|
ExecutionMode string
|
|
|
|
|
SearchQuery string
|
|
|
|
|
AppVersion *string
|
|
|
|
|
AndroidVersion *string
|
|
|
|
|
PDDVersion *string
|
|
|
|
|
StartedAt time.Time
|
|
|
|
|
ReceivedAt time.Time
|
|
|
|
|
ObservationCount int
|
|
|
|
|
CollectionComplete bool
|
|
|
|
|
ReceivedAfterExecutionExpiry bool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type CandidateObservation struct {
|
|
|
|
|
TaskID string
|
|
|
|
|
ExecutionID string
|
|
|
|
|
Ordinal int
|
|
|
|
|
Title string
|
|
|
|
|
SKUText string
|
|
|
|
|
PriceText string
|
|
|
|
|
ProductURL string
|
|
|
|
|
ImageURL string
|
|
|
|
|
EvidenceAssetIDs []string
|
|
|
|
|
CollectionStatus string
|
|
|
|
|
ObservedAt time.Time
|
2026-07-28 12:21:55 +08:00
|
|
|
Identity *CandidateObservationIdentity
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type CandidateObservationIdentity struct {
|
|
|
|
|
CandidateKey string
|
|
|
|
|
ExecutionID string
|
|
|
|
|
CandidateOrdinal int
|
|
|
|
|
CardSignature string
|
|
|
|
|
DetailSignature string
|
|
|
|
|
DetailEvidenceSHA256 string
|
|
|
|
|
SpecificationEvidenceSHA256 string
|
|
|
|
|
IdentityVersion int
|
|
|
|
|
CreatedAt time.Time
|
2026-07-28 11:57:25 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type CandidateModelRun struct {
|
|
|
|
|
ExecutionID string
|
|
|
|
|
ProviderID string
|
|
|
|
|
Model string
|
|
|
|
|
PromptVersion string
|
|
|
|
|
SchemaVersion int
|
|
|
|
|
RecommendationThreshold float64
|
|
|
|
|
RequestSHA256 string
|
|
|
|
|
ResultSHA256 string
|
|
|
|
|
CreatedAt time.Time
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type CandidateEvaluationRecord struct {
|
|
|
|
|
ExecutionID string
|
|
|
|
|
CandidateOrdinal int
|
|
|
|
|
Decision string
|
|
|
|
|
Score float64
|
|
|
|
|
Confidence float64
|
|
|
|
|
MatchedJSON string
|
|
|
|
|
MissingOrUncertainJSON string
|
|
|
|
|
RejectionReasonsJSON string
|
|
|
|
|
HardConstraintsJSON string
|
|
|
|
|
CreatedAt time.Time
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type CandidateRecommendationRecord struct {
|
|
|
|
|
ExecutionID string
|
|
|
|
|
CandidateOrdinal int
|
|
|
|
|
Conclusion string
|
|
|
|
|
PolicyVersion string
|
|
|
|
|
ReasonsJSON string
|
|
|
|
|
CreatedAt time.Time
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type CandidateHumanReview struct {
|
|
|
|
|
ID string
|
|
|
|
|
TaskID string
|
|
|
|
|
ExecutionID string
|
|
|
|
|
TaskContentSHA256 string
|
|
|
|
|
Version int
|
|
|
|
|
ReasonSchemaVersion int
|
|
|
|
|
Outcome string
|
|
|
|
|
SelectedCandidateOrdinal *int
|
|
|
|
|
PrimaryReasonCode string
|
|
|
|
|
Note string
|
|
|
|
|
SupersedesReviewID *string
|
|
|
|
|
ActorUserID string
|
|
|
|
|
ActorDeviceID *string
|
|
|
|
|
CreatedAt time.Time
|
|
|
|
|
ReceivedAfterExecutionExpiry bool
|
|
|
|
|
Items []CandidateHumanReviewItem
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type CandidateHumanReviewItem struct {
|
|
|
|
|
CandidateOrdinal int
|
|
|
|
|
Label string
|
|
|
|
|
PrimaryReasonCode string
|
|
|
|
|
ReasonCodes []string
|
|
|
|
|
Note string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type CandidateDecisionDataset struct {
|
|
|
|
|
SearchRun *CandidateSearchRun
|
|
|
|
|
Observations []CandidateObservation
|
|
|
|
|
ModelRun *CandidateModelRun
|
|
|
|
|
Evaluations []CandidateEvaluationRecord
|
|
|
|
|
Recommendation *CandidateRecommendationRecord
|
|
|
|
|
HumanReviews []CandidateHumanReview
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-28 12:50:42 +08:00
|
|
|
type OrderAuthorizationStatus string
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
OrderAuthorizationPendingDelivery OrderAuthorizationStatus = "PENDING_DELIVERY"
|
|
|
|
|
OrderAuthorizationDelivered OrderAuthorizationStatus = "DELIVERED"
|
|
|
|
|
OrderAuthorizationAcknowledged OrderAuthorizationStatus = "ACKNOWLEDGED"
|
|
|
|
|
OrderAuthorizationExecuting OrderAuthorizationStatus = "EXECUTING"
|
|
|
|
|
OrderAuthorizationConsumed OrderAuthorizationStatus = "CONSUMED"
|
|
|
|
|
OrderAuthorizationFailed OrderAuthorizationStatus = "FAILED"
|
|
|
|
|
OrderAuthorizationRevoked OrderAuthorizationStatus = "REVOKED"
|
|
|
|
|
OrderAuthorizationSuperseded OrderAuthorizationStatus = "SUPERSEDED"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type OrderAuthorization struct {
|
|
|
|
|
ID string
|
|
|
|
|
TaskID string
|
|
|
|
|
ExecutionID string
|
|
|
|
|
AuthorizationVersion int
|
|
|
|
|
CandidateKey string
|
|
|
|
|
TaskContentSHA256 string
|
|
|
|
|
TaskVersion int64
|
|
|
|
|
ReviewID string
|
|
|
|
|
ReviewVersion int
|
|
|
|
|
UserID string
|
|
|
|
|
DeviceID string
|
|
|
|
|
ClaimGeneration int64
|
|
|
|
|
OriginalSKU string
|
|
|
|
|
Quantity int
|
|
|
|
|
CandidateSKUText string
|
|
|
|
|
CandidatePriceText string
|
|
|
|
|
CardSignature string
|
|
|
|
|
DetailSignature string
|
|
|
|
|
DetailEvidenceSHA256 string
|
|
|
|
|
SpecificationEvidenceSHA256 string
|
|
|
|
|
Status OrderAuthorizationStatus
|
|
|
|
|
SupersedesAuthorizationID *string
|
|
|
|
|
CreatedByUserID string
|
|
|
|
|
CreatedAt time.Time
|
|
|
|
|
DeliveredAt *time.Time
|
|
|
|
|
AcknowledgedAt *time.Time
|
|
|
|
|
ExecutionStartedAt *time.Time
|
|
|
|
|
ConsumedAt *time.Time
|
|
|
|
|
FailedAt *time.Time
|
|
|
|
|
RevokedAt *time.Time
|
|
|
|
|
FailureCode *string
|
|
|
|
|
FailureMessage *string
|
2026-07-28 13:23:18 +08:00
|
|
|
CommandSHA256 *string
|
|
|
|
|
DeliveryAttemptCount int
|
|
|
|
|
LastDeliveredAt *time.Time
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type DeviceOrderCommand struct {
|
|
|
|
|
ID string
|
|
|
|
|
SchemaVersion int
|
|
|
|
|
Type string
|
|
|
|
|
AuthorizationVersion int
|
|
|
|
|
TaskID string
|
|
|
|
|
ExecutionID string
|
|
|
|
|
TaskContentSHA256 string
|
|
|
|
|
OriginalSKU string
|
|
|
|
|
Quantity int
|
|
|
|
|
CandidateKey string
|
|
|
|
|
ObservedOrdinal int
|
|
|
|
|
CandidateTitle string
|
|
|
|
|
CandidateSKUText string
|
|
|
|
|
CandidatePriceText string
|
|
|
|
|
CardSignature string
|
|
|
|
|
DetailSignature string
|
|
|
|
|
DetailEvidenceSHA256 string
|
|
|
|
|
SpecificationEvidenceSHA256 string
|
|
|
|
|
CommandSHA256 string
|
|
|
|
|
AuthorizationStatus OrderAuthorizationStatus
|
2026-07-28 12:50:42 +08:00
|
|
|
}
|
|
|
|
|
|
2026-07-28 16:52:29 +08:00
|
|
|
type OrderDryRunStatus string
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
OrderDryRunPreparing OrderDryRunStatus = "PREPARING"
|
|
|
|
|
OrderDryRunReady OrderDryRunStatus = "READY"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type OrderDryRun struct {
|
|
|
|
|
ID string
|
|
|
|
|
AuthorizationID string
|
|
|
|
|
TaskID string
|
|
|
|
|
ExecutionID string
|
|
|
|
|
CommandSHA256 string
|
|
|
|
|
Status OrderDryRunStatus
|
|
|
|
|
CardSignature *string
|
|
|
|
|
DetailSignature *string
|
|
|
|
|
ObservedTitle *string
|
|
|
|
|
SelectedSKU *string
|
|
|
|
|
Quantity *int
|
|
|
|
|
UnitPriceCents *int64
|
|
|
|
|
TotalPriceCents *int64
|
|
|
|
|
EvidenceAssetID *string
|
|
|
|
|
EvidenceSHA256 *string
|
|
|
|
|
StartedAt time.Time
|
|
|
|
|
ReadyAt *time.Time
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-28 17:48:54 +08:00
|
|
|
type OrderSubmissionStatus string
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
OrderSubmissionFenced OrderSubmissionStatus = "FENCED"
|
|
|
|
|
OrderSubmissionReconciled OrderSubmissionStatus = "RECONCILED"
|
|
|
|
|
OrderSubmissionManualReview OrderSubmissionStatus = "MANUAL_REVIEW"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type OrderSubmission struct {
|
|
|
|
|
ID string
|
|
|
|
|
AuthorizationID string
|
|
|
|
|
DryRunID string
|
|
|
|
|
TaskID string
|
|
|
|
|
ExecutionID string
|
|
|
|
|
CommandSHA256 string
|
|
|
|
|
DryRunEvidenceSHA256 string
|
|
|
|
|
Status OrderSubmissionStatus
|
|
|
|
|
ExpectedTitle string
|
|
|
|
|
ExpectedSKU string
|
|
|
|
|
ExpectedQuantity int
|
|
|
|
|
ExpectedUnitPriceCents int64
|
|
|
|
|
ExpectedTotalPriceCents int64
|
|
|
|
|
PlatformOrderNo *string
|
|
|
|
|
PlatformOrderedAt *time.Time
|
|
|
|
|
PlatformOrderStatus *string
|
|
|
|
|
ReconciliationEvidenceAssetID *string
|
|
|
|
|
ReconciliationEvidenceSHA256 *string
|
|
|
|
|
ManualReasonCode *string
|
|
|
|
|
FencedAt time.Time
|
|
|
|
|
ReconciledAt *time.Time
|
|
|
|
|
ManualReviewAt *time.Time
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-27 12:23:05 +08:00
|
|
|
type ExecutionReport struct {
|
2026-07-28 11:57:25 +08:00
|
|
|
Events []ExecutionEvent
|
|
|
|
|
EvidenceAssets []ExecutionEvidenceAsset
|
|
|
|
|
CandidateBatch *ExecutionCandidateBatch
|
|
|
|
|
DecisionDataset *CandidateDecisionDataset
|
|
|
|
|
Outcome *ExecutionOutcome
|
2026-07-27 12:23:05 +08:00
|
|
|
}
|
|
|
|
|
|
2026-07-26 14:03:32 +08:00
|
|
|
type TaskDetail struct {
|
2026-07-28 12:50:42 +08:00
|
|
|
Task PurchaseTask
|
|
|
|
|
Asset Asset
|
|
|
|
|
Execution *TaskExecution
|
|
|
|
|
Events []TaskEvent
|
|
|
|
|
Report *ExecutionReport
|
|
|
|
|
OrderAuthorizations []OrderAuthorization
|
2026-07-26 14:03:32 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type TaskValidationError struct {
|
|
|
|
|
Fields map[string]string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *TaskValidationError) Error() string {
|
|
|
|
|
return "purchase task validation failed"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func ValidateTaskInput(
|
|
|
|
|
creatorSubject string,
|
|
|
|
|
sourceRef *string,
|
|
|
|
|
title string,
|
|
|
|
|
description string,
|
|
|
|
|
sku string,
|
|
|
|
|
imageAssetID string,
|
|
|
|
|
quantity int,
|
|
|
|
|
) error {
|
|
|
|
|
fields := make(map[string]string)
|
|
|
|
|
if strings.TrimSpace(creatorSubject) == "" {
|
|
|
|
|
fields["creator_subject"] = "required"
|
|
|
|
|
}
|
|
|
|
|
if sourceRef != nil {
|
|
|
|
|
if strings.TrimSpace(*sourceRef) == "" {
|
|
|
|
|
fields["source_ref"] = "must not be blank"
|
|
|
|
|
} else if len([]byte(*sourceRef)) > MaxSourceRefBytes {
|
|
|
|
|
fields["source_ref"] = fmt.Sprintf(
|
|
|
|
|
"must not exceed %d UTF-8 bytes",
|
|
|
|
|
MaxSourceRefBytes,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(title) == "" {
|
|
|
|
|
fields["title"] = "required"
|
|
|
|
|
} else {
|
|
|
|
|
if utf8.RuneCountInString(title) > MaxTitleRunes {
|
|
|
|
|
fields["title"] = fmt.Sprintf(
|
|
|
|
|
"must not exceed %d characters",
|
|
|
|
|
MaxTitleRunes,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
if len([]byte(title)) > MaxTitleBytes {
|
|
|
|
|
fields["title"] = fmt.Sprintf(
|
|
|
|
|
"must not exceed %d UTF-8 bytes",
|
|
|
|
|
MaxTitleBytes,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if len([]byte(description)) > MaxDescriptionBytes {
|
|
|
|
|
fields["description"] = fmt.Sprintf(
|
|
|
|
|
"must not exceed %d UTF-8 bytes",
|
|
|
|
|
MaxDescriptionBytes,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(sku) == "" {
|
|
|
|
|
fields["sku"] = "required"
|
|
|
|
|
} else if len([]byte(sku)) > MaxSKUBytes {
|
|
|
|
|
fields["sku"] = fmt.Sprintf(
|
|
|
|
|
"must not exceed %d UTF-8 bytes",
|
|
|
|
|
MaxSKUBytes,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(imageAssetID) == "" {
|
|
|
|
|
fields["image_asset_id"] = "required"
|
|
|
|
|
}
|
|
|
|
|
if quantity <= 0 {
|
|
|
|
|
fields["quantity"] = "must be a positive integer"
|
|
|
|
|
}
|
|
|
|
|
if len(fields) > 0 {
|
|
|
|
|
return &TaskValidationError{Fields: fields}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func ParseOptionalCNY(value *string) (*int64, error) {
|
|
|
|
|
if value == nil {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
trimmed := strings.TrimSpace(*value)
|
|
|
|
|
if trimmed == "" {
|
|
|
|
|
return nil, errors.New("budget must be omitted or positive")
|
|
|
|
|
}
|
|
|
|
|
if strings.HasPrefix(trimmed, "+") || strings.HasPrefix(trimmed, "-") {
|
|
|
|
|
return nil, errors.New("budget must be positive")
|
|
|
|
|
}
|
|
|
|
|
parts := strings.Split(trimmed, ".")
|
|
|
|
|
if len(parts) > 2 || parts[0] == "" || len(parts[0]) > 16 {
|
|
|
|
|
return nil, errors.New("budget format is invalid")
|
|
|
|
|
}
|
|
|
|
|
if !allDigits(parts[0]) {
|
|
|
|
|
return nil, errors.New("budget format is invalid")
|
|
|
|
|
}
|
|
|
|
|
fraction := ""
|
|
|
|
|
if len(parts) == 2 {
|
|
|
|
|
fraction = parts[1]
|
|
|
|
|
if fraction == "" || len(fraction) > 2 || !allDigits(fraction) {
|
|
|
|
|
return nil, errors.New("budget format is invalid")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for len(fraction) < 2 {
|
|
|
|
|
fraction += "0"
|
|
|
|
|
}
|
|
|
|
|
whole, err := strconv.ParseInt(parts[0], 10, 64)
|
|
|
|
|
if err != nil || whole > (int64(^uint64(0)>>1)-99)/100 {
|
|
|
|
|
return nil, errors.New("budget is too large")
|
|
|
|
|
}
|
|
|
|
|
centsPart, err := strconv.ParseInt(fraction, 10, 64)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, errors.New("budget format is invalid")
|
|
|
|
|
}
|
|
|
|
|
cents := whole*100 + centsPart
|
|
|
|
|
if cents <= 0 {
|
|
|
|
|
return nil, errors.New("budget must be positive")
|
|
|
|
|
}
|
|
|
|
|
return ¢s, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func FormatOptionalCNY(cents *int64) *string {
|
|
|
|
|
if cents == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
value := fmt.Sprintf("%d.%02d", *cents/100, *cents%100)
|
|
|
|
|
return &value
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func CanCancel(status TaskStatus) bool {
|
2026-07-26 16:16:36 +08:00
|
|
|
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)
|
2026-07-26 14:03:32 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func IsValidTaskStatus(status TaskStatus) bool {
|
|
|
|
|
switch status {
|
|
|
|
|
case TaskStatusPending,
|
|
|
|
|
TaskStatusClaimed,
|
|
|
|
|
TaskStatusRunning,
|
|
|
|
|
TaskStatusWaitingConfirmation,
|
|
|
|
|
TaskStatusSucceeded,
|
|
|
|
|
TaskStatusFailed,
|
|
|
|
|
TaskStatusCanceled:
|
|
|
|
|
return true
|
|
|
|
|
default:
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func allDigits(value string) bool {
|
|
|
|
|
if value == "" {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
for _, char := range value {
|
|
|
|
|
if char < '0' || char > '9' {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
}
|