From 7181652f01b0dcbff3f0daf99c9a0a1d19b4f3e1 Mon Sep 17 00:00:00 2001 From: ganzizi Date: Wed, 12 Apr 2023 16:25:26 +0800 Subject: [PATCH] =?UTF-8?q?feat=20=E5=88=9D=E6=AD=A5=E8=9E=8D=E5=90=88?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E6=B5=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/Api/WorkflowController.php | 604 ++++++++++++++++++ app/Http/Controllers/Api/apiswagger.php | 510 +++++++++++++++ app/Models/User.php | 3 + app/Models/WorkflowProcMsg.php | 30 + app/Module/Base.php | 39 ++ cmd | 1 + ...04_11_122557_create_workflow_proc_msgs.php | 38 ++ docker-compose.yml | 18 + resources/views/push/bot.blade.php | 43 ++ routes/web.php | 14 +- 10 files changed, 1295 insertions(+), 5 deletions(-) create mode 100755 app/Http/Controllers/Api/WorkflowController.php create mode 100644 app/Http/Controllers/Api/apiswagger.php create mode 100644 app/Models/WorkflowProcMsg.php create mode 100644 database/migrations/2023_04_11_122557_create_workflow_proc_msgs.php diff --git a/app/Http/Controllers/Api/WorkflowController.php b/app/Http/Controllers/Api/WorkflowController.php new file mode 100755 index 000000000..8c95130bb --- /dev/null +++ b/app/Http/Controllers/Api/WorkflowController.php @@ -0,0 +1,604 @@ +flow_url = 'http://dootask-workflow-'.env('APP_ID'); + $this->flow_url = 'http://192.168.100.219:8700'; + } + + /** + * @api {post} api/workflow/procdef/all 01. 查询流程定义 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup workflow + * @apiName procdef__all + * + * @apiQuery {String} name 流程名称 + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function procdef__all() + { + //{"name": "请假"} + $data['name'] = Request::input('name'); + $procdef = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/procdef/findAll', json_encode($data)); + $procdef = json_decode($procdef['data'] ?? '', true); + if (!$procdef || $procdef['status'] != 200) { + return Base::retError($procdef['message'] ?? '查询流程失败'); + } + return Base::retSuccess('success', Base::arrayKeyToUnderline($procdef['data'])); + } + + /** + * @api {get} api/workflow/procdef/del 02. 删除流程定义 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup workflow + * @apiName procdef__del + * + * @apiQuery {String} id 流程ID + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function procdef__del() + { + $data['id'] = Request::input('id'); + $procdef = Ihttp::ihttp_get($this->flow_url.'/api/v1/workflow/procdef/delById?'.http_build_query($data)); + $procdef = json_decode($procdef['data'] ?? '', true); + if (!$procdef || $procdef['status'] != 200) { + return Base::retError($procdef['message'] ?? '删除流程失败'); + } + return Base::retSuccess('success', Base::arrayKeyToUnderline($procdef['data'])); + } + + /** + * @api {post} api/workflow/process/start 03. 启动流程 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup workflow + * @apiName process__start + * + * @apiQuery {String} proc_name 流程名称 + * @apiQuery {Number} userid 用户ID + * @apiQuery {Number} department_id 部门ID + * @apiQuery {Array} [var] 启动流程类型信息(格式:[{type,startTime,endTime,description}]) + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function process__start() + { + //{"procName":"请假222","userID":"2", "departmentId":1, "var":{"type":"事假","startTime":"12", "endTime":"1", "description":"请假描述"}} + $data['userid'] = Request::input('userid'); + $data['department_id'] = intval(Request::input('department_id')); + $data['proc_name'] = Request::input('proc_name'); + // + $var = json_decode(Request::input('var'), true); + $data['var'] = $var; + $process = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/process/start', json_encode(Base::arrayKeyToCamel($data))); + $process = json_decode($process['data'] ?? '', true); + if (!$process || $process['status'] != 200) { + return Base::retError($process['message'] ?? '流程启动失败'); + } + // + $process = Base::arrayKeyToUnderline($process['data']); + $process = $this->getProcessById($process['id']); //获取最新的流程信息 + if ($process['candidate']) { + $userid = explode(',', $process['candidate']); + $toUser = User::whereIn('userid', $userid)->get(); + $botUser = User::botGetOrCreate('approval-alert'); + if (empty($botUser)) { + return Base::retError('审批机器人不存在'); + } + foreach ($toUser as $val) { + if ($val->bot) { + continue; + } + $dialog = WebSocketDialog::checkUserDialog($botUser, $val->userid); + if (empty($dialog)) { + continue; + } + $this->workflowMsg('workflow_reviewer', $dialog, $botUser, $val, $process, 'start'); + } + // 抄送人 + $notifier = $this->handleProcessNode($process); + if ($notifier) { + foreach ($notifier as $val) { + $dialog = WebSocketDialog::checkUserDialog($botUser, $val['target_id']); + $this->workflowMsg('workflow_notifier', $dialog, $botUser, $process, $process); + } + } + } + + return Base::retSuccess('success', $process); + } + + /** + * @api {post} api/workflow/task/complete 04. 审批 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup workflow + * @apiName task__complete + * + * @apiQuery {Number} task_id 流程ID + * @apiQuery {String} pass 标题 [true-通过,false-拒绝] + * @apiQuery {Number} userid 用户ID + * @apiQuery {String} comment 评论 + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function task__complete() + { + //{"taskID":51,"pass":"true","userID":"2","comment": "评论备注51"} + $data['task_id'] = intval(Request::input('task_id')); + $data['pass'] = Request::input('pass'); + $data['userid'] = Request::input('userid'); + $data['comment'] = Request::input('comment'); + info(Base::arrayKeyToCamel($data)); + $task = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/task/complete', json_encode(Base::arrayKeyToCamel($data))); + info($task); + $task = json_decode($task['data'] ?? '', true); + if (!$task || $task['status'] != 200) { + return Base::retError($task['message'] ?? '审批失败'); + } + // + $task = Base::arrayKeyToUnderline($task['data']); + $pass = $data['pass'] == 'true' ? 'pass' : 'refuse'; + $process = $this->getProcessById($task['proc_inst_id']); + info($process); + $botUser = User::botGetOrCreate('approval-alert'); + if (empty($botUser)) { + return Base::retError('审批机器人不存在'); + } + // 在流程信息关联的用户中查找 + $toUser = WorkflowProcMsg::where('proc_inst_id', $process['id'])->get()->toArray(); + info($toUser); + foreach ($toUser as $val) { + $dialog = WebSocketDialog::checkUserDialog($botUser, $val['userid']); + if (empty($dialog)) { + continue; + } + info('11111111'); + $this->workflowMsg('workflow_reviewer', $dialog, $botUser, $val, $process, $pass); + } + // 发起人 + if($process['is_finished'] == true) { + $dialog = WebSocketDialog::checkUserDialog($botUser, $process['start_user_id']); + $this->workflowMsg('workflow_submitter', $dialog, $botUser, ['userid' => $data['userid']], $process, $pass); + } + // 抄送人 + $notifier = $this->handleProcessNode($process, $task['step']); + if ($notifier && $pass == 'pass') { + foreach ($notifier as $val) { + $dialog = WebSocketDialog::checkUserDialog($botUser, $val['target_id']); + $this->workflowMsg('workflow_notifier', $dialog, $botUser, $process, $process); + } + } + return Base::retSuccess('success', $task); + } + + /** + * @api {post} api/workflow/task/withdraw 05. 撤回 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup workflow + * @apiName task__withdraw + * + * @apiQuery {Number} task_id 流程ID + * @apiQuery {String} userid 用户ID + * @apiQuery {Number} proc_inst_id 流程实例ID + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function task__withdraw() + { + //{"taskID":2,"userID":"2","procInstID":1} + $data['task_id'] = intval(Request::input('task_id')); + $data['userid'] = Request::input('userid'); + $data['proc_inst_id'] = intval(Request::input('proc_inst_id')); + info(Base::arrayKeyToCamel($data)); + $task = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/task/withdraw', json_encode(Base::arrayKeyToCamel($data))); + info($task); + $task = json_decode($task['data'] ?? '', true); + if (!$task || $task['status'] != 200) { + return Base::retError($task['message'] ?? '撤回失败'); + } + // + $process = $this->getProcessById($data['proc_inst_id']); + $botUser = User::botGetOrCreate('approval-alert'); + if (empty($botUser)) { + return Base::retError('审批机器人不存在'); + } + // 在流程信息关联的用户中查找 + $toUser = WorkflowProcMsg::where('proc_inst_id', $process['id'])->get()->toArray(); + info($toUser); + foreach ($toUser as $val) { + $dialog = WebSocketDialog::checkUserDialog($botUser, $val['userid']); + if (empty($dialog)) { + continue; + } + info('11111111'); + //发送撤回提醒 + $this->workflowMsg('workflow_reviewer', $dialog, $botUser, $val, $process, 'withdraw'); + } + return Base::retSuccess('success', Base::arrayKeyToUnderline($task['data'])); + } + + /** + * @api {post} api/workflow/process/findTask 06. 查询需要我审批的流程 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup workflow + * @apiName process__findTask + * + * @apiQuery {Number} userID 用户ID + * @apiQuery {String} groups 用户组 + * @apiQuery {String} departments 部门 + * @apiQuery {String} company 公司 + * @apiQuery {String} procName 流程名称 + * @apiQuery {Number} pageIndex 页码 + * @apiQuery {Number} pageSize 每页条数 + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function process__findTask() + { + //{"userID":"2","groups":["人事"],"departments":["技术中心"],"company":"A公司","procName": "请假","pageIndex":1,"pageSize":10} + $data['userID'] = Request::input('userID'); + // $data['groups'] = Request::input('groups'); + // $data['departments'] = Request::input('departments'); + // $data['company'] = Request::input('company'); + // $data['procName'] = Request::input('procName'); + $data['pageIndex'] = Request::input('pageIndex'); + $data['pageSize'] = Request::input('pageSize'); + info($data); + $workflow = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/process/findTask', json_encode($data)); + info($workflow); + $workflow = json_decode($workflow['data'] ?? '', true); + if (!$workflow || $workflow['status'] != 200) { + return Base::retError($workflow['message'] ?? '查询失败'); + } + return Base::retSuccess('success', Base::arrayKeyToUnderline($workflow['data'])); + } + + /** + * @api {post} api/workflow/identitylink/findParticipant 07. 查询流程审批人与评论 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup workflow + * @apiName identitylink__findParticipant + * + * @apiQuery {Number} procInstID 流程ID + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function identitylink__findParticipant() + { + $data['procInstID'] = Request::input('procInstID'); + + $get = Ihttp::ihttp_get($this->flow_url.'/api/v1/workflow/identitylink/findParticipant?procInstID=' . $data['procInstID']); + info($get); + $workflow = json_decode($get['data'] ?? '', true); + if (!$workflow || $get['msg'] != 200) { + return Base::retError($workflow['message'] ?? 'fail'); + } + return Base::retSuccess('success', Base::arrayKeyToUnderline($workflow)); + } + + /** + * @api {post} api/workflow/procHistory/findTask 08. 查询我审批的流程(已结束) + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup workflow + * @apiName procHistory__findTask + * + * @apiQuery {Number} user_id 用户ID + * @apiQuery {String} company 公司 + * @apiQuery {Number} pageIndex 页码 + * @apiQuery {Number} pageSize 每页条数 + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function procHistory__findTask() + { + //{"userID":"2","company":"A公司","pageIndex":1,"pageSize":2} + $data['userID'] = Request::input('userID'); + $data['company'] = Request::input('company'); + $data['pageIndex'] = Request::input('pageIndex'); + $data['pageSize'] = Request::input('pageSize'); + + $workflow = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/procHistory/findTask', json_encode($data)); + info($workflow); + $workflow = json_decode($workflow['data'] ?? '', true); + if (!$workflow || $workflow['status'] != 200) { + return Base::retError($workflow['message'] ?? 'fail'); + } + return Base::retSuccess('success', Base::arrayKeyToUnderline($workflow['data'])); + } + + /** + * @api {post} api/workflow/procHistory/startByMyself 09. 查询我发起的流程(已结束) + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup workflow + * @apiName procHistory__startByMyself + * + * @apiQuery {Number} user_id 用户ID + * @apiQuery {String} company 公司 + * @apiQuery {Number} pageIndex 页码 + * @apiQuery {Number} pageSize 每页条数 + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function procHistory__startByMyself() + { + //{"userID":"2","company":"A公司","pageIndex":1,"pageSize":1} + $data['userID'] = Request::input('userID'); + $data['company'] = Request::input('company'); + $data['pageIndex'] = Request::input('pageIndex'); + $data['pageSize'] = Request::input('pageSize'); + + $workflow = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/procHistory/startByMyself', json_encode($data)); + info($workflow); + $workflow = json_decode($workflow['data'] ?? '', true); + if (!$workflow || $workflow['status'] != 200) { + return Base::retError($workflow['message'] ?? 'fail'); + } + return Base::retSuccess('success', Base::arrayKeyToUnderline($workflow['data'])); + } + + /** + * @api {post} api/workflow/process/startByMyself 10. 查询我发起的流程(审批中) + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup workflow + * @apiName process__startByMyself + * + * @apiQuery {Number} user_id 用户ID + * @apiQuery {String} company 公司 + * @apiQuery {Number} pageIndex 页码 + * @apiQuery {Number} pageSize 每页条数 + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function process__startByMyself() + { + //{"userID":"2","company":"A公司","pageIndex":1,"pageSize":1} + $data['userID'] = Request::input('userID'); + $data['company'] = Request::input('company'); + $data['pageIndex'] = Request::input('pageIndex'); + $data['pageSize'] = Request::input('pageSize'); + + $workflow = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/process/startByMyself', json_encode($data)); + info($workflow); + $workflow = json_decode($workflow['data'] ?? '', true); + if (!$workflow || $workflow['status'] != 200) { + return Base::retError($workflow['message'] ?? 'fail'); + } + return Base::retSuccess('success', Base::arrayKeyToUnderline($workflow['data'])); + } + + /** + * @api {post} api/workflow/process/FindProcNotify 11. 查询抄送我的流程(审批中) + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup workflow + * @apiName process__FindProcNotify + * + * @apiQuery {Number} user_id 用户ID + * @apiQuery {String} company 公司 + * @apiQuery {Array} groups 抄送组 + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function process__FindProcNotify() + { + //{"userID":"2","company":"A公司","groups":["人事","产品经理"]} + $data['userID'] = Request::input('userID'); + $data['company'] = Request::input('company'); + $data['groups'] = Request::input('groups'); + + $workflow = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/process/FindProcNotify', json_encode($data)); + info($workflow); + $workflow = json_decode($workflow['data'] ?? '', true); + if (!$workflow || $workflow['status'] != 200) { + return Base::retError($workflow['message'] ?? 'fail'); + } + return Base::retSuccess('success', Base::arrayKeyToUnderline($workflow['data'])); + } + + /** + * @api {post} api/workflow/procHistory/FindProcNotify 12. 查询抄送我的流程(已结束) + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup workflow + * @apiName procHistory__FindProcNotify + * + * @apiQuery {Number} user_id 用户ID + * @apiQuery {String} company 公司 + * @apiQuery {Array} groups 抄送组 + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function procHistory__FindProcNotify() + { + //{"userID":"2","company":"A公司","groups":["人事","产品经理"]} + $data['userID'] = Request::input('userID'); + $data['company'] = Request::input('company'); + $data['groups'] = Request::input('groups'); + + $workflow = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/procHistory/FindProcNotify', json_encode($data)); + info($workflow); + $workflow = json_decode($workflow['data'] ?? '', true); + if (!$workflow || $workflow['status'] != 200) { + return Base::retError($workflow['message'] ?? 'fail'); + } + return Base::retSuccess('success', Base::arrayKeyToUnderline($workflow['data'])); + } + + /** + * @api {get} api/workflow/process/findById 13. 根据流程ID查询流程(所有) + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup workflow + * @apiName process__findById + * + * @apiQuery {Number} id 流程ID + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function process__findById() + { + //{"id":"2"} + $data['id'] = intval(Request::input('id')); + $workflow = $this->getProcessById($data['id']); + return Base::retSuccess('success', $workflow); + } + + // 审批机器人消息-审核人 + public function workflowMsg($type, $dialog, $botUser, $toUser, $process, $action = null) + { + $data = [ + 'nickname' => User::userid2nickname($type == 'workflow_submitter' ? $toUser['userid'] : $process['start_user_id']), + 'proc_def_name' => $process['proc_def_name'], + 'department' => $process['department'], + 'type' => $process['var']['type'], + 'start_time' => $process['var']['start_time'], + 'end_time' => $process['var']['end_time'], + ]; + info($action); + $text = view('push.bot', ['type' => $type, 'action' => $action, 'data' => (object)$data])->render(); + $text = preg_replace("/^\x20+/", "", $text); + $text = preg_replace("/\n\x20+/", "\n", $text); + $msg_action = null; + if ($action == 'withdraw' || $action == 'pass' || $action == 'refuse') { + info(222222222222); + // 如果任务没有完成,则不需要更新消息 + info($process['is_finished'] == true); + if ($process['is_finished'] != true) { + return true; + } + info('===========22222222========'); + // 任务完成,给发起人发送消息 + if($type == 'workflow_submitter' && $action != 'withdraw'){ + info('==================任务完成,给发起人发送消息========'); + return WebSocketDialogMsg::sendMsg($msg_action, $dialog->id, 'text', ['text' => $text], $botUser->userid, false, false, true); + } + // 查找最后一条消息msg_id + $msg_action = 'update-'.$toUser['msg_id']; + info($msg_action); + } + info(33333333333333); + $msg = WebSocketDialogMsg::sendMsg($msg_action, $dialog->id, 'text', ['text' => $text], $botUser->userid, false, false, true); + // 保存关联信息 + if ($action == 'start') { + info(666666666666); + // 创建关联信息 + $proc_msg = new WorkflowProcMsg(); + $proc_msg->proc_inst_id = $process['id']; + $proc_msg->msg_id = $msg['data']->id; + $proc_msg->userid = $toUser->userid; + $proc_msg->save(); + } + return true; + } + + // 根据ID获取流程 + public function getProcessById($id) + { + $data['id'] = intval($id); + $process = Ihttp::ihttp_get($this->flow_url."/api/v1/workflow/process/findById?".http_build_query($data)); + $process = json_decode($process['data'] ?? '', true); + if (!$process || $process['status'] != 200) { + return Base::retError($process['message'] ?? 'fail'); + } + return Base::arrayKeyToUnderline($process['data']); + } + + // 处理流程节点返回是否有抄送人 + public function handleProcessNode($process, $step = 0) + { + // 获取流程节点 + $process_node = $process['node_infos']; + //判断下一步是否有抄送人 + $step = $step + 1; + $next_node = $process_node[$step] ?? []; + info($next_node); + if ($next_node) { + if ($next_node['type'] == 'notifier'){ + return $next_node['node_user_list'] ?? []; + } + } + return []; + } + + // 根据ID查询流程实例的参与者(审批中) + public function getUserProcessParticipantById($id) + { + $data['id'] = intval($id); + $user = Ihttp::ihttp_get($this->flow_url."/api/v1/user/identitylink/findParticipant?".http_build_query($data)); + $user = json_decode($user['data'] ?? '', true); + if (!$user || $user['status'] != 200) { + return Base::retError($user['message'] ?? 'fail'); + } + return $user; + } +} diff --git a/app/Http/Controllers/Api/apiswagger.php b/app/Http/Controllers/Api/apiswagger.php new file mode 100644 index 000000000..8c9fcefce --- /dev/null +++ b/app/Http/Controllers/Api/apiswagger.php @@ -0,0 +1,510 @@ + $text) { + $original = $matchs[0][$key]; + // + preg_match("/\*\s*\@api\s+\{(get|post|put|delete|head|options|patch|propfind|proppatch|mkcol|copy|move|lock|unlock)\}\s+(.*?)\s+(.*?)\n/i", $original, $matchApi); + preg_match("/\*\s*\@apiDescription\s+(.*?)\n/i", $original, $matchDesc); + preg_match("/\*\s*\@apiGroup\s+(.*?)\n/i", $original, $matchGroup); + if (empty($matchApi) || empty($matchGroup)) { + continue; + } + $tagText = $tags[$matchGroup[1]] ?? $matchGroup[1]; + // + preg_match_all("/\*\s*\@(apiHeader|apiBody|apiParam|apiQuery)\s+\{(.*?)\}(.*?)\n([\s\S]*?)(?=(\*\s*@|\*\s*\/))/i", $original, $matchParam); + $paramArray = []; + foreach ($matchParam[1] as $index => $value) { + $paramItem = []; + $in = ''; + if ($matchParam[1][$index] == 'apiHeader') { + $in = 'header'; + } elseif ($matchParam[1][$index] == 'apiQuery') { + $in = 'query'; + } elseif ($matchParam[1][$index] == 'apiBody') { + $in = 'formData'; + } elseif ($matchParam[1][$index] == 'apiParam') { + $schema = $matchParam[4][$index]; + $schema = preg_replace("/(\\n|^)\s*\*/", '', $schema); + $schemaData = json5_decode($schema); + if ($schemaData) { + $paramItem = [ + "name" => "root", + "in" => "body", + "schema" => [ + "\$schema" => "http://json-schema.org/draft-04/schema#", + ] + ]; + $isArr = __isArr($schemaData); + $schemaData = $isArr ? $schemaData[1] : $schemaData; + $properties = __propertieData($schemaData); + if ($isArr) { + $paramItem["schema"]["type"] = "array"; + $paramItem["schema"]["items"] = [ + "type" => "object", + "properties" => $properties + ]; + } else { + $paramItem["schema"]["type"] = "object"; + $paramItem["schema"]["properties"] = $properties; + } + } else { + $in = 'query'; + } + } else { + continue; + } + if ($in) { + preg_match("/^\s*(\[*(.*?)\]*)\s+(.*?)$/i", $matchParam[3][$index], $matchParam3); + if ($matchParam3) { + $type = strtolower($matchParam[2][$index]); + $name = $matchParam3[2] ?: $matchParam3[3]; + $desc = trim($matchParam3[3]); + $matchParam4 = $matchParam[4][$index]; + $matchParam4 = preg_replace("/(\\n|^)\s*\*/", "\n", $matchParam4); + if ($matchParam4) { + $matchParam4 = preg_replace("/(\\n|^)\s*-/", "$1", trim($matchParam4)); + $desc = trim(sprintf("%s\n%s", $desc, $matchParam4)); + } + $paramArray[] = [ + "name" => $name, + "in" => $in, + "required" => !str_starts_with($matchParam3[1], '['), + "description" => $desc, + "type" => $type, + ]; + } + } + if ($paramItem) { + $paramArray[] = $paramItem; + } + } + // + preg_match_all("/\*\s*\@apiSuccess\s+\{(.*?)\}(.*?)\n([\s\S]*?)(?=(\*\s*@|\*\s*\/))/i", $original, $matchSuccess); + $successArray = []; + $requiredArray = []; + foreach ($matchSuccess[1] as $index => $value) { + preg_match("/^\s*(\[*(.*?)\]*)\s+(.*?)$/i", $matchSuccess[2][$index], $matchSuccess2); + $name = $matchSuccess2[2] ?: $matchSuccess2[3]; + if (!str_starts_with($matchSuccess2[1], '[')) { + $requiredArray[] = $name; + } + $type = strtolower($matchSuccess[1][$index]); + $successArray[$name] = [ + 'type' => strtolower($matchSuccess[1][$index]), + 'description' => $matchSuccess2[3] + ]; + if (in_array($type, ['object', 'array', 'json'])) { + $exampleSuccess = $matchSuccess[3]; + preg_match("/\*\s*\@apiSuccessExample\s+\{(.*?)\}\s*{$name}:*([\s\S]*?)(?=(\*\s*@|\*\s*\/))/i", $original, $matchExample); + if ($matchExample) { + $exampleSuccess = [$matchExample[2]]; + } + $schemaData = []; + if (array_filter($exampleSuccess, function ($item) use (&$schemaData) { + $item = preg_replace("/(\\n|^)\s*\*/", '', $item); + $itemData = json5_decode($item); + if ($itemData) { + $schemaData = $itemData; + return true; + } + return false; + })) { + $isArr = __isArr($schemaData); + $schemaData = $isArr ? $schemaData[1] : $schemaData; + $successArray[$name]["properties"] = __propertieData($schemaData); + } + } + } + $responsesSchema = [ + "\$schema" => "http://json-schema.org/draft-04/schema#", + 'type' => 'object', + 'properties' => $successArray, + 'required' => $requiredArray, + ]; + // + $paths[$matchApi[2]][$matchApi[1]] = [ + 'tags' => [$tagText], + 'consumes' => strtolower($matchApi[1]) == 'post' ? ['multipart/form-data'] : [], + 'summary' => $matchApi[3], + 'description' => $matchDesc ? trim($matchDesc[1]) : '', + 'parameters' => $paramArray, + 'responses' => [ + '200' => [ + 'description' => 'successful operation', + 'schema' => $responsesSchema + ] + ] + ]; + } +} +// +$tagArray = []; +foreach ($tags as $tag) { + $tagArray[] = [ + 'name' => $tag, + 'description' => $tag, + ]; +} +// +$title = "Api"; +$version = "0.0.1"; +$packagePath = dirname($path, 4) . '/package.json'; +if (file_exists($packagePath)) { + $packageArray = json_decode(file_get_contents(dirname($path, 4) . '/package.json'), true); + if ($packageArray) { + $title = $packageArray['name']; + $version = $packageArray['version']; + } +} +// +$content = [ + 'swagger' => '2.0', + 'info' => [ + 'title' => $title, + 'version' => $version, + ], + 'basePath' => '/', + 'tags' => $tagArray, + 'schemes' => ['http'], + 'paths' => $paths +]; + +$filePath = dirname($path, 4) . '/public/docs'; +mkdir($filePath, 0777, true); +file_put_contents($filePath . '/swagger.json', __array2json($content, true)); + +echo "Success \n"; + + +/** ************************************************************** */ +/** ************************************************************** */ +/** ************************************************************** */ + +function __propertieData($jsonArray) +{ + list($jsonArray) = json5_value_note($jsonArray); + $properties = []; + foreach ($jsonArray as $k => $v) { + $properties[$k] = [ + "type" => "string", + ]; + list($value, $note) = json5_value_note($v); + if ($note) { + $properties[$k]["description"] = $note; + } + if (__isArr($value)) { + $properties[$k]["type"] = "array"; + $properties[$k]["items"] = __propertieItem($value); + } elseif (__isJson($value)) { + $properties[$k]["type"] = "object"; + $properties[$k]["properties"] = __propertieItem($value); + } elseif (floatval($value) == $value) { + $properties[$k]["type"] = str_contains("$value", ".") ? "integer" : "number"; + } + } + return $properties; +} + +function __propertieItem($data) +{ + if (__isArr($data)) { + return [ + 'type' => 'object', + 'properties' => $data ? __propertieData($data[0]) : json_decode('{}'), + ]; + } elseif (__isJson($data)) { + return $data ? __propertieData($data) : json_decode('{}'); + } + return json_decode('{}'); +} + +function __array2json($array, $options = 0) +{ + if (!is_array($array)) { + return $array; + } + if ($options === true) { + $options = JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES; + } + try { + return json_encode($array, $options); + } catch (Exception $e) { + return ''; + } +} + +function __isArr($data) +{ + return is_array($data) && str_starts_with(__array2json($data), '['); +} + +function __isJson($data) +{ + return is_array($data) && str_starts_with(__array2json($data), '{'); +} + +/** ************************************************************** */ +/** ************************************************************** */ +/** ************************************************************** */ + +// JSON5 for PHP +// [LICENSE] MIT +// [URL] https://github.com/kujirahand/JSON5-PHP + +function json5_value_note($v) +{ + if (is_array($v) && $v['type'] === '__note__') { + return [$v['value'], $v['note']]; + } + return [$v, null]; +} + +function json5_decode($json5, $assoc = true) +{ + return json5_value($json5, $assoc); +} + +function json5_value(&$json5, $assoc) +{ + $json5 = trim($json5); + json5_comment($json5); // skip comment + // check 1char + $c = substr($json5, 0, 1); + // Object + if ($c == '{') { + return json5_note($json5, json5_object($json5, $assoc)); + } + // Array + if ($c == '[') { + return json5_note($json5, json5_array($json5, $assoc)); + } + // String + if ($c == '"' || $c == "'") { + return json5_note($json5, json5_string($json5)); + } + // null / true / false / Infinity + if (strncasecmp($json5, "null", 4) == 0) { + $json5 = substr($json5, 4); + return json5_note($json5, null); + } + if (strncasecmp($json5, "true", 4) == 0) { + $json5 = substr($json5, 4); + return json5_note($json5, true); + } + if (strncasecmp($json5, "false", 5) == 0) { + $json5 = substr($json5, 5); + return json5_note($json5, false); + } + if (strncasecmp($json5, "infinity", 8) == 0) { + $json5 = substr($json5, 8); + return json5_note($json5, INF); + } + // hex + if (preg_match('/^(0x[a-zA-Z0-9]+)/', $json5, $m)) { + $num = $m[1]; + $json5 = substr($json5, strlen($num)); + return json5_note($json5, intval($num, 16)); + } + // number + if (preg_match('/^((\+|\-)?\d*\.?\d*[eE]?(\+|\-)?\d*)/', $json5, $m)) { + $num = $m[1]; + $json5 = substr($json5, strlen($num)); + return json5_note($json5, floatval($num)); + } + // other char ... maybe error + $json5 = substr($json5, 1); + return json5_note($json5, null); +} + +function json5_note($original, $value) +{ + $array = explode("\n", $original); + preg_match("/\/\/(.*?)(\\n|$)/i", $array[0], $match); + if ($match) { + return [ + 'type' => '__note__', + 'value' => $value, + 'note' => trim($match[1]), + ]; + } + return $value; +} + +function json5_comment(&$json5) +{ + while ($json5 != '') { + $json5 = ltrim($json5); + $c2 = substr($json5, 0, 2); + // block comment + if ($c2 == '/*') { + json5_token($json5, '*/'); + continue; + } + // line comment + if ($c2 == '//') { + json5_token($json5, "\n"); + continue; + } + break; + } +} + +function json5_string(&$json5) +{ + // check flag + $flag = substr($json5, 0, 1); + $json5 = substr($json5, 1); + $str = ""; + while ($json5 != "") { + // check + $c = mb_substr($json5, 0, 1); + $json5 = substr($json5, strlen($c)); + // Is terminator? + if ($c == $flag) break; + // escape + if ($c == "\\") { + if (str_starts_with($json5, "\r\n")) { + $json5 = substr($json5, 2); + $str .= "\r\n"; + continue; + } + if (str_starts_with($json5, "\n")) { + $json5 = substr($json5, 1); + $str .= "\n"; + continue; + } + if (substr($json5, 0, 1) == $flag) { + $json5 = substr($json5, 1); + $str .= "\\" . $flag; + continue; + } + } + $str .= $c; + } + $str = json_decode('"' . $str . '"'); // extract scape chars... + return $str; +} + +function json5_array(&$json5, $assoc) +{ + // skip '[' + $json5 = substr($json5, 1); + $res = array(); + while ($json5 != '') { + json5_comment($json5); + // Array terminator? + if (strncmp($json5, ']', 1) == 0) { + $json5 = substr($json5, 1); + break; + } + // get value + $res[] = json5_value($json5, $assoc); + // skip comma + $json5 = ltrim($json5); + if (str_starts_with($json5, ',')) { + $json5 = substr($json5, 1); + } + } + return $res; +} + +function json5_object(&$json5, $assoc) +{ + // skip '{' + $json5 = substr($json5, 1); + if ($assoc) { + $res = array(); + } else { + $res = new \stdClass(); + } + + while ($json5 != '') { + json5_comment($json5); + // Object terminator? + if (strncmp($json5, '}', 1) == 0) { + $json5 = substr($json5, 1); + break; + } + // get KEY + $c = substr($json5, 0, 1); + if ($c == '"' || $c == "'") { + $key = json5_string($json5); + json5_token($json5, ':'); + } else { + $key = trim(json5_token($json5, ":")); + } + // get VALUE + $value = json5_value($json5, $assoc); + + if ($assoc) { + $res[$key] = $value; + } else { + $res->$key = $value; + } + + // skip Comma + $json5 = ltrim($json5); + if (strncmp($json5, ',', 1) == 0) { + $json5 = substr($json5, 1); + } + } + return $res; +} + +function json5_token(&$str, $spl) +{ + $i = strpos($str, $spl); + if ($i === FALSE) { + $result = $str; + $str = ""; + return $result; + } + $result = substr($str, 0, $i); + $str = substr($str, $i + strlen($spl)); + return $result; +} + + +/** + * @param $path + * @return array + */ +function controllerFiles($path) +{ + //判断目录是否为空 + if (!file_exists($path)) { + return []; + } + $files = scandir($path); + $fileItem = []; + foreach ($files as $v) { + $newPath = $path . DIRECTORY_SEPARATOR . $v; + if (is_dir($newPath) && $v != '.' && $v != '..') { + $fileItem = array_merge($fileItem, controllerFiles($newPath)); + } else if (is_file($newPath) && str_ends_with($newPath, 'Controller.php')) { + $fileItem[] = $newPath; + } + } + return $fileItem; +} diff --git a/app/Models/User.php b/app/Models/User.php index 8d573b0f3..40d63c24b 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -605,6 +605,9 @@ class User extends AbstractModel case 'anon-msg': $update['nickname'] = '匿名消息'; break; + case 'approval-alert': + $update['nickname'] = '审批'; + break; case 'bot-manager': $update['nickname'] = '机器人管理'; break; diff --git a/app/Models/WorkflowProcMsg.php b/app/Models/WorkflowProcMsg.php new file mode 100644 index 000000000..c61dfc2cf --- /dev/null +++ b/app/Models/WorkflowProcMsg.php @@ -0,0 +1,30 @@ + $value) { + //如果是数组,递归调用 + if (is_array($value)) { + $value = self::arrayKeyToUnderline($value); + } + $newKey = strtolower(preg_replace('/(?<=[a-z])([A-Z])/', '_$1', $key)); + $newArray[$newKey] = $value; + } + return $newArray; + } + + /** + * 多维数组字母转驼峰格式 + * + * @param [type] $array + * @return array + */ + public static function arrayKeyToCamel($array) + { + $newArray = []; + foreach ($array as $key => $value) { + //如果是数组,递归调用 + if (is_array($value)) { + $value = self::arrayKeyToCamel($value); + } + $newKey = lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $key)))); + $newArray[$newKey] = $value; + } + return $newArray; + } } diff --git a/cmd b/cmd index b7957bc67..05e9e1b9b 100755 --- a/cmd +++ b/cmd @@ -393,6 +393,7 @@ if [ $# -gt 0 ]; then elif [[ "$1" == "doc" ]]; then shift 1 run_exec php "php app/Http/Controllers/Api/apidoc.php" + run_exec php "php app/Http/Controllers/Api/apiswagger.php" docker run -it --rm -v ${cur_path}:/home/node/apidoc kuaifan/apidoc -i app/Http/Controllers/Api -o public/docs elif [[ "$1" == "debug" ]]; then shift 1 diff --git a/database/migrations/2023_04_11_122557_create_workflow_proc_msgs.php b/database/migrations/2023_04_11_122557_create_workflow_proc_msgs.php new file mode 100644 index 000000000..a96bd5d6b --- /dev/null +++ b/database/migrations/2023_04_11_122557_create_workflow_proc_msgs.php @@ -0,0 +1,38 @@ +bigIncrements('id'); + $table->bigInteger('proc_inst_id')->nullable()->default(0)->comment('流程实例ID'); + $table->bigInteger('userid')->nullable()->default(0)->comment('会员ID'); + $table->bigInteger('msg_id')->nullable()->default(0)->comment('消息ID'); + $table->timestamps(); + }); + } + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('workflow_proc_msgs', function (Blueprint $table) { + Schema::dropIfExists('workflow_proc_msgs'); + }); + } +} diff --git a/docker-compose.yml b/docker-compose.yml index 81424ff28..3acfef799 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -148,6 +148,24 @@ services: ipv4_address: "${APP_IPPR}.10" restart: unless-stopped + workflow: + container_name: "dootask-workflow-${APP_ID}" + image: "weifashi/go-workflow:1.0.0" + ports: + - "8800:80" + environment: + TZ: "Asia/Shanghai" + MYSQL_HOST: "${DB_HOST}" + MYSQL_PORT: "${DB_PORT}" + MYSQL_DBNAME: "${DB_DATABASE}" + MYSQL_USERNAME: "${DB_USERNAME}" + MYSQL_PASSWORD: "${DB_PASSWORD}" + MYSQL_Prefix: "${DB_PREFIX}workflow_" + networks: + extnetwork: + ipv4_address: "${APP_IPPR}.11" + restart: always + networks: extnetwork: name: "dootask-networks-${APP_ID}" diff --git a/resources/views/push/bot.blade.php b/resources/views/push/bot.blade.php index 83dd031ff..0f1d338d1 100755 --- a/resources/views/push/bot.blade.php +++ b/resources/views/push/bot.blade.php @@ -108,6 +108,49 @@ version: 系统版本 @elseif ($type === 'notice') {{$notice}} +@elseif ($type === 'workflow_reviewer') +{{$data->nickname}}提交的「{{$data->proc_def_name}}」待你审批 + +申请人:{{$data->nickname}} {{$data->department}} + +审批事由 +假期类型:{{$data->type}} +开始时间:{{$data->start_time}} +结束时间:{{$data->end_time}} + + @if ($action === 'pass') + 已同意 + @elseif ($action === 'refuse') + 已拒绝 + @elseif ($action === 'withdraw') + 已撤销 + @else + 同意 + 拒绝 + @endif + +@elseif ($type === 'workflow_notifier') +抄送{{$data->nickname}}提交的「{{$data->proc_def_name}}」记录 + +申请人:{{$data->nickname}} {{$data->department}} + +审批事由 +假期类型:{{$data->type}} +开始时间:{{$data->start_time}} +结束时间:{{$data->end_time}} +查看详情 +@elseif ($type === 'workflow_submitter') + @if ($action === 'pass') + 您发起的「{{$data->proc_def_name}}」已通过 + @else + 您发起的「{{$data->proc_def_name}}」被{{$data->nickname}}拒绝 + @endif + +审批事由 +假期类型:{{$data->type}} +开始时间:{{$data->start_time}} +结束时间:{{$data->end_time}} +查看详情 @else 你好,我是你的机器人助理,你可以发送 /help 查看帮助菜单。 @endif diff --git a/routes/web.php b/routes/web.php index 5c320a075..09b8cd321 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,14 +1,15 @@ middleware(['webapi'])->group(function () { // 公开接口 Route::any('public/{method}', PublicController::class); Route::any('public/{method}/{action}', PublicController::class); + // 审批 + Route::any('workflow/{method}', WorkflowController::class); + Route::any('workflow/{method}/{action}', WorkflowController::class); }); /**