Browse Source

Merge remote-tracking branch 'origin/master'

YunaiV 3 years ago
parent
commit
ae9e9b3de0
62 changed files with 3804 additions and 655 deletions
  1. 38 35
      sql/quartz.sql
  2. 3 605
      sql/ruoyi-vue-pro.sql
  3. 7 0
      yudao-admin-server/pom.xml
  4. 114 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/controller/oa/OaLeaveController.java
  5. 48 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/controller/oa/vo/OaLeaveBaseVO.java
  6. 15 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/controller/oa/vo/OaLeaveCreateReqVO.java
  7. 44 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/controller/oa/vo/OaLeaveExcelVO.java
  8. 54 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/controller/oa/vo/OaLeaveExportReqVO.java
  9. 56 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/controller/oa/vo/OaLeavePageReqVO.java
  10. 16 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/controller/oa/vo/OaLeaveRespVO.java
  11. 32 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/controller/oa/vo/OaLeaveUpdateReqVO.java
  12. 34 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/controller/workflow/ProcessDefinitionController.java
  13. 60 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/controller/workflow/TaskController.java
  14. 19 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/controller/workflow/vo/TaskHandleVO.java
  15. 15 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/controller/workflow/vo/TaskQueryReqVO.java
  16. 17 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/controller/workflow/vo/TaskReqVO.java
  17. 24 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/controller/workflow/vo/TaskStepVO.java
  18. 16 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/controller/workflow/vo/TodoTaskPageReqVO.java
  19. 33 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/controller/workflow/vo/TodoTaskRespVO.java
  20. 34 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/convert/oa/OaLeaveConvert.java
  21. 9 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/convert/workflow/TodoTaskConvert.java
  22. 60 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/dal/dataobject/oa/OaLeaveDO.java
  23. 29 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/dal/dataobject/process/ProcessDefinitionDO.java
  24. 46 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/dal/mysql/oa/OaLeaveMapper.java
  25. 13 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/enums/OaErrorCodeConstants.java
  26. 2 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/package-info.java
  27. 62 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/service/config/UserGroupManagerService.java
  28. 31 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/service/config/UserGroupsProvider.java
  29. 76 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/service/oa/OaLeaveService.java
  30. 43 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/service/oa/ReportBackEndProcessor.java
  31. 141 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/service/oa/impl/OaLeaveServiceImpl.java
  32. 29 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/service/process/ProcessService.java
  33. 109 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/service/process/impl/ProcessServiceImpl.java
  34. 26 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/service/workflow/TaskService.java
  35. 266 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/service/workflow/impl/TaskServiceImpl.java
  36. 24 3
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/service/auth/impl/SysAuthServiceImpl.java
  37. 16 2
      yudao-admin-server/src/main/resources/application-local.yaml
  38. 16 3
      yudao-admin-server/src/main/resources/application.yaml
  39. 52 0
      yudao-admin-server/src/main/resources/mybatis-config/mybatis-config.xml
  40. 152 0
      yudao-admin-server/src/main/resources/processes/leave-formkey.bpmn
  41. 9 0
      yudao-admin-ui/src/api/oa/flow.js
  42. 62 0
      yudao-admin-ui/src/api/oa/leave.js
  43. 83 0
      yudao-admin-ui/src/api/oa/todo.js
  44. 70 0
      yudao-admin-ui/src/router/index.js
  45. 3 0
      yudao-admin-ui/src/utils/dict.js
  46. 36 0
      yudao-admin-ui/src/views/oa/flow/index.vue
  47. 93 0
      yudao-admin-ui/src/views/oa/leave/apply/index.vue
  48. 190 0
      yudao-admin-ui/src/views/oa/leave/approve-hr/index.vue
  49. 190 0
      yudao-admin-ui/src/views/oa/leave/approve-leader/index.vue
  50. 137 0
      yudao-admin-ui/src/views/oa/leave/confirm/index.vue
  51. 377 0
      yudao-admin-ui/src/views/oa/leave/index.vue
  52. 211 0
      yudao-admin-ui/src/views/oa/leave/modify/index.vue
  53. 303 0
      yudao-admin-ui/src/views/oa/todo/index.vue
  54. 9 0
      yudao-dependencies/pom.xml
  55. 1 0
      yudao-framework/pom.xml
  56. 71 0
      yudao-framework/yudao-spring-boot-starter-activiti/pom.xml
  57. 32 0
      yudao-framework/yudao-spring-boot-starter-activiti/src/main/java/cn/iocoder/yudao/framework/activiti/config/YudaoActivitiConfiguration.java
  58. 1 0
      yudao-framework/yudao-spring-boot-starter-activiti/src/main/java/cn/iocoder/yudao/framework/activiti/package-info.java
  59. 13 0
      yudao-framework/yudao-spring-boot-starter-security/pom.xml
  60. 17 6
      yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/LoginUser.java
  61. 4 1
      yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java
  62. 11 0
      更新日志.md

+ 38 - 35
sql/quartz.sql

@@ -1,17 +1,17 @@
 /*
  Navicat Premium Data Transfer
 
- Source Server         : local-mysql001
+ Source Server         : 127.0.0.1
  Source Server Type    : MySQL
- Source Server Version : 50718
+ Source Server Version : 80026
  Source Host           : localhost:3306
  Source Schema         : ruoyi-vue-pro
 
  Target Server Type    : MySQL
- Target Server Version : 50718
+ Target Server Version : 80026
  File Encoding         : 65001
 
- Date: 03/05/2021 12:01:37
+ Date: 30/10/2021 13:46:03
 */
 
 SET NAMES utf8mb4;
