Эх сурвалжийг харах

feature: 完成所有审核流程

eyes4 5 сар өмнө
parent
commit
69c4d78c28
63 өөрчлөгдсөн 2028 нэмэгдсэн , 489 устгасан
  1. 8 2
      base-lib/src/core-models/AcsFunction.ts
  2. 1 0
      base-lib/src/core-models/AcsModule.ts
  3. 7 1
      base-lib/src/core-models/AcsRoleFunction.ts
  4. 1 1
      base-lib/src/core-models/BpmnForm.ts
  5. 1 0
      base-lib/src/core-models/BpmnWork.ts
  6. 1 0
      base-lib/src/core-models/PrjFile.ts
  7. 1 0
      base-lib/src/core-models/PrjMembers.ts
  8. 1 0
      base-lib/src/core-models/PrjPlanTask.ts
  9. 1 0
      base-lib/src/core-models/PrjPlanTaskDraft.ts
  10. 6 5
      base-lib/src/core-models/PrjTaskOutcome.ts
  11. 6 5
      base-lib/src/core-models/PrjTaskOutcomeDraft.ts
  12. 286 74
      pmr-biz-manager/bpmn/任务审核流程.bpmn
  13. 125 51
      pmr-biz-manager/bpmn/立项流程.bpmn
  14. 84 28
      pmr-biz-manager/bpmn/计划创建审核流程.bpmn
  15. 79 25
      pmr-biz-manager/bpmn/计划变更审核流程.bpmn
  16. 189 0
      pmr-biz-manager/readme.md
  17. 70 11
      pmr-biz-manager/src/app.ts
  18. 287 57
      pmr-biz-manager/src/bpmn/flow_engine.ts
  19. 4 1
      pmr-biz-manager/src/routes/api/cfg/doc_type/get_list.ts
  20. 0 59
      pmr-biz-manager/src/routes/api/cfg/org/add.ts
  21. 84 0
      pmr-biz-manager/src/routes/api/cfg/region/add.ts
  22. 82 0
      pmr-biz-manager/src/routes/api/cfg/region/get_tree.ts
  23. 118 0
      pmr-biz-manager/src/routes/api/cfg/region/modify.ts
  24. 74 0
      pmr-biz-manager/src/routes/api/cfg/region/remove.ts
  25. 2 2
      pmr-biz-manager/src/routes/api/cfg/staff/modify.ts
  26. 14 2
      pmr-biz-manager/src/routes/api/prj/contract/get_list.ts
  27. 5 1
      pmr-biz-manager/src/routes/api/prj/doc/remove.ts
  28. 4 3
      pmr-biz-manager/src/routes/api/prj/info/add.ts
  29. 1 0
      pmr-biz-manager/src/routes/api/prj/info/get_checkers.ts
  30. 1 1
      pmr-biz-manager/src/routes/api/prj/info/get_list.ts
  31. 11 9
      pmr-biz-manager/src/routes/api/prj/info/modify.ts
  32. 5 2
      pmr-biz-manager/src/routes/api/prj/info/overview.ts
  33. 1 0
      pmr-biz-manager/src/routes/api/prj/member/add.ts
  34. 7 4
      pmr-biz-manager/src/routes/api/prj/member/modify.ts
  35. 6 1
      pmr-biz-manager/src/routes/api/prj/member/remove.ts
  36. 19 8
      pmr-biz-manager/src/routes/api/prj/outcome/add.ts
  37. 1 1
      pmr-biz-manager/src/routes/api/prj/outcome/detail.ts
  38. 4 3
      pmr-biz-manager/src/routes/api/prj/outcome/download.ts
  39. 18 2
      pmr-biz-manager/src/routes/api/prj/outcome/get_list.ts
  40. 27 3
      pmr-biz-manager/src/routes/api/prj/outcome/modify.ts
  41. 24 13
      pmr-biz-manager/src/routes/api/prj/outcome/remove.ts
  42. 10 4
      pmr-biz-manager/src/routes/api/prj/outcome/remove_file.ts
  43. 7 4
      pmr-biz-manager/src/routes/api/prj/outcome/upload.ts
  44. 8 2
      pmr-biz-manager/src/routes/api/prj/plan/add_task.ts
  45. 6 5
      pmr-biz-manager/src/routes/api/prj/plan/get_tasks.ts
  46. 38 22
      pmr-biz-manager/src/routes/api/prj/plan/modify_task.ts
  47. 20 10
      pmr-biz-manager/src/routes/api/prj/plan/remove_task.ts
  48. 8 5
      pmr-biz-manager/src/routes/api/prj/plan/set_check_flow.ts
  49. 10 4
      pmr-biz-manager/src/routes/api/prj/plan/set_progress.ts
  50. 5 5
      pmr-biz-manager/src/routes/api/prj/plan/task_detail.ts
  51. 3 3
      pmr-biz-manager/src/routes/api/prj/week_report/modify.ts
  52. 114 0
      pmr-biz-manager/src/routes/api/prj/work/get_chain.ts
  53. 20 5
      pmr-biz-manager/src/routes/api/prj/work/get_list.ts
  54. 2 0
      pmr-biz-manager/src/routes/api/prj/work/stat.ts
  55. 8 8
      pmr-biz-manager/src/routes/api/prj/work/submit.ts
  56. 1 1
      pmr-biz-manager/src/routes/api/prj/work/undone_count.ts
  57. 1 1
      pmr-biz-manager/src/routes/api/prj/work/undone_stat.ts
  58. 1 0
      pmr-biz-manager/src/routes/api/report/prj/multi_stat.ts
  59. 50 20
      pmr-biz-manager/src/utils/bpmn_work_helper.ts
  60. 1 0
      pmr-biz-manager/src/utils/define.ts
  61. 27 9
      pmr-biz-manager/src/utils/plan_task_helper.ts
  62. 2 2
      pmr-biz-manager/src/utils/prj_logger.ts
  63. 20 4
      pmr-biz-manager/src/utils/prj_premission_helper.ts

+ 8 - 2
base-lib/src/core-models/AcsFunction.ts

@@ -122,6 +122,11 @@ export class AcsFunction extends Model {
             (811, 181, 'add', '添加员工', 'cfg.staff.add', true, 1),
             (812, 181, 'modify', '修改员工', 'cfg.staff.modify', true, 1),
             (813, 181, 'remove', '删除员工', 'cfg.staff.remove', true, 1),
+            
+            (820, 182, 'get_tree', '获取区域树', 'cfg.region.get_tree', true, 0),
+            (821, 182, 'add', '添加区域', 'cfg.region.add', true, 1),
+            (822, 182, 'modify', '修改区域', 'cfg.region.modify', true, 1),
+            (823, 182, 'remove', '删除区域', 'cfg.region.remove', true, 1),
 
             (1000, 201, 'get_list', '获取客户列表', 'biz.customer.get_list', true, 0),
             (1001, 201, 'add', '添加客户信息', 'biz.customer.add', true, 1),
@@ -203,11 +208,12 @@ export class AcsFunction extends Model {
             (2201, 311, 'detail', '获取工作详情', 'prj.work.detail', true, 0),
             (2202, 311, 'submit', '提交工作', 'prj.work.submit', true, 1),
             (2203, 311, 'upload_file', '上传工作项附件', 'prj.work.upload_file', true, 1),
-            (2204, 311, 'download_file', '下载工作项附件', 'prj.work.download_file', true, 0)
+            (2204, 311, 'download_file', '下载工作项附件', 'prj.work.download_file', true, 0),
             (2205, 311, 'undone_count', '获取未完成工作项总数', 'prj.work.undone_count', true, 0),
             (2206, 311, 'done_stat', '获取已完成工作项统计信息', 'prj.work.done_stat', true, 0),
             (2207, 311, 'undone_stat', '获取未完成工作项统计信息', 'prj.work.undone_stat', true, 0),
-            (2208, 311, 'stat', '获取工作项统计信息', 'prj.work.stat', true, 0)
+            (2208, 311, 'stat', '获取工作项统计信息', 'prj.work.stat', true, 0),
+            (2209, 311, 'get_chain', '获取工作项链', 'prj.work.get_chain', true, 0)
 
          ON CONFLICT DO NOTHING `,
         `select setval('tb_acs_function_id_seq', 20000 ) where (select nextval('tb_acs_function_id_seq')) < 20000;`

+ 1 - 0
base-lib/src/core-models/AcsModule.ts

@@ -89,6 +89,7 @@ export class AcsModule extends Model {
             
             (180, 'org', '组织机构管理', 100, text2ltree('100.180'), 'cfg.org'),
             (181, 'staff', '员工管理', 100, text2ltree('100.181'), 'cfg.staff'),
+            (182, 'region', '区域管理', 100, text2ltree('100.182'), 'cfg.region'),
             
             (200, 'biz', '营销接口', null, text2ltree('200'), 'biz'),
             (201, 'customer', '客户管理', 200, text2ltree('200.201'), 'biz.customer'),

+ 7 - 1
base-lib/src/core-models/AcsRoleFunction.ts

@@ -101,6 +101,11 @@ export class AcsRoleFunction extends Model {
             ('admin', 811),
             ('admin', 812),
             ('admin', 813),
+            
+            ('admin', 820),
+            ('admin', 821),
+            ('admin', 822),
+            ('admin', 823),
 
             ('admin', 1000),
             ('admin', 1001),
@@ -186,7 +191,8 @@ export class AcsRoleFunction extends Model {
             ('admin', 2205),
             ('admin', 2206),
             ('admin', 2207),
-            ('admin', 2208)
+            ('admin', 2208),
+            ('admin', 2209)
             
             ON CONFLICT DO NOTHING `
     ]

+ 1 - 1
base-lib/src/core-models/BpmnForm.ts

@@ -51,7 +51,7 @@ export class BpmnForm extends Model {
             comment: '表单标题'
         },
         schema: {
-            type: DataTypes.JSONB,
+            type: DataTypes.TEXT,
             comment: '表单定义'
         }
     }

+ 1 - 0
base-lib/src/core-models/BpmnWork.ts

@@ -21,6 +21,7 @@ export class BpmnWork extends Model {
     declare completed_at: string;
     declare process_type: number;
     declare status: number;
+    declare case_id: string;
 
     static table_name = 'tb_bpmn_work';
 

+ 1 - 0
base-lib/src/core-models/PrjFile.ts

@@ -11,6 +11,7 @@ import {PrjFileCategory} from "@core-models/PrjFileCategory";
 export class PrjFile extends Model {
     declare id: string;
     declare prj_id: string;
+    declare category_id: string;
     declare filename: string;
     declare uploaded_at: string;
     declare dependent_id: string;

+ 1 - 0
base-lib/src/core-models/PrjMembers.ts

@@ -11,6 +11,7 @@ import {PrjInfo} from "@core-models/PrjInfo";
 export class PrjMembers extends Model {
     // declare id: string;
     declare member_id: string;
+    declare role_id: string;
 
     static table_name = 'tb_prj_members';
 

+ 1 - 0
base-lib/src/core-models/PrjPlanTask.ts

@@ -9,6 +9,7 @@ import {PrjInfo} from "@core-models/PrjInfo";
 
 export class PrjPlanTask extends Model {
     declare id: string;
+    declare name: string;
     declare pid: string;
     declare status: number;
     declare prj_id: string;

+ 1 - 0
base-lib/src/core-models/PrjPlanTaskDraft.ts

@@ -9,6 +9,7 @@ import {PrjInfo} from "@core-models/PrjInfo";
 
 export class PrjPlanTaskDraft extends Model {
     declare id: string;
+    declare name: string;
     declare pid: string;
     declare status: number;
     declare prj_id: string;

+ 6 - 5
base-lib/src/core-models/PrjTaskOutcome.ts

@@ -12,6 +12,7 @@ export class PrjTaskOutcome extends Model {
     declare status: number;
     declare task_id: string;
     declare uploaded: boolean;
+    declare object_name: string;
 
     static table_name = 'tb_prj_task_outcome';
 
@@ -94,11 +95,11 @@ export class PrjTaskOutcome extends Model {
             defaultValue: "0000000000000000",
             comment: '排序编号(从小到大排序)'
         },
-        // object_name: {
-        //     type: DataTypes.STRING,
-        //     allowNull: true,
-        //     comment: 'oss中保存的object name'
-        // },
+        object_name: {
+            type: DataTypes.STRING,
+            allowNull: true,
+            comment: 'oss中保存的object name'
+        },
         // filename: {
         //     type: DataTypes.STRING,
         //     allowNull: true,

+ 6 - 5
base-lib/src/core-models/PrjTaskOutcomeDraft.ts

@@ -14,6 +14,7 @@ export class PrjTaskOutcomeDraft extends Model {
     declare task_id: string;
     declare uploaded: boolean;
     declare change_marker: number;
+    declare object_name: string;
 
     static table_name = 'tb_prj_task_outcome_draft';
 
@@ -99,11 +100,11 @@ export class PrjTaskOutcomeDraft extends Model {
             defaultValue: "0000000000000000",
             comment: '排序编号(从小到大排序)'
         },
-        // object_name: {
-        //     type: DataTypes.STRING,
-        //     allowNull: true,
-        //     comment: 'oss中保存的object name'
-        // },
+        object_name: {
+            type: DataTypes.STRING,
+            allowNull: true,
+            comment: 'oss中保存的object name'
+        },
         // filename: {
         //     type: DataTypes.STRING,
         //     allowNull: true,

+ 286 - 74
pmr-biz-manager/bpmn/任务审核流程.bpmn

@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <bpmn2:definitions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmn2="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" id="sample-diagram" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="5.22.0" xsi:schemaLocation="http://www.omg.org/spec/BPMN/20100524/MODEL BPMN20.xsd">
-  <bpmn2:process id="ProcessOutcomeApprove" name="立项审批流程" isExecutable="true" camunda:versionTag="1.0">
+  <bpmn2:process id="ProcessOutcomeApprove" name="任务审核流程" isExecutable="true" camunda:versionTag="1.0">
     <bpmn2:startEvent id="event_start">
       <bpmn2:extensionElements>
         <camunda:properties>
@@ -18,6 +18,37 @@
           <camunda:inputParameter name="draft">${true}</camunda:inputParameter>
           <camunda:inputParameter name="task_id">${environment.variables.task_id}</camunda:inputParameter>
         </camunda:inputOutput>
+        <camunda:executionListener event="end">
+          <camunda:script scriptFormat="JavaScript">
+(async function () {
+    // 提交审核
+    try {
+        let owner = await this.environment.services.owner(); // 获取任务责任人
+        if (!owner) {
+            await this.environment.services.gen_warn_task('admin', '提交任务审核时未找到负责人',
+                `提交任务审核时未找到负责人,流程出错。case id: ${this.environment.variables.id}`);
+        }
+        // 获取任务审核人列表       
+        let checkers = await this.environment.services.get_handlers('task_checker');
+        if (!checkers || checkers.length === 0) return;
+        let checkers_str = "";
+        for (var i = 0; i &lt; checkers.length; i++) {
+            checkers_str += `&lt;b&gt;${checkers[i].name}&lt;/b&gt; 进行${checkers[i].action};&lt;br&gt;`;
+        }
+        let check_type = this.environment.variables.check_type;
+        let check_type_str = check_type === 2 ? "并行审核": "顺序审核";
+        await this.environment.services.gen_remind_task(owner.id, `任务“${this.environment.variables.task_name}”的审核申请已提交`, `&lt;b&gt;${owner.name}&lt;/b&gt;已于 &lt;b&gt;` +
+            this.environment.services.get_datetime() + `&lt;/b&gt; 提交了任务&lt;b&gt;“${this.environment.variables.task_name}”&lt;/b&gt;的审核申请,将由: &lt;br&gt; ${checkers_str}审核方式为:${check_type_str}`);
+        await this.environment.services.logger(owner.id, owner.name, `提交了任务&lt;b&gt;“${this.environment.variables.task_name}”&lt;/b&gt;的审核申请,将由: &lt;br&gt; ${checkers_str}审核方式为:${check_type_str}`);
+        
+    } catch (e) {
+        this.environment.Logger('work').error(e);
+    }
+})(); 
+
+          </camunda:script>
+        </camunda:executionListener>
+        <camunda:taskListener class="" event="assignment" />
       </bpmn2:extensionElements>
       <bpmn2:incoming>Flow_1t45qnv</bpmn2:incoming>
       <bpmn2:incoming>Flow_0gyvyyo</bpmn2:incoming>
@@ -27,13 +58,79 @@
     </bpmn2:userTask>
     <bpmn2:sequenceFlow id="Flow_15zn1tu" sourceRef="event_start" targetRef="task_do_task" />
     <bpmn2:sequenceFlow id="Flow_1rg4oob" sourceRef="task_request_task" targetRef="Gateway_1whgd2i">
-      <bpmn2:documentation>状态变为申请审核(40)</bpmn2:documentation>
+      <bpmn2:documentation>状态变为审核中(50)</bpmn2:documentation>
       <bpmn2:extensionElements>
         <camunda:properties>
-          <camunda:property name="task_status" value="40" />
+          <camunda:property name="task_status" value="50" />
+          <camunda:property name="task_status_name" value="任务审核中" />
         </camunda:properties>
       </bpmn2:extensionElements>
     </bpmn2:sequenceFlow>
+    <bpmn2:userTask id="task_approve_task_sync" name="审核任务成果" camunda:formKey="task_approve" camunda:assignee="task_checker">
+      <bpmn2:documentation>状态变为审核中(50)</bpmn2:documentation>
+      <bpmn2:extensionElements>
+        <camunda:inputOutput>
+          <camunda:inputParameter name="show_in_my_works">${true}</camunda:inputParameter>
+          <camunda:inputParameter name="process_type">${1}</camunda:inputParameter>
+          <camunda:inputParameter name="target_type"> project-plan</camunda:inputParameter>
+          <camunda:inputParameter name="prj_id">${environment.variables.prj_id}</camunda:inputParameter>
+          <camunda:inputParameter name="draft">${false}</camunda:inputParameter>
+          <camunda:inputParameter name="task_id">${environment.variables.task_id}</camunda:inputParameter>
+        </camunda:inputOutput>
+        <camunda:executionListener event="end">
+          <camunda:script scriptFormat="JavaScript">
+(async function () {
+    try {
+        // 并行审核任务,多人同时执行
+        let owner = await this.environment.services.owner(); // 获取任务责任人
+        if (!owner) {
+            await this.environment.services.gen_warn_task('admin', '任务审核时未找到负责人',
+                `任务审核时未找到负责人,流程出错。case id: ${this.environment.variables.id}`);
+        }
+        this.environment.Logger('work').debug(this.content);
+        let checker = this.content.handlers[this.content.index]; // 获取当前审核人,这个是并行审核任务,有多个审核人并行审核
+        let result = this.content.output[this.content.index]; // 来自于审核人表单结果
+        if (result.pass) { // 如果审核通过
+            // 生成阅读任务
+            await this.environment.services.gen_remind_task(owner.id, `${checker.name}:任务“${this.environment.variables.task_name}”的${checker.action}已通过`,
+                `&lt;b&gt;${checker.name}&lt;/b&gt;已于 &lt;b&gt;${this.environment.services.get_datetime()}&lt;/b&gt; 对任务“${this.environment.variables.task_name}”的交付物进行了${checker.action}并&lt;span style="color: rgb(20, 201, 201);"&gt;通过。&lt;/span&gt;`);
+            // 生成任务动态
+            await this.environment.services.logger(checker.id, checker.name, `任务“${this.environment.variables.task_name}”的${checker.action}&lt;span style="color: rgb(20, 201, 201);"&gt;已通过&lt;/span&gt;`);
+            // 归档审核文件
+            if (result.opinion) {
+                await this.environment.services.place_on_files(result.opinion, `${checker.action}意见(通过)`);
+            }
+            if (result.annotated_draft) {
+                await this.environment.services.place_on_files(result.annotated_draft, `${checker.action}批注稿(通过)`);
+            }
+            
+        } else {
+            // 生成阅读任务
+            await this.environment.services.gen_remind_task(owner.id, `${checker.name}任务“${this.environment.variables.task_name}”的${checker.action}已驳回`,
+                `&lt;b&gt;${checker.name}&lt;/b&gt;已于 &lt;b&gt;${this.environment.services.get_datetime()}&lt;/b&gt;  对任务“${this.environment.variables.task_name}”的交付物进行了${checker.action}并&lt;span style="color: red;"&gt;驳回。&lt;/span&gt;`);
+            // 生成任务动态
+            await this.environment.services.logger(checker.id, checker.name, `任务“${this.environment.variables.task_name}”在${checker.action}时&lt;span style="color: red;"&gt;被驳回&lt;/span&gt;`);
+            // 归档审核文件
+            if (result.opinion) {
+                await this.environment.services.place_on_files(result.opinion, `${checker.action}意见(驳回)`);
+            }
+            if (result.annotated_draft) {
+                await this.environment.services.place_on_files(result.annotated_draft, `${checker.action}批注稿(驳回)`);
+            }
+        }
+    } catch (e) {
+        this.environment.Logger('work').error(e);
+    }
+})();  
+</camunda:script>
+        </camunda:executionListener>
+      </bpmn2:extensionElements>
+      <bpmn2:incoming>Flow_0iv9g3p</bpmn2:incoming>
+      <bpmn2:outgoing>Flow_1su3b3p</bpmn2:outgoing>
+      <bpmn2:multiInstanceLoopCharacteristics camunda:collection="${content.handlers}" camunda:elementVariable="handler">
+        <bpmn2:loopCardinality xsi:type="bpmn2:tFormalExpression">${content.handlers.length}</bpmn2:loopCardinality>
+      </bpmn2:multiInstanceLoopCharacteristics>
+    </bpmn2:userTask>
     <bpmn2:userTask id="task_approve_task_async" name="审核任务成果" camunda:formKey="task_approve" camunda:assignee="task_checker">
       <bpmn2:documentation>状态变为审核中(50)</bpmn2:documentation>
       <bpmn2:extensionElements>
@@ -42,10 +139,56 @@
           <camunda:inputParameter name="process_type">${1}</camunda:inputParameter>
           <camunda:inputParameter name="target_type"> project-plan</camunda:inputParameter>
           <camunda:inputParameter name="prj_id">${environment.variables.prj_id}</camunda:inputParameter>
-          <camunda:inputParameter name="task_status">${50}</camunda:inputParameter>
           <camunda:inputParameter name="draft">${false}</camunda:inputParameter>
           <camunda:inputParameter name="task_id">${environment.variables.task_id}</camunda:inputParameter>
         </camunda:inputOutput>
+        <camunda:executionListener event="end">
+          <camunda:script scriptFormat="JavaScript">
+(async function () {
+    try {
+        // 串行审核任务,多人按顺序执行审核
+        let owner = await this.environment.services.owner(); // 获取任务责任人
+        if (!owner) {
+            await this.environment.services.gen_warn_task('admin', '任务审核时未找到负责人',
+                `任务审核时未找到负责人,流程出错。case id: ${this.environment.variables.id}`);
+        }
+        this.environment.Logger('work').debug(this.content);
+        let checker = this.content.handlers[this.content.index]; // 获取当前审核人,这个是并行审核任务,有多个审核人并行审核
+        let result = this.content.output[this.content.index]; // 来自于审核人表单结果
+        if (result.pass) { // 如果审核通过
+            // 生成阅读任务
+            await this.environment.services.gen_remind_task(owner.id, `${checker.name}:任务“${this.environment.variables.task_name}”的${checker.action}已通过`,
+                `&lt;b&gt;${checker.name}&lt;/b&gt;已于 &lt;b&gt;${this.environment.services.get_datetime()}&lt;/b&gt; 对任务“${this.environment.variables.task_name}”的交付物进行了${checker.action}并&lt;span style="color: rgb(20, 201, 201);"&gt;通过。&lt;/span&gt;。`);
+            // 生成任务动态
+            await this.environment.services.logger(checker.id, checker.name, `任务“${this.environment.variables.task_name}”的${checker.action}&lt;span style="color: rgb(20, 201, 201);"&gt;已通过&lt;/span&gt;`);
+            // 归档审核文件
+            if (result.opinion) {
+                await this.environment.services.place_on_files(result.opinion, `${checker.action}意见(通过)`);
+            }
+            if (result.annotated_draft) {
+                await this.environment.services.place_on_files(result.annotated_draft, `${checker.action}批注稿(通过)`);
+            }
+            
+        } else {
+            // 生成阅读任务
+            await this.environment.services.gen_remind_task(owner.id, `${checker.name}:任务“${this.environment.variables.task_name}”的${checker.action}已驳回`,
+                `&lt;b&gt;${checker.name}&lt;/b&gt;已于 &lt;b&gt;${this.environment.services.get_datetime()}&lt;/b&gt;  对任务“${this.environment.variables.task_name}”的交付物进行了${checker.action}并&lt;span style="color: red;"&gt;驳回。&lt;/span&gt;。`);
+            // 生成任务动态
+            await this.environment.services.logger(checker.id, checker.name, `任务“${this.environment.variables.task_name}”在${checker.action}时&lt;span style="color: red;"&gt;被驳回&lt;/span&gt;`);
+            // 归档审核文件
+            if (result.opinion) {
+                await this.environment.services.place_on_files(result.opinion, `${checker.action}意见(驳回)`);
+            }
+            if (result.annotated_draft) {
+                await this.environment.services.place_on_files(result.annotated_draft, `${checker.action}批注稿(驳回)`);
+            }
+        }
+    } catch (e) {
+        this.environment.Logger('work').error(e);
+    }
+})();  
+          </camunda:script>
+        </camunda:executionListener>
       </bpmn2:extensionElements>
       <bpmn2:incoming>Flow_02nannf</bpmn2:incoming>
       <bpmn2:outgoing>Flow_193tusd</bpmn2:outgoing>
@@ -65,6 +208,7 @@
       <bpmn2:extensionElements>
         <camunda:properties>
           <camunda:property name="task_status" value="100" />
+          <camunda:property name="task_status_name" value="任务审核通过" />
         </camunda:properties>
       </bpmn2:extensionElements>
       <bpmn2:conditionExpression xsi:type="bpmn2:tFormalExpression" language="JavaScript">next(null, this.environment.variables.pass===true);</bpmn2:conditionExpression>
@@ -74,6 +218,7 @@
       <bpmn2:extensionElements>
         <camunda:properties>
           <camunda:property name="task_status" value="30" />
+          <camunda:property name="task_status_name" value="任务审核未通过" />
         </camunda:properties>
       </bpmn2:extensionElements>
     </bpmn2:sequenceFlow>
@@ -83,6 +228,24 @@
         <camunda:inputOutput>
           <camunda:inputParameter name="process_type">${1}</camunda:inputParameter>
         </camunda:inputOutput>
+        <camunda:executionListener event="end">
+          <camunda:script scriptFormat="JavaScript">
+(async function() {
+    try{
+        let owner = await this.environment.services.owner(); // 获取任务责任人
+        if (!owner) {
+            await this.environment.services.gen_warn_task('admin', '任务审核申请被撤回时未找到负责人',
+                `任务审核申请被撤回时未找到负责人,流程出错。case id: ${this.environment.variables.id}`);
+        }
+        await this.environment.services.gen_remind_task(owner.id, `任务“${this.environment.variables.task_name}”的审核申请已被撤回`,
+            `${owner.name}已于 ${this.environment.services.get_datetime()} &lt;span style="color: rgb(22,93,255);"&gt;撤回&lt;/span&gt;了任务&lt;b&gt;“${this.environment.variables.task_name}”&lt;/b&gt;的审核申请。`);
+        await this.environment.services.logger(owner.id, owner.name, `撤回了任务&lt;b&gt;“${this.environment.variables.task_name}”&lt;/b&gt;的审核申请。`);
+    } catch(e){
+        this.environment.Logger('work').error(e);
+    }
+})();            
+</camunda:script>
+        </camunda:executionListener>
       </bpmn2:extensionElements>
       <bpmn2:incoming>Flow_0bhibn0</bpmn2:incoming>
       <bpmn2:outgoing>Flow_0tj9nd6</bpmn2:outgoing>
@@ -98,7 +261,7 @@
       <bpmn2:incoming>Flow_0tj9nd6</bpmn2:incoming>
       <bpmn2:signalEventDefinition id="SignalEventDefinition_1tt7p06" signalRef="Signal_0hhmd7l" />
     </bpmn2:intermediateThrowEvent>
-    <bpmn2:sequenceFlow id="Flow_02taytj" sourceRef="event_throw_approved" targetRef="Event_end">
+    <bpmn2:sequenceFlow id="Flow_02taytj" sourceRef="event_throw_approved" targetRef="Activity_10ywpg4">
       <bpmn2:extensionElements />
     </bpmn2:sequenceFlow>
     <bpmn2:intermediateThrowEvent id="event_throw_approved" name="已审核事件">
@@ -120,7 +283,7 @@
     <bpmn2:sequenceFlow id="Flow_0nzbvpg" sourceRef="Event_catch_withdraw" targetRef="task_modify_task1">
       <bpmn2:extensionElements />
     </bpmn2:sequenceFlow>
-    <bpmn2:sequenceFlow id="Flow_06cxzjx" sourceRef="event_throw_approved_back" targetRef="Activity_1dcslsv" />
+    <bpmn2:sequenceFlow id="Flow_06cxzjx" sourceRef="event_throw_approved_back" targetRef="Activity_0cir5fq" />
     <bpmn2:manualTask id="task_do_task" name="实施任务工作">
       <bpmn2:incoming>Flow_15zn1tu</bpmn2:incoming>
       <bpmn2:outgoing>Flow_0gyvyyo</bpmn2:outgoing>
@@ -129,26 +292,14 @@
       <bpmn2:extensionElements />
     </bpmn2:sequenceFlow>
     <bpmn2:manualTask id="task_modify_task1" name="修改交付物">
-      <bpmn2:incoming>Flow_06kumr8</bpmn2:incoming>
       <bpmn2:incoming>Flow_0nzbvpg</bpmn2:incoming>
+      <bpmn2:incoming>Flow_1eauhgu</bpmn2:incoming>
       <bpmn2:outgoing>Flow_1t45qnv</bpmn2:outgoing>
     </bpmn2:manualTask>
-    <bpmn2:scriptTask id="Activity_1dcslsv" name="提醒审核被驳回" scriptFormat="Javascript">
-      <bpmn2:extensionElements />
-      <bpmn2:incoming>Flow_06cxzjx</bpmn2:incoming>
-      <bpmn2:outgoing>Flow_06kumr8</bpmn2:outgoing>
-      <bpmn2:script>this.environment.services.get_handlers('owner').then((handlers) =&gt; {
-for (var i = 0; i &lt; handlers.length; i++) {
-  this.environment.services.gen_remind_task(handlers[i].id, '任务审核被驳回', '任务审核被驳回');
-}
-next();
-});</bpmn2:script>
-    </bpmn2:scriptTask>
     <bpmn2:endEvent id="Event_end">
       <bpmn2:incoming>Flow_1ucg773</bpmn2:incoming>
-      <bpmn2:incoming>Flow_02taytj</bpmn2:incoming>
+      <bpmn2:incoming>Flow_1xp7bfo</bpmn2:incoming>
     </bpmn2:endEvent>
-    <bpmn2:sequenceFlow id="Flow_06kumr8" sourceRef="Activity_1dcslsv" targetRef="task_modify_task1" />
     <bpmn2:boundaryEvent id="Event_catch_withdraw" name="撤回" attachedToRef="task_approve_task_async">
       <bpmn2:outgoing>Flow_0nzbvpg</bpmn2:outgoing>
       <bpmn2:signalEventDefinition id="SignalEventDefinition_111s194" signalRef="Signal_0hhmd7l" />
@@ -159,27 +310,8 @@ next();
       <bpmn2:outgoing>Flow_0iv9g3p</bpmn2:outgoing>
     </bpmn2:exclusiveGateway>
     <bpmn2:sequenceFlow id="Flow_02nannf" sourceRef="Gateway_1whgd2i" targetRef="task_approve_task_async" />
-    <bpmn2:userTask id="task_approve_task_sync" name="审核任务成果" camunda:formKey="task_approve" camunda:assignee="task_checker">
-      <bpmn2:documentation>状态变为审核中(50)</bpmn2:documentation>
-      <bpmn2:extensionElements>
-        <camunda:inputOutput>
-          <camunda:inputParameter name="show_in_my_works">${true}</camunda:inputParameter>
-          <camunda:inputParameter name="process_type">${1}</camunda:inputParameter>
-          <camunda:inputParameter name="target_type"> project-plan</camunda:inputParameter>
-          <camunda:inputParameter name="prj_id">${environment.variables.prj_id}</camunda:inputParameter>
-          <camunda:inputParameter name="task_status">${50}</camunda:inputParameter>
-          <camunda:inputParameter name="draft">${false}</camunda:inputParameter>
-          <camunda:inputParameter name="task_id">${environment.variables.task_id}</camunda:inputParameter>
-        </camunda:inputOutput>
-      </bpmn2:extensionElements>
-      <bpmn2:incoming>Flow_0iv9g3p</bpmn2:incoming>
-      <bpmn2:outgoing>Flow_1su3b3p</bpmn2:outgoing>
-      <bpmn2:multiInstanceLoopCharacteristics camunda:collection="${content.handlers}" camunda:elementVariable="handler">
-        <bpmn2:loopCardinality xsi:type="bpmn2:tFormalExpression">${content.handlers.length}</bpmn2:loopCardinality>
-      </bpmn2:multiInstanceLoopCharacteristics>
-    </bpmn2:userTask>
     <bpmn2:sequenceFlow id="Flow_0iv9g3p" sourceRef="Gateway_1whgd2i" targetRef="task_approve_task_sync">
-      <bpmn2:conditionExpression xsi:type="bpmn2:tFormalExpression" language="JavaScript">next(null, this.environment.variables.check_type===1);</bpmn2:conditionExpression>
+      <bpmn2:conditionExpression xsi:type="bpmn2:tFormalExpression" language="JavaScript">next(null, this.environment.variables.check_type===2);</bpmn2:conditionExpression>
     </bpmn2:sequenceFlow>
     <bpmn2:sequenceFlow id="Flow_1su3b3p" sourceRef="task_approve_task_sync" targetRef="Activity_0gfds7w" />
     <bpmn2:sequenceFlow id="Flow_0b7s3zy" sourceRef="Event_1gpzq6w" targetRef="task_modify_task2" />
@@ -196,7 +328,9 @@ next();
     <bpmn2:scriptTask id="Activity_0gfds7w" name="计算审核结果" scriptFormat="JavaScript">
       <bpmn2:incoming>Flow_1su3b3p</bpmn2:incoming>
       <bpmn2:outgoing>Flow_1rcwh5r</bpmn2:outgoing>
-      <bpmn2:script>// 只要有一个未通过,就算未通过审核
+      <bpmn2:script>
+try {
+// 只要有一个未通过,就算未通过审核
 var passed = true;
 let a = this.environment.services.get_value('a');
 for (var i = 0; i &lt; this.environment.output.task_approve_task_sync.length; i++) {
@@ -207,7 +341,69 @@ for (var i = 0; i &lt; this.environment.output.task_approve_task_sync.length; i+
     }
 }
 this.environment.variables.pass = passed;
-next();</bpmn2:script>
+} catch (e) {
+    this.environment.variables.pass = false;
+} finally {
+        next();
+}
+      </bpmn2:script>
+    </bpmn2:scriptTask>
+    <bpmn2:sequenceFlow id="Flow_09c65ah" sourceRef="Activity_0cir5fq" targetRef="Activity_0c44yrz" />
+    <bpmn2:scriptTask id="Activity_0cir5fq" name="归档交付物文件" scriptFormat="JavaScript">
+      <bpmn2:incoming>Flow_06cxzjx</bpmn2:incoming>
+      <bpmn2:outgoing>Flow_09c65ah</bpmn2:outgoing>
+      <bpmn2:script>
+(async function () {
+    try {
+        await this.environment.services.place_on_outcome("内部审核驳回");
+    } catch (e) {
+        this.environment.Logger('work').error(e);
+    } finally {
+        next();
+    }
+})(); 
+      </bpmn2:script>
+    </bpmn2:scriptTask>
+    <bpmn2:sequenceFlow id="Flow_1xp7bfo" sourceRef="Activity_10ywpg4" targetRef="Event_end" />
+    <bpmn2:scriptTask id="Activity_10ywpg4" name="归档交付物文件" scriptFormat="JavaScript">
+      <bpmn2:incoming>Flow_02taytj</bpmn2:incoming>
+      <bpmn2:outgoing>Flow_1xp7bfo</bpmn2:outgoing>
+      <bpmn2:script>
+(async function () {
+    try {
+        await this.environment.services.place_on_outcome("内部审核通过", "outcome");
+    } catch (e) {
+        this.environment.Logger('work').error(e);
+    } finally {
+        next();
+    }
+})(); 
+      </bpmn2:script>
+    </bpmn2:scriptTask>
+    <bpmn2:sequenceFlow id="Flow_1eauhgu" sourceRef="Activity_0c44yrz" targetRef="task_modify_task1" />
+    <bpmn2:scriptTask id="Activity_0c44yrz" name="提醒审核未通过" scriptFormat="JavaScript">
+      <bpmn2:incoming>Flow_09c65ah</bpmn2:incoming>
+      <bpmn2:outgoing>Flow_1eauhgu</bpmn2:outgoing>
+      <bpmn2:script>
+(async function () {
+    try {
+        let owner = await this.environment.services.owner(); // 获取任务责任人
+        if (!owner) {
+            await this.environment.services.gen_warn_task('admin', '提交任务审核时未找到负责人',
+                `提交任务审核时未找到负责人,流程出错。case id: ${this.environment.variables.id}`);
+        }
+        // 生成阅读任务
+            await this.environment.services.gen_remind_task(owner.id, `任务“${this.environment.variables.task_name}”的交付物审核未通过。`,
+                `任务“${this.environment.variables.task_name}”的交付物审核未通过,请修改后重新提交审核。`);
+        // 生成任务动态
+            await this.environment.services.logger(checker.id, checker.name, `任务“${this.environment.variables.task_name}”的交付物审核未通过。`);
+    } catch (e) {
+        this.environment.Logger('work').error(e);
+    } finally {
+        next();
+    }
+})(); 
+      </bpmn2:script>
     </bpmn2:scriptTask>
     <bpmn2:textAnnotation id="TextAnnotation_1vnw6c1">
       <bpmn2:text>所有人都审核通过才算通过</bpmn2:text>
@@ -227,12 +423,16 @@ next();</bpmn2:script>
         <dc:Bounds x="380" y="240" width="100" height="80" />
         <bpmndi:BPMNLabel />
       </bpmndi:BPMNShape>
+      <bpmndi:BPMNShape id="Activity_1wy7wpe_di" bpmnElement="task_approve_task_sync">
+        <dc:Bounds x="630" y="170" width="100" height="80" />
+        <bpmndi:BPMNLabel />
+      </bpmndi:BPMNShape>
       <bpmndi:BPMNShape id="Activity_0ds6ym0_di" bpmnElement="task_approve_task_async">
         <dc:Bounds x="630" y="305" width="100" height="80" />
         <bpmndi:BPMNLabel />
       </bpmndi:BPMNShape>
       <bpmndi:BPMNShape id="Gateway_06baw71_di" bpmnElement="gateway_approved" isMarkerVisible="true">
-        <dc:Bounds x="905" y="255" width="50" height="50" />
+        <dc:Bounds x="995" y="255" width="50" height="50" />
       </bpmndi:BPMNShape>
       <bpmndi:BPMNShape id="Activity_0fonc56_di" bpmnElement="task_with_draw_task">
         <dc:Bounds x="450" y="630" width="100" height="80" />
@@ -245,15 +445,15 @@ next();</bpmn2:script>
         </bpmndi:BPMNLabel>
       </bpmndi:BPMNShape>
       <bpmndi:BPMNShape id="Event_0tntlum_di" bpmnElement="event_throw_approved">
-        <dc:Bounds x="1032" y="262" width="36" height="36" />
+        <dc:Bounds x="1122" y="262" width="36" height="36" />
         <bpmndi:BPMNLabel>
-          <dc:Bounds x="1023" y="305" width="55" height="14" />
+          <dc:Bounds x="1113" y="305" width="55" height="14" />
         </bpmndi:BPMNLabel>
       </bpmndi:BPMNShape>
       <bpmndi:BPMNShape id="Event_1gjhb1n_di" bpmnElement="event_throw_approved_back">
-        <dc:Bounds x="912" y="442" width="36" height="36" />
+        <dc:Bounds x="1002" y="382" width="36" height="36" />
         <bpmndi:BPMNLabel>
-          <dc:Bounds x="952" y="453" width="55" height="14" />
+          <dc:Bounds x="1042" y="393" width="55" height="14" />
         </bpmndi:BPMNLabel>
       </bpmndi:BPMNShape>
       <bpmndi:BPMNShape id="Activity_0isolrc_di" bpmnElement="task_do_task">
@@ -264,19 +464,12 @@ next();</bpmn2:script>
         <dc:Bounds x="460" y="460" width="100" height="80" />
         <bpmndi:BPMNLabel />
       </bpmndi:BPMNShape>
-      <bpmndi:BPMNShape id="Activity_1fktaxz_di" bpmnElement="Activity_1dcslsv">
-        <dc:Bounds x="800" y="460" width="100" height="80" />
-      </bpmndi:BPMNShape>
       <bpmndi:BPMNShape id="Event_1jljt88_di" bpmnElement="Event_end">
-        <dc:Bounds x="1132" y="262" width="36" height="36" />
+        <dc:Bounds x="1352" y="262" width="36" height="36" />
       </bpmndi:BPMNShape>
       <bpmndi:BPMNShape id="Gateway_1whgd2i_di" bpmnElement="Gateway_1whgd2i" isMarkerVisible="true">
         <dc:Bounds x="525" y="255" width="50" height="50" />
       </bpmndi:BPMNShape>
-      <bpmndi:BPMNShape id="Activity_1wy7wpe_di" bpmnElement="task_approve_task_sync">
-        <dc:Bounds x="630" y="170" width="100" height="80" />
-        <bpmndi:BPMNLabel />
-      </bpmndi:BPMNShape>
       <bpmndi:BPMNShape id="Activity_09ebfre_di" bpmnElement="task_modify_task2">
         <dc:Bounds x="480" y="80" width="100" height="80" />
       </bpmndi:BPMNShape>
@@ -284,6 +477,18 @@ next();</bpmn2:script>
         <dc:Bounds x="760" y="170" width="100" height="80" />
         <bpmndi:BPMNLabel />
       </bpmndi:BPMNShape>
+      <bpmndi:BPMNShape id="Activity_1s88kbx_di" bpmnElement="Activity_0cir5fq">
+        <dc:Bounds x="960" y="480" width="100" height="80" />
+        <bpmndi:BPMNLabel />
+      </bpmndi:BPMNShape>
+      <bpmndi:BPMNShape id="Activity_0n7x8km_di" bpmnElement="Activity_10ywpg4">
+        <dc:Bounds x="1190" y="240" width="100" height="80" />
+        <bpmndi:BPMNLabel />
+      </bpmndi:BPMNShape>
+      <bpmndi:BPMNShape id="Activity_0apwby6_di" bpmnElement="Activity_0c44yrz">
+        <dc:Bounds x="760" y="480" width="100" height="80" />
+        <bpmndi:BPMNLabel />
+      </bpmndi:BPMNShape>
       <bpmndi:BPMNShape id="TextAnnotation_1vnw6c1_di" bpmnElement="TextAnnotation_1vnw6c1">
         <dc:Bounds x="860" y="90" width="100" height="41" />
         <bpmndi:BPMNLabel />
@@ -318,20 +523,20 @@ next();</bpmn2:script>
         <di:waypoint x="730" y="345" />
         <di:waypoint x="880" y="345" />
         <di:waypoint x="880" y="280" />
-        <di:waypoint x="900" y="280" />
+        <di:waypoint x="995" y="280" />
       </bpmndi:BPMNEdge>
       <bpmndi:BPMNEdge id="Flow_0zubkyw_di" bpmnElement="Flow_0zubkyw">
-        <di:waypoint x="955" y="280" />
-        <di:waypoint x="1032" y="280" />
+        <di:waypoint x="1045" y="280" />
+        <di:waypoint x="1122" y="280" />
         <bpmndi:BPMNLabel>
-          <dc:Bounds x="983" y="262" width="22" height="14" />
+          <dc:Bounds x="1073" y="262" width="22" height="14" />
         </bpmndi:BPMNLabel>
       </bpmndi:BPMNEdge>
       <bpmndi:BPMNEdge id="Flow_0nwaga3_di" bpmnElement="Flow_0nwaga3">
-        <di:waypoint x="930" y="305" />
-        <di:waypoint x="930" y="442" />
+        <di:waypoint x="1020" y="305" />
+        <di:waypoint x="1020" y="382" />
         <bpmndi:BPMNLabel>
-          <dc:Bounds x="934" y="342" width="22" height="14" />
+          <dc:Bounds x="1024" y="323" width="22" height="14" />
         </bpmndi:BPMNLabel>
       </bpmndi:BPMNEdge>
       <bpmndi:BPMNEdge id="Flow_0bhibn0_di" bpmnElement="Flow_0bhibn0">
@@ -344,14 +549,14 @@ next();</bpmn2:script>
         <di:waypoint x="662" y="670" />
       </bpmndi:BPMNEdge>
       <bpmndi:BPMNEdge id="Flow_02taytj_di" bpmnElement="Flow_02taytj">
-        <di:waypoint x="1068" y="280" />
-        <di:waypoint x="1132" y="280" />
+        <di:waypoint x="1158" y="280" />
+        <di:waypoint x="1190" y="280" />
       </bpmndi:BPMNEdge>
       <bpmndi:BPMNEdge id="Flow_1ucg773_di" bpmnElement="Flow_1ucg773">
         <di:waypoint x="500" y="612" />
         <di:waypoint x="500" y="590" />
-        <di:waypoint x="1150" y="590" />
-        <di:waypoint x="1150" y="298" />
+        <di:waypoint x="1370" y="590" />
+        <di:waypoint x="1370" y="298" />
       </bpmndi:BPMNEdge>
       <bpmndi:BPMNEdge id="Flow_1t45qnv_di" bpmnElement="Flow_1t45qnv">
         <di:waypoint x="510" y="460" />
@@ -365,18 +570,13 @@ next();</bpmn2:script>
         <di:waypoint x="560" y="500" />
       </bpmndi:BPMNEdge>
       <bpmndi:BPMNEdge id="Flow_06cxzjx_di" bpmnElement="Flow_06cxzjx">
-        <di:waypoint x="930" y="478" />
-        <di:waypoint x="930" y="500" />
-        <di:waypoint x="900" y="500" />
+        <di:waypoint x="1020" y="418" />
+        <di:waypoint x="1020" y="480" />
       </bpmndi:BPMNEdge>
       <bpmndi:BPMNEdge id="Flow_0gyvyyo_di" bpmnElement="Flow_0gyvyyo">
         <di:waypoint x="330" y="280" />
         <di:waypoint x="380" y="280" />
       </bpmndi:BPMNEdge>
-      <bpmndi:BPMNEdge id="Flow_06kumr8_di" bpmnElement="Flow_06kumr8">
-        <di:waypoint x="800" y="520" />
-        <di:waypoint x="560" y="520" />
-      </bpmndi:BPMNEdge>
       <bpmndi:BPMNEdge id="Flow_02nannf_di" bpmnElement="Flow_02nannf">
         <di:waypoint x="550" y="305" />
         <di:waypoint x="550" y="345" />
@@ -405,7 +605,19 @@ next();</bpmn2:script>
         <di:waypoint x="860" y="210" />
         <di:waypoint x="880" y="210" />
         <di:waypoint x="880" y="280" />
-        <di:waypoint x="900" y="280" />
+        <di:waypoint x="995" y="280" />
+      </bpmndi:BPMNEdge>
+      <bpmndi:BPMNEdge id="Flow_09c65ah_di" bpmnElement="Flow_09c65ah">
+        <di:waypoint x="960" y="520" />
+        <di:waypoint x="860" y="520" />
+      </bpmndi:BPMNEdge>
+      <bpmndi:BPMNEdge id="Flow_1xp7bfo_di" bpmnElement="Flow_1xp7bfo">
+        <di:waypoint x="1290" y="280" />
+        <di:waypoint x="1352" y="280" />
+      </bpmndi:BPMNEdge>
+      <bpmndi:BPMNEdge id="Flow_1eauhgu_di" bpmnElement="Flow_1eauhgu">
+        <di:waypoint x="760" y="520" />
+        <di:waypoint x="560" y="520" />
       </bpmndi:BPMNEdge>
       <bpmndi:BPMNEdge id="Association_0p6rlsh_di" bpmnElement="Association_0p6rlsh">
         <di:waypoint x="848" y="170" />

+ 125 - 51
pmr-biz-manager/bpmn/立项流程.bpmn

@@ -15,6 +15,33 @@
           <camunda:inputParameter name="show_in_my_works">${false}</camunda:inputParameter>
           <camunda:inputParameter name="process_type">${1}</camunda:inputParameter>
         </camunda:inputOutput>
+        <camunda:executionListener event="end">
+          <camunda:script scriptFormat="JavaScript">
+(async function () {
+    try {
+        let owner = await this.environment.services.owner(); // 获取任务责任人
+        if (!owner) {
+            await this.environment.services.gen_warn_task('admin', '提交立项审核时未找到负责人',
+                `提交立项审核时未找到负责人,流程出错。case id: ${this.environment.variables.id}`);
+        }
+        // 获取项目审核人列表       
+        let checkers = await this.environment.services.get_handlers('project_checker');
+        if (!checkers || checkers.length === 0) return;
+        let checkers_str = "";
+        for (var i = 0; i &lt; checkers.length; i++) {
+            if (i &gt; 0) checkers_str += '、';
+            checkers_str += `${checkers[i].name}`;
+        }
+        await this.environment.services.gen_remind_task(owner.id, '立项申请已提交', `&lt;b&gt;${owner.name}&lt;/b&gt;已于 &lt;b&gt;` +
+            this.environment.services.get_datetime() + `&lt;/b&gt; 提交了立项申请,将由: &lt;br&gt; &lt;b&gt;${checkers_str}&lt;/b&gt; 进行立项审核。`);
+        await this.environment.services.logger(owner.id, owner.name, `提交了立项申请,将由: &lt;br&gt; &lt;b&gt;${checkers_str}&lt;/b&gt; &lt;br&gt;进行立项审核。`);
+        
+    } catch (e) {
+        this.environment.Logger('work').error(e);
+    }
+})();           
+</camunda:script>
+        </camunda:executionListener>
       </bpmn2:extensionElements>
       <bpmn2:incoming>Flow_1t45qnv</bpmn2:incoming>
       <bpmn2:incoming>Flow_0gyvyyo</bpmn2:incoming>
@@ -32,10 +59,34 @@
           <camunda:inputParameter name="process_type">${1}</camunda:inputParameter>
           <camunda:inputParameter name="target_type">project-detail</camunda:inputParameter>
           <camunda:inputParameter name="prj_id">${environment.variables.prj_id}</camunda:inputParameter>
-          <camunda:inputParameter name="prj_phase">overview_create</camunda:inputParameter>
-          <camunda:inputParameter name="prj_phase_name">立项审核中</camunda:inputParameter>
-          <camunda:inputParameter name="task_desc">{"memo": "{{}}"}</camunda:inputParameter>
         </camunda:inputOutput>
+        <camunda:executionListener event="end">
+          <camunda:script scriptFormat="JavaScript">
+(async function () {
+    try {
+        let owner = await this.environment.services.owner(); // 获取任务责任人
+        if (!owner) {
+            await this.environment.services.gen_warn_task('admin', '立项申请时未找到负责人',
+                `立项申请时未找到负责人,流程出错。case id: ${this.environment.variables.id}`);
+        }
+        this.environment.Logger('work').debug(this.content);
+        let checker = this.content.handlers[this.content.index]; // 获取当前审核人,这个是循环任务,有多个审核人依次审核
+        let result = this.content.output[this.content.index]; // 来自于审核人表单结果
+        if (result.pass) { // 如果审核通过
+            await this.environment.services.gen_remind_task(owner.id, `${checker.name}已批准立项`,
+                '&lt;b&gt;' + checker.name + '&lt;/b&gt;已于 &lt;b&gt;' + this.environment.services.get_datetime() + '&lt;/b&gt; 批准立项。审核意见如下:&lt;br&gt;' + result.opinion);
+            await this.environment.services.logger(checker.id, checker.name, '批准立项,审核意见:' + result.opinion);
+        } else {
+            await this.environment.services.gen_remind_task(owner.id, `${checker.name}已驳回立项`,
+                '&lt;b&gt;' + checker.name + '&lt;/b&gt;已于 &lt;b&gt;' + this.environment.services.get_datetime() + '&lt;/b&gt;  驳回立项。审核意见如下:&lt;br&gt;' + result.opinion);
+            await this.environment.services.logger(checker.id, checker.name, '驳回立项,审核意见:&lt;span style="color: red;"&gt;' + result.opinion + '&lt;/span&gt;');
+        }
+    } catch (e) {
+        this.environment.Logger('work').error(e);
+    }
+})();          
+</camunda:script>
+        </camunda:executionListener>
       </bpmn2:extensionElements>
       <bpmn2:incoming>Flow_0jpbzcr</bpmn2:incoming>
       <bpmn2:outgoing>Flow_193tusd</bpmn2:outgoing>
@@ -72,6 +123,24 @@
         <camunda:inputOutput>
           <camunda:inputParameter name="process_type">${1}</camunda:inputParameter>
         </camunda:inputOutput>
+        <camunda:executionListener event="end">
+          <camunda:script scriptFormat="JavaScript">
+(async function() {
+    try{
+        let owner = await this.environment.services.owner(); // 获取任务责任人
+        if (!owner) {
+            await this.environment.services.gen_warn_task('admin', '立项申请被撤回时未找到负责人',
+                `立项申请被撤回时未找到负责人,流程出错。case id: ${this.environment.variables.id}`);
+        }
+        await this.environment.services.gen_remind_task(owner.id, '立项申请已被撤回',
+            owner.name+'已于 ' + this.environment.services.get_datetime() + ' 撤回立项申请。');
+        await this.environment.services.logger(owner.id, owner.name, `撤回了立项申请。`);
+    } catch(e){
+        this.environment.Logger('work').error(e);
+    }
+})();            
+</camunda:script>
+        </camunda:executionListener>
       </bpmn2:extensionElements>
       <bpmn2:incoming>Flow_12nug8s</bpmn2:incoming>
       <bpmn2:outgoing>Flow_0tj9nd6</bpmn2:outgoing>
@@ -109,7 +178,7 @@
     <bpmn2:sequenceFlow id="Flow_0nzbvpg" sourceRef="Event_catch_withdraw" targetRef="task_modify">
       <bpmn2:extensionElements>
         <camunda:properties>
-          <camunda:property name="prj_phase" value="new" />
+          <camunda:property name="prj_phase" value="reject_create" />
           <camunda:property name="prj_phase_name" value="立项申请已撤回" />
         </camunda:properties>
       </bpmn2:extensionElements>
@@ -123,23 +192,16 @@
       <bpmn2:extensionElements />
     </bpmn2:sequenceFlow>
     <bpmn2:manualTask id="task_modify" name="修改项目信息">
+      <bpmn2:extensionElements>
+        <camunda:executionListener event="start">
+          <camunda:script scriptFormat="JavaScript">console.log("test");</camunda:script>
+        </camunda:executionListener>
+      </bpmn2:extensionElements>
       <bpmn2:incoming>Flow_0nzbvpg</bpmn2:incoming>
-      <bpmn2:incoming>Flow_06kumr8</bpmn2:incoming>
       <bpmn2:incoming>Flow_01n3edp</bpmn2:incoming>
+      <bpmn2:incoming>Flow_0sb2avy</bpmn2:incoming>
       <bpmn2:outgoing>Flow_1t45qnv</bpmn2:outgoing>
     </bpmn2:manualTask>
-    <bpmn2:sequenceFlow id="Flow_06kumr8" sourceRef="Activity_1dcslsv" targetRef="task_modify" />
-    <bpmn2:scriptTask id="Activity_1dcslsv" name="提醒审核被驳回" scriptFormat="Javascript">
-      <bpmn2:extensionElements />
-      <bpmn2:incoming>Flow_0sb2avy</bpmn2:incoming>
-      <bpmn2:outgoing>Flow_06kumr8</bpmn2:outgoing>
-      <bpmn2:script>this.environment.services.get_handlers('owner').then((handlers) =&gt; {
-for (var i = 0; i &lt; handlers.length; i++) {
-  this.environment.services.gen_remind_task(handlers[i].id, '立项申请被驳回', '立项申请已被驳回');
-}
-next();
-});</bpmn2:script>
-    </bpmn2:scriptTask>
     <bpmn2:endEvent id="Event_end">
       <bpmn2:incoming>Flow_1ucg773</bpmn2:incoming>
       <bpmn2:incoming>Flow_0if8for</bpmn2:incoming>
@@ -173,12 +235,16 @@ this.environment.services.get_handlers('project_checker').then((handlers) =&gt;
     <bpmn2:scriptTask id="Activity_0zp61jx" name="提醒设置审核组成员" scriptFormat="JavaScript">
       <bpmn2:incoming>Flow_10qwx33</bpmn2:incoming>
       <bpmn2:outgoing>Flow_01n3edp</bpmn2:outgoing>
-      <bpmn2:script>this.environment.services.get_handlers('owner').then((handlers) =&gt; {
-for (var i = 0; i &lt; handlers.length; i++) {
-  this.environment.services.gen_remind_task(handlers[i].id, '提醒:没有配置审核组成员', '立项申请前,请先配置审核组成员,否则无法执行立项审核任务。');
-}
-next();
-});</bpmn2:script>
+      <bpmn2:script>try{
+  this.environment.services.get_handlers('owner').then((handlers) =&gt; {
+    for (var i = 0; i &lt; handlers.length; i++) {
+      this.environment.services.gen_remind_task(handlers[i].id, '提醒:没有配置审核组成员', '立项申请前,请先配置审核组成员,否则无法执行立项审核任务。');
+    }
+  next();
+  });
+}catch(e){
+   console.log(e);
+}</bpmn2:script>
     </bpmn2:scriptTask>
     <bpmn2:inclusiveGateway id="Gateway_1hhlhsh">
       <bpmn2:incoming>Flow_0a0l2t3</bpmn2:incoming>
@@ -186,22 +252,38 @@ next();
       <bpmn2:outgoing>Flow_0jpbzcr</bpmn2:outgoing>
     </bpmn2:inclusiveGateway>
     <bpmn2:sequenceFlow id="Flow_12nug8s" sourceRef="Gateway_1hhlhsh" targetRef="task_with_draw" />
-    <bpmn2:sequenceFlow id="Flow_0jpbzcr" sourceRef="Gateway_1hhlhsh" targetRef="task_approve" />
-    <bpmn2:sequenceFlow id="Flow_0sb2avy" sourceRef="task_place_on_file_reject" targetRef="Activity_1dcslsv" />
+    <bpmn2:sequenceFlow id="Flow_0jpbzcr" sourceRef="Gateway_1hhlhsh" targetRef="task_approve">
+      <bpmn2:extensionElements>
+        <camunda:properties>
+          <camunda:property name="prj_phase" value="overview_create" />
+          <camunda:property name="prj_phase_name" value="立项审核中" />
+        </camunda:properties>
+      </bpmn2:extensionElements>
+    </bpmn2:sequenceFlow>
+    <bpmn2:sequenceFlow id="Flow_0sb2avy" sourceRef="task_place_on_file_reject" targetRef="task_modify" />
     <bpmn2:scriptTask id="task_place_on_file_reject" name="归档审核未通过的立项文件" scriptFormat="Javascript">
       <bpmn2:incoming>Flow_06cxzjx</bpmn2:incoming>
       <bpmn2:outgoing>Flow_0sb2avy</bpmn2:outgoing>
-      <bpmn2:script>let files = this.environment.output.task_request.files;
+      <bpmn2:script>try{
+let files = this.environment.output.task_request.files;
 
-this.environment.services.place_on_files(files, "立项驳回").then(()=&gt; next());</bpmn2:script>
+this.environment.services.place_on_files(files, "立项驳回").then(()=&gt; next());
+} catch(e){
+this.environment.Logger('work').error(e); 
+next();      
+}</bpmn2:script>
     </bpmn2:scriptTask>
     <bpmn2:sequenceFlow id="Flow_0if8for" sourceRef="Activity_10dmh6u" targetRef="Event_end" />
     <bpmn2:scriptTask id="Activity_10dmh6u" name="归档审核通过的立项文件" scriptFormat="JavaScript">
       <bpmn2:incoming>Flow_02taytj</bpmn2:incoming>
       <bpmn2:outgoing>Flow_0if8for</bpmn2:outgoing>
-      <bpmn2:script>let files = this.environment.output.task_request.files;
+      <bpmn2:script>try{
+let files = this.environment.output.task_request.files;
 
-this.environment.services.place_on_files(files, "立项通过", "project_set_up").then(()=&gt; next());</bpmn2:script>
+this.environment.services.place_on_files(files, "立项通过", "project_set_up").then(()=&gt; next());
+}catch(e){
+next();
+}</bpmn2:script>
     </bpmn2:scriptTask>
   </bpmn2:process>
   <bpmn2:signal id="Signal_0hhmd7l" name="cancel_sign" />
@@ -218,7 +300,7 @@ this.environment.services.place_on_files(files, "立项通过", "project_set_up"
         <bpmndi:BPMNLabel />
       </bpmndi:BPMNShape>
       <bpmndi:BPMNShape id="Activity_0ds6ym0_di" bpmnElement="task_approve">
-        <dc:Bounds x="860" y="280" width="100" height="80" />
+        <dc:Bounds x="858" y="280" width="100" height="80" />
         <bpmndi:BPMNLabel />
       </bpmndi:BPMNShape>
       <bpmndi:BPMNShape id="Gateway_06baw71_di" bpmnElement="gateway_approved" isMarkerVisible="true">
@@ -247,7 +329,7 @@ this.environment.services.place_on_files(files, "立项通过", "project_set_up"
         </bpmndi:BPMNLabel>
       </bpmndi:BPMNShape>
       <bpmndi:BPMNShape id="Activity_0isolrc_di" bpmnElement="task_create">
-        <dc:Bounds x="250" y="190" width="100" height="80" />
+        <dc:Bounds x="230" y="190" width="100" height="80" />
         <bpmndi:BPMNLabel />
       </bpmndi:BPMNShape>
       <bpmndi:BPMNShape id="Activity_1k7kq2t_di" bpmnElement="task_modify">
@@ -268,9 +350,6 @@ this.environment.services.place_on_files(files, "立项通过", "project_set_up"
       <bpmndi:BPMNShape id="Gateway_06flgld_di" bpmnElement="Gateway_1hhlhsh">
         <dc:Bounds x="765" y="155" width="50" height="50" />
       </bpmndi:BPMNShape>
-      <bpmndi:BPMNShape id="Activity_1fktaxz_di" bpmnElement="Activity_1dcslsv">
-        <dc:Bounds x="600" y="540" width="100" height="80" />
-      </bpmndi:BPMNShape>
       <bpmndi:BPMNShape id="Activity_0hdmx5i_di" bpmnElement="task_place_on_file_reject">
         <dc:Bounds x="860" y="540" width="100" height="80" />
         <bpmndi:BPMNLabel />
@@ -285,21 +364,21 @@ this.environment.services.place_on_files(files, "立项通过", "project_set_up"
         </bpmndi:BPMNLabel>
       </bpmndi:BPMNShape>
       <bpmndi:BPMNShape id="Event_0s1xb2l_di" bpmnElement="Event_catch_withdraw">
-        <dc:Bounds x="922" y="342" width="36" height="36" />
+        <dc:Bounds x="920" y="342" width="36" height="36" />
         <bpmndi:BPMNLabel>
-          <dc:Bounds x="949" y="378" width="22" height="14" />
+          <dc:Bounds x="947" y="378" width="22" height="14" />
         </bpmndi:BPMNLabel>
       </bpmndi:BPMNShape>
       <bpmndi:BPMNEdge id="Flow_15zn1tu_di" bpmnElement="Flow_15zn1tu">
         <di:waypoint x="188" y="230" />
-        <di:waypoint x="250" y="230" />
+        <di:waypoint x="230" y="230" />
       </bpmndi:BPMNEdge>
       <bpmndi:BPMNEdge id="Flow_1rg4oob_di" bpmnElement="Flow_1rg4oob">
         <di:waypoint x="480" y="230" />
         <di:waypoint x="510" y="230" />
       </bpmndi:BPMNEdge>
       <bpmndi:BPMNEdge id="Flow_193tusd_di" bpmnElement="Flow_193tusd">
-        <di:waypoint x="960" y="320" />
+        <di:waypoint x="958" y="320" />
         <di:waypoint x="1115" y="320" />
       </bpmndi:BPMNEdge>
       <bpmndi:BPMNEdge id="Flow_0zubkyw_di" bpmnElement="Flow_0zubkyw">
@@ -337,8 +416,8 @@ this.environment.services.place_on_files(files, "立项通过", "project_set_up"
         <di:waypoint x="450" y="270" />
       </bpmndi:BPMNEdge>
       <bpmndi:BPMNEdge id="Flow_0nzbvpg_di" bpmnElement="Flow_0nzbvpg">
-        <di:waypoint x="940" y="378" />
-        <di:waypoint x="940" y="440" />
+        <di:waypoint x="938" y="378" />
+        <di:waypoint x="938" y="440" />
         <di:waypoint x="590" y="440" />
       </bpmndi:BPMNEdge>
       <bpmndi:BPMNEdge id="Flow_06cxzjx_di" bpmnElement="Flow_06cxzjx">
@@ -347,14 +426,9 @@ this.environment.services.place_on_files(files, "立项通过", "project_set_up"
         <di:waypoint x="960" y="580" />
       </bpmndi:BPMNEdge>
       <bpmndi:BPMNEdge id="Flow_0gyvyyo_di" bpmnElement="Flow_0gyvyyo">
-        <di:waypoint x="350" y="230" />
+        <di:waypoint x="330" y="230" />
         <di:waypoint x="380" y="230" />
       </bpmndi:BPMNEdge>
-      <bpmndi:BPMNEdge id="Flow_06kumr8_di" bpmnElement="Flow_06kumr8">
-        <di:waypoint x="600" y="580" />
-        <di:waypoint x="530" y="580" />
-        <di:waypoint x="530" y="490" />
-      </bpmndi:BPMNEdge>
       <bpmndi:BPMNEdge id="Flow_01ekshh_di" bpmnElement="Flow_01ekshh">
         <di:waypoint x="610" y="230" />
         <di:waypoint x="655" y="230" />
@@ -373,9 +447,7 @@ this.environment.services.place_on_files(files, "立项通过", "project_set_up"
       </bpmndi:BPMNEdge>
       <bpmndi:BPMNEdge id="Flow_01n3edp_di" bpmnElement="Flow_01n3edp">
         <di:waypoint x="680" y="360" />
-        <di:waypoint x="680" y="400" />
-        <di:waypoint x="626" y="400" />
-        <di:waypoint x="626" y="430" />
+        <di:waypoint x="680" y="430" />
         <di:waypoint x="590" y="430" />
       </bpmndi:BPMNEdge>
       <bpmndi:BPMNEdge id="Flow_12nug8s_di" bpmnElement="Flow_12nug8s">
@@ -386,11 +458,13 @@ this.environment.services.place_on_files(files, "立项通过", "project_set_up"
       <bpmndi:BPMNEdge id="Flow_0jpbzcr_di" bpmnElement="Flow_0jpbzcr">
         <di:waypoint x="790" y="205" />
         <di:waypoint x="790" y="320" />
-        <di:waypoint x="860" y="320" />
+        <di:waypoint x="858" y="320" />
       </bpmndi:BPMNEdge>
       <bpmndi:BPMNEdge id="Flow_0sb2avy_di" bpmnElement="Flow_0sb2avy">
         <di:waypoint x="860" y="580" />
-        <di:waypoint x="700" y="580" />
+        <di:waypoint x="725" y="580" />
+        <di:waypoint x="725" y="450" />
+        <di:waypoint x="590" y="450" />
       </bpmndi:BPMNEdge>
       <bpmndi:BPMNEdge id="Flow_0if8for_di" bpmnElement="Flow_0if8for">
         <di:waypoint x="1410" y="320" />

+ 84 - 28
pmr-biz-manager/bpmn/计划创建审核流程.bpmn

@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <bpmn2:definitions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmn2="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" id="sample-diagram" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="5.22.0" xsi:schemaLocation="http://www.omg.org/spec/BPMN/20100524/MODEL BPMN20.xsd">
-  <bpmn2:process id="ProcessPlanApprove" name="立项审批流程" isExecutable="true" camunda:versionTag="1.0">
+  <bpmn2:process id="ProcessPlanApprove" name="计划创建审批流程" isExecutable="true" camunda:versionTag="1.0">
     <bpmn2:startEvent id="event_start">
       <bpmn2:extensionElements>
         <camunda:properties>
@@ -9,14 +9,41 @@
       </bpmn2:extensionElements>
       <bpmn2:outgoing>Flow_15zn1tu</bpmn2:outgoing>
     </bpmn2:startEvent>
-    <bpmn2:userTask id="task_request_plan" name="申请审批计划" camunda:assignee="owner">
+    <bpmn2:userTask id="task_request_plan" name="申请创建计划" camunda:assignee="owner">
       <bpmn2:extensionElements>
         <camunda:inputOutput>
-          <camunda:inputParameter name="show_in_my_works">${true}</camunda:inputParameter>
+          <camunda:inputParameter name="show_in_my_works">${false}</camunda:inputParameter>
           <camunda:inputParameter name="process_type">${1}</camunda:inputParameter>
           <camunda:inputParameter name="target_type">project-plan</camunda:inputParameter>
           <camunda:inputParameter name="draft">${false}</camunda:inputParameter>
         </camunda:inputOutput>
+        <camunda:executionListener event="end">
+          <camunda:script scriptFormat="JavaScript">
+(async function () {
+    try {
+        let owner = await this.environment.services.owner(); // 获取任务责任人
+        if (!owner) {
+            await this.environment.services.gen_warn_task('admin', '提交计划审核时未找到负责人',
+                `提交计划审核时未找到负责人,流程出错。case id: ${this.environment.variables.id}`);
+        }
+        // 获取审核人列表       
+        let checkers = await this.environment.services.get_handlers('project_checker');
+        if (!checkers || checkers.length === 0) return;
+        let checkers_str = "";
+        for (var i = 0; i &lt; checkers.length; i++) {
+            if (i &gt; 0) checkers_str += '、';
+            checkers_str += `${checkers[i].name}`;
+        }
+        await this.environment.services.gen_remind_task(owner.id, '项目计划已提交', `&lt;b&gt;${owner.name}&lt;/b&gt;已于 &lt;b&gt;` +
+            this.environment.services.get_datetime() + `&lt;/b&gt; 提交了项目计划,将由: &lt;br&gt; &lt;b&gt;${checkers_str}&lt;/b&gt; 进行审核。`);
+        await this.environment.services.logger(owner.id, owner.name, `提交了项目计划,将由: &lt;br&gt; &lt;b&gt;${checkers_str}&lt;/b&gt; &lt;br&gt;进行审核。`);
+        
+    } catch (e) {
+        this.environment.Logger('work').error(e);
+    }
+})();             
+          </camunda:script>
+        </camunda:executionListener>
       </bpmn2:extensionElements>
       <bpmn2:incoming>Flow_1t45qnv</bpmn2:incoming>
       <bpmn2:incoming>Flow_0gyvyyo</bpmn2:incoming>
@@ -27,20 +54,47 @@
     <bpmn2:sequenceFlow id="Flow_1rg4oob" sourceRef="task_request_plan" targetRef="task_approve_plan">
       <bpmn2:extensionElements>
         <camunda:properties>
-          <camunda:property name="prj_phase" value="apply_plan" />
+          <camunda:property name="prj_phase" value="overview_plan" />
+          <camunda:property name="prj_phase_name" value="计划审核中" />
         </camunda:properties>
       </bpmn2:extensionElements>
     </bpmn2:sequenceFlow>
-    <bpmn2:userTask id="task_approve_plan" name="审核计划" camunda:formKey="prj_approve" camunda:assignee="project_checker">
+    <bpmn2:userTask id="task_approve_plan" name="审核计划" camunda:formKey="plan_create_approve" camunda:assignee="project_checker">
       <bpmn2:extensionElements>
         <camunda:inputOutput>
           <camunda:inputParameter name="show_in_my_works">${true}</camunda:inputParameter>
           <camunda:inputParameter name="process_type">${1}</camunda:inputParameter>
           <camunda:inputParameter name="target_type"> project-plan</camunda:inputParameter>
           <camunda:inputParameter name="prj_id">${environment.variables.prj_id}</camunda:inputParameter>
-          <camunda:inputParameter name="prj_phase">overview_plan</camunda:inputParameter>
           <camunda:inputParameter name="draft">${false}</camunda:inputParameter>
         </camunda:inputOutput>
+        <camunda:executionListener event="end">
+          <camunda:script scriptFormat="JavaScript">
+(async function () {
+    try {
+        let owner = await this.environment.services.owner(); // 获取任务责任人
+        if (!owner) {
+            await this.environment.services.gen_warn_task('admin', '计划申请时未找到负责人',
+                `计划申请时未找到负责人,流程出错。case id: ${this.environment.variables.id}`);
+        }
+        this.environment.Logger('work').debug(this.content);
+        let checker = this.content.handlers[this.content.index]; // 获取当前审核人,这个是循环任务,有多个审核人依次审核
+        let result = this.content.output[this.content.index]; // 来自于审核人表单结果
+        if (result.pass) { // 如果审核通过
+            await this.environment.services.gen_remind_task(owner.id, `${checker.name}已批准项目计划`,
+                '&lt;b&gt;' + checker.name + '&lt;/b&gt;已于 &lt;b&gt;' + this.environment.services.get_datetime() + '&lt;/b&gt; 批准项目计划。审核意见如下:&lt;br&gt;' + result.opinion);
+            await this.environment.services.logger(checker.id, checker.name, '批准项目计划,审核意见:' + result.opinion);
+        } else {
+            await this.environment.services.gen_remind_task(owner.id, `${checker.name}已驳回项目计划`,
+                '&lt;b&gt;' + checker.name + '&lt;/b&gt;已于 &lt;b&gt;' + this.environment.services.get_datetime() + '&lt;/b&gt;  驳回项目计划。审核意见如下:&lt;br&gt;' + result.opinion);
+            await this.environment.services.logger(checker.id, checker.name, '驳回项目计划,审核意见:&lt;span style="color: red;"&gt;' + result.opinion + '&lt;/span&gt;');
+        }
+    } catch (e) {
+        this.environment.Logger('work').error(e);
+    }
+})();  
+          </camunda:script>
+        </camunda:executionListener>
       </bpmn2:extensionElements>
       <bpmn2:incoming>Flow_1rg4oob</bpmn2:incoming>
       <bpmn2:outgoing>Flow_193tusd</bpmn2:outgoing>
@@ -59,6 +113,7 @@
       <bpmn2:extensionElements>
         <camunda:properties>
           <camunda:property name="prj_phase" value="doing" />
+          <camunda:property name="prj_phase_name" value="执行中" />
         </camunda:properties>
       </bpmn2:extensionElements>
       <bpmn2:conditionExpression xsi:type="bpmn2:tFormalExpression" language="JavaScript">next(null, this.environment.variables.pass===true);</bpmn2:conditionExpression>
@@ -67,6 +122,7 @@
       <bpmn2:extensionElements>
         <camunda:properties>
           <camunda:property name="prj_phase" value="reject_plan" />
+          <camunda:property name="prj_phase_name" value="计划被驳回" />
         </camunda:properties>
       </bpmn2:extensionElements>
     </bpmn2:sequenceFlow>
@@ -76,6 +132,24 @@
         <camunda:inputOutput>
           <camunda:inputParameter name="process_type">${1}</camunda:inputParameter>
         </camunda:inputOutput>
+        <camunda:executionListener event="end">
+          <camunda:script scriptFormat="JavaScript">
+(async function() {
+    try{
+        let owner = await this.environment.services.owner(); // 获取任务责任人
+        if (!owner) {
+            await this.environment.services.gen_warn_task('admin', '项目计划申请被撤回时未找到负责人',
+                `项目计划申请被撤回时未找到负责人,流程出错。case id: ${this.environment.variables.id}`);
+        }
+        await this.environment.services.gen_remind_task(owner.id, '项目计划已被撤回',
+            owner.name+'已于 ' + this.environment.services.get_datetime() + ' 撤回项目计划。');
+        await this.environment.services.logger(owner.id, owner.name, `撤回了项目计划申请。`);
+    } catch(e){
+        this.environment.Logger('work').error(e);
+    }
+})(); 
+          </camunda:script>
+        </camunda:executionListener>
       </bpmn2:extensionElements>
       <bpmn2:incoming>Flow_0bhibn0</bpmn2:incoming>
       <bpmn2:outgoing>Flow_0tj9nd6</bpmn2:outgoing>
@@ -84,6 +158,7 @@
       <bpmn2:extensionElements>
         <camunda:properties>
           <camunda:property name="prj_phase" value="created" />
+          <camunda:property name="prj_phase_name" value="计划申请已撤回" />
         </camunda:properties>
       </bpmn2:extensionElements>
     </bpmn2:sequenceFlow>
@@ -117,7 +192,7 @@
     <bpmn2:sequenceFlow id="Flow_0nzbvpg" sourceRef="Event_catch_withdraw" targetRef="task_modify">
       <bpmn2:extensionElements />
     </bpmn2:sequenceFlow>
-    <bpmn2:sequenceFlow id="Flow_06cxzjx" sourceRef="event_throw_approved_back" targetRef="Activity_1dcslsv" />
+    <bpmn2:sequenceFlow id="Flow_06cxzjx" sourceRef="event_throw_approved_back" targetRef="task_modify" />
     <bpmn2:manualTask id="task_create_plan" name="创建计划">
       <bpmn2:incoming>Flow_15zn1tu</bpmn2:incoming>
       <bpmn2:outgoing>Flow_0gyvyyo</bpmn2:outgoing>
@@ -127,21 +202,9 @@
     </bpmn2:sequenceFlow>
     <bpmn2:manualTask id="task_modify" name="修改计划信息">
       <bpmn2:incoming>Flow_0nzbvpg</bpmn2:incoming>
-      <bpmn2:incoming>Flow_06kumr8</bpmn2:incoming>
+      <bpmn2:incoming>Flow_06cxzjx</bpmn2:incoming>
       <bpmn2:outgoing>Flow_1t45qnv</bpmn2:outgoing>
     </bpmn2:manualTask>
-    <bpmn2:sequenceFlow id="Flow_06kumr8" sourceRef="Activity_1dcslsv" targetRef="task_modify" />
-    <bpmn2:scriptTask id="Activity_1dcslsv" name="提醒审核被驳回" scriptFormat="Javascript">
-      <bpmn2:extensionElements />
-      <bpmn2:incoming>Flow_06cxzjx</bpmn2:incoming>
-      <bpmn2:outgoing>Flow_06kumr8</bpmn2:outgoing>
-      <bpmn2:script>this.environment.services.get_handlers('owner').then((handlers) =&gt; {
-for (var i = 0; i &lt; handlers.length; i++) {
-  this.environment.services.gen_remind_task(handlers[i].id, '计划申请被驳回', '计划申请已被驳回');
-}
-next();
-});</bpmn2:script>
-    </bpmn2:scriptTask>
     <bpmn2:endEvent id="Event_end">
       <bpmn2:incoming>Flow_1ucg773</bpmn2:incoming>
       <bpmn2:incoming>Flow_02taytj</bpmn2:incoming>
@@ -197,9 +260,6 @@ next();
         <dc:Bounds x="460" y="240" width="100" height="80" />
         <bpmndi:BPMNLabel />
       </bpmndi:BPMNShape>
-      <bpmndi:BPMNShape id="Activity_1fktaxz_di" bpmnElement="Activity_1dcslsv">
-        <dc:Bounds x="700" y="240" width="100" height="80" />
-      </bpmndi:BPMNShape>
       <bpmndi:BPMNShape id="Event_1jljt88_di" bpmnElement="Event_end">
         <dc:Bounds x="1072" y="132" width="36" height="36" />
       </bpmndi:BPMNShape>
@@ -274,16 +334,12 @@ next();
       <bpmndi:BPMNEdge id="Flow_06cxzjx_di" bpmnElement="Flow_06cxzjx">
         <di:waypoint x="870" y="258" />
         <di:waypoint x="870" y="280" />
-        <di:waypoint x="800" y="280" />
+        <di:waypoint x="560" y="280" />
       </bpmndi:BPMNEdge>
       <bpmndi:BPMNEdge id="Flow_0gyvyyo_di" bpmnElement="Flow_0gyvyyo">
         <di:waypoint x="350" y="150" />
         <di:waypoint x="380" y="150" />
       </bpmndi:BPMNEdge>
-      <bpmndi:BPMNEdge id="Flow_06kumr8_di" bpmnElement="Flow_06kumr8">
-        <di:waypoint x="700" y="280" />
-        <di:waypoint x="560" y="280" />
-      </bpmndi:BPMNEdge>
     </bpmndi:BPMNPlane>
   </bpmndi:BPMNDiagram>
 </bpmn2:definitions>

+ 79 - 25
pmr-biz-manager/bpmn/计划变更审核流程.bpmn

@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <bpmn2:definitions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmn2="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" id="sample-diagram" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="5.22.0" xsi:schemaLocation="http://www.omg.org/spec/BPMN/20100524/MODEL BPMN20.xsd">
-  <bpmn2:process id="ProcessPlanApprove" name="立项审批流程" isExecutable="true" camunda:versionTag="1.0">
+  <bpmn2:process id="ProcessPlanApprove" name="计划变更审批流程" isExecutable="true" camunda:versionTag="1.0">
     <bpmn2:startEvent id="event_start">
       <bpmn2:extensionElements>
         <camunda:properties>
@@ -12,11 +12,38 @@
     <bpmn2:userTask id="task_request_plan_alter" name="申请变更计划" camunda:assignee="owner">
       <bpmn2:extensionElements>
         <camunda:inputOutput>
-          <camunda:inputParameter name="show_in_my_works">${true}</camunda:inputParameter>
+          <camunda:inputParameter name="show_in_my_works">${false}</camunda:inputParameter>
           <camunda:inputParameter name="process_type">${1}</camunda:inputParameter>
           <camunda:inputParameter name="target_type">project-plan</camunda:inputParameter>
           <camunda:inputParameter name="draft">${true}</camunda:inputParameter>
         </camunda:inputOutput>
+        <camunda:executionListener event="end">
+          <camunda:script scriptFormat="JavaScript">
+(async function () {
+    try {
+        let owner = await this.environment.services.owner(); // 获取任务责任人
+        if (!owner) {
+            await this.environment.services.gen_warn_task('admin', '提交计划变更审核时未找到负责人',
+                `提交计划变更审核时未找到负责人,流程出错。case id: ${this.environment.variables.id}`);
+        }
+        // 获取审核人列表       
+        let checkers = await this.environment.services.get_handlers('project_checker');
+        if (!checkers || checkers.length === 0) return;
+        let checkers_str = "";
+        for (var i = 0; i &lt; checkers.length; i++) {
+            if (i &gt; 0) checkers_str += '、';
+            checkers_str += `${checkers[i].name}`;
+        }
+        await this.environment.services.gen_remind_task(owner.id, '计划变更已提交', `&lt;b&gt;${owner.name}&lt;/b&gt;已于 &lt;b&gt;` +
+            this.environment.services.get_datetime() + `&lt;/b&gt; 提交了计划变更,将由: &lt;br&gt; &lt;b&gt;${checkers_str}&lt;/b&gt; 进行审核。`);
+        await this.environment.services.logger(owner.id, owner.name, `提交了计划变更,将由: &lt;br&gt; &lt;b&gt;${checkers_str}&lt;/b&gt; &lt;br&gt;进行审核。`);
+        
+    } catch (e) {
+        this.environment.Logger('work').error(e);
+    }
+})(); 
+          </camunda:script>
+        </camunda:executionListener>
       </bpmn2:extensionElements>
       <bpmn2:incoming>Flow_1t45qnv</bpmn2:incoming>
       <bpmn2:incoming>Flow_0gyvyyo</bpmn2:incoming>
@@ -31,7 +58,7 @@
         </camunda:properties>
       </bpmn2:extensionElements>
     </bpmn2:sequenceFlow>
-    <bpmn2:userTask id="task_approve_plan_alter" name="审核计划变更" camunda:formKey="prj_approve" camunda:assignee="project_checker">
+    <bpmn2:userTask id="task_approve_plan_alter" name="审核计划变更" camunda:formKey="plan_alt_approve" camunda:assignee="project_checker">
       <bpmn2:extensionElements>
         <camunda:inputOutput>
           <camunda:inputParameter name="show_in_my_works">${true}</camunda:inputParameter>
@@ -42,6 +69,33 @@
           <camunda:inputParameter name="draft">${true}</camunda:inputParameter>
           <camunda:inputParameter name="reverse_target">${{"target": "project-plan", "draft": false}}</camunda:inputParameter>
         </camunda:inputOutput>
+        <camunda:executionListener event="end">
+          <camunda:script scriptFormat="JavaScript">
+(async function () {
+    try {
+        let owner = await this.environment.services.owner(); // 获取任务责任人
+        if (!owner) {
+            await this.environment.services.gen_warn_task('admin', '计划变更申请时未找到负责人',
+                `计划变更申请时未找到负责人,流程出错。case id: ${this.environment.variables.id}`);
+        }
+        this.environment.Logger('work').debug(this.content);
+        let checker = this.content.handlers[this.content.index]; // 获取当前审核人,这个是循环任务,有多个审核人依次审核
+        let result = this.content.output[this.content.index]; // 来自于审核人表单结果
+        if (result.pass) { // 如果审核通过
+            await this.environment.services.gen_remind_task(owner.id, `${checker.name}已批准计划变更`,
+                '&lt;b&gt;' + checker.name + '&lt;/b&gt;已于 &lt;b&gt;' + this.environment.services.get_datetime() + '&lt;/b&gt; 批准计划变更。审核意见如下:&lt;br&gt;' + result.opinion);
+            await this.environment.services.logger(checker.id, checker.name, '批准计划变更,审核意见:' + result.opinion);
+        } else {
+            await this.environment.services.gen_remind_task(owner.id, `${checker.name}已驳回计划变更`,
+                '&lt;b&gt;' + checker.name + '&lt;/b&gt;已于 &lt;b&gt;' + this.environment.services.get_datetime() + '&lt;/b&gt;  驳回计划变更。审核意见如下:&lt;br&gt;' + result.opinion);
+            await this.environment.services.logger(checker.id, checker.name, '驳回计划变更,审核意见:&lt;span style="color: red;"&gt;' + result.opinion + '&lt;/span&gt;');
+        }
+    } catch (e) {
+        this.environment.Logger('work').error(e);
+    }
+})();  
+</camunda:script>
+        </camunda:executionListener>
       </bpmn2:extensionElements>
       <bpmn2:incoming>Flow_1rg4oob</bpmn2:incoming>
       <bpmn2:outgoing>Flow_193tusd</bpmn2:outgoing>
@@ -77,6 +131,25 @@
         <camunda:inputOutput>
           <camunda:inputParameter name="process_type">${1}</camunda:inputParameter>
         </camunda:inputOutput>
+        <camunda:executionListener event="end">
+          <camunda:script scriptFormat="JavaScript">
+(async function() {
+    try{
+        let owner = await this.environment.services.owner(); // 获取任务责任人
+        if (!owner) {
+            await this.environment.services.gen_warn_task('admin', '计划变更申请被撤回时未找到负责人',
+                `计划变更申请被撤回时未找到负责人,流程出错。case id: ${this.environment.variables.id}`);
+        }
+        await this.environment.services.gen_remind_task(owner.id, '计划变更已被撤回',
+            owner.name+'已于 ' + this.environment.services.get_datetime() + ' 撤回计划变更申请。');
+        await this.environment.services.logger(owner.id, owner.name, `撤回了计划变更申请。`);    
+
+    } catch(e){
+        this.environment.Logger('work').error(e);
+    }
+})(); 
+</camunda:script>
+        </camunda:executionListener>
       </bpmn2:extensionElements>
       <bpmn2:incoming>Flow_0bhibn0</bpmn2:incoming>
       <bpmn2:outgoing>Flow_0tj9nd6</bpmn2:outgoing>
@@ -118,7 +191,7 @@
     <bpmn2:sequenceFlow id="Flow_0nzbvpg" sourceRef="Event_catch_withdraw" targetRef="task_modify_plan_alter">
       <bpmn2:extensionElements />
     </bpmn2:sequenceFlow>
-    <bpmn2:sequenceFlow id="Flow_06cxzjx" sourceRef="event_throw_approved_back" targetRef="Activity_1dcslsv" />
+    <bpmn2:sequenceFlow id="Flow_06cxzjx" sourceRef="event_throw_approved_back" targetRef="task_modify_plan_alter" />
     <bpmn2:manualTask id="task_modify_plan" name="修改计划">
       <bpmn2:incoming>Flow_15zn1tu</bpmn2:incoming>
       <bpmn2:outgoing>Flow_0gyvyyo</bpmn2:outgoing>
@@ -128,21 +201,9 @@
     </bpmn2:sequenceFlow>
     <bpmn2:manualTask id="task_modify_plan_alter" name="修改计划信息">
       <bpmn2:incoming>Flow_0nzbvpg</bpmn2:incoming>
-      <bpmn2:incoming>Flow_06kumr8</bpmn2:incoming>
+      <bpmn2:incoming>Flow_06cxzjx</bpmn2:incoming>
       <bpmn2:outgoing>Flow_1t45qnv</bpmn2:outgoing>
     </bpmn2:manualTask>
-    <bpmn2:sequenceFlow id="Flow_06kumr8" sourceRef="Activity_1dcslsv" targetRef="task_modify_plan_alter" />
-    <bpmn2:scriptTask id="Activity_1dcslsv" name="提醒审核被驳回" scriptFormat="Javascript">
-      <bpmn2:extensionElements />
-      <bpmn2:incoming>Flow_06cxzjx</bpmn2:incoming>
-      <bpmn2:outgoing>Flow_06kumr8</bpmn2:outgoing>
-      <bpmn2:script>this.environment.services.get_handlers('owner').then((handlers) =&gt; {
-for (var i = 0; i &lt; handlers.length; i++) {
-  this.environment.services.gen_remind_task(handlers[i].id, '计划变更申请被驳回', '计划变更申请已被驳回');
-}
-next();
-});</bpmn2:script>
-    </bpmn2:scriptTask>
     <bpmn2:endEvent id="Event_end">
       <bpmn2:incoming>Flow_1ucg773</bpmn2:incoming>
       <bpmn2:incoming>Flow_02taytj</bpmn2:incoming>
@@ -198,9 +259,6 @@ next();
         <dc:Bounds x="460" y="240" width="100" height="80" />
         <bpmndi:BPMNLabel />
       </bpmndi:BPMNShape>
-      <bpmndi:BPMNShape id="Activity_1fktaxz_di" bpmnElement="Activity_1dcslsv">
-        <dc:Bounds x="700" y="240" width="100" height="80" />
-      </bpmndi:BPMNShape>
       <bpmndi:BPMNShape id="Event_1jljt88_di" bpmnElement="Event_end">
         <dc:Bounds x="1072" y="132" width="36" height="36" />
       </bpmndi:BPMNShape>
@@ -275,16 +333,12 @@ next();
       <bpmndi:BPMNEdge id="Flow_06cxzjx_di" bpmnElement="Flow_06cxzjx">
         <di:waypoint x="870" y="258" />
         <di:waypoint x="870" y="280" />
-        <di:waypoint x="800" y="280" />
+        <di:waypoint x="560" y="280" />
       </bpmndi:BPMNEdge>
       <bpmndi:BPMNEdge id="Flow_0gyvyyo_di" bpmnElement="Flow_0gyvyyo">
         <di:waypoint x="350" y="150" />
         <di:waypoint x="380" y="150" />
       </bpmndi:BPMNEdge>
-      <bpmndi:BPMNEdge id="Flow_06kumr8_di" bpmnElement="Flow_06kumr8">
-        <di:waypoint x="700" y="280" />
-        <di:waypoint x="560" y="280" />
-      </bpmndi:BPMNEdge>
     </bpmndi:BPMNPlane>
   </bpmndi:BPMNDiagram>
 </bpmn2:definitions>

+ 189 - 0
pmr-biz-manager/readme.md

@@ -26,4 +26,193 @@ output: chr.output,
 state: 'iteration.completed',
 preventComplete: true
 });
+```
+
+立项申请
+```json
+{
+  "type": "object",
+  "title": "条件",
+  "required": [
+    "memo"
+  ],
+  "ui:order": [
+    "memo",
+    "files"
+  ],
+  "properties": {
+    "memo": {
+      "type": "string",
+      "title": "立项申请理由",
+      "format": "textarea",
+      "minRows": 10,
+      "description": "请输入立项申请的相关说明"
+    },
+    "files": {
+      "type": "upload",
+      "title": "立项文件",
+      "ui:options": {
+        "type": "upload",
+        "accept": ".doc,.docx,application/msword",
+        "limitCount": 1
+      },
+      "description": "上传立项相关文件,如可行性报告等"
+    }
+  }
+}
+```
+
+立项审核
+```json
+{
+  "type": "object",
+  "title": "条件",
+  "required": [
+    "pass"
+  ],
+  "ui:order": [
+    "memo",
+    "files",
+    "pass",
+    "opinion"
+  ],
+  "properties": {
+    "memo": {
+      "type": "string",
+      "title": "申请理由",
+      "format": "textarea",
+      "default": "${environment.output.task_request.memo}",
+      "readonly": true
+    },
+    "files": {
+      "type": "download",
+      "title": "立项文件",
+      "default": ${JSON.stringify(environment.output.task_request.files)}
+    },
+    "pass": {
+      "type": "boolean",
+      "title": "是否通过审核?",
+      "default": false
+    },
+    "opinion": {
+      "type": "string",
+      "title": "审核意见",
+      "format": "textarea",
+      "default": "",
+      "minRows": 6,
+      "minLength": 10,
+      "ui:options": {
+        "rows": 10,
+        "type": "textarea"
+      }
+    }
+  }
+}
+```
+
+选择审核方式
+```json
+{
+  "type": "object",
+  "title": "选择审核方式",
+  "required": [],
+  "ui:order": [
+    "check_type"
+  ],
+  "properties": {
+    "check_type": {
+      "enum": [
+        1,
+        2
+      ],
+      "type": "integer",
+      "title": "审核方式",
+      "enumNames": [
+        "顺序审核",
+        "并行审核"
+      ],
+      "description": "审核方式,目前是并行和串行"
+    }
+  }
+}
+```
+
+任务审核
+```json
+{
+  "type": "object",
+  "title": "条件",
+  "required": [
+    "pass"
+  ],
+  "ui:order": [
+    "pass",
+    "opinion",
+    "annotated_draft"
+  ],
+  "properties": {
+    "pass": {
+      "type": "boolean",
+      "title": "是否通过?",
+      "default": false
+    },
+    "opinion": {
+      "type": "upload",
+      "title": "审核意见",
+      "ui:options": {
+        "type": "upload",
+        "accept": ".doc,.docx,application/msword",
+        "limitCount": 1
+      }
+    },
+    "annotated_draft": {
+      "type": "upload",
+      "title": "批注稿",
+      "ui:options": {
+        "type": "upload",
+        "accept": ".doc,.docx,application/msword",
+        "limitCount": 1
+      }
+    }
+  }
+}
+```
+
+运行过程中,内部js的环境:environment
+```json
+{
+    "output": {
+      "task_request_task": {
+        "id": "task_request_task",
+        "executionId": "task_request_task_8b0c71a536",
+        "user_id": "admin",
+        "check_type": 1
+      }
+    },
+    "variables": {
+      "prj_id": "63f331ea0f400000",
+      "owner": "admin",
+      "task_id": "63f3321769400000",
+      "fields": {
+        "routingKey": "run.execute",
+        "exchange": "run",
+        "consumerTag": "_process-run"
+      },
+      "content": {
+        "id": "ProcessOutcomeApprove",
+        "type": "bpmn:Process",
+        "name": "立项审批流程",
+        "executionId": "ProcessOutcomeApprove_ab7febc2ca",
+        "parent": {
+          "id": "sample-diagram",
+          "type": "bpmn:Definitions"
+        }
+      },
+      "properties": {
+        "messageId": "smq.mid-2a55e1a76c",
+        "timestamp": 1717127830563
+      },
+      "check_type": 1
+    }
+}
 ```

+ 70 - 11
pmr-biz-manager/src/app.ts

@@ -16,7 +16,8 @@ import {BpmnCase} from "@core-models/BpmnCase";
 import {bpmn_flow_on_end} from "@src/utils/bpmn_work_helper";
 import {OutcomeStatus} from "@src/utils/define";
 import {prj_stat_monthly, task_delay_rate_daily, task_stat_daily} from "@src/utils/prj_stat";
-import {evaluate} from "@util/Executor";
+import {evaluate, executor} from "@util/Executor";
+import {PrjPlanTask} from "@core-models/PrjPlanTask";
 
 const schedule = require('node-schedule');
 
@@ -24,7 +25,7 @@ dayjs.extend(utc);
 dayjs.extend(timezone);
 dayjs.tz.guess();
 
-let result = evaluate(JSON.stringify({
+let json = `{
     "type": "object",
     "title": "条件",
     "required": [
@@ -39,9 +40,14 @@ let result = evaluate(JSON.stringify({
             "type": "string",
             "title": "申请理由",
             "format": "textarea",
-            "default": "${environment.output.task_request.memo}",
+            "default": "\${environment.output.task_request.memo}",
             "readonly": true
         },
+        "files": {
+            "type": "download",
+            "title": "附件",
+            "default": \${JSON.stringify(environment.output.task_request.files)}
+        },
         "pass": {
             "type": "boolean",
             "title": "是否通过审核?",
@@ -60,12 +66,52 @@ let result = evaluate(JSON.stringify({
             }
         }
     }
-}), {global: {environment: {output: {
-    task_request: {
-        memo: "memo text"
-    }
-            }}}})
+}`
+
+
+let result = executor(json, {global: {
+    environment: {
+        output: {
+            task_request: {
+                memo: "memo text",
+                files: [
+                    {
+                        id: '1',
+                        name: 'file1'
+                    },
+                    {
+                        id: '2',
+                        name: 'file2'
+                    }
+                ]
+            }
+        }
+    },
+         get_files: async (files: string[]) => {
+            let result: any[] = [];
+           for (let file of files) {
+               let f = await PrjFile.findOne({where: {id: file}});
+               if (f) {
+                   result.push({id: file, filename: f.filename});
+               }
+           }
+           return result;
+        },
+}})
 console.log(result);
+// result = executor(`
+//                     ((num) => {
+//                      return new Promise((resolve) => {
+//                           setTimeout(function () {
+//                             return resolve(Math.round(num))
+//                         }, 30);
+//                      });
+//                      })(\${test})
+//                 `, {global: {test: 1.5}})
+//
+// result.then((r) => {
+//     console.log(r)
+// });
 
 // dayjs.tz("2014-06-01 12:00", "Asia/Shanghai")
 new ServiceApp().start(new MyRouter('@src/routes', guards), Models).then(async () => {
@@ -115,7 +161,8 @@ new ServiceApp().start(new MyRouter('@src/routes', guards), Models).then(async (
                                         doc_size: record.size
                                     }, {
                                         where: {id: contract_id},
-                                        transaction: t
+                                        transaction: t,
+                                        returning: false
                                     })
                                     break;
                                 case '20':  // 扫描件
@@ -125,7 +172,8 @@ new ServiceApp().start(new MyRouter('@src/routes', guards), Models).then(async (
                                         pdf_size: record.size
                                     }, {
                                         where: {id: contract_id},
-                                        transaction: t
+                                        transaction: t,
+                                        returning: false
                                     })
                                     break;
                             }
@@ -159,10 +207,14 @@ new ServiceApp().start(new MyRouter('@src/routes', guards), Models).then(async (
 
     let flow_cases = await BpmnCase.findAll({where: {completed_at: null}, raw: true});
     for (let flow of flow_cases) {
+        let task = await PrjPlanTask.findOne({where: {id: flow.task_id}, raw: true});
+        if (!task) continue;
         await new FlowEngine(flow.model_id, flow.model, {
             prj_id: flow.prj_id,
             owner: flow.creator_id,
-            id: flow.id
+            id: flow.id,
+            task_id: flow.task_id,
+            task_name: task.name
         }, bpmn_flow_on_end).run(flow.state);
     }
 
@@ -173,6 +225,13 @@ new ServiceApp().start(new MyRouter('@src/routes', guards), Models).then(async (
         await task_delay_rate_daily();
     });
 
+    schedule.scheduleJob('0 * * * *', async function () {
+       FlowEngine.case_engine_map.forEach( (value, key) => {
+           console.log(key);
+           // console.log(value);
+       });
+    });
+
     // let bpmn = await BpmnModel.findOne({where: {id: 'task'}, raw: true});
     // if (bpmn) {
     //

+ 287 - 57
pmr-biz-manager/src/bpmn/flow_engine.ts

@@ -13,14 +13,19 @@ import {BpmnCase} from "@core-models/BpmnCase";
 import {BpmnWork} from "@core-models/BpmnWork";
 import {Logger} from "@util/Logger";
 import {BpmnForm} from "@core-models/BpmnForm";
-
+import BpmnModdle from 'bpmn-moddle';
+import Serializer, { TypeResolver } from 'moddle-context-serializer';
+import * as elements from 'bpmn-elements';
 const camunda = require('camunda-bpmn-moddle/resources/camunda.json');
 import {resolveExpression} from '@aircall/expression-parser';
 import {PrjPlanTask} from "@core-models/PrjPlanTask";
 import {IChecker} from "@src/utils/define";
 import {PrjLogger} from "@src/utils/prj_logger";
 import {PrjFile} from "@core-models/PrjFile";
-import {evaluate} from "@util/Executor";
+import {executor} from "@util/Executor";
+import {Script} from 'vm';
+import {BpmnModel} from "@core-models/BpmnModel";
+import {PrjTaskOutcome} from "@core-models/PrjTaskOutcome";
 
 export interface IHandler {
     id: string;         // 处理人账号id
@@ -34,7 +39,17 @@ export interface IFlowArgs {
     prj_id: string;
     owner: string;
     task_id?: string;
-    outcome_id?: string;
+    task_name?: string;
+}
+
+interface IFiles {
+    id: string;
+    name: string;
+}
+const kTypeResolver = Symbol.for('type resolver');
+
+async function delay(ms) {
+    return new Promise(resolve => setTimeout(resolve, ms));
 }
 
 export class FlowEngine extends EventEmitter {
@@ -66,6 +81,41 @@ export class FlowEngine extends EventEmitter {
         this._callback = callback;
         this.engine = this.new_engine(source, args);
         FlowEngine.case_engine_map.set(this._id, this);
+        this.engine.on('task.complete', (context) => {
+            const {task, scriptInvocation, output} = context;
+
+            if (scriptInvocation) {
+                const script = scriptInvocation.script;
+                console.log(`Task "${task.id}" completed with script result: ${script}`);
+            } else {
+                console.log(`Task "${task.id}" completed.`);
+            }
+
+            console.log(`Task "${task.id}" completed with output: ${output}`);
+        });
+    }
+
+    async getContext(source, options = {}) {
+        const moddleContext = await this.getModdleContext(source, options);
+
+        // if (moddleContext.warnings) {
+        //     moddleContext.warnings.forEach(({ error, message, element, property }) => {
+        //         if (error) return console.error(message);
+        //         console.error(`<${element.id}> ${property}:`, message);
+        //     });
+        // }
+
+        const types = TypeResolver({
+            ...elements,
+            // ...options.elements,
+        });
+
+        return Serializer(moddleContext, types);
+    }
+
+    getModdleContext(source, options) {
+        const bpmnModdle = new BpmnModdle(options);
+        return bpmnModdle.fromXML(source);
     }
 
     async run(state?: any) {
@@ -73,6 +123,7 @@ export class FlowEngine extends EventEmitter {
         listener.on('flow.take', this.on_flow_take);
         if (state) {
             this.engine = this.engine.recover(state);
+
             this.execution = await this.engine.resume({listener}, async (err, _execution) => {
                 if (err) console.log(err);
                 console.log('Execution completed with id', this.id);
@@ -85,6 +136,7 @@ export class FlowEngine extends EventEmitter {
                 await this.complete();
             });
         }
+
         Logger.info(`flow engine started: ${this.engine.environment}`);
     }
 
@@ -131,6 +183,20 @@ export class FlowEngine extends EventEmitter {
                 camunda,
             },
             services: {
+                owner: async () =>  {
+                    let owner = await AcsUserInfo.findOne({where: {id: this._owner}, raw: true});
+                    if (!owner) return;
+                    return {
+                        id: owner.id,
+                        name: owner.name
+                    };
+                },
+                get_datetime(): string {
+                    return dayjs().format('YYYY-MM-DD HH:mm:ss');
+                },
+                get_date(): string {
+                    return dayjs().format('YYYY-MM-DD');
+                },
                 get_value(key: string): any {
                     return self._shared_values.get(key);
                 },
@@ -140,13 +206,13 @@ export class FlowEngine extends EventEmitter {
                 get_handlers: (tag: string) => {
                     return this.get_handlers(tag);
                 },
-                gen_remind_task: (assigned_to: string, name: string, detail: string) => {
-                    BpmnWork.create({
-                        id: IdGen.id(),
+                gen_remind_task: async (assigned_to: string, name: string, detail: string, execution_id?: string) => {
+                    await BpmnWork.upsert({
+                        id: execution_id ? execution_id : IdGen.id(),
                         name: name,
-                        prj_id: this._prj_id,
-                        task_id: this._task_id ? this._task_id : null,
-                        case_id: this._id,
+                        prj_id: self._prj_id,
+                        task_id: self._task_id ? self._task_id : null,
+                        case_id: self._id,
                         assigned_to: assigned_to,
                         started_at: dayjs(),
                         process_type: 2,
@@ -154,32 +220,89 @@ export class FlowEngine extends EventEmitter {
                         desc: detail
                     })
                 },
-                set_prj_phase: (phase_id: string) => {
-                    PrjInfo.update({phase_id: phase_id}, {
-                        where: {id: this._prj_id},
+                logger: async (user_id: string, user_name: string, content: string) => {
+                    await PrjLogger.log({
+                        creator_id: user_id,
+                        creator_name: user_name,
+                        content: content,
+                        prj_id: self._prj_id,
+                        task_id: self._task_id,
+                    });
+                },
+                gen_warn_task: async (assigned_to: string, name: string, detail: string, execution_id?: string) => {
+                    await BpmnWork.upsert({
+                        id: execution_id ? execution_id : IdGen.id(),
+                        name: name,
+                        prj_id: self._prj_id,
+                        task_id: self._task_id ? self._task_id : null,
+                        case_id: self._id,
+                        assigned_to: assigned_to,
+                        started_at: dayjs(),
+                        process_type: 3,
+                        show_in_my_works: true,
+                        desc: detail
+                    })
+                },
+                set_prj_phase: async (phase_id: string) => {
+                   await PrjInfo.update({phase_id: phase_id}, {
+                        where: {id: self._prj_id},
                         returning: false
                     })
                 },
-                // 将流程中的文件进行归档
-                place_on_files: async (files: string | undefined, suffix: string, category_id?: string, _memo?: string) => {
+                // 归档任务交付物文档
+                place_on_outcome: async (suffix: string, category_id?: string, _memo?: string) => {
+                    console.log('$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$' + self._task_id);
+
+                    Logger.info(`place on outcome, _memo: ${_memo}, category_id: ${category_id}, suffix: ${suffix}`)
+                    if (!self._task_id) return;
+                    Logger.error('place on outcome, _memo: ${_memo}, category_id: ${category_id}, suffix: ${suffix}');
+                    let files = await PrjTaskOutcome.findAll({
+                        where: {
+                            task_id: self._task_id,
+                        },
+                        raw: true
+                    });
+                    Logger.error(files);
+                    for (let file of files) {
+                        let prj_file = await PrjFile.findOne({
+                            where: {id: file.object_name},raw: true
+                        });
+                        Logger.error(prj_file);
+                        if (!prj_file || !prj_file.uploaded) return;
+                        let filename = prj_file.filename;
+                        // 分离文件的文件名和后缀到一个数组中
+                        let file_parts = filename.split('.');
+                        // 将文档时间和附加的suffix后缀插入的文件名后
+                        let date = dayjs(prj_file.uploaded_at ? prj_file.uploaded_at : new Date()).format('YYYY-MM-DD');
+                        file_parts.splice(file_parts.length > 1 ? file_parts.length - 1 : file_parts.length, 0, `${suffix}(${date})`);
+                        filename = file_parts.join('.');
+                        Logger.info(`place on outcome, filename: ${filename}, category_id: ${category_id}, suffix: ${suffix}`)
+                        await PrjFile.update({
+                            filename: filename,
+                            category_id: category_id
+                        }, {where: {id: file.object_name}, returning: false});
+                    }
+                },
+                // 将流程表单中提交的文件进行归档
+                place_on_files: async (files: IFiles[] | undefined, suffix: string, category_id?: string, _memo?: string) => {
                     Logger.trace(files);
                     if (!files) return;
-                    for (let file_id of files) {
-                        let file = await PrjFile.findOne({
-                            where: {id: file_id}
+                    for (let file of files) {
+                        let prj_file = await PrjFile.findOne({
+                            where: {id: file.id}
                         });
-                        if (!file || !file.uploaded) return;
-                        let filename = file.filename;
+                        if (!prj_file || !prj_file.uploaded) return;
+                        let filename = prj_file.filename;
                         // 分离文件的文件名和后缀到一个数组中
                         let file_parts = filename.split('.');
                         // 将文档时间和附加的suffix后缀插入的文件名后
-                        let date = dayjs(file.uploaded_at? file.uploaded_at : new Date()).format('YYYY-MM-DD');
+                        let date = dayjs(prj_file.uploaded_at ? prj_file.uploaded_at : new Date()).format('YYYY-MM-DD');
                         file_parts.splice(file_parts.length > 1 ? file_parts.length - 1 : file_parts.length, 0, `${suffix}(${date})`);
                         filename = file_parts.join('.');
                         await PrjFile.update({
                             filename: filename,
-                            category_id:  category_id
-                        }, {where: {id: file_id}});
+                            category_id: category_id
+                        }, {where: {id: file.id}, returning: false});
                     }
 
                 }
@@ -202,25 +325,37 @@ export class FlowEngine extends EventEmitter {
                     bpmnActivity.on('end', this.on_end);
                     bpmnActivity.on('leave', this.on_leave);
 
+                    // let extension_script;
+                    // if (bpmnActivity.behaviour.extensionElements?.values) {
+                    //     for (const extn of bpmnActivity.behaviour.extensionElements.values) {
+                    //         if (extn.$type === 'camunda:ExecutionListener') {
+                    //             const event = extn.event;
+                    //             const script = extn.script.values;
+                    //             if (event === 'end') extension_script = script;
+                    //         }
+                    //     }
+                    // }
+
                     if (bpmnActivity.type !== 'bpmn:UserTask') return;
-                    bpmnActivity.broker.subscribeTmp('execution', 'execute.iteration.batch', (routingKey, msg) => {
-                        Logger.info('execute.iteration.batch');
-                        Logger.info(msg);
-                    });
-                    bpmnActivity.broker.subscribeTmp('execution', 'execute.discard', (routingKey, msg) => {
-                        Logger.trace('execute.discard');
-                        Logger.info(msg);
-                    });
+                    // bpmnActivity.broker.subscribeTmp('execution', 'execute.iteration.batch', (routingKey, msg) => {
+                    //     Logger.info('execute.iteration.batch');
+                    //     Logger.info(msg);
+                    // });
+                    bpmnActivity.broker.subscribeOnce('execution', 'execute.discard', self.on_execute_discard);
+                    bpmnActivity.broker.subscribeOnce('event', 'activity.wait', self.on_activity_wait);
+                    // bpmnActivity.broker.subscribeOnce('event', 'activity.discard', self.on_activity_discard);
 
                     bpmnActivity.broker.subscribeOnce('execution', 'execute.iteration.completed', self.on_loop_task_completed);
                     return {
                         activate: () => {
                             // bpmnActivity.on('end', async (elementApi, engineApi) => {
                             //     Logger.trace(`activity on end ${elementApi.id}`);
-                            // },{consumerTag: `format-on-end_${IdGen.id()}`}),
-                            //     bpmnActivity.on('execute.iteration.completed', async (elementApi, engineApi) => {
-                            //             Logger.trace(`activity on execute.iteration.completed ${elementApi.id}`);
-                            //         },{consumerTag: `format-on-execute.iteration.completed_${IdGen.id()}`});
+                            //     if (extension_script) {
+                            //         console.log(extension_script);
+                            //     }
+                            // },{consumerTag: `format-on-end_${IdGen.id()}`});
+
+
                             // @ts-ignore
                             bpmnActivity.on('enter', async (_elementApi, _engineApi) => {
                                 bpmnActivity.broker.publish('format', 'run.format.start', {endRoutingKey: 'run.format.complete'});
@@ -265,6 +400,7 @@ export class FlowEngine extends EventEmitter {
                                     // end call
                                 }
                                 bpmnActivity.broker.publish('format', 'run.format.complete', {msg: "complete"});
+
                             }, {consumerTag: `format-on-enter_${IdGen.id()}`});
 
                             //
@@ -287,12 +423,47 @@ export class FlowEngine extends EventEmitter {
         });
     }
 
+    on_execute_discard = async (routingKey, msg, activity) => {
+        Logger.trace('execute.discard');
+        Logger.info(msg);
+        activity.broker.subscribeOnce('execution', 'execute.discard', this.on_execute_discard);
+        await BpmnWork.update({completed_at: dayjs(), status: 3}, {
+            where: {
+                id: msg.content.executionId
+            },
+            returning: false,
+        });
+    }
+
+    // on_activity_discard = async (routingKey, msg, activity) => {
+    //     Logger.trace('activity.discard');
+    //     Logger.info(msg);
+    //     activity.broker.subscribeOnce('event', 'activity.discard', this.on_activity_discard);
+    // }
+
+    on_activity_wait = async (routingKey, msg, activity) => {
+        Logger.trace('activity.wait');
+        Logger.info(msg);
+        // let activity = this.execution.getActivityById(msg.content.id);
+        activity.broker.subscribeOnce('event', 'activity.wait', this.on_activity_wait);
+        await this.exec_on_start_js(activity.behaviour, activity, msg);
+    }
+
     on_loop_task_completed = async (routingKey, msg, _consumerCount) => {
         let activity = this.execution.getActivityById(msg.content.id);
+        console.log(`##### log state immediately in execute.iteration.completed ${activity.id}  ${activity.executionId}`);
+        // const {task, scriptInvocation, output} = activity;
+        //
+        // if (scriptInvocation) {
+        //     const script = scriptInvocation.script;
+        //     console.log(script);
+        // }
         activity.broker.subscribeOnce('execution', 'execute.iteration.completed', this.on_loop_task_completed);
         // Logger.info(`execute.iteration.completed, count: `);
         // Logger.info(consumerCount);
         // Logger.info(msg);
+        await this.exec_on_end_js(activity.behaviour, activity, msg);
+
         if (msg.content.input?.prj_phase) {
             await PrjInfo.update({phase_id: msg.content.input.prj_phase}, {
                 where: {id: this._prj_id},
@@ -305,6 +476,7 @@ export class FlowEngine extends EventEmitter {
                 returning: false
             })
         }
+
         if (!msg.content || !msg.content.output || !msg.content.output[msg.content.index]) return;
         await BpmnWork.update({
                 completed_at: dayjs(),
@@ -343,7 +515,9 @@ export class FlowEngine extends EventEmitter {
             }
         }
 
+        // 如果没有任务处理人,自动结束任务
         if (!handler) {
+            activity.signal();
             return;
         }
 
@@ -368,8 +542,10 @@ export class FlowEngine extends EventEmitter {
                 return obj;
             }, {});
         }
-        console.log(activity.content.form);
-        console.log({global: {environment: activity.environment}});
+
+        // 等待前面的通知任务完成后,再生成新任务,这样时间线看起来是保持顺序的
+        await delay(200);
+        // 进入节点,创建此流程节点的工作项
         await BpmnWork.findOrCreate({
             where: {id: activity.executionId},
             defaults: {
@@ -388,14 +564,19 @@ export class FlowEngine extends EventEmitter {
                 target_type: target_type,
                 params: params,
                 form: activity.content.form ?
-                    evaluate(JSON.stringify(activity.content.form), {
-                        global: {
-                            ...activity
-                        }
-                    })
+                    {
+                        id: activity.content.form.id,
+                        title: activity.content.form.title,
+                        schema: executor(activity.content.form.schema, {
+                            global: {
+                                ...activity
+                            }
+                        })
+                    }
                     : null
             }
         });
+        // TODO: 定期检查这个map中是否有已经被删除的execution
         FlowEngine.execution_engine_map.set(activity.executionId, this);
         await BpmnCase.update({state: await this.state()}, {where: {id: this._id}, returning: false});
     }
@@ -404,9 +585,12 @@ export class FlowEngine extends EventEmitter {
         console.log(`##### log state immediately in enter ${activity.id} ${activity.executionId}`);
         console.log(activity.environment.output);
         // console.log(activity.environment.output.task_request);
+        // if (activity.type !== 'bpmn:UserTask') return;
+
     }
 
-    on_end = async (activity) => {
+    on_end = async (activity, message) => {
+        let self = this;
         if (activity.type !== 'bpmn:UserTask') return;
         console.log(`##### log state immediately in end ${activity.id} ${activity.executionId}`);
 
@@ -432,14 +616,30 @@ export class FlowEngine extends EventEmitter {
 
         let id;
         if (activity.content.isMultiInstance) {
-            id = `${activity.executionId}_${activity.content.index}`;
-            await BpmnWork.update({
-                completed_at: dayjs(),
-                handler: activity.content.output[activity.content.index].user_id,
-                status: 2
-            }, {where: {id: id}, returning: false});
+            // id = `${activity.executionId}_${activity.content.index}`;
+            // console.log(activity.content);
+            // await BpmnWork.update({
+            //     completed_at: dayjs(),
+            //     handler: activity.content.output[activity.content.index].user_id,
+            //     status: 2
+            // }, {where: {id: id}, returning: false});
         } else {
             id = activity.executionId;
+            this.exec_on_end_js(activity.owner.behaviour, activity);
+            // if (activity.owner.behaviour.extensionElements?.values) {
+            //     for (const extn of activity.owner.behaviour.extensionElements.values) {
+            //         if (extn.$type === 'camunda:ExecutionListener') {
+            //             const event = extn.event;
+            //             const script = extn.script.value;
+            //             if (event === 'end') {
+            //                 let compile =  new Script(script, { filename: `${extn.$type}/${id}/on${event}` });
+            //                 // const execScript = activity.environment.scripts.compile(script.scriptFormat, `${extn.$type}/${id}/on${event}`, script);
+            //                 compile.runInNewContext({ ...message, environment: activity.environment });
+            //
+            //             }
+            //         }
+            //     }
+            // }
 
             await BpmnWork.update({
                 completed_at: dayjs(),
@@ -457,12 +657,7 @@ export class FlowEngine extends EventEmitter {
     on_leave = async (activity) => {
         if (activity.type !== 'bpmn:UserTask') return;
         console.log(`##### log state immediately in leave ${activity.id} ${activity.executionId}`);
-        await BpmnWork.update({completed_at: dayjs(), status: 2}, {
-            where: {
-                execution_id: activity.executionId
-            },
-            returning: false,
-        });
+
     }
 
     on_flow_take = async (flow) => {
@@ -473,7 +668,7 @@ export class FlowEngine extends EventEmitter {
 
                 if (extn.$type === 'camunda:Properties' && extn.values) {
                     for (let value of extn.values) {
-                        console.log(value);
+                        // console.log(value);
                         switch (value.name) {
                             case 'prj_phase':
                                 await PrjInfo.update({phase_id: value.value}, {
@@ -490,7 +685,7 @@ export class FlowEngine extends EventEmitter {
                                 })
                                 break;
                             case 'task_status':
-                                console.log(this);
+                                // console.log(this);
                                 Logger.trace(`flow_take task_id ${this._task_id}`)
                                 await PrjPlanTask.update({status: value.value}, {
                                     where: {id: this._task_id}, returning: false
@@ -610,7 +805,7 @@ export class FlowEngine extends EventEmitter {
                 }
                 break;
             case 'all':
-                let all = await AcsUserRole.sequelize!.query<AcsUserInfo>(`
+                let all: any = await AcsUserRole.sequelize!.query<AcsUserInfo>(`
                     select 
                         staff.id, staff.name
                     from ${AcsUserInfo.table_name} staff,  ${AcsDomain.table_name} domain, ${AcsUserDomain.table_name} user_domain
@@ -648,4 +843,39 @@ export class FlowEngine extends EventEmitter {
         // })
         return handlers;
     }
+
+    async exec_on_start_js(behaviour: any, activity: any, message?: any) {
+        if (behaviour.extensionElements?.values) {
+            for (const extn of behaviour.extensionElements.values) {
+                if (extn.$type.toLowerCase() === 'camunda:ExecutionListener'.toLowerCase()) {
+                    if (!extn.script) continue;
+                    const event = extn.event;
+                    const script = extn.script.value;
+                    if (event === 'start') {
+                        let compile = new Script(script, {filename: `${extn.$type}/${activity.executionId}/on${event}`});
+                        await compile.runInNewContext({...message, environment: activity.environment});
+                    }
+                }
+            }
+        }
+    }
+
+    async exec_on_end_js(behaviour: any, activity: any, message?: any) {
+        if (behaviour.extensionElements?.values) {
+            for (const extn of behaviour.extensionElements.values) {
+                if (extn.$type.toLowerCase() === 'camunda:ExecutionListener'.toLowerCase()) {
+                    if (!extn.script) continue;
+                    const event = extn.event;
+                    const script = extn.script.value;
+                    if (event === 'end') {
+                        let compile = new Script(script, {filename: `${extn.$type}/${activity.executionId}/on${event}`});
+                        Logger.trace(`-------------to exec on end js, id: ${activity.executionId}`);
+                        await compile.runInNewContext({...message, environment: activity.environment});
+                        Logger.trace(`-------------execed on end js, id: ${activity.executionId}`)
+
+                    }
+                }
+            }
+        }
+    }
 }

+ 4 - 1
pmr-biz-manager/src/routes/api/cfg/doc_type/get_list.ts

@@ -64,7 +64,10 @@ const v1_0: IApiProcessor = {
             name: 'name',
             color: 'color',
             allow_upload: 'allow_upload'
-        }
+        },
+        order: [
+            ['order_index', 'asc']
+        ]
     }
 }
 

+ 0 - 59
pmr-biz-manager/src/routes/api/cfg/org/add.ts

@@ -2,65 +2,6 @@ import {IApiProcessor} from "@core/Defined";
 import {AcsDomain} from "@core-models/AcsDomain";
 import DataCURD from "@core/DataCURD";
 
-// interface IData {
-//     pid: number;
-//     name: string;
-//     memo?: string;
-// }
-//
-// function add(json: IRequest, _param: IMethodParams, _cached_data: ICachedData): Promise<unknown> {
-//     return new Promise<unknown>(async (resolve, reject) => {
-//         let data: IData = <IData>json.data;
-//         try {
-//             let parent_path = await get_parent_path(data);
-//             let result = await do_add(data, parent_path);
-//             resolve(result);
-//         } catch (e) {
-//             reject(e);
-//         }
-//     });
-// }
-//
-// function get_parent_path(params: IData): Promise<string> {
-//     return new Promise<string>(async (resolve, reject) => {
-//         try {
-//             let record = await AcsDomain.findOne({where: {id: params.pid},raw: true});
-//             if (!record)  return reject(Resp.gen_err(Resp.Forbidden));
-//             resolve(record.path);
-//         } catch (e) {
-//             reject(e);
-//         }
-//     });
-// }
-//
-// function do_add(params: IData, parent_path: string): Promise<unknown> {
-//     return new Promise<unknown>(async (resolve, reject) => {
-//         let sql: string;
-//         let replacements: ISQLReplacements = {};
-//         if (params.pid === 0) {
-//             sql = `insert into ${AcsDomain.table_name} (name, pid, path, memo, tag)
-//                 values(:name, ${params.pid}, text2ltree(to_char(nextval('tb_acs_domain_id_seq')-1, 'FM999999999')),
-//                 :memo, :tag) returning id`;
-//             replacements = {name: params.name, memo: params.memo, category: 'org'};
-//         } else {
-//             sql = `insert into ${AcsDomain.table_name} (name, pid, path, memo, tag)
-//              values(:name, ${params.pid},
-//              text2ltree( :parent_path || '.' || to_char(nextval('tb_acs_domain_id_seq')-1, 'FM999999999')),
-//              :memo, :tag) returning id`;
-//             replacements = {name: params.name, parent_path: parent_path, memo: params.memo, category: 'org'};
-//         }
-//         try {
-//             const [results, metadata] = await AcsDomain.sequelize!.query(sql, {replacements: replacements});
-//             if (!results) return reject(Resp.gen_err(Resp.PostgresqlServerError));
-//             resolve({
-//                 id: (<AcsDomain>results[0]).id
-//             });
-//         } catch (e) {
-//             reject(Resp.gen_db_err(e));
-//         }
-//     })
-// }
-
 const v1_0: IApiProcessor = {
     schema: {
         "type": "object",

+ 84 - 0
pmr-biz-manager/src/routes/api/cfg/region/add.ts

@@ -0,0 +1,84 @@
+import {IApiProcessor} from "@core/Defined";
+import {AcsDomain} from "@core-models/AcsDomain";
+import DataCURD from "@core/DataCURD";
+
+const v1_0: IApiProcessor = {
+    schema: {
+        "type": "object",
+        "properties": {
+            "data": {
+                "type": "object",
+                "properties": {
+                    "name": {
+                        "type": "string",
+                        "title": "部门名称"
+                    },
+                    "memo": {
+                        "type": "string"
+                    }
+                },
+                "x-apifox-orders": [
+                    "name",
+                    "memo"
+                ],
+                "required": [
+                    "name"
+                ],
+                "title": "请求参数内容"
+            },
+            "ver": {
+                "type": "string",
+                "title": "版本号",
+                "description": "文档中没有说明时,默认为1.0",
+                "examples": [
+                    "1.0"
+                ],
+                "default": "1.0"
+            },
+            "app_id": {
+                "type": "string",
+                "title": "应用id",
+                "description": "平台分配给客户端的app id。一个应用一个id。",
+                "default": "f2741721-1c07-430e-9f01-5529692340f9"
+            },
+            "timestamp": {
+                "type": "integer",
+                "title": "UTC时间戳",
+                "description": "UTC时间戳,精确到毫秒。平台对超过五分钟的消息,将做忽略处理",
+                "default": null
+            },
+            "token": {
+                "type": "string",
+                "title": "身份认证token",
+                "description": "平台登录后分配的token"
+            }
+        },
+        "x-apifox-orders": [
+            "ver",
+            "app_id",
+            "timestamp",
+            "token",
+            "data"
+        ],
+        "required": [
+            "data",
+            "ver",
+            "app_id",
+            "timestamp",
+            "token"
+        ]
+    },
+    method: DataCURD.internal_add_tree_node,
+    method_params: {
+        model: AcsDomain,
+        addition: {
+            pid: 100,
+            category: 'region'
+        }
+    }
+}
+
+module.exports = {
+    default: v1_0,
+    v1_0: v1_0
+}

+ 82 - 0
pmr-biz-manager/src/routes/api/cfg/region/get_tree.ts

@@ -0,0 +1,82 @@
+import {IApiProcessor} from "@core/Defined";
+import DataCURD from "@core/DataCURD";
+import {AcsDomain} from "@core-models/AcsDomain";
+
+const v1_0: IApiProcessor = {
+    schema: {
+        "type": "object",
+        "properties": {
+            "data": {
+                "type": "object",
+                "properties": {
+                    "pid": {
+                        "type": "integer",
+                        "title": "父机构id",
+                        "description": "无父类型时取整个区域树"
+                    }
+                },
+                "x-apifox-orders": [
+                    "pid"
+                ],
+                "title": "请求参数内容"
+            },
+            "ver": {
+                "type": "string",
+                "title": "版本号",
+                "description": "文档中没有说明时,默认为1.0",
+                "examples": [
+                    "1.0"
+                ]
+            },
+            "app_id": {
+                "type": "string",
+                "title": "应用id",
+                "description": "平台分配给客户端的app id。一个应用一个id。"
+            },
+            "timestamp": {
+                "type": "integer",
+                "description": "UTC时间戳,精确到毫秒。平台对超过五分钟的消息,将做忽略处理",
+                "title": "UTC时间戳"
+            },
+            "sign": {
+                "type": "string",
+                "title": "签名"
+            },
+            "token": {
+                "type": "string",
+                "title": "身份认证token",
+                "description": "平台登录后分配的token"
+            }
+        },
+        "x-apifox-orders": [
+            "ver",
+            "app_id",
+            "timestamp",
+            "token",
+            "sign",
+            "data"
+        ],
+        "required": [
+            "data",
+            "ver",
+            "app_id",
+            "timestamp",
+            "token"
+        ]
+    },
+    method: DataCURD.internal_get_tree,
+    method_params: {
+        model: AcsDomain,
+        addition: {
+            pid: 100,
+            category: ['region'],
+            with_pid: false,
+            output_name: 'tree'
+        }
+    }
+}
+
+module.exports = {
+    default: v1_0,
+    v1_0: v1_0
+}

+ 118 - 0
pmr-biz-manager/src/routes/api/cfg/region/modify.ts

@@ -0,0 +1,118 @@
+import {ADMINISTRATOR, IApiProcessor, ICachedData, IMethodParams, IRequest} from "@core/Defined";
+import {AcsDomain} from "@core-models/AcsDomain";
+import DataCURD from "@core/DataCURD";
+
+interface IData {
+    id: string;
+    name?: string;
+    memo?: string;
+}
+
+function get_where(json: IRequest, _params: IMethodParams, _cached_data: ICachedData): Promise<any> {
+    return new Promise<any>(async (resolve, reject) => {
+        try {
+            let data: IData = <IData>json.data;
+            resolve({id: data.id, category: 'region'});
+        } catch (e) {
+            reject(e);
+        }
+    })
+}
+
+const v1_0: IApiProcessor = {
+    schema: {
+        "type": "object",
+        "properties": {
+            "data": {
+                "type": "object",
+                "properties": {
+                    "name": {
+                        "type": "string",
+                        "title": "部门名称"
+                    },
+                    "memo": {
+                        "type": "string",
+                        "title": "备注"
+                    },
+                    "id": {
+                        "type": "integer",
+                        "title": "部门id"
+                    }
+                },
+                "x-apifox-orders": [
+                    "id",
+                    "name",
+                    "memo"
+                ],
+                "required": [
+                    "id"
+                ],
+                "anyOf": [
+                    {
+                        "required": [
+                            "name"
+                        ]
+                    },
+                    {
+                        "required": [
+                            "memo"
+                        ]
+                    }
+                ],
+                "title": "请求参数内容"
+            },
+            "ver": {
+                "type": "string",
+                "title": "版本号",
+                "description": "文档中没有说明时,默认为1.0",
+                "examples": [
+                    "1.0"
+                ],
+                "default": "1.0"
+            },
+            "app_id": {
+                "type": "string",
+                "title": "应用id",
+                "description": "平台分配给客户端的app id。一个应用一个id。"
+            },
+            "timestamp": {
+                "type": "integer",
+                "title": "UTC时间戳",
+                "description": "UTC时间戳,精确到毫秒。平台对超过五分钟的消息,将做忽略处理"
+            },
+            "token": {
+                "type": "string",
+                "title": "身份认证token",
+                "description": "平台登录后分配的token"
+            }
+        },
+        "x-apifox-orders": [
+            "ver",
+            "app_id",
+            "timestamp",
+            "token",
+            "data"
+        ],
+        "required": [
+            "data",
+            "ver",
+            "app_id",
+            "timestamp",
+            "token"
+        ]
+    },
+    method: DataCURD.internal_update_row,
+    method_params:{
+        model: AcsDomain,
+        where: get_where,
+        input_data_map: {
+            name: 'name',
+            memo: 'memo'
+        }
+    }
+}
+
+module.exports = {
+    default: v1_0,
+    v1_0: v1_0
+}

+ 74 - 0
pmr-biz-manager/src/routes/api/cfg/region/remove.ts

@@ -0,0 +1,74 @@
+import {AcsDomain} from "@core-models/AcsDomain";
+import DataCURD from "@core/DataCURD";
+import {IApiProcessor, ICachedData, IMethodParams, IRequest} from "@core/Defined";
+
+interface IData {
+    id: string;
+}
+
+function get_where(json: IRequest, _params: IMethodParams, _cached_data: ICachedData): Promise<any> {
+    return new Promise<any>(async (resolve, reject) => {
+        try {
+            let data: IData = <IData>json.data;
+            resolve({id: data.id, category: 'region'});
+        } catch (e) {
+            reject(e);
+        }
+    })
+}
+
+const v1_0: IApiProcessor = {
+    schema: {
+        "$schema": "http://json-schema.org/draft-07/schema#",
+        "type": "object",
+        "properties": {
+            "ver": {
+                "type": "string",
+                "title": "版本号,默认1.0",
+                "default": "1.0"
+            },
+            "app_id": {
+                "type": "string",
+                "title": "平台分配的app id"
+            },
+            "token": {
+                "type": "string",
+                "title": "登录后获得的令牌"
+            },
+            "timestamp": {
+                "type": "integer",
+                "title": "时间戳"
+            },
+            "sign": {
+                "type": "string",
+                "title": "签名"
+            },
+            "data": {
+                "type": "object",
+                "title": "业务参数",
+                "properties": {
+                    "id": {
+                        "type": "number"
+                    }
+                },
+                "required": [
+                    "id"
+                ]
+            }
+        },
+        "required": [
+            "ver", "app_id", "timestamp",
+            "data"
+        ]
+    },
+    method: DataCURD.internal_remove_row,
+    method_params: {
+        model: AcsDomain,
+        where: get_where
+    }
+}
+
+module.exports = {
+    default: v1_0,
+    v1_0: v1_0,
+}

+ 2 - 2
pmr-biz-manager/src/routes/api/cfg/staff/modify.ts

@@ -133,11 +133,11 @@ const v1_0: IApiProcessor = {
                         "title": "员工 id"
                     },
                     "email": {
-                        "type": "string",
+                        "type": ["string", "null"],
                         "title": "邮箱号"
                     },
                     "memo": {
-                        "type": "string",
+                        "type": ["string", "null"],
                         "title": "备注"
                     },
                     "status": {

+ 14 - 2
pmr-biz-manager/src/routes/api/prj/contract/get_list.ts

@@ -45,6 +45,11 @@ interface IData {
      * 合同名称模糊查询
      */
     name?: string;
+
+    /**
+     * 是否已绑定到项目
+     */
+    binded?: boolean;
 }
 
 function get_list(json: IRequest, params: IMethodParams, cached_data: ICachedData): Promise<WhereOptions> {
@@ -55,7 +60,7 @@ function get_list(json: IRequest, params: IMethodParams, cached_data: ICachedDat
             let select_sql = `
                 select contract.id, contract.name, 
                     contract.customer_id, customer.name as customer_name,
-                    contract.prj_id, prj.name as prj_name,
+                    prj.id as prj_id, prj.name as prj_name,
                     prj.leader_id, leader.name as leader_name,
                     cast(contract.amount as float), contract.handler_id, handler.name as handler_name,
                    
@@ -66,7 +71,7 @@ function get_list(json: IRequest, params: IMethodParams, cached_data: ICachedDat
             `;
             let condition = `
                 from ${BizContractInfo.table_name} contract
-                left join ${PrjInfo.table_name} prj on prj.id = contract.prj_id
+                left join ${PrjInfo.table_name} prj on prj.contract_id = contract.id
                 left join ${AcsUserInfo.table_name} handler on handler.id = contract.handler_id
                 left join ${BizCustomer.table_name} customer on customer.id = contract.customer_id
                 left join ${AcsUserInfo.table_name} leader on leader.id = prj.leader_id
@@ -103,6 +108,13 @@ function get_list(json: IRequest, params: IMethodParams, cached_data: ICachedDat
                 condition += ' and contract.signed_at <= :end_to ';
                 replacements.end_to = data.end_to + ' 23:59:59';
             }
+            if (data.binded !== undefined) {
+                if (data.binded)
+                    condition += ' and prj.id is not null ';
+                else
+                    condition += ' and prj.id is null ';
+                replacements.binded = data.binded;
+            }
 
             count_sql += condition;
             select_sql += condition;

+ 5 - 1
pmr-biz-manager/src/routes/api/prj/doc/remove.ts

@@ -2,19 +2,23 @@ import {IApiProcessor, ICachedData, IMethodParams, IRequest} from "@core/Defined
 import {Resp} from "@util/Resp";
 import {Oss} from "@util/Oss";
 import {PrjFile} from "@core-models/PrjFile";
-
+import {PrjFileCategory} from "@core-models/PrjFileCategory";
 interface IData {
     id: string;
 }
 
 // TODO: 部分文档不允许删除,或删除时需要与其它信息同步
 
+
 function remove(json: IRequest, params: IMethodParams, cached_data: ICachedData): Promise<any> {
     return new Promise(async (resolve, reject) => {
         try {
             let data = <IData>json.data;
             let file = await PrjFile.findOne({where: {id: data.id}, raw: true});
             if (!file) throw Resp.gen_err(Resp.ResourceNotFound, `文档(id:${data.id})不存在。`);
+            let category = await PrjFileCategory.findOne({where: {id: file.category_id}, raw: true});
+            if (!category) return reject(Resp.gen_err(Resp.ResourceNotFound, "文档类型不存在,id: "+ file.category_id));
+            if (category.allow_upload === false) return reject(Resp.gen_err(Resp.Forbidden, '此文档类型不允许自由删除。'))
             await PrjFile.destroy({where: {id: data.id}});
             let object_name = file.id;
             let oss = Oss.get_instance('pmr-doc');

+ 4 - 3
pmr-biz-manager/src/routes/api/prj/info/add.ts

@@ -1,6 +1,6 @@
 import {IApiProcessor, ICachedData, IMethodParams, IRequest} from "@core/Defined";
 import {Resp} from "@util/Resp";
-import {WhereOptions} from "sequelize";
+import {Transaction, WhereOptions} from "sequelize";
 import DataCURD from "@core/DataCURD";
 import {IdGen} from "@util/IdGen";
 import dayjs from "dayjs";
@@ -107,7 +107,7 @@ function add(json: IRequest, params: IMethodParams, cached_data: ICachedData): P
             let bpmn = await BpmnModel.findOne({where: {id: 'prj'}, transaction: t});
             if (!bpmn) throw Resp.gen_err(Resp.InternalServerError, '项目流程模型数据缺失!');
             let flow = new FlowEngine(bpmn.id, bpmn.content, {prj_id: id, owner: cached_data.user_id}, bpmn_flow_on_end);
-            await flow.run();
+
             await BpmnCase.create({
                 id: flow.id,
                 model_id: bpmn.id,
@@ -122,9 +122,10 @@ function add(json: IRequest, params: IMethodParams, cached_data: ICachedData): P
                 creator_name: cached_data.user_name,
                 content: `项目`,
                 prj_id: id,
-                t: t
+                t: t as Transaction
             });
             await t.commit();
+            await flow.run();
             resolve({id: id});
         } catch (e) {
             await t.rollback();

+ 1 - 0
pmr-biz-manager/src/routes/api/prj/info/get_checkers.ts

@@ -14,6 +14,7 @@ interface IData  {
 function get_checkers(json: IRequest, params: IMethodParams, cached_data: ICachedData): Promise<any> {
     return new Promise<any>(async (resolve, reject) => {
         try {
+            // TODO: 要返回checker的name
             let data = <IData>json.data;
             let prj = await PrjInfo.findOne({
                 where: {

+ 1 - 1
pmr-biz-manager/src/routes/api/prj/info/get_list.ts

@@ -119,7 +119,7 @@ function get_list(json: IRequest, params: IMethodParams, cached_data: ICachedDat
                 left join ${BpmnCase.table_name} flow on flow.prj_id = prj.id 
                 LEFT JOIN ${BpmnWork.table_name} work ON work.prj_id = prj.id and work.case_id = flow.id and
                     work.assigned_to = '${cached_data.user_id}' and 
-                    work.status <> 2 and work.process_type = 1 and
+                    work.status < 2 and work.process_type = 1 and
                     flow.model_id = 'prj'
                 where (
                     -- 项目负责人可以取到自己的项目

+ 11 - 9
pmr-biz-manager/src/routes/api/prj/info/modify.ts

@@ -68,25 +68,27 @@ function permission_guard(content: IRequest, cached_data: ICachedData): Promise<
             return reject(Resp.gen_err(Resp.ResourceNotFound, '项目不存在。'));
         let phase = await PrjPhaseDefine.findOne({where: {id: prj.phase_id}, raw: true});
         if (!phase) return reject(Resp.gen_err(Resp.ResourceNotFound, '项目状态信息出错,id: ' + prj.id));
+
+        let is_privileged_account = await is_project_privileged_account(cached_data.user_id);
+
         if (phase.order_index >= 20 ) {
             if (data.type_id !== undefined && prj.type_id !== data.type_id)  {
                 return reject(Resp.gen_err(Resp.Forbidden, '项目立项后不允许修改项目类型。'))
             }
-            if (!await is_project_privileged_account(cached_data.user_id)) {
-                if (data.deliver_at)
+            if (!is_privileged_account) {
+                if (data.deliver_at && dayjs(data.deliver_at).format('YYYY-MM-DD') !== dayjs(prj.deliver_at).format('YYYY-MM-DD'))
                     return reject(Resp.gen_err(Resp.Forbidden, '项目已立项,您没有权限修改项目交付时间,请确认您是特权人员。'));
             }
-
         }
 
         /// 只有项目特权人员才可以修改项目负责人
         if (data.leader_id && data.leader_id !== prj.leader_id) {
-            if (!await is_project_privileged_account(cached_data.user_id)) {
+            if (!is_privileged_account) {
                 return  reject(Resp.gen_err(Resp.Forbidden, '您没有权限修改项目负责人,请确认您是特权人员。'));
             }
         }
         if (!await is_project_modifiable(cached_data.user_id, prj.id))
-            return reject(Resp.gen_err(Resp.Forbidden, '您没有权限修改该项目信息,请确认您是项目负责人或特权人员。'));
+            return reject(Resp.gen_err(Resp.Forbidden, '您没有权限修改本项目的信息,请确认您是项目负责人或特权人员。'));
         if (data.deliver_at) {
             data.deliver_at += ' 23:59:59';
         }
@@ -258,7 +260,7 @@ const v1_0: IApiProcessor = {
                         "description": "所在地区id"
                     },
                     "customer_id": {
-                        "type": "string",
+                        "type": ["string", "null"],
                         "title": "客户名称",
                         "description": "客户id"
                     },
@@ -268,17 +270,17 @@ const v1_0: IApiProcessor = {
                         "title": "预计交付时间"
                     },
                     "bizman_id": {
-                        "type": "string",
+                        "type": ["string", "null"],
                         "description": "商务负责人id",
                         "title": "商务负责人"
                     },
                     "intro": {
-                        "type": "string",
+                        "type": ["string", "null"],
                         "description": "项目简介",
                         "title": "项目简介"
                     },
                     "contract_id": {
-                        "type": "string",
+                        "type": ["string", "null"],
                         "title": "合同id",
                         "description": "合同id"
                     },

+ 5 - 2
pmr-biz-manager/src/routes/api/prj/info/overview.ts

@@ -18,6 +18,7 @@ import {BpmnModel} from "@core-models/BpmnModel";
 import {FlowEngine} from "@src/bpmn/flow_engine";
 import {bpmn_flow_on_end} from "@src/utils/bpmn_work_helper";
 import dayjs from "dayjs";
+import {BizContractInfo} from "@core-models/BizContractInfo";
 
 interface IData {
     id: string;
@@ -47,7 +48,7 @@ async function overview(json: IRequest, params: IMethodParams, cached_data: ICac
             prj.region_id, region.name as region_name,
             customer.industry_id as industry_id, industry.name as industry_name,
             customer.level_id as level_id, level.name as level_name,
-            prj.contract_id,
+            prj.contract_id, contract.name as contract_name,
             prj.intro, 
             customer_region.id as customer_region_id, customer_region.name as customer_region_name,
             customer_bizman.id as customer_bizman_id, customer_bizman.name as customer_bizman_name,
@@ -76,12 +77,13 @@ async function overview(json: IRequest, params: IMethodParams, cached_data: ICac
         left join ${AcsDomain.table_name} region on prj.region_id = region.id
         left join ${BizCustomerIndustry.table_name} industry on customer.industry_id = industry.id
         left join ${BizCustomerLevel.table_name} level on customer.level_id = level.id
+        left join ${BizContractInfo.table_name} contract on contract.id = prj.contract_id
         left join ${AcsDomain.table_name} customer_region on customer.region_id = customer_region.id
         left join ${AcsUserInfo.table_name} customer_bizman on customer.bizman_id = customer_bizman.id
         left join ${BpmnCase.table_name} flow on flow.prj_id = prj.id 
         LEFT JOIN ${BpmnWork.table_name} work ON work.prj_id = prj.id and work.case_id = flow.id and
             work.assigned_to = '${cached_data.user_id}' and 
-            work.status <> 2 and work.process_type = 1 and 
+            work.status < 2 and work.process_type = 1 and 
             (flow.model_id = 'prj')
                 
         where prj.id = :prj_id
@@ -94,6 +96,7 @@ async function overview(json: IRequest, params: IMethodParams, cached_data: ICac
             region.name,
             customer.industry_id , industry.name,
             customer.level_id, level.name,
+            contract_id, contract.name,
             customer_region.id, customer_region.name,
             customer_bizman.id, customer_bizman.name
     `;

+ 1 - 0
pmr-biz-manager/src/routes/api/prj/member/add.ts

@@ -68,6 +68,7 @@ function add(json: IRequest, params: IMethodParams, cached_data: ICachedData): P
                 prj_id: data.prj_id,
                 member_id: data.account_id,
                 role_id: data.role_id,
+                memo: data.memo,
                 draft: false,
                 created_at: dayjs().format('YYYY-MM-DD HH:mm:ss')
             }

+ 7 - 4
pmr-biz-manager/src/routes/api/prj/member/modify.ts

@@ -25,7 +25,6 @@ interface IData {
      * 项目组成员角色id
      */
     role_id?: string;
-    [property: string]: any;
 }
 
 async function get_where(json: IRequest, params: IMethodParams, cached_data: ICachedData): Promise<WhereOptions> {
@@ -43,8 +42,12 @@ function guard(json: IRequest, cached_data: ICachedData): Promise<void> {
         let prj_info = await PrjInfo.findOne({where: {id: data.prj_id}, raw: true});
         if (!prj_info) return reject(Resp.gen_err(Resp.ResourceNotFound));
 
-        if (prj_info.leader_id === data.account_id && data.role_id) return reject(Resp.gen_err(Resp.Forbidden, '不允许修改项目负责人的角色。'));
-        if (user === data.account_id && data.role_id) return reject(Resp.gen_err(Resp.PrjModifySelfRoleForbidden, '不允许修改自己的项目组成员角色。'));
+        let leader = await PrjMembers.findOne({where: {prj_id: data.prj_id, member_id: prj_info.leader_id}, raw: true});
+        if (!leader) return reject(Resp.gen_err(Resp.ResourceNotFound, `项目负责人不是项目组成员,信息出错,请联系软件开发人员。`));
+
+        if (prj_info.leader_id === data.account_id && data.role_id && data.role_id !== leader.role_id)
+            return reject(Resp.gen_err(Resp.Forbidden, '不允许修改项目负责人的角色。'));
+        if (user === data.account_id && data.role_id && data.role_id !== leader.role_id) return reject(Resp.gen_err(Resp.PrjModifySelfRoleForbidden, '不允许修改自己的项目组成员角色。'));
 
         let phase = await PrjPhaseDefine.findOne({where: {id: prj_info.phase_id}, raw: true});
         if (!phase) return reject(Resp.gen_err(Resp.ResourceNotFound, '项目状态信息出错,id: ' + prj_info.id));
@@ -86,7 +89,7 @@ const v1_0: IApiProcessor = {
                         "title": "项目组成员角色id"
                     },
                     "memo": {
-                        "type": "string",
+                        "type": ["string", "null"],
                         "title": "说明"
                     }
                 },

+ 6 - 1
pmr-biz-manager/src/routes/api/prj/member/remove.ts

@@ -35,7 +35,12 @@ export function accountGuard(json: IRequest, cached_data: ICachedData): Promise<
         let user = cached_data.user_id;
         let data = <IData>json.data;
         if (!user) return reject(Resp.gen_err(Resp.Forbidden));
-        if (user === ADMINISTRATOR) return resolve();
+        // if (user === ADMINISTRATOR) return resolve();
+        let prj_info = await PrjInfo.findOne({where: {id: data.prj_id}, raw: true});
+        if (!prj_info) return reject(Resp.gen_err(Resp.ResourceNotFound));
+        if (prj_info.leader_id === data.account_id) {
+            return reject(Resp.gen_err(Resp.Forbidden, '不允许项目负责人。'));
+        }
         if (!await is_project_modifiable(user, data.prj_id))
             return reject(Resp.gen_err(Resp.Forbidden, '您没有权限删除项目组成员,请确认您是项目负责人或特权人员。'));
         if (user === data.account_id) return reject(Resp.gen_err(Resp.PrjRemoveSelfForbidden));

+ 19 - 8
pmr-biz-manager/src/routes/api/prj/outcome/add.ts

@@ -29,7 +29,7 @@ interface IData {
     /**
      * 计划交付时间(YYYY-MM-DD)
      */
-    plan_time: Date;
+    plan_time: string;
     /**
      * 任务id
      */
@@ -59,12 +59,12 @@ function statusGuard(json: IRequest, cached_data: ICachedData): Promise<void> {
         if (phase.order_index >= 80) return reject(Resp.gen_err(Resp.InvalidFlow, '当前项目阶段不允许添加交付物。'));
 
         // if (data.draft == true) {
-            if (phase.order_index >= 60 && phase.order_index <= 65) {
-                return reject(Resp.gen_err(Resp.InvalidFlow, '计划变更审核中,现阶段不允许添加交付物。'));
-            }
+        if (phase.order_index >= 60 && phase.order_index <= 65) {
+            return reject(Resp.gen_err(Resp.InvalidFlow, '计划变更审核中,现阶段不允许添加交付物。'));
+        }
         // } else {
         if (data.draft === false) {
-            if (phase.order_index >=30 && phase.order_index <= 40)
+            if (phase.order_index >= 30 && phase.order_index <= 40)
                 return reject(Resp.gen_err(Resp.InvalidFlow, '项目计划审核中,当前阶段不允许添加。'));
             if (phase.order_index === 70)
                 return reject(Resp.gen_err(Resp.InvalidFlow, '当前项目阶段不允许直接添加交付物,应在草稿中添加后重新提交计划变更审核。'));
@@ -74,10 +74,21 @@ function statusGuard(json: IRequest, cached_data: ICachedData): Promise<void> {
         }
 
 
+        // 检查交付物计划交付时间不能晚于任务结束时间
+        if (data.plan_time && task.end_at) {
+            data.plan_time = data.plan_time + ' 23:59:59';
+
+            let plan_time = dayjs(data.plan_time);
+            let end_time = dayjs(task.end_at);
+            if (plan_time.isAfter(end_time)) {
+                return reject(Resp.gen_err(Resp.InvalidFlow, '交付物计划交付时间不能晚于任务结束时间。'));
+            }
+        }
         resolve();
     });
 }
 
+
 function add(json: IRequest, params: IMethodParams, cached_data: ICachedData): Promise<WhereOptions> {
     return new Promise<WhereOptions>(async (resolve, reject) => {
         let t = await PrjPlanTaskDraft.sequelize!.transaction();
@@ -98,9 +109,9 @@ function add(json: IRequest, params: IMethodParams, cached_data: ICachedData): P
             let model = data.draft ? PrjTaskOutcomeDraft : PrjTaskOutcome;
             await model.create(value, {transaction: t});
             /// 如果计划任务是草稿,且还没有生成计划变更任务,则生成任务
-            if (data.draft === true ) {
+            if (data.draft === true) {
                 let task = await PrjPlanTaskDraft.findOne({where: {id: data.task_id}, raw: true, transaction: t});
-                if (task) {
+                if (task && task.change_marker !== ChangeMarker.Added) {
                     await start_plan_alt_flow(task.prj_id, cached_data.user_id, t as Transaction);
                     await PrjPlanTaskDraft.update({change_marker: ChangeMarker.Changed},
                         {where: {id: data.task_id}, transaction: t, returning: false});
@@ -112,7 +123,7 @@ function add(json: IRequest, params: IMethodParams, cached_data: ICachedData): P
             await t.rollback();
             reject(Resp.throw_err(e));
         }
-    })
+    });
 }
 
 const v1_0: IApiProcessor = {

+ 1 - 1
pmr-biz-manager/src/routes/api/prj/outcome/detail.ts

@@ -118,7 +118,7 @@ function detail(json: IRequest, params: IMethodParams, cached_data: ICachedData)
                 left join ${PrjInfo.table_name} prj on prj.id = task.prj_id
                 left join ${AcsUserInfo.table_name} handler on handler.id = task.handler_id
                 left join ${AcsUserInfo.table_name} creator on creator.id = outcome.creator_id
-                left join ${PrjFile.table_name} file on file.dependent_id = outcome.id
+                left join ${PrjFile.table_name} file on file.id = outcome.object_name
                 where outcome.id = :outcome_id
             `, {type: QueryTypes.SELECT, raw: true, replacements: {
                     outcome_id: data.outcome_id,

+ 4 - 3
pmr-biz-manager/src/routes/api/prj/outcome/download.ts

@@ -15,9 +15,10 @@ function download(json: IRequest, params: IMethodParams, cached_data: ICachedDat
             let outcome = await PrjTaskOutcome.findOne({where: {id: data.id}, raw: true});
             if (!outcome) throw Resp.gen_err(Resp.ResourceNotFound, `交付物记录(id:${data.id})不存在。`);
             if (outcome.uploaded === false) throw Resp.gen_err(Resp.ResourceNotFound, '交付物文档尚未上传。');
-            let prj_file = await PrjFile.findOne({where: {dependent_id: outcome.id, category_id: PrjFileType.Outcome}, raw: true});
-            if (!prj_file) throw Resp.gen_err(Resp.ResourceNotFound, `交付物记录(id:${data.id})的文档不存在。`);
-            let object_name = prj_file.id;
+
+            let object_name = outcome.object_name;
+            let prj_file = await PrjFile.findOne({where: {id: object_name}, raw: true});
+            if (!prj_file) throw Resp.gen_err(Resp.ResourceNotFound, `交付物(id:${data.id})的文档(id: ${object_name})不存在。`);
             let oss = Oss.get_instance('pmr-doc');
             let result = await oss.presigned_url(oss.bucket, object_name, 300, prj_file.filename);
             resolve({url: result, filename: prj_file.filename});

+ 18 - 2
pmr-biz-manager/src/routes/api/prj/outcome/get_list.ts

@@ -5,6 +5,8 @@ import DataCURD, {ISQLReplacements} from "@core/DataCURD";
 import {PrjTaskOutcome} from "@core-models/PrjTaskOutcome";
 import {PrjTaskOutcomeDraft} from "@core-models/PrjTaskOutcomeDraft";
 import {PrjFile} from "@core-models/PrjFile";
+import {PrjPlanTask} from "@core-models/PrjPlanTask";
+import {PrjPlanTaskDraft} from "@core-models/PrjPlanTaskDraft";
 interface IData {
     /**
      * 是否是副本
@@ -53,6 +55,7 @@ function get_list(json: IRequest, params: IMethodParams, cached_data: ICachedDat
         try {
             let data = <IData>json.data;
             let model = data.draft ? PrjTaskOutcomeDraft : PrjTaskOutcome;
+            let task_model = data.draft ? PrjPlanTaskDraft : PrjPlanTask;
             let sql = `
                 select outcome.id, outcome.name as name,
                        outcome.status, 
@@ -62,8 +65,21 @@ function get_list(json: IRequest, params: IMethodParams, cached_data: ICachedDat
             if (data.draft) sql += ', outcome.change_marker '
             sql += `
                 from ${model.table_name} outcome
-                left join ${PrjFile.table_name} file on file.dependent_id = outcome.id
-                where outcome.task_id = :task_id
+                left join ${PrjFile.table_name} file on file.id = outcome.object_name
+                where outcome.task_id in (
+                    WITH RECURSIVE ChildNodes AS (
+                        -- 基础情况:从给定的id开始
+                        SELECT id, name
+                        FROM ${task_model.table_name}
+                        WHERE id = :task_id  -- 这里替换成你要查询的节点id
+                        UNION ALL
+                        -- 递归情况:连接子节点
+                        SELECT A.id, A.name
+                        FROM  ${task_model.table_name} A
+                        INNER JOIN ChildNodes ON A.pid = ChildNodes.id
+                    )
+                    SELECT id FROM ChildNodes
+                )
             `
             let replacements: ISQLReplacements = {task_id: data.task_id};
             let result = await model.sequelize!.query(sql, {type: QueryTypes.SELECT, replacements: replacements, raw: true});

+ 27 - 3
pmr-biz-manager/src/routes/api/prj/outcome/modify.ts

@@ -11,6 +11,10 @@ import {ChangeMarker, TaskStatus} from "@src/utils/define";
 import {PrjPlanTask} from "@core-models/PrjPlanTask";
 import {PrjInfo} from "@core-models/PrjInfo";
 import {PrjPhaseDefine} from "@core-models/PrjPhaseDefine";
+import {
+    is_project_task_outcome_modifiable,
+    is_project_task_progress_modifiable
+} from "@src/utils/prj_premission_helper";
 
 interface IData {
     /**
@@ -32,7 +36,7 @@ interface IData {
     /**
      * 计划交付时间(YYYY-MM-DD)
      */
-    plan_time?: Date;
+    plan_time?: string;
 
     change_marker?: number;
     // [property: string]: any;
@@ -74,7 +78,8 @@ function statusGuard(json: IRequest, cached_data: ICachedData): Promise<void> {
                 data.change_marker = ChangeMarker.Changed;
             }
         }
-
+        if (!await is_project_task_outcome_modifiable(user, outcome.task_id, data.draft))
+            return reject(Resp.gen_err(Resp.Forbidden, '您没有权限修改此交付物信息。'));
         let model = data.draft ? PrjPlanTaskDraft : PrjPlanTask;
         let task = await model.findOne({where: {id: outcome.task_id}, raw: true});
         if (!task) return reject(Resp.gen_err(Resp.ResourceNotFound, '任务不存在。'));
@@ -104,6 +109,19 @@ function statusGuard(json: IRequest, cached_data: ICachedData): Promise<void> {
             return reject(Resp.gen_err(Resp.InvalidFlow, '当前任务阶段不允许修改交付物。'));
         }
 
+        if (data.plan_time) {
+            data.plan_time = data.plan_time + ' 23:59:59';
+
+            let plan_time = dayjs(data.plan_time);
+            let begin_time = dayjs(task.begin_at);
+            let end_time = dayjs(task.end_at);
+            if (plan_time.isBefore(begin_time)) {
+                return reject(Resp.gen_err(Resp.ParamsError, '交付物计划交付时间不能早于任务开始时间。'));
+            }
+            if (plan_time.isAfter(end_time)) {
+                return reject(Resp.gen_err(Resp.ParamsError, '交付物计划交付时间不能晚于任务结束时间。'));
+            }
+        }
 
         resolve();
     });
@@ -122,8 +140,14 @@ async function before_respond(content: IRequest, params: IMethodParams, cached_d
     try {
         /// 如果是草稿,且还没有生成计划变更任务,则生成任务
         if (data.draft ) {
+            let outcome = <PrjTaskOutcomeDraft>result;
+            if (outcome.change_marker !== ChangeMarker.Added) {
+                // 将交付物的状态改为已修改
+                await PrjTaskOutcomeDraft.update({change_marker: ChangeMarker.Changed},
+                        {where: {id: outcome.id}, transaction: t});
+            }
             let task = await PrjPlanTaskDraft.findOne({where: {id: result.task_id}, transaction: t});
-            if (task){
+            if (task && task.change_marker !== ChangeMarker.Added){
                 await start_plan_alt_flow(task.prj_id, cached_data.user_id, t as Transaction);
                 await PrjPlanTaskDraft.update({change_marker: ChangeMarker.Changed},
                     {where: {id: result.task_id}, transaction: t, returning: false});

+ 24 - 13
pmr-biz-manager/src/routes/api/prj/outcome/remove.ts

@@ -10,6 +10,10 @@ import {Logger} from "@util/Logger";
 import {ChangeMarker, TaskStatus} from "@src/utils/define";
 import {PrjPlanTask} from "@core-models/PrjPlanTask";
 import {Transaction} from "sequelize";
+import {
+    is_project_task_outcome_modifiable,
+    is_project_task_progress_modifiable
+} from "@src/utils/prj_premission_helper";
 
 interface IData {
     /**
@@ -29,20 +33,26 @@ async function remove(json: IRequest, params: IMethodParams, cached_data: ICache
     let t = await PrjInfo.sequelize!.transaction();
     try {
         if (data.draft) {
-            let outcome = await PrjTaskOutcomeDraft.update({change_marker: ChangeMarker.Deleted}, {
-                where: {id: data.id},
-                transaction: t,
-                returning: true
-            });
-            Logger.trace(outcome);
-            if (!outcome[1][0]) throw Resp.gen_err(Resp.ResourceNotFound);
-            let task = await PrjPlanTaskDraft.findOne({where: {id: outcome[1][0].task_id}, transaction: t});
-            if (task) {
-                await start_plan_alt_flow(task.prj_id, cached_data.user_id, t as Transaction);
-                await PrjPlanTaskDraft.update({change_marker: ChangeMarker.Changed},
-                    {where: {id: task.id}, transaction: t, returning: false});
+            let outcome = await PrjTaskOutcomeDraft.findOne({where: {id: data.id}, transaction: t});
+            if (outcome && outcome.change_marker === ChangeMarker.Added) {
+                await PrjTaskOutcomeDraft.destroy({where: {id: data.id}, transaction: t});
+            } else {
+                let outcome = await PrjTaskOutcomeDraft.update({change_marker: ChangeMarker.Deleted}, {
+                    where: {id: data.id},
+                    transaction: t,
+                    returning: true
+                });
+                Logger.trace(outcome);
+                if (!outcome[1][0]) throw Resp.gen_err(Resp.ResourceNotFound);
+                let task = await PrjPlanTaskDraft.findOne({where: {id: outcome[1][0].task_id}, transaction: t});
+                if (task && task.change_marker !== ChangeMarker.Added) {
+                    await start_plan_alt_flow(task.prj_id, cached_data.user_id, t as Transaction);
+                    await PrjPlanTaskDraft.update({change_marker: ChangeMarker.Changed},
+                        {where: {id: task.id}, transaction: t, returning: false});
+                }
             }
 
+
         } else {
             await PrjTaskOutcome.destroy({where: {id: data.id}, transaction: t});
         }
@@ -80,7 +90,8 @@ function statusGuard(json: IRequest, cached_data: ICachedData): Promise<void> {
         //         data.change_marker = ChangeMarker.Changed;
         //     }
         // }
-
+        if (!await is_project_task_outcome_modifiable(user, outcome.task_id, data.draft))
+            return reject(Resp.gen_err(Resp.Forbidden, '您没有权限删除此交付物信息。'));
         let model = data.draft ? PrjPlanTaskDraft : PrjPlanTask;
         let task = await model.findOne({where: {id: outcome.task_id}, raw: true});
         if (!task) return reject(Resp.gen_err(Resp.ResourceNotFound, '任务不存在。'));

+ 10 - 4
pmr-biz-manager/src/routes/api/prj/outcome/remove_file.ts

@@ -8,6 +8,7 @@ import {PrjTaskOutcome} from "@core-models/PrjTaskOutcome";
 import {PrjPlanTask} from "@core-models/PrjPlanTask";
 import {PrjInfo} from "@core-models/PrjInfo";
 import {PrjPhaseDefine} from "@core-models/PrjPhaseDefine";
+import {is_project_task_progress_modifiable} from "../../../../utils/prj_premission_helper";
 
 interface IData {
     /**
@@ -25,6 +26,13 @@ function statusGuard(json: IRequest, cached_data: ICachedData): Promise<void> {
         let outcome = await PrjTaskOutcome.findOne({where: {id: data.id}, raw: true});
         if (!outcome) return reject(Resp.gen_err(Resp.ResourceNotFound, "交付物不存在,id: " + data.id));
 
+        // 缓存object name
+        cached_data.object_name = outcome.object_name;
+
+        // TODO: 检查是否是项目组成员或有访问项目的权限
+        if (!await is_project_task_progress_modifiable(cached_data.user_id, outcome.task_id))
+            return reject(Resp.gen_err(Resp.Forbidden, '您没有权限删除此交付物的文件。'));
+
         let task = await PrjPlanTask.findOne({where: {id: outcome.task_id}, raw: true});
         if (!task) return reject(Resp.gen_err(Resp.ResourceNotFound, '任务不存在。'));
 
@@ -59,10 +67,8 @@ function remove_file(json: IRequest, params: IMethodParams, cached_data: ICached
         let t = await BizContractInfo.sequelize!.transaction();
         try {
             let data = <IData>json.data;
-            let file = await PrjFile.findOne({where: {dependent_id: data.id, category_id: PrjFileType.Outcome}, raw: true, transaction: t});
-            if (!file) throw Resp.gen_err(Resp.ResourceNotFound, `交付物文件(id:${data.id})不存在。`);
-            let prj_id = file.prj_id;
-            // TODO: 检查是否是项目组成员或有访问项目的权限
+            let file = await PrjFile.findOne({where: {id: cached_data.object_name}, raw: true, transaction: t});
+            if (!file) throw Resp.gen_err(Resp.ResourceNotFound, `交付物(id:${data.id})的文件(id ${cached_data.object_name})不存在。`);
 
             if (file.uploaded === false) throw Resp.gen_err(Resp.ResourceNotFound, '附件尚未上传。')
 

+ 7 - 4
pmr-biz-manager/src/routes/api/prj/outcome/upload.ts

@@ -10,7 +10,9 @@ import {PrjFile} from "@core-models/PrjFile";
 import dayjs from "dayjs";
 import {PrjPlanTaskDraft} from "@core-models/PrjPlanTaskDraft";
 import {PrjPhaseDefine} from "@core-models/PrjPhaseDefine";
-import {TaskStatus} from "@src/utils/define";
+import {PrjFileType, TaskStatus} from "@src/utils/define";
+import {is_project_task_progress_modifiable} from "@src/utils/prj_premission_helper";
+import {PrjFileCategory} from "@core-models/PrjFileCategory";
 
 interface IData {
     /**
@@ -32,6 +34,8 @@ function statusGuard(json: IRequest, cached_data: ICachedData): Promise<void> {
         let outcome = await PrjTaskOutcome.findOne({where: {id: data.id}, raw: true});
         if (!outcome) return reject(Resp.gen_err(Resp.ResourceNotFound, "交付物不存在,id: " + data.id));
 
+        if (!await is_project_task_progress_modifiable(cached_data.user_id, outcome.task_id))
+            return reject(Resp.gen_err(Resp.Forbidden, '您没有权限上传此交付物文件。'));
         let task = await PrjPlanTask.findOne({where: {id: outcome.task_id}, raw: true});
         if (!task) return reject(Resp.gen_err(Resp.ResourceNotFound, '任务不存在。'));
 
@@ -79,11 +83,10 @@ function get_upload_url(json: IRequest, params: IMethodParams, cached_data: ICac
             if (!prj || prj.length <= 0) throw Resp.gen_err(Resp.ResourceNotFound, '交付物记录不存在。');
             let prj_id = prj[0].id;
             let object_name = `project/${prj_id}/outcome/${data.id}`;
-            // await PrjTaskOutcome.update({object_name: object_name, filename: data.filename}, {where: {id: data.id}, transaction: t});
-
+            await PrjTaskOutcome.update({object_name: object_name}, {where: {id: data.id}, transaction: t, returning: false});
 
             await PrjFile.upsert({
-                category_id: 'outcome',
+                category_id: PrjFileType.Intermediate,
                 prj_id: prj_id,
                 req_upload_time: dayjs(),
                 id: object_name,

+ 8 - 2
pmr-biz-manager/src/routes/api/prj/plan/add_task.ts

@@ -16,6 +16,8 @@ import {bpmn_flow_on_end} from "@src/utils/bpmn_work_helper";
 import {PrjPlanTaskDraft} from "@core-models/PrjPlanTaskDraft";
 import {ChangeMarker} from "@src/utils/define";
 import {is_project_modifiable, is_project_task_addable} from "@src/utils/prj_premission_helper";
+import {PrjTaskOutcomeDraft} from "@core-models/PrjTaskOutcomeDraft";
+import {PrjTaskOutcome} from "@core-models/PrjTaskOutcome";
 
 interface IData {
     /**
@@ -96,7 +98,7 @@ function guard(json: IRequest, cached_data: ICachedData): Promise<void> {
 }
 
 // 如添加的是子任务,查找父任务是否存在
-// 如任务已经执行,不能再作为主任务添加子任务
+// 如任务已经执行,且之前没有包含子任务,不能再作为主任务添加子任务
 function check_parent_guard(json: IRequest, cached_data: ICachedData): Promise<void> {
     return new Promise<void>(async (resolve, reject) => {
         let data = <IData>json.data;
@@ -104,8 +106,12 @@ function check_parent_guard(json: IRequest, cached_data: ICachedData): Promise<v
         let model = data.draft ? PrjPlanTaskDraft : PrjPlanTask;
         let task = await model.findOne({where: {id: data.task_pid}, raw: true});
         if (!task) return reject(Resp.gen_err(Resp.ResourceNotFound, '没有找到指定的父任务'));
-        if (task.status > 0) return reject(Resp.gen_err(Resp.InvalidFlow, '任务已开始执行,此任务下不允许添加子任务。'));
 
+        let outcome = await (data.draft?PrjTaskOutcomeDraft:PrjTaskOutcome).findOne({where: {task_id: data.task_pid}, raw: true});
+        if (outcome) return reject(Resp.gen_err(Resp.InvalidFlow, '任务已设置了交付物,此任务下不允许添加子任务。'));
+        // 检查父任务下是否已经包含了子任务,如果没有,且父任务已经开始执行,则不允许添加子任务
+        let child_tasks = await model.findAll({where: {pid: data.task_pid}, raw: true});
+        if (task.status > 0 && child_tasks.length == 0) return reject(Resp.gen_err(Resp.InvalidFlow, '任务已开始执行,此任务下不允许添加子任务。'));
         resolve();
     });
 }

+ 6 - 5
pmr-biz-manager/src/routes/api/prj/plan/get_tasks.ts

@@ -190,11 +190,11 @@ function statusGuard(json: IRequest, cached_data: ICachedData): Promise<void> {
                     await PrjTaskOutcome.sequelize!.query(`
                         INSERT INTO ${PrjTaskOutcomeDraft.table_name}
                             (id, version, name, task_id, status, creator_id, plan_time,
-                            completed_at, created_at, updated_at, description, order_id, uploaded)
+                            completed_at, created_at, updated_at, description, order_id, uploaded, object_name)
                         SELECT outcome.id, outcome.version, outcome.name, 
                             outcome.task_id, outcome.status, outcome.creator_id, outcome.plan_time,
                             outcome.completed_at, outcome.created_at, outcome.updated_at, outcome.description, 
-                            outcome.order_id, outcome.uploaded
+                            outcome.order_id, outcome.uploaded, outcome.object_name
                             FROM ${PrjTaskOutcome.table_name} outcome, ${PrjPlanTask.table_name} task
                             WHERE task.prj_id = :prj_id and outcome.task_id = task.id;
                         `, {type: QueryTypes.INSERT, transaction: t, replacements: {prj_id: data.prj_id}});
@@ -208,7 +208,8 @@ function statusGuard(json: IRequest, cached_data: ICachedData): Promise<void> {
                     `, {type: QueryTypes.UPDATE, transaction: t, replacements: {prj_id: data.prj_id}});
                     await PrjTaskOutcome.sequelize!.query(`
                         UPDATE ${PrjTaskOutcomeDraft.table_name} 
-                        SET  status = B.status, updated_at = B.updated_at, uploaded = B.uploaded, completed_at = B.completed_at
+                        SET  status = B.status, updated_at = B.updated_at, uploaded = B.uploaded, completed_at = B.completed_at,
+                            object_name = B.object_name
                         FROM ${PrjTaskOutcome.table_name} B, ${PrjPlanTask.table_name} C
                         WHERE ${PrjTaskOutcomeDraft.table_name}.id = B.id and 
                             B.task_id = C.id and C.prj_id = :prj_id;
@@ -280,7 +281,7 @@ function get_tasks(json: IRequest, params: IMethodParams, cached_data: ICachedDa
                     left join ${BpmnCase.table_name} flow on flow.prj_id = :prj_id
                     LEFT JOIN ${BpmnWork.table_name} work ON work.case_id = flow.id and
                         work.assigned_to = '${cached_data.user_id}' and 
-                        work.status <> 2 and 
+                        work.status < 2 and work.process_type = 1 and
                         flow.model_id = 'task' and
                         work.task_id = task.id
                     where task.prj_id = :prj_id 
@@ -306,7 +307,7 @@ function get_tasks(json: IRequest, params: IMethodParams, cached_data: ICachedDa
                 select work.id, work.name, work.form 
                 from ${BpmnWork.table_name} work, ${BpmnCase.table_name} flow, ${BpmnModel.table_name} bpmn
                 where work.case_id = flow.id and flow.model_id = bpmn.id and 
-                    work.prj_id = :prj_id and work.status <> 2 and work.process_type = 1 and
+                    work.prj_id = :prj_id and work.status < 2 and work.process_type = 1 and
                     bpmn.id = :bpmn_id and work.assigned_to = :assigned_to
                 `, {
                 type: QueryTypes.SELECT,

+ 38 - 22
pmr-biz-manager/src/routes/api/prj/plan/modify_task.ts

@@ -6,7 +6,11 @@ import dayjs, {Dayjs} from "dayjs";
 import {Resp} from "@util/Resp";
 import {PrjPhaseDefine} from "@core-models/PrjPhaseDefine";
 import {PrjPlanTask} from "@core-models/PrjPlanTask";
-import {start_plan_alt_flow, update_parent_task_info} from "../../../../utils/plan_task_helper";
+import {
+    get_outcome_max_plan_time,
+    start_plan_alt_flow,
+    update_parent_task_info
+} from "../../../../utils/plan_task_helper";
 import {PrjPlanTaskDraft} from "@core-models/PrjPlanTaskDraft";
 import {Logger} from "@util/Logger";
 import {ChangeMarker, TaskStatus} from "@src/utils/define";
@@ -45,22 +49,22 @@ interface IData {
     [property: string]: any;
 }
 
-function time_guard(json: IRequest, cached_data: ICachedData): Promise<void> {
-    return new Promise<void>(async (resolve, reject) => {
-        let data = <IData>json.data;
-
-        let model = data.draft ? PrjPlanTaskDraft : PrjPlanTask;
-        let task = await model.findOne({where: {id: data.task_id}, raw: true});
-        if (!task) return reject(Resp.gen_err(Resp.ResourceNotFound));
-        let begin_at = data.begin_at ? data.begin_at : task.begin_at;
-        let end_at = data.end_at ? data.end_at : task.end_at;
-        if (dayjs(end_at).diff(begin_at) < 0) {
-            return reject(Resp.gen_err(Resp.IIllegalRequest, '任务预计完成时间不能早于或等于任务开始时间。'));
-        }
-
-        resolve();
-    });
-}
+// function time_guard(json: IRequest, cached_data: ICachedData): Promise<void> {
+//     return new Promise<void>(async (resolve, reject) => {
+//         let data = <IData>json.data;
+//
+//         let model = data.draft ? PrjPlanTaskDraft : PrjPlanTask;
+//         let task = await model.findOne({where: {id: data.task_id}, raw: true});
+//         if (!task) return reject(Resp.gen_err(Resp.ResourceNotFound));
+//         let begin_at = data.begin_at ? data.begin_at : task.begin_at;
+//         let end_at = data.end_at ? data.end_at : task.end_at;
+//         if (dayjs(end_at).diff(begin_at) < 0) {
+//             return reject(Resp.gen_err(Resp.IIllegalRequest, '任务预计完成时间不能早于或等于任务开始时间。'));
+//         }
+//
+//         resolve();
+//     });
+// }
 
 function guard(json: IRequest, cached_data: ICachedData): Promise<void> {
     return new Promise<void>(async (resolve, reject) => {
@@ -73,7 +77,7 @@ function guard(json: IRequest, cached_data: ICachedData): Promise<void> {
         if (!task)
             return reject(Resp.gen_err(Resp.ResourceNotFound, '计划任务项不存在。'));
         if (!await is_project_task_modifiable(model, user, task.id))
-            return reject(Resp.gen_err(Resp.Forbidden, `您没有权限修改任务 ${task.id}。`));
+            return reject(Resp.gen_err(Resp.Forbidden, `您没有权限修改计划,请确认您是本项目的负责人或特权人员。`));
 
         let prj_info = await PrjInfo.findOne({where: {id: task.prj_id}, raw: true});
         if (!prj_info) return reject(Resp.gen_err(Resp.ResourceNotFound));
@@ -124,6 +128,16 @@ function modify(json: IRequest, params: IMethodParams, cached_data: ICachedData)
             // 检查起止时间是否合法
             let begin_at = data.begin_at || task.begin_at;
             let end_at = data.end_at || task.end_at;
+            if (data.begin_at && !data.end_at && dayjs(end_at).diff(dayjs(begin_at)) <= 0) {
+                end_at = dayjs(begin_at).format('YYYY-MM-DD 23:59:59');
+                data.end_at = end_at;
+            }
+            let max_outcome_plan_time = await get_outcome_max_plan_time(data.draft, task.id, t);
+
+            // 检查任务结束时间不能早于项目立项时间
+            if (dayjs(end_at).diff(dayjs(begin_at)) <= 0)
+                throw Resp.gen_err(Resp.ParamsError, '任务结束时间必须晚于开始时间。');
+
             // 检查计划开始时间不能早于项目立项时间
             if (dayjs(begin_at).diff(dayjs(cached_data.started_at)) < 0) {
                 return reject(Resp.gen_err(Resp.ParamsError, `计划开始时间不能早于项目立项时间 ${dayjs(cached_data.started_at).format('YYYY-MM-DD')}。`));
@@ -133,8 +147,10 @@ function modify(json: IRequest, params: IMethodParams, cached_data: ICachedData)
                 return reject(Resp.gen_err(Resp.ParamsError, `计划结束时间不能晚于项目交付时间 ${dayjs(cached_data.deliver_at).format('YYYY-MM-DD')}。`));
             }
 
-            if (dayjs(end_at).diff(dayjs(begin_at)) <= 0)
-                throw Resp.gen_err(Resp.ParamsError, '任务结束时间必须晚于开始时间。');
+            // 检查任务结束时间不能早交付物计划交付时间
+            if (dayjs(end_at).diff(dayjs(max_outcome_plan_time)) < 0) {
+                return reject(Resp.gen_err(Resp.ParamsError, `任务结束时间不能早于最晚的交付物计划交付时间 ${dayjs(max_outcome_plan_time).format('YYYY-MM-DD')}。`));
+            }
 
             if (data.draft) {
                 Logger.trace(`[prj-plan-task-modify] draft task:  ${data.task_id}, change_marker: ${(<PrjPlanTaskDraft>task).change_marker}`);
@@ -210,7 +226,7 @@ const v1_0: IApiProcessor = {
                         "title": "计划结束时间(YYYY-MM-DD)"
                     },
                     "description": {
-                        "type": "string",
+                        "type": ["string", "null"],
                         "title": "任务描述"
                     },
                     "begin_at": {
@@ -274,7 +290,7 @@ const v1_0: IApiProcessor = {
             description: "description"
         }
     },
-    guards: [time_guard, guard]
+    guards: [guard]
 }
 
 module.exports = {

+ 20 - 10
pmr-biz-manager/src/routes/api/prj/plan/remove_task.ts

@@ -49,17 +49,17 @@ function statusGuard(json: IRequest, cached_data: ICachedData): Promise<void> {
         let phase = await PrjPhaseDefine.findOne({where: {id: prj_info.phase_id}, raw: true});
         if (!phase) return reject(Resp.gen_err(Resp.ResourceNotFound, '项目状态信息出错,id: ' + prj_info.id));
 
-        if (data.draft) {
-            if (phase.order_index >= 60 && phase.order_index <= 65) return reject(Resp.gen_err(Resp.InvalidFlow, '计划变更已提交审核,当前阶段不允许删除。'));
-            return resolve();/// 其它情况下草稿允许随意删除
-        }
+
 
         // 任务已启动,有进度或已提交了交付物,不允许删除
         let outcome_model = data.draft ? PrjTaskOutcomeDraft : PrjTaskOutcome;
         let outcome = await outcome_model.findOne({where: {uploaded: true, task_id: data.task_id}, raw: true});
         if (task.progress > 0 || outcome) return reject(Resp.gen_err(Resp.InvalidFlow, '任务已启动,不允许删除。'));
 
-
+        if (data.draft) {
+            if (phase.order_index >= 60 && phase.order_index <= 65) return reject(Resp.gen_err(Resp.InvalidFlow, '计划变更已提交审核,当前阶段不允许删除。'));
+            return resolve();/// 其它情况下草稿允许随意删除
+        }
 
         if (phase.order_index < 20) return reject(Resp.gen_err(Resp.InvalidFlow, '需要先对项目进行立项,立项后才可制订计划。'));
         if (phase.order_index >= 30 && phase.order_index <= 40 && data.draft === false)
@@ -81,16 +81,26 @@ async function remove(json: IRequest, params: IMethodParams, cached_data: ICache
         let child = await model.findOne({where: {pid: data.task_id}, raw: true, transaction: t});
         if (child) throw Resp.gen_err(Resp.IIllegalRequest, '计划任务项有子任务,无法删除。');
 
+
         if (data.draft) {
-            await model.update({change_marker: ChangeMarker.Deleted}, {
-                where: {id: data.task_id},
-                transaction: t,
-                returning: false
-            });
+            if ((<PrjPlanTaskDraft>task).change_marker! = ChangeMarker.Added) {
+                // 删除交付物记录
+                await PrjTaskOutcomeDraft.destroy({where: {task_id: data.task_id}, transaction: t});
+                // 在草稿中新增的条目可以直接删除
+                await model.destroy({where: {id: data.task_id}, transaction: t});
+            } else {
+                await model.update({change_marker: ChangeMarker.Deleted}, {
+                    where: {id: data.task_id},
+                    transaction: t,
+                    returning: false
+                });
+            }
+
             /// 如果计划任务是草稿,且还没有生成计划变更任务,则生成任务
             await start_plan_alt_flow(task.prj_id, cached_data.user_id, t);
 
         } else {
+            await PrjTaskOutcome.destroy({where: {task_id: data.task_id}, transaction: t});
             await model.destroy({where: {id: data.task_id}, transaction: t});
         }
         await update_parent_task_info(data.draft, task.prj_id, task.pid, t);

+ 8 - 5
pmr-biz-manager/src/routes/api/prj/plan/set_check_flow.ts

@@ -81,13 +81,16 @@ function set_check_flow(json: IRequest, params: IMethodParams, cached_data: ICac
         try {
             /// 如果计划任务是草稿,且还没有生成计划变更任务,则生成任务
             if (data.draft === true ) {
-                await PrjPlanTaskDraft.update({
-                    checkers: data.checkers,
-                    change_marker: ChangeMarker.Changed
-                }, {where: {id: data.task_id}, transaction: t, returning: false});
+
                 let task = await PrjPlanTaskDraft.findOne({where: {id: data.task_id}, transaction: t, raw: true});
-                if (task)
+                if (task) {
+                    await PrjPlanTaskDraft.update({
+                        checkers: data.checkers,
+                        change_marker: task.change_marker === ChangeMarker.Added ? ChangeMarker.Added : ChangeMarker.Changed
+                    }, {where: {id: data.task_id}, transaction: t, returning: false});
                     await start_plan_alt_flow(task.prj_id, cached_data.user_id, t as Transaction);
+                }
+
             } else {
                 await PrjPlanTask.update({checkers: data.checkers},
                     {where: {id: data.task_id}, returning: false});

+ 10 - 4
pmr-biz-manager/src/routes/api/prj/plan/set_progress.ts

@@ -35,7 +35,9 @@ function statusGuard(json: IRequest, cached_data: ICachedData): Promise<void> {
         if (task.status === TaskStatus.ApplyForReview || task.status === TaskStatus.Reviewing) {
             return reject(Resp.gen_err(Resp.InvalidFlow, '计划任务项审核中,当前阶段不允许修改进度。'));
         }
-        if (task.status === TaskStatus.DueDone || task.status === TaskStatus.Completed) {
+
+        // 任务已完成,并且是有审核流程的,则不允许将已完成的任务回滚到未完成状态。
+        if (task.status === TaskStatus.DueDone || task.status === TaskStatus.Completed && task.checkers.length > 0) {
             return reject(Resp.gen_err(Resp.InvalidFlow, '计划任务项已完成,当前阶段不允许修改进度。'));
         }
 
@@ -66,11 +68,15 @@ function set_progress(json: IRequest, params: IMethodParams, cached_data: ICache
             let task = await PrjPlanTask.findOne({where: {id: data.task_id}, transaction: t, raw: true});
             if (!task) throw Resp.gen_err(Resp.ResourceNotFound, '任务不存在');
             let status = task.status;
-            if (data.progress > 0 && status === TaskStatus.New) status = TaskStatus.Doing;
+            if (data.progress > 0) status = TaskStatus.Doing;
             else if (data.progress === 0) status = TaskStatus.New;
 
-            if (data.progress === 100) {    // 不能直接设置任务完成度为100%,因为还有一个审核流程
-                data.progress = 99;
+            if (data.progress === 100) {    // 如果还有一个审核流程,就不能直接设置任务完成度为100%,
+                if (task.checkers && task.checkers.length > 0)
+                    data.progress = 99;
+                else
+                    status = TaskStatus.Completed;
+
             }
 
             // 如果条件正确的话,开启任务审核流程,第一个工作项是"提交审核"

+ 5 - 5
pmr-biz-manager/src/routes/api/prj/plan/task_detail.ts

@@ -25,8 +25,8 @@ function guard(json: IRequest, cached_data: ICachedData): Promise<void> {
     return new Promise<void>(async (resolve, reject) => {
         let user = cached_data.user_id;
         let data = <IData>json.data;
-        if (!await is_project_task_accessible(user, data.task_id))
-            return reject(Resp.gen_err(Resp.Forbidden, '您没有权限访问该项目。'));
+        if (!await is_project_task_accessible(user, data.task_id, data.draft))
+            return reject(Resp.gen_err(Resp.Forbidden, '您没有权限访问此任务。'));
         resolve();
     });
 }
@@ -42,8 +42,8 @@ function task_detail(json: IRequest, params: IMethodParams, cached_data: ICached
                     task.name, task.progress, task.status, 
                     task.handler_id, handler.name as handler_name, 
                     task.creator_id, creator.name as creator_name,
-                    TO_CHAR(task.begin_at, 'yyyy-MM-dd HH24' || ':' || 'MI' || ':' || 'ss') as begin_at,
-                    TO_CHAR(task.end_at, 'yyyy-MM-dd HH24' || ':' || 'MI' || ':' || 'ss') as end_at,
+                    TO_CHAR(task.begin_at, 'yyyy-MM-dd') as begin_at,
+                    TO_CHAR(task.end_at, 'yyyy-MM-dd') as end_at,
                     TO_CHAR(task.updated_at, 'yyyy-MM-dd HH24' || ':' || 'MI' || ':' || 'ss') as updated_at,
                     TO_CHAR(task.created_at, 'yyyy-MM-dd HH24' || ':' || 'MI' || ':' || 'ss') as created_at,
                     TO_CHAR(task.completed_at, 'yyyy-MM-dd HH24' || ':' || 'MI' || ':' || 'ss') as completed_at,
@@ -72,7 +72,7 @@ function task_detail(json: IRequest, params: IMethodParams, cached_data: ICached
                     left join ${AcsUserInfo.table_name} creator on creator.id = task.creator_id
                     LEFT JOIN ${BpmnWork.table_name} work ON 
                         work.assigned_to = '${cached_data.user_id}' and 
-                        work.status <> 2 and 
+                        work.status < 2 and work.process_type = 1 and 
                         work.task_id = task.id
                     where task.id = :task_id 
                     group by task.id, task.pid,

+ 3 - 3
pmr-biz-manager/src/routes/api/prj/week_report/modify.ts

@@ -80,15 +80,15 @@ const v1_0: IApiProcessor = {
                         "title": "周报id"
                     },
                     "summary": {
-                        "type": "string",
+                        "type": ["string", "null"],
                         "title": "本周总结"
                     },
                     "plan": {
-                        "type": "string",
+                        "type": ["string", "null"],
                         "title": "下周计划"
                     },
                     "issue": {
-                        "type": "string",
+                        "type": ["string", "null"],
                         "title": "需要协调的问题"
                     }
                 },

+ 114 - 0
pmr-biz-manager/src/routes/api/prj/work/get_chain.ts

@@ -0,0 +1,114 @@
+
+import {IApiProcessor, ICachedData, IMethodParams, IRequest} from "@core/Defined";
+import {Resp} from "@util/Resp";
+import {QueryTypes, WhereOptions} from "sequelize";
+import DataCURD, {ISQLReplacements} from "@core/DataCURD";
+import {AcsUserInfo} from "@core-models/AcsUserInfo";
+import {BpmnWork} from "@core-models/BpmnWork";
+import {PrjInfo} from "@core-models/PrjInfo";
+interface IData {
+    /**
+     * 通过工作流中的一个节点id查询目前已完成的工作链
+     */
+    work_id: string;
+}
+
+
+function get_list(json: IRequest, params: IMethodParams, cached_data: ICachedData): Promise<WhereOptions> {
+    return new Promise<WhereOptions>(async (resolve, reject) => {
+        try {
+            let data = <IData>json.data;
+            let work = await BpmnWork.findOne({where: {id: data.work_id}, raw: true});
+            if (!work) throw Resp.gen_err(Resp.ResourceNotFound, '工作不存在');
+
+            let sql = `
+                select work.id, work.name, 
+                    work.process_type, work.status,
+                    work.assigned_to as handler_id, handler.name as handler_name, work.desc,
+                    
+                    TO_CHAR(work.started_at, 'yyyy-MM-dd HH24' || ':' || 'MI' || ':' || 'ss') as started_at,
+                    TO_CHAR(work.completed_at, 'yyyy-MM-dd HH24' || ':' || 'MI' || ':' || 'ss') as completed_at,
+                    TO_CHAR(work.due_at, 'yyyy-MM-dd HH24' || ':' || 'MI' || ':' || 'ss') as due_at
+                from ${BpmnWork.table_name} work
+                left join ${AcsUserInfo.table_name} handler on handler.id = work.assigned_to
+                where  work.case_id = :case_id and show_in_my_works = true and work.status <> 3 
+                order by work.started_at desc
+            `;
+
+            let replacements: ISQLReplacements = {case_id: work.case_id};
+
+            let result = await BpmnWork.sequelize!.query(sql, {type: QueryTypes.SELECT, replacements: replacements, raw: true});
+            let list = DataCURD.filter_null(result);
+            resolve({list: list});
+        } catch (e) {
+            reject(Resp.throw_err(e));
+        }
+    })
+}
+
+const v1_0: IApiProcessor = {
+    schema: {
+        "type": "object",
+        "properties": {
+            "data": {
+                "type": "object",
+                "properties": {
+                    "work_id": {
+                        "type": "string",
+                        "title": " 工作项id"
+                    }
+                },
+                "x-apifox-orders": [
+                    "work_id"
+                ],
+                "title": "请求参数内容",
+                "required": [
+                    "work_id"
+                ]
+            },
+            "ver": {
+                "type": "string",
+                "title": "版本号",
+                "description": "文档中没有说明时,默认为1.0",
+                "default": "1.0"
+            },
+            "app_id": {
+                "type": "string",
+                "title": "应用id",
+                "description": "平台分配给客户端的app id。一个应用一个id。"
+            },
+            "timestamp": {
+                "type": "integer",
+                "title": "UTC时间戳",
+                "description": "UTC时间戳,精确到毫秒。平台对超过五分钟的消息,将做忽略处理"
+            },
+            "token": {
+                "type": "string",
+                "title": "身份认证token",
+                "description": "平台登录后分配的token"
+            }
+        },
+        "x-apifox-orders": [
+            "ver",
+            "app_id",
+            "timestamp",
+            "token",
+            "data"
+        ],
+        "required": [
+            "data",
+            "ver",
+            "app_id",
+            "timestamp",
+            "token"
+        ]
+    },
+    method: get_list,
+    method_params: {}
+}
+
+
+module.exports = {
+    default: v1_0,
+    v1_0: v1_0
+}

+ 20 - 5
pmr-biz-manager/src/routes/api/prj/work/get_list.ts

@@ -23,13 +23,17 @@ interface IData {
      */
     page_size: number;
     /**
-     * 精确通过项目编号查找文档
+     * 精确通过项目编号查找
      */
     prj_id?: string;
     /**
      * 工作状态,0=进行中,1=已过期,2=已完成
      */
     status?: number;
+    /**
+     * 工作类型,1=待阅,1=待办
+     */
+    process_type?: number;
 }
 
 
@@ -52,7 +56,7 @@ function get_list(json: IRequest, params: IMethodParams, cached_data: ICachedDat
                 from ${BpmnWork.table_name} work
                 left join ${PrjInfo.table_name} prj on work.prj_id = prj.id
                 left join ${AcsUserInfo.table_name} leader on leader.id = prj.leader_id
-                where work.show_in_my_works = true and work.assigned_to = :user_id
+                where work.show_in_my_works = true and work.assigned_to = :user_id and work.status <> 3
             `;
             let replacements: ISQLReplacements = {user_id: cached_data.user_id};
             if (data.prj_id !== undefined) {
@@ -60,10 +64,15 @@ function get_list(json: IRequest, params: IMethodParams, cached_data: ICachedDat
                 replacements.prj_id = `%${data.prj_id}%`;
             }
 
+            if (data.process_type !== undefined) {
+                condition += ` and work.process_type = :process_type `;
+                replacements.process_type = data.process_type;
+            }
+
             if (data.status !== undefined) {
 
-                if (data.status === 3) {
-                    condition += ` and work.status <> :status `;
+                if (data.status === 3) { // 这里的3表示取未完成项
+                    condition += ` and work.status < :status `;
                     replacements.status = 2;
                 } else {
                     condition += ` and work.status = :status `;
@@ -120,6 +129,11 @@ const v1_0: IApiProcessor = {
                         "type": "string",
                         "title": "精确通过项目编号查找文档"
                     },
+                    "process_type": {
+                        "type": "integer",
+                        "title": "任务类型",
+                        "description": "1=待阅,2=待办"
+                    },
                     "begin_from": {
                         "type": "string",
                         "title": "查询起始",
@@ -135,13 +149,14 @@ const v1_0: IApiProcessor = {
                     "status": {
                         "type": "integer",
                         "title": "任务状态",
-                        "description": "0=进行中,1=已过期,2=已完成"
+                        "description": "0=进行中,1=已过期,2=已完成,3=已取消"
                     }
                 },
                 "x-apifox-orders": [
                     "page_no",
                     "page_size",
                     "prj_id",
+                    "process_type",
                     "begin_from",
                     "end_to",
                     "status"

+ 2 - 0
pmr-biz-manager/src/routes/api/prj/work/stat.ts

@@ -13,6 +13,7 @@ function stat(json: IRequest, params: IMethodParams, cached_data: ICachedData):
                 where: {
                     assigned_to: cached_data.user_id,
                     show_in_my_works: true,
+                    status: {[Op.ne]: 3}
                 }
             });
             result.push({
@@ -44,6 +45,7 @@ function stat(json: IRequest, params: IMethodParams, cached_data: ICachedData):
                 where: {
                     assigned_to: cached_data.user_id,
                     show_in_my_works: true,
+                    status: {[Op.ne]: 3},
                     started_at: {
                         [Op.gte]: new Date(new Date().getFullYear() + '-01-01 00:00:00'),
                         [Op.lte]: new Date(new Date().getFullYear() + '-12-31 23:59:59')

+ 8 - 8
pmr-biz-manager/src/routes/api/prj/work/submit.ts

@@ -24,18 +24,18 @@ function submit(json: IRequest, params: IMethodParams, cached_data: ICachedData)
             let engine = FlowEngine.execution_engine_map.get(data.id);
             if (engine) {
                 let work = await BpmnWork.findOne({where: {id: data.id}, raw: true});
-                if (work && work.status !== 2) {
+                if (work && work.status < 2) {
                     // if (data.form_data && data.form_data.pass !== undefined) {
                     //     data.form_data.break = !data.form_data.pass;
                     // }
                     await engine.signal(work.activity_id, data.id, cached_data.user_id, {...data.form_data});
-                    await PrjLogger.log({
-                        creator_id: cached_data.user_id,
-                        creator_name: cached_data.user_name,
-                        content: work.name,
-                        prj_id: work.prj_id,
-                        task_id: work.task_id,
-                    })
+                    // await PrjLogger.log({
+                    //     creator_id: cached_data.user_id,
+                    //     creator_name: cached_data.user_name,
+                    //     content: work.name,
+                    //     prj_id: work.prj_id,
+                    //     task_id: work.task_id,
+                    // });
                 }
 
             }

+ 1 - 1
pmr-biz-manager/src/routes/api/prj/work/undone_count.ts

@@ -13,7 +13,7 @@ function stat(json: IRequest, params: IMethodParams, cached_data: ICachedData):
                     assigned_to: cached_data.user_id,
                     show_in_my_works: true,
                     status: {
-                        [Op.ne]: 2
+                        [Op.lt]: 2
                     }
                 }
             });

+ 1 - 1
pmr-biz-manager/src/routes/api/prj/work/undone_stat.ts

@@ -14,7 +14,7 @@ function stat(json: IRequest, params: IMethodParams, cached_data: ICachedData):
                     assigned_to: cached_data.user_id,
                     show_in_my_works: true,
                     status: {
-                        [Op.ne]: 2
+                        [Op.lt]: 2
                     }
                 }
             });

+ 1 - 0
pmr-biz-manager/src/routes/api/report/prj/multi_stat.ts

@@ -6,6 +6,7 @@ import {PrjInfo} from "@core-models/PrjInfo";
 import {BizCustomer} from "@core-models/BizCustomer";
 import {BizContractInfo} from "@core-models/BizContractInfo";
 
+// 多个统计,用于首页仪表盘
 function stat(_json: IRequest, _params: IMethodParams, _cached_data: ICachedData): Promise<any> {
     return new Promise<any>(async (resolve, reject) => {
         try {

+ 50 - 20
pmr-biz-manager/src/utils/bpmn_work_helper.ts

@@ -13,6 +13,8 @@ import {BpmnCase} from "@core-models/BpmnCase";
 import dayjs from "dayjs";
 import {PrjLogger} from "@src/utils/prj_logger";
 import {AcsUserInfo} from "@core-models/AcsUserInfo";
+import {BpmnWork} from "@core-models/BpmnWork";
+import {update_parent_task_info} from "@src/utils/plan_task_helper";
 
 
 export async function bpmn_flow_on_end(id: string, model_id: string, prj_id: string, owner: string): Promise<void> {
@@ -38,7 +40,7 @@ export async function bpmn_flow_on_end(id: string, model_id: string, prj_id: str
                 creator_id: owner,
                 creator_name: user_name,
                 prj_id: prj_id,
-                content: `项目计划制定成功`,
+                content: `项目计划已通过审核,开始执行。`,
             });
             break;
         case 'plan_alter':
@@ -49,7 +51,7 @@ export async function bpmn_flow_on_end(id: string, model_id: string, prj_id: str
                     creator_id: owner,
                     creator_name: user_name,
                     prj_id: prj_id,
-                    content: `项目计划变更成功`,
+                    content: `项目计划变更已通过审核。`,
                     t
                 }as any);
                 // 更新计划变更版本
@@ -80,6 +82,15 @@ export async function bpmn_flow_on_end(id: string, model_id: string, prj_id: str
                     WHERE change_marker = ${ChangeMarker.Added} and prj_id = :prj_id;
                 `, {type: QueryTypes.INSERT, transaction: t, replacements: {prj_id: prj_id}});
                 // 3. 删除
+                // TODO: 清除已上传的交付物文件
+                await BpmnWork.sequelize!.query(`
+                    DELETE FROM ${BpmnWork.table_name}
+                    WHERE task_id IN (SELECT id FROM ${PrjPlanTaskDraft.table_name} WHERE change_marker = ${ChangeMarker.Deleted} and prj_id = :prj_id);
+                `, {type: QueryTypes.DELETE, transaction: t, replacements: {prj_id: prj_id}});
+                await PrjTaskOutcome.sequelize!.query(`
+                    DELETE FROM ${PrjTaskOutcome.table_name}
+                    WHERE task_id IN (SELECT id FROM ${PrjPlanTaskDraft.table_name} WHERE change_marker = ${ChangeMarker.Deleted} and prj_id = :prj_id);
+                `, {type: QueryTypes.DELETE, transaction: t, replacements: {prj_id: prj_id}});
                 await PrjPlanTask.sequelize!.query(`
                     DELETE FROM ${PrjPlanTask.table_name}
                     WHERE id IN (SELECT id FROM ${PrjPlanTaskDraft.table_name} WHERE change_marker = ${ChangeMarker.Deleted} and prj_id = :prj_id);
@@ -92,7 +103,6 @@ export async function bpmn_flow_on_end(id: string, model_id: string, prj_id: str
                     SET name = B.name, task_id = B.task_id, status = B.status, creator_id = B.creator_id,
                         plan_time = B.plan_time, completed_at = B.completed_at,
                         created_at = B.created_at, updated_at = B.updated_at, description = B.description, order_id = B.order_id,
-                        uploaded = B.uploaded,
                         version = B.version
                     FROM ${PrjTaskOutcomeDraft.table_name} B, ${PrjPlanTaskDraft.table_name} C
                     WHERE B.change_marker = ${ChangeMarker.Changed} and 
@@ -132,30 +142,49 @@ export async function bpmn_flow_on_end(id: string, model_id: string, prj_id: str
 
                 await t.commit();
             } catch (e) {
+                await t.rollback();
                 Logger.error(e);
                 return Resp.gen_err(Resp.InternalServerError, '计划变更失败');
             }
-
+            // TODO: 移除已删除的任务的流程
 
             break;
         case 'task':
             let flow_case = await BpmnCase.findOne({where: {id: id, model_id: 'task'}, raw: true});
             if (flow_case) {
-                await PrjPlanTask.update({
-                    progress: 100,
-                    status: TaskStatus.Completed,
-                    completed_at: dayjs().format('YYYY-MM-DD HH:mm:ss'),
-                }, {
-                    where: {id: flow_case.task_id},
-                    returning: false
-                });
-                await PrjLogger.log({
-                    creator_id: owner,
-                    creator_name: user_name,
-                    content: `任务审核通过,任务完成`,
-                    prj_id: prj_id,
-                    task_id: flow_case.task_id,
-                })
+                let t = await PrjTaskOutcome.sequelize!.transaction();
+                try {
+                    let task = await PrjPlanTask.update({
+                        progress: 100,
+                        status: TaskStatus.Completed,
+                        completed_at: dayjs().format('YYYY-MM-DD HH:mm:ss'),
+                    }, {
+                        where: {id: flow_case.task_id},
+                        returning: true,
+                        transaction: t
+                    });
+                    await update_parent_task_info(false, prj_id, task[1][0].pid, t);
+                    // 设置所有该任务下的交付物的完成时间
+                    await PrjTaskOutcome.update({
+                        completed_at: dayjs().format('YYYY-MM-DD HH:mm:ss')
+                    }, {
+                        where: {task_id: flow_case.task_id},
+                        transaction: t
+                    })
+                    await PrjLogger.log({
+                        creator_id: owner,
+                        creator_name: user_name,
+                        content: `任务审核通过,任务完成`,
+                        prj_id: prj_id,
+                        task_id: flow_case.task_id,
+                        t: t
+                    })
+                    await t.commit();
+                } catch (e) {
+                    Logger.error(e);
+                    await t.rollback();
+                }
+
             }
             break;
     }
@@ -181,7 +210,8 @@ export async function create_task_approve_flow_if_not_exists(prj_id: string, own
         let flow = new FlowEngine('task', bpmn.content, {
                 prj_id: prj_id,
                 owner: owner,
-                task_id: task_id
+                task_id: task_id,
+                task_name: task.name
             },
             bpmn_flow_on_end
         );

+ 1 - 0
pmr-biz-manager/src/utils/define.ts

@@ -34,6 +34,7 @@ export enum PrjFileType {
     Contract = 'contract',
     WeekReport = 'week_report',
     Outcome = 'outcome',
+    Intermediate = 'intermediate'
 }
 
 // 任务审核流程

+ 27 - 9
pmr-biz-manager/src/utils/plan_task_helper.ts

@@ -1,4 +1,4 @@
-import {Op, Transaction, WhereOptions} from "sequelize";
+import {Op, QueryTypes, Transaction, WhereOptions} from "sequelize";
 import dayjs, {Dayjs} from "dayjs";
 import {PrjPlanTask} from "@core-models/PrjPlanTask";
 import {PrjInfo} from "@core-models/PrjInfo";
@@ -9,11 +9,11 @@ import {Resp} from "@util/Resp";
 import {FlowEngine} from "@src/bpmn/flow_engine";
 import {bpmn_flow_on_end} from "@src/utils/bpmn_work_helper";
 import {BpmnCase} from "@core-models/BpmnCase";
-import {ChangeMarker} from "@src/utils/define";
+import {ChangeMarker, TaskStatus} from "@src/utils/define";
 import {PrjTaskOutcome} from "@core-models/PrjTaskOutcome";
 import {PrjTaskOutcomeDraft} from "@core-models/PrjTaskOutcomeDraft";
 
-export async function update_parent_task_info(draft: boolean, prj_id: string, task_pid: string | null, t: Transaction, max?: Dayjs, min?: Dayjs): Promise<void> {
+export async function update_parent_task_info(draft: boolean, prj_id: string, task_pid: string | null, t?: any, max?: Dayjs, min?: Dayjs): Promise<void> {
     Logger.trace(`prj_id: ${prj_id} task_id: ${task_pid}`)
 
     let model = draft ? PrjPlanTaskDraft : PrjPlanTask;
@@ -21,7 +21,7 @@ export async function update_parent_task_info(draft: boolean, prj_id: string, ta
     if (draft) where.change_marker = {[Op.ne]: ChangeMarker.Deleted};
     let child_tasks = await model.findAll({
         where: where,
-        transaction: t
+        transaction: t as any
     });
     let total_duration = 0;
     let completed = 0;
@@ -48,8 +48,9 @@ export async function update_parent_task_info(draft: boolean, prj_id: string, ta
         let task = await model.findOne({where: {id: task_pid}, raw: true, transaction: t});
         if (!task) return;
         let status = task.status;
-        if (progress > 0 && status === 0) status = 10;
-        else if (progress === 0) status = 0;
+        if (progress > 0 && status === 0) status = TaskStatus.Doing;
+        else if (progress === 0) status = TaskStatus.New;
+        else if (progress === 100) status = TaskStatus.Completed;
         let parent = await model.update({
             begin_at: min!,
             end_at: max!,
@@ -59,13 +60,13 @@ export async function update_parent_task_info(draft: boolean, prj_id: string, ta
         if (parent[0] === 0) return;
         await update_parent_task_info(draft, prj_id, parent[1][0].pid, t);
     } else {
-        await PrjInfo.update({progress: progress}, {where: {id: prj_id}, transaction: t});
+        await PrjInfo.update({progress: progress}, {where: {id: prj_id}, transaction: t, returning: false});
     }
 
     // }
 }
 
-export async function start_plan_alt_flow(prj_id: string, user_id: string, t?: Transaction): Promise<void> {
+export async function start_plan_alt_flow(prj_id: string, user_id: string, t?: any): Promise<void> {
     let flow_case = await BpmnCase.findOne({
         where: {
             prj_id: prj_id,
@@ -101,7 +102,7 @@ export interface TaskInfo {
     outcome_status: number;
 }
 
-export async function get_task_info_by_outcome_id(draft: boolean, outcome_id: string, t?: Transaction): Promise<TaskInfo | undefined> {
+export async function get_task_info_by_outcome_id(draft: boolean, outcome_id: string, t?: any): Promise<TaskInfo | undefined> {
     let outcome_model = draft ? PrjTaskOutcomeDraft : PrjTaskOutcome;
     let outcome = await outcome_model.findOne({
         where: {outcome_id: outcome_id},
@@ -123,3 +124,20 @@ export async function get_task_info_by_outcome_id(draft: boolean, outcome_id: st
     };
 }
 
+export async function get_outcome_max_plan_time(draft: boolean, task_id: string, t?: any): Promise<Date | null> {
+
+    let outcome_model = draft ? PrjTaskOutcomeDraft : PrjTaskOutcome;
+    let sql = `
+        select max(plan_time) as max_time from ${outcome_model.tableName} where task_id = :task_id
+    `
+    let outcome: any = await outcome_model.sequelize!.query(sql, {
+        replacements: {task_id: task_id},
+        type: QueryTypes.SELECT,
+        transaction: t
+
+    });
+
+    if (!outcome || outcome.length === 0) return null;
+
+    return outcome[0].max_time;
+}

+ 2 - 2
pmr-biz-manager/src/utils/prj_logger.ts

@@ -43,7 +43,7 @@ export class PrjLogger {
     static async updated(info: IPrjLogInfo) {
         /// 以修改了: <旧内容>-><新内容>的格式生成日志内容
         if (info.old_content) {
-            info.content = `修改了: <span style="color: red;">${info.old_content}</span>-><span style="color: blue;">${info.content}</span>`;
+            info.content = `修改了: <span style="color: red;">${info.old_content}</span>-><span style="color: rgb(22,93,255);">${info.content}</span>`;
         } else {
             info.content = `修改了: ${info.content}`;
         }
@@ -52,7 +52,7 @@ export class PrjLogger {
     }
 
     static async status_changed(info: IPrjLogInfo) {
-        info.content = `状态变更为: <span style="color: blue;">${info.content}</span>`;
+        info.content = `状态变更为: <span style="color: rgb(22,93,255);">${info.content}</span>`;
         await PrjLogger.log(info);
     }
 }

+ 20 - 4
pmr-biz-manager/src/utils/prj_premission_helper.ts

@@ -123,8 +123,10 @@ export async function is_project_modifiable(account_id: string, project_id: stri
 }
 
 /// 是否有访问项目任务的权限
-export async function is_project_task_accessible(account_id: string, task_id: string): Promise<boolean> {
-    let task = await PrjPlanTask.findOne({where: {id: task_id}, raw: true});
+export async function is_project_task_accessible(account_id: string, task_id: string, draft?: boolean): Promise<boolean> {
+    if (draft === undefined) draft = false;
+    let model = draft ? PrjPlanTaskDraft : PrjPlanTask;
+    let task = await model.findOne({where: {id: task_id}, raw: true});
     if (task && (
         task.handler_id === account_id ||
         await is_project_accessible(account_id, task.prj_id)
@@ -139,7 +141,7 @@ export async function is_project_task_accessible(account_id: string, task_id: st
 
 /// 是否有权限添加任务
 export async function is_project_task_addable(account_id: string, project_id: string): Promise<boolean> {
-    if ( await is_project_modifiable(account_id, project_id)) {
+    if (await is_project_modifiable(account_id, project_id)) {
         Logger.trace(`${account_id} 有权添加项目 ${project_id} 的任务。`);
         return true;
     }
@@ -151,7 +153,7 @@ export async function is_project_task_addable(account_id: string, project_id: st
 /// 是否有修改项目任务的权限
 export async function is_project_task_modifiable(model: any, account_id: string, task_id: string): Promise<boolean> {
     let task = await model.findOne({where: {id: task_id}, raw: true});
-    if (task &&  await is_project_modifiable(account_id, task.prj_id)) {
+    if (task && await is_project_modifiable(account_id, task.prj_id)) {
         Logger.trace(`${account_id} 有权修改项目 ${task.prj_id} 的任务 ${task_id}。`);
         return true;
     }
@@ -168,6 +170,20 @@ export async function is_project_task_progress_modifiable(account_id: string, ta
         return true;
     }
 
+    Logger.trace(`${account_id} 没有权限修改任务 ${task_id}的进度。`);
+    return false;
+}
+
+/// 是否有修改项目任务进度的权限
+export async function is_project_task_outcome_modifiable(account_id: string, task_id: string, draft?: boolean): Promise<boolean> {
+    if (draft === undefined) draft = false;
+    let model = draft ? PrjPlanTaskDraft : PrjPlanTask;
+    let task = await model.findOne({where: {id: task_id}, raw: true});
+    if (task && (task.handler_id === account_id || await is_project_modifiable(account_id, task.prj_id))) {
+        Logger.trace(`${account_id} 有权修改项目 ${task.prj_id} 的任务 ${task_id}的进度。`);
+        return true;
+    }
+
     Logger.trace(`${account_id} 没有权限修改任务 ${task_id}的进度。`);
     return false;
 }