AutomaticDispatch.php 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  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\common\model\works\ServiceWork;
  9. use app\common\model\goods_time\GoodsTime;
  10. use app\common\model\master_worker\MasterWorker;
  11. use app\common\model\works\ServiceWorkAllocateWorkerLog;
  12. use app\workerapi\logic\ServiceWorkerAllocateWorkerLogic;
  13. use app\common\model\master_worker\MasterWorkerServiceTime;
  14. class AutomaticDispatch extends Command
  15. {
  16. protected $defaultServiceTime = 300; //默认服务时长 300 分钟
  17. protected function configure()
  18. {
  19. $this->setName('automatic_dispatch')
  20. ->setDescription('自动派单');
  21. }
  22. protected function execute(Input $input, Output $output)
  23. {
  24. $this->autoDispatch();
  25. }
  26. /*
  27. * 自动派单总分100分
  28. 1、地理效率得分(distance score)25%
  29. 2、工程师权重「自定义是否有证」(weight score)20%
  30. 3、工程师综合服务分 55%
  31. */
  32. protected function autoDispatch()
  33. {
  34. $size = 30;
  35. $distanceRate = 0.25;
  36. $weightRate = 0.2;
  37. $comprehensiveRate = 0.55;
  38. // 获取当前时间的前五分钟时间戳
  39. $fiveMinutesAgo = time() - 300; // 300 秒 = 5 分钟
  40. while(true) {
  41. $list = ServiceWork::where('work_status',0)
  42. ->where(function ($query) use ($fiveMinutesAgo) {
  43. $query->where('exec_time', 0)->whereOr('exec_time', '<', $fiveMinutesAgo);
  44. })
  45. ->where('create_time','>', strtotime("-1 days"))
  46. ->field('id,category_type,goods_category_id,lon,lat,province,city,title,appointment_time,address,mobile')
  47. ->limit($size)
  48. ->select()
  49. ->toArray();
  50. if (!$list) {
  51. sleep(5);
  52. }
  53. $isExec = 0;
  54. foreach($list as $item) {
  55. try {
  56. // 获取符合条件的工程师
  57. $worker = MasterWorker::alias('a')->leftJoin('master_worker_score b','a.id = b.worker_id')
  58. ->where([
  59. ['is_disable','=',0],
  60. ['work_status','=',0],
  61. ['accept_order_status','=',0],
  62. ['city','=',$item['city']],
  63. ])
  64. ->whereRaw('FIND_IN_SET('.$item['goods_category_id'].', a.category_ids)')
  65. ->field('a.id,a.distance,a.lon,a.lat,a.worker_number,a.real_name,b.comprehensive_score,b.weight_score')
  66. ->orderRaw('(b.comprehensive_score + b.weight_score) desc')
  67. ->limit(100)
  68. ->select()
  69. ->toArray();
  70. $queue = [];
  71. foreach($worker as $value) {
  72. //过滤已接过此单的师傅
  73. $exists = ServiceWorkAllocateWorkerLog::where('work_id', $item['id'])->where('master_worker_id',$value['id'])->count();
  74. if ($exists) {
  75. continue;
  76. }
  77. if ( $value['distance'] > 0) {
  78. //校验客户的地址是否在工程师的接单区域内
  79. $realDistance = haversineDistance($item['lat'],$item['lon'], $value['lat'],$value['lon'],$value['distance']);
  80. if ($realDistance > $value['distance']) {
  81. continue;
  82. }
  83. }
  84. //计算地理效率得分
  85. $realDistance = ceil($realDistance / 1000);
  86. $travelTime = $realDistance * 2;//预计每公里行驶2分钟
  87. $distanceScore = 100 - ($travelTime * 1.5) - ($realDistance * 5);
  88. $distanceScore = bcadd($distanceScore, 0, 2);
  89. $tmpDistanceRate = bcmul($distanceScore, $distanceRate, 2);
  90. $tmpRate = 0;
  91. $value['travelTime'] = $travelTime;
  92. $value['comprehensive_score'] = isset($value['comprehensive_score']) ? $value['comprehensive_score'] : 0;
  93. $value['weight_score'] = isset($value['weight_score']) ? $value['weight_score'] : 0;
  94. $tmpRate = bcmul($value['comprehensive_score'], $comprehensiveRate, 2) + bcmul($value['weight_score'], $weightRate, 2);
  95. $tmpRate = bcadd($tmpRate, $tmpDistanceRate,2);
  96. $queue[$tmpRate."_".$value['id']] = $value;
  97. }
  98. //按照工程师的总分值倒序排序
  99. krsort($queue);
  100. foreach($queue as $worker) {
  101. $serviceTime = MasterWorkerServiceTime::where('master_worker_id',$worker['id'])->where('goods_category_id',$item['goods_category_id'])->value('service_time');
  102. if (empty($serviceTime)) {
  103. $serviceTime = GoodsTime::whereRaw('FIND_IN_SET('.$item['goods_category_id'].', goods_category_ids)')->value('service_time');
  104. $serviceTime = $serviceTime ?? $this->defaultServiceTime;//默认服务时长
  105. }
  106. //预约开始时间和结束时间
  107. $appointment_time = is_numeric($item['appointment_time']) ? $item['appointment_time'] : strtotime($item['appointment_time']);
  108. $estimated_finish_time = $appointment_time + $serviceTime * 60 + $worker['travelTime'] * 60;
  109. //校验客户的预约时间是否在工程师的空挡期内
  110. $count = ServiceWork::where([
  111. ['master_worker_id','=',$worker['id']],
  112. ['work_status','>=',1],
  113. ['work_status','<=',5],
  114. ])
  115. ->where(function ($query) use ($appointment_time,$estimated_finish_time) {
  116. $query->where('appointment_time', 'between',[$appointment_time, $estimated_finish_time])
  117. ->whereOr('estimated_finish_time', 'between', [$appointment_time, $estimated_finish_time]);
  118. })
  119. ->count();
  120. if ($count == 0) {
  121. $res = $this->allocateWorker($item,$worker,$estimated_finish_time);
  122. if ($res === true) {
  123. $isExec = 1;
  124. }
  125. break;
  126. }
  127. }
  128. } catch (\Exception $e) {
  129. Log::write('自动派单异常:'.$e->getMessage());
  130. sleep(5);
  131. }
  132. if ($isExec == 0) {
  133. ServiceWork::where('id',$item['id'])->update([
  134. 'exec_time' => time(),
  135. ]);
  136. }
  137. }
  138. }
  139. }
  140. /**
  141. * 分配工程师
  142. */
  143. protected function allocateWorker($workDetail, $worker, $estimated_finish_time)
  144. {
  145. Db::startTrans();
  146. try {
  147. ServiceWork::where('id',$workDetail['id'])->update([
  148. 'master_worker_id'=>$worker['id'],
  149. 'work_status'=>1,
  150. 'estimated_finish_time' => $estimated_finish_time,
  151. 'dispatch_time'=>time(),
  152. 'exec_time' => time(),
  153. ]);
  154. MasterWorker::setWorktotal('inc',$worker['id']);
  155. $work_log = [
  156. 'work_id'=>$workDetail['id'],
  157. 'master_worker_id'=>$worker['id'],
  158. 'type' => 0,
  159. 'opera_log'=>'系统自动派单于'.date('Y-m-d H:i:s',time()).'分配了工程师'.'编号['.$worker['worker_number'].']'.$worker['real_name']
  160. ];
  161. ServiceWorkerAllocateWorkerLogic::add($work_log);
  162. Db::commit();
  163. } catch (\Exception $e) {
  164. Db::rollback();
  165. Log::write('自动派单分配工程师异常:'.$e->getMessage());
  166. return false;
  167. }
  168. // 工程师派单通知【给工程师的通知】【公众号通知,不发短信】
  169. $res = event('Notice', [
  170. 'scene_id' => 113,
  171. 'params' => [
  172. 'user_id' => $worker['id'],
  173. 'order_id' => $workDetail['id'],
  174. 'thing9' => $workDetail['title'],
  175. 'time7' => $workDetail['appointment_time'],
  176. 'thing8' => (iconv_strlen($workDetail['address'])>15)?(mb_substr($workDetail['address'],0,15,'UTF-8').'...'):$workDetail['address'],
  177. 'phone_number6' => asteriskString($workDetail['mobile']),
  178. ]
  179. ]);
  180. return true;
  181. }
  182. }