UpdateWorkerScore.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. <?php
  2. namespace app\common\command;
  3. use think\facade\Log;
  4. use think\console\Input;
  5. use think\console\Output;
  6. use think\console\Command;
  7. use think\console\input\Argument;
  8. use app\common\model\works\IssueWork;
  9. use app\common\model\works\ReturnWork;
  10. use app\common\model\works\ServiceWork;
  11. use app\common\model\goods_time\GoodsTime;
  12. use app\common\model\reviews\GoodsReviews;
  13. use app\common\model\master_worker\MasterWorker;
  14. use app\common\model\effective\OrderEffectiveLog;
  15. use app\common\model\works\ServiceWorkReceiveLog;
  16. use app\common\model\master_worker\MasterWorkerTeam;
  17. use app\common\model\master_worker\MasterWorkerScore;
  18. use app\common\model\works\ServiceWorkAllocateWorkerLog;
  19. use app\common\model\master_worker\MasterWorkerServiceTime;
  20. use app\common\model\works\ServiceWorkLog;
  21. use app\common\model\service_area\ServiceArea;
  22. class UpdateWorkerScore extends Command
  23. {
  24. protected $defaultServiceTime = 300; //默认服务时长 300 分钟
  25. protected function configure()
  26. {
  27. $this->setName('update_worker_score')
  28. ->setDescription('更新工程师综合评分和服务时长')
  29. ->addArgument('type', Argument::OPTIONAL, '类型可选');
  30. }
  31. protected function execute(Input $input, Output $output)
  32. {
  33. // 获取传递的参数
  34. $type = $input->getArgument('type');
  35. if ($type == 'init') {
  36. $this->initMasterAreaId();
  37. } else {
  38. //更新工程师评分
  39. $this->changeWorderScore();
  40. //更新团队服务评分
  41. $this->changeWorkTeamScore();
  42. }
  43. }
  44. /*
  45. * 长期合作师傅综合评分 100分 (每一项默认20%)
  46. 评分周期:每周,保修和返修统计前30天的数据
  47. 1、用户评分
  48. 2、完单率 = 完结工单/总工单
  49. 3、客户粘性率 = 工程师第一次联系客户时间-领单时间 和定义的时间进行比较
  50. 4、加单率 = 加单个数/派单数
  51. 5、上门率 = 已上门/总单数
  52. */
  53. protected function changeWorderScore()
  54. {
  55. $startTime = date('Y-m-d 00:00:00', strtotime('-7 days'));
  56. $endTime = date('Y-m-d 23:59:59', strtotime('-1 days'));
  57. $page = 0;
  58. $size = 50;
  59. while(true) {
  60. $page++;
  61. $offset = ($page - 1) * $size;
  62. $list = MasterWorker::alias("a")
  63. ->leftJoin("master_worker_score b","a.id = b.worker_id")
  64. ->field('a.id,a.category_ids,b.comprehensive_score_history')
  65. ->limit($offset, $size)
  66. ->select()
  67. ->toArray();
  68. if (!$list) {
  69. break;
  70. }
  71. foreach($list as $item) {
  72. $workId = $item['id'];
  73. $this->updateComprehensiveScore($startTime,$endTime,$workId,$item['comprehensive_score_history']);
  74. //更新工程师平均服务时长
  75. $this->updateServiceTime($startTime,$endTime,$item);
  76. }
  77. }
  78. }
  79. /**
  80. * 初始化工程师服务区域ID
  81. */
  82. protected function initMasterAreaId()
  83. {
  84. $last_id = 0;
  85. while($last_id >= 0) {
  86. $list = MasterWorker::where('lon','>',0)
  87. ->where('lat','>',0)
  88. ->where('id','>',$last_id)
  89. ->field('id,lon,lat')
  90. ->order('id','asc')
  91. ->limit(50)
  92. ->select()->toArray();
  93. if (!$list) {
  94. $last_id = -1;
  95. break;
  96. }
  97. foreach($list as $item) {
  98. $last_id = $item['id'];
  99. $service_area_id = ServiceArea::serviceAreaId(['lon' => $item['lon'], 'lat' => $item['lat']]);
  100. if ($service_area_id) {
  101. MasterWorker::where('id',$item['id'])->update(['service_area_id' => $service_area_id]);
  102. }
  103. }
  104. }
  105. }
  106. /**
  107. * 初始化工程师汇总评分数据,只执行一次即可
  108. */
  109. protected function initMasterWorkerScore()
  110. {
  111. $masterWorker = MasterWorker::field('id')->order('id','asc')->select()->toArray();
  112. foreach($masterWorker as $item) {
  113. //添加工程师汇总评分数据
  114. MasterWorkerScore::create([
  115. 'worker_id' => $item['id']
  116. ]);
  117. }
  118. }
  119. /*
  120. * 每周统计并更新一次工程师的综合评分
  121. 1、用户评分
  122. 2、完单率 = 完结工单/总工单
  123. 3、接单率 = 已领的工单/总工单
  124. 4、工单投诉率 = 投诉的工单(判定工程师原因) /已完成工单
  125. 5、客户粘性率 = 工程师第一次联系客户时间-领单时间 和定义的时间(10分钟)进行比较
  126. 6、保修率 = 保修工单/已完成工单(去除保修单)
  127. 7、返修率 = 返修工单/已完成工单
  128. 8、加单率 = 加单个数/派单数
  129. */
  130. protected function changeTemporaryWorderScore()
  131. {
  132. $startTime = date('Y-m-d 00:00:00', strtotime('-7 days'));
  133. $endTime = date('Y-m-d 23:59:59', strtotime('-1 days'));
  134. $page = 0;
  135. $size = 50;
  136. while(true) {
  137. $page++;
  138. $offset = ($page - 1) * $size;
  139. $list = MasterWorker::alias("a")
  140. ->leftJoin("master_worker_score b","a.id = b.worker_id")
  141. ->field('a.id,a.category_ids,a.type,b.comprehensive_score_history')
  142. ->limit($offset, $size)
  143. ->select()
  144. ->toArray();
  145. if (!$list) {
  146. break;
  147. }
  148. foreach($list as $item) {
  149. $workId = $item['id'];
  150. if ($item['type'] == 1) {
  151. $this->updateComprehensiveScore($startTime,$endTime,$workId,$item['comprehensive_score_history']);
  152. } else {
  153. $this->updateTemporaryComprehensiveScore($startTime,$endTime,$workId,$item['comprehensive_score_history']);
  154. }
  155. //更新工程师平均服务时长
  156. $this->updateServiceTime($startTime,$endTime,$item);
  157. }
  158. }
  159. }
  160. /**
  161. * 更新工程师服务类目的平均服务时长
  162. */
  163. protected function updateServiceTime($startTime,$endTime,$worker) {
  164. if ($worker['category_ids']) {
  165. $category_ids = explode(",",$worker['category_ids']);
  166. foreach($category_ids as $categoryId) {
  167. if (!$categoryId) {
  168. continue;
  169. }
  170. $avgTime = ServiceWork::where('master_worker_id',$worker['id'])->where('service_status',3)->whereIn('work_type',[0,1])->whereBetweenTime('create_time', $startTime, $endTime)->field('AVG(finished_time - appointment_time) as avg_time')->find();
  171. if (isset($avgTime['avg_time']) && $avgTime['avg_time'] > 0) {
  172. $avgTime = $avgTime['avg_time'] / 60 ;
  173. } else {
  174. $avgTime = GoodsTime::whereRaw('FIND_IN_SET('.$categoryId.', goods_category_ids)')->value('service_time');
  175. $avgTime = $avgTime ?? $this->defaultServiceTime;//默认服务时长
  176. }
  177. $exists = MasterWorkerServiceTime::where('master_worker_id',$worker['id'])->where('goods_category_id',$categoryId)->value('id');
  178. if ($exists) {
  179. MasterWorkerServiceTime::where('master_worker_id',$worker['id'])->where('goods_category_id',$categoryId)->update([
  180. 'service_time' => $avgTime
  181. ]);
  182. } else {
  183. MasterWorkerServiceTime::create([
  184. 'master_worker_id' => $worker['id'],
  185. 'goods_category_id' => $categoryId,
  186. 'service_time' => $avgTime
  187. ]);
  188. }
  189. }
  190. }
  191. }
  192. /*
  193. * 长期合作师傅综合评分 100分 (每一项默认20%)
  194. 评分周期:每周
  195. 1、用户评分
  196. 2、完单率 = 完结工单/总工单
  197. 3、客户粘性率 = 工程师第一次联系客户时间-领单时间 和定义的时间进行比较
  198. 4、加单率 = 加单个数/派单数
  199. 5、上门率 = 已上门/总单数
  200. */
  201. protected function updateComprehensiveScore($startTime,$endTime,$workId,$historyScore) {
  202. try {
  203. //查询本周平均评分值
  204. $goodsReviewsAvg = GoodsReviews::alias('a')->leftJoin("service_work b","a.work_id = b.id")->whereBetweenTime('a.create_time', $startTime, $endTime)->avg('rating');
  205. $commentScore = $goodsReviewsAvg > 0 ? bcdiv($goodsReviewsAvg, 5, 2) : 0;
  206. //总工单:统计派单日志记录
  207. $allOrder = ServiceWorkAllocateWorkerLog::where('master_worker_id',$workId)->whereBetweenTime('create_time', $startTime, $endTime)->count();
  208. //完结工单:统计工单表已完成的工单
  209. $completeOrder = ServiceWork::where('master_worker_id',$workId)->where('service_status',3)->where('work_pay_status','in',[1,2])->whereBetweenTime('create_time', $startTime, $endTime)->count();
  210. if ($allOrder == 0) {
  211. $completionRate = 0;
  212. } else {
  213. //完单率
  214. $completionRate = bcdiv($completeOrder, $allOrder ,2);
  215. }
  216. if ($completeOrder == 0) {
  217. $addRate = 0;
  218. } else {
  219. //加单率
  220. $addWord = ServiceWork::where('master_worker_id',$workId)->where('work_type',2)->whereBetweenTime('create_time', $startTime, $endTime)->count();
  221. $addRate = bcdiv($addWord, $completeOrder ,2);
  222. $addRate = $addRate > 1 ? 1 : $addRate;
  223. }
  224. //客户粘性率(10分钟内)
  225. $avgTime = ServiceWork::where('master_worker_id',$workId)->where('work_status','>',1)->where('first_contact_time','>',0)->whereBetweenTime('create_time', $startTime, $endTime)->field('AVG(first_contact_time - receive_time) as avg_time')->find();
  226. $avgTime = $avgTime['avg_time'] ? $avgTime['avg_time'] / 60 : 0;
  227. $viscosityRate = $avgTime <= 10 && $avgTime > 0 ? 1 : 0;
  228. //上门率
  229. $doorCount = ServiceWorkLog::where('master_worker_id',$workId)->where('opera_log','%like%','已上门')->whereBetweenTime('create_time', $startTime, $endTime)->count();
  230. $doorRate = bcdiv($doorCount, $allOrder,2);
  231. //工程师汇总评分
  232. $comprehensiveScore = $commentScore + $completionRate + $viscosityRate + $addRate + $doorRate;
  233. $comprehensiveScore = bcdiv($comprehensiveScore, 8, 2) * 100;
  234. $this->doComprehenSivescore($workId,$comprehensiveScore,$historyScore,$doorCount);
  235. } catch (\Exception $e) {
  236. Log::write('更新长期合作工程师综合评分异常:'.$e->getMessage());
  237. return false;
  238. }
  239. }
  240. /**
  241. * 更新工程师综合评分 (短期合作工程师)
  242. */
  243. protected function updateTemporaryComprehensiveScore($startTime,$endTime,$workId,$historyScore) {
  244. try {
  245. //查询本周平均评分值
  246. $goodsReviewsAvg = GoodsReviews::alias('a')->leftJoin("service_work b","a.work_id = b.id")->whereBetweenTime('a.create_time', $startTime, $endTime)->avg('rating');
  247. $commentScore = $goodsReviewsAvg > 0 ? bcdiv($goodsReviewsAvg, 5, 2) : 0;
  248. //总工单:统计派单日志记录
  249. $allOrder = ServiceWorkAllocateWorkerLog::where('master_worker_id',$workId)->whereBetweenTime('create_time', $startTime, $endTime)->count();
  250. //完结工单:统计工单表已完成的工单
  251. $completeOrder = ServiceWork::where('master_worker_id',$workId)->where('service_status',3)->where('work_pay_status','in',[1,2])->whereBetweenTime('create_time', $startTime, $endTime)->count();
  252. //接单量:统计工程师接单的数量
  253. $acceptOrder = ServiceWorkReceiveLog::where('master_worker_id',$workId)->whereBetweenTime('create_time', $startTime, $endTime)->count();
  254. if ($allOrder == 0) {
  255. $completionRate = 0;
  256. $acceptRate = 0;
  257. } else {
  258. //完单率
  259. $completionRate = bcdiv($completeOrder, $allOrder ,2);
  260. //接单率
  261. $acceptRate = bcdiv($acceptOrder, $allOrder ,2);
  262. }
  263. if ($completeOrder == 0) {
  264. $issueRate = 0;
  265. $returnRate = 0;
  266. $addRate = 0;
  267. } else {
  268. //工单投诉率
  269. $issueWork = IssueWork::where('master_worker_id',$workId)->where('responsible',2)->whereBetweenTime('create_time', $startTime, $endTime)->count();
  270. $issueRate = bcdiv($issueWork, $completeOrder ,2);
  271. $issueRate = 1 - ($issueRate > 1 ? 1 : $issueRate);
  272. //返修率
  273. $returnWord = ReturnWork::where('master_worker_id',$workId)->whereBetweenTime('create_time', date('Y-m-d 00:00:00', strtotime('-30 days')), $endTime)->count();
  274. $returnRate = bcdiv($returnWord, $completeOrder ,2);
  275. $returnRate = 1 - ($returnRate > 1 ? 1 : $returnRate);
  276. //加单率
  277. $addWord = ServiceWork::where('master_worker_id',$workId)->where('work_type',2)->whereBetweenTime('create_time', $startTime, $endTime)->count();
  278. $addRate = bcdiv($addWord, $completeOrder ,2);
  279. $addRate = $addRate > 1 ? 1 : $addRate;
  280. }
  281. //客户粘性率(10分钟内)
  282. $avgTime = ServiceWork::where('master_worker_id',$workId)->where('work_status','>',1)->where('first_contact_time','>',0)->whereBetweenTime('create_time', $startTime, $endTime)->field('AVG(first_contact_time - receive_time) as avg_time')->find();
  283. $avgTime = $avgTime['avg_time'] ? $avgTime['avg_time'] / 60 : 0;
  284. $viscosityRate = $avgTime <= 10 && $avgTime > 0 ? 1 : 0;
  285. $partOrder = ServiceWork::where('master_worker_id',$workId)->where('work_status','>',1)->where('order_effective_id',0)->whereBetweenTime('create_time', $startTime, $endTime)->count();
  286. if ($partOrder == 0) {
  287. $warrantyRate = 0;
  288. } else {
  289. //保修率
  290. $effectiveOder = OrderEffectiveLog::alias('a')->leftJoin('service_work b', 'a.work_id = b.id')->where('b.id',$workId)->whereBetweenTime('a.create_time', date('Y-m-d 00:00:00', strtotime('-30 days')), $endTime)->count();
  291. $warrantyRate = bcdiv($effectiveOder, $partOrder ,2);
  292. $warrantyRate = 1 - ($warrantyRate > 1 ? 1 : $warrantyRate);
  293. }
  294. //工程师汇总评分
  295. $comprehensiveScore = $commentScore + $completionRate + $acceptRate + $issueRate + $viscosityRate + $warrantyRate + $returnRate + $addRate;
  296. $comprehensiveScore = bcdiv($comprehensiveScore, 8, 2) * 100;
  297. $this->doComprehenSivescore($workId,$comprehensiveScore,$historyScore,$acceptOrder);
  298. } catch (\Exception $e) {
  299. Log::write('更新短期合作工程师综合评分异常:'.$e->getMessage());
  300. return false;
  301. }
  302. }
  303. /**
  304. * 综合评分活跃度衰退机制
  305. 1. 引入双周期加权计算机制
  306. - 主周期(7天)数据:用于正常活跃状态的评分计算
  307. - 辅助周期(30天)数据:用于低活跃状态的评分平滑
  308. 2. 设置活跃度阈值(建议值)
  309. - 活跃状态:周接单量≥5单
  310. - 低活跃状态:周接单量1-4单
  311. - 停单状态:周接单量=0单
  312. 3. 衰减系数梯度表:
  313. | 状态类型 | 衰减系数 | 数据来源权重 |
  314. |---------------|----------|----------------------|
  315. | 活跃状态 | 1.0 | 100%主周期数据 |
  316. | 低活跃状态 | 0.8 | 70%主周期+30%辅助周期|
  317. | 停单状态 | 0.6 | 50%主周期+50%辅助周期|
  318. */
  319. public static function doComprehenSivescore($workId,$curScore,$historyScore,$acceptOrder)
  320. {
  321. $historyScore = $historyScore ? explode(",",$historyScore) : [];
  322. $argScore = $historyScore ? array_sum($historyScore)/count($historyScore) : 0;
  323. if ($acceptOrder >= 1 && $acceptOrder < 5) {
  324. $curScore = bcadd($curScore * 0.7, $argScore * 0.3, 2);
  325. } elseif ($acceptOrder == 0) {
  326. $curScore = bcadd($argScore * 0.5, 0, 2);
  327. }
  328. if ($curScore > 0) {
  329. if (count($historyScore) >= 4) {
  330. //移除第一个元素
  331. array_shift($historyScore);
  332. }
  333. $historyScore[] = $curScore;
  334. }
  335. MasterWorkerScore::where('worker_id',$workId)->update(['comprehensive_score' => $curScore, 'comprehensive_score_history' => implode(",",$historyScore)]);
  336. }
  337. /**
  338. * 更新团队服务评分
  339. */
  340. protected function changeWorkTeamScore() {
  341. $page = 0;
  342. $size = 50;
  343. while(true) {
  344. $page++;
  345. $offset = ($page - 1) * $size;
  346. $list = MasterWorkerTeam::alias('a')->leftJoin('master_worker b','a.master_worker_id=b.id')->field('a.id,b.tenant_id')
  347. ->limit($offset, $size)
  348. ->select()
  349. ->toArray();
  350. if (!$list) {
  351. break;
  352. }
  353. try {
  354. foreach($list as $item) {
  355. $comprehensiveScore = 3.5;
  356. if ($item['tenant_id'] > 0) {
  357. $avg = MasterWorker::alias('a')->leftJoin('master_worker_score b','a.id=b.worker_id')
  358. ->where('a.tenant_id',$item['tenant_id'])
  359. ->field('AVG(b.comprehensive_score) as avg')
  360. ->find();
  361. if (isset($avg['avg']) && $avg['avg'] > 0) {
  362. $comprehensiveScore = bcdiv($avg['avg'], 20 , 1);
  363. $comprehensiveScore = $comprehensiveScore < 3.5 ? 3.5 : $comprehensiveScore;
  364. $comprehensiveScore = $comprehensiveScore > 5 ? 5 : $comprehensiveScore;
  365. }
  366. }
  367. MasterWorkerTeam::where('id',$item['id'])->update(['comprehensive_score' => $comprehensiveScore]);
  368. }
  369. } catch (\Exception $e) {
  370. Log::write('更新团队综合评分异常:'.$e->getMessage());
  371. return false;
  372. }
  373. }
  374. }
  375. }