Skip to main content

sz_rust_workflow/instance/
models.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2024-2026 SZ-Rust Team
3//
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6
7/// 实例状态,对齐 spec 6.4.4。
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum InstanceStatus {
11    Created,
12    Running,
13    Suspended,
14    Terminated,
15    Completed,
16    Withdrawn,
17    Error,
18}
19
20impl InstanceStatus {
21    /// 是否可办理(接受事件与任务办理)。
22    pub fn is_handleable(&self) -> bool {
23        matches!(self, Self::Running)
24    }
25}
26
27impl std::fmt::Display for InstanceStatus {
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        f.write_str(match self {
30            Self::Created => "created",
31            Self::Running => "running",
32            Self::Suspended => "suspended",
33            Self::Terminated => "terminated",
34            Self::Completed => "completed",
35            Self::Withdrawn => "withdrawn",
36            Self::Error => "error",
37        })
38    }
39}
40
41/// 任务状态,对齐 spec 6.5。
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
43#[serde(rename_all = "snake_case")]
44pub enum TaskStatus {
45    Pending,
46    Completed,
47    Rejected,
48    Transferred,
49    Invalidated,
50}
51
52impl std::fmt::Display for TaskStatus {
53    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        f.write_str(match self {
55            Self::Pending => "pending",
56            Self::Completed => "completed",
57            Self::Rejected => "rejected",
58            Self::Transferred => "transferred",
59            Self::Invalidated => "invalidated",
60        })
61    }
62}
63
64/// 任务办理动作,对齐 spec 6.5。
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
66#[serde(rename_all = "snake_case")]
67pub enum TaskAction {
68    Approve,
69    Reject,
70    Transfer,
71    AddSign,
72    Withdraw,
73}
74
75/// 流程实例,对齐 design 2.3.2 类图。
76#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
77pub struct FlowInstance {
78    pub instance_id: String,
79    pub flow_key: String,
80    pub version: semver::Version,
81    pub status: InstanceStatus,
82    /// 当前所在节点(并行网关可能多节点)
83    pub current_nodes: Vec<String>,
84    /// 流程上下文
85    pub context: serde_json::Value,
86    /// 乐观锁版本号
87    pub version_lock: u64,
88    pub created_at: DateTime<Utc>,
89    pub updated_at: DateTime<Utc>,
90    pub initiator: String,
91}
92
93impl FlowInstance {
94    pub fn new(
95        instance_id: impl Into<String>,
96        flow_key: impl Into<String>,
97        version: semver::Version,
98        initiator: impl Into<String>,
99        context: serde_json::Value,
100        start_node: impl Into<String>,
101    ) -> Self {
102        let now = Utc::now();
103        Self {
104            instance_id: instance_id.into(),
105            flow_key: flow_key.into(),
106            version,
107            status: InstanceStatus::Running,
108            current_nodes: vec![start_node.into()],
109            context,
110            version_lock: 0,
111            created_at: now,
112            updated_at: now,
113            initiator: initiator.into(),
114        }
115    }
116
117    /// 自增版本锁并更新时间戳。
118    pub fn bump_version(&mut self) {
119        self.version_lock += 1;
120        self.updated_at = Utc::now();
121    }
122}
123
124/// 任务,对齐 spec 6.5。
125#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
126pub struct Task {
127    pub task_id: String,
128    pub instance_id: String,
129    pub node_id: String,
130    pub candidates: Vec<String>,
131    pub status: TaskStatus,
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub assignee: Option<String>,
134    #[serde(skip_serializing_if = "Option::is_none")]
135    pub action: Option<TaskAction>,
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub handled_at: Option<DateTime<Utc>>,
138    pub created_at: DateTime<Utc>,
139}
140
141impl Task {
142    pub fn new_pending(
143        task_id: impl Into<String>,
144        instance_id: impl Into<String>,
145        node_id: impl Into<String>,
146        candidates: Vec<String>,
147    ) -> Self {
148        Self {
149            task_id: task_id.into(),
150            instance_id: instance_id.into(),
151            node_id: node_id.into(),
152            candidates,
153            status: TaskStatus::Pending,
154            assignee: None,
155            action: None,
156            handled_at: None,
157            created_at: Utc::now(),
158        }
159    }
160}
161
162/// 审批记录,对齐 spec 6.6。
163#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
164pub struct ApprovalRecord {
165    pub record_id: String,
166    pub instance_id: String,
167    pub task_id: String,
168    pub node_id: String,
169    pub actor: String,
170    pub action: TaskAction,
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub comment: Option<String>,
173    #[serde(skip_serializing_if = "Option::is_none")]
174    pub target_user: Option<String>,
175    pub timestamp: DateTime<Utc>,
176}
177
178/// 历史条目类型。
179#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
180#[serde(rename_all = "snake_case")]
181pub enum HistoryEntryType {
182    Transition,
183    NodeEnter,
184    NodeLeave,
185    TaskHandled,
186    InstanceLifecycle,
187}
188
189/// 历史条目。
190#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
191pub struct HistoryEntry {
192    pub entry_id: String,
193    pub instance_id: String,
194    pub entry_type: HistoryEntryType,
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub from_node: Option<String>,
197    #[serde(skip_serializing_if = "Option::is_none")]
198    pub to_node: Option<String>,
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub from_state: Option<String>,
201    #[serde(skip_serializing_if = "Option::is_none")]
202    pub to_state: Option<String>,
203    pub context_snapshot: serde_json::Value,
204    pub timestamp: DateTime<Utc>,
205}
206
207impl HistoryEntry {
208    pub fn transition(
209        entry_id: impl Into<String>,
210        instance_id: impl Into<String>,
211        from_state: impl Into<String>,
212        to_state: impl Into<String>,
213        context_snapshot: serde_json::Value,
214    ) -> Self {
215        Self {
216            entry_id: entry_id.into(),
217            instance_id: instance_id.into(),
218            entry_type: HistoryEntryType::Transition,
219            from_node: None,
220            to_node: None,
221            from_state: Some(from_state.into()),
222            to_state: Some(to_state.into()),
223            context_snapshot,
224            timestamp: Utc::now(),
225        }
226    }
227
228    pub fn node_event(
229        entry_id: impl Into<String>,
230        instance_id: impl Into<String>,
231        from_node: Option<String>,
232        to_node: impl Into<String>,
233        entry_type: HistoryEntryType,
234    ) -> Self {
235        Self {
236            entry_id: entry_id.into(),
237            instance_id: instance_id.into(),
238            entry_type,
239            from_node,
240            to_node: Some(to_node.into()),
241            from_state: None,
242            to_state: None,
243            context_snapshot: serde_json::Value::Null,
244            timestamp: Utc::now(),
245        }
246    }
247}
248
249/// 分页请求。
250#[derive(Debug, Clone, Serialize, Deserialize)]
251pub struct PageRequest {
252    pub page: u32,
253    pub page_size: u32,
254}
255
256impl Default for PageRequest {
257    fn default() -> Self {
258        Self {
259            page: 1,
260            page_size: 20,
261        }
262    }
263}
264
265impl PageRequest {
266    pub fn offset(&self) -> usize {
267        ((self.page.saturating_sub(1)) as usize) * (self.page_size as usize)
268    }
269    pub fn limit(&self) -> usize {
270        self.page_size as usize
271    }
272}
273
274/// 分页结果。
275#[derive(Debug, Clone, Serialize)]
276pub struct PageResult<T> {
277    pub items: Vec<T>,
278    pub total: u64,
279    pub page: u32,
280    pub page_size: u32,
281}
282
283impl<T> PageResult<T> {
284    pub fn new(items: Vec<T>, total: u64, req: &PageRequest) -> Self {
285        Self {
286            items,
287            total,
288            page: req.page,
289            page_size: req.page_size,
290        }
291    }
292}
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297
298    #[test]
299    fn instance_status_is_handleable() {
300        assert!(InstanceStatus::Running.is_handleable());
301        assert!(!InstanceStatus::Suspended.is_handleable());
302        assert!(!InstanceStatus::Created.is_handleable());
303        assert!(!InstanceStatus::Terminated.is_handleable());
304    }
305
306    #[test]
307    fn flow_instance_new() {
308        let inst = FlowInstance::new(
309            "i1",
310            "leave",
311            semver::Version::new(1, 0, 0),
312            "user1",
313            serde_json::json!({}),
314            "start",
315        );
316        assert_eq!(inst.instance_id, "i1");
317        assert_eq!(inst.status, InstanceStatus::Running);
318        assert_eq!(inst.current_nodes, vec!["start"]);
319        assert_eq!(inst.version_lock, 0);
320    }
321
322    #[test]
323    fn flow_instance_bump_version() {
324        let mut inst = FlowInstance::new(
325            "i1",
326            "leave",
327            semver::Version::new(1, 0, 0),
328            "u1",
329            serde_json::json!({}),
330            "s",
331        );
332        let old = inst.updated_at;
333        std::thread::sleep(std::time::Duration::from_millis(1));
334        inst.bump_version();
335        assert_eq!(inst.version_lock, 1);
336        assert!(inst.updated_at > old);
337    }
338
339    #[test]
340    fn task_new_pending() {
341        let t = Task::new_pending("t1", "i1", "n1", vec!["u1".into(), "u2".into()]);
342        assert_eq!(t.status, TaskStatus::Pending);
343        assert_eq!(t.candidates, vec!["u1", "u2"]);
344        assert!(t.assignee.is_none());
345    }
346
347    #[test]
348    fn page_request_offset() {
349        let r = PageRequest {
350            page: 3,
351            page_size: 10,
352        };
353        assert_eq!(r.offset(), 20);
354        assert_eq!(r.limit(), 10);
355
356        let r2 = PageRequest::default();
357        assert_eq!(r2.offset(), 0);
358        assert_eq!(r2.limit(), 20);
359    }
360
361    #[test]
362    fn history_entry_transition() {
363        let e = HistoryEntry::transition("e1", "i1", "draft", "review", serde_json::json!({}));
364        assert_eq!(e.entry_type, HistoryEntryType::Transition);
365        assert_eq!(e.from_state.as_deref(), Some("draft"));
366        assert_eq!(e.to_state.as_deref(), Some("review"));
367    }
368
369    #[test]
370    fn history_entry_node_event() {
371        let e = HistoryEntry::node_event(
372            "e1",
373            "i1",
374            Some("n0".into()),
375            "n1",
376            HistoryEntryType::NodeEnter,
377        );
378        assert_eq!(e.entry_type, HistoryEntryType::NodeEnter);
379        assert_eq!(e.from_node.as_deref(), Some("n0"));
380        assert_eq!(e.to_node.as_deref(), Some("n1"));
381    }
382
383    #[test]
384    fn enums_serde() {
385        assert_eq!(
386            serde_json::to_string(&InstanceStatus::Running).unwrap(),
387            "\"running\""
388        );
389        assert_eq!(
390            serde_json::to_string(&TaskStatus::Pending).unwrap(),
391            "\"pending\""
392        );
393        assert_eq!(
394            serde_json::to_string(&TaskAction::Approve).unwrap(),
395            "\"approve\""
396        );
397    }
398}