Skip to main content

openlark_workflow/
service.rs

1// approval v4 用户级接口(用户态,需 user_access_token)
2
3// approval v4 用户级接口的公开类型重新导出
4// 这些 Request/Body/Response 类型供用户直接 new() + builder + execute() 使用
5// (用户态接口需 user_access_token,不适合封装成 service helper)
6pub use crate::approval::approval::v4::instance::add_cc::{
7    AddCcInstanceBodyV4, AddCcInstanceRequestV4, AddCcInstanceResponseV4,
8};
9pub use crate::approval::approval::v4::instance::detail::{
10    DetailInstanceRequestV4, DetailInstanceResponseV4, DetailInstanceTaskV4,
11};
12pub use crate::approval::approval::v4::instance::initiated::{
13    InitiatedInstanceItemV4, InitiatedInstanceRequestV4, InitiatedInstanceResponseV4,
14    InstanceSummaryV4,
15};
16pub use crate::approval::approval::v4::instance::recall::{
17    RecallInstanceBodyV4, RecallInstanceRequestV4, RecallInstanceResponseV4,
18};
19pub use crate::approval::approval::v4::instance::remind::{
20    RemindInstanceBodyV4, RemindInstanceRequestV4, RemindInstanceResponseV4,
21};
22pub use crate::approval::approval::v4::task::add_sign::{
23    AddSignTaskBodyV4, AddSignTaskRequestV4, AddSignTaskResponseV4,
24};
25pub use crate::approval::approval::v4::task::forward::{
26    ForwardTaskBodyV4, ForwardTaskRequestV4, ForwardTaskResponseV4,
27};
28pub use crate::approval::approval::v4::task::list::{
29    ListTaskItemV4, ListTaskRequestV4, ListTaskResponseV4, TaskSummaryV4,
30};
31pub use crate::approval::approval::v4::task::pass::{
32    PassTaskBodyV4, PassTaskRequestV4, PassTaskResponseV4,
33};
34pub use crate::approval::approval::v4::task::refuse::{
35    RefuseTaskBodyV4, RefuseTaskRequestV4, RefuseTaskResponseV4,
36};
37pub use crate::approval::approval::v4::task::rollback::{
38    RollbackTaskBodyV4, RollbackTaskRequestV4, RollbackTaskResponseV4,
39};
40
41use openlark_core::{SDKResult, config::Config};
42use std::sync::Arc;
43
44use crate::common::constants::MAX_PAGE_SIZE;
45
46/// 任务列表查询 helper。
47///
48/// 用于封装常见的任务列表过滤条件,并让 helper 统一处理分页。
49#[derive(Debug, Clone, Default, PartialEq)]
50pub struct WorkflowTaskListQuery {
51    /// 任务清单 GUID。
52    pub tasklist_guid: Option<String>,
53    /// 分组 GUID。
54    pub section_guid: Option<String>,
55    /// 过滤条件。
56    pub filter: Option<String>,
57    /// 排序条件。
58    pub sort: Option<serde_json::Value>,
59    /// 用户 ID 类型。
60    pub user_type: Option<String>,
61    /// 分页大小。
62    pub page_size: Option<i32>,
63}
64
65impl WorkflowTaskListQuery {
66    /// 为指定任务清单创建查询条件。
67    pub fn for_tasklist(tasklist_guid: impl Into<String>) -> Self {
68        Self {
69            tasklist_guid: Some(tasklist_guid.into()),
70            ..Self::default()
71        }
72    }
73
74    /// 设置分组 GUID。
75    pub fn section_guid(mut self, section_guid: impl Into<String>) -> Self {
76        self.section_guid = Some(section_guid.into());
77        self
78    }
79
80    /// 设置过滤条件。
81    pub fn filter(mut self, filter: impl Into<String>) -> Self {
82        self.filter = Some(filter.into());
83        self
84    }
85
86    /// 设置排序条件。
87    pub fn sort(mut self, sort: serde_json::Value) -> Self {
88        self.sort = Some(sort);
89        self
90    }
91
92    /// 设置用户 ID 类型。
93    pub fn user_type(mut self, user_type: impl Into<String>) -> Self {
94        self.user_type = Some(user_type.into());
95        self
96    }
97
98    /// 设置分页大小。
99    pub fn page_size(mut self, page_size: i32) -> Self {
100        self.page_size = Some(page_size);
101        self
102    }
103}
104
105/// 任务创建 helper。
106///
107/// 只覆盖高频创建字段(标题、描述、截止、优先级、执行者、所属清单等),
108/// 不试图替代完整 typed `CreateTaskRequest`(自定义字段 / 子任务 / 重复规则等仍走 typed API)。
109#[derive(Debug, Clone, PartialEq)]
110pub struct WorkflowTaskCreate {
111    /// 任务标题(必填)。
112    pub summary: String,
113    /// 任务描述。
114    pub description: Option<String>,
115    /// 开始时间。
116    pub start: Option<String>,
117    /// 截止时间。
118    pub due: Option<String>,
119    /// 优先级。
120    pub priority: Option<i32>,
121    /// 执行者。
122    pub assignee: Option<String>,
123    /// 任务清单 GUID。
124    pub tasklist_guid: Option<String>,
125    /// 分组 GUID。
126    pub section_guid: Option<String>,
127    /// 关注者。
128    pub followers: Option<Vec<String>>,
129    /// 提醒时间。
130    pub remind_time: Option<String>,
131}
132
133impl WorkflowTaskCreate {
134    /// 以必填标题创建任务描述。
135    pub fn new(summary: impl Into<String>) -> Self {
136        Self {
137            summary: summary.into(),
138            description: None,
139            start: None,
140            due: None,
141            priority: None,
142            assignee: None,
143            tasklist_guid: None,
144            section_guid: None,
145            followers: None,
146            remind_time: None,
147        }
148    }
149
150    /// 设置任务描述。
151    pub fn description(mut self, description: impl Into<String>) -> Self {
152        self.description = Some(description.into());
153        self
154    }
155
156    /// 设置开始时间。
157    pub fn start(mut self, start: impl Into<String>) -> Self {
158        self.start = Some(start.into());
159        self
160    }
161
162    /// 设置截止时间。
163    pub fn due(mut self, due: impl Into<String>) -> Self {
164        self.due = Some(due.into());
165        self
166    }
167
168    /// 设置优先级。
169    pub fn priority(mut self, priority: i32) -> Self {
170        self.priority = Some(priority);
171        self
172    }
173
174    /// 设置执行者。
175    pub fn assignee(mut self, assignee: impl Into<String>) -> Self {
176        self.assignee = Some(assignee.into());
177        self
178    }
179
180    /// 设置任务清单 GUID。
181    pub fn tasklist_guid(mut self, tasklist_guid: impl Into<String>) -> Self {
182        self.tasklist_guid = Some(tasklist_guid.into());
183        self
184    }
185
186    /// 设置分组 GUID。
187    pub fn section_guid(mut self, section_guid: impl Into<String>) -> Self {
188        self.section_guid = Some(section_guid.into());
189        self
190    }
191
192    /// 设置关注者列表。
193    pub fn followers(mut self, followers: Vec<String>) -> Self {
194        self.followers = Some(followers);
195        self
196    }
197
198    /// 设置提醒时间。
199    pub fn remind_time(mut self, remind_time: impl Into<String>) -> Self {
200        self.remind_time = Some(remind_time.into());
201        self
202    }
203}
204
205/// 任务变更 helper。
206///
207/// 只覆盖高频可变字段,不试图替代完整 typed request。
208#[derive(Debug, Clone, Default, PartialEq)]
209pub struct WorkflowTaskMutation {
210    /// 任务标题。
211    pub summary: Option<String>,
212    /// 任务描述。
213    pub description: Option<String>,
214    /// 截止时间。
215    pub due: Option<String>,
216    /// 优先级。
217    pub priority: Option<i32>,
218    /// 执行者。
219    pub assignee: Option<String>,
220    /// 状态。
221    pub status: Option<String>,
222}
223
224impl WorkflowTaskMutation {
225    /// 创建空的任务变更描述。
226    pub fn new() -> Self {
227        Self::default()
228    }
229
230    /// 设置任务标题。
231    pub fn summary(mut self, summary: impl Into<String>) -> Self {
232        self.summary = Some(summary.into());
233        self
234    }
235
236    /// 设置任务描述。
237    pub fn description(mut self, description: impl Into<String>) -> Self {
238        self.description = Some(description.into());
239        self
240    }
241
242    /// 设置截止时间。
243    pub fn due(mut self, due: impl Into<String>) -> Self {
244        self.due = Some(due.into());
245        self
246    }
247
248    /// 设置优先级。
249    pub fn priority(mut self, priority: i32) -> Self {
250        self.priority = Some(priority);
251        self
252    }
253
254    /// 设置执行者。
255    pub fn assignee(mut self, assignee: impl Into<String>) -> Self {
256        self.assignee = Some(assignee.into());
257        self
258    }
259
260    /// 设置审批状态。
261    /// 设置任务状态。
262    pub fn status(mut self, status: impl Into<String>) -> Self {
263        self.status = Some(status.into());
264        self
265    }
266}
267
268/// 审批任务查询 helper。
269///
270/// 用于封装审批待办的常见筛选条件。
271#[derive(Debug, Clone, Default, PartialEq)]
272pub struct ApprovalTaskQuery {
273    /// 用户 ID。
274    pub user_id: String,
275    /// 审批主题。
276    pub topic: String,
277    /// 用户 ID 类型。
278    pub user_id_type: Option<String>,
279    /// 审批状态。
280    pub status: Option<String>,
281    /// 实例编码。
282    pub instance_code: Option<String>,
283    /// 分页大小。
284    pub page_size: Option<i32>,
285}
286
287impl ApprovalTaskQuery {
288    /// 创建审批任务查询条件。
289    pub fn new(user_id: impl Into<String>, topic: impl Into<String>) -> Self {
290        Self {
291            user_id: user_id.into(),
292            topic: topic.into(),
293            ..Self::default()
294        }
295    }
296
297    /// 设置用户 ID 类型。
298    pub fn user_id_type(mut self, user_id_type: impl Into<String>) -> Self {
299        self.user_id_type = Some(user_id_type.into());
300        self
301    }
302
303    /// 设置审批状态。
304    /// 设置任务状态。
305    pub fn status(mut self, status: impl Into<String>) -> Self {
306        self.status = Some(status.into());
307        self
308    }
309
310    /// 设置实例编码。
311    pub fn instance_code(mut self, instance_code: impl Into<String>) -> Self {
312        self.instance_code = Some(instance_code.into());
313        self
314    }
315
316    /// 设置分页大小。
317    pub fn page_size(mut self, page_size: i32) -> Self {
318        self.page_size = Some(page_size);
319        self
320    }
321}
322
323/// 审批任务操作 helper。
324///
325/// 统一高频审批动作的 `task_id + comment` 组合。
326#[derive(Debug, Clone, Default, PartialEq)]
327pub struct ApprovalTaskAction {
328    /// 审批定义编码。
329    pub approval_code: String,
330    /// 审批实例编码。
331    pub instance_code: String,
332    /// 操作人用户 ID。
333    pub user_id: String,
334    /// 审批任务 ID。
335    pub task_id: String,
336    /// 用户 ID 类型。
337    pub user_id_type: Option<String>,
338    /// 备注。
339    pub comment: Option<String>,
340    /// 表单内容。
341    pub form: Option<String>,
342}
343
344impl ApprovalTaskAction {
345    /// 创建审批任务动作参数。
346    pub fn new(
347        approval_code: impl Into<String>,
348        instance_code: impl Into<String>,
349        user_id: impl Into<String>,
350        task_id: impl Into<String>,
351    ) -> Self {
352        Self {
353            approval_code: approval_code.into(),
354            instance_code: instance_code.into(),
355            user_id: user_id.into(),
356            task_id: task_id.into(),
357            user_id_type: None,
358            comment: None,
359            form: None,
360        }
361    }
362
363    /// 设置备注。
364    pub fn comment(mut self, comment: impl Into<String>) -> Self {
365        self.comment = Some(comment.into());
366        self
367    }
368
369    /// 设置用户 ID 类型。
370    pub fn user_id_type(mut self, user_id_type: impl Into<String>) -> Self {
371        self.user_id_type = Some(user_id_type.into());
372        self
373    }
374
375    /// 设置表单内容。
376    pub fn form(mut self, form: impl Into<String>) -> Self {
377        self.form = Some(form.into());
378        self
379    }
380}
381
382/// 审批任务条目类型别名。
383pub type ApprovalTaskItem = crate::approval::approval::v4::task::query::TaskItemV4;
384
385/// WorkflowService:工作流服务的统一入口
386///
387/// 提供对任务、审批、看板 API 的访问能力
388#[derive(Clone)]
389pub struct WorkflowService {
390    config: Arc<Config>,
391}
392
393impl WorkflowService {
394    /// 创建工作流服务入口。
395    pub fn new(config: Config) -> Self {
396        Self {
397            config: Arc::new(config),
398        }
399    }
400
401    #[cfg(feature = "v1")]
402    /// 返回 v1 任务服务入口。
403    pub fn v1(&self) -> crate::v1::TaskV1 {
404        crate::v1::TaskV1::new(self.config.clone())
405    }
406
407    #[cfg(feature = "v2")]
408    /// 返回 v2 任务服务入口。
409    pub fn v2(&self) -> crate::v2::TaskV2 {
410        crate::v2::TaskV2::new(self.config.clone())
411    }
412
413    /// 列取任务并自动处理分页。
414    #[cfg(feature = "v2")]
415    pub async fn list_tasks_all(
416        &self,
417        query: WorkflowTaskListQuery,
418    ) -> SDKResult<Vec<crate::v2::task::models::TaskItem>> {
419        use crate::v2::task::list::ListTasksRequest;
420
421        let mut items = Vec::new();
422        let mut page_token: Option<String> = None;
423
424        loop {
425            let mut request = ListTasksRequest::new(self.config.clone())
426                .page_size(query.page_size.unwrap_or(MAX_PAGE_SIZE));
427
428            if let Some(tasklist_guid) = &query.tasklist_guid {
429                request = request.tasklist_guid(tasklist_guid.clone());
430            }
431            if let Some(section_guid) = &query.section_guid {
432                request = request.section_guid(section_guid.clone());
433            }
434            if let Some(filter) = &query.filter {
435                request = request.filter(filter.clone());
436            }
437            if let Some(sort) = &query.sort {
438                request = request.sort(sort.clone());
439            }
440            if let Some(user_type) = &query.user_type {
441                request = request.user_type(user_type.clone());
442            }
443            if let Some(token) = &page_token {
444                request = request.page_token(token.clone());
445            }
446
447            let response = request.execute().await?;
448            items.extend(response.items);
449
450            if !response.has_more {
451                break;
452            }
453            page_token = response.page_token;
454        }
455
456        Ok(items)
457    }
458
459    /// 使用 helper 风格创建任务(高频字段)。
460    ///
461    /// 在 typed `CreateTaskRequest` 之上固化常见创建动作,返回业务结果
462    /// `CreateTaskResponse`,而不是底层响应壳。
463    #[cfg(feature = "v2")]
464    pub async fn create_task(
465        &self,
466        create: WorkflowTaskCreate,
467    ) -> SDKResult<crate::v2::task::models::CreateTaskResponse> {
468        use crate::v2::task::create::CreateTaskRequest;
469
470        let mut request = CreateTaskRequest::new(self.config.clone()).summary(create.summary);
471        if let Some(description) = create.description {
472            request = request.description(description);
473        }
474        if let Some(start) = create.start {
475            request = request.start(start);
476        }
477        if let Some(due) = create.due {
478            request = request.due(due);
479        }
480        if let Some(priority) = create.priority {
481            request = request.priority(priority);
482        }
483        if let Some(assignee) = create.assignee {
484            request = request.assignee(assignee);
485        }
486        if let Some(tasklist_guid) = create.tasklist_guid {
487            request = request.tasklist_guid(tasklist_guid);
488        }
489        if let Some(section_guid) = create.section_guid {
490            request = request.section_guid(section_guid);
491        }
492        if let Some(followers) = create.followers {
493            request = request.followers(followers);
494        }
495        if let Some(remind_time) = create.remind_time {
496            request = request.remind_time(remind_time);
497        }
498
499        request.execute().await
500    }
501
502    /// 使用 helper 风格更新任务高频字段。
503    #[cfg(feature = "v2")]
504    pub async fn mutate_task(
505        &self,
506        task_guid: impl Into<String>,
507        mutation: WorkflowTaskMutation,
508    ) -> SDKResult<crate::v2::task::models::UpdateTaskResponse> {
509        use crate::v2::task::update::UpdateTaskRequest;
510
511        let mut request = UpdateTaskRequest::new(self.config.clone(), task_guid.into());
512        if let Some(summary) = mutation.summary {
513            request = request.summary(summary);
514        }
515        if let Some(description) = mutation.description {
516            request = request.description(description);
517        }
518        if let Some(due) = mutation.due {
519            request = request.due(due);
520        }
521        if let Some(priority) = mutation.priority {
522            request = request.priority(priority);
523        }
524        if let Some(assignee) = mutation.assignee {
525            request = request.assignee(assignee);
526        }
527        if let Some(status) = mutation.status {
528            request = request.status(status);
529        }
530
531        request.execute().await
532    }
533
534    /// 完成任务 helper。
535    #[cfg(feature = "v2")]
536    pub async fn complete_task(
537        &self,
538        task_guid: impl Into<String>,
539    ) -> SDKResult<crate::v2::task::models::CompleteTaskResponse> {
540        use crate::v2::task::complete::CompleteTaskRequest;
541
542        CompleteTaskRequest::new(self.config.clone(), task_guid.into())
543            .execute()
544            .await
545    }
546
547    /// 重新打开任务 helper。
548    #[cfg(feature = "v2")]
549    pub async fn reopen_task(
550        &self,
551        task_guid: impl Into<String>,
552    ) -> SDKResult<crate::v2::task::models::UncompleteTaskResponse> {
553        use crate::v2::task::uncomplete::UncompleteTaskRequest;
554
555        UncompleteTaskRequest::new(self.config.clone(), task_guid.into())
556            .execute()
557            .await
558    }
559
560    /// 查询审批任务,并支持按状态/实例做本地筛选。
561    pub async fn query_approval_tasks(
562        &self,
563        query: ApprovalTaskQuery,
564    ) -> SDKResult<Vec<ApprovalTaskItem>> {
565        let mut items = Vec::new();
566        let mut page_token: Option<String> = None;
567
568        loop {
569            let mut request = crate::approval::approval::v4::task::query::QueryTaskRequestV4::new(
570                self.config.clone(),
571            )
572            .user_id(query.user_id.clone())
573            .topic(query.topic.clone())
574            .page_size(query.page_size.unwrap_or(MAX_PAGE_SIZE));
575
576            if let Some(user_id_type) = &query.user_id_type {
577                request = request.user_id_type(user_id_type.clone());
578            }
579            if let Some(token) = &page_token {
580                request = request.page_token(token.clone());
581            }
582
583            let response = request.execute().await?;
584            items.extend(response.tasks);
585
586            if !response.has_more.unwrap_or(false) {
587                break;
588            }
589            page_token = response.page_token;
590        }
591
592        if let Some(status) = &query.status {
593            items.retain(|item| item.status == *status);
594        }
595        if let Some(instance_code) = &query.instance_code {
596            items.retain(|item| item.instance_code == *instance_code);
597        }
598
599        Ok(items)
600    }
601
602    /// 同意审批任务 helper。
603    ///
604    /// 成功/失败由 `SDKResult` 表达:飞书 approval v4 同意接口响应 data 为空,
605    /// 不再伪造恒为 `true` 的 `success` 字段(#350 P9 接口形状撒谎修正)。
606    pub async fn approve_task(&self, action: ApprovalTaskAction) -> SDKResult<()> {
607        let mut request = crate::approval::approval::v4::task::approve::ApproveTaskRequestV4::new(
608            self.config.clone(),
609        )
610        .approval_code(action.approval_code)
611        .instance_code(action.instance_code)
612        .user_id(action.user_id)
613        .task_id(action.task_id);
614        if let Some(user_id_type) = action.user_id_type {
615            request = request.user_id_type(user_id_type);
616        }
617        if let Some(comment) = action.comment {
618            request = request.comment(comment);
619        }
620        if let Some(form) = action.form {
621            request = request.form(form);
622        }
623        request.execute().await?;
624        Ok(())
625    }
626
627    /// 拒绝审批任务 helper。
628    ///
629    /// 成功/失败由 `SDKResult` 表达;响应 data 为空时不伪造 `success: true`。
630    pub async fn reject_task(&self, action: ApprovalTaskAction) -> SDKResult<()> {
631        let mut request = crate::approval::approval::v4::task::reject::RejectTaskRequestV4::new(
632            self.config.clone(),
633        )
634        .approval_code(action.approval_code)
635        .instance_code(action.instance_code)
636        .user_id(action.user_id)
637        .task_id(action.task_id);
638        if let Some(user_id_type) = action.user_id_type {
639            request = request.user_id_type(user_id_type);
640        }
641        if let Some(comment) = action.comment {
642            request = request.comment(comment);
643        }
644        if let Some(form) = action.form {
645            request = request.form(form);
646        }
647        request.execute().await?;
648        Ok(())
649    }
650
651    /// 重新提交审批任务 helper。
652    ///
653    /// 成功/失败由 `SDKResult` 表达;响应 data 为空时不伪造 `success: true`。
654    pub async fn resubmit_task(&self, action: ApprovalTaskAction) -> SDKResult<()> {
655        let mut request =
656            crate::approval::approval::v4::task::resubmit::ResubmitTaskRequestV4::new(
657                self.config.clone(),
658            )
659            .approval_code(action.approval_code)
660            .instance_code(action.instance_code)
661            .user_id(action.user_id)
662            .task_id(action.task_id);
663        if let Some(user_id_type) = action.user_id_type {
664            request = request.user_id_type(user_id_type);
665        }
666        if let Some(comment) = action.comment {
667            request = request.comment(comment);
668        }
669        if let Some(form) = action.form {
670            request = request.form(form);
671        }
672        request.execute().await?;
673        Ok(())
674    }
675}
676
677#[cfg(test)]
678#[allow(unused_imports)]
679mod tests {
680    use super::*;
681    use serde_json::json;
682
683    #[test]
684    fn test_task_list_query_builder() {
685        let query = WorkflowTaskListQuery::for_tasklist("tasklist_123")
686            .section_guid("section_456")
687            .filter("status = incomplete")
688            .sort(json!([{"field": "due", "order": "asc"}]))
689            .user_type("open_id")
690            .page_size(50);
691
692        assert_eq!(query.tasklist_guid.as_deref(), Some("tasklist_123"));
693        assert_eq!(query.section_guid.as_deref(), Some("section_456"));
694        assert_eq!(query.filter.as_deref(), Some("status = incomplete"));
695        assert_eq!(query.user_type.as_deref(), Some("open_id"));
696        assert_eq!(query.page_size, Some(50));
697    }
698
699    #[test]
700    fn test_task_mutation_builder() {
701        let mutation = WorkflowTaskMutation::new()
702            .summary("完成项目文档")
703            .description("补齐 workflow helper")
704            .due("2026-09-30T23:59:59Z")
705            .priority(3)
706            .assignee("ou_xxx")
707            .status("in_progress");
708
709        assert_eq!(mutation.summary.as_deref(), Some("完成项目文档"));
710        assert_eq!(
711            mutation.description.as_deref(),
712            Some("补齐 workflow helper")
713        );
714        assert_eq!(mutation.due.as_deref(), Some("2026-09-30T23:59:59Z"));
715        assert_eq!(mutation.priority, Some(3));
716        assert_eq!(mutation.assignee.as_deref(), Some("ou_xxx"));
717        assert_eq!(mutation.status.as_deref(), Some("in_progress"));
718    }
719
720    #[test]
721    fn test_task_create_builder() {
722        let create = WorkflowTaskCreate::new("编写 release notes")
723            .description("补齐 0.20 create_task helper")
724            .due("2026-08-01T18:00:00Z")
725            .start("2026-07-27T09:00:00Z")
726            .priority(2)
727            .assignee("ou_owner")
728            .tasklist_guid("tasklist_abc")
729            .section_guid("section_xyz")
730            .followers(vec!["ou_follower".to_string()])
731            .remind_time("2026-07-31T09:00:00Z");
732
733        assert_eq!(create.summary, "编写 release notes");
734        assert_eq!(
735            create.description.as_deref(),
736            Some("补齐 0.20 create_task helper")
737        );
738        assert_eq!(create.due.as_deref(), Some("2026-08-01T18:00:00Z"));
739        assert_eq!(create.start.as_deref(), Some("2026-07-27T09:00:00Z"));
740        assert_eq!(create.priority, Some(2));
741        assert_eq!(create.assignee.as_deref(), Some("ou_owner"));
742        assert_eq!(create.tasklist_guid.as_deref(), Some("tasklist_abc"));
743        assert_eq!(create.section_guid.as_deref(), Some("section_xyz"));
744        assert_eq!(create.followers, Some(vec!["ou_follower".to_string()]));
745        assert_eq!(create.remind_time.as_deref(), Some("2026-07-31T09:00:00Z"));
746    }
747
748    #[test]
749    fn test_approval_task_query_builder() {
750        let query = ApprovalTaskQuery::new("ou_xxx", "1")
751            .user_id_type("open_id")
752            .status("PENDING")
753            .instance_code("instance_123")
754            .page_size(100);
755
756        assert_eq!(query.user_id, "ou_xxx");
757        assert_eq!(query.topic, "1");
758        assert_eq!(query.user_id_type.as_deref(), Some("open_id"));
759        assert_eq!(query.status.as_deref(), Some("PENDING"));
760        assert_eq!(query.instance_code.as_deref(), Some("instance_123"));
761        assert_eq!(query.page_size, Some(100));
762    }
763
764    #[test]
765    fn test_approval_task_action_builder() {
766        let action =
767            ApprovalTaskAction::new("approval_code", "instance_code", "ou_xxx", "task_123")
768                .user_id_type("open_id")
769                .comment("已确认")
770                .form("[{}]");
771
772        assert_eq!(action.approval_code, "approval_code");
773        assert_eq!(action.instance_code, "instance_code");
774        assert_eq!(action.user_id, "ou_xxx");
775        assert_eq!(action.task_id, "task_123");
776        assert_eq!(action.user_id_type.as_deref(), Some("open_id"));
777        assert_eq!(action.comment.as_deref(), Some("已确认"));
778        assert_eq!(action.form.as_deref(), Some("[{}]"));
779    }
780
781    /// #572:create_task helper 透传高频字段并返回业务 CreateTaskResponse。
782    #[cfg(feature = "v2")]
783    #[tokio::test]
784    async fn test_create_task_helper_posts_high_frequency_fields() {
785        use wiremock::matchers::{body_partial_json, method, path};
786        use wiremock::{Mock, MockServer, ResponseTemplate};
787
788        let server = MockServer::start().await;
789        Mock::given(method("POST"))
790            .and(path("/open-apis/task/v2/tasks"))
791            .and(body_partial_json(json!({
792                "summary": "编写 release notes",
793                "description": "补齐 0.20 create_task helper",
794                "priority": 2,
795                "assignee": "ou_owner",
796                "tasklist_guid": "tasklist_abc"
797            })))
798            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
799                "code": 0,
800                "msg": "success",
801                "data": {
802                    "task_guid": "task_created_001",
803                    "summary": "编写 release notes",
804                    "description": "补齐 0.20 create_task helper",
805                    "status": "todo",
806                    "tasklist_guid": "tasklist_abc",
807                    "section_guid": null,
808                    "created_at": "2026-07-27T00:00:00Z",
809                    "updated_at": "2026-07-27T00:00:00Z"
810                }
811            })))
812            .mount(&server)
813            .await;
814
815        let service = WorkflowService::new(
816            Config::builder()
817                .app_id("ci_app_id")
818                .app_secret("ci_app_secret")
819                .base_url(server.uri())
820                .enable_token_cache(false)
821                .build(),
822        );
823
824        let response = service
825            .create_task(
826                WorkflowTaskCreate::new("编写 release notes")
827                    .description("补齐 0.20 create_task helper")
828                    .priority(2)
829                    .assignee("ou_owner")
830                    .tasklist_guid("tasklist_abc"),
831            )
832            .await
833            .expect("create_task 应在飞书成功响应时返回业务结果");
834
835        assert_eq!(response.task_guid, "task_created_001");
836        assert_eq!(response.summary, "编写 release notes");
837        assert_eq!(response.status, "todo");
838        assert_eq!(response.tasklist_guid.as_deref(), Some("tasklist_abc"));
839
840        let received = server.received_requests().await.unwrap_or_default();
841        assert_eq!(received.len(), 1);
842        assert_eq!(received[0].url.path(), "/open-apis/task/v2/tasks");
843    }
844
845    /// #350:approve/reject/resubmit 成功时返回 `Ok(())`,不再伪造恒真 `success`。
846    #[tokio::test]
847    async fn test_approve_reject_resubmit_helpers_return_unit_on_success() {
848        use wiremock::matchers::{method, path};
849        use wiremock::{Mock, MockServer, ResponseTemplate};
850
851        let server = MockServer::start().await;
852        let paths = [
853            "/open-apis/approval/v4/tasks/approve",
854            "/open-apis/approval/v4/tasks/reject",
855            "/open-apis/approval/v4/tasks/resubmit",
856        ];
857        for p in paths {
858            Mock::given(method("POST"))
859                .and(path(p))
860                .respond_with(ResponseTemplate::new(200).set_body_json(json!({
861                    "code": 0,
862                    "msg": "success",
863                    "data": {}
864                })))
865                .mount(&server)
866                .await;
867        }
868
869        let service = WorkflowService::new(
870            Config::builder()
871                .app_id("ci_app_id")
872                .app_secret("ci_app_secret")
873                .base_url(server.uri())
874                .enable_token_cache(false)
875                .build(),
876        );
877
878        let action =
879            ApprovalTaskAction::new("approval_code", "instance_code", "ou_xxx", "task_123")
880                .user_id_type("open_id")
881                .comment("ok")
882                .form("[]");
883
884        service
885            .approve_task(action.clone())
886            .await
887            .expect("approve_task 应在飞书成功响应时返回 Ok(())");
888        service
889            .reject_task(action.clone())
890            .await
891            .expect("reject_task 应在飞书成功响应时返回 Ok(())");
892        service
893            .resubmit_task(action)
894            .await
895            .expect("resubmit_task 应在飞书成功响应时返回 Ok(())");
896
897        let received = server.received_requests().await.unwrap_or_default();
898        assert_eq!(
899            received.len(),
900            3,
901            "三个 helper 应各打一次飞书 approval v4 端点"
902        );
903        let hit: Vec<_> = received.iter().map(|r| r.url.path().to_string()).collect();
904        for p in paths {
905            assert!(
906                hit.iter().any(|h| h == p),
907                "missing request to {p}; got {hit:?}"
908            );
909        }
910    }
911
912    /// #350:底层失败时 helper 传播 Err,而非恒真 success。
913    ///
914    /// `ApproveTaskRequestV4` 对飞书 `code != 0` 且无 `data` 的响应走
915    /// `missing_response_data`(Validation),而不是把 `msg` 映射成 `CoreError::Api`。
916    /// 本测试锁定 helper 契约:`Err` 必须向上抛出,不能伪装 `Ok(())`。
917    #[tokio::test]
918    async fn test_approve_task_helper_propagates_api_error() {
919        use openlark_core::error::CoreError;
920        use wiremock::matchers::{method, path};
921        use wiremock::{Mock, MockServer, ResponseTemplate};
922
923        let server = MockServer::start().await;
924        Mock::given(method("POST"))
925            .and(path("/open-apis/approval/v4/tasks/approve"))
926            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
927                "code": 99991400,
928                "msg": "invalid approval task"
929            })))
930            .mount(&server)
931            .await;
932
933        let service = WorkflowService::new(
934            Config::builder()
935                .app_id("ci_app_id")
936                .app_secret("ci_app_secret")
937                .base_url(server.uri())
938                .enable_token_cache(false)
939                .build(),
940        );
941
942        let err = service
943            .approve_task(ApprovalTaskAction::new(
944                "approval_code",
945                "instance_code",
946                "ou_xxx",
947                "task_123",
948            ))
949            .await
950            .expect_err("飞书失败响应应传播为 Err,不得伪装 Ok(())");
951        assert!(
952            matches!(err, CoreError::Validation { .. } | CoreError::Api(_)),
953            "expected Validation (missing data on non-zero code) or Api, got {err:?}"
954        );
955        let msg = err.to_string();
956        assert!(
957            msg.contains("服务器没有返回有效的数据") || msg.contains("invalid approval task"),
958            "error should surface leaf validation or Feishu msg, got: {err}"
959        );
960
961        let received = server.received_requests().await.unwrap_or_default();
962        assert_eq!(received.len(), 1);
963        assert_eq!(
964            received[0].url.path(),
965            "/open-apis/approval/v4/tasks/approve"
966        );
967    }
968}