feat(tasks): implement atomic claims and leases
This commit is contained in:
@@ -160,18 +160,7 @@ func (h *adminHandlers) assetContent(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
defer result.Content.Close()
|
||||
|
||||
ctx.Header("Cache-Control", "private, no-store")
|
||||
ctx.Header("Content-Type", result.Asset.MediaType)
|
||||
ctx.Header("Content-Length", strconv.FormatInt(result.Asset.SizeBytes, 10))
|
||||
ctx.Header("ETag", `"`+result.Asset.SHA256+`"`)
|
||||
ctx.Header("X-Content-Type-Options", "nosniff")
|
||||
ctx.Header(
|
||||
"Content-Disposition",
|
||||
`inline; filename="`+result.Asset.ID+`.jpg"`,
|
||||
)
|
||||
ctx.Status(http.StatusOK)
|
||||
_, _ = io.Copy(ctx.Writer, result.Content)
|
||||
writeAssetContent(ctx, result)
|
||||
}
|
||||
|
||||
func (h *adminHandlers) createTask(ctx *gin.Context) {
|
||||
@@ -302,13 +291,32 @@ func (h *adminHandlers) taskDetail(ctx *gin.Context) {
|
||||
events := make([]gin.H, 0, len(detail.Events))
|
||||
for _, event := range detail.Events {
|
||||
events = append(events, gin.H{
|
||||
"id": event.ID,
|
||||
"actor_user_id": event.ActorUserID,
|
||||
"type": event.Type,
|
||||
"message": event.Message,
|
||||
"occurred_at": formatTime(event.OccurredAt),
|
||||
"id": event.ID,
|
||||
"actor_user_id": event.ActorUserID,
|
||||
"actor_device_id": event.ActorDeviceID,
|
||||
"type": event.Type,
|
||||
"message": event.Message,
|
||||
"occurred_at": formatTime(event.OccurredAt),
|
||||
})
|
||||
}
|
||||
var claim any
|
||||
if detail.Task.ClaimGeneration > 0 {
|
||||
claim = gin.H{
|
||||
"user_id": detail.Task.ClaimedByUserID,
|
||||
"device_id": detail.Task.ClaimedByDeviceID,
|
||||
"generation": detail.Task.ClaimGeneration,
|
||||
"issued_at": formatOptionalTime(detail.Task.ClaimIssuedAt),
|
||||
"expires_at": formatOptionalTime(detail.Task.ClaimExpiresAt),
|
||||
"cancel_requested_at": formatOptionalTime(
|
||||
detail.Task.CancelRequestedAt,
|
||||
),
|
||||
"cancel_requested_by_user_id": detail.Task.CancelRequestedByUserID,
|
||||
}
|
||||
}
|
||||
var execution any
|
||||
if detail.Execution != nil {
|
||||
execution = executionResponse(*detail.Execution)
|
||||
}
|
||||
ctx.Header("Cache-Control", "no-store")
|
||||
ctx.JSON(http.StatusOK, gin.H{
|
||||
"id": detail.Task.ID,
|
||||
@@ -327,8 +335,8 @@ func (h *adminHandlers) taskDetail(ctx *gin.Context) {
|
||||
"currency": detail.Task.Currency,
|
||||
},
|
||||
"derived_requirement": nil,
|
||||
"claim": nil,
|
||||
"execution": nil,
|
||||
"claim": claim,
|
||||
"execution": execution,
|
||||
"events": events,
|
||||
"assets": []gin.H{
|
||||
assetResponse(detail.Asset),
|
||||
@@ -441,6 +449,8 @@ func writeUsecaseError(ctx *gin.Context, err error) {
|
||||
}
|
||||
case usecase.ErrorKindNotFound:
|
||||
status = http.StatusNotFound
|
||||
case usecase.ErrorKindForbidden:
|
||||
status = http.StatusForbidden
|
||||
case usecase.ErrorKindConflict:
|
||||
status = http.StatusConflict
|
||||
case usecase.ErrorKindUnavailable:
|
||||
@@ -501,15 +511,19 @@ func assetResponse(asset domain.Asset) gin.H {
|
||||
|
||||
func taskSummaryResponse(task domain.PurchaseTask) gin.H {
|
||||
return gin.H{
|
||||
"id": task.ID,
|
||||
"status": task.Status,
|
||||
"title": task.Title,
|
||||
"sku": task.SKU,
|
||||
"quantity": task.Quantity,
|
||||
"max_budget": domain.FormatOptionalCNY(task.MaxBudgetCents),
|
||||
"created_at": formatTime(task.CreatedAt),
|
||||
"updated_at": formatTime(task.UpdatedAt),
|
||||
"version": task.Version,
|
||||
"id": task.ID,
|
||||
"status": task.Status,
|
||||
"title": task.Title,
|
||||
"sku": task.SKU,
|
||||
"quantity": task.Quantity,
|
||||
"max_budget": domain.FormatOptionalCNY(task.MaxBudgetCents),
|
||||
"created_at": formatTime(task.CreatedAt),
|
||||
"updated_at": formatTime(task.UpdatedAt),
|
||||
"version": task.Version,
|
||||
"cancel_requested": task.CancelRequestedAt != nil,
|
||||
"cancel_requested_at": formatOptionalTime(
|
||||
task.CancelRequestedAt,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -522,3 +536,41 @@ func taskListItemResponse(task domain.PurchaseTask) gin.H {
|
||||
func formatTime(value time.Time) string {
|
||||
return value.UTC().Format(time.RFC3339Nano)
|
||||
}
|
||||
|
||||
func formatOptionalTime(value *time.Time) any {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
return formatTime(*value)
|
||||
}
|
||||
|
||||
func executionResponse(execution domain.TaskExecution) gin.H {
|
||||
return gin.H{
|
||||
"id": execution.ID,
|
||||
"attempt_no": execution.AttemptNo,
|
||||
"claim_generation": execution.ClaimGeneration,
|
||||
"user_id": execution.UserID,
|
||||
"device_id": execution.DeviceID,
|
||||
"current_step": execution.CurrentStep,
|
||||
"order_submitted": execution.OrderSubmitted,
|
||||
"started_at": formatTime(execution.StartedAt),
|
||||
"last_heartbeat_at": formatOptionalTime(
|
||||
execution.LastHeartbeatAt,
|
||||
),
|
||||
"finished_at": formatOptionalTime(execution.FinishedAt),
|
||||
}
|
||||
}
|
||||
|
||||
func writeAssetContent(ctx *gin.Context, result usecase.AssetContent) {
|
||||
ctx.Header("Cache-Control", "private, no-store")
|
||||
ctx.Header("Content-Type", result.Asset.MediaType)
|
||||
ctx.Header("Content-Length", strconv.FormatInt(result.Asset.SizeBytes, 10))
|
||||
ctx.Header("ETag", `"`+result.Asset.SHA256+`"`)
|
||||
ctx.Header("X-Content-Type-Options", "nosniff")
|
||||
ctx.Header(
|
||||
"Content-Disposition",
|
||||
`inline; filename="`+result.Asset.ID+`.jpg"`,
|
||||
)
|
||||
ctx.Status(http.StatusOK)
|
||||
_, _ = io.Copy(ctx.Writer, result.Content)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user