DingService.php 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655
  1. <?php
  2. namespace App\Service;
  3. use App\Model\DDEmployee;
  4. use App\Model\Record;
  5. use Illuminate\Support\Facades\Redis;
  6. class DingService extends Service
  7. {
  8. public function getAccessToken($type)
  9. {
  10. $key = DDEmployee::type_redis[$type];
  11. if(empty($key)) return [false, 'token名称未设置'];
  12. $token = Redis::get($key);
  13. if(! empty($token)) return [true, ['access_token' => $token]];
  14. $config = config("dingtalk.{$type}");
  15. if(empty($config)) return [false, '钉钉配置信息不存在'];
  16. $appKey = $config['app_key'] ?? null;
  17. $appSecret = $config['app_secret'] ?? null;
  18. if (!$appKey) return [false, 'AppKey 配置不完整'];
  19. if (!$appSecret) return [false, 'AppSecret 配置不完整'];
  20. $url = "https://api.dingtalk.com/v1.0/oauth2/accessToken";
  21. $resp = $this->curlOpen1($url, [
  22. 'request' => 'post',
  23. 'header' => ['Content-Type: application/json'],
  24. 'json' => [
  25. "appKey" => $appKey,
  26. "appSecret" => $appSecret
  27. ]
  28. ]);
  29. $res = json_decode($resp, true);
  30. $accessToken = $res['accessToken'] ?? "";
  31. $expires_in = $res['expires_in'] ?? 0;
  32. if(empty($accessToken)) return [false, 'AccessToken获取失败'];
  33. Redis::setex($key, $expires_in, $accessToken);
  34. return [true, ['access_token' => $accessToken]];
  35. }
  36. /**
  37. * 根据前端传来的免登 code 获取用户信息
  38. * @param string $code 前端 dd.getAuthCode 获取的 code
  39. * @return array [bool, data] bool 表示成功与否,data 成功返回用户信息,失败返回错误信息
  40. */
  41. public function getUserByCode($data, $LoginType)
  42. {
  43. if(empty($LoginType)) return [false, 'login_type类型不能为空'];
  44. if(! isset(DDEmployee::type[$LoginType])) return [false, 'login_type类型不存在或错误'];
  45. $code = $data['code'] ?? "";
  46. if (empty($code)) return [false, '钉钉授权code不能为空'];
  47. // 1. 获取 access_token
  48. [$success, $tokenData] = $this->getAccessToken($LoginType);
  49. if (! $success) return [false, $tokenData]; // tokenData 是错误信息
  50. $accessToken = $tokenData['access_token'];
  51. // 2. 用 code 换取用户信息(v2 接口)
  52. $url = "https://oapi.dingtalk.com/topapi/v2/user/getuserinfo?access_token={$accessToken}";
  53. $resp = $this->curlOpen1($url, [
  54. 'request' => 'post',
  55. 'header' => [
  56. "Content-Type: application/json",
  57. ],
  58. 'json' => [
  59. "code" => $code
  60. ]
  61. ]);
  62. $res = json_decode($resp, true);
  63. if (!isset($res['errcode'])) {
  64. return [false, '接口返回异常: ' . $resp];
  65. }
  66. if ($res['errcode'] !== 0) {
  67. return [false, '获取用户信息失败: ' . $res['errmsg']];
  68. }
  69. if(! empty($res['result'])){
  70. $result = $res['result'];
  71. DDEmployee::updateOrCreate(
  72. ['userid' => $result['userid']],
  73. ['name' => $result['name'], 'userid' => $result['userid'], 'login_type' => $LoginType]
  74. );
  75. }
  76. return [true, $res];
  77. }
  78. private function getManDetail($user, $accessToken){
  79. // 3. 根据 userid 获取详细用户信息(包括部门)
  80. $urlDetail = "https://oapi.dingtalk.com/topapi/v2/user/get?access_token={$accessToken}";
  81. $respDetail = $this->curlOpen1($urlDetail, [
  82. 'request' => 'post',
  83. 'header' => ["Content-Type: application/json"],
  84. 'json' => ["userid" => $user['userId']]
  85. ]);
  86. $detail = json_decode($respDetail, true);
  87. if (!isset($detail['errcode'])) return [false, '获取用户详情接口异常: ' . $respDetail];
  88. if ($detail['errcode'] !== 0) return [false, '获取用户详情失败: ' . $detail['errmsg']];
  89. if (empty($detail['result'])) return [false, '获取用户详情失败,结果为空'];
  90. // 返回完整用户信息
  91. return [true, $detail['result']];
  92. }
  93. public function createProcessInstance($data, $user)
  94. {
  95. if(empty($data['type'])) return [false, '单据类型不能为空'];
  96. $type = $data['type'];
  97. if(empty($data['order_number'])) return [false,'订单号不能为空'];
  98. [$success, $msg] = $this->checkCreateProcessInstance($data, $user);
  99. if(! $success) return [false, $msg];
  100. //获取模板id
  101. $code = $this->getModelCode($type, $user);
  102. //获取模板数据
  103. [$success, $formData] = $this->getFormData($data, $user);
  104. if(! $success) return [false, $formData];
  105. // 1. 获取 access_token
  106. [$success, $tokenData] = $this->getAccessToken($user['login_type']);
  107. if (!$success) return [false, $tokenData];
  108. $accessToken = $tokenData['access_token'];
  109. $userId = $user['userId'];
  110. [$success, $userDetail] = $this->getManDetail($user, $accessToken);
  111. if(!$success) return [false, $userDetail];
  112. //创建审批
  113. [$success, $msg] = $this->createFlow($accessToken, $code, $userId, $userDetail, $formData);
  114. if(! $success) return [false, $msg];
  115. //记录信息
  116. $this->recordDatabase($data, $user, $msg);
  117. return [true, ''];
  118. }
  119. private function recordDatabase($data, $user, $process_instance_id){
  120. $type = $data['type'];
  121. Record::insert([
  122. 'type' => $type,
  123. 'login_type' => $user['login_type'],
  124. 'database' => $user['zt_database'],
  125. 'order_number'=> $data['order_number'],
  126. 'crt_time' => time(),
  127. 'process_instance_id' => $process_instance_id
  128. ]);
  129. return [true, ''];
  130. }
  131. private function checkCreateProcessInstance($data, $user){
  132. list($status,$msg) = $this->limitingSendRequestBackgExpire($data['order_number'].$data['type'].$user['zt_database']);
  133. if(! $status) return [false,$msg];
  134. $type = $data['type'];
  135. $login_type = $data['login_type'];
  136. $bool = Record::where('del_time',0)
  137. ->where('type', $type)
  138. ->where('login_type', $login_type)
  139. ->where('database', $user['zt_database'])
  140. ->where('order_number',$data['order_number'])
  141. ->exists();
  142. if($bool) return [false, '单号' . $data['order_number'] . '已创建审批流'];
  143. return [true, ''];
  144. }
  145. private function createFlow($accessToken, $code, $userId, $userDetail, $formData){
  146. // 2. 请求 URL
  147. $url = "https://oapi.dingtalk.com/topapi/processinstance/create?access_token={$accessToken}";
  148. // 3. 请求体
  149. $payload = [
  150. "process_code" => $code, // 审批模板编码
  151. "originator_user_id" => $userId, // 发起人 userId
  152. "dept_id" => $userDetail['dept_id_list'][0], // 发起人部门 ID
  153. "form_component_values" => $formData, // 表单数据
  154. ];
  155. // 4. 发送请求
  156. $resp = $this->curlOpen1($url, [
  157. 'request' => 'post',
  158. 'header' => [
  159. "Content-Type: application/json",
  160. ],
  161. 'json' => $payload
  162. ]);
  163. $res = json_decode($resp, true);
  164. if (!isset($res['errcode'])) {
  165. return [false, "接口返回异常: " . $resp];
  166. }
  167. if ($res['errcode'] !== 0) {
  168. return [false, "创建审批实例失败: " . $res['errmsg']];
  169. }
  170. return [true, $res['process_instance_id']];
  171. }
  172. /**
  173. * 获取当前用户待审批的实例 ID 列表
  174. * @param string $accessToken
  175. * @param string $userId 钉钉用户的 userId
  176. * @param int $cursor 分页游标,从0开始
  177. * @param int $size 分页大小,最大100
  178. */
  179. public function getTodoProcessList($data, $user)
  180. {
  181. [$success, $tokenData] = $this->getAccessToken($user['login_type']);
  182. if (!$success) return [false, $tokenData];
  183. $accessToken = $tokenData['access_token'];
  184. $cursor = $data['cursor'] ?? 0;
  185. $size = $data['size'] ?? 20;
  186. // 1. 请求 URL
  187. $url = "https://oapi.dingtalk.com/topapi/process/todo/list?access_token={$accessToken}";
  188. // 2. 请求体
  189. $payload = [
  190. "userid" => $user['userid'],
  191. "cursor" => $cursor,
  192. "size" => $size,
  193. ];
  194. // 3. 发送请求
  195. $resp = $this->curlOpen1($url, [
  196. 'request' => 'post',
  197. 'header' => ["Content-Type: application/json"],
  198. 'json' => $payload
  199. ]);
  200. $res = json_decode($resp, true);
  201. if (!isset($res['errcode']) || $res['errcode'] !== 0) return [false, "获取待办列表失败: " . ($res['errmsg'] ?? '未知错误')];
  202. // 4. 提取 process_instance_id
  203. // 返回的结果中包含数据列表,我们需要从中提取所有的实例ID
  204. $list = $res['result']['list'] ?? [];
  205. $instanceIds = array_column($list, 'process_instance_id');
  206. $result = Record::whereIn('process_instance_id', $instanceIds)
  207. ->select('type', 'order_number')
  208. ->get()->toArray();
  209. return [true, [
  210. 'list' => $result, // 所有的待我审核
  211. 'has_more' => $res['result']['has_more'] ?? false, // 是否还有更多
  212. 'next_cursor' => $res['result']['next_cursor'] ?? null // 下一页游标
  213. ]];
  214. }
  215. /**
  216. * 执行审批操作(同意/拒绝)
  217. * @param string $accessToken
  218. * @param string $processInstanceId 审批实例ID
  219. * @param string $action 'agree' 为同意,'refuse' 为拒绝
  220. * @param string $remark 审批意见
  221. * @param string $userId 执行审批操作的人员ID
  222. */
  223. public function executeApproval($data, $user)
  224. {
  225. [$success, $tokenData] = $this->getAccessToken($user['login_type']);
  226. if (!$success) return [false, $tokenData];
  227. $accessToken = $tokenData['access_token'];
  228. $processInstanceId = $data['process_instance_id'] ?? 0;
  229. $action = $data['action'] ?? 'agree';
  230. $remark = $data['remark'] ?? '同意';
  231. // 1. 请求 URL
  232. $url = "https://oapi.dingtalk.com/topapi/process/instance/execute?access_token={$accessToken}";
  233. // 2. 构造请求体
  234. $payload = [
  235. "process_instance_id" => $processInstanceId,
  236. "remark" => $remark,
  237. "result" => $action, // 'agree' 或 'refuse'
  238. "task_id" => $this->getTaskIdByInstanceId($accessToken, $processInstanceId, $user['userid']), // 获取当前人的任务ID
  239. ];
  240. // 3. 发送请求
  241. $resp = $this->curlOpen1($url, [
  242. 'request' => 'post',
  243. 'header' => ["Content-Type: application/json"],
  244. 'json' => $payload
  245. ]);
  246. $res = json_decode($resp, true);
  247. if (!isset($res['errcode']) || $res['errcode'] !== 0) {
  248. return [false, "审批操作失败: " . ($res['errmsg'] ?? '未知错误')];
  249. }
  250. return [true, "操作成功"];
  251. }
  252. /**
  253. * 获取指定实例中属于某个用户的待办任务 ID
  254. */
  255. private function getTaskIdByInstanceId($accessToken, $instanceId, $userId)
  256. {
  257. $url = "https://oapi.dingtalk.com/topapi/processinstance/get?access_token={$accessToken}";
  258. $resp = $this->curlOpen1($url, [
  259. 'request' => 'post',
  260. 'header' => ["Content-Type: application/json"],
  261. 'json' => ["process_instance_id" => $instanceId]
  262. ]);
  263. $res = json_decode($resp, true);
  264. $tasks = $res['process_instance']['tasks'] ?? [];
  265. foreach ($tasks as $task) {
  266. // 状态为 RUNNING (进行中) 且 处理人是当前用户
  267. if ($task['task_status'] === 'RUNNING' && $task['userid'] === $userId) {
  268. return $task['task_id'];
  269. }
  270. }
  271. return null;
  272. }
  273. private function getModelCode($type, $user){
  274. $login_type = $user['login_type'];
  275. if($login_type == DDEmployee::type_one){
  276. //浙江
  277. if($type == 1){
  278. // 采购单
  279. $code = "";
  280. }elseif ($type == 2){
  281. // 请购单
  282. $code = "";
  283. }elseif ($type == 3){
  284. // 采购入库单
  285. $code = "";
  286. }elseif ($type == 4){
  287. // 存货
  288. $code = "";
  289. }elseif ($type == 5){
  290. // 供应商
  291. $code = "";
  292. }
  293. }elseif ($login_type == DDEmployee::type_two){
  294. //杭州
  295. if($type == 1){
  296. // 采购单
  297. $code = "";
  298. }elseif ($type == 2){
  299. // 请购单
  300. $code = "";
  301. }elseif ($type == 3){
  302. // 采购入库单
  303. $code = "";
  304. }elseif ($type == 4){
  305. // 存货
  306. $code = "";
  307. }elseif ($type == 5){
  308. // 供应商
  309. $code = "";
  310. }
  311. }
  312. return $code ?? "";
  313. }
  314. private function getFormData($data, $user){
  315. //cs
  316. // $formData = [ [ "name" => "订单日期", "value" => "2025-09-23" ], [ "name" => "订单编号", "value" => "PO20250923001" ], [ "name" => "业务类型", "value" => "标准采购" ], [ "name" => "供应商", "value" => "XX供应商有限公司" ], [ "name" => "制单人", "value" => "陈庆鹏" ], [ "name" => "表体", "value" => json_encode([ [ [ "name" => "存货名称", "value" => "打印机" ], [ "name" => "数量", "value" => "2" ], [ "name" => "主计量单位", "value" => "台" ], [ "name" => "原币价税合计", "value" => "3000" ] ], [ [ "name" => "存货名称", "value" => "显示器" ], [ "name" => "数量", "value" => "5" ], [ "name" => "主计量单位", "value" => "个" ], [ "name" => "原币价税合计", "value" => "5000" ] ] ], JSON_UNESCAPED_UNICODE) ] ];
  317. // return [true, $formData];
  318. //cs
  319. $service = new U8ServerService($user);
  320. $error = $service->getError();
  321. if(! empty($error)) return [false, $error];
  322. [$success, $order] = $service->getOrderDetails($data, $user);
  323. if(! $success) return [false, $order];
  324. $type = $data['type'];
  325. if($type == 1){
  326. // 采购单
  327. $formData = $this->typeOne($order);
  328. }elseif ($type == 2){
  329. // 采购请购单
  330. $formData = $this->typeTwo($order);
  331. }elseif($type == 3){
  332. // 采购入库单
  333. $formData = $this->typeThree($order);
  334. }elseif($type == 4){
  335. // 存货
  336. $formData = $this->typeThree($order);
  337. }elseif($type == 5){
  338. // 供应商
  339. $formData = $this->typeThree($order);
  340. }
  341. if(empty($formData)) return [false, '审批参数不能为空'];
  342. return [true, $formData];
  343. }
  344. private function typeOne($userOrder){
  345. if (empty($userOrder)) return [];
  346. $formData = [
  347. [
  348. "name" => "订单日期",
  349. "value" => date('Y-m-d', strtotime($userOrder['order_date'] ?? ''))
  350. ],
  351. [
  352. "name" => "订单编号",
  353. "value" => $userOrder['order_number'] ?? ''
  354. ],
  355. [
  356. "name" => "业务类型",
  357. "value" => $userOrder['business_type'] ?? ''
  358. ],
  359. [
  360. "name" => "供应商",
  361. "value" => $userOrder['supplier_title'] ?? ''
  362. ],
  363. [
  364. "name" => "制单人",
  365. "value" => $userOrder['crt_name'] ?? ''
  366. ],
  367. [
  368. "name" => "表体", // 对应 TableField 的 label
  369. "value" => json_encode(
  370. array_map(function($item){
  371. return [
  372. [
  373. "name" => "存货名称",
  374. "value" => $item['product_title'] ?? ''
  375. ],
  376. [
  377. "name" => "数量",
  378. "value" => $item['quantity'] ?? 0
  379. ],
  380. [
  381. "name" => "主计量", // 修改这里,对应模板字段
  382. "value" => $item['unit_title'] ?? ''
  383. ],
  384. [
  385. "name" => "原币价税合计",
  386. "value" => $item['amount'] ?? 0
  387. ]
  388. ];
  389. }, $userOrder['detail'] ?? []),
  390. JSON_UNESCAPED_UNICODE
  391. )
  392. ]
  393. ];
  394. return $formData;
  395. }
  396. private function typeTwo($userOrder){
  397. if (empty($userOrder)) return [];
  398. $formData = [
  399. [
  400. "name" => "单据号",
  401. "value" => $userOrder['order_number'] ?? ''
  402. ],
  403. [
  404. "name" => "日期",
  405. "value" => date('Y-m-d', strtotime($userOrder['order_date'] ?? ''))
  406. ],
  407. [
  408. "name" => "业务类型",
  409. "value" => $userOrder['business_type'] ?? ''
  410. ],
  411. [
  412. "name" => "请购人",
  413. "value" => $userOrder['purchase_name'] ?? ''
  414. ],
  415. [
  416. "name" => "制单人",
  417. "value" => $userOrder['crt_name'] ?? ''
  418. ],
  419. [
  420. "name" => "表体", // 对应 TableField 的 label
  421. "value" => json_encode(
  422. array_map(function($item){
  423. return [
  424. [
  425. "name" => "存货名称",
  426. "value" => $item['product_title'] ?? ''
  427. ],
  428. [
  429. "name" => "数量",
  430. "value" => $item['quantity'] ?? 0
  431. ],
  432. [
  433. "name" => "主计量", // 修改这里,对应模板字段
  434. "value" => $item['unit_title'] ?? ''
  435. ],
  436. [
  437. "name" => "要求到货日期",
  438. "value" => $item['need_arrived_date'] ?? ''
  439. ]
  440. ];
  441. }, $userOrder['detail'] ?? []),
  442. JSON_UNESCAPED_UNICODE
  443. )
  444. ]
  445. ];
  446. return $formData;
  447. }
  448. private function typeThree($userOrder){
  449. if (empty($userOrder)) return [];
  450. $formData = [
  451. [
  452. "name" => "单据编号",
  453. "value" => $userOrder['order_number'] ?? ''
  454. ],
  455. [
  456. "name" => "日期",
  457. "value" => date('Y-m-d', strtotime($userOrder['order_date'] ?? ''))
  458. ],
  459. [
  460. "name" => "供应商",
  461. "value" => $userOrder['supplier_title'] ?? ''
  462. ],
  463. [
  464. "name" => "制单人",
  465. "value" => $userOrder['crt_name'] ?? ''
  466. ],
  467. [
  468. "name" => "总金额",
  469. "value" => $userOrder['total_amount'] ?? 0
  470. ],
  471. [
  472. "name" => "表体", // 对应 TableField 的 label
  473. "value" => json_encode(
  474. array_map(function($item){
  475. return [
  476. [
  477. "name" => "来源",
  478. "value" => $item['source'] ?? ''
  479. ],
  480. [
  481. "name" => "来源单据号",
  482. "value" => $item['source_order_number'] ?? ''
  483. ],
  484. [
  485. "name" => "存货名称",
  486. "value" => $item['product_title'] ?? ''
  487. ],
  488. [
  489. "name" => "数量",
  490. "value" => $item['number'] ?? 0,
  491. ],
  492. [
  493. "name" => "主计量", // 修改这里,对应模板字段
  494. "value" => $item['unit_title'] ?? ''
  495. ],
  496. [
  497. "name" => "申请金额",
  498. "value" => $item['amount'] ?? 0
  499. ]
  500. ];
  501. }, $userOrder['detail'] ?? []),
  502. JSON_UNESCAPED_UNICODE
  503. )
  504. ]
  505. ];
  506. return $formData;
  507. }
  508. public function getTemplateFields($data)
  509. {
  510. $processCode = $data['code'] ?? "";
  511. if (empty($processCode)) {
  512. return [false, '模板编号 process_code 不能为空'];
  513. }
  514. [$ok, $tokenData] = $this->getAccessToken();
  515. if (! $ok) return [false, $tokenData];
  516. $accessToken = $tokenData['access_token'];
  517. // 注意这里是 GET,并且 processCode 是 query 参数
  518. $url = "https://api.dingtalk.com/v1.0/workflow/forms/schemas/processCodes?processCode={$processCode}";
  519. $resp = $this->curlOpen1($url, [
  520. 'request' => 'get',
  521. 'header' => [
  522. "Content-Type: application/json",
  523. "x-acs-dingtalk-access-token: {$accessToken}"
  524. ],
  525. ]);
  526. $res = json_decode($resp, true);
  527. if (isset($res['schemas'])) {
  528. return [true, $res['schemas']];
  529. } else {
  530. return [false, $res];
  531. }
  532. }
  533. protected function curlOpen1($url, $config = [])
  534. {
  535. $default = [
  536. 'post' => false,
  537. 'request' => 'get',
  538. 'header' => [],
  539. 'json' => null,
  540. 'timeout' => 30
  541. ];
  542. $arr = array_merge($default, $config);
  543. $ch = curl_init();
  544. curl_setopt($ch, CURLOPT_URL, $url);
  545. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  546. curl_setopt($ch, CURLOPT_TIMEOUT, $arr['timeout']);
  547. curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
  548. curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
  549. if (!empty($arr['header'])) {
  550. curl_setopt($ch, CURLOPT_HTTPHEADER, $arr['header']);
  551. }
  552. if ($arr['post'] || $arr['request'] !== 'get') {
  553. curl_setopt($ch, CURLOPT_CUSTOMREQUEST, strtoupper($arr['request']));
  554. if ($arr['json'] !== null) {
  555. curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($arr['json'], JSON_UNESCAPED_UNICODE));
  556. }
  557. }
  558. $result = curl_exec($ch);
  559. if ($result === false) {
  560. $err = curl_error($ch);
  561. curl_close($ch);
  562. return json_encode(['error' => $err]);
  563. }
  564. curl_close($ch);
  565. return $result;
  566. }
  567. }