AutomaticDispatch.php 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641
  1. <?php
  2. namespace app\common\command;
  3. use think\facade\Db;
  4. use think\facade\Log;
  5. use think\console\Input;
  6. use think\console\Output;
  7. use think\console\Command;
  8. use app\adminapi\service\WeCallService;
  9. use app\common\model\works\ServiceWork;
  10. use app\common\model\goods_time\GoodsTime;
  11. use app\common\model\master_worker\MasterWorker;
  12. use app\common\model\works\ServiceWorkAnomalous;
  13. use app\common\model\master_worker\MasterWorkerTeam;
  14. use app\common\model\works\ServiceWorkAllocateWorkerLog;
  15. use app\workerapi\logic\ServiceWorkerAllocateWorkerLogic;
  16. use app\common\model\master_worker\MasterWorkerServiceTime;
  17. use app\common\model\master_worker\MasterWorkerStop;
  18. use think\facade\Cache;
  19. class AutomaticDispatch extends Command
  20. {
  21. //地理时效评分占比
  22. protected $distanceRate = 0.25;
  23. //工程师权重评分占比
  24. protected $weightRate = 0.2;
  25. //工程师综合评分占比
  26. protected $comprehensiveRate = 0.55;
  27. //默认服务时长 180 分钟
  28. protected $defaultServiceTime = 180;
  29. //外呼客户列表
  30. protected $customerList = [];
  31. //服务类目
  32. protected $categoryType = [
  33. 1 => '安装',
  34. 2 => '维修',
  35. 3 => '清洗',
  36. ];
  37. protected function configure()
  38. {
  39. $this->setName('automatic_dispatch')
  40. ->setDescription('自动派单');
  41. }
  42. protected function execute(Input $input, Output $output)
  43. {
  44. //自动派单
  45. // $this->autoDispatch();
  46. // //执行外呼任务
  47. // $h = date('H');
  48. // if ($h >= 8 && $h <= 22) {
  49. // $this->startTask();
  50. // }
  51. //异常工单:已过预约时间工程师未确认上门
  52. $this->workAnomalous();
  53. //异常工单:上门时间超过两小时工单未确认完成
  54. $this->unFinishedWorkAnomalous();
  55. if (!Cache::get("automatic_dispatch_acceptOrderStatus_".date("Y-m-d"))) {
  56. //长期合作工程师:停单/开启接单
  57. $this->acceptOrderStatus();
  58. Cache::set("automatic_dispatch_acceptOrderStatus_".date("Y-m-d"), 1, 24*60*60);//秒数
  59. }
  60. }
  61. /**
  62. * 长期合作工程师停单/启单
  63. */
  64. public function acceptOrderStatus()
  65. {
  66. $startTime = strtotime(date('Y-m-d 00:00:00'));
  67. $endTime = strtotime(date('Y-m-d 23:59:59', strtotime("-1 days")));
  68. $list = MasterWorkerStop::where(function ($query) use ($startTime, $endTime) {
  69. $query->where('start_time', $startTime)
  70. ->whereOr('end_time', $endTime);
  71. })
  72. ->field("id,worker_id,start_time,end_time")
  73. ->order("start_time", "ASC")
  74. ->select()
  75. ->toArray();
  76. if (!$list) {
  77. return ;
  78. }
  79. foreach($list as $item) {
  80. try {
  81. $accept_order_status = strtotime($item['start_time']) == $startTime ? 0 : 1;
  82. MasterWorker::where('id', $item['worker_id'])->update(['accept_order_status' => $accept_order_status]);
  83. } catch (\Exception $e) {
  84. Log::write('长期合作工程师停单/启单异常:'.$e->getMessage());
  85. }
  86. }
  87. }
  88. /**
  89. * 异常工单:已过预约时间工程师未确认上门
  90. */
  91. protected function workAnomalous()
  92. {
  93. $size = 100;
  94. $startTime = strtotime(date('Y-m-d 00:00:00'));
  95. $endTime = time(); // 当前时间戳
  96. $list = ServiceWork::alias("a")
  97. ->leftJoin('service_work_anomalous b','a.id = b.work_id')
  98. ->where('a.work_status','<=',3)
  99. ->where('a.service_status','<',2)
  100. ->where('a.refund_approval',0)
  101. ->where('a.work_pay_status',1)
  102. ->where('a.appointment_time','between', [$startTime, $endTime])
  103. ->whereNotExists(function($query) {
  104. $query->table('la_service_work_anomalous c')
  105. ->whereRaw('a.id = c.work_id')
  106. ->where('c.reason_type', 3);
  107. })
  108. ->field('a.id,a.appointment_time,b.id as anomalous_id,b.reason_type')
  109. ->group('a.id')
  110. ->order('a.create_time','asc')
  111. ->limit($size)
  112. ->select()
  113. ->toArray();
  114. if (!$list) {
  115. return ;
  116. }
  117. foreach($list as $item) {
  118. try {
  119. ServiceWorkAnomalous::create([
  120. 'work_id' => $item['id'],
  121. 'reason_type' => 3,
  122. 'reason' => '已过预约时间工程师未确认上门',
  123. ]);
  124. } catch (\Exception $e) {
  125. Log::write('异常工单:'.$e->getMessage());
  126. }
  127. }
  128. }
  129. /**
  130. * 异常工单:上门时间超过两小时工单未确认完成
  131. */
  132. protected function unFinishedWorkAnomalous()
  133. {
  134. $size = 100;
  135. $startTime = strtotime(date('Y-m-d 00:00:00'));
  136. $endTime = time() - 7200; // 当前时间减去7200秒(2小时)
  137. $list = ServiceWork::alias("a")
  138. ->leftJoin('service_work_anomalous b','a.id = b.work_id')
  139. ->where('a.work_status', '>=', 4)
  140. ->where('a.service_status','<',3)
  141. ->where('a.refund_approval',0)
  142. ->where('a.work_pay_status',1)
  143. ->where('a.appointment_time','between', [$startTime, $endTime])
  144. ->whereNotExists(function($query) {
  145. $query->table('la_service_work_anomalous c')
  146. ->whereRaw('a.id = c.work_id')
  147. ->where('c.reason_type', 4);
  148. })
  149. ->field('a.id,a.appointment_time,b.id as anomalous_id')
  150. ->group('a.id')
  151. ->order('a.create_time','asc')
  152. ->limit($size)
  153. ->select()
  154. ->toArray();
  155. if (!$list) {
  156. return ;
  157. }
  158. foreach($list as $item) {
  159. try {
  160. ServiceWorkAnomalous::create([
  161. 'work_id' => $item['id'],
  162. 'reason_type' => 4,
  163. 'reason' => '上门时间超过两小时工单未确认完成',
  164. ]);
  165. } catch (\Exception $e) {
  166. Log::write('异常工单:'.$e->getMessage());
  167. }
  168. }
  169. }
  170. /*
  171. * 自动派单总分100分
  172. 1、地理效率得分(distance score)25%
  173. 2、工程师权重「自定义是否有证」(weight score)20%
  174. 3、工程师综合服务分 55%
  175. */
  176. protected function autoDispatch()
  177. {
  178. $size = 100;
  179. $startTime = strtotime(date('Y-m-d 00:00:00'));
  180. $endTime = strtotime(date('Y-m-d 23:59:59'));
  181. $list = ServiceWork::where('work_status',0)
  182. ->where('service_status',0)
  183. ->where('refund_approval',0)
  184. ->where('work_pay_status',1)
  185. ->where('exec_num','<', 2)
  186. ->where('appointment_time','between', [$startTime, $endTime])
  187. ->field('id,category_type,goods_category_id,service_area_id,lon,lat,province,city,title,appointment_time,address,mobile,work_sn')
  188. ->order('create_time','asc')
  189. ->limit($size)
  190. ->select()
  191. ->toArray();
  192. if (!$list) {
  193. return ;
  194. }
  195. foreach($list as $item) {
  196. try {
  197. //优先平台工程师派单(长期合作)
  198. $res = $this->platformWorker($item);
  199. if ($res === false) {
  200. //优先平台工程师派单(短期合作)
  201. $res = $this->platformTemporaryWorker($item);
  202. }
  203. if ($res === false) {
  204. //门店负责人派单
  205. $res = $this->teamWorker($item);
  206. if ($res === false) {
  207. ServiceWork::where('id',$item['id'])
  208. ->update([
  209. 'exec_num' => 2,
  210. ]);
  211. ServiceWorkAnomalous::create([
  212. 'work_id' => $item['id'],
  213. 'reason_type' => 2,
  214. 'reason' => '自动派单:找不到工程师',
  215. ]);
  216. }
  217. }
  218. } catch (\Exception $e) {
  219. Log::write('自动派单异常:'.$e->getMessage());
  220. }
  221. }
  222. }
  223. /**
  224. * 执行外呼任务
  225. */
  226. protected function startTask() {
  227. if ($this->customerList) {
  228. $weCallService = new WeCallService();
  229. $res = $weCallService->importUser($this->customerList);
  230. if (isset($res['code']) && $res['code'] == 200) {
  231. $res = $weCallService->startTask();
  232. }
  233. }
  234. $this->customerList = [];
  235. }
  236. /**
  237. * 派单给平台工程师(长期合作)
  238. */
  239. protected function platformWorker($item) {
  240. // 定义地球半径(单位:米)
  241. $earthRadius = 6371000;
  242. // 定义 Haversine 公式计算距离的 SQL 片段
  243. $distanceCalculation = "{$earthRadius} * 2 * ASIN(SQRT(
  244. POWER(SIN((RADIANS({$item['lat']}) - RADIANS(a.lat)) / 2), 2) +
  245. COS(RADIANS({$item['lat']})) * COS(RADIANS(a.lat)) *
  246. POWER(SIN((RADIANS({$item['lon']}) - RADIANS(a.lon)) / 2), 2)
  247. ))";
  248. // 计算距离的字段定义
  249. $real_distance = Db::raw("{$distanceCalculation} AS real_distance");
  250. // 获取符合条件的工程师
  251. $worker = MasterWorker::alias('a')
  252. ->leftJoin('master_worker_score b', 'a.id = b.worker_id')
  253. ->where([
  254. ['type', '=', 2],
  255. ['is_disable', '=', 0],
  256. ['work_status', '=', 0],
  257. ['accept_order_status', '=', 1],
  258. ['city', '=', $item['city']],
  259. ['service_area_id', '=', $item['service_area_id']],
  260. ['tenant_id', '=', 0]
  261. ])
  262. ->distinct('a.id')
  263. ->whereRaw('FIND_IN_SET(' . $item['goods_category_id'] . ', a.category_ids)')
  264. //->whereRaw("{$distanceCalculation} <= a.distance")
  265. ->field([
  266. 'a.id',
  267. 'a.tenant_id',
  268. 'a.distance',
  269. 'a.lon',
  270. 'a.lat',
  271. 'a.worker_number',
  272. 'a.real_name',
  273. 'a.mobile',
  274. 'a.is_wecall',
  275. 'b.comprehensive_score',
  276. 'b.weight_score',
  277. $real_distance
  278. ])
  279. ->orderRaw('(b.comprehensive_score + b.weight_score) desc')
  280. ->limit(100)
  281. ->select()
  282. ->toArray();
  283. //echo MasterWorker::getLastSql();die;
  284. $queue = [];
  285. foreach($worker as $key => $value) {
  286. //过滤已接过此单的师傅
  287. $exists = ServiceWorkAllocateWorkerLog::where('work_id', $item['id'])->where('master_worker_id',$value['id'])->count();
  288. if ($exists) {
  289. continue;
  290. }
  291. //计算地理效率得分
  292. $realDistance = bcdiv($value['real_distance'],1000,2);
  293. $travelTime = $realDistance * 2;//预计每公里行驶2分钟
  294. $distanceScore = 100 - ($travelTime * 1.5) - ($realDistance * 5);
  295. $distanceScore = bcadd($distanceScore, 0, 2);
  296. $tmpDistanceRate = bcmul($distanceScore, $this->distanceRate, 2);
  297. $tmpRate = 0;
  298. $value['travelTime'] = $travelTime;
  299. $value['comprehensive_score'] = isset($value['comprehensive_score']) ? $value['comprehensive_score'] : 0;
  300. $value['weight_score'] = isset($value['weight_score']) ? $value['weight_score'] : 0;
  301. $tmpRate = bcmul($value['comprehensive_score'], $this->comprehensiveRate, 2) + bcmul($value['weight_score'], $this->weightRate, 2);
  302. $tmpRate = bcadd($tmpRate, $tmpDistanceRate,2);
  303. $tmpKey = isset($queue[$tmpRate]) ? bcadd($tmpRate, $key / 100,2) : $tmpRate;//防止键名重复
  304. $queue[$tmpKey] = $value;
  305. }
  306. //按照工程师的总分值倒序排序
  307. krsort($queue);
  308. foreach($queue as $worker) {
  309. $serviceTime = MasterWorkerServiceTime::where('master_worker_id',$worker['id'])->where('goods_category_id',$item['goods_category_id'])->value('service_time');
  310. if (empty($serviceTime)) {
  311. $serviceTime = GoodsTime::whereRaw('FIND_IN_SET('.$item['goods_category_id'].', goods_category_ids)')->value('service_time');
  312. $serviceTime = $serviceTime ?? $this->defaultServiceTime;//默认服务时长
  313. }
  314. //预约开始时间和结束时间
  315. $appointment_time = is_numeric($item['appointment_time']) ? $item['appointment_time'] : strtotime($item['appointment_time']);
  316. $estimated_finish_time = $appointment_time + $serviceTime * 60 + $worker['travelTime'] * 60;
  317. //校验客户的预约时间是否在工程师的空挡期内
  318. $count = ServiceWork::where([
  319. ['master_worker_id','=',$worker['id']],
  320. ['work_status','>=',1],
  321. ['work_status','<=',5],
  322. ['service_status','<',4]
  323. ])
  324. ->where(function ($query) use ($appointment_time,$estimated_finish_time) {
  325. $query->where('appointment_time', 'between',[$appointment_time, $estimated_finish_time])
  326. ->whereOr('estimated_finish_time', 'between', [$appointment_time, $estimated_finish_time]);
  327. })
  328. ->count();
  329. if ($count == 0) {
  330. $operaLog = '系统自动派单于'.date('Y-m-d H:i:s',time()).'分配了工程师'.'编号['.$worker['worker_number'].']'.$worker['real_name'];
  331. $res = $this->allocateWorker($item,$worker['id'],$worker['tenant_id'],$operaLog,$estimated_finish_time);
  332. if ($res === true && $worker['is_wecall'] == 1) {
  333. $this->customerList[] = [
  334. 'phone' => $worker['mobile'],
  335. 'properties' => [
  336. '订单号' => substr($item['work_sn'], -4),
  337. // '详细地址'=>$item['address'],
  338. // '服务类型'=> isset($this->categoryType[$item['category_type']]) ? $this->categoryType[$item['category_type']] : '',
  339. // '客户手机号'=>$item['mobile']
  340. ]
  341. ];
  342. }
  343. return $res;
  344. }
  345. }
  346. return false;
  347. }
  348. /**
  349. * 派单给平台工程师(短期合作)
  350. */
  351. protected function platformTemporaryWorker($item) {
  352. // 定义地球半径(单位:米)
  353. $earthRadius = 6371000;
  354. // 定义 Haversine 公式计算距离的 SQL 片段
  355. $distanceCalculation = "{$earthRadius} * 2 * ASIN(SQRT(
  356. POWER(SIN((RADIANS({$item['lat']}) - RADIANS(a.lat)) / 2), 2) +
  357. COS(RADIANS({$item['lat']})) * COS(RADIANS(a.lat)) *
  358. POWER(SIN((RADIANS({$item['lon']}) - RADIANS(a.lon)) / 2), 2)
  359. ))";
  360. // 计算距离的字段定义
  361. $real_distance = Db::raw("{$distanceCalculation} AS real_distance");
  362. // 获取符合条件的工程师
  363. $worker = MasterWorker::alias('a')
  364. ->leftJoin('master_worker_score b', 'a.id = b.worker_id')
  365. ->where([
  366. ['type', '=', 1],
  367. ['is_disable', '=', 0],
  368. ['work_status', '=', 0],
  369. ['accept_order_status', '=', 1],
  370. ['city', '=', $item['city']],
  371. ['service_area_id', '=', $item['service_area_id']],
  372. ['tenant_id', '=', 0]
  373. ])
  374. ->distinct('a.id')
  375. ->whereRaw('FIND_IN_SET(' . $item['goods_category_id'] . ', a.category_ids)')
  376. ->whereRaw("{$distanceCalculation} <= a.distance")
  377. ->field([
  378. 'a.id',
  379. 'a.tenant_id',
  380. 'a.distance',
  381. 'a.lon',
  382. 'a.lat',
  383. 'a.worker_number',
  384. 'a.real_name',
  385. 'a.mobile',
  386. 'a.is_wecall',
  387. 'b.comprehensive_score',
  388. 'b.weight_score',
  389. $real_distance
  390. ])
  391. ->orderRaw('(b.comprehensive_score + b.weight_score) desc')
  392. ->limit(100)
  393. ->select()
  394. ->toArray();
  395. //echo MasterWorker::getLastSql();die;
  396. $queue = [];
  397. foreach($worker as $key => $value) {
  398. //过滤已接过此单的师傅
  399. $exists = ServiceWorkAllocateWorkerLog::where('work_id', $item['id'])->where('master_worker_id',$value['id'])->count();
  400. if ($exists) {
  401. continue;
  402. }
  403. //计算地理效率得分
  404. $realDistance = bcdiv($value['real_distance'],1000,2);
  405. $travelTime = $realDistance * 2;//预计每公里行驶2分钟
  406. $distanceScore = 100 - ($travelTime * 1.5) - ($realDistance * 5);
  407. $distanceScore = bcadd($distanceScore, 0, 2);
  408. $tmpDistanceRate = bcmul($distanceScore, $this->distanceRate, 2);
  409. $tmpRate = 0;
  410. $value['travelTime'] = $travelTime;
  411. $value['comprehensive_score'] = isset($value['comprehensive_score']) ? $value['comprehensive_score'] : 0;
  412. $value['weight_score'] = isset($value['weight_score']) ? $value['weight_score'] : 0;
  413. $tmpRate = bcmul($value['comprehensive_score'], $this->comprehensiveRate, 2) + bcmul($value['weight_score'], $this->weightRate, 2);
  414. $tmpRate = bcadd($tmpRate, $tmpDistanceRate,2);
  415. $tmpKey = isset($queue[$tmpRate]) ? bcadd($tmpRate, $key / 100,2) : $tmpRate;//防止键名重复
  416. $queue[$tmpKey] = $value;
  417. }
  418. //按照工程师的总分值倒序排序
  419. krsort($queue);
  420. foreach($queue as $worker) {
  421. $serviceTime = MasterWorkerServiceTime::where('master_worker_id',$worker['id'])->where('goods_category_id',$item['goods_category_id'])->value('service_time');
  422. if (empty($serviceTime)) {
  423. $serviceTime = GoodsTime::whereRaw('FIND_IN_SET('.$item['goods_category_id'].', goods_category_ids)')->value('service_time');
  424. $serviceTime = $serviceTime ?? $this->defaultServiceTime;//默认服务时长
  425. }
  426. //预约开始时间和结束时间
  427. $appointment_time = is_numeric($item['appointment_time']) ? $item['appointment_time'] : strtotime($item['appointment_time']);
  428. $estimated_finish_time = $appointment_time + $serviceTime * 60 + $worker['travelTime'] * 60;
  429. //校验客户的预约时间是否在工程师的空挡期内
  430. $count = ServiceWork::where([
  431. ['master_worker_id','=',$worker['id']],
  432. ['work_status','>=',1],
  433. ['work_status','<=',5],
  434. ['service_status','<',4]
  435. ])
  436. ->where(function ($query) use ($appointment_time,$estimated_finish_time) {
  437. $query->where('appointment_time', 'between',[$appointment_time, $estimated_finish_time])
  438. ->whereOr('estimated_finish_time', 'between', [$appointment_time, $estimated_finish_time]);
  439. })
  440. ->count();
  441. if ($count == 0) {
  442. $operaLog = '系统自动派单于'.date('Y-m-d H:i:s',time()).'分配了工程师'.'编号['.$worker['worker_number'].']'.$worker['real_name'];
  443. $res = $this->allocateWorker($item,$worker['id'],$worker['tenant_id'],$operaLog,$estimated_finish_time);
  444. if ($res === true && $worker['is_wecall'] == 1) {
  445. $this->customerList[] = [
  446. 'phone' => $worker['mobile'],
  447. 'properties' => [
  448. '订单号' => substr($item['work_sn'], -4),
  449. // '详细地址'=>$item['address'],
  450. // '服务类型'=> isset($this->categoryType[$item['category_type']]) ? $this->categoryType[$item['category_type']] : '',
  451. // '客户手机号'=>$item['mobile']
  452. ]
  453. ];
  454. }
  455. return $res;
  456. }
  457. }
  458. return false;
  459. }
  460. /**
  461. * 派单给门店负责人
  462. */
  463. protected function teamWorker($item) {
  464. // 地球半径,单位:米
  465. $earthRadius = 6371000;
  466. // 定义 Haversine 公式计算距离的 SQL 片段
  467. $distanceCalculation = "{$earthRadius} * 2 * ASIN(SQRT(
  468. POWER(SIN((RADIANS({$item['lat']}) - RADIANS(lat)) / 2), 2) +
  469. COS(RADIANS({$item['lat']})) * COS(RADIANS(lat)) *
  470. POWER(SIN((RADIANS({$item['lon']}) - RADIANS(lon)) / 2), 2)
  471. ))";
  472. // 计算距离的字段定义
  473. $real_distance = Db::raw("{$distanceCalculation} AS real_distance");
  474. // 判断预约时间是上午还是下午
  475. $isAm = date("H", strtotime($item['appointment_time'])) < 12 ? 1 : 0;
  476. // 根据上午或下午构建查询条件
  477. $whereRaw = $isAm ? 'am_order < am_limit' : 'pm_order < pm_limit';
  478. // 获取符合条件的工程师团队
  479. $worker = MasterWorkerTeam::where([
  480. ['accept_order_status', '=', 1],
  481. ['city', '=', $item['city']],
  482. ['service_area_id', '=', $item['service_area_id']],
  483. ])
  484. ->whereRaw($whereRaw)
  485. ->whereRaw('FIND_IN_SET(' . $item['goods_category_id'] . ', goods_category_ids)')
  486. // 使用 Haversine 公式替换 ST_Distance_Sphere 函数进行距离筛选
  487. ->whereRaw("{$distanceCalculation} <= distance")
  488. ->field([
  489. 'id',
  490. 'lon',
  491. 'lat',
  492. 'distance',
  493. 'tenant_id',
  494. 'team_name',
  495. 'master_worker_id',
  496. 'am_order',
  497. 'am_limit',
  498. 'pm_order',
  499. 'pm_limit',
  500. 'min_order',
  501. 'comprehensive_score',
  502. $real_distance
  503. ])
  504. ->order('comprehensive_score', 'desc')
  505. ->limit(100)
  506. ->select()
  507. ->toArray();
  508. //echo MasterWorkerTeam::getLastSql();die;
  509. $minQueue = [];
  510. $queue = [];
  511. foreach($worker as $key => $value) {
  512. //过滤已接过此单的师傅
  513. $exists = ServiceWorkAllocateWorkerLog::where('work_id', $item['id'])->where('master_worker_id',$value['master_worker_id'])->count();
  514. if ($exists) {
  515. continue;
  516. }
  517. if ($value['am_order'] + $value['pm_order'] < $value['min_order']) {
  518. $minQueue[] = $value;
  519. } else {
  520. $queue[] = $value;
  521. }
  522. }
  523. $queue = array_merge($minQueue,$queue);
  524. //优先给接单数量不足最低接单数的团队派单,其次再给服务评分高的派单
  525. foreach($queue as $worker) {
  526. $operaLog = '系统自动派单于'.date('Y-m-d H:i:s',time()).'分配了团队ID['.$worker['id'].']'.$worker['team_name'];
  527. $res = $this->allocateWorker($item, $worker['master_worker_id'], $worker['tenant_id'], $operaLog);
  528. if ($res === true) {
  529. $updateData = $isAm == 1 ? ['am_order' => Db::raw('am_order + 1')] : ['pm_order' => Db::raw('pm_order + 1')];
  530. MasterWorkerTeam::where('id',$worker['id'])->update($updateData);
  531. $this->customerList[] = [
  532. 'phone' => MasterWorker::where('id',$worker['master_worker_id'])->value('mobile'),
  533. 'properties' => [
  534. '订单号' => substr($item['work_sn'], -4),
  535. // '详细地址'=>$item['address'],
  536. // '服务类型'=> isset($this->categoryType[$item['category_type']]) ? $this->categoryType[$item['category_type']] : '',
  537. // '客户手机号'=>$item['mobile']
  538. ]
  539. ];
  540. return true;
  541. }
  542. }
  543. return false;
  544. }
  545. /**
  546. * 分配工程师
  547. */
  548. protected function allocateWorker($workDetail, $masterWorkerId, $tenant_id,$operaLog, $estimated_finish_time=0)
  549. {
  550. Db::startTrans();
  551. try {
  552. ServiceWork::where('id',$workDetail['id'])->update([
  553. 'master_worker_id'=>$masterWorkerId,
  554. 'tenant_id' => $tenant_id,
  555. 'work_status'=>1,
  556. 'estimated_finish_time' => $estimated_finish_time,
  557. 'dispatch_time'=>time(),
  558. 'exec_num' => Db::raw('exec_num + 1'),
  559. ]);
  560. MasterWorker::setWorktotal('inc',$masterWorkerId);
  561. $work_log = [
  562. 'work_id'=>$workDetail['id'],
  563. 'master_worker_id'=>$masterWorkerId,
  564. 'type' => 0,
  565. 'opera_log'=> $operaLog
  566. ];
  567. ServiceWorkerAllocateWorkerLogic::add($work_log);
  568. Db::commit();
  569. } catch (\Exception $e) {
  570. Db::rollback();
  571. Log::write('自动派单分配工程师异常:'.$e->getMessage());
  572. return false;
  573. }
  574. // 工程师派单通知【给工程师的通知】【公众号通知,不发短信】
  575. $res = event('Notice', [
  576. 'scene_id' => 113,
  577. 'params' => [
  578. 'user_id' => $masterWorkerId,
  579. 'order_id' => $workDetail['id'],
  580. 'thing9' => $workDetail['title'],
  581. 'time7' => $workDetail['appointment_time'],
  582. 'thing8' => (iconv_strlen($workDetail['address'])>15)?(mb_substr($workDetail['address'],0,15,'UTF-8').'...'):$workDetail['address'],
  583. 'phone_number6' => asteriskString($workDetail['mobile']),
  584. ]
  585. ]);
  586. return true;
  587. }
  588. }