General.php 45 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249
  1. <?php
  2. namespace addons\workorder\library;
  3. use think\Db;
  4. use think\Exception;
  5. use think\exception\PDOException;
  6. use think\Lang;
  7. use think\Validate;
  8. use app\common\library\Email;
  9. use app\common\library\Sms as Smslib;
  10. use addons\workorder\model\Orders;
  11. use addons\workorder\model\Records;
  12. use addons\workorder\model\Engineer;
  13. class General
  14. {
  15. protected static $instance = null;
  16. protected static $fileField = ['image', 'images', 'file', 'files'];
  17. protected static $imgArr = ['jpg', 'png', 'bmp', 'jpeg', 'gif'];
  18. protected $workorderConfig = [];
  19. public function __construct()
  20. {
  21. $this->workorderConfig = get_addon_config('workorder');
  22. $this->workorderConfig['notice_mail'] = isset($this->workorderConfig['notice_mail']) ? explode(',', $this->workorderConfig['notice_mail']) : [];
  23. $this->workorderConfig['notice_sms'] = isset($this->workorderConfig['notice_sms']) ? explode(',', $this->workorderConfig['notice_sms']) : [];
  24. Lang::load(ROOT_PATH . 'addons' . DS . 'workorder' . DS . 'lang' . DS . \think\Request::instance()->langset() . '.php');
  25. }
  26. public static function instance($options = [])
  27. {
  28. if (is_null(self::$instance)) {
  29. self::$instance = new static($options);
  30. }
  31. return self::$instance;
  32. }
  33. /**
  34. * 计算一个工程师的平均回复时间
  35. * 通常在有新的回复时调用
  36. * @param int $engineerId 工程师ID
  37. * @return float
  38. * @throws \think\Exception
  39. * @throws \think\exception\PDOException
  40. */
  41. public static function calcEngineerAvgResTime($engineerId)
  42. {
  43. $workorderTimeStatistics = Db::name('workorder_time_statistics')
  44. ->alias('s')
  45. ->join('workorder_orders o', 'o.id=s.order_id')
  46. ->where('o.deletetime', null)
  47. ->where('s.type', 'in', '1,2')
  48. ->where('s.engineer_id', $engineerId)
  49. ->where('s.time_consum', '>', '0')
  50. ->avg('s.time_consum');
  51. Db::name('workorder_engineers')->where('id', $engineerId)->update([
  52. 'avg_response_time' => ceil($workorderTimeStatistics)
  53. ]);
  54. return $workorderTimeStatistics;
  55. }
  56. public static function orderNumberChangeCalcEngineerStatistics($orderId, $engineer = false, $type = 'del')
  57. {
  58. if ($engineer) {
  59. self::calcEngineerAvgResTime($engineer);
  60. }
  61. // 沟通记录
  62. $records = Records::where('order_id', $orderId)
  63. ->order('createtime asc')
  64. ->select();
  65. // 共有哪些工程师
  66. $engineers = [];
  67. if ($engineer) {
  68. $engineers[$engineer] = $engineer;
  69. }
  70. foreach ($records as $index => $record) {
  71. if ($record->engineer_id) {
  72. $engineers[$record->engineer_id] = $record->engineer_id;
  73. }
  74. }
  75. $engineers = Engineer::select($engineers);
  76. Db::startTrans();
  77. try {
  78. if ($type == 'del') {
  79. foreach ($engineers as $item) {
  80. $item->work_order_quantity--;
  81. $item->save();
  82. }
  83. } elseif ($type == 'restore') {
  84. foreach ($engineers as $item) {
  85. $item->work_order_quantity++;
  86. $item->save();
  87. }
  88. }
  89. Db::commit();
  90. } catch (PDOException | Exception $e) {
  91. Db::rollback();
  92. return false;
  93. }
  94. return true;
  95. }
  96. public function autoClose()
  97. {
  98. if ($this->workorderConfig['auto_close'] == 0) {
  99. return true;
  100. }
  101. $nowTime = time();
  102. $orders = Orders::all(['status' => '3']);
  103. foreach ($orders as $index => $order) {
  104. $userLastRecordTime = Records::where('order_id', $order->id)
  105. ->where('user_id', '>', 0)
  106. ->order('createtime desc')
  107. ->value('createtime');
  108. $autoCloseTime = $userLastRecordTime + (int)($this->workorderConfig['auto_close'] * 3600);
  109. if ($autoCloseTime <= time()) {
  110. $order->status = '4';
  111. $order->save();
  112. $record = [
  113. 'order_id' => $order->id,
  114. 'engineer_id' => $order->engineer_id,
  115. 'message_type' => 3,
  116. 'message' => __('The job has been closed automatically!')
  117. ];
  118. Records::create($record);
  119. // 记录结单耗时
  120. $timeStatistics = Db::name('workorder_time_statistics')
  121. ->where('order_id', $order->id)
  122. ->where('type', 0)
  123. ->where('time_consum', null)
  124. ->find();
  125. if ($timeStatistics) {
  126. Db::name('workorder_time_statistics')->where('id', $timeStatistics['id'])->update([
  127. 'endtime' => $nowTime,
  128. 'time_consum' => $nowTime - $timeStatistics['starttime'],
  129. 'engineer_id' => $order->engineer_id,// 防转移,冲正为当前工程师
  130. ]);
  131. }
  132. }
  133. }
  134. return true;
  135. }
  136. /**
  137. * 发送邮件通知
  138. * @param object $row 工单
  139. * @param string $event 事件
  140. * @return bool
  141. * @throws \think\exception\DbException
  142. */
  143. public function mailNotice($row, $event, $subject, $message)
  144. {
  145. if (!in_array($event, $this->workorderConfig['notice_mail'])) {
  146. return false;
  147. }
  148. if ($event == 'user_order_handle' || $event == 'user_got_reply') {
  149. if (!isset($row->email) || !isset($row->remind) || !$row->email || ($row->remind != 2)) {
  150. return false;
  151. }
  152. } else {
  153. if (!$row->engineer_id) {
  154. return false;
  155. }
  156. $row = \addons\workorder\model\Engineer::get($row->engineer_id);
  157. $row->email = $row->user->email;
  158. }
  159. if (!Validate::is($row->email, "email")) {
  160. return false;
  161. }
  162. $email = new Email;
  163. $result = $email->to($row->email)
  164. ->subject($subject)
  165. ->message('<div style="min-height:550px; padding: 100px 55px 200px;">' . $message . '</div>')
  166. ->send();
  167. if ($result) {
  168. return true;
  169. } else {
  170. //$email->getError();
  171. return false;
  172. }
  173. }
  174. /**
  175. * 发送短信通知
  176. * @param object $row 工单
  177. * @param string $event 事件
  178. * @return bool
  179. * @throws \think\exception\DbException
  180. */
  181. public function smsNotice($row, $event)
  182. {
  183. if (!in_array($event, $this->workorderConfig['notice_sms'])) {
  184. return false;
  185. }
  186. // 最多12个字符
  187. $msg = mb_strlen($row->title) > 12 ? mb_substr($row->title, 0, 9) . '...' : $row->title;
  188. // 兼容短信宝和创蓝
  189. $smsbao = get_addon_info('smsbao');
  190. $clsms = get_addon_info('clsms');
  191. if (($smsbao && $smsbao['state'] == 1) || ($clsms && $clsms['state'] == 1)) {
  192. switch ($event) {
  193. case 'user_order_handle':
  194. $msg = __('The engineer has viewed the work order you submitted:%s and is processing it.', [$msg]);
  195. break;
  196. case 'user_got_reply':
  197. $msg = __('The engineer has replied to your work order:%s, please check the feedback in time.', [$msg]);
  198. break;
  199. case 'engineer_new_order':
  200. $msg = __('Dear engineer, you have received a new work order:%s, please handle it in time.', [$msg]);
  201. break;
  202. case 'engineer_got_reply':
  203. $msg = __('Dear engineer, work order:%s, user feedback has been received, please handle it in time.', [$msg]);
  204. break;
  205. case 'engineer_urging':
  206. $msg = __('Dear engineer, work order:%s, the user hopes you will reply as soon as possible.', [$msg]);
  207. break;
  208. default:
  209. $msg = __('Sending scenario not recognized by the work order system');
  210. break;
  211. }
  212. }
  213. if ($event == 'user_order_handle' || $event == 'user_got_reply') {
  214. if (!isset($row->mobile) || !isset($row->remind) || !$row->mobile || ($row->remind != 1)) {
  215. return false;
  216. }
  217. } else {
  218. if (!$row->engineer_id) {
  219. return false;
  220. }
  221. $row = \addons\workorder\model\Engineer::get($row->engineer_id);
  222. $row->mobile = $row->user->mobile;
  223. }
  224. if (!$row->mobile || !Validate::regex($row->mobile, "^1\d{10}$")) {
  225. return false;
  226. }
  227. if (!\think\Hook::get('sms_send')) {
  228. return false;
  229. }
  230. // 兼容创蓝
  231. if ($clsms && $clsms['state'] == 1) {
  232. $clsms = new \addons\clsms\library\Clsms();
  233. $result = $clsms->smstype(0)->mobile($row->mobile)->msg($msg)->send();
  234. if ($result) {
  235. return true;
  236. } else {
  237. return false;
  238. }
  239. }
  240. // 兼容阿里云短信
  241. $alisms = get_addon_info('alisms');
  242. if ($alisms && $alisms['state'] == 1) {
  243. $params = [
  244. 'mobile' => $row->mobile,
  245. 'msg' => [
  246. 'title' => $msg
  247. ],
  248. 'event' => $event
  249. ];
  250. $ret = \think\Hook::listen('sms_notice', $params, null, true);
  251. } else {
  252. $ret = Smslib::notice($row->mobile, $msg, $event);
  253. }
  254. if ($ret) {
  255. return true;
  256. } else {
  257. return false;
  258. }
  259. }
  260. /**
  261. * 获取文件后缀
  262. * @param string $filename 文件路径/名称
  263. * @return string
  264. */
  265. public static function getFileExtension($filename)
  266. {
  267. $filename = explode('.', $filename);
  268. return end($filename);
  269. }
  270. /**
  271. * 生成文件后缀图片
  272. * @param string $suffix 后缀
  273. * @param null $background
  274. * @return string
  275. */
  276. public static function buildSuffixImage($suffix, $background = null)
  277. {
  278. $suffix = mb_substr(strtoupper($suffix), 0, 4);
  279. $total = unpack('L', hash('adler32', $suffix, true))[1];
  280. $hue = $total % 360;
  281. list($r, $g, $b) = hsv2rgb($hue / 360, 0.3, 0.9);
  282. $background = $background ? $background : "rgb({$r},{$g},{$b})";
  283. $icon = <<<EOT
  284. <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
  285. <path style="fill:#E2E5E7;" d="M128,0c-17.6,0-32,14.4-32,32v448c0,17.6,14.4,32,32,32h320c17.6,0,32-14.4,32-32V128L352,0H128z"/>
  286. <path style="fill:#B0B7BD;" d="M384,128h96L352,0v96C352,113.6,366.4,128,384,128z"/>
  287. <polygon style="fill:#CAD1D8;" points="480,224 384,128 480,128 "/>
  288. <path style="fill:{$background};" d="M416,416c0,8.8-7.2,16-16,16H48c-8.8,0-16-7.2-16-16V256c0-8.8,7.2-16,16-16h352c8.8,0,16,7.2,16,16 V416z"/>
  289. <path style="fill:#CAD1D8;" d="M400,432H96v16h304c8.8,0,16-7.2,16-16v-16C416,424.8,408.8,432,400,432z"/>
  290. <g><text><tspan x="220" y="380" font-size="124" font-family="Verdana, Helvetica, Arial, sans-serif" fill="white" text-anchor="middle">{$suffix}</tspan></text></g>
  291. </svg>
  292. EOT;
  293. return $icon;
  294. }
  295. /**
  296. * 清理xss
  297. * @param string $val
  298. * @return string
  299. */
  300. public static function removeXss($val)
  301. {
  302. if (!$val) {
  303. return '';
  304. }
  305. if (function_exists('xss_clean')) {
  306. return xss_clean($val);
  307. }
  308. $val = preg_replace('/([\x00-\x08,\x0b-\x0c,\x0e-\x19])/', '', $val);
  309. $search = 'abcdefghijklmnopqrstuvwxyz';
  310. $search .= 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
  311. $search .= '1234567890!@#$%^&*()';
  312. $search .= '~`";:?+/={}[]-_|\'\\';
  313. for ($i = 0; $i < strlen($search); $i++) {
  314. $val = preg_replace('/(&#[xX]0{0,8}' . dechex(ord($search[$i])) . ';?)/i', $search[$i], $val); // with a ;
  315. $val = preg_replace('/(&#0{0,8}' . ord($search[$i]) . ';?)/', $search[$i], $val); // with a ;
  316. }
  317. $ra1 = [
  318. 'javascript',
  319. 'vbscript',
  320. 'expression',
  321. 'applet',
  322. 'meta',
  323. 'xml',
  324. 'blink',
  325. 'link',
  326. 'style',
  327. 'script',
  328. 'embed',
  329. 'object',
  330. 'iframe',
  331. 'frame',
  332. 'frameset',
  333. 'ilayer',
  334. 'layer',
  335. 'bgsound',
  336. 'title',
  337. 'base'
  338. ];
  339. $ra2 = [
  340. 'onabort',
  341. 'onactivate',
  342. 'onafterprint',
  343. 'onafterupdate',
  344. 'onbeforeactivate',
  345. 'onbeforecopy',
  346. 'onbeforecut',
  347. 'onbeforedeactivate',
  348. 'onbeforeeditfocus',
  349. 'onbeforepaste',
  350. 'onbeforeprint',
  351. 'onbeforeunload',
  352. 'onbeforeupdate',
  353. 'onblur',
  354. 'onbounce',
  355. 'oncellchange',
  356. 'onchange',
  357. 'onclick',
  358. 'oncontextmenu',
  359. 'oncontrolselect',
  360. 'oncopy',
  361. 'oncut',
  362. 'ondataavailable',
  363. 'ondatasetchanged',
  364. 'ondatasetcomplete',
  365. 'ondblclick',
  366. 'ondeactivate',
  367. 'ondrag',
  368. 'ondragend',
  369. 'ondragenter',
  370. 'ondragleave',
  371. 'ondragover',
  372. 'ondragstart',
  373. 'ondrop',
  374. 'onerror',
  375. 'onerrorupdate',
  376. 'onfilterchange',
  377. 'onfinish',
  378. 'onfocus',
  379. 'onfocusin',
  380. 'onfocusout',
  381. 'onhelp',
  382. 'onkeydown',
  383. 'onkeypress',
  384. 'onkeyup',
  385. 'onlayoutcomplete',
  386. 'onload',
  387. 'onlosecapture',
  388. 'onmousedown',
  389. 'onmouseenter',
  390. 'onmouseleave',
  391. 'onmousemove',
  392. 'onmouseout',
  393. 'onmouseover',
  394. 'onmouseup',
  395. 'onmousewheel',
  396. 'onmove',
  397. 'onmoveend',
  398. 'onmovestart',
  399. 'onpaste',
  400. 'onpropertychange',
  401. 'onreadystatechange',
  402. 'onreset',
  403. 'onresize',
  404. 'onresizeend',
  405. 'onresizestart',
  406. 'onrowenter',
  407. 'onrowexit',
  408. 'onrowsdelete',
  409. 'onrowsinserted',
  410. 'onscroll',
  411. 'onselect',
  412. 'onselectionchange',
  413. 'onselectstart',
  414. 'onstart',
  415. 'onstop',
  416. 'onsubmit',
  417. 'onunload'
  418. ];
  419. $ra = array_merge($ra1, $ra2);
  420. $found = true;
  421. while ($found == true) {
  422. $val_before = $val;
  423. for ($i = 0; $i < sizeof($ra); $i++) {
  424. $pattern = '/';
  425. for ($j = 0; $j < strlen($ra[$i]); $j++) {
  426. if ($j > 0) {
  427. $pattern .= '(';
  428. $pattern .= '(&#[xX]0{0,8}([9ab]);)';
  429. $pattern .= '|';
  430. $pattern .= '|(&#0{0,8}([9|10|13]);)';
  431. $pattern .= ')*';
  432. }
  433. $pattern .= $ra[$i][$j];
  434. }
  435. $pattern .= '/i';
  436. $replacement = substr($ra[$i], 0, 2) . '<k>' . substr($ra[$i], 2);
  437. $val = preg_replace($pattern, $replacement, $val);
  438. if ($val_before == $val) {
  439. $found = false;
  440. }
  441. }
  442. }
  443. return $val;
  444. }
  445. public static function createOrder($row, $userId)
  446. {
  447. $fields = Orders::getFields(null, 0);
  448. $records = [];
  449. // 要入沟通记录的字段-所有文件自动入沟通记录
  450. $otherField = '';
  451. $recordField = [
  452. 'describe' => '0',// 描述-富文本
  453. 'confidential' => '4',// 机密信息-机密信息
  454. ];
  455. foreach ($fields as $index => $field) {
  456. if ($field['isrequire'] && $field['type_list'] != 'text' && (!isset($row[$field['name']]) || $row[$field['name']] == '')) {
  457. return ['code' => 0, 'msg' => __('%s can not be empty!', $field['title'])];
  458. }
  459. if (isset($row[$field['name']]) && is_array($row[$field['name']])) {
  460. $row[$field['name']] = implode(',', $row[$field['name']]);
  461. }
  462. if ($field['type_list'] == 'editor') {
  463. $fieldOrigin = \think\Request::instance()->param($field['name'], '', 'trim');
  464. if (!$fieldOrigin) {
  465. $fieldOrigin = \think\Request::instance()->param('row/a', '', 'trim');
  466. $fieldOrigin = isset($fieldOrigin[$field['name']]) ? $fieldOrigin[$field['name']] : '';
  467. }
  468. $row[$field['name']] = self::removeXss($fieldOrigin);
  469. }
  470. // 将uniapp端的城市名与数据库中的进行兼容
  471. if ($field['type_list'] == 'city' && isset($row[$field['name']]) && $row[$field['name']]) {
  472. $city = explode('/', $row[$field['name']]);
  473. if ($city && mb_strpos($city[0], '市') !== false) {
  474. $city[1] = $city[0];
  475. $city[0] = mb_substr($city[0], 0, mb_strlen($city[0]) - 1);
  476. $row[$field['name']] = implode('/', $city);
  477. }
  478. }
  479. if (array_key_exists($field['name'], $recordField) && isset($row[$field['name']]) && $row[$field['name']]) {
  480. $records[] = [
  481. 'user_id' => $userId,
  482. 'message_type' => $recordField[$field['name']],
  483. 'message' => $row[$field['name']]
  484. ];
  485. }
  486. if (in_array($field['type_list'], self::$fileField) && isset($row[$field['name']]) && $row[$field['name']]) {
  487. $attachment = explode(',', trim($row[$field['name']], ','));
  488. $message_type = ($field['type_list'] == 'image' || $field['type_list'] == 'images') ? 1 : 2;
  489. foreach ($attachment as $item) {
  490. $itemInfo = pathinfo($item);
  491. if (in_array($itemInfo['extension'], self::$imgArr)) {
  492. $message_type = 1;
  493. }
  494. $records[] = [
  495. 'user_id' => $userId,
  496. 'message_type' => $message_type,
  497. 'message' => $item
  498. ];
  499. }
  500. }
  501. // 非聊天记录字段且非文件字段且非基本信息字段,对值进行格式化后写入聊天记录
  502. if (!array_key_exists($field['name'], $recordField) && !in_array($field['type_list'], self::$fileField) && !$field['isbasicinfo']) {
  503. $fieldValue = self::fieldValue($field, $row[$field['name']]);
  504. if ($fieldValue) {
  505. $otherField .= $field['title'] . ':' . $fieldValue . '<br />';
  506. }
  507. }
  508. }
  509. if ($otherField) {
  510. $records[] = [
  511. 'user_id' => $userId,
  512. 'message_type' => 0,
  513. 'message' => $otherField
  514. ];
  515. }
  516. $row['user_id'] = $userId;
  517. $row['status'] = 0;
  518. $row['createtime'] = time();
  519. $row['updatetime'] = time();
  520. try {
  521. $order = Orders::create($row);
  522. if ($records) {
  523. foreach ($records as $index => $record) {
  524. $records[$index]['batch'] = 1;
  525. $records[$index]['order_id'] = $order->id;
  526. }
  527. $recordsModel = new Records;
  528. $recordsModel->saveAll($records);
  529. }
  530. // 分派工单
  531. $General = General::instance();
  532. $engineerId = $General->distribution($order, $userId);
  533. // 准备记录结单和首回耗时
  534. $timeStatistics[0] = [
  535. 'type' => 0,
  536. 'order_id' => $order->id,
  537. 'engineer_id' => $engineerId,
  538. 'starttime' => time()
  539. ];
  540. $timeStatistics[1] = $timeStatistics[0];
  541. $timeStatistics[1]['type'] = 2;
  542. Db::name('workorder_time_statistics')->insertAll($timeStatistics);
  543. } catch (Exception $e) {
  544. return ['code' => 0, 'msg' => 'error:' . $e->getMessage()];
  545. }
  546. return [
  547. 'code' => 1,
  548. 'data' => ['id' => $order->id]
  549. ];
  550. }
  551. public static function formatFileSize($num)
  552. {
  553. $p = 0;
  554. $format = 'bytes';
  555. if ($num > 0 && $num < 1024) {
  556. $p = 0;
  557. return number_format($num) . ' ' . $format;
  558. }
  559. if ($num >= 1024 && $num < pow(1024, 2)) {
  560. $p = 1;
  561. $format = 'KB';
  562. }
  563. if ($num >= pow(1024, 2) && $num < pow(1024, 3)) {
  564. $p = 2;
  565. $format = 'MB';
  566. }
  567. if ($num >= pow(1024, 3) && $num < pow(1024, 4)) {
  568. $p = 3;
  569. $format = 'GB';
  570. }
  571. if ($num >= pow(1024, 4) && $num < pow(1024, 5)) {
  572. $p = 3;
  573. $format = 'TB';
  574. }
  575. $num /= pow(1024, $p);
  576. return number_format($num, 2) . ' ' . $format;
  577. }
  578. public function createEvaluate($order, $row, $userId)
  579. {
  580. $nowTime = time();
  581. if (!$row['stars']) {
  582. return ['code' => 0, 'msg' => __('Please select the overall evaluation~')];
  583. } elseif (!isset($row['solved'])) {
  584. return ['code' => 0, 'msg' => __('Please select whether the problem has been solved~')];
  585. }
  586. $row['order_id'] = $order->id;
  587. $row['category_id'] = $order->category_id;
  588. $row['user_id'] = $userId;
  589. $row['createtime'] = $nowTime;
  590. if (Db::name('workorder_evaluate')->insert($row)) {
  591. $order->status = 5;
  592. $order->save();
  593. // 记录结单耗时
  594. $timeStatistics = Db::name('workorder_time_statistics')
  595. ->where('order_id', $order->id)
  596. ->where('type', 0)
  597. ->where('time_consum', null)
  598. ->find();
  599. if ($timeStatistics) {
  600. Db::name('workorder_time_statistics')->where('id', $timeStatistics['id'])->update([
  601. 'endtime' => $nowTime,
  602. 'time_consum' => $nowTime - $timeStatistics['starttime'],
  603. 'engineer_id' => $order->engineer_id,// 防转移,冲正为当前工程师
  604. ]);
  605. }
  606. return ['code' => 1, 'msg' => __('Evaluation submitted successfully~')];
  607. } else {
  608. return ['code' => 0, 'msg' => __('Evaluation failed, please try again!')];
  609. }
  610. }
  611. /**
  612. * 回复工单
  613. * @param object $order 工单数据
  614. * @param object $user 用户数据
  615. * @param object $engineer 工程师数据
  616. * @param array $row 回复数据
  617. * @var $order Orders 实例
  618. */
  619. public function createReply($order, $user, $engineer, $row)
  620. {
  621. $nowTime = time();
  622. $replyField = $order->getFields(null, $user ? 1 : 2);
  623. // 要入沟通记录的字段-所有文件自动入沟通记录
  624. $recordField = [
  625. 'reply_describe' => '0',// 描述-富文本
  626. 'reply_confidential' => '4',// 机密信息-机密信息
  627. ];
  628. $records = [];
  629. $otherField = '';
  630. foreach ($replyField as $index => $field) {
  631. if ($field['isrequire'] && (!isset($row[$field['name']]) || $row[$field['name']] == '')) {
  632. return ['code' => 0, 'msg' => __('%s can not be empty!', $field['title'])];
  633. }
  634. if ($field['type_list'] == 'editor') {
  635. $fieldOrigin = \think\Request::instance()->param($field['name'], '', 'trim');
  636. if (!$fieldOrigin) {
  637. $fieldOrigin = \think\Request::instance()->param('row/a', '', 'trim');
  638. $fieldOrigin = $fieldOrigin[$field['name']] ?? '';
  639. }
  640. $row[$field['name']] = self::removeXss($fieldOrigin);
  641. }
  642. if (array_key_exists($field['name'], $recordField) && isset($row[$field['name']]) && $row[$field['name']]) {
  643. $records[] = [
  644. 'message_type' => $recordField[$field['name']],
  645. 'message' => $row[$field['name']]
  646. ];
  647. }
  648. if (in_array($field['type_list'], self::$fileField) && isset($row[$field['name']]) && $row[$field['name']]) {
  649. $attachment = explode(',', trim($row[$field['name']], ','));
  650. $message_type = ($field['type_list'] == 'image' || $field['type_list'] == 'images') ? 1 : 2;
  651. foreach ($attachment as $item) {
  652. $itemInfo = pathinfo($item);
  653. if (in_array($itemInfo['extension'], self::$imgArr)) {
  654. $message_type = 1;
  655. }
  656. $records[] = [
  657. 'message_type' => $message_type,
  658. 'message' => $item
  659. ];
  660. }
  661. }
  662. // 非回复字段且非文件字段,对值进行格式化后写入聊天记录
  663. if (!array_key_exists($field['name'], $recordField) && !in_array($field['type_list'], self::$fileField)) {
  664. $fieldValue = self::fieldValue($field, $row[$field['name']]);
  665. if ($fieldValue) {
  666. $otherField .= $field['title'] . ':' . $fieldValue . '<br />';
  667. }
  668. }
  669. }
  670. if ($otherField) {
  671. $records[] = [
  672. 'message_type' => 0,
  673. 'message' => $otherField
  674. ];
  675. }
  676. $batch = Records::where('order_id', $order->id)->max('batch');
  677. foreach ($records as $index => $record) {
  678. $records[$index]['order_id'] = $order->id;
  679. $records[$index]['batch'] = $batch + 1;
  680. if ($user) {
  681. $records[$index]['sender'] = 'user';
  682. $records[$index]['user_id'] = $user->id;
  683. $records[$index]['nickname'] = $user->nickname;
  684. $records[$index]['avatar'] = cdnurl($user->avatar, true);
  685. } elseif ($engineer) {
  686. $records[$index]['sender'] = 'engineer';
  687. $records[$index]['engineer_id'] = $engineer->id;
  688. $records[$index]['title'] = $engineer->title;
  689. $records[$index]['avatar'] = ($engineer->user && $engineer->user->avatar) ? cdnurl($engineer->user->avatar, true) : (function_exists('letter_avatar') ? letter_avatar($engineer->title) : cdnurl('/assets/img/avatar.png', true));
  690. }
  691. }
  692. $recordsModel = new Records;
  693. $records = $recordsModel->allowField(true)->saveAll($records);
  694. if ($records) {
  695. $timeStatistics = Db::name('workorder_time_statistics')
  696. ->where('order_id', $order->id)
  697. ->where('type', 'in', '1,2')
  698. ->where('time_consum', null)
  699. ->find();
  700. $order->title = $order->title ?? __('Untitled');
  701. if ($engineer) {
  702. if ($order->status == 2) {
  703. $order->status = 3;
  704. $this->mailNotice($order, 'user_got_reply', __('[new reply received for work order]') . $order->title, __('Work order:%s the engineer has replied to your question. Please check / feed back in time.', [$order->title]));
  705. $this->smsNotice($order, 'user_got_reply');
  706. }
  707. if ($order->lasturgingtime) {
  708. $order->lasturgingtime = null;
  709. }
  710. // 计算回复时间
  711. if ($timeStatistics) {
  712. Db::name('workorder_time_statistics')->where('id', $timeStatistics['id'])->update([
  713. 'endtime' => $nowTime,
  714. 'time_consum' => $nowTime - $timeStatistics['starttime'],
  715. 'engineer_id' => $order->engineer_id,// 防转移,冲正为当前工程师
  716. ]);
  717. $this->calcEngineerAvgResTime($order->engineer_id);
  718. }
  719. } else {
  720. if ($order->status == 3) {
  721. $order->status = 2;
  722. $this->mailNotice($order, 'engineer_got_reply', __('[work order receives new feedback]') . $order->title, __('Work order:%s the user has fed back new information, please check / reply in time.', [$order->title]));
  723. $this->smsNotice($order, 'engineer_got_reply');
  724. }
  725. // 准备记录回复时间
  726. if (!$timeStatistics) {
  727. $timeStatistics = [
  728. 'type' => 1,
  729. 'order_id' => $order->id,
  730. 'engineer_id' => $order->engineer_id,
  731. 'starttime' => time()
  732. ];
  733. Db::name('workorder_time_statistics')->insert($timeStatistics);
  734. }
  735. }
  736. $order->allowField(true)->save();
  737. return [
  738. 'code' => 1,
  739. 'msg' => __('Reply Success~'),
  740. 'data' => [
  741. 'records' => $records
  742. ]
  743. ];
  744. }
  745. return ['code' => 0, 'msg' => __('Nothing happened~')];
  746. }
  747. public function transfer($order, $transferEngineer)
  748. {
  749. $engineer = Engineer::get($transferEngineer);
  750. if (!$engineer->user_id) {
  751. return ['code' => 0, 'msg' => __('The engineer has not bound users!')];
  752. }
  753. if ($engineer->user_id == $order->user_id) {
  754. return ['code' => 0, 'msg' => __('Engineer and issuer cannot be the same person!')];
  755. }
  756. if ($engineer && $engineer->status == '1') {
  757. $order->engineer_id = $transferEngineer;
  758. $order->save();
  759. $engineer->lastreceivetime = time();
  760. $engineer->work_order_quantity++;
  761. $engineer->save();
  762. $record = [
  763. 'order_id' => $order->id,
  764. 'engineer_id' => $transferEngineer,
  765. 'message_type' => 3,
  766. 'message' => __('The work order has been transferred to:%s', $engineer->title)
  767. ];
  768. Records::create($record);
  769. return ['code' => 1, 'msg' => __('Work order transferred~')];
  770. }
  771. return ['code' => 0, 'msg' => __('Nothing happened~')];
  772. }
  773. public function distribution($order, $userId)
  774. {
  775. if (isset($order->engineer_id) && $order->engineer_id) {
  776. return false;
  777. } elseif (!$order->category_id) {
  778. return false;
  779. }
  780. $distribution_engineer = null;
  781. $where['status'] = '1';
  782. $where['user_id'] = ['<>', $userId];
  783. if ($this->workorderConfig['distribution_type'] == 2) {
  784. // 技能分派-循环
  785. $category_engineer = Db::name('workorder_category')->where('id', $order->category_id)->value('we_ids');
  786. if (!$category_engineer) {
  787. return false;
  788. }
  789. $where['id'] = ['in', $category_engineer];
  790. $this->workorderConfig['distribution_type'] = 0;
  791. } elseif ($this->workorderConfig['distribution_type'] == 3) {
  792. // 技能分派-负载
  793. $category_engineer = Db::name('workorder_category')->where('id', $order->category_id)->value('we_ids');
  794. if (!$category_engineer) {
  795. return false;
  796. }
  797. $where['id'] = ['in', $category_engineer];
  798. $this->workorderConfig['distribution_type'] = 1;
  799. }
  800. if ($this->workorderConfig['distribution_type'] == 0) {
  801. $distribution_engineer = Engineer::where($where)->order('lastreceivetime asc')->find();
  802. } elseif ($this->workorderConfig['distribution_type'] == 1) {
  803. $engineer = Engineer::where($where)->select();
  804. foreach ($engineer as $index => $item) {
  805. $orderNumber = Orders::where('engineer_id', $item->id)->count();
  806. if ($orderNumber == 0) {
  807. $distribution_engineer = $item;
  808. break;
  809. } else {
  810. if (isset($minOrder)) {
  811. if ($orderNumber < $minOrder) {
  812. $minOrder = $orderNumber;
  813. }
  814. } else {
  815. $distribution_engineer = $item;
  816. $minOrder = $orderNumber;
  817. }
  818. }
  819. }
  820. }
  821. if (!$distribution_engineer) {
  822. return false;
  823. }
  824. $distribution_engineer->lastreceivetime = time();
  825. $distribution_engineer->work_order_quantity++;
  826. $distribution_engineer->save();
  827. $order->engineer_id = $distribution_engineer->id;
  828. $order->status = 1;
  829. $order->save();
  830. // 发送通知
  831. if (isset($order->title)) {
  832. $this->mailNotice($order, 'engineer_new_order', __('[new work order]') . $order->title, __('The user submitted a new job:%s please process it as soon as possible.', [$order->title]));
  833. $this->smsNotice($order, 'engineer_new_order');
  834. }
  835. return $distribution_engineer->id;
  836. }
  837. public static function handleUrl($url, $category)
  838. {
  839. if (preg_match('/^https?:\/\//i', $url)) {
  840. if (strpos($url, '?') === false) {
  841. $url .= '?category=' . $category;
  842. } else {
  843. $url .= '&category=' . $category;
  844. }
  845. return $url;
  846. } else {
  847. return url($url, ['category' => $category]);
  848. }
  849. }
  850. /**
  851. * 推荐知识点
  852. */
  853. public static function recKbs($id, $category = false, $limit = 20)
  854. {
  855. $recKbs = [];
  856. if ($category) {
  857. $kbsIds = Db::name('workorder_category')
  858. ->where('id', $category)
  859. ->where('status', '1')
  860. ->where('deletetime', null)
  861. ->value('kbs_ids');
  862. if ($kbsIds) {
  863. $recKbs = Db::name('workorder_kbs')
  864. ->field('id,title,url')
  865. ->where('id', 'in', $kbsIds)
  866. ->where('status', '1')
  867. ->where('deletetime', null)
  868. ->where('id', '<>', $id)
  869. ->order('weigh desc')
  870. ->limit($limit)
  871. ->select();
  872. }
  873. }
  874. if (!$recKbs) {
  875. $recKbs = Db::name('workorder_kbs')
  876. ->field('id,title')
  877. ->where('status', '1')
  878. ->where('deletetime', null)
  879. ->where('id', '<>', $id)
  880. ->order('weigh desc,views desc')
  881. ->limit($limit)
  882. ->select();
  883. }
  884. return $recKbs;
  885. }
  886. public function engineerViewed($order)
  887. {
  888. if ($order->status == 1) {
  889. $order->status = 2;
  890. $order->allowField(true)->save();
  891. $record = [
  892. 'order_id' => $order->id,
  893. 'engineer_id' => $order->engineer_id,
  894. 'message_type' => 3,
  895. 'message' => __('The engineer has viewed your submitted questions')
  896. ];
  897. Records::create($record);
  898. // 发送通知
  899. $this->mailNotice($order, 'user_order_handle', __('[work order is already being processed]') . $order->title, __('Work order:%s the engineer has reviewed the problem you submitted and is processing it', [$order->title]));
  900. $this->smsNotice($order, 'user_order_handle');
  901. return true;
  902. }
  903. return false;
  904. }
  905. /**
  906. * 工单详情处理
  907. * @var Orders $order
  908. */
  909. public function orderInfoHandle($order, $isUser, $isCurrentEngineer)
  910. {
  911. $nowTime = time();
  912. // 下次可催办时间
  913. $urging_rate = (int)($this->workorderConfig['urging_rate'] * 60);
  914. $nextUrgingTime = (int)$order->lasturgingtime + $urging_rate;
  915. $order->title = $order->title ?? __('Untitled');
  916. // 标记工程师处理中
  917. if ($isCurrentEngineer) {
  918. $this->engineerViewed($order);
  919. }
  920. $order->urging = false;
  921. $order->close = false;
  922. if ($isUser && $order->status <= 2 && $nextUrgingTime <= $nowTime && $order->engineer_id && $this->workorderConfig['user_urging']) {
  923. $order->urging = true;
  924. }
  925. if ($order->status <= 3) {
  926. $order->close = true;
  927. }
  928. $order->status = $this->handleStatus($order->status, $isCurrentEngineer);
  929. // 处理用户头像
  930. if ($order->user) {
  931. $order->user->avatar = $order->user->avatar ? cdnurl($order->user->avatar, true) : (function_exists('letter_avatar') ? letter_avatar($order->user->nickname) : cdnurl('/assets/img/avatar.png', true));
  932. }
  933. $basicField = [];// 工单基本信息字段
  934. $allField = $order->getFields($order, 0);
  935. foreach ($allField as $index => $field) {
  936. if ($field['isbasicinfo']) {
  937. $field['value'] = self::fieldValue($field, $field['value']);
  938. $basicField[] = $field;
  939. }
  940. }
  941. if ($isCurrentEngineer) {
  942. $basicField[] = [
  943. 'title' => __('Submit user'),
  944. 'value' => $order->user->nickname
  945. ];
  946. if (!$this->workorderConfig['engineer_close']) {
  947. $order->close = false;
  948. }
  949. }
  950. return [
  951. 'order' => $order,
  952. 'basicField' => $basicField
  953. ];
  954. }
  955. public static function fieldValue($field, $value)
  956. {
  957. if ($field['type_list'] == 'switch') {
  958. return $value ? __('open') : __('close');
  959. }
  960. if ($value == '') {
  961. return '';
  962. }
  963. $listField = ['select', 'selects', 'checkbox', 'radio'];
  964. if (in_array($field['type_list'], $listField)) {
  965. if (is_array($field['values_list'])) {
  966. $valueTmp = '';
  967. if (!is_array($value)) {
  968. $value = explode(',', $value);
  969. }
  970. foreach ($value as $key => $item) {
  971. $valueTmp .= isset($field['values_list'][$item]) ? $field['values_list'][$item] . ',' : '';
  972. }
  973. return trim($valueTmp, ',');
  974. }
  975. }
  976. return $value;
  977. }
  978. /**
  979. * 关闭工单
  980. * @return array
  981. * @var Orders $order
  982. */
  983. public function closeOrder($order, $isCurrentEngineer)
  984. {
  985. $nowTime = time();
  986. if ($order->status == 4 || $order->status == 5) {
  987. return ['code' => 0, 'msg' => __('The work order has been closed!')];
  988. }
  989. if ($isCurrentEngineer && ($this->workorderConfig['engineer_close'] == 0)) {
  990. return ['code' => 0, 'msg' => __('The engineer was not allowed to close the work order~')];
  991. }
  992. $order->status = 4;
  993. $order->save();
  994. $timeStatistics = Db::name('workorder_time_statistics')
  995. ->where('order_id', $order->id)
  996. ->where('type', 0)
  997. ->where('time_consum', null)
  998. ->find();
  999. if ($timeStatistics) {
  1000. // 记录结单耗时
  1001. Db::name('workorder_time_statistics')
  1002. ->where('id', $timeStatistics['id'])
  1003. ->update([
  1004. 'endtime' => $nowTime,
  1005. 'time_consum' => $nowTime - $timeStatistics['starttime'],
  1006. 'engineer_id' => $order->engineer_id,// 防转移,冲正为当前工程师
  1007. ]);
  1008. }
  1009. return ['code' => 1, 'msg' => __('Work order closed successfully~')];
  1010. }
  1011. /**
  1012. * 催单
  1013. * @return array
  1014. * @var Orders $order
  1015. */
  1016. public function urgingOrder($order)
  1017. {
  1018. $nowTime = time();
  1019. if (!$this->workorderConfig['user_urging']) {
  1020. return ['code' => 0, 'msg' => __('Reminder function not enabled~')];
  1021. }
  1022. // 下次可催办时间-在isPost内有使用
  1023. $urging_rate = (int)($this->workorderConfig['urging_rate'] * 60);
  1024. $nextUrgingTime = (int)$order->lasturgingtime + $urging_rate;
  1025. if ($nextUrgingTime <= $nowTime) {
  1026. $order->lasturgingtime = $nowTime;
  1027. $order->save();
  1028. $order->title = $order->title ?? __('Untitled');
  1029. $this->mailNotice($order, 'engineer_urging', __('[work order reminder]') . $order->title, __('Dear engineer, work order:%s, the user hopes you will reply as soon as possible.', [$order->title]));
  1030. $this->smsNotice($order, 'engineer_urging');
  1031. return ['code' => 1, 'msg' => __('Reminder message sent successfully~')];
  1032. } else {
  1033. return ['code' => 0, 'msg' => __('Urge message sent frequently!')];
  1034. }
  1035. }
  1036. /**
  1037. * 获取一个工单的聊天记录和所有工程师
  1038. * @param int $id 工单ID
  1039. * @param int $engineer 工单当前工程师
  1040. * @return array
  1041. */
  1042. public function orderRecords($id, $engineer = 0)
  1043. {
  1044. // 沟通记录
  1045. $records = Records::where('order_id', $id)
  1046. ->order('createtime asc')
  1047. ->select();
  1048. // 共有哪些工程师
  1049. $engineers = [];
  1050. if ($engineer) {
  1051. $engineers[$engineer] = $engineer;
  1052. }
  1053. foreach ($records as $index => $record) {
  1054. if ($record->engineer_id) {
  1055. $engineers[$record->engineer_id] = $record->engineer_id;
  1056. }
  1057. if ($record->engineer && $record->engineer->user) {
  1058. $record->engineer->user->avatar = $record->engineer->user->avatar ? cdnurl($record->engineer->user->avatar, true) : (function_exists('letter_avatar') ? letter_avatar($record->engineer->title) : cdnurl('/assets/img/avatar.png', true));
  1059. }
  1060. }
  1061. $engineers = Engineer::select($engineers);
  1062. $engineerInfoConfig = Engineer::getEngineerInfoConfig();
  1063. foreach ($engineers as $index => $engineer) {
  1064. if (!$engineer->introduce) {
  1065. $engineer->introduce = ($engineer->user && $engineer->user->bio) ? $engineer->user->bio : '';
  1066. }
  1067. $engineer->all_order_number = in_array('all_order_number', $engineerInfoConfig) ? $engineer->all_order_number : false;
  1068. $engineer->wx = in_array('wx', $engineerInfoConfig) ? $engineer->wx : false;
  1069. $engineer->qq = in_array('qq', $engineerInfoConfig) ? $engineer->qq : false;
  1070. if ($engineer->user) {
  1071. $engineer->user->mobile = in_array('mobile', $engineerInfoConfig) ? $engineer->user->mobile : false;
  1072. $engineer->user->email = in_array('email', $engineerInfoConfig) ? $engineer->user->email : false;
  1073. $engineer->user->avatar = $engineer->user->avatar ? cdnurl($engineer->user->avatar, true) : (function_exists('letter_avatar') ? letter_avatar($engineer->title) : cdnurl('/assets/img/avatar.png', true));
  1074. }
  1075. }
  1076. return [
  1077. 'records' => $records,
  1078. 'engineers' => $engineers
  1079. ];
  1080. }
  1081. public function handleStatus($status, $isEngineer = false)
  1082. {
  1083. $colors = [
  1084. 'info',
  1085. 'info',
  1086. 'warning',
  1087. 'danger',
  1088. 'danger',
  1089. 'success'
  1090. ];
  1091. $statusLang = __('Status ' . $status);
  1092. if ($isEngineer) {
  1093. switch ($status) {
  1094. case 3:
  1095. $statusLang = __('Waiting for user feedback');
  1096. break;
  1097. case 4:
  1098. $statusLang = __('To be evaluated by users');
  1099. break;
  1100. case 2:
  1101. $statusLang = __('Waiting for your reply');
  1102. break;
  1103. }
  1104. }
  1105. return [
  1106. 'pc' => '<span class="text-' . $colors[$status] . '">' . $statusLang . '</span>',
  1107. 'h5' => '<span class="text-' . $colors[$status] . '">[' . $statusLang . ']</span>',
  1108. 'original' => $statusLang,
  1109. 'number' => $status
  1110. ];
  1111. }
  1112. }