1
0

AutomaticDispatch.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  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\master_worker\MasterWorkerTeam;
  12. use app\common\model\works\ServiceWorkAllocateWorkerLog;
  13. use app\workerapi\logic\ServiceWorkerAllocateWorkerLogic;
  14. use app\common\model\master_worker\MasterWorkerServiceTime;
  15. class AutomaticDispatch extends Command
  16. {
  17. //地理时效评分占比
  18. protected $distanceRate = 0.25;
  19. //工程师权重评分占比
  20. protected $weightRate = 0.2;
  21. //工程师综合评分占比
  22. protected $comprehensiveRate = 0.55;
  23. //默认服务时长 300 分钟
  24. protected $defaultServiceTime = 300;
  25. protected function configure()
  26. {
  27. $this->setName('automatic_dispatch')
  28. ->setDescription('自动派单');
  29. }
  30. protected function execute(Input $input, Output $output)
  31. {
  32. $this->autoDispatch();
  33. }
  34. /*
  35. * 自动派单总分100分
  36. 1、地理效率得分(distance score)25%
  37. 2、工程师权重「自定义是否有证」(weight score)20%
  38. 3、工程师综合服务分 55%
  39. */
  40. protected function autoDispatch()
  41. {
  42. $size = 30;
  43. $startTime = strtotime(date('Y-m-d 00:00:00'));
  44. $endTime = strtotime(date('Y-m-d 23:59:59'));
  45. while(true) {
  46. // 获取当前时间的前五分钟时间戳
  47. $fiveMinutesAgo = time() - 120; // 300 秒 = 2 分钟
  48. $list = ServiceWork::where('work_status',0)
  49. ->where(function ($query) use ($fiveMinutesAgo) {
  50. $query->where('exec_time', 0)->whereOr('exec_time', '<', $fiveMinutesAgo);
  51. })
  52. ->where('appointment_time','between', [$startTime, $endTime])
  53. ->field('id,category_type,goods_category_id,lon,lat,province,city,title,appointment_time,address,mobile')
  54. ->order('create_time','asc')
  55. ->limit($size)
  56. ->select()
  57. ->toArray();
  58. if (!$list) {
  59. sleep(5);
  60. }
  61. $isExec = 1;//是否派单成功
  62. foreach($list as $item) {
  63. try {
  64. //优先平台工程师派单
  65. $res = $this->platformWorker($item);
  66. if ($res === false) {
  67. //门店负责人派单
  68. $res = $this->teamWorker($item);
  69. if ($res === false) {
  70. $isExec = 0;
  71. }
  72. }
  73. } catch (\Exception $e) {
  74. print_r($e->getMessage());
  75. Log::write('自动派单异常:'.$e->getMessage());
  76. sleep(5);
  77. }
  78. if ($isExec == 0) {
  79. ServiceWork::where('id',$item['id'])->update([
  80. 'exec_time' => time(),
  81. ]);
  82. }
  83. }
  84. }
  85. }
  86. /**
  87. * 派单给平台工程师
  88. */
  89. protected function platformWorker($item) {
  90. $real_distance = Db::raw("ST_Distance_Sphere(
  91. POINT({$item['lon']}, {$item['lat']}),
  92. POINT(lon, lat)
  93. ) AS real_distance");
  94. // 获取符合条件的工程师
  95. $worker = MasterWorker::alias('a')->leftJoin('master_worker_score b','a.id = b.worker_id')
  96. ->where([
  97. ['is_disable','=',0],
  98. ['work_status','=',0],
  99. ['accept_order_status','=',1],
  100. ['city','=',$item['city']],
  101. ['tenant_id','=',0]
  102. ])
  103. ->whereRaw('FIND_IN_SET('.$item['goods_category_id'].', a.category_ids)')
  104. ->whereRaw("ST_Distance_Sphere(
  105. POINT({$item['lon']}, {$item['lat']}),
  106. POINT(lon, lat)
  107. ) <= distance")
  108. ->field(['a.id','a.tenant_id','a.distance','a.lon','a.lat','a.worker_number','a.real_name','b.comprehensive_score','b.weight_score',$real_distance])
  109. ->orderRaw('(b.comprehensive_score + b.weight_score) desc')
  110. ->limit(100)
  111. ->select()
  112. ->toArray();
  113. $queue = [];
  114. foreach($worker as $key => $value) {
  115. //过滤已接过此单的师傅
  116. $exists = ServiceWorkAllocateWorkerLog::where('work_id', $item['id'])->where('master_worker_id',$value['id'])->count();
  117. if ($exists) {
  118. continue;
  119. }
  120. //计算地理效率得分
  121. $realDistance = ceil($value['real_distance'] / 1000);
  122. $travelTime = $realDistance * 2;//预计每公里行驶2分钟
  123. $distanceScore = 100 - ($travelTime * 1.5) - ($realDistance * 5);
  124. $distanceScore = bcadd($distanceScore, 0, 2);
  125. $tmpDistanceRate = bcmul($distanceScore, $this->distanceRate, 2);
  126. $tmpRate = 0;
  127. $value['travelTime'] = $travelTime;
  128. $value['comprehensive_score'] = isset($value['comprehensive_score']) ? $value['comprehensive_score'] : 0;
  129. $value['weight_score'] = isset($value['weight_score']) ? $value['weight_score'] : 0;
  130. $tmpRate = bcmul($value['comprehensive_score'], $this->comprehensiveRate, 2) + bcmul($value['weight_score'], $this->weightRate, 2);
  131. $tmpRate = bcadd($tmpRate, $tmpDistanceRate,2);
  132. $tmpKey = isset($queue[$tmpRate]) ? bcadd($tmpRate, $key / 100,2) : $tmpRate;//防止键名重复
  133. $queue[$tmpKey] = $value;
  134. }
  135. //按照工程师的总分值倒序排序
  136. krsort($queue);
  137. foreach($queue as $worker) {
  138. $serviceTime = MasterWorkerServiceTime::where('master_worker_id',$worker['id'])->where('goods_category_id',$item['goods_category_id'])->value('service_time');
  139. if (empty($serviceTime)) {
  140. $serviceTime = GoodsTime::whereRaw('FIND_IN_SET('.$item['goods_category_id'].', goods_category_ids)')->value('service_time');
  141. $serviceTime = $serviceTime ?? $this->defaultServiceTime;//默认服务时长
  142. }
  143. //预约开始时间和结束时间
  144. $appointment_time = is_numeric($item['appointment_time']) ? $item['appointment_time'] : strtotime($item['appointment_time']);
  145. $estimated_finish_time = $appointment_time + $serviceTime * 60 + $worker['travelTime'] * 60;
  146. //校验客户的预约时间是否在工程师的空挡期内
  147. $count = ServiceWork::where([
  148. ['master_worker_id','=',$worker['id']],
  149. ['work_status','>=',1],
  150. ['work_status','<=',5],
  151. ])
  152. ->where(function ($query) use ($appointment_time,$estimated_finish_time) {
  153. $query->where('appointment_time', 'between',[$appointment_time, $estimated_finish_time])
  154. ->whereOr('estimated_finish_time', 'between', [$appointment_time, $estimated_finish_time]);
  155. })
  156. ->count();
  157. if ($count == 0) {
  158. $operaLog = '系统自动派单于'.date('Y-m-d H:i:s',time()).'分配了工程师'.'编号['.$worker['worker_number'].']'.$worker['real_name'];
  159. $res = $this->allocateWorker($item,$worker['id'],$worker['tenant_id'],$operaLog,$estimated_finish_time);
  160. if ($res === true) {
  161. return true;
  162. }
  163. }
  164. }
  165. return false;
  166. }
  167. /**
  168. * 派单给门店负责人
  169. */
  170. protected function teamWorker($item) {
  171. $real_distance = Db::raw("ST_Distance_Sphere(
  172. POINT({$item['lon']}, {$item['lat']}),
  173. POINT(lon, lat)
  174. ) AS real_distance");
  175. $isAm = date("H",strtotime($item['appointment_time'])) < 12 ? 1 : 0;
  176. $whereRaw = $isAm ? 'am_order < am_limit' : 'pm_order < pm_limit';
  177. // 获取符合条件的工程师
  178. $worker = MasterWorkerTeam::where([
  179. ['accept_order_status','=',1],
  180. ['city','=',$item['city']],
  181. ])
  182. ->whereRaw($whereRaw)
  183. ->whereRaw('FIND_IN_SET('.$item['goods_category_id'].', goods_category_ids)')
  184. ->whereRaw("ST_Distance_Sphere(
  185. POINT({$item['lon']}, {$item['lat']}),
  186. POINT(lon, lat)
  187. ) <= distance")
  188. ->field(['id','lon','lat','distance','tenant_id','team_name','master_worker_id','am_order','am_limit','pm_order','pm_limit','min_order','comprehensive_score', $real_distance])
  189. ->order('comprehensive_score','desc')
  190. ->limit(100)
  191. ->select()
  192. ->toArray();
  193. $minQueue = [];
  194. $queue = [];
  195. foreach($worker as $key => $value) {
  196. //过滤已接过此单的师傅
  197. $exists = ServiceWorkAllocateWorkerLog::where('work_id', $item['id'])->where('master_worker_id',$value['master_worker_id'])->count();
  198. if ($exists) {
  199. continue;
  200. }
  201. if ( $value['distance'] > 0) {
  202. //校验客户的地址是否在工程师的接单区域内
  203. $realDistance = haversineDistance($item['lat'],$item['lon'], $value['lat'],$value['lon'],$value['distance']);
  204. if ($realDistance > $value['distance']) {
  205. continue;
  206. }
  207. }
  208. if ($value['am_order'] + $value['pm_order'] < $value['min_order']) {
  209. $minQueue[] = $value;
  210. } else {
  211. $queue[] = $value;
  212. }
  213. }
  214. $queue = array_merge($minQueue,$queue);
  215. //优先给接单数量不足最低接单数的团队派单,其次再给服务评分高的派单
  216. foreach($queue as $worker) {
  217. $operaLog = '系统自动派单于'.date('Y-m-d H:i:s',time()).'分配了团队ID['.$worker['id'].']'.$worker['team_name'];
  218. $res = $this->allocateWorker($item, $worker['master_worker_id'], $worker['tenant_id'], $operaLog);
  219. if ($res === true) {
  220. $updateData = $isAm == 1 ? ['am_order' => Db::raw('am_order + 1')] : ['pm_order' => Db::raw('pm_order + 1')];
  221. MasterWorkerTeam::where('id',$worker['id'])->update($updateData);
  222. return true;
  223. }
  224. }
  225. return false;
  226. }
  227. /**
  228. * 分配工程师
  229. */
  230. protected function allocateWorker($workDetail, $masterWorkerId, $tenant_id,$operaLog, $estimated_finish_time=0)
  231. {
  232. Db::startTrans();
  233. try {
  234. ServiceWork::where('id',$workDetail['id'])->update([
  235. 'master_worker_id'=>$masterWorkerId,
  236. 'tenant_id' => $tenant_id,
  237. 'work_status'=>1,
  238. 'estimated_finish_time' => $estimated_finish_time,
  239. 'dispatch_time'=>time(),
  240. 'exec_time' => time(),
  241. ]);
  242. MasterWorker::setWorktotal('inc',$masterWorkerId);
  243. $work_log = [
  244. 'work_id'=>$workDetail['id'],
  245. 'master_worker_id'=>$masterWorkerId,
  246. 'type' => 0,
  247. 'opera_log'=> $operaLog
  248. ];
  249. ServiceWorkerAllocateWorkerLogic::add($work_log);
  250. Db::commit();
  251. } catch (\Exception $e) {
  252. Db::rollback();
  253. Log::write('自动派单分配工程师异常:'.$e->getMessage());
  254. return false;
  255. }
  256. // 工程师派单通知【给工程师的通知】【公众号通知,不发短信】
  257. $res = event('Notice', [
  258. 'scene_id' => 113,
  259. 'params' => [
  260. 'user_id' => $masterWorkerId,
  261. 'order_id' => $workDetail['id'],
  262. 'thing9' => $workDetail['title'],
  263. 'time7' => $workDetail['appointment_time'],
  264. 'thing8' => (iconv_strlen($workDetail['address'])>15)?(mb_substr($workDetail['address'],0,15,'UTF-8').'...'):$workDetail['address'],
  265. 'phone_number6' => asteriskString($workDetail['mobile']),
  266. ]
  267. ]);
  268. return true;
  269. }
  270. }