@@ -29,7 +29,7 @@ CREATE TABLE `QRTZ_BLOB_TRIGGERS` (
   PRIMARY KEY (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`),
   KEY `SCHED_NAME` (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`),
   CONSTRAINT `qrtz_blob_triggers_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `QRTZ_TRIGGERS` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
 
 -- ----------------------------
 -- Records of QRTZ_BLOB_TRIGGERS
@@ -46,7 +46,7 @@ CREATE TABLE `QRTZ_CALENDARS` (
   `CALENDAR_NAME` varchar(190) NOT NULL,
   `CALENDAR` blob NOT NULL,
   PRIMARY KEY (`SCHED_NAME`,`CALENDAR_NAME`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
 
 -- ----------------------------
 -- Records of QRTZ_CALENDARS
@@ -66,12 +66,13 @@ CREATE TABLE `QRTZ_CRON_TRIGGERS` (
   `TIME_ZONE_ID` varchar(80) DEFAULT NULL,
   PRIMARY KEY (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`),
   CONSTRAINT `qrtz_cron_triggers_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `QRTZ_TRIGGERS` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
 
 -- ----------------------------
 -- Records of QRTZ_CRON_TRIGGERS
 -- ----------------------------
 BEGIN;
+INSERT INTO `QRTZ_CRON_TRIGGERS` VALUES ('schedulerName', 'payNotifyJob', 'DEFAULT', '* * * * * ?', 'Asia/Shanghai');
 INSERT INTO `QRTZ_CRON_TRIGGERS` VALUES ('schedulerName', 'sysUserSessionTimeoutJob', 'DEFAULT', '0 * * * * ? *', 'Asia/Shanghai');
 COMMIT;
 
@@ -85,9 +86,9 @@ CREATE TABLE `QRTZ_FIRED_TRIGGERS` (
   `TRIGGER_NAME` varchar(190) NOT NULL,
   `TRIGGER_GROUP` varchar(190) NOT NULL,
   `INSTANCE_NAME` varchar(190) NOT NULL,
-  `FIRED_TIME` bigint(13) NOT NULL,
-  `SCHED_TIME` bigint(13) NOT NULL,
-  `PRIORITY` int(11) NOT NULL,
+  `FIRED_TIME` bigint NOT NULL,
+  `SCHED_TIME` bigint NOT NULL,
+  `PRIORITY` int NOT NULL,
   `STATE` varchar(16) NOT NULL,
   `JOB_NAME` varchar(190) DEFAULT NULL,
   `JOB_GROUP` varchar(190) DEFAULT NULL,
@@ -100,7 +101,7 @@ CREATE TABLE `QRTZ_FIRED_TRIGGERS` (
   KEY `IDX_QRTZ_FT_JG` (`SCHED_NAME`,`JOB_GROUP`),
   KEY `IDX_QRTZ_FT_T_G` (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`),
   KEY `IDX_QRTZ_FT_TG` (`SCHED_NAME`,`TRIGGER_GROUP`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
 
 -- ----------------------------
 -- Records of QRTZ_FIRED_TRIGGERS
@@ -126,12 +127,13 @@ CREATE TABLE `QRTZ_JOB_DETAILS` (
   PRIMARY KEY (`SCHED_NAME`,`JOB_NAME`,`JOB_GROUP`),
   KEY `IDX_QRTZ_J_REQ_RECOVERY` (`SCHED_NAME`,`REQUESTS_RECOVERY`),
   KEY `IDX_QRTZ_J_GRP` (`SCHED_NAME`,`JOB_GROUP`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
 
 -- ----------------------------
 -- Records of QRTZ_JOB_DETAILS
 -- ----------------------------
 BEGIN;
+INSERT INTO `QRTZ_JOB_DETAILS` VALUES ('schedulerName', 'payNotifyJob', 'DEFAULT', NULL, 'cn.iocoder.yudao.framework.quartz.core.handler.JobHandlerInvoker', '0', '1', '1', '0', 0xACED0005737200156F72672E71756172747A2E4A6F62446174614D61709FB083E8BFA9B0CB020000787200266F72672E71756172747A2E7574696C732E537472696E674B65794469727479466C61674D61708208E8C3FBC55D280200015A0013616C6C6F77735472616E7369656E74446174617872001D6F72672E71756172747A2E7574696C732E4469727479466C61674D617013E62EAD28760ACE0200025A000564697274794C00036D617074000F4C6A6176612F7574696C2F4D61703B787001737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F4000000000000C770800000010000000027400064A4F425F49447372000E6A6176612E6C616E672E4C6F6E673B8BE490CC8F23DF0200014A000576616C7565787200106A6176612E6C616E672E4E756D62657286AC951D0B94E08B020000787000000000000000057400104A4F425F48414E444C45525F4E414D4574000C7061794E6F746966794A6F627800);
 INSERT INTO `QRTZ_JOB_DETAILS` VALUES ('schedulerName', 'sysUserSessionTimeoutJob', 'DEFAULT', NULL, 'cn.iocoder.yudao.framework.quartz.core.handler.JobHandlerInvoker', '0', '1', '1', '0', 0xACED0005737200156F72672E71756172747A2E4A6F62446174614D61709FB083E8BFA9B0CB020000787200266F72672E71756172747A2E7574696C732E537472696E674B65794469727479466C61674D61708208E8C3FBC55D280200015A0013616C6C6F77735472616E7369656E74446174617872001D6F72672E71756172747A2E7574696C732E4469727479466C61674D617013E62EAD28760ACE0200025A000564697274794C00036D617074000F4C6A6176612F7574696C2F4D61703B787001737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F4000000000000C770800000010000000027400064A4F425F49447372000E6A6176612E6C616E672E4C6F6E673B8BE490CC8F23DF0200014A000576616C7565787200106A6176612E6C616E672E4E756D62657286AC951D0B94E08B020000787000000000000000037400104A4F425F48414E444C45525F4E414D457400187379735573657253657373696F6E54696D656F75744A6F627800);
 COMMIT;
 
@@ -143,7 +145,7 @@ CREATE TABLE `QRTZ_LOCKS` (
   `SCHED_NAME` varchar(120) NOT NULL,
   `LOCK_NAME` varchar(40) NOT NULL,
   PRIMARY KEY (`SCHED_NAME`,`LOCK_NAME`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
 
 -- ----------------------------
 -- Records of QRTZ_LOCKS
@@ -161,7 +163,7 @@ CREATE TABLE `QRTZ_PAUSED_TRIGGER_GRPS` (
   `SCHED_NAME` varchar(120) NOT NULL,
   `TRIGGER_GROUP` varchar(190) NOT NULL,
   PRIMARY KEY (`SCHED_NAME`,`TRIGGER_GROUP`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
 
 -- ----------------------------
 -- Records of QRTZ_PAUSED_TRIGGER_GRPS
@@ -176,16 +178,16 @@ DROP TABLE IF EXISTS `QRTZ_SCHEDULER_STATE`;
 CREATE TABLE `QRTZ_SCHEDULER_STATE` (
   `SCHED_NAME` varchar(120) NOT NULL,
   `INSTANCE_NAME` varchar(190) NOT NULL,
-  `LAST_CHECKIN_TIME` bigint(13) NOT NULL,
-  `CHECKIN_INTERVAL` bigint(13) NOT NULL,
+  `LAST_CHECKIN_TIME` bigint NOT NULL,
+  `CHECKIN_INTERVAL` bigint NOT NULL,
   PRIMARY KEY (`SCHED_NAME`,`INSTANCE_NAME`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
 
 -- ----------------------------
 -- Records of QRTZ_SCHEDULER_STATE
 -- ----------------------------
 BEGIN;
-INSERT INTO `QRTZ_SCHEDULER_STATE` VALUES ('schedulerName', 'Yunai1620010117445', 1620010210071, 15000);
+INSERT INTO `QRTZ_SCHEDULER_STATE` VALUES ('schedulerName', 'Yunai.local1635571630493', 1635572537879, 15000);
 COMMIT;
 
 -- ----------------------------
@@ -196,12 +198,12 @@ CREATE TABLE `QRTZ_SIMPLE_TRIGGERS` (
   `SCHED_NAME` varchar(120) NOT NULL,
   `TRIGGER_NAME` varchar(190) NOT NULL,
   `TRIGGER_GROUP` varchar(190) NOT NULL,
-  `REPEAT_COUNT` bigint(7) NOT NULL,
-  `REPEAT_INTERVAL` bigint(12) NOT NULL,
-  `TIMES_TRIGGERED` bigint(10) NOT NULL,
+  `REPEAT_COUNT` bigint NOT NULL,
+  `REPEAT_INTERVAL` bigint NOT NULL,
+  `TIMES_TRIGGERED` bigint NOT NULL,
   PRIMARY KEY (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`),
   CONSTRAINT `qrtz_simple_triggers_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `QRTZ_TRIGGERS` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
 
 -- ----------------------------
 -- Records of QRTZ_SIMPLE_TRIGGERS
@@ -220,17 +222,17 @@ CREATE TABLE `QRTZ_SIMPROP_TRIGGERS` (
   `STR_PROP_1` varchar(512) DEFAULT NULL,
   `STR_PROP_2` varchar(512) DEFAULT NULL,
   `STR_PROP_3` varchar(512) DEFAULT NULL,
-  `INT_PROP_1` int(11) DEFAULT NULL,
-  `INT_PROP_2` int(11) DEFAULT NULL,
-  `LONG_PROP_1` bigint(20) DEFAULT NULL,
-  `LONG_PROP_2` bigint(20) DEFAULT NULL,
+  `INT_PROP_1` int DEFAULT NULL,
+  `INT_PROP_2` int DEFAULT NULL,
+  `LONG_PROP_1` bigint DEFAULT NULL,
+  `LONG_PROP_2` bigint DEFAULT NULL,
   `DEC_PROP_1` decimal(13,4) DEFAULT NULL,
   `DEC_PROP_2` decimal(13,4) DEFAULT NULL,
   `BOOL_PROP_1` varchar(1) DEFAULT NULL,
   `BOOL_PROP_2` varchar(1) DEFAULT NULL,
   PRIMARY KEY (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`),
   CONSTRAINT `qrtz_simprop_triggers_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `QRTZ_TRIGGERS` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
 
 -- ----------------------------
 -- Records of QRTZ_SIMPROP_TRIGGERS
@@ -249,15 +251,15 @@ CREATE TABLE `QRTZ_TRIGGERS` (
   `JOB_NAME` varchar(190) NOT NULL,
   `JOB_GROUP` varchar(190) NOT NULL,
   `DESCRIPTION` varchar(250) DEFAULT NULL,
-  `NEXT_FIRE_TIME` bigint(13) DEFAULT NULL,
-  `PREV_FIRE_TIME` bigint(13) DEFAULT NULL,
-  `PRIORITY` int(11) DEFAULT NULL,
+  `NEXT_FIRE_TIME` bigint DEFAULT NULL,
+  `PREV_FIRE_TIME` bigint DEFAULT NULL,
+  `PRIORITY` int DEFAULT NULL,
   `TRIGGER_STATE` varchar(16) NOT NULL,
   `TRIGGER_TYPE` varchar(8) NOT NULL,
-  `START_TIME` bigint(13) NOT NULL,
-  `END_TIME` bigint(13) DEFAULT NULL,
+  `START_TIME` bigint NOT NULL,
+  `END_TIME` bigint DEFAULT NULL,
   `CALENDAR_NAME` varchar(190) DEFAULT NULL,
-  `MISFIRE_INSTR` smallint(2) DEFAULT NULL,
+  `MISFIRE_INSTR` smallint DEFAULT NULL,
   `JOB_DATA` blob,
   PRIMARY KEY (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`),
   KEY `IDX_QRTZ_T_J` (`SCHED_NAME`,`JOB_NAME`,`JOB_GROUP`),
@@ -273,13 +275,14 @@ CREATE TABLE `QRTZ_TRIGGERS` (
   KEY `IDX_QRTZ_T_NFT_ST_MISFIRE` (`SCHED_NAME`,`MISFIRE_INSTR`,`NEXT_FIRE_TIME`,`TRIGGER_STATE`),
   KEY `IDX_QRTZ_T_NFT_ST_MISFIRE_GRP` (`SCHED_NAME`,`MISFIRE_INSTR`,`NEXT_FIRE_TIME`,`TRIGGER_GROUP`,`TRIGGER_STATE`),
   CONSTRAINT `qrtz_triggers_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `JOB_NAME`, `JOB_GROUP`) REFERENCES `QRTZ_JOB_DETAILS` (`SCHED_NAME`, `JOB_NAME`, `JOB_GROUP`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
 
 -- ----------------------------
 -- Records of QRTZ_TRIGGERS
 -- ----------------------------
 BEGIN;
-INSERT INTO `QRTZ_TRIGGERS` VALUES ('schedulerName', 'sysUserSessionTimeoutJob', 'DEFAULT', 'sysUserSessionTimeoutJob', 'DEFAULT', NULL, 1620010260000, 1620010200000, 5, 'WAITING', 'CRON', 1613649236000, 0, NULL, 0, 0xACED0005737200156F72672E71756172747A2E4A6F62446174614D61709FB083E8BFA9B0CB020000787200266F72672E71756172747A2E7574696C732E537472696E674B65794469727479466C61674D61708208E8C3FBC55D280200015A0013616C6C6F77735472616E7369656E74446174617872001D6F72672E71756172747A2E7574696C732E4469727479466C61674D617013E62EAD28760ACE0200025A000564697274794C00036D617074000F4C6A6176612F7574696C2F4D61703B787001737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F4000000000000C770800000010000000037400114A4F425F48414E444C45525F504152414D707400124A4F425F52455452595F494E54455256414C737200116A6176612E6C616E672E496E746567657212E2A0A4F781873802000149000576616C7565787200106A6176612E6C616E672E4E756D62657286AC951D0B94E08B0200007870000007D074000F4A4F425F52455452595F434F554E547371007E0009000000037800);
+INSERT INTO `QRTZ_TRIGGERS` VALUES ('schedulerName', 'payNotifyJob', 'DEFAULT', 'payNotifyJob', 'DEFAULT', NULL, 1635572540000, 1635572539000, 5, 'WAITING', 'CRON', 1635294882000, 0, NULL, 0, 0xACED0005737200156F72672E71756172747A2E4A6F62446174614D61709FB083E8BFA9B0CB020000787200266F72672E71756172747A2E7574696C732E537472696E674B65794469727479466C61674D61708208E8C3FBC55D280200015A0013616C6C6F77735472616E7369656E74446174617872001D6F72672E71756172747A2E7574696C732E4469727479466C61674D617013E62EAD28760ACE0200025A000564697274794C00036D617074000F4C6A6176612F7574696C2F4D61703B787001737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F4000000000000C770800000010000000037400114A4F425F48414E444C45525F504152414D707400124A4F425F52455452595F494E54455256414C737200116A6176612E6C616E672E496E746567657212E2A0A4F781873802000149000576616C7565787200106A6176612E6C616E672E4E756D62657286AC951D0B94E08B02000078700000000074000F4A4F425F52455452595F434F554E5471007E000B7800);
+INSERT INTO `QRTZ_TRIGGERS` VALUES ('schedulerName', 'sysUserSessionTimeoutJob', 'DEFAULT', 'sysUserSessionTimeoutJob', 'DEFAULT', NULL, 1635572580000, 1635572520000, 5, 'WAITING', 'CRON', 1613649236000, 0, NULL, 0, 0xACED0005737200156F72672E71756172747A2E4A6F62446174614D61709FB083E8BFA9B0CB020000787200266F72672E71756172747A2E7574696C732E537472696E674B65794469727479466C61674D61708208E8C3FBC55D280200015A0013616C6C6F77735472616E7369656E74446174617872001D6F72672E71756172747A2E7574696C732E4469727479466C61674D617013E62EAD28760ACE0200025A000564697274794C00036D617074000F4C6A6176612F7574696C2F4D61703B787001737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F4000000000000C770800000010000000037400114A4F425F48414E444C45525F504152414D707400124A4F425F52455452595F494E54455256414C737200116A6176612E6C616E672E496E746567657212E2A0A4F781873802000149000576616C7565787200106A6176612E6C616E672E4E756D62657286AC951D0B94E08B0200007870000007D074000F4A4F425F52455452595F434F554E547371007E0009000000037800);
 COMMIT;
 
 SET FOREIGN_KEY_CHECKS = 1;

File diff suppressed because it is too large
+ 3 - 605
sql/ruoyi-vue-pro.sql


+ 7 - 0
yudao-admin-server/pom.xml

@@ -36,6 +36,11 @@
             <artifactId>yudao-spring-boot-starter-biz-sms</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-activiti</artifactId>
+        </dependency>
+
         <!-- Web 相关 -->
         <dependency>
             <groupId>cn.iocoder.boot</groupId>
@@ -112,6 +117,8 @@
             <artifactId>yudao-spring-boot-starter-excel</artifactId>
         </dependency>
 
+
+
         <dependency>
             <groupId>org.apache.velocity</groupId>
             <artifactId>velocity-engine-core</artifactId>

+ 114 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/controller/oa/OaLeaveController.java

@@ -0,0 +1,114 @@
+package cn.iocoder.yudao.adminserver.modules.activiti.controller.oa;
+
+import cn.iocoder.yudao.adminserver.modules.activiti.controller.oa.vo.*;
+import cn.iocoder.yudao.adminserver.modules.activiti.convert.oa.OaLeaveConvert;
+import cn.iocoder.yudao.adminserver.modules.activiti.dal.dataobject.oa.OaLeaveDO;
+import cn.iocoder.yudao.adminserver.modules.activiti.service.oa.OaLeaveService;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
+import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiImplicitParam;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletResponse;
+import javax.validation.Valid;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
+
+// TODO @jason:Oa=》OA 会不会好点,名词缩写哈
+@Api(tags = "请假申请")
+@RestController
+@RequestMapping("/oa/leave")
+@Validated
+public class OaLeaveController {
+
+    @Resource
+    private OaLeaveService leaveService;
+
+    @PostMapping("/create")
+    @ApiOperation("创建请假申请")
+    @PreAuthorize("@ss.hasPermission('oa:leave:create')")
+    public CommonResult<Long> createLeave(@Valid @RequestBody OaLeaveCreateReqVO createReqVO) {
+        // TODO @芋艿:processKey 自己去理解下。不过得把 leave 变成枚举
+        createReqVO.setProcessKey("leave");
+        return success(leaveService.createLeave(createReqVO));
+    }
+
+    @PostMapping("/form-key/create")
+    @ApiOperation("创建外置请假申请")
+    public CommonResult<Long> createFormKeyLeave(@Valid @RequestBody OaLeaveCreateReqVO createReqVO) {
+        // TODO @芋艿:processKey 自己去理解下。不过得把 formkey 变成枚举
+        createReqVO.setProcessKey("leave-formkey");
+        return success(leaveService.createLeave(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @ApiOperation("更新请假申请")
+    @PreAuthorize("@ss.hasPermission('oa:leave:update')")
+    public CommonResult<Boolean> updateLeave(@Valid @RequestBody OaLeaveUpdateReqVO updateReqVO) {
+        leaveService.updateLeave(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @ApiOperation("删除请假申请")
+    @ApiImplicitParam(name = "id", value = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('oa:leave:delete')")
+    public CommonResult<Boolean> deleteLeave(@RequestParam("id") Long id) {
+        leaveService.deleteLeave(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @ApiOperation("获得请假申请")
+    @ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class)
+    @PreAuthorize("@ss.hasPermission('oa:leave:query')")
+    public CommonResult<OaLeaveRespVO> getLeave(@RequestParam("id") Long id) {
+        OaLeaveDO leave = leaveService.getLeave(id);
+        return success(OaLeaveConvert.INSTANCE.convert(leave));
+    }
+
+    @GetMapping("/list")
+    @ApiOperation("获得请假申请列表")
+    @ApiImplicitParam(name = "ids", value = "编号列表", required = true, example = "1024,2048", dataTypeClass = List.class)
+    @PreAuthorize("@ss.hasPermission('oa:leave:query')")
+    public CommonResult<List<OaLeaveRespVO>> getLeaveList(@RequestParam("ids") Collection<Long> ids) {
+        List<OaLeaveDO> list = leaveService.getLeaveList(ids);
+        return success(OaLeaveConvert.INSTANCE.convertList(list));
+    }
+
+    @GetMapping("/page")
+    @ApiOperation("获得请假申请分页")
+    @PreAuthorize("@ss.hasPermission('oa:leave:query')")
+    public CommonResult<PageResult<OaLeaveRespVO>> getLeavePage(@Valid OaLeavePageReqVO pageVO) {
+        //值查询自己申请请假
+        // TODO @芋艿:这里的传值,到底前端搞,还是后端搞。
+        pageVO.setUserId(SecurityFrameworkUtils.getLoginUser().getUsername());
+        PageResult<OaLeaveDO> pageResult = leaveService.getLeavePage(pageVO);
+        return success(OaLeaveConvert.INSTANCE.convertPage(pageResult));
+    }
+
+    @GetMapping("/export-excel")
+    @ApiOperation("导出请假申请 Excel")
+    @PreAuthorize("@ss.hasPermission('oa:leave:export')")
+    @OperateLog(type = EXPORT)
+    public void exportLeaveExcel(@Valid OaLeaveExportReqVO exportReqVO,
+              HttpServletResponse response) throws IOException {
+        List<OaLeaveDO> list = leaveService.getLeaveList(exportReqVO);
+        // 导出 Excel
+        List<OaLeaveExcelVO> datas = OaLeaveConvert.INSTANCE.convertList02(list);
+        ExcelUtils.write(response, "请假申请.xls", "数据", OaLeaveExcelVO.class, datas);
+    }
+
+}

+ 48 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/controller/oa/vo/OaLeaveBaseVO.java

@@ -0,0 +1,48 @@
+package cn.iocoder.yudao.adminserver.modules.activiti.controller.oa.vo;
+
+import lombok.*;
+import java.util.*;
+import io.swagger.annotations.*;
+import javax.validation.constraints.*;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+/**
+* 请假申请 Base VO,提供给添加、修改、详细的子 VO 使用
+* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
+*/
+@Data
+public class OaLeaveBaseVO {
+
+    @ApiModelProperty(value = "流程id")
+    private String processInstanceId;
+
+    @ApiModelProperty(value = "状态", required = true)
+    private Integer status;
+
+    @ApiModelProperty(value = "申请人id", required = true)
+    private String userId;
+
+    @ApiModelProperty(value = "开始时间", required = true)
+    @NotNull(message = "开始时间不能为空")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private Date startTime;
+
+    @ApiModelProperty(value = "结束时间", required = true)
+    @NotNull(message = "结束时间不能为空")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private Date endTime;
+
+    @ApiModelProperty(value = "请假类型")
+    private String leaveType;
+
+    @ApiModelProperty(value = "原因")
+    private String reason;
+
+    @ApiModelProperty(value = "申请时间", required = true)
+    @NotNull(message = "申请时间不能为空")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private Date applyTime;
+
+}

+ 15 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/controller/oa/vo/OaLeaveCreateReqVO.java

@@ -0,0 +1,15 @@
+package cn.iocoder.yudao.adminserver.modules.activiti.controller.oa.vo;
+
+import lombok.*;
+import java.util.*;
+import io.swagger.annotations.*;
+import javax.validation.constraints.*;
+
+@ApiModel("请假申请创建 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class OaLeaveCreateReqVO extends OaLeaveBaseVO {
+
+    private String processKey;
+}

+ 44 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/controller/oa/vo/OaLeaveExcelVO.java

@@ -0,0 +1,44 @@
+package cn.iocoder.yudao.adminserver.modules.activiti.controller.oa.vo;
+
+import lombok.*;
+import java.util.*;
+import io.swagger.annotations.*;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+
+/**
+ * 请假申请 Excel VO
+ *
+ * @author 芋艿
+ */
+@Data
+public class OaLeaveExcelVO {
+
+    @ExcelProperty("请假表单主键")
+    private Long id;
+
+    @ExcelProperty("流程id")
+    private String processInstanceId;
+
+    @ExcelProperty("状态")
+    private Integer status;
+
+    @ExcelProperty("申请人id")
+    private String userId;
+
+    @ExcelProperty("开始时间")
+    private Date startTime;
+
+    @ExcelProperty("结束时间")
+    private Date endTime;
+
+    @ExcelProperty("请假类型")
+    private String leaveType;
+
+    @ExcelProperty("原因")
+    private String reason;
+
+    @ExcelProperty("申请时间")
+    private Date applyTime;
+
+}

+ 54 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/controller/oa/vo/OaLeaveExportReqVO.java

@@ -0,0 +1,54 @@
+package cn.iocoder.yudao.adminserver.modules.activiti.controller.oa.vo;
+
+import lombok.*;
+import java.util.*;
+import io.swagger.annotations.*;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@ApiModel(value = "请假申请 Excel 导出 Request VO", description = "参数和 OaLeavePageReqVO 是一致的")
+@Data
+public class OaLeaveExportReqVO {
+
+    @ApiModelProperty(value = "流程id")
+    private String processInstanceId;
+
+    @ApiModelProperty(value = "状态")
+    private Integer status;
+
+    @ApiModelProperty(value = "申请人id")
+    private String userId;
+
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    @ApiModelProperty(value = "开始开始时间")
+    private Date beginStartTime;
+
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    @ApiModelProperty(value = "结束开始时间")
+    private Date endStartTime;
+
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    @ApiModelProperty(value = "开始结束时间")
+    private Date beginEndTime;
+
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    @ApiModelProperty(value = "结束结束时间")
+    private Date endEndTime;
+
+    @ApiModelProperty(value = "请假类型")
+    private String leaveType;
+
+    @ApiModelProperty(value = "原因")
+    private String reason;
+
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    @ApiModelProperty(value = "开始申请时间")
+    private Date beginApplyTime;
+
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    @ApiModelProperty(value = "结束申请时间")
+    private Date endApplyTime;
+
+}

+ 56 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/controller/oa/vo/OaLeavePageReqVO.java

@@ -0,0 +1,56 @@
+package cn.iocoder.yudao.adminserver.modules.activiti.controller.oa.vo;
+
+import lombok.*;
+import java.util.*;
+import io.swagger.annotations.*;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@ApiModel("请假申请分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class OaLeavePageReqVO extends PageParam {
+
+    @ApiModelProperty(value = "流程id")
+    private String processInstanceId;
+
+    @ApiModelProperty(value = "状态")
+    private Integer status;
+
+    @ApiModelProperty(value = "申请人id")
+    private String userId;
+
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    @ApiModelProperty(value = "开始开始时间")
+    private Date beginStartTime;
+
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    @ApiModelProperty(value = "结束开始时间")
+    private Date endStartTime;
+
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    @ApiModelProperty(value = "开始结束时间")
+    private Date beginEndTime;
+
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    @ApiModelProperty(value = "结束结束时间")
+    private Date endEndTime;
+
+    @ApiModelProperty(value = "请假类型")
+    private String leaveType;
+
+    @ApiModelProperty(value = "原因")
+    private String reason;
+
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    @ApiModelProperty(value = "开始申请时间")
+    private Date beginApplyTime;
+
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    @ApiModelProperty(value = "结束申请时间")
+    private Date endApplyTime;
+
+}

+ 16 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/controller/oa/vo/OaLeaveRespVO.java

@@ -0,0 +1,16 @@
+package cn.iocoder.yudao.adminserver.modules.activiti.controller.oa.vo;
+
+import lombok.*;
+import java.util.*;
+import io.swagger.annotations.*;
+
+@ApiModel("请假申请 Response VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class OaLeaveRespVO extends OaLeaveBaseVO {
+
+    @ApiModelProperty(value = "请假表单主键", required = true)
+    private Long id;
+
+}

+ 32 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/controller/oa/vo/OaLeaveUpdateReqVO.java

@@ -0,0 +1,32 @@
+package cn.iocoder.yudao.adminserver.modules.activiti.controller.oa.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import javax.validation.constraints.NotNull;
+import java.util.Map;
+
+@ApiModel("请假申请更新 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class OaLeaveUpdateReqVO extends OaLeaveBaseVO {
+
+    @ApiModelProperty(value = "请假表单主键", required = true)
+    @NotNull(message = "请假表单主键不能为空")
+    private Long id;
+
+    // TODO @json:swagger 和 validator 的注解要加哈。
+
+    private String taskId;
+
+    private String comment;
+
+    private Map<String,Object> variables;
+
+    // TODO @芋艿:variables 的作用是啥。
+
+}

+ 34 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/controller/workflow/ProcessDefinitionController.java

@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.adminserver.modules.activiti.controller.workflow;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import org.activiti.api.process.runtime.ProcessRuntime;
+import org.activiti.engine.RepositoryService;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+
+// TODO @json:swagger 和 validation 的注解,后续要补全下哈。可以等 workflow 基本写的差不多之后
+@RestController
+@RequestMapping("/workflow/process/definition")
+public class ProcessDefinitionController {
+
+    @Resource
+    private RepositoryService repositoryService;
+
+    @Resource
+    private ProcessRuntime processRuntime;
+
+
+    @GetMapping(value = "/getStartForm")
+    public CommonResult<String> getStartForm(@RequestParam("processKey") String processKey){
+        //这样查似乎有问题??, 暂时写死
+//        final ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().
+//                processDefinitionKey(processKey).latestVersion().singleResult();
+//        processRuntime.processDefinition(processDefinition.getId()).getFormKey();
+        return CommonResult.success("/flow/leave/apply");
+    }
+
+}

+ 60 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/controller/workflow/TaskController.java

@@ -0,0 +1,60 @@
+package cn.iocoder.yudao.adminserver.modules.activiti.controller.workflow;
+
+import cn.iocoder.yudao.adminserver.modules.activiti.controller.workflow.vo.*;
+import cn.iocoder.yudao.adminserver.modules.activiti.service.workflow.TaskService;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.validation.Valid;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+// TODO @json:swagger 和 validation 的注解,后续要补全下哈。可以等 workflow 基本写的差不多之后
+@Api(tags = "工作流待办任务")
+@RestController
+@RequestMapping("/workflow/task")
+public class TaskController {
+
+    @Resource
+    private TaskService taskService;
+
+    @GetMapping("/todo/page")
+    @ApiOperation("获取待办任务分页")
+    public CommonResult<PageResult<TodoTaskRespVO>> getTodoTaskPage(@Valid TodoTaskPageReqVO pageVO) {
+        return success(taskService.getTodoTaskPage(pageVO));
+    }
+
+    @GetMapping("/claim")
+    @ApiOperation("签收任务")
+    public CommonResult<Boolean> claimTask(@RequestParam("id") String taskId) {
+        taskService.claimTask(taskId);
+        return success(true);
+    }
+
+    @PostMapping("/task-steps")
+    public CommonResult<TaskHandleVO> getTaskSteps(@RequestBody TaskQueryReqVO taskQuery) {
+        return success(taskService.getTaskSteps(taskQuery));
+    }
+
+    @PostMapping("/formKey")
+    public CommonResult<TodoTaskRespVO> getTaskFormKey(@RequestBody TaskQueryReqVO taskQuery) {
+        return success(taskService.getTaskFormKey(taskQuery));
+    }
+
+    @PostMapping("/complete")
+    public CommonResult<Boolean> complete(@RequestBody TaskReqVO taskReq) {
+        taskService.completeTask(taskReq);
+        return success(true);
+    }
+
+    @GetMapping("/process/history-steps")
+    public CommonResult<List<TaskStepVO>> getHistorySteps(@RequestParam("id") String processInstanceId) {
+        return success(taskService.getHistorySteps(processInstanceId));
+    }
+
+}

+ 19 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/controller/workflow/vo/TaskHandleVO.java

@@ -0,0 +1,19 @@
+package cn.iocoder.yudao.adminserver.modules.activiti.controller.workflow.vo;
+
+import lombok.Data;
+import lombok.ToString;
+
+import java.util.List;
+
+@Data
+@ToString
+public class TaskHandleVO {
+
+    private Object formObject;
+
+
+    private List<TaskStepVO> historyTask;
+
+
+    private String taskVariable;
+}

+ 15 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/controller/workflow/vo/TaskQueryReqVO.java

@@ -0,0 +1,15 @@
+package cn.iocoder.yudao.adminserver.modules.activiti.controller.workflow.vo;
+
+import lombok.Data;
+import lombok.ToString;
+
+@Data
+@ToString
+public class TaskQueryReqVO {
+
+    private String processKey;
+
+    private String taskId;
+
+    private String businessKey;
+}

+ 17 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/controller/workflow/vo/TaskReqVO.java

@@ -0,0 +1,17 @@
+package cn.iocoder.yudao.adminserver.modules.activiti.controller.workflow.vo;
+
+import lombok.Data;
+import lombok.ToString;
+
+import java.util.Map;
+
+@Data
+@ToString
+public class TaskReqVO {
+
+    private String taskId;
+
+    private Map<String,Object> variables;
+
+    private String comment;
+}

+ 24 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/controller/workflow/vo/TaskStepVO.java

@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.adminserver.modules.activiti.controller.workflow.vo;
+
+import lombok.Data;
+import lombok.ToString;
+
+import java.util.Date;
+
+@Data
+@ToString
+public class TaskStepVO {
+
+    private String stepName;
+
+    private Date startTime;
+
+    private Date endTime;
+
+    private String assignee;
+
+    private String comment;
+
+    private Integer status;
+
+}

+ 16 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/controller/workflow/vo/TodoTaskPageReqVO.java

@@ -0,0 +1,16 @@
+package cn.iocoder.yudao.adminserver.modules.activiti.controller.workflow.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.annotations.ApiModel;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+@ApiModel("待办任务申请分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class TodoTaskPageReqVO extends PageParam {
+
+    private String assignee;
+}

+ 33 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/controller/workflow/vo/TodoTaskRespVO.java

@@ -0,0 +1,33 @@
+package cn.iocoder.yudao.adminserver.modules.activiti.controller.workflow.vo;
+
+import io.swagger.annotations.ApiModel;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+@ApiModel("待办任务 Response VO")
+@Data
+@ToString
+public class TodoTaskRespVO {
+
+    private String id;
+
+    /**
+     * 1:未签收
+     * 2:已签收
+     */
+    private Integer status;
+
+
+    private String processName;
+
+
+    private String processKey;
+
+
+    private String businessKey;
+
+
+    private String formKey;
+
+}

+ 34 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/convert/oa/OaLeaveConvert.java

@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.adminserver.modules.activiti.convert.oa;
+
+import java.util.*;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+import cn.iocoder.yudao.adminserver.modules.activiti.controller.oa.vo.*;
+import cn.iocoder.yudao.adminserver.modules.activiti.dal.dataobject.oa.OaLeaveDO;
+
+/**
+ * 请假申请 Convert
+ *
+ * @author 芋艿
+ */
+@Mapper
+public interface OaLeaveConvert {
+
+    OaLeaveConvert INSTANCE = Mappers.getMapper(OaLeaveConvert.class);
+
+    OaLeaveDO convert(OaLeaveCreateReqVO bean);
+
+    OaLeaveDO convert(OaLeaveUpdateReqVO bean);
+
+    OaLeaveRespVO convert(OaLeaveDO bean);
+
+    List<OaLeaveRespVO> convertList(List<OaLeaveDO> list);
+
+    PageResult<OaLeaveRespVO> convertPage(PageResult<OaLeaveDO> page);
+
+    List<OaLeaveExcelVO> convertList02(List<OaLeaveDO> list);
+
+}

+ 9 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/convert/workflow/TodoTaskConvert.java

@@ -0,0 +1,9 @@
+package cn.iocoder.yudao.adminserver.modules.activiti.convert.workflow;
+
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+@Mapper
+public interface TodoTaskConvert {
+    TodoTaskConvert INSTANCE = Mappers.getMapper(TodoTaskConvert.class);
+}

+ 60 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/dal/dataobject/oa/OaLeaveDO.java

@@ -0,0 +1,60 @@
+package cn.iocoder.yudao.adminserver.modules.activiti.dal.dataobject.oa;
+
+import lombok.*;
+import java.util.*;
+import com.baomidou.mybatisplus.annotation.*;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+
+/**
+ * 请假申请 DO
+ *
+ * @author 芋艿
+ */
+@TableName("oa_leave")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class OaLeaveDO extends BaseDO {
+
+    /**
+     * 请假表单主键
+     */
+    @TableId
+    private Long id;
+    /**
+     * 流程id
+     */
+    private String processInstanceId;
+    /**
+     * 状态
+     */
+    private Integer status;
+    /**
+     * 申请人id
+     */
+    private String userId;
+    /**
+     * 开始时间
+     */
+    private Date startTime;
+    /**
+     * 结束时间
+     */
+    private Date endTime;
+    /**
+     * 请假类型
+     */
+    private String leaveType;
+    /**
+     * 原因
+     */
+    private String reason;
+    /**
+     * 申请时间
+     */
+    private Date applyTime;
+
+}

+ 29 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/dal/dataobject/process/ProcessDefinitionDO.java

@@ -0,0 +1,29 @@
+package cn.iocoder.yudao.adminserver.modules.activiti.dal.dataobject.process;
+
+/**
+ * 流程模型实体类 映射  activiti ProcessDefinition接口
+ *
+ * @author ZJQ
+ * @date 2021/9/7 23:23
+ */
+public class ProcessDefinitionDO {
+
+    private String id;
+
+    private String category;
+
+    private String key;
+
+    private String name;
+
+    private String version;
+
+    private String resourceName;
+
+    private String deploymentId;
+
+    private String diagramResourceName;
+
+    private boolean suspended;
+
+}

+ 46 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/dal/mysql/oa/OaLeaveMapper.java

@@ -0,0 +1,46 @@
+package cn.iocoder.yudao.adminserver.modules.activiti.dal.mysql.oa;
+
+import java.util.*;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.adminserver.modules.activiti.dal.dataobject.oa.OaLeaveDO;
+import org.apache.ibatis.annotations.Mapper;
+import cn.iocoder.yudao.adminserver.modules.activiti.controller.oa.vo.*;
+
+/**
+ * 请假申请 Mapper
+ *
+ * @author 芋艿
+ */
+@Mapper
+public interface OaLeaveMapper extends BaseMapperX<OaLeaveDO> {
+
+    default PageResult<OaLeaveDO> selectPage(OaLeavePageReqVO reqVO) {
+        return selectPage(reqVO, new QueryWrapperX<OaLeaveDO>()
+                .eqIfPresent("process_instance_id", reqVO.getProcessInstanceId())
+                .eqIfPresent("status", reqVO.getStatus())
+                .eqIfPresent("user_id", reqVO.getUserId())
+                .betweenIfPresent("start_time", reqVO.getBeginStartTime(), reqVO.getEndStartTime())
+                .betweenIfPresent("end_time", reqVO.getBeginEndTime(), reqVO.getEndEndTime())
+                .eqIfPresent("leave_type", reqVO.getLeaveType())
+                .eqIfPresent("reason", reqVO.getReason())
+                .betweenIfPresent("apply_time", reqVO.getBeginApplyTime(), reqVO.getEndApplyTime())
+                .orderByDesc("id")        );
+    }
+
+    default List<OaLeaveDO> selectList(OaLeaveExportReqVO reqVO) {
+        return selectList(new QueryWrapperX<OaLeaveDO>()
+                .eqIfPresent("process_instance_id", reqVO.getProcessInstanceId())
+                .eqIfPresent("status", reqVO.getStatus())
+                .eqIfPresent("user_id", reqVO.getUserId())
+                .betweenIfPresent("start_time", reqVO.getBeginStartTime(), reqVO.getEndStartTime())
+                .betweenIfPresent("end_time", reqVO.getBeginEndTime(), reqVO.getEndEndTime())
+                .eqIfPresent("leave_type", reqVO.getLeaveType())
+                .eqIfPresent("reason", reqVO.getReason())
+                .betweenIfPresent("apply_time", reqVO.getBeginApplyTime(), reqVO.getEndApplyTime())
+                .orderByDesc("id")        );
+    }
+
+}

+ 13 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/enums/OaErrorCodeConstants.java

@@ -0,0 +1,13 @@
+package cn.iocoder.yudao.adminserver.modules.activiti.enums;
+
+import cn.iocoder.yudao.framework.common.exception.ErrorCode;
+/**
+ * activiti 系统 错误码枚举类
+ *
+ * 003 activiti
+ * 001 oa
+ * activiti 系统,使用 1-003-000-000 段
+ */
+public interface OaErrorCodeConstants {
+    ErrorCode LEAVE_NOT_EXISTS = new ErrorCode(1003001001, "请假申请不存在");
+}

+ 2 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/package-info.java

@@ -0,0 +1,2 @@
+// TODO @芋艿:思考下 activiti、oa 的定位,边界,模块的拆分
+package cn.iocoder.yudao.adminserver.modules.activiti;

+ 62 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/service/config/UserGroupManagerService.java

@@ -0,0 +1,62 @@
+package cn.iocoder.yudao.adminserver.modules.activiti.service.config;
+
+
+import cn.iocoder.yudao.adminserver.modules.system.service.dept.SysPostService;
+import cn.iocoder.yudao.adminserver.modules.system.service.permission.SysPermissionService;
+import cn.iocoder.yudao.adminserver.modules.system.service.user.SysUserService;
+import cn.iocoder.yudao.coreservice.modules.system.dal.dataobject.user.SysUserDO;
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.security.core.LoginUser;
+import org.activiti.api.runtime.shared.identity.UserGroupManager;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.stereotype.Service;
+
+
+import javax.annotation.Resource;
+import java.util.*;
+import java.util.stream.Collectors;
+
+import static java.util.Collections.singleton;
+
+@Service
+public class UserGroupManagerService implements UserGroupManager {
+
+    @Resource
+    private  UserDetailsService userDetailsService;
+
+    @Resource
+    private SysUserService userService;
+
+    @Resource
+    private SysPostService  sysPostService;
+
+    /**
+     * 暂时使用岗位来代替
+     * @param userId
+     * @return
+     */
+    @Override
+    public List<String> getUserGroups(String userId) {
+//        final LoginUser loginUser = (LoginUser) userDetailsService.loadUserByUsername(userId);
+//        final Long id = loginUser.getId();
+        final SysUserDO user = userService.getUserByUsername(userId);
+        return  sysPostService.getPosts(user.getPostIds()).stream().map(post -> post.getCode()).collect(Collectors.toList());
+
+    }
+
+    @Override
+    public List<String> getUserRoles(String userId) {
+       return Arrays.asList("ROLE_ACTIVITI_USER");
+    }
+
+    @Override
+    public List<String> getGroups() {
+        throw new UnsupportedOperationException("getGroups is now un supported");
+    }
+
+    @Override
+    public List<String> getUsers() {
+        throw new UnsupportedOperationException("getGroups is now un supported");
+    }
+}

+ 31 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/service/config/UserGroupsProvider.java

@@ -0,0 +1,31 @@
+package cn.iocoder.yudao.adminserver.modules.activiti.service.config;
+
+import cn.iocoder.yudao.framework.security.core.LoginUser;
+import org.activiti.api.runtime.shared.security.PrincipalGroupsProvider;
+import org.springframework.security.core.Authentication;
+import org.springframework.stereotype.Service;
+
+import java.security.Principal;
+import java.util.Collections;
+import java.util.List;
+
+@Service
+public class UserGroupsProvider implements PrincipalGroupsProvider {
+
+    @Override
+    public List<String> getGroups(Principal principal) {
+
+        if(principal instanceof Authentication){
+            Authentication authentication = (Authentication) principal;
+            final Object user = authentication.getPrincipal();
+            if(  user instanceof LoginUser){
+                return ((LoginUser) user).getGroups();
+            }else{
+                return Collections.emptyList();
+            }
+        }else{
+            return Collections.emptyList();
+        }
+
+    }
+}

+ 76 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/service/oa/OaLeaveService.java

@@ -0,0 +1,76 @@
+package cn.iocoder.yudao.adminserver.modules.activiti.service.oa;
+
+
+import cn.iocoder.yudao.adminserver.modules.activiti.controller.oa.vo.OaLeaveCreateReqVO;
+import cn.iocoder.yudao.adminserver.modules.activiti.controller.oa.vo.OaLeaveExportReqVO;
+import cn.iocoder.yudao.adminserver.modules.activiti.controller.oa.vo.OaLeavePageReqVO;
+import cn.iocoder.yudao.adminserver.modules.activiti.controller.oa.vo.OaLeaveUpdateReqVO;
+import cn.iocoder.yudao.adminserver.modules.activiti.dal.dataobject.oa.OaLeaveDO;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+import javax.validation.Valid;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * 请假申请 Service 接口
+ *
+ * @author 芋艿
+ */
+public interface OaLeaveService {
+
+    /**
+     * 创建请假申请
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createLeave(@Valid OaLeaveCreateReqVO createReqVO);
+
+    /**
+     * 更新请假申请
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateLeave(@Valid OaLeaveUpdateReqVO updateReqVO);
+
+    /**
+     * 删除请假申请
+     *
+     * @param id 编号
+     */
+    void deleteLeave(Long id);
+
+    /**
+     * 获得请假申请
+     *
+     * @param id 编号
+     * @return 请假申请
+     */
+    OaLeaveDO getLeave(Long id);
+
+    /**
+     * 获得请假申请列表
+     *
+     * @param ids 编号
+     * @return 请假申请列表
+     */
+    List<OaLeaveDO> getLeaveList(Collection<Long> ids);
+
+    /**
+     * 获得请假申请分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 请假申请分页
+     */
+    PageResult<OaLeaveDO> getLeavePage(OaLeavePageReqVO pageReqVO);
+
+    /**
+     * 获得请假申请列表, 用于 Excel 导出
+     *
+     * @param exportReqVO 查询条件
+     * @return 请假申请列表
+     */
+    List<OaLeaveDO> getLeaveList(OaLeaveExportReqVO exportReqVO);
+
+}

+ 43 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/service/oa/ReportBackEndProcessor.java

@@ -0,0 +1,43 @@
+package cn.iocoder.yudao.adminserver.modules.activiti.service.oa;
+
+import cn.iocoder.yudao.adminserver.modules.activiti.dal.dataobject.oa.OaLeaveDO;
+import cn.iocoder.yudao.adminserver.modules.activiti.dal.mysql.oa.OaLeaveMapper;
+import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
+import org.activiti.engine.delegate.DelegateExecution;
+import org.activiti.engine.delegate.ExecutionListener;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+
+@Component
+public class ReportBackEndProcessor implements ExecutionListener {
+
+    @Resource
+    private OaLeaveMapper leaveMapper;
+
+
+//    @Override
+//    @Transactional(rollbackFor = Exception.class)
+//    public void notify(DelegateTask delegateTask) {
+//        final String businessKey = delegateTask.getExecution().getProcessInstanceBusinessKey();
+//        UpdateWrapper<OaLeaveDO> updateWrapper = new UpdateWrapper<>();
+//        updateWrapper.eq("id", Long.valueOf(businessKey));
+//        OaLeaveDO updateDo = new OaLeaveDO();
+//        updateDo.setStatus(2);
+//        leaveMapper.update(updateDo, updateWrapper);
+//    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void notify(DelegateExecution delegateExecution) {
+        final String businessKey = delegateExecution.getProcessInstanceBusinessKey();
+        // TODO @json:service 不要出现 dao 的元素,例如说 UpdateWrapper。这里,我们可以调用 updateById 方法
+        UpdateWrapper<OaLeaveDO> updateWrapper = new UpdateWrapper<>();
+        updateWrapper.eq("id", Long.valueOf(businessKey));
+        OaLeaveDO updateDo = new OaLeaveDO();
+        updateDo.setStatus(2);  // TODO @json:status 要枚举起来,不要出现 magic number
+        leaveMapper.update(updateDo, updateWrapper);
+    }
+
+}

+ 141 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/service/oa/impl/OaLeaveServiceImpl.java

@@ -0,0 +1,141 @@
+package cn.iocoder.yudao.adminserver.modules.activiti.service.oa.impl;
+
+import cn.iocoder.yudao.adminserver.modules.activiti.controller.oa.vo.OaLeaveCreateReqVO;
+import cn.iocoder.yudao.adminserver.modules.activiti.controller.oa.vo.OaLeaveExportReqVO;
+import cn.iocoder.yudao.adminserver.modules.activiti.controller.oa.vo.OaLeavePageReqVO;
+import cn.iocoder.yudao.adminserver.modules.activiti.controller.oa.vo.OaLeaveUpdateReqVO;
+import cn.iocoder.yudao.adminserver.modules.activiti.convert.oa.OaLeaveConvert;
+import cn.iocoder.yudao.adminserver.modules.activiti.dal.dataobject.oa.OaLeaveDO;
+import cn.iocoder.yudao.adminserver.modules.activiti.dal.mysql.oa.OaLeaveMapper;
+import cn.iocoder.yudao.adminserver.modules.activiti.service.oa.OaLeaveService;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
+import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
+import org.activiti.api.task.model.Task;
+import org.activiti.api.task.model.builders.TaskPayloadBuilder;
+import org.activiti.api.task.runtime.TaskRuntime;
+import org.activiti.engine.RuntimeService;
+import org.activiti.engine.runtime.ProcessInstance;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.validation.annotation.Validated;
+
+import javax.annotation.Resource;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.adminserver.modules.activiti.enums.OaErrorCodeConstants.LEAVE_NOT_EXISTS;
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+
+/**
+ * 请假申请 Service 实现类
+ *
+ * @author 芋艿
+ */
+@Service
+@Validated
+public class OaLeaveServiceImpl implements OaLeaveService {
+
+    @Resource
+    private OaLeaveMapper leaveMapper;
+
+    @Resource
+    private RuntimeService runtimeService;
+
+    @Resource
+    private org.activiti.engine.TaskService activitiTaskService;
+
+    @Resource
+    private TaskRuntime taskRuntime;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Long createLeave(OaLeaveCreateReqVO createReqVO) {
+        // 插入 OA 请假单
+        OaLeaveDO leave = OaLeaveConvert.INSTANCE.convert(createReqVO);
+        leave.setStatus(1);
+        leave.setUserId(SecurityFrameworkUtils.getLoginUser().getUsername());
+        leaveMapper.insert(leave);
+
+        // 创建工作流
+        Map<String, Object> variables = new HashMap<>();
+        // 如何得到部门领导人,暂时写死
+        variables.put("deptLeader", "admin"); // TODO @芋艿:需要部门的负责人
+        Long id = leave.getId();
+        String businessKey = String.valueOf(id);
+        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(createReqVO.getProcessKey(), businessKey, variables);
+        String processInstanceId = processInstance.getProcessInstanceId();
+
+        // TODO @json:service 不要出现 dao 的元素,例如说 UpdateWrapper。这里,我们可以调用 updateById 方法
+        // 将工作流的编号,更新到 OA 请假单中
+        UpdateWrapper<OaLeaveDO> updateWrapper = new UpdateWrapper<>();
+        updateWrapper.eq("id", id);
+        OaLeaveDO updateDo = new OaLeaveDO();
+        updateDo.setProcessInstanceId(processInstanceId);
+        leaveMapper.update(updateDo, updateWrapper);
+        return id;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateLeave(OaLeaveUpdateReqVO updateReqVO) {
+        // 校验存在
+        this.validateLeaveExists(updateReqVO.getId());
+
+        final Task task = taskRuntime.task(updateReqVO.getTaskId());
+        activitiTaskService.addComment(task.getId(), task.getProcessInstanceId(), updateReqVO.getComment());
+        Map<String, Object> variables = updateReqVO.getVariables();
+
+        //如何得到部门领导人, 暂时写死
+        variables.put("deptLeader", "admin");
+        taskRuntime.complete(TaskPayloadBuilder.complete().withTaskId(task.getId())
+                .withVariables(variables)
+                .build());
+        // TODO @jason:不需要加 final 哈。虽然是不变,但是代码比较少这么去写
+        final Object reApply = variables.get("reApply");
+        // TODO @jason:直接使用 Objects.equals(reApply, true) 就可以
+        if((reApply instanceof Boolean) && (Boolean)reApply){
+            // 更新 表单
+            OaLeaveDO updateObj = OaLeaveConvert.INSTANCE.convert(updateReqVO);
+            leaveMapper.updateById(updateObj);
+        }
+    }
+
+    @Override
+    public void deleteLeave(Long id) {
+        // 校验存在
+        this.validateLeaveExists(id);
+        // 删除
+        leaveMapper.deleteById(id);
+        // TODO @jason:需要调用 runtimeService 的 delete 方法,删除???
+    }
+
+    private void validateLeaveExists(Long id) {
+        if (leaveMapper.selectById(id) == null) {
+            throw exception(LEAVE_NOT_EXISTS);
+        }
+    }
+
+    @Override
+    public OaLeaveDO getLeave(Long id) {
+        return leaveMapper.selectById(id);
+    }
+
+    @Override
+    public List<OaLeaveDO> getLeaveList(Collection<Long> ids) {
+        return leaveMapper.selectBatchIds(ids);
+    }
+
+    @Override
+    public PageResult<OaLeaveDO> getLeavePage(OaLeavePageReqVO pageReqVO) {
+        return leaveMapper.selectPage(pageReqVO);
+    }
+
+    @Override
+    public List<OaLeaveDO> getLeaveList(OaLeaveExportReqVO exportReqVO) {
+        return leaveMapper.selectList(exportReqVO);
+    }
+
+}

+ 29 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/service/process/ProcessService.java

@@ -0,0 +1,29 @@
+package cn.iocoder.yudao.adminserver.modules.activiti.service.process;
+
+import org.springframework.web.multipart.MultipartFile;
+
+/**
+ * 流程基础管理
+ *
+ * @author ZJQ
+ * @date 2021/9/5 21:00
+ */
+public interface ProcessService {
+
+    /**
+     * 上传流程文件,进行流程模型部署
+     * @param multipartFile 上传文件
+     */
+    void deployProcess(MultipartFile multipartFile);
+
+
+    /**
+     * 激活或者挂起流程模型实体
+     * @param processDefinitionId 流程模型实体id
+     * @param type 类型
+     * @return 状态
+     */
+    String setActivOrHang(String processDefinitionId,String type);
+
+
+}

+ 109 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/service/process/impl/ProcessServiceImpl.java

@@ -0,0 +1,109 @@
+package cn.iocoder.yudao.adminserver.modules.activiti.service.process.impl;
+
+import cn.iocoder.yudao.adminserver.modules.activiti.service.process.ProcessService;
+import lombok.extern.slf4j.Slf4j;
+import org.activiti.engine.RepositoryService;
+import org.activiti.engine.repository.Deployment;
+import org.activiti.engine.repository.ProcessDefinition;
+import org.apache.commons.io.FilenameUtils;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.annotation.Resource;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+import java.util.zip.ZipInputStream;
+import static cn.iocoder.yudao.adminserver.modules.system.enums.SysErrorCodeConstants.FILE_UPLOAD_FAILED;
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+
+/**
+ * 流程基础管理
+ *
+ * @author ZJQ
+ * @date 2021/9/5 21:04
+ */
+@Service
+@Slf4j
+public class ProcessServiceImpl implements ProcessService {
+
+    private static final String BPMN20_XML = "bpmn20.xml";
+
+    @Resource
+    private RepositoryService repositoryService;
+
+    /**
+     * 上传流程文件,进行流程部署
+     * @param multipartFile 上传文件
+     */
+    @Override
+    public void deployProcess(MultipartFile multipartFile) {
+        String fileName = multipartFile.getOriginalFilename();
+        try (InputStream inputStream = multipartFile.getInputStream()){
+            Deployment deployment = getDeplymentByType(inputStream,fileName);
+            //获取部署成功的流程模型
+            List<ProcessDefinition> processDefinitions = repositoryService.createProcessDefinitionQuery().deploymentId(deployment.getId()).list();
+            processDefinitions.forEach((processDefinition)->{
+                //设置线上部署流程模型名字
+                String proDefId = processDefinition.getId();
+                repositoryService.setProcessDefinitionCategory(proDefId,fileName);
+                log.info("流程文件部署成功,流程ID="+proDefId);
+            });
+        } catch (IOException e) {
+           log.error("流程部署出现异常"+e);
+        }
+    }
+
+    /**
+     * 激活或者挂起流程模型实体
+     * @param processDefinitionId 流程模型实体id
+     * @param type 类型
+     * @return 提示
+     */
+    @Override
+    public String setActivOrHang(String processDefinitionId, String type) {
+        String result = "无操作";
+        switch (type){
+            case "active":
+                repositoryService.activateProcessDefinitionById(processDefinitionId,true,null);
+                result = "已激活ID为【"+processDefinitionId+"】的流程模型实例";
+                break;
+            case "suspend":
+                repositoryService.suspendProcessDefinitionById(processDefinitionId,true,null);
+                result = "已挂起ID为【"+processDefinitionId+"】的流程模型实例";
+                break;
+            default:
+                break;
+        }
+        return result;
+    }
+
+
+    /**
+     * 根据上传文件类型对应实现不同方式的流程部署
+     * @param inputStream 文件输入流
+     * @param fileName 文件名
+     * @return 文件部署流程
+     */
+    public Deployment getDeplymentByType(InputStream inputStream,String fileName){
+        Deployment deployment;
+        String type = FilenameUtils.getExtension(fileName);
+        switch (type){
+            case "bpmn":
+                String baseName = FilenameUtils.getBaseName(fileName);
+                deployment = repositoryService.createDeployment().addInputStream(baseName+"."+BPMN20_XML,inputStream).deploy();
+                break;
+            case "png":
+                deployment = repositoryService.createDeployment().addInputStream(fileName,inputStream).deploy();
+                break;
+            case "zip":
+            case "bar":
+                ZipInputStream zipInputStream = new ZipInputStream(inputStream);
+                deployment = repositoryService.createDeployment().addZipInputStream(zipInputStream).deploy();
+                break;
+            default:
+                throw exception(FILE_UPLOAD_FAILED);
+        }
+        return deployment;
+    }
+}

+ 26 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/service/workflow/TaskService.java

@@ -0,0 +1,26 @@
+package cn.iocoder.yudao.adminserver.modules.activiti.service.workflow;
+
+import cn.iocoder.yudao.adminserver.modules.activiti.controller.workflow.vo.*;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+import java.util.List;
+
+// TODO @芋艿:前缀,注释
+public interface TaskService {
+
+    PageResult<TodoTaskRespVO> getTodoTaskPage(TodoTaskPageReqVO pageReqVO);
+
+    void claimTask(String taskId);
+
+    void getTaskHistory(String taskId);
+
+    void completeTask(TaskReqVO taskReq);
+
+//    void flowImage(String taskId, HttpServletResponse response);
+    TaskHandleVO getTaskSteps(TaskQueryReqVO taskQuery);
+
+    List<TaskStepVO> getHistorySteps(String processInstanceId);
+
+    TodoTaskRespVO getTaskFormKey(TaskQueryReqVO taskQuery);
+
+}

+ 266 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/activiti/service/workflow/impl/TaskServiceImpl.java

@@ -0,0 +1,266 @@
+package cn.iocoder.yudao.adminserver.modules.activiti.service.workflow.impl;
+
+import cn.iocoder.yudao.adminserver.modules.activiti.controller.workflow.vo.*;
+import cn.iocoder.yudao.adminserver.modules.activiti.dal.mysql.oa.OaLeaveMapper;
+import cn.iocoder.yudao.adminserver.modules.activiti.service.workflow.TaskService;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.security.core.LoginUser;
+import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
+import com.google.common.collect.ImmutableMap;
+import org.activiti.api.runtime.shared.query.Page;
+import org.activiti.api.runtime.shared.query.Pageable;
+import org.activiti.api.task.model.Task;
+import org.activiti.api.task.model.builders.ClaimTaskPayloadBuilder;
+import org.activiti.api.task.model.builders.TaskPayloadBuilder;
+import org.activiti.api.task.runtime.TaskRuntime;
+import org.activiti.engine.HistoryService;
+import org.activiti.engine.RepositoryService;
+import org.activiti.engine.history.HistoricActivityInstance;
+import org.activiti.engine.history.HistoricProcessInstance;
+import org.activiti.engine.repository.ProcessDefinition;
+import org.activiti.engine.task.Comment;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+@Service
+public class TaskServiceImpl implements TaskService {
+
+    @Resource
+    private  TaskRuntime taskRuntime;
+
+    @Resource
+    private org.activiti.engine.TaskService activitiTaskService;
+
+    @Resource
+    private HistoryService  historyService;
+
+    @Resource
+    private RepositoryService repositoryService;
+
+    @Resource
+    private OaLeaveMapper leaveMapper;
+
+    private static Map<String,String>  taskVariable =  ImmutableMap.<String,String>builder()
+                    .put("deptLeaderVerify","deptLeaderApproved")
+                    .put("hrVerify","hrApproved")
+                    .build();
+
+    public TaskServiceImpl() {
+
+    }
+
+    @Override
+    public PageResult<TodoTaskRespVO> getTodoTaskPage(TodoTaskPageReqVO pageReqVO) {
+        final LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
+        // TODO @jason:封装一个方法,用于转换成 activiti 的分页对象
+        final Pageable pageable = Pageable.of((pageReqVO.getPageNo() - 1) * pageReqVO.getPageSize(), pageReqVO.getPageSize());
+        Page<Task> pageTasks = taskRuntime.tasks(pageable);
+        // TODO @jason:convert 里转换
+        List<Task> tasks = pageTasks.getContent();
+        int totalItems = pageTasks.getTotalItems();
+        final List<TodoTaskRespVO> respVOList = tasks.stream().map(task -> {
+            TodoTaskRespVO respVO = new TodoTaskRespVO();
+            respVO.setId(task.getId());
+            final ProcessDefinition definition = repositoryService.getProcessDefinition(task.getProcessDefinitionId());
+            respVO.setProcessName(definition.getName());
+            respVO.setProcessKey(definition.getKey());
+            respVO.setBusinessKey(task.getBusinessKey());
+            respVO.setStatus(task.getAssignee() == null ? 1 : 2);
+            return respVO;
+        }).collect(Collectors.toList());
+        // TODO @jason:要注意泛型哈。
+        return new PageResult(respVOList, Long.valueOf(totalItems)); // TODO @jason:(long) 转换即可
+    }
+
+
+    @Override
+    public void claimTask(String taskId) {
+        taskRuntime.claim(new ClaimTaskPayloadBuilder()
+                                .withTaskId(taskId)
+                                .withAssignee(SecurityFrameworkUtils.getLoginUser().getUsername())
+                                .build());
+    }
+
+    @Override
+    public void getTaskHistory(String taskId) {
+        final List<HistoricProcessInstance> list = historyService.createHistoricProcessInstanceQuery().
+                processInstanceId("8e2801fc-1a38-11ec-98ce-74867a13730f").list();
+    }
+
+    // TODO @jason:一个方法里,会有多个方法的调用,最好写下对应的注释。这样容易理解
+    @Override
+    @Transactional
+    public void completeTask(TaskReqVO taskReq) {
+        final Task task = taskRuntime.task(taskReq.getTaskId());
+
+        final Map<String, Object> variables = taskReq.getVariables();
+
+        activitiTaskService.addComment(taskReq.getTaskId(), task.getProcessInstanceId(), taskReq.getComment());
+
+        taskRuntime.complete(TaskPayloadBuilder.complete().withTaskId(taskReq.getTaskId())
+                .withVariables(taskReq.getVariables())
+                .build());
+
+//        if(variables.containsValue(Boolean.FALSE)){
+//            final String businessKey = task.getBusinessKey();
+//            UpdateWrapper<OaLeaveDO> updateWrapper = new UpdateWrapper<>();
+//            updateWrapper.eq("id", Long.valueOf(businessKey));
+//            OaLeaveDO updateDo = new OaLeaveDO();
+//            updateDo.setStatus(2);
+//            leaveMapper.update(updateDo, updateWrapper);
+//        }
+
+    }
+
+//    @Override
+//    public void flowImage(String taskId, HttpServletResponse response) {
+//
+//        final Task task = taskRuntime.task(taskId);
+//        BpmnModel bpmnModel = repositoryService.getBpmnModel(task.getProcessDefinitionId());
+//        final Process process = bpmnModel.getMainProcess();
+//        ProcessDefinitionEntity processDefinition = repositoryService.createProcessDefinitionQuery().processDefinitionId(task.getProcessDefinitionId()).singleResult();
+//        List<String> activeActivityIds = runtimeService.getActiveActivityIds(executionId);
+//        List<String> highLightedFlows = getHighLightedFlows(processDefinition, processInstance.getId());
+//        ProcessDiagramGenerator diagramGenerator = processEngineConfiguration.getProcessDiagramGenerator();
+//        InputStream imageStream =diagramGenerator.generateDiagram(bpmnModel, "png", activeActivityIds, highLightedFlows);
+//
+//        // 输出资源内容到相应对象
+//        byte[] b = new byte[1024];
+//        int len;
+//        while ((len = imageStream.read(b, 0, 1024)) != -1) {
+//            response.getOutputStream().write(b, 0, len);
+//        }
+//    }
+
+    @Override
+    public TaskHandleVO getTaskSteps(TaskQueryReqVO taskQuery) {
+        TaskHandleVO handleVO = new TaskHandleVO();
+
+//        String processKey = taskQuery.getProcessKey();
+//        if ("leave".equals(processKey)) {
+//            String businessKey = taskQuery.getBusinessKey();
+//            final OaLeaveDO leave = leaveMapper.selectById(Long.valueOf(businessKey));
+//            handleVO.setFormObject( OaLeaveConvert.INSTANCE.convert(leave));
+//        }
+
+//
+//        final String taskDefKey = task.getTaskDefinitionKey();
+//        final String variableName = Optional.ofNullable(taskVariable.get(taskDefKey)).orElse("");
+//        handleVO.setTaskVariable(variableName);
+        final Task task = taskRuntime.task(taskQuery.getTaskId());
+
+        List<TaskStepVO> steps = getTaskSteps(task.getProcessInstanceId());
+
+        handleVO.setHistoryTask(steps);
+        return handleVO;
+    }
+
+
+    private List<TaskStepVO> getTaskSteps(String processInstanceId) {
+        // 获得已完成的活动
+        List<HistoricActivityInstance> finished = historyService.createHistoricActivityInstanceQuery()
+                .processInstanceId(processInstanceId)
+                .activityType("userTask")
+                .finished()
+                .orderByHistoricActivityInstanceStartTime().asc().list();
+        // 获得对应的步骤
+        List<TaskStepVO> steps = new ArrayList<>();
+        finished.forEach(instance -> {
+            // TODO @jason:放到 convert 里
+            TaskStepVO step = new TaskStepVO();
+            step.setStepName(instance.getActivityName());
+            step.setStartTime(instance.getStartTime());
+            step.setEndTime(instance.getEndTime());
+            step.setAssignee(instance.getAssignee());
+            step.setStatus(1);
+            // TODO @jason:一般判数组为空,使用 CollUtil.isEmpty 会好点哈。另外,null 时候,不用填写 "" 的哈
+            List<Comment> comments = activitiTaskService.getTaskComments(instance.getTaskId());
+            if (comments.size() > 0) {
+                step.setComment(comments.get(0).getFullMessage());
+            } else {
+                step.setComment("");
+            }
+            steps.add(step);
+        });
+
+        // 获得未完成的活动
+        List<HistoricActivityInstance> unfinished = historyService
+                .createHistoricActivityInstanceQuery()
+                .processInstanceId(processInstanceId)
+                .activityType("userTask")
+                .unfinished().list();
+        // 获得对应的步骤
+        // TODO @json:其实已完成和未完成,它们的 convert 的逻辑,是一致的
+        for (HistoricActivityInstance instance : unfinished) {
+            TaskStepVO step = new TaskStepVO();
+            step.setStepName(instance.getActivityName());
+            step.setStartTime(instance.getStartTime());
+            step.setEndTime(instance.getEndTime());
+            step.setAssignee(Optional.ofNullable(instance.getAssignee()).orElse(""));
+            step.setComment("");
+            step.setStatus(0);
+            steps.add(step);
+        }
+        return steps;
+    }
+
+
+    @Override
+    public List<TaskStepVO> getHistorySteps(String processInstanceId) {
+        return getTaskSteps(processInstanceId);
+    }
+
+    @Override
+    public TodoTaskRespVO getTaskFormKey(TaskQueryReqVO taskQuery) {
+        final Task task = taskRuntime.task(taskQuery.getTaskId());
+        // 转换结果
+        TodoTaskRespVO respVO = new TodoTaskRespVO();
+        respVO.setFormKey(task.getFormKey());
+        respVO.setBusinessKey(task.getBusinessKey());
+        respVO.setId(task.getId());
+        return respVO;
+    }
+
+//    private List<String> getHighLightedFlows(ProcessDefinitionEntity processDefinition, String processInstanceId) {
+//
+//        List<String> highLightedFlows = new ArrayList<String>();
+//        List<HistoricActivityInstance> historicActivityInstances = historyService
+//                .createHistoricActivityInstanceQuery()
+//                .processInstanceId(processInstanceId)
+//                .orderByHistoricActivityInstanceStartTime().asc().list();
+//
+//        List<String> historicActivityInstanceList = new ArrayList<String>();
+//        for (HistoricActivityInstance hai : historicActivityInstances) {
+//            historicActivityInstanceList.add(hai.getActivityId());
+//        }
+
+//        // add current activities to list
+//        List<String> highLightedActivities = runtimeService.getActiveActivityIds(processInstanceId);
+//        historicActivityInstanceList.addAll(highLightedActivities);
+
+        // activities and their sequence-flows
+//        for (ActivityImpl activity : processDefinition.getActivities()) {
+//            int index = historicActivityInstanceList.indexOf(activity.getId());
+//
+//            if (index >= 0 && index + 1 < historicActivityInstanceList.size()) {
+//                List<PvmTransition> pvmTransitionList = activity
+//                        .getOutgoingTransitions();
+//                for (PvmTransition pvmTransition : pvmTransitionList) {
+//                    String destinationFlowId = pvmTransition.getDestination().getId();
+//                    if (destinationFlowId.equals(historicActivityInstanceList.get(index + 1))) {
+//                        highLightedFlows.add(pvmTransition.getId());
+//                    }
+//                }
+//            }
+//        }
+//        return highLightedFlows;
+//    }
+
+}

+ 24 - 3
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/service/auth/impl/SysAuthServiceImpl.java

@@ -1,5 +1,11 @@
 package cn.iocoder.yudao.adminserver.modules.system.service.auth.impl;
 
+import cn.iocoder.yudao.adminserver.modules.system.service.auth.SysUserSessionService;
+import cn.iocoder.yudao.adminserver.modules.system.service.dept.SysPostService;
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
+import cn.iocoder.yudao.framework.security.core.LoginUser;
+import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils;
 import cn.hutool.core.collection.CollUtil;
 import cn.iocoder.yudao.adminserver.modules.system.controller.auth.vo.auth.SysAuthLoginReqVO;
 import cn.iocoder.yudao.adminserver.modules.system.controller.auth.vo.auth.SysAuthSocialBindReqVO;
@@ -40,11 +46,15 @@ import org.springframework.util.Assert;
 
 import javax.annotation.Resource;
 import java.util.List;
+import java.util.Optional;
 import java.util.Objects;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 import static cn.iocoder.yudao.adminserver.modules.system.enums.SysErrorCodeConstants.*;
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.adminserver.modules.system.enums.SysErrorCodeConstants.*;
+import static java.util.Collections.EMPTY_LIST;
 import static java.util.Collections.singleton;
 
 /**
@@ -73,6 +83,7 @@ public class SysAuthServiceImpl implements SysAuthService {
     @Resource
     private SysUserSessionCoreService userSessionCoreService;
     @Resource
+    private SysPostService sysPostService;
     private SysSocialService socialService;
 
     // TODO @timfruit:静态枚举类,需要都大写,例如说 USER_TYPE_ENUM;静态变量,放在普通变量前面;这个实践不错哈。
@@ -86,7 +97,9 @@ public class SysAuthServiceImpl implements SysAuthService {
             throw new UsernameNotFoundException(username);
         }
         // 创建 LoginUser 对象
-        return SysAuthConvert.INSTANCE.convert(user);
+        LoginUser loginUser =  SysAuthConvert.INSTANCE.convert(user);
+        loginUser.setPostIds(user.getPostIds());
+        return loginUser;
     }
 
     @Override
@@ -112,11 +125,18 @@ public class SysAuthServiceImpl implements SysAuthService {
         // 使用账号密码,进行登录。
         LoginUser loginUser = this.login0(reqVO.getUsername(), reqVO.getPassword());
         loginUser.setRoleIds(this.getUserRoleIds(loginUser.getId())); // 获取用户角色列表
-
-        // 缓存登用户到 Redis 中,返回 sessionId 编号
+        loginUser.setGroups(this.getUserPosts(loginUser.getPostIds()));
+        // 缓存登用户到 Redis 中,返回 sessionId 编号
         return userSessionCoreService.createUserSession(loginUser, userIp, userAgent);
     }
 
+
+    private List<String> getUserPosts(Set<Long> postIds) {
+        return Optional.ofNullable(postIds).map(ids->
+               sysPostService.getPosts(ids).stream().map(post -> post.getCode()).collect(Collectors.toList())
+        ).orElse(EMPTY_LIST);
+    }
+
     private void verifyCaptcha(String username, String captchaUUID, String captchaCode) {
         final SysLoginLogTypeEnum logTypeEnum = SysLoginLogTypeEnum.LOGIN_USERNAME;
         String code = captchaService.getCaptchaCode(captchaUUID);
@@ -144,6 +164,7 @@ public class SysAuthServiceImpl implements SysAuthService {
             // 调用 Spring Security 的 AuthenticationManager#authenticate(...) 方法,使用账号密码进行认证
             // 在其内部,会调用到 loadUserByUsername 方法,获取 User 信息
             authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
+           //  org.activiti.engine.impl.identity.Authentication.setAuthenticatedUserId(username);
         } catch (BadCredentialsException badCredentialsException) {
             this.createLoginLog(username, logTypeEnum, SysLoginResultEnum.BAD_CREDENTIALS);
             throw exception(AUTH_LOGIN_BAD_CREDENTIALS);

+ 16 - 2
yudao-admin-server/src/main/resources/application-local.yaml

@@ -47,13 +47,27 @@ spring:
           url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.master.name}?useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT
           driver-class-name: com.mysql.jdbc.Driver
           username: root
-          password: 123456
+          password: root
         slave: # 模拟从库,可根据自己需要修改
           name: ruoyi-vue-pro
           url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.slave.name}?useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT
           driver-class-name: com.mysql.jdbc.Driver
           username: root
-          password: 123456
+          password: root
+
+
+  activiti:
+    #1.false:默认值,activiti启动时,对比数据库表中保存的版本,如果不匹配。将抛出异常
+    #2.true:启动时会对数据库中所有表进行更新操作,如果表存在,不做处理,反之,自动创建表
+    #3.create_drop:启动时自动创建表,关闭时自动删除表
+    #4.drop_create:启动时,删除旧表,再创建新表
+    database-schema-update: true
+    #activiti7默认不生成历史信息表,需手动设置开启
+    db-history-used: true
+    check-process-definitions: true
+    #full:保存历史数据的最高级别,可保存全部流程相关细节,包括流程流转各节点参数
+    history-level: full
+
 
   # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
   redis:

+ 16 - 3
yudao-admin-server/src/main/resources/application.yaml

@@ -22,15 +22,28 @@ spring:
 
 # MyBatis Plus 的配置项
 mybatis-plus:
-  configuration:
-    map-underscore-to-camel-case: true # 虽然默认为 true ,但是还是显示去指定下。
-    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 打印日志
+#  在 mybatis-config/mybatis-config.xml 中设置
+#  configuration:
+#    map-underscore-to-camel-case: true # 虽然默认为 true ,但是还是显示去指定下。
+#    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 打印日志
   global-config:
     db-config:
       id-type: AUTO # 自增 ID
       logic-delete-value: 1 # 逻辑已删除值(默认为 1)
       logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
   mapper-locations: classpath*:mapper/*.xml
+  config-location: classpath:mybatis-config/mybatis-config.xml
+  configuration-properties:
+    prefix: ""
+    wildcardEscapeClause: ""
+    limitBefore: ""
+    limitAfter: "LIMIT #{maxResults} OFFSET #{firstResult}"
+    limitBetween: ""
+    limitOuterJoinBetween: ""
+    limitBeforeNativeQuery: ""
+    orderBy: "order by ${orderByColumns}"
+    blobType: "BLOB"
+    boolValue: "TRUE"
   type-aliases-package: ${yudao.info.base-package}.modules.*.dal.dataobject, ${yudao.core-service.base-package}.modules.*.dal.dataobject
 
 --- #################### 芋道相关配置 ####################

+ 52 - 0
yudao-admin-server/src/main/resources/mybatis-config/mybatis-config.xml

@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
+
+<configuration>
+    <settings>
+        <setting name="lazyLoadingEnabled" value="false" />
+        <setting name="mapUnderscoreToCamelCase" value="true"/>
+    </settings>
+    <typeAliases>
+        <typeAlias type="org.activiti.engine.impl.persistence.ByteArrayRefTypeHandler" alias="ByteArrayRefTypeHandler"/>
+        <typeAlias type="org.activiti.engine.impl.db.IbatisVariableTypeHandler" alias="IbatisVariableTypeHandler"/>
+    </typeAliases>
+    <typeHandlers>
+        <typeHandler handler="ByteArrayRefTypeHandler"
+                     javaType="org.activiti.engine.impl.persistence.entity.ByteArrayRef"
+                     jdbcType="VARCHAR"/>
+        <typeHandler handler="IbatisVariableTypeHandler"
+                     javaType="org.activiti.engine.impl.variable.VariableType"
+                     jdbcType="VARCHAR"/>
+    </typeHandlers>
+    <mappers>
+        <mapper resource="org/activiti/db/mapping/common.xml" />
+        <mapper resource="org/activiti/db/mapping/entity/Attachment.xml" />
+        <mapper resource="org/activiti/db/mapping/entity/ByteArray.xml" />
+        <mapper resource="org/activiti/db/mapping/entity/Comment.xml" />
+        <mapper resource="org/activiti/db/mapping/entity/DeadLetterJob.xml" />
+        <mapper resource="org/activiti/db/mapping/entity/Deployment.xml" />
+        <mapper resource="org/activiti/db/mapping/entity/Execution.xml" />
+        <mapper resource="org/activiti/db/mapping/entity/HistoricActivityInstance.xml" />
+        <mapper resource="org/activiti/db/mapping/entity/HistoricDetail.xml" />
+        <mapper resource="org/activiti/db/mapping/entity/HistoricProcessInstance.xml" />
+        <mapper resource="org/activiti/db/mapping/entity/HistoricVariableInstance.xml" />
+        <mapper resource="org/activiti/db/mapping/entity/HistoricTaskInstance.xml" />
+        <mapper resource="org/activiti/db/mapping/entity/HistoricIdentityLink.xml" />
+        <mapper resource="org/activiti/db/mapping/entity/IdentityLink.xml" />
+        <mapper resource="org/activiti/db/mapping/entity/Job.xml" />
+        <mapper resource="org/activiti/db/mapping/entity/Model.xml" />
+        <mapper resource="org/activiti/db/mapping/entity/ProcessDefinition.xml" />
+        <mapper resource="org/activiti/db/mapping/entity/ProcessDefinitionInfo.xml" />
+        <mapper resource="org/activiti/db/mapping/entity/Property.xml" />
+        <mapper resource="org/activiti/db/mapping/entity/Resource.xml" />
+        <mapper resource="org/activiti/db/mapping/entity/SuspendedJob.xml" />
+        <mapper resource="org/activiti/db/mapping/entity/TableData.xml" />
+        <mapper resource="org/activiti/db/mapping/entity/Task.xml" />
+        <mapper resource="org/activiti/db/mapping/entity/TimerJob.xml" />
+        <mapper resource="org/activiti/db/mapping/entity/VariableInstance.xml" />
+        <mapper resource="org/activiti/db/mapping/entity/EventSubscription.xml" />
+        <mapper resource="org/activiti/db/mapping/entity/EventLogEntry.xml" />
+        <mapper resource="org/activiti/db/mapping/entity/IntegrationContext.xml" />
+    </mappers>
+</configuration>

+ 152 - 0
yudao-admin-server/src/main/resources/processes/leave-formkey.bpmn

@@ -0,0 +1,152 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:activiti="http://activiti.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://bpmn.io/schema/bpmn">
+  <process id="leave-formkey" name="请假流程-外置表单" isExecutable="true">
+    <documentation>外置表单</documentation>
+    <startEvent id="startevent1" name="Start" activiti:initiator="applyUserId" activiti:formKey="/flow/leave/apply"></startEvent>
+    <userTask id="deptLeaderVerify" name="部门经理审批" activiti:assignee="${deptLeader}" activiti:formKey="/flow/leave/approve-leader"></userTask>
+    <exclusiveGateway id="exclusivegateway1" name="Exclusive Gateway"></exclusiveGateway>
+    <userTask id="hrVerify" name="人事经理审批" activiti:candidateGroups="hr" activiti:formKey="/flow/leave/approve-hr"></userTask>
+    <exclusiveGateway id="exclusivegateway2" name="Exclusive Gateway"></exclusiveGateway>
+    <userTask id="reportBack" name="申请人确认" activiti:assignee="${applyUserId}" activiti:formKey="/flow/leave/confirm">
+    </userTask>
+    <endEvent id="endevent1" name="End"></endEvent>
+    <userTask id="modifyApply" name="调整申请内容" activiti:assignee="${applyUserId}" activiti:formKey="/flow/leave/modify"></userTask>
+    <exclusiveGateway id="exclusivegateway3" name="Exclusive Gateway"></exclusiveGateway>
+    <sequenceFlow id="flow1" sourceRef="startevent1" targetRef="deptLeaderVerify"></sequenceFlow>
+    <sequenceFlow id="flow2" sourceRef="deptLeaderVerify" targetRef="exclusivegateway1"></sequenceFlow>
+    <sequenceFlow id="flow3" name="同意" sourceRef="exclusivegateway1" targetRef="hrVerify">
+      <conditionExpression xsi:type="tFormalExpression"><![CDATA[${deptLeaderApproved == 'true'}]]></conditionExpression>
+    </sequenceFlow>
+    <sequenceFlow id="flow4" sourceRef="hrVerify" targetRef="exclusivegateway2"></sequenceFlow>
+    <sequenceFlow id="flow5" name="同意" sourceRef="exclusivegateway2" targetRef="reportBack">
+      <conditionExpression xsi:type="tFormalExpression"><![CDATA[${hrApproved == 'true'}]]></conditionExpression>
+    </sequenceFlow>
+    <sequenceFlow id="flow6" sourceRef="reportBack" targetRef="endevent1">
+      <extensionElements>
+        <activiti:executionListener event="take"  delegateExpression="${reportBackEndProcessor}"></activiti:executionListener>
+      </extensionElements>
+    </sequenceFlow>
+    <sequenceFlow id="flow7" name="不同意" sourceRef="exclusivegateway2" targetRef="modifyApply">
+      <conditionExpression xsi:type="tFormalExpression"><![CDATA[${hrApproved == 'false'}]]></conditionExpression>
+    </sequenceFlow>
+    <sequenceFlow id="flow8" name="不同意" sourceRef="exclusivegateway1" targetRef="modifyApply">
+      <conditionExpression xsi:type="tFormalExpression"><![CDATA[${deptLeaderApproved == 'false'}]]></conditionExpression>
+    </sequenceFlow>
+    <sequenceFlow id="flow9" sourceRef="modifyApply" targetRef="exclusivegateway3"></sequenceFlow>
+    <sequenceFlow id="flow10" name="调整后继续申请" sourceRef="exclusivegateway3" targetRef="deptLeaderVerify">
+      <conditionExpression xsi:type="tFormalExpression"><![CDATA[${reApply == 'true'}]]></conditionExpression>
+    </sequenceFlow>
+    <sequenceFlow id="flow11" name="取消申请,并设置取消标志" sourceRef="exclusivegateway3" targetRef="endevent1">
+      <extensionElements>
+        <activiti:executionListener event="take"  delegateExpression="${reportBackEndProcessor}"></activiti:executionListener>
+      </extensionElements>
+      <conditionExpression xsi:type="tFormalExpression"><![CDATA[${reApply == 'false'}]]></conditionExpression>
+    </sequenceFlow>
+    <textAnnotation id="textannotation1" textFormat="text/plain">
+      <text>请求被驳回后员工可以选择继续申请,或者取消本次申请</text>
+    </textAnnotation>
+    <association id="association1" sourceRef="modifyApply" targetRef="textannotation1"></association>
+  </process>
+  <bpmndi:BPMNDiagram id="BPMNDiagram_leave-formkey">
+    <bpmndi:BPMNPlane bpmnElement="leave-formkey" id="BPMNPlane_leave-formkey">
+      <bpmndi:BPMNShape bpmnElement="startevent1" id="BPMNShape_startevent1">
+        <omgdc:Bounds height="35.0" width="35.0" x="10.0" y="50.0"></omgdc:Bounds>
+      </bpmndi:BPMNShape>
+      <bpmndi:BPMNShape bpmnElement="deptLeaderVerify" id="BPMNShape_deptLeaderVerify">
+        <omgdc:Bounds height="55.0" width="105.0" x="90.0" y="40.0"></omgdc:Bounds>
+      </bpmndi:BPMNShape>
+      <bpmndi:BPMNShape bpmnElement="exclusivegateway1" id="BPMNShape_exclusivegateway1">
+        <omgdc:Bounds height="40.0" width="40.0" x="230.0" y="47.0"></omgdc:Bounds>
+      </bpmndi:BPMNShape>
+      <bpmndi:BPMNShape bpmnElement="hrVerify" id="BPMNShape_hrVerify">
+        <omgdc:Bounds height="55.0" width="105.0" x="310.0" y="40.0"></omgdc:Bounds>
+      </bpmndi:BPMNShape>
+      <bpmndi:BPMNShape bpmnElement="exclusivegateway2" id="BPMNShape_exclusivegateway2">
+        <omgdc:Bounds height="40.0" width="40.0" x="470.0" y="47.0"></omgdc:Bounds>
+      </bpmndi:BPMNShape>
+      <bpmndi:BPMNShape bpmnElement="reportBack" id="BPMNShape_reportBack">
+        <omgdc:Bounds height="55.0" width="105.0" x="580.0" y="40.0"></omgdc:Bounds>
+      </bpmndi:BPMNShape>
+      <bpmndi:BPMNShape bpmnElement="endevent1" id="BPMNShape_endevent1">
+        <omgdc:Bounds height="35.0" width="35.0" x="615.0" y="213.0"></omgdc:Bounds>
+      </bpmndi:BPMNShape>
+      <bpmndi:BPMNShape bpmnElement="modifyApply" id="BPMNShape_modifyApply">
+        <omgdc:Bounds height="55.0" width="105.0" x="198.0" y="120.0"></omgdc:Bounds>
+      </bpmndi:BPMNShape>
+      <bpmndi:BPMNShape bpmnElement="exclusivegateway3" id="BPMNShape_exclusivegateway3">
+        <omgdc:Bounds height="40.0" width="40.0" x="230.0" y="210.0"></omgdc:Bounds>
+      </bpmndi:BPMNShape>
+      <bpmndi:BPMNShape bpmnElement="textannotation1" id="BPMNShape_textannotation1">
+        <omgdc:Bounds height="57.0" width="112.0" x="340.0" y="168.0"></omgdc:Bounds>
+      </bpmndi:BPMNShape>
+      <bpmndi:BPMNEdge bpmnElement="flow1" id="BPMNEdge_flow1">
+        <omgdi:waypoint x="45.0" y="67.0"></omgdi:waypoint>
+        <omgdi:waypoint x="90.0" y="67.0"></omgdi:waypoint>
+      </bpmndi:BPMNEdge>
+      <bpmndi:BPMNEdge bpmnElement="flow2" id="BPMNEdge_flow2">
+        <omgdi:waypoint x="195.0" y="67.0"></omgdi:waypoint>
+        <omgdi:waypoint x="230.0" y="67.0"></omgdi:waypoint>
+      </bpmndi:BPMNEdge>
+      <bpmndi:BPMNEdge bpmnElement="flow3" id="BPMNEdge_flow3">
+        <omgdi:waypoint x="270.0" y="67.0"></omgdi:waypoint>
+        <omgdi:waypoint x="310.0" y="67.0"></omgdi:waypoint>
+        <bpmndi:BPMNLabel>
+          <omgdc:Bounds height="11.0" width="22.0" x="269.0" y="50.0"></omgdc:Bounds>
+        </bpmndi:BPMNLabel>
+      </bpmndi:BPMNEdge>
+      <bpmndi:BPMNEdge bpmnElement="flow4" id="BPMNEdge_flow4">
+        <omgdi:waypoint x="415.0" y="67.0"></omgdi:waypoint>
+        <omgdi:waypoint x="470.0" y="67.0"></omgdi:waypoint>
+      </bpmndi:BPMNEdge>
+      <bpmndi:BPMNEdge bpmnElement="flow5" id="BPMNEdge_flow5">
+        <omgdi:waypoint x="510.0" y="67.0"></omgdi:waypoint>
+        <omgdi:waypoint x="580.0" y="67.0"></omgdi:waypoint>
+        <bpmndi:BPMNLabel>
+          <omgdc:Bounds height="11.0" width="22.0" x="529.0" y="50.0"></omgdc:Bounds>
+        </bpmndi:BPMNLabel>
+      </bpmndi:BPMNEdge>
+      <bpmndi:BPMNEdge bpmnElement="flow6" id="BPMNEdge_flow6">
+        <omgdi:waypoint x="632.0" y="95.0"></omgdi:waypoint>
+        <omgdi:waypoint x="632.0" y="213.0"></omgdi:waypoint>
+      </bpmndi:BPMNEdge>
+      <bpmndi:BPMNEdge bpmnElement="flow7" id="BPMNEdge_flow7">
+        <omgdi:waypoint x="490.0" y="87.0"></omgdi:waypoint>
+        <omgdi:waypoint x="490.0" y="147.0"></omgdi:waypoint>
+        <omgdi:waypoint x="303.0" y="147.0"></omgdi:waypoint>
+        <bpmndi:BPMNLabel>
+          <omgdc:Bounds height="11.0" width="33.0" x="438.0" y="119.0"></omgdc:Bounds>
+        </bpmndi:BPMNLabel>
+      </bpmndi:BPMNEdge>
+      <bpmndi:BPMNEdge bpmnElement="flow8" id="BPMNEdge_flow8">
+        <omgdi:waypoint x="250.0" y="87.0"></omgdi:waypoint>
+        <omgdi:waypoint x="250.0" y="120.0"></omgdi:waypoint>
+        <bpmndi:BPMNLabel>
+          <omgdc:Bounds height="11.0" width="33.0" x="260.0" y="87.0"></omgdc:Bounds>
+        </bpmndi:BPMNLabel>
+      </bpmndi:BPMNEdge>
+      <bpmndi:BPMNEdge bpmnElement="flow9" id="BPMNEdge_flow9">
+        <omgdi:waypoint x="250.0" y="175.0"></omgdi:waypoint>
+        <omgdi:waypoint x="250.0" y="210.0"></omgdi:waypoint>
+      </bpmndi:BPMNEdge>
+      <bpmndi:BPMNEdge bpmnElement="flow10" id="BPMNEdge_flow10">
+        <omgdi:waypoint x="230.0" y="230.0"></omgdi:waypoint>
+        <omgdi:waypoint x="142.0" y="230.0"></omgdi:waypoint>
+        <omgdi:waypoint x="142.0" y="95.0"></omgdi:waypoint>
+        <bpmndi:BPMNLabel>
+          <omgdc:Bounds height="11.0" width="77.0" x="159.0" y="210.0"></omgdc:Bounds>
+        </bpmndi:BPMNLabel>
+      </bpmndi:BPMNEdge>
+      <bpmndi:BPMNEdge bpmnElement="flow11" id="BPMNEdge_flow11">
+        <omgdi:waypoint x="270.0" y="230.0"></omgdi:waypoint>
+        <omgdi:waypoint x="615.0" y="230.0"></omgdi:waypoint>
+        <bpmndi:BPMNLabel>
+          <omgdc:Bounds height="33.0" width="100.0" x="58.0" y="-37.0"></omgdc:Bounds>
+        </bpmndi:BPMNLabel>
+      </bpmndi:BPMNEdge>
+      <bpmndi:BPMNEdge bpmnElement="association1" id="BPMNEdge_association1">
+        <omgdi:waypoint x="303.0" y="147.0"></omgdi:waypoint>
+        <omgdi:waypoint x="396.0" y="168.0"></omgdi:waypoint>
+      </bpmndi:BPMNEdge>
+    </bpmndi:BPMNPlane>
+  </bpmndi:BPMNDiagram>
+</definitions>

+ 9 - 0
yudao-admin-ui/src/api/oa/flow.js

@@ -0,0 +1,9 @@
+import request from '@/utils/request'
+
+
+export function getStartForm(processKey) {
+  return request({
+    url: '/workflow/process/definition/getStartForm?processKey='+processKey,
+    method: 'get'
+  })
+}

+ 62 - 0
yudao-admin-ui/src/api/oa/leave.js

@@ -0,0 +1,62 @@
+import request from '@/utils/request'
+
+// 创建请假申请
+export function createLeave(data) {
+  return request({
+    url: '/oa/leave/create',
+    method: 'post',
+    data: data
+  })
+}
+
+// 更新请假申请
+export function updateLeave(data) {
+  return request({
+    url: '/oa/leave/update',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除请假申请
+export function deleteLeave(id) {
+  return request({
+    url: '/oa/leave/delete?id=' + id,
+    method: 'delete'
+  })
+}
+
+// 获得请假申请
+export function getLeave(id) {
+  return request({
+    url: '/oa/leave/get?id=' + id,
+    method: 'get'
+  })
+}
+
+// 获得请假申请分页
+export function getLeavePage(query) {
+  return request({
+    url: '/oa/leave/page',
+    method: 'get',
+    params: query
+  })
+}
+
+export function createFormKeyLeave(data) {
+  return request({
+    url: '/oa/leave/form-key/create',
+    method: 'post',
+    data: data
+  })
+}
+
+// 导出请假申请 Excel
+export function exportLeaveExcel(query) {
+  return request({
+    url: '/oa/leave/export-excel',
+    method: 'get',
+    params: query,
+    responseType: 'blob'
+  })
+}

+ 83 - 0
yudao-admin-ui/src/api/oa/todo.js

@@ -0,0 +1,83 @@
+import request from '@/utils/request'
+
+// 创建请假申请
+export function createLeave(data) {
+  return request({
+    url: '/oa/leave/create',
+    method: 'post',
+    data: data
+  })
+}
+
+// 更新请假申请
+export function updateLeave(data) {
+  return request({
+    url: '/oa/leave/update',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除请假申请
+export function deleteLeave(id) {
+  return request({
+    url: '/oa/leave/delete?id=' + id,
+    method: 'delete'
+  })
+}
+
+// 获得请假申请
+export function getLeave(id) {
+  return request({
+    url: '/oa/leave/get?id=' + id,
+    method: 'get'
+  })
+}
+
+// 获得待办任务分页
+export function getTodoTaskPage(query) {
+  return request({
+    url: '/workflow/task/todo/page',
+    method: 'get',
+    params: query
+  })
+}
+
+// 签收任务
+export function claimTask(id) {
+  return request({
+    url: '/workflow/task/claim?id=' + id,
+    method: 'get'
+  })
+}
+
+export function completeTask(data) {
+  return request({
+    url: '/workflow/task/complete',
+    method: 'post',
+    data: data
+  })
+}
+
+export function taskSteps(data) {
+  return request({
+    url: '/workflow/task/task-steps',
+    method: 'post',
+    data: data
+  })
+}
+
+export function getTaskFormKey(data) {
+  return request({
+    url: '/workflow/task/formKey',
+    method: 'post',
+    data: data
+  })
+}
+
+export function processHistorySteps(id) {
+  return request({
+    url: '/workflow/task/process/history-steps?id='+id,
+    method: 'get'
+  })
+}

+ 70 - 0
yudao-admin-ui/src/router/index.js

@@ -123,6 +123,76 @@ export const constantRoutes = [
         meta: { title: '修改生成配置' }
       }
     ]
+  },
+  {
+    path: '/flow',
+    component: Layout,
+    hidden: true,
+    redirect: 'noredirect',
+    children: [
+      {
+        path: 'leave/apply',
+        component: (resolve) => require(['@/views/oa/leave/apply/index'], resolve),
+        name: '请假表单',
+        meta: { title: '请假表单', icon: 'form' }
+      }
+    ]
+  },
+  {
+    path: '/flow',
+    component: Layout,
+    hidden: true,
+    redirect: 'noredirect',
+    children: [
+      {
+        path: 'leave/approve-leader',
+        component: (resolve) => require(['@/views/oa/leave/approve-leader/index'], resolve),
+        name: '请假表单-部门领导审批',
+        meta: { title: '请假表单-部门领导审批', icon: 'form' }
+      }
+    ]
+  },
+  {
+    path: '/flow',
+    component: Layout,
+    hidden: true,
+    redirect: 'noredirect',
+    children: [
+      {
+        path: 'leave/approve-hr',
+        component: (resolve) => require(['@/views/oa/leave/approve-hr/index'], resolve),
+        name: '请假表单-人事审批',
+        meta: { title: '请假表单-人事审批', icon: 'form' }
+      }
+    ]
+  },
+  {
+    path: '/flow',
+    component: Layout,
+    hidden: true,
+    redirect: 'noredirect',
+    children: [
+      {
+        path: 'leave/confirm',
+        component: (resolve) => require(['@/views/oa/leave/confirm/index'], resolve),
+        name: '请假表单-确认',
+        meta: { title: '请假表单-确认', icon: 'form' }
+      }
+    ]
+  },
+  {
+    path: '/flow',
+    component: Layout,
+    hidden: true,
+    redirect: 'noredirect',
+    children: [
+      {
+        path: 'leave/modify',
+        component: (resolve) => require(['@/views/oa/leave/modify/index'], resolve),
+        name: '请假表单-修改',
+        meta: { title: '请假表单-修改', icon: 'form' }
+      }
+    ]
   }
 ]
 

+ 3 - 0
yudao-admin-ui/src/utils/dict.js

@@ -30,6 +30,9 @@ export const DICT_TYPE = {
   INF_API_ERROR_LOG_PROCESS_STATUS: 'inf_api_error_log_process_status',
 
   TOOL_CODEGEN_TEMPLATE_TYPE: 'tool_codegen_template_type',
+
+  OA_LEAVE_STATUS: 'oa_leave_status',
+  OA_LEAVE_TYPE: 'oa_leave_type'
 }
 
 /**

+ 36 - 0
yudao-admin-ui/src/views/oa/flow/index.vue

@@ -0,0 +1,36 @@
+<template>
+  <div class="app-container">
+    <el-row>
+      <el-button type="primary" v-on:click="leave">请假申请</el-button>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import { getStartForm } from "@/api/oa/flow";
+export default {
+  name: "Flow",
+  components: {
+  },
+  data() {
+    return {
+      // 表单参数
+      form: {},
+
+      rules: {
+      }
+    };
+  },
+  created() {
+
+  },
+  methods: {
+    leave() {
+      getStartForm('leave-formkey').then(response => {
+        const route = response.data;
+        this.$router.replace(route);
+      });
+    }
+  }
+};
+</script>

+ 93 - 0
yudao-admin-ui/src/views/oa/leave/apply/index.vue

@@ -0,0 +1,93 @@
+<template>
+  <div class="app-container">
+    <!-- 对话框(添加 / 修改) -->
+      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
+        <el-form-item label="开始时间" prop="startTime">
+          <el-date-picker clearable size="small" v-model="form.startTime" type="date" value-format="timestamp" placeholder="选择开始时间" />
+        </el-form-item>
+        <el-form-item label="结束时间" prop="endTime">
+          <el-date-picker clearable size="small" v-model="form.endTime" type="date" value-format="timestamp" placeholder="选择结束时间" />
+        </el-form-item>
+        <el-form-item label="请假类型" prop="leaveType">
+          <el-select v-model="form.leaveType" placeholder="请选择">
+            <el-option
+              v-for="dict in leaveTypeDictData"
+              :key="parseInt(dict.value)"
+              :label="dict.label"
+              :value="parseInt(dict.value)"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="原因" prop="reason">
+          <el-col :span="10">
+            <el-input
+              type="textarea"
+              :rows="3"
+              v-model="form.reason"
+              placeholder="请输入原因" />
+          </el-col>
+        </el-form-item>
+        <el-form-item label="申请时间" prop="applyTime">
+          <el-date-picker clearable size="small" v-model="form.applyTime" type="date" value-format="timestamp" placeholder="选择申请时间" />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="submitForm">确 定</el-button>
+        </el-form-item>
+      </el-form>
+  </div>
+</template>
+
+<script>
+import { createFormKeyLeave } from "@/api/oa/leave"
+import { getDictDataLabel, getDictDatas, DICT_TYPE } from '@/utils/dict'
+export default {
+  name: "ApplyLeave",
+  components: {
+  },
+  data() {
+    return {
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {
+        startTime: [{ required: true, message: "开始时间不能为空", trigger: "blur" }],
+        endTime: [{ required: true, message: "结束时间不能为空", trigger: "blur" }],
+        applyTime: [{ required: true, message: "申请时间不能为空", trigger: "blur" }],
+      },
+      statusFormat(row, column) {
+        return getDictDataLabel(DICT_TYPE.OA_LEAVE_STATUS, row.status)
+      },
+      leaveTypeDictData: getDictDatas(DICT_TYPE.OA_LEAVE_TYPE),
+      leaveStatusData: getDictDatas(DICT_TYPE.OA_LEAVE_STATUS)
+    };
+  },
+  created() {
+  },
+  methods: {
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (!valid) {
+          return;
+        }
+        // 修改的提交
+        // if (this.form.id != null) {
+        //   updateLeave(this.form).then(response => {
+        //     this.msgSuccess("修改成功");
+        //   });
+        //   return;
+        // }
+        // 添加的提交
+        createFormKeyLeave(this.form).then(response => {
+          this.msgSuccess("新增成功");
+          this.$store.dispatch('tagsView/delView', this.$route).then(({ visitedViews }) => {
+            //if (this.isActive(this.$route)) {
+            this.$router.push({path: '/oa/todo'})
+            //}
+          })
+        });
+      });
+    }
+  }
+};
+</script>

+ 190 - 0
yudao-admin-ui/src/views/oa/leave/approve-hr/index.vue

@@ -0,0 +1,190 @@
+<template>
+  <div class="app-container">
+    <el-tabs type="border-card">
+      <el-tab-pane label="任务处理">
+        <el-form ref="form" :model="form"  label-width="80px">
+          <el-row :gutter="20">
+            <el-col :span="6"><el-form-item label="申请人" >{{form.userId}}</el-form-item></el-col>
+            <el-col :span="6">
+              <el-form-item label="请假类型" prop="leaveType">
+                {{ getDictDataLabel(DICT_TYPE.OA_LEAVE_TYPE, form.leaveType) }}
+              </el-form-item>
+            </el-col>
+            <el-col :span="6"><el-form-item label="原因" prop="reason">{{form.reason}}</el-form-item></el-col>
+          </el-row>
+          <el-row :gutter="20">
+            <el-col :span="6"><el-form-item label="开始时间" >{{ parseTime(form.startTime) }}</el-form-item></el-col>
+            <el-col :span="6"><el-form-item label="结束时间" prop="endTime">{{ parseTime(form.endTime) }}</el-form-item></el-col>
+            <el-form-item label="申请时间" prop="applyTime">{{ parseTime(form.applyTime) }}</el-form-item>
+          </el-row>
+        </el-form>
+        <el-divider></el-divider>
+        <el-form ref="taskForm" :model="leaveApprove" :rules="rules"  label-width="80px">
+          <el-form-item label="是否同意" prop="approved">
+            <el-select v-model="leaveApprove.approved" placeholder="是否同意" v-on:change="approveChange">
+              <el-option
+                v-for="dict in approvedData"
+                :key="parseInt(dict.value)"
+                :label="dict.label"
+                :value="parseInt(dict.value)"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="处理意见" prop="comment">
+            <el-col :span="11">
+              <el-input
+                type="textarea"
+                :rows="3"
+                v-model="leaveApprove.comment">
+              </el-input>
+            </el-col>
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" @click="submitForm">确 定</el-button>
+          </el-form-item>
+        </el-form>
+      </el-tab-pane>
+      <el-tab-pane label="历史跟踪">
+        <el-steps :active="stepActive" simple finish-status="success">
+          <el-step :title="stepTitle(item)" icon="el-icon-edit" v-for="(item) in handleTask.historyTask" ></el-step>
+        </el-steps>
+        <br/>
+        <el-steps direction="vertical" :active="stepActive"  space="65px">
+          <el-step :title="stepTitle(item)" :description="stepDes(item)" v-for="(item) in handleTask.historyTask" ></el-step>
+        </el-steps>
+      </el-tab-pane>
+      <el-tab-pane label="流程图">流程图-TODO</el-tab-pane>
+    </el-tabs>
+
+  </div>
+</template>
+
+<script>
+import { getLeave } from "@/api/oa/leave"
+import { completeTask,taskSteps } from "@/api/oa/todo";
+import { getDictDataLabel, getDictDatas, DICT_TYPE } from '@/utils/dict'
+export default {
+  name: "HrApproveLeave",
+  components: {
+  },
+  data() {
+    return {
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {
+        comment: [{ required: true, message: "意见不能为空", trigger: "blur" }]
+      },
+      handleTask: {
+        historyTask:[]
+      },
+      leaveApprove: {
+        approved : 1,
+        variables: {},
+        taskId: "",
+        comment: "同意"
+      },
+      approvedData: [
+        {
+          value: 1,
+          label: '同意'
+        },
+        {
+          value: 0,
+          label: '不同意'
+        }
+      ],
+      statusFormat(row, column) {
+        return getDictDataLabel(DICT_TYPE.OA_LEAVE_STATUS, row.status)
+      },
+      leaveTypeDictData: getDictDatas(DICT_TYPE.OA_LEAVE_TYPE),
+      leaveStatusData: getDictDatas(DICT_TYPE.OA_LEAVE_STATUS)
+    };
+  },
+  computed:{
+     stepActive: function () {
+         let idx = 0;
+         for(let i=0; i<this.handleTask.historyTask.length; i++){
+           if(this.handleTask.historyTask[i].status === 1){
+             idx= idx+1;
+           }else{
+             break;
+           }
+         }
+         return idx;
+     },
+    stepTitle() {
+      return function (item) {
+        let name = item.stepName;
+        if (item.status === 1) {
+          name += '(已完成)'
+        }
+        if (item.status === 0) {
+          name += '(进行中)'
+        }
+        return name;
+      }
+    },
+    stepDes(){
+      return function (item) {
+        let desc = "";
+        if (item.status === 1) {
+          desc+="审批人:["+ item.assignee +"]    审批意见: [" + item.comment + "]   审批时间: " + this.parseTime(item.endTime);
+        }
+        return desc;
+      }
+    }
+  },
+  created() {
+    const businessKey = this.$route.query.businessKey;
+    const taskId = this.$route.query.taskId;
+    this.leaveApprove.taskId = taskId;
+    this.getForm(businessKey);
+  },
+  methods: {
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["taskForm"].validate(valid => {
+        if (!valid) {
+          return;
+        }
+        if (this.leaveApprove.approved == 1) {
+          this.leaveApprove.variables["hrApproved"] = true;
+        }
+        if (this.leaveApprove.approved == 0) {
+          this.leaveApprove.variables["hrApproved"] = false;
+        }
+        completeTask(this.leaveApprove).then(response => {
+          this.msgSuccess("执行任务成功");
+          this.$store.dispatch('tagsView/delView', this.$route).then(({ visitedViews }) => {
+            //if (this.isActive(this.$route)) {
+            this.$router.push({path: '/oa/todo'})
+            //}
+          })
+        })
+      });
+    },
+    getForm(id){
+      getLeave(id).then(response => {
+        this.form = response.data;
+      });
+      const data = {
+        taskId : this.leaveApprove.taskId,
+        businessKey: id,
+      }
+      taskSteps(data).then(response => {
+        this.handleTask = response.data;
+
+      });
+    },
+    approveChange(){
+      if (this.leaveApprove.approved === 1) {
+        this.leaveApprove.comment = "同意"
+      }
+      if(this.leaveApprove.approved === 0){
+        this.leaveApprove.comment = "不同意"
+      }
+    }
+  }
+};
+</script>

+ 190 - 0
yudao-admin-ui/src/views/oa/leave/approve-leader/index.vue

@@ -0,0 +1,190 @@
+<template>
+  <div class="app-container">
+    <el-tabs type="border-card">
+      <el-tab-pane label="任务处理">
+        <el-form ref="form" :model="form"  label-width="80px">
+          <el-row :gutter="20">
+            <el-col :span="6"><el-form-item label="申请人" >{{form.userId}}</el-form-item></el-col>
+            <el-col :span="6">
+              <el-form-item label="请假类型" prop="leaveType">
+                {{ getDictDataLabel(DICT_TYPE.OA_LEAVE_TYPE, form.leaveType) }}
+              </el-form-item>
+            </el-col>
+            <el-col :span="6"><el-form-item label="原因" prop="reason">{{form.reason}}</el-form-item></el-col>
+          </el-row>
+          <el-row :gutter="20">
+            <el-col :span="6"><el-form-item label="开始时间" >{{ parseTime(form.startTime) }}</el-form-item></el-col>
+            <el-col :span="6"><el-form-item label="结束时间" prop="endTime">{{ parseTime(form.endTime) }}</el-form-item></el-col>
+            <el-form-item label="申请时间" prop="applyTime">{{ parseTime(form.applyTime) }}</el-form-item>
+          </el-row>
+        </el-form>
+        <el-divider></el-divider>
+        <el-form ref="taskForm" :model="leaveApprove" :rules="rules"  label-width="80px">
+          <el-form-item label="是否同意" prop="approved">
+            <el-select v-model="leaveApprove.approved" placeholder="是否同意" v-on:change="approveChange">
+              <el-option
+                v-for="dict in approvedData"
+                :key="parseInt(dict.value)"
+                :label="dict.label"
+                :value="parseInt(dict.value)"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="处理意见" prop="comment">
+            <el-col :span="11">
+              <el-input
+                type="textarea"
+                :rows="3"
+                v-model="leaveApprove.comment">
+              </el-input>
+            </el-col>
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" @click="submitForm">确 定</el-button>
+          </el-form-item>
+        </el-form>
+      </el-tab-pane>
+      <el-tab-pane label="历史跟踪">
+        <el-steps :active="stepActive" simple finish-status="success">
+          <el-step :title="stepTitle(item)" icon="el-icon-edit" v-for="(item) in handleTask.historyTask" ></el-step>
+        </el-steps>
+        <br/>
+        <el-steps direction="vertical" :active="stepActive"  space="65px">
+          <el-step :title="stepTitle(item)" :description="stepDes(item)" v-for="(item) in handleTask.historyTask" ></el-step>
+        </el-steps>
+      </el-tab-pane>
+      <el-tab-pane label="流程图">流程图-TODO</el-tab-pane>
+    </el-tabs>
+  </div>
+</template>
+
+<script>
+import { getLeave } from "@/api/oa/leave"
+import { completeTask,taskSteps } from "@/api/oa/todo";
+import { getDictDataLabel, getDictDatas, DICT_TYPE } from '@/utils/dict'
+export default {
+  name: "ApproveLeaderLeave",
+  components: {
+  },
+  data() {
+    return {
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {
+        comment: [{ required: true, message: "意见不能为空", trigger: "blur" }]
+      },
+      handleTask: {
+        historyTask:[]
+      },
+      leaveApprove: {
+        approved : 1,
+        variables: {},
+        taskId: "",
+        comment: "同意"
+      },
+      approvedData: [
+        {
+          value: 1,
+          label: '同意'
+        },
+        {
+          value: 0,
+          label: '不同意'
+        }
+      ],
+      statusFormat(row, column) {
+        return getDictDataLabel(DICT_TYPE.OA_LEAVE_STATUS, row.status)
+      },
+      leaveTypeDictData: getDictDatas(DICT_TYPE.OA_LEAVE_TYPE),
+      leaveStatusData: getDictDatas(DICT_TYPE.OA_LEAVE_STATUS)
+    };
+  },
+  created() {
+    const businessKey = this.$route.query.businessKey;
+    const taskId = this.$route.query.taskId;
+    this.leaveApprove.taskId = taskId;
+    this.getForm(businessKey);
+  },
+  computed:{
+    stepActive: function () {
+      let idx = 0;
+      for(let i=0; i<this.handleTask.historyTask.length; i++){
+        if(this.handleTask.historyTask[i].status === 1){
+          idx= idx+1;
+        }else{
+          break;
+        }
+      }
+      return idx;
+    },
+    stepTitle() {
+      return function (item) {
+        let name = item.stepName;
+        if (item.status === 1) {
+          name += '(已完成)'
+        }
+        if (item.status === 0) {
+          name += '(进行中)'
+        }
+        return name;
+      }
+    },
+    stepDes(){
+      return function (item) {
+        let desc = "";
+        if (item.status === 1) {
+          desc+="审批人:["+ item.assignee +"]    审批意见: [" + item.comment + "]   审批时间: " + this.parseTime(item.endTime);
+        }
+        return desc;
+      }
+    }
+  },
+  methods: {
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["taskForm"].validate(valid => {
+        if (!valid) {
+          return;
+        }
+        if (this.leaveApprove.approved == 1) {
+          this.leaveApprove.variables["deptLeaderApproved"] = true;
+        }
+        if (this.leaveApprove.approved == 0) {
+          this.leaveApprove.variables["deptLeaderApproved"] = false;
+        }
+        completeTask(this.leaveApprove).then(response => {
+          this.msgSuccess("执行任务成功");
+          this.$store.dispatch('tagsView/delView', this.$route).then(({ visitedViews }) => {
+            //if (this.isActive(this.$route)) {
+            this.$router.push({path: '/oa/todo'})
+            //}
+          })
+        });
+      });
+
+    },
+    getForm(id){
+      getLeave(id).then(response => {
+        this.form = response.data;
+      });
+      const data = {
+        taskId : this.leaveApprove.taskId,
+        businessKey: id,
+      }
+      taskSteps(data).then(response => {
+        this.handleTask = response.data;
+
+      });
+    },
+    approveChange(){
+      if (this.leaveApprove.approved === 1) {
+        this.leaveApprove.comment = "同意"
+      }
+      if(this.leaveApprove.approved === 0){
+        this.leaveApprove.comment = "不同意"
+      }
+    }
+  }
+};
+</script>

+ 137 - 0
yudao-admin-ui/src/views/oa/leave/confirm/index.vue

@@ -0,0 +1,137 @@
+<template>
+  <div class="app-container">
+    <el-tabs type="border-card">
+      <el-tab-pane label="任务处理">
+        <el-form ref="form" :model="form"  label-width="80px">
+          <el-row :gutter="20">
+            <el-col :span="6"><el-form-item label="申请人" >{{form.userId}}</el-form-item></el-col>
+            <el-col :span="6">
+              <el-form-item label="请假类型" prop="leaveType">
+                {{ getDictDataLabel(DICT_TYPE.OA_LEAVE_TYPE, form.leaveType) }}
+              </el-form-item>
+            </el-col>
+            <el-col :span="6"><el-form-item label="原因" prop="reason">{{form.reason}}</el-form-item></el-col>
+          </el-row>
+          <el-row :gutter="20">
+            <el-col :span="6"><el-form-item label="开始时间" >{{ parseTime(form.startTime) }}</el-form-item></el-col>
+            <el-col :span="6"><el-form-item label="结束时间" prop="endTime">{{ parseTime(form.endTime) }}</el-form-item></el-col>
+            <el-form-item label="申请时间" prop="applyTime">{{ parseTime(form.applyTime) }}</el-form-item>
+          </el-row>
+          <el-row :gutter="20">
+            <el-col :span="6"><el-button type="primary" @click="submitForm">确 定</el-button></el-col>
+          </el-row>
+        </el-form>
+      </el-tab-pane>
+      <el-tab-pane label="历史跟踪">
+        <el-steps :active="stepActive" simple finish-status="success">
+          <el-step :title="stepTitle(item)" icon="el-icon-edit" v-for="(item) in handleTask.historyTask" ></el-step>
+        </el-steps>
+        <br/>
+        <el-steps direction="vertical" :active="stepActive"  space="65px">
+          <el-step :title="stepTitle(item)" :description="stepDes(item)" v-for="(item) in handleTask.historyTask" ></el-step>
+        </el-steps>
+      </el-tab-pane>
+      <el-tab-pane label="流程图">流程图-TODO</el-tab-pane>
+    </el-tabs>
+  </div>
+</template>
+
+<script>
+import { getLeave } from "@/api/oa/leave"
+import { completeTask,taskSteps } from "@/api/oa/todo";
+import { getDictDataLabel, getDictDatas, DICT_TYPE } from '@/utils/dict'
+export default {
+  name: "ConfirmLeave",
+  components: {
+  },
+  data() {
+    return {
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {
+      },
+      handleTask: {
+        historyTask:[]
+      },
+      leaveApprove: {
+        variables: {},
+        taskId: "",
+        comment: ""
+      },
+      statusFormat(row, column) {
+        return getDictDataLabel(DICT_TYPE.OA_LEAVE_STATUS, row.status)
+      },
+      leaveTypeDictData: getDictDatas(DICT_TYPE.OA_LEAVE_TYPE),
+      leaveStatusData: getDictDatas(DICT_TYPE.OA_LEAVE_STATUS)
+    };
+  },
+  created() {
+    const businessKey = this.$route.query.businessKey;
+    const taskId = this.$route.query.taskId;
+    this.leaveApprove.taskId = taskId;
+    this.getForm(businessKey);
+  },
+  computed:{
+    stepActive: function () {
+      let idx = 0;
+      for(let i=0; i<this.handleTask.historyTask.length; i++){
+        if(this.handleTask.historyTask[i].status === 1){
+          idx= idx+1;
+        }else{
+          break;
+        }
+      }
+      return idx;
+    },
+    stepTitle() {
+      return function (item) {
+        let name = item.stepName;
+        if (item.status === 1) {
+          name += '(已完成)'
+        }
+        if (item.status === 0) {
+          name += '(进行中)'
+        }
+        return name;
+      }
+    },
+    stepDes(){
+      return function (item) {
+        let desc = "";
+        if (item.status === 1) {
+          desc+="审批人:["+ item.assignee +"]    审批意见: [" + item.comment + "]   审批时间: " + this.parseTime(item.endTime);
+        }
+        return desc;
+      }
+    }
+  },
+  methods: {
+    /** 提交按钮 */
+    submitForm() {
+      completeTask(this.leaveApprove).then(response => {
+        this.msgSuccess("执行任务成功");
+        this.$store.dispatch('tagsView/delView', this.$route).then(({ visitedViews }) => {
+          //if (this.isActive(this.$route)) {
+          this.$router.push({path: '/oa/todo'})
+          //}
+        })
+      })
+    },
+    getForm(id){
+      getLeave(id).then(response => {
+        this.form = response.data;
+      });
+      const data = {
+        taskId : this.leaveApprove.taskId,
+        businessKey: id,
+      }
+      taskSteps(data).then(response => {
+        this.handleTask = response.data;
+
+      });
+
+    }
+  }
+};
+</script>

+ 377 - 0
yudao-admin-ui/src/views/oa/leave/index.vue

@@ -0,0 +1,377 @@
+<template>
+  <div class="app-container">
+
+    <!-- 搜索工作栏 -->
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
+      <el-form-item label="流程id" prop="processInstanceId">
+        <el-input v-model="queryParams.processInstanceId" placeholder="请输入流程id" clearable size="small" @keyup.enter.native="handleQuery"/>
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-select v-model="queryParams.status" placeholder="请选择状态">
+          <el-option
+            v-for="dict in leaveStatusData"
+            :key="parseInt(dict.value)"
+            :label="dict.label"
+            :value="parseInt(dict.value)"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="开始时间">
+        <el-date-picker v-model="dateRangeStartTime" size="small" style="width: 240px" value-format="yyyy-MM-dd"
+                        type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期" />
+      </el-form-item>
+      <el-form-item label="结束时间">
+        <el-date-picker v-model="dateRangeEndTime" size="small" style="width: 240px" value-format="yyyy-MM-dd"
+                        type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期" />
+      </el-form-item>
+      <el-form-item label="请假类型" prop="leaveType">
+        <el-select v-model="queryParams.leaveType" placeholder="请选择请假类型">
+          <el-option
+            v-for="dict in leaveTypeDictData"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="原因" prop="reason">
+        <el-input v-model="queryParams.reason" placeholder="请输入原因" clearable size="small" @keyup.enter.native="handleQuery"/>
+      </el-form-item>
+      <el-form-item label="申请时间">
+        <el-date-picker v-model="dateRangeApplyTime" size="small" style="width: 240px" value-format="yyyy-MM-dd"
+                        type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期" />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <!-- 操作工具栏 -->
+    <el-row :gutter="10" class="mb8">
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <!-- 列表 -->
+    <el-table v-loading="loading" :data="list">
+      <el-table-column label="请假表单主键" align="center" prop="id" />
+      <el-table-column label="状态" align="center" prop="status" :formatter="statusFormat" />
+      <el-table-column label="申请人id" align="center" prop="userId" />
+      <el-table-column label="开始时间" align="center" prop="startTime" width="180">
+        <template slot-scope="scope">
+          <span>{{ parseTime(scope.row.startTime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="结束时间" align="center" prop="endTime" width="180">
+        <template slot-scope="scope">
+          <span>{{ parseTime(scope.row.endTime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="请假类型" align="center" prop="leaveType" :formatter="leaveTypeFormat" />
+      <el-table-column label="原因" align="center" prop="reason" />
+      <el-table-column label="申请时间" align="center" prop="applyTime" width="180">
+        <template slot-scope="scope">
+          <span>{{ parseTime(scope.row.applyTime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template slot-scope="scope">
+          <el-button size="mini" type="text" icon="el-icon-edit" @click="handleStep(scope.row)">审批进度</el-button>
+          <el-button size="mini" type="text" icon="el-icon-edit" @click="handleDetail(scope.row)">详情</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页组件 -->
+    <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
+                @pagination="getList"/>
+
+    <!-- 对话框(添加 / 修改) -->
+    <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
+        <el-form-item label="开始时间" prop="startTime">
+          <el-date-picker clearable size="small" v-model="form.startTime" type="date" value-format="timestamp" placeholder="选择开始时间" />
+        </el-form-item>
+        <el-form-item label="结束时间" prop="endTime">
+          <el-date-picker clearable size="small" v-model="form.endTime" type="date" value-format="timestamp" placeholder="选择结束时间" />
+        </el-form-item>
+        <el-form-item label="请假类型" prop="leaveType">
+          <el-select v-model="form.leaveType" placeholder="请选择">
+            <el-option
+              v-for="dict in leaveTypeDictData"
+              :key="dict.value"
+              :label="dict.label"
+              :value="dict.value"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="原因" prop="reason">
+          <el-input v-model="form.reason" placeholder="请输入原因" />
+        </el-form-item>
+        <el-form-item label="申请时间" prop="applyTime">
+          <el-date-picker clearable size="small" v-model="form.applyTime" type="date" value-format="timestamp" placeholder="选择申请时间" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </el-dialog>
+
+    <el-dialog :title="title" :visible.sync="dialogDetailVisible" width="500px" append-to-body>
+      <el-form ref="form" :model="form"  label-width="80px">
+      <el-form-item label="状态" >
+        {{ getDictDataLabel(DICT_TYPE.OA_LEAVE_STATUS, form.status) }}
+      </el-form-item>
+      <el-form-item label="申请人id" >{{form.userId}}</el-form-item>
+      <el-form-item label="开始时间" >{{ parseTime(form.startTime) }}</el-form-item>
+      <el-form-item label="结束时间" prop="endTime">{{ parseTime(form.endTime) }}</el-form-item>
+      <el-form-item label="请假类型" prop="leaveType">
+        {{ getDictDataLabel(DICT_TYPE.OA_LEAVE_TYPE, form.leaveType) }}
+      </el-form-item>
+      <el-form-item label="原因" prop="reason">{{form.reason}}</el-form-item>
+      <el-form-item label="申请时间" prop="applyTime">{{ parseTime(form.applyTime) }}</el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="dialogDetailVisible = false">确 定</el-button>
+        <el-button @click="dialogDetailVisible = false">取 消</el-button>
+      </div>
+    </el-dialog>
+
+    <el-dialog :title="title" :visible.sync="dialogStepsVisible" width="600px" append-to-body>
+      <el-steps :active="stepActive" finish-status="success" >
+        <el-step :title="stepTitle(item)" :description="' 办理人:' + item.assignee " icon="el-icon-edit"  v-for="(item) in handleTask.historyTask"></el-step>
+      </el-steps>
+      <br/>
+      <el-steps direction="vertical" :active="stepActive">
+        <el-step :title="stepTitle(item)" :description="stepDes(item)" v-for="(item) in handleTask.historyTask"></el-step>
+      </el-steps>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="dialogStepsVisible = false">确 定</el-button>
+        <el-button @click="dialogStepsVisible = false">取 消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { createLeave, updateLeave, deleteLeave, getLeave, getLeavePage, exportLeaveExcel } from "@/api/oa/leave"
+import { getDictDataLabel, getDictDatas, DICT_TYPE } from '@/utils/dict'
+import { processHistorySteps } from '@/api/oa/todo'
+export default {
+  name: "Leave",
+  components: {
+  },
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 请假申请列表
+      list: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      //进度弹出层
+      dialogDetailVisible: false,
+      //审批进度弹出层
+      dialogStepsVisible: false,
+      dateRangeStartTime: [],
+      dateRangeEndTime: [],
+      dateRangeApplyTime: [],
+      // 查询参数
+      queryParams: {
+        pageNo: 1,
+        pageSize: 10,
+        processInstanceId: null,
+        status: null,
+        userId: null,
+        leaveType: null,
+        reason: null,
+      },
+      // 表单参数
+      form: {},
+      handleTask: {
+        historyTask:[],
+        taskVariable: "",
+        formObject: {}
+      },
+      steps:[{
+        stepName:"步骤一"
+      }],
+      // 表单校验
+      rules: {
+        startTime: [{ required: true, message: "开始时间不能为空", trigger: "blur" }],
+        endTime: [{ required: true, message: "结束时间不能为空", trigger: "blur" }],
+        applyTime: [{ required: true, message: "申请时间不能为空", trigger: "blur" }],
+      },
+      statusFormat(row, column) {
+        return getDictDataLabel(DICT_TYPE.OA_LEAVE_STATUS, row.status)
+      },
+      leaveTypeFormat(row, column) {
+        return getDictDataLabel(DICT_TYPE.OA_LEAVE_TYPE, row.leaveType)
+      },
+      leaveTypeDictData: getDictDatas(DICT_TYPE.OA_LEAVE_TYPE),
+      leaveStatusData: getDictDatas(DICT_TYPE.OA_LEAVE_STATUS)
+    };
+  },
+  created() {
+    this.getList();
+  },
+  computed: {
+    stepActive: function () {
+      let idx = 0;
+      for (let i = 0; i < this.handleTask.historyTask.length; i++) {
+        if (this.handleTask.historyTask[i].status === 1) {
+          idx = idx + 1;
+        } else {
+          break;
+        }
+      }
+      return idx;
+    },
+    stepTitle() {
+      return function (item) {
+        let name = item.stepName;
+        if (item.status === 1) {
+          name += '(已完成)'
+        }
+        if (item.status === 0) {
+          name += '(进行中)'
+        }
+        return name;
+      }
+    },
+    stepDes() {
+      return function (item) {
+        let desc = "";
+        if (item.status === 1) {
+          desc += "审批人:[" + item.assignee + "]    审批意见: [" + item.comment + "]   审批时间: " + this.parseTime(item.endTime);
+        }
+        return desc;
+      }
+    }
+  },
+  methods: {
+    /** 查询列表 */
+    getList() {
+      this.loading = true;
+      // 处理查询参数
+      let params = {...this.queryParams};
+      this.addBeginAndEndTime(params, this.dateRangeStartTime, 'startTime');
+      this.addBeginAndEndTime(params, this.dateRangeEndTime, 'endTime');
+      this.addBeginAndEndTime(params, this.dateRangeApplyTime, 'applyTime');
+      // 执行查询
+      getLeavePage(params).then(response => {
+        this.list = response.data.list;
+        this.total = response.data.total;
+        this.loading = false;
+      });
+    },
+    /** 取消按钮 */
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    /** 表单重置 */
+    reset() {
+      this.form = {
+        id: undefined,
+        processInstanceId: undefined,
+        status: undefined,
+        userId: undefined,
+        startTime: undefined,
+        endTime: undefined,
+        leaveType: undefined,
+        reason: undefined,
+        applyTime: undefined,
+      };
+      this.resetForm("form");
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNo = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.dateRangeStartTime = [];
+      this.dateRangeEndTime = [];
+      this.dateRangeApplyTime = [];
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.reset();
+      this.open = true;
+      this.title = "添加请假申请";
+    },
+    /** 详情按钮操作 */
+    handleDetail(row) {
+      this.reset();
+      const id = row.id;
+      getLeave(id).then(response => {
+        this.form = response.data;
+        this.dialogDetailVisible = true
+        this.title = "请假详情";
+      });
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (!valid) {
+          return;
+        }
+        // 修改的提交
+        if (this.form.id != null) {
+          updateLeave(this.form).then(response => {
+            this.msgSuccess("修改成功");
+            this.open = false;
+            this.getList();
+          });
+          return;
+        }
+        // 添加的提交
+        createLeave(this.form).then(response => {
+          this.msgSuccess("新增成功");
+          this.open = false;
+          this.getList();
+        });
+      });
+    },
+    /** 审批进度 */
+    handleStep(row) {
+      const id = row.processInstanceId;
+      processHistorySteps(id).then(response => {
+        this.handleTask.historyTask = response.data;
+        this.dialogStepsVisible = true
+        this.title = "审批进度";
+      });
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      // 处理查询参数
+      let params = {...this.queryParams};
+      params.pageNo = undefined;
+      params.pageSize = undefined;
+      this.addBeginAndEndTime(params, this.dateRangeStartTime, 'startTime');
+      this.addBeginAndEndTime(params, this.dateRangeEndTime, 'endTime');
+      this.addBeginAndEndTime(params, this.dateRangeApplyTime, 'applyTime');
+      // 执行导出
+      this.$confirm('是否确认导出所有请假申请数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(function() {
+          return exportLeaveExcel(params);
+        }).then(response => {
+          this.downloadExcel(response, '请假申请.xls');
+        })
+    }
+  }
+};
+</script>

+ 211 - 0
yudao-admin-ui/src/views/oa/leave/modify/index.vue

@@ -0,0 +1,211 @@
+<template>
+  <div class="app-container">
+    <el-tabs type="border-card">
+      <el-tab-pane label="任务处理">
+        <el-form ref="form" :model="form"  :rules="rules" label-width="100px">
+          <el-row>
+            <el-col :span="15">
+              <el-form-item label="是否调整申请" prop="reApply">
+                <el-select v-model="reApplySelect"  placeholder="是否调整申请" v-on:change="reApplyChange">
+                  <el-option
+                    v-for="dict in reApplyData"
+                    :key="parseInt(dict.value)"
+                    :label="dict.label"
+                    :value="parseInt(dict.value)"
+                  />
+                </el-select>
+              </el-form-item>
+            </el-col>
+          </el-row>
+          <el-row :gutter="20" v-show="modifyShow">
+            <el-col :span="8"><el-form-item label="申请人" >{{form.userId}}</el-form-item></el-col>
+            <el-col :span="8">
+              <el-form-item label="申请时间" prop="applyTime">{{ parseTime(form.applyTime) }}</el-form-item>
+            </el-col>
+          </el-row>
+          <el-row :gutter="20" v-show="modifyShow">
+            <el-col :span="8">
+              <el-form-item label="开始时间" prop="startTime">
+                <el-date-picker clearable size="small" v-model="form.startTime" type="date" value-format="timestamp" placeholder="选择开始时间" />
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item label="结束时间" prop="endTime">
+                <el-date-picker clearable size="small" v-model="form.endTime" type="date" value-format="timestamp" placeholder="选择结束时间" />
+              </el-form-item>
+            </el-col>
+          </el-row>
+          <el-row v-show="modifyShow" >
+            <el-col :span="8">
+              <el-form-item label="请假类型" prop="leaveType">
+                <el-select v-model="form.leaveType" placeholder="请选择">
+                  <el-option
+                    v-for="dict in leaveTypeDictData"
+                    :key="dict.value"
+                    :label="dict.label"
+                    :value="dict.value"
+                  />
+                </el-select>
+              </el-form-item>
+            </el-col>
+          </el-row>
+          <el-row v-show="modifyShow">
+            <el-col :span="15">
+              <el-form-item label="原因" prop="reason">
+                <el-input
+                  type="textarea"
+                  :rows="3"
+                  v-model="form.reason"
+                  placeholder="请输入原因" />
+              </el-form-item>
+            </el-col>
+          </el-row>
+          <el-form-item>
+            <el-button type="primary" @click="submitForm">确 定</el-button>
+          </el-form-item>
+        </el-form>
+      </el-tab-pane>
+      <el-tab-pane label="历史跟踪">
+        <el-steps :active="stepActive" simple finish-status="success">
+          <el-step :title="stepTitle(item)" icon="el-icon-edit" v-for="(item) in handleTask.historyTask" ></el-step>
+        </el-steps>
+        <br/>
+        <el-steps direction="vertical" :active="stepActive"  space="65px">
+          <el-step :title="stepTitle(item)" :description="stepDes(item)" v-for="(item) in handleTask.historyTask" ></el-step>
+        </el-steps>
+      </el-tab-pane>
+      <el-tab-pane label="流程图">流程图-TODO</el-tab-pane>
+    </el-tabs>
+
+  </div>
+</template>
+
+<script>
+import { getLeave,updateLeave } from "@/api/oa/leave"
+import { taskSteps } from "@/api/oa/todo"
+import { getDictDataLabel, getDictDatas, DICT_TYPE } from '@/utils/dict'
+export default {
+  name: "HrApproveLeave",
+  components: {
+  },
+  data() {
+    return {
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {
+      },
+      handleTask: {
+        historyTask:[]
+      },
+      modifyShow: true,
+      reApplySelect: 1,
+      reApplyData: [
+        {
+          value: 0,
+          label: '取消申请'
+        },
+        {
+          value: 1,
+          label: '继续申请'
+        }
+      ],
+      statusFormat(row, column) {
+        return getDictDataLabel(DICT_TYPE.OA_LEAVE_STATUS, row.status)
+      },
+      leaveTypeDictData: getDictDatas(DICT_TYPE.OA_LEAVE_TYPE),
+      leaveStatusData: getDictDatas(DICT_TYPE.OA_LEAVE_STATUS)
+    };
+  },
+  mounted() {
+    const businessKey = this.$route.query.businessKey;
+    const taskId = this.$route.query.taskId;
+
+    this.getForm(businessKey,taskId);
+  },
+  computed:{
+    stepActive: function () {
+      let idx = 0;
+      for(let i=0; i<this.handleTask.historyTask.length; i++){
+        if(this.handleTask.historyTask[i].status === 1){
+          idx= idx+1;
+        }else{
+          break;
+        }
+      }
+      return idx;
+    },
+    stepTitle() {
+      return function (item) {
+        let name = item.stepName;
+        if (item.status === 1) {
+          name += '(已完成)'
+        }
+        if (item.status === 0) {
+          name += '(进行中)'
+        }
+        return name;
+      }
+    },
+    stepDes(){
+      return function (item) {
+        let desc = "";
+        if (item.status === 1) {
+          desc+="审批人:["+ item.assignee +"]    审批意见: [" + item.comment + "]   审批时间: " + this.parseTime(item.endTime);
+        }
+        return desc;
+      }
+    }
+  },
+  methods: {
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (!valid) {
+          return;
+        }
+        if (this.reApplySelect === 1) {
+          this.form.variables["reApply"] = true;
+          this.form.comment = '调整请假申请';
+        }
+        if (this.reApplySelect === 0) {
+          this.form.variables["reApply"] = false;
+          this.form.comment = '取消请假申请';
+        }
+        updateLeave(this.form).then(response => {
+          this.msgSuccess("修改成功");
+          this.$store.dispatch('tagsView/delView', this.$route).then(({ visitedViews }) => {
+            //if (this.isActive(this.$route)) {
+            this.$router.push({path: '/oa/todo'})
+            //}
+          })
+        });
+      });
+    },
+    getForm(id, taskId){
+      getLeave(id).then(response => {
+        this.form = response.data;
+        this.form.taskId = taskId;
+        this.form.variables = {};
+      });
+      const data = {
+        taskId : taskId,
+        businessKey: id,
+      }
+      taskSteps(data).then(response => {
+        this.handleTask = response.data;
+
+      });
+    },
+    reApplyChange(){
+      if (this.reApplySelect === 1) {
+        this.modifyShow = true;
+      }
+      if (this.reApplySelect === 0) {
+        this.modifyShow = false;
+      }
+    }
+
+  }
+};
+</script>

+ 303 - 0
yudao-admin-ui/src/views/oa/todo/index.vue

@@ -0,0 +1,303 @@
+<template>
+  <div class="app-container">
+
+    <!-- 搜索工作栏 -->
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
+      <el-form-item label="状态" prop="status">
+        <el-select v-model="queryParams.status" placeholder="请选择状态">
+          <el-option
+            v-for="dict in leaveStatusData"
+            :key="parseInt(dict.value)"
+            :label="dict.label"
+            :value="parseInt(dict.value)"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+
+
+    <!-- 列表 -->
+    <el-table v-loading="loading" :data="list">
+      <el-table-column label="任务Id" align="center" prop="id" />
+      <el-table-column label="流程名称" align="center" prop="processName" />
+      <el-table-column label="任务状态" align="center"  :formatter="statusFormat" prop="status" />
+<!--      <el-table-column label="申请人id" align="center" prop="userId" />-->
+<!--      <el-table-column label="开始时间" align="center" prop="startTime" width="180">-->
+<!--        <template slot-scope="scope">-->
+<!--          <span>{{ parseTime(scope.row.startTime) }}</span>-->
+<!--        </template>-->
+<!--      </el-table-column>-->
+<!--      <el-table-column label="结束时间" align="center" prop="endTime" width="180">-->
+<!--        <template slot-scope="scope">-->
+<!--          <span>{{ parseTime(scope.row.endTime) }}</span>-->
+<!--        </template>-->
+<!--      </el-table-column>-->
+<!--      <el-table-column label="请假类型" align="center" prop="leaveType" />-->
+<!--      <el-table-column label="原因" align="center" prop="reason" />-->
+<!--      <el-table-column label="申请时间" align="center" prop="applyTime" width="180">-->
+<!--        <template slot-scope="scope">-->
+<!--          <span>{{ parseTime(scope.row.applyTime) }}</span>-->
+<!--        </template>-->
+<!--      </el-table-column>-->
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template slot-scope="scope">
+          <el-button size="mini" type="text" icon="el-icon-edit" v-if="scope.row.status == 1"  @click="handleClaim(scope.row)">签收</el-button>
+          <el-button size="mini" type="text" icon="el-icon-edit" v-if="scope.row.status == 2"  @click="getTaskFormKey(scope.row)">办理</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页组件 -->
+    <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
+                @pagination="getList"/>
+
+    <el-dialog :title="title" :visible.sync="open" width="600px" append-to-body>
+      <el-tabs tab-position="left" style="height: 500px;">
+        <el-tab-pane label="详情">
+          <el-form ref="form" :model="handleTask.formObject"  label-width="80px">
+            <el-form-item label="状态" >
+              {{ getDictDataLabel(DICT_TYPE.OA_LEAVE_STATUS, handleTask.formObject.status) }}
+            </el-form-item>
+            <el-form-item label="申请人id" >{{handleTask.formObject.userId}}</el-form-item>
+            <el-form-item label="开始时间" >{{ parseTime(handleTask.formObject.startTime) }}</el-form-item>
+            <el-form-item label="结束时间" prop="endTime">{{ parseTime(handleTask.formObject.endTime) }}</el-form-item>
+            <el-form-item label="请假类型" prop="leaveType">
+              {{ getDictDataLabel(DICT_TYPE.OA_LEAVE_TYPE, handleTask.formObject.leaveType) }}
+            </el-form-item>
+            <el-form-item label="原因" prop="reason">{{handleTask.formObject.reason}}</el-form-item>
+            <el-form-item label="申请时间" prop="applyTime">{{ parseTime(handleTask.formObject.applyTime) }}</el-form-item>
+          </el-form>
+        </el-tab-pane>
+        <el-tab-pane label="任务处理">
+          <el-steps :active="handleTask.historyTask.length-1" simple finish-status="success">
+            <el-step :title="item.stepName" icon="el-icon-edit" v-for="(item) in handleTask.historyTask" ></el-step>
+          </el-steps>
+          <br/>
+          <el-steps direction="vertical" :active="handleTask.historyTask.length-1" finish-status="success" space="60px">
+            <el-step :title="item.stepName" :description="item.comment" v-for="(item) in handleTask.historyTask" ></el-step>
+          </el-steps>
+          <br/>
+          <el-form ref="taskForm" :model="task"  label-width="80px" v-show="handleTask.taskVariable !=''">
+              <el-form-item label="处理意见" prop="approved">
+                <el-select v-model="task.approved" placeholder="处理意见">
+                  <el-option
+                    v-for="dict in approvedData"
+                    :key="parseInt(dict.value)"
+                    :label="dict.label"
+                    :value="parseInt(dict.value)"
+                  />
+                </el-select>
+              </el-form-item>
+              <el-input
+                type="textarea"
+                :rows="2"
+                v-model="task.comment">
+              </el-input>
+          </el-form>
+          <br/>
+          <el-button type="primary" @click="submitTask">提交</el-button>
+        </el-tab-pane>
+      </el-tabs>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { completeTask, taskSteps, getTaskFormKey,deleteLeave, getLeave, getTodoTaskPage, claimTask } from "@/api/oa/todo";
+import { getDictDataLabel, getDictDatas, DICT_TYPE } from '@/utils/dict'
+export default {
+  name: "Todo",
+  components: {
+  },
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 请假申请列表
+      list: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      // 查询参数
+      queryParams: {
+        pageNo: 1,
+        pageSize: 10
+      },
+      // 表单参数
+      form: {},
+      handleTask: {
+        historyTask:[{
+            stepName:"步骤一"
+          }
+        ],
+        taskVariable: "",
+        formObject: {}
+      },
+      steps:[{
+        stepName:"步骤一"
+      }],
+      task: {
+        approved : 1,
+        variables: {},
+        taskId: undefined,
+        comment: ""
+      },
+      rules: {
+      },
+      leaveTypeDictData: getDictDatas(DICT_TYPE.OA_LEAVE_TYPE),
+      leaveStatusData: getDictDatas(DICT_TYPE.OA_LEAVE_STATUS),
+      approvedData: [
+        {
+          value: 1,
+          label: '同意'
+        },
+        {
+          value: 0,
+          label: '不同意'
+        }
+      ]
+    };
+  },
+  created() {
+    this.getList();
+  },
+  methods: {
+    /** 查询列表 */
+    getList() {
+      this.loading = true;
+      // 处理查询参数
+      let params = {...this.queryParams};
+      // 执行查询
+      getTodoTaskPage(params).then(response => {
+        this.list = response.data.list;
+        this.total = response.data.total;
+        this.loading = false;
+      });
+    },
+    /** 取消按钮 */
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    /** 表单重置 */
+    reset() {
+      this.form = {
+        id: undefined,
+        processInstanceId: undefined,
+        status: undefined,
+        userId: undefined,
+        startTime: undefined,
+        endTime: undefined,
+        leaveType: undefined,
+        reason: undefined,
+        applyTime: undefined,
+      };
+      this.resetForm("form");
+    },
+
+    statusFormat(row, column) {
+      return row.status == 1 ? "未签收" : "已签收";
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNo = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.dateRangeStartTime = [];
+      this.dateRangeEndTime = [];
+      this.dateRangeApplyTime = [];
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    getTaskFormKey(row) {
+      const taskId = row.id;
+      const data = {
+        taskId : taskId
+      }
+      getTaskFormKey(data).then(response => {
+        const resp = response.data;
+        const path = resp.formKey;
+        const taskId = resp.id;
+        const businessKey =  resp.businessKey;
+        const route = {
+          path: path,
+          query: {
+            businessKey: businessKey,
+            taskId:taskId
+          }
+        }
+        this.$router.replace(route);
+      });
+    },
+    handleLeaveApprove(row) {
+      this.reset();
+      const businessKey = row.businessKey;
+      const taskId = row.id;
+      const processKey = row.processKey;
+      const data = {
+        taskId : taskId,
+        businessKey: businessKey,
+        processKey: processKey
+      }
+      taskSteps(data).then(response => {
+        this.form = {};
+        this.handleTask = response.data;
+        this.task.taskId = taskId;
+        this.open = true;
+        this.title = "任务办理";
+      });
+    },
+    /** 任务签收操作 */
+    handleClaim(row) {
+      this.reset();
+      const id = row.id;
+      claimTask(id).then(() => {
+        this.getList();
+        this.msgSuccess("签收成功");
+      });
+    },
+    /** 提交任务 */
+    submitTask() {
+      const taskVariableName = this.handleTask.taskVariable;
+      if (taskVariableName != "") {
+        if (this.task.approved == 1) {
+          this.task.variables[taskVariableName] = true;
+        }
+        if (this.task.approved == 0) {
+          this.task.variables[taskVariableName] = false;
+        }
+      }
+      completeTask(this.task).then(response => {
+        this.msgSuccess("执行任务成功");
+        this.open = false;
+        this.getList();
+      })
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const id = row.id;
+      this.$confirm('是否确认删除请假申请编号为"' + id + '"的数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(function() {
+          return deleteLeave(id);
+        }).then(() => {
+          this.getList();
+          this.msgSuccess("删除成功");
+        })
+    }
+  }
+};
+</script>

+ 9 - 0
yudao-dependencies/pom.xml

@@ -438,6 +438,15 @@
                 <version>${justauth.version}</version>
             </dependency>
 
+
+
+            <!-- activiti begin -->
+            <dependency>
+                <groupId>cn.iocoder.boot</groupId>
+                <artifactId>yudao-spring-boot-starter-activiti</artifactId>
+                <version>${revision}</version>
+            </dependency>
+            <!-- activiti end -->
         </dependencies>
     </dependencyManagement>
 

+ 1 - 0
yudao-framework/pom.xml

@@ -28,6 +28,7 @@
         <module>yudao-spring-boot-starter-biz-operatelog</module>
         <module>yudao-spring-boot-starter-biz-dict</module>
         <module>yudao-spring-boot-starter-biz-sms</module>
+        <module>yudao-spring-boot-starter-activiti</module>
         <module>yudao-spring-boot-starter-biz-pay</module>
         <module>yudao-spring-boot-starter-biz-weixin</module>
         <module>yudao-spring-boot-starter-extension</module>

+ 71 - 0
yudao-framework/yudao-spring-boot-starter-activiti/pom.xml

@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <groupId>cn.iocoder.boot</groupId>
+        <artifactId>yudao-framework</artifactId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>yudao-spring-boot-starter-activiti</artifactId>
+    <packaging>jar</packaging>
+
+    <name>${artifactId}</name>
+    <description>Activiti 拓展</description>
+    <url>https://github.com/YunaiV/ruoyi-vue-pro</url>
+
+
+    <properties>
+        <activiti.version>7.1.0.M6</activiti.version>
+    </properties>
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>org.activiti.dependencies</groupId>
+                <artifactId>activiti-dependencies</artifactId>
+                <version>${activiti.version}</version>
+                <scope>import</scope>
+                <type>pom</type>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+    <dependencies>
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-common</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.mybatis</groupId>
+            <artifactId>mybatis</artifactId>
+            <optional>true</optional>
+        </dependency>
+
+        <!--使用mybatis plus需排除掉mybatis-->
+        <dependency>
+            <groupId>org.activiti</groupId>
+            <artifactId>activiti-spring-boot-starter</artifactId>
+            <version>${activiti.version}</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>de.odysseus.juel</groupId>
+                    <artifactId>juel-api</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>de.odysseus.juel</groupId>
+                    <artifactId>juel-spi</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.mybatis</groupId>
+                    <artifactId>mybatis</artifactId>
+                </exclusion>
+                <exclusion>
+                    <artifactId>el-api</artifactId>
+                    <groupId>javax.el</groupId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+    </dependencies>
+
+</project>

+ 32 - 0
yudao-framework/yudao-spring-boot-starter-activiti/src/main/java/cn/iocoder/yudao/framework/activiti/config/YudaoActivitiConfiguration.java

@@ -0,0 +1,32 @@
+package cn.iocoder.yudao.framework.activiti.config;
+
+import org.activiti.api.runtime.shared.identity.UserGroupManager;
+import org.activiti.spring.SpringProcessEngineConfiguration;
+import org.activiti.spring.boot.ProcessEngineConfigurationConfigurer;
+import org.apache.ibatis.session.SqlSessionFactory;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.stereotype.Component;
+
+@Configuration
+public class YudaoActivitiConfiguration {
+
+
+
+
+    @Component
+    public static class SqlSessionFactoryProcessEngineConfigurationConfigurer
+            implements ProcessEngineConfigurationConfigurer {
+
+        private final SqlSessionFactory sqlSessionFactory;
+        public SqlSessionFactoryProcessEngineConfigurationConfigurer(SqlSessionFactory sessionFactory) {
+            this.sqlSessionFactory = sessionFactory;
+        }
+
+        @Override
+        public void configure(SpringProcessEngineConfiguration springProcessEngineConfiguration) {
+            springProcessEngineConfiguration.setSqlSessionFactory(sqlSessionFactory);
+        }
+    }
+
+
+}

+ 1 - 0
yudao-framework/yudao-spring-boot-starter-activiti/src/main/java/cn/iocoder/yudao/framework/activiti/package-info.java

@@ -0,0 +1 @@
+package cn.iocoder.yudao.framework.activiti;

+ 13 - 0
yudao-framework/yudao-spring-boot-starter-security/pom.xml

@@ -44,6 +44,19 @@
             <artifactId>spring-boot-starter-security</artifactId>
         </dependency>
 
+        <!-- TODO 芋艿: -->
+        <dependency>
+            <groupId>org.activiti</groupId>
+            <artifactId>activiti-engine</artifactId>
+            <version>7.1.0.M6</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>*</groupId>
+                    <artifactId>*</artifactId>
+                </exclusion>
+            </exclusions>
+            <optional>true</optional>
+        </dependency>
     </dependencies>
 
 </project>

+ 17 - 6
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/LoginUser.java

@@ -5,12 +5,10 @@ import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
 import com.fasterxml.jackson.annotation.JsonIgnore;
 import lombok.Data;
 import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
 import org.springframework.security.core.userdetails.UserDetails;
 
-import java.util.Collection;
-import java.util.Date;
-import java.util.HashSet;
-import java.util.Set;
+import java.util.*;
 
 /**
  * 登录用户信息
@@ -56,6 +54,18 @@ public class LoginUser implements UserDetails {
      */
     private Integer status;
 
+
+    /**
+     * 所属岗位
+     */
+    private Set<Long> postIds;
+
+    /**
+     * group  目前指岗位代替
+     */
+    private List<String> groups;
+
+
     // TODO @芋艿:怎么去掉 deptId
 
     @Override
@@ -65,7 +75,6 @@ public class LoginUser implements UserDetails {
     }
 
     @Override
-    @JsonIgnore
     public String getUsername() {
         return username;
     }
@@ -79,7 +88,9 @@ public class LoginUser implements UserDetails {
     @Override
     @JsonIgnore// 避免序列化
     public Collection<? extends GrantedAuthority> getAuthorities() {
-        return new HashSet<>();
+        List<GrantedAuthority> list = new ArrayList<>(1);
+        list.add(new SimpleGrantedAuthority("ROLE_ACTIVITI_USER"));
+        return list;
     }
 
     @Override

+ 4 - 1
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java

@@ -90,14 +90,17 @@ public class SecurityFrameworkUtils {
     public static void setLoginUser(LoginUser loginUser, HttpServletRequest request) {
         // 创建 UsernamePasswordAuthenticationToken 对象
         UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
-                loginUser, null, null);
+                loginUser, null, loginUser.getAuthorities());
         authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
         // 设置到上下文
+        //何时调用  SecurityContextHolder.clearContext. spring security filter 应该会调用 clearContext
         SecurityContextHolder.getContext().setAuthentication(authenticationToken);
         // 额外设置到 request 中,用于 ApiAccessLogFilter 可以获取到用户编号;
         // 原因是,Spring Security 的 Filter 在 ApiAccessLogFilter 后面,在它记录访问日志时,线上上下文已经没有用户编号等信息
         WebFrameworkUtils.setLoginUserId(request, loginUser.getId());
         WebFrameworkUtils.setLoginUserType(request, loginUser.getUserType());
+        // TODO @jason:使用 userId 会不会更合适哈?
+        org.activiti.engine.impl.identity.Authentication.setAuthenticatedUserId(loginUser.getUsername());
     }
 
 }

+ 11 - 0
更新日志.md

@@ -6,6 +6,17 @@
 ## [v1.3.0] 待定
 
 * 工作流
+  * 修改表单为外置表单
+  * 新增菜单流程申请
+  * 请假流程如下
+    1. 请假人 在流程申请, 点击请假申请,填写表单提交
+    2. 部门领导(目前写死admin)  登陆,待办任务中 点击办理, 进行审批
+    3. hr 登陆(用户名:hradmin 密码:123456)  待办任务中 点击办理, 进行审批
+    4. 请假人登陆, 待办任务中, 确认
+    5. 流程结束
+  * 请假查询中,可以查询本人的请假申请, 和进度
+  * 流程跟踪图 待实现
+
 
 ## [v1.2.0] 进行中
 

Some files were not shown because too many files changed in this diff