Files
cmroubao/backend-api/internal/domain/task.go
T

601 lines
16 KiB
Go
Raw Normal View History

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 {
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
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-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
}
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
Identity *CandidateObservationIdentity
}
type CandidateObservationIdentity struct {
CandidateKey string
ExecutionID string
CandidateOrdinal int
CardSignature string
DetailSignature string
DetailEvidenceSHA256 string
SpecificationEvidenceSHA256 string
IdentityVersion int
CreatedAt time.Time
}
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
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
}
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
}
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 {
Events []ExecutionEvent
EvidenceAssets []ExecutionEvidenceAsset
CandidateBatch *ExecutionCandidateBatch
DecisionDataset *CandidateDecisionDataset
Outcome *ExecutionOutcome
2026-07-27 12:23:05 +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
OrderSubmissions []OrderSubmission
}
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 &cents, 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 {
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 {
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
}