package mysql import ( "context" "database/sql" "encoding/json" "errors" "strings" "git.yuntuo.hk/k2safe/orbit/internal/fault" core "git.yuntuo.hk/k2safe/orbit/model" "git.yuntuo.hk/k2safe/orbit/platform/auth" "git.yuntuo.hk/k2safe/orbit/pulse/service" "git.yuntuo.hk/k2safe/orbit/weave/instance" ) const workflowEventColumns = `e.id,e.aggregate_kind,e.aggregate_id,e.aggregate_revision,e.type,e.occurred_at,IF(OCTET_LENGTH(CAST(e.payload AS CHAR))<=65536,e.payload,NULL)` const workflowEventTables = ` FROM orbit_events e FORCE INDEX(ix_event_workflow_history) LEFT JOIN weave_steps s ON e.aggregate_kind='workflow_step' AND s.namespace_id=e.namespace_id AND s.id=e.aggregate_id AND s.workflow_id=e.workflow_id WHERE e.namespace_id=? AND e.workflow_id=? AND (e.aggregate_kind<>'workflow_step' OR s.id IS NULL OR s.key<>'$root')` type workflowEventParent struct { view instance.View steps map[core.ID]instance.StepView } func readWorkflowEventParent(ctx context.Context, tx *sql.Tx, ns, id core.ID, p auth.Permissions) (workflowEventParent, error) { var out workflowEventParent view, gate, ver, err := readWorkflowForQuery(ctx, tx, ns, id, p) if err != nil { return out, err } steps, err := readWorkflowStepViews(ctx, tx, ns, id, gate, ver, "") if err != nil { return out, err } out.view = view out.steps = map[core.ID]instance.StepView{} for _, step := range steps { out.steps[step.ID] = step } return out, nil } func scanWorkflowEvent(row scanner) (instance.EventDetail, error) { var out instance.EventDetail var raw []byte err := row.Scan(&out.ID, &out.AggregateKind, &out.AggregateID, &out.AggregateRevision, &out.Type, &out.OccurredAt, &raw) out.Payload = raw return out, err } func eventIDField(fields map[string]json.RawMessage, name string) (core.ID, error) { var id core.ID if json.Unmarshal(fields[name], &id) != nil || id.Validate() != nil { return "", errors.New("invalid workflow event reference") } return id, nil } func verifyWorkflowEvent(ctx context.Context, tx *sql.Tx, h workflowEventParent, e *instance.EventDetail) error { if e.ID.Validate() != nil || e.AggregateID.Validate() != nil || e.AggregateRevision == 0 || len(e.Payload) == 0 { return errors.New("workflow event metadata invalid or oversized") } var fields map[string]json.RawMessage if json.Unmarshal(e.Payload, &fields) != nil || fields == nil || json.Unmarshal(fields["state"], &e.State) != nil { return errors.New("workflow event state missing") } e.WorkflowID = h.view.ID switch e.AggregateKind { case "workflow": if e.AggregateID != h.view.ID || e.AggregateRevision > h.view.Revision { return errors.New("workflow event aggregate mismatch") } e.StateScope = "WORKFLOW" switch e.Type { case "workflow.started": def, err := eventIDField(fields, "definition_id") if err != nil || def != h.view.DefinitionID { return errors.New("workflow event definition mismatch") } version, err := eventIDField(fields, "definition_version_id") if err != nil || version != h.view.DefinitionVersionID { return errors.New("workflow event fixed version mismatch") } case "workflow.advanced", "workflow.controlled", "workflow.recovered", "workflow.blocked", "workflow.stopped": case "workflow.signal.accepted": e.StateScope = "SIGNAL" signal, err := eventIDField(fields, "signal_id") if err != nil { return err } if err = verifyEventSignal(ctx, tx, h, signal, 0); err != nil { return err } owner, err := eventIDField(fields, "workflow_id") if err != nil || owner != h.view.ID { return errors.New("workflow signal event scope mismatch") } default: return errors.New("unsupported workflow event type") } case "workflow_step": step, ok := h.steps[e.AggregateID] if !ok || e.AggregateRevision > step.Revision || e.Type != "workflow.step.changed" { return errors.New("workflow step event aggregate mismatch") } owner, err := eventIDField(fields, "workflow_id") if err != nil || owner != h.view.ID { return errors.New("workflow step event scope mismatch") } e.StepKey = step.Key e.StateScope = "STEP" if raw, ok := fields["key"]; ok { var key string if json.Unmarshal(raw, &key) != nil || key != step.Key { return errors.New("workflow event step key mismatch") } } if raw, ok := fields["activation_id"]; ok && string(raw) != "null" { activation, err := eventIDField(fields, "activation_id") if err != nil { return err } var count int if err = tx.QueryRowContext(ctx, `SELECT COUNT(*) FROM weave_activations WHERE namespace_id=? AND workflow_id=? AND step_id=? AND id=?`, h.view.NamespaceID, h.view.ID, step.ID, activation).Scan(&count); err != nil { return err } if count != 1 { return errors.New("workflow event historical activation mismatch") } } case "workflow_signal": e.StateScope = "SIGNAL" if e.Type != "workflow.signal.changed" { return errors.New("unsupported workflow signal event") } owner, err := eventIDField(fields, "workflow_id") if err != nil || owner != h.view.ID { return errors.New("workflow signal event scope mismatch") } signal, err := eventIDField(fields, "signal_id") if err != nil || signal != e.AggregateID { return errors.New("workflow event signal reference mismatch") } if err = verifyEventSignal(ctx, tx, h, signal, e.AggregateRevision); err != nil { return err } default: return errors.New("unsupported workflow event aggregate") } valid := false switch e.StateScope { case "WORKFLOW": switch e.State { case "RUNNING", "WAITING", "CANCELLING", "NEEDS_REVIEW", "SUCCEEDED", "FAILED", "CANCELLED": valid = true } case "STEP": switch e.State { case "BLOCKED", "ACTIVE", "WAITING", "NEEDS_REVIEW", "SUCCEEDED", "FAILED", "CANCELLED", "SKIPPED": valid = true } case "SIGNAL": switch e.State { case "PENDING", "CONSUMED", "EXPIRED", "REJECTED": valid = true } } if !valid { return errors.New("workflow event recorded state invalid") } return nil } func verifyEventSignal(ctx context.Context, tx *sql.Tx, h workflowEventParent, id core.ID, revision uint64) error { var actual uint64 err := tx.QueryRowContext(ctx, `SELECT revision FROM weave_signals WHERE namespace_id=? AND workflow_id=? AND id=?`, h.view.NamespaceID, h.view.ID, id).Scan(&actual) if err != nil { return err } if revision > actual { return errors.New("workflow signal event version mismatch") } return nil } func (s *WorkflowInstanceStore) ListEvents(ctx context.Context, t auth.Token, id core.ID, in service.HistoryPageInput) (instance.EventPage, error) { out := instance.EventPage{WorkflowID: id, Items: []instance.EventSummary{}} err := s.transaction(ctx, t, instance.AuditAction, func(tx *sql.Tx, p auth.Permissions) error { h, err := readWorkflowEventParent(ctx, tx, t.NamespaceID, id, p) if err != nil { return err } out.Revision = h.view.Revision q := `SELECT ` + workflowEventColumns + workflowEventTables args := []any{t.NamespaceID, id} if in.Cursor != "" { c, err := decodeHistoryCursorKind(in.Cursor, t.NamespaceID, id, "", "workflow-events") if err != nil { return err } q += ` AND (e.occurred_at in.Limit if more { entries = entries[:in.Limit] } for _, e := range entries { out.Items = append(out.Items, e.EventSummary) } if more { last := out.Items[len(out.Items)-1] out.NextCursor, err = encodeHistoryCursor(historyCursor{Version: 1, Kind: "workflow-events", Namespace: t.NamespaceID, Task: id, ID: last.ID, Created: &last.OccurredAt}) } return err }) if err != nil { return instance.EventPage{}, err } return out, nil } func (s *WorkflowInstanceStore) GetEvent(ctx context.Context, t auth.Token, id, event core.ID) (instance.EventDetail, error) { var out instance.EventDetail err := s.transaction(ctx, t, instance.AuditAction, func(tx *sql.Tx, p auth.Permissions) error { h, err := readWorkflowEventParent(ctx, tx, t.NamespaceID, id, p) if err != nil { return err } // 明确事件ID后走主键;仍保留流程派生范围和实际归属核验。 q := `SELECT ` + workflowEventColumns + strings.Replace(workflowEventTables, "FORCE INDEX(ix_event_workflow_history)", "FORCE INDEX(PRIMARY)", 1) + ` AND e.id=?` out, err = scanWorkflowEvent(tx.QueryRowContext(ctx, q, t.NamespaceID, id, event)) if errors.Is(err, sql.ErrNoRows) { return fault.NotFound } if err != nil { return err } return verifyWorkflowEvent(ctx, tx, h, &out) }) if err != nil { return instance.EventDetail{}, err } return out, nil }