package mysql import ( "context" "database/sql" "encoding/base64" "encoding/hex" "encoding/json" "time" "git.yuntuo.hk/k2safe/orbit/internal/fault" "git.yuntuo.hk/k2safe/orbit/internal/jsonvalue" core "git.yuntuo.hk/k2safe/orbit/model" "git.yuntuo.hk/k2safe/orbit/platform/auth" pulse "git.yuntuo.hk/k2safe/orbit/pulse/model" "git.yuntuo.hk/k2safe/orbit/pulse/service" ) // taskCursor 是已授权查询的排序位置,不能代替凭据;v2摘要绑定全部筛选。 type taskCursor struct { Version int `json:"v,omitempty"` Namespace core.ID `json:"n"` Queue core.ID `json:"q"` State pulse.TaskState `json:"s"` Created time.Time `json:"t"` ID core.ID `json:"i"` Filter string `json:"f,omitempty"` } func taskFilterDigest(in service.ListInput) (string, error) { digest, err := requestDigest(struct { Domain string `json:"domain"` Queue core.ID `json:"queue"` State pulse.TaskState `json:"state"` BusinessKey string `json:"business_key"` From *time.Time `json:"from"` Before *time.Time `json:"before"` }{"orbit.task-query.v2", in.QueueID, in.State, in.BusinessKey, in.CreatedFrom, in.CreatedBefore}) return hex.EncodeToString(digest[:]), err } func decodeTaskCursor(value string, ns core.ID, in service.ListInput, filter string) (taskCursor, error) { var c taskCursor invalid := fault.Invalid("cursor does not match this task query") raw, err := base64.RawURLEncoding.Strict().DecodeString(value) if err != nil || len(raw) > 768 { return c, invalid } if _, err = jsonvalue.Canonical(raw); err != nil { return c, invalid } var fields map[string]json.RawMessage if json.Unmarshal(raw, &fields) != nil || fields == nil { return c, invalid } for k, v := range fields { switch k { case "v", "n", "q", "s", "t", "i", "f": default: return c, invalid } if string(v) == "null" { return c, invalid } } for _, k := range []string{"n", "q", "s", "t", "i"} { if _, ok := fields[k]; !ok { return c, invalid } } if json.Unmarshal(raw, &c) != nil || c.Namespace != ns || c.Queue != in.QueueID || c.State != in.State || c.ID.Validate() != nil || c.Created.Year() < 2000 || c.Created.Year() > 9998 || c.Created.Nanosecond()%1000 != 0 { return c, invalid } if c.Version == 0 { // 旧版未带版本的游标仅兼容原队列/状态查询,不能附加新筛选。 if _, ok := fields["v"]; ok { return c, invalid } if _, ok := fields["f"]; ok { return c, invalid } if in.BusinessKey != "" || in.CreatedFrom != nil || in.CreatedBefore != nil { return c, invalid } } else if c.Version != 2 || c.Filter != filter { return c, invalid } if in.CreatedFrom != nil && c.Created.Before(*in.CreatedFrom) || in.CreatedBefore != nil && !c.Created.Before(*in.CreatedBefore) { return c, invalid } return c, nil } const taskSummaryColumns = `id,queue_id,state,revision,handler,handler_version,COALESCE(business_key,''),COALESCE(concurrency_key,''),priority,available_at,deadline_at,attempt_count,created_at,updated_at,effect_uncertain` // taskListSQL 选择固定索引名,等值筛选全部位于创建时间前;游标不依赖状态更新时间。 func taskListSQL(ns core.ID, in service.ListInput, filter string) (string, []any, error) { index := "ix_task_created" if in.State != "" { index = "ix_task_state_created" } if in.BusinessKey != "" { index = "ix_task_queue_business_created" if in.State != "" { index = "ix_task_queue_business_state_created" } } q := `SELECT ` + taskSummaryColumns + ` FROM pulse_tasks FORCE INDEX (` + index + `) WHERE namespace_id=? AND queue_id=?` args := []any{ns, in.QueueID} if in.BusinessKey != "" { q += " AND business_key=?" args = append(args, in.BusinessKey) } if in.State != "" { q += " AND state=?" args = append(args, in.State) } if in.CreatedFrom != nil { q += " AND created_at>=?" args = append(args, *in.CreatedFrom) } if in.CreatedBefore != nil { q += " AND created_at in.Limit { out.Items = out.Items[:in.Limit] last := out.Items[len(out.Items)-1] raw, err := json.Marshal(taskCursor{Version: 2, Namespace: token.NamespaceID, Queue: in.QueueID, State: in.State, Created: last.CreatedAt, ID: last.ID, Filter: filter}) if err != nil { return err } out.NextCursor = base64.RawURLEncoding.EncodeToString(raw) } return nil }) return out, err }