DingService.php 22 KB

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