diff --git a/application/admin/command/Test.php b/application/admin/command/Test.php index 3356e58..dabed0b 100644 --- a/application/admin/command/Test.php +++ b/application/admin/command/Test.php @@ -3,12 +3,26 @@ namespace app\admin\command; use app\admin\addresmart\Address; +use app\admin\controller\orders\DispatchLogic; +use app\admin\model\Order; +use app\admin\model\OrderDispatch; +use app\admin\model\OrderReview; +use app\admin\model\Worker; +use app\admin\model\WorkerItem; +use think\Collection; use think\console\Command; use think\console\Input; use think\console\Output; +use think\Db; +use think\exception\DbException; +use think\Hook; +use think\Model; +use function Symfony\Component\Clock\now; class Test extends Command { + + protected function configure() { $this->setName('test') @@ -17,10 +31,12 @@ class Test extends Command protected function execute(Input $input, Output $output) { - $string = '张三 13800138000 120113196808214821深圳市龙华区龙华街道1980科技文化产业园3栋317 地板'; - $r = Address::smart($string); - dd($r); + $order = (new Order())->find(110); + $id = (new DispatchLogic())->getMaxScoreWorker($order); + dd($id); } + + } diff --git a/application/admin/controller/Order.php b/application/admin/controller/Order.php index 8211365..978ac81 100644 --- a/application/admin/controller/Order.php +++ b/application/admin/controller/Order.php @@ -3,7 +3,7 @@ namespace app\admin\controller; use app\admin\addresmart\Address; -use app\admin\model\AuthGroupAccess; +use app\admin\controller\orders\DispatchLogic; use app\admin\model\order\Invoice; use app\admin\model\OrderDispatch; use app\admin\model\Worker; @@ -27,7 +27,6 @@ use function Symfony\Component\Clock\now; */ class Order extends Backend { - /** * Order模型对象 * @var \app\admin\model\Order @@ -347,24 +346,13 @@ class Order extends Backend private function autoDispatch($order) { - if ($order->dispatch_type != 2) { - return false; - } +// +// if ($order->dispatch_type != 2) { +// return false; +// } - $worker_ids = (new Worker())->where('area_id', $order->area_id) - ->where('status', 1) - ->field(['id', 'area_id', 'lng'], 'lat') - ->column('id'); - $worker_items_ids = (new WorkerItem()) - ->where('item_id', $order->item_id) - ->whereIn('worker_id', $worker_ids) - ->field(['worker_id'], 'lat') - ->column('worker_id'); - - $out_workers = array_intersect($worker_ids, $worker_items_ids); - - $worker_id = $out_workers[0] ?? false; + $worker_id = (new DispatchLogic())->getMaxScoreWorker($order); if (!$worker_id) { $order->dispatch_type = 1; @@ -389,7 +377,7 @@ class Order extends Backend $orderDispatch->allowField(true)->save($insert); $order->status = \app\admin\model\Order::STATUS_DISPATCHED; $order->dispatch_time = date('Y-m-d H:i:s'); - // $order->dispatch_admin_id = $this->auth->id; + // $order->dispatch_admin_id = $this->auth->id; $order->worker_id = $worker_id; $order->save(); diff --git a/application/admin/controller/orders/DispatchLogic.php b/application/admin/controller/orders/DispatchLogic.php new file mode 100644 index 0000000..a65dcd0 --- /dev/null +++ b/application/admin/controller/orders/DispatchLogic.php @@ -0,0 +1,310 @@ +lng,$order->lat]); + + $worker_ids = array_column($worker_info,'id'); + + + $worker_items_ids = (new WorkerItem()) + ->where('item_id', $order->item_id) + ->whereIn('worker_id', $worker_ids) + ->field(['worker_id']) + ->column('worker_id'); + + $out_worker_ids = array_intersect($worker_ids, $worker_items_ids); + + $worker_doings = (new OrderDispatch()) + ->whereIn('worker_id', $out_worker_ids) + ->group('worker_id') + ->field(['worker_id','count(if(status > 0 & status < 60,1,null)) count']) + ->select(); + + $worker_doing_map = []; + foreach ($worker_doings as $item){ + $worker_doing_map[$item->worker_id] = $item->toArray(); + } + + // 获取工人信息; + $worker_rate = $this->getWorkerRate($out_worker_ids); + + $worker_scores = []; + $worker_rate_map = array_column($worker_rate,null,'worker_id'); + + foreach ($worker_info as $item){ + if (in_array($item['id'],$out_worker_ids)){ + $worker_scores [] = [ + 'id' => $item['id'], + 'distance' => $item['distance'], +// 'star' => $item['star'], + 'doing' => $worker_doing_map[$item['id']]['count'] ?? 0, + 'status' => $worker_rate_map[$item['id']] ?? [ + 'arrive_rate' => 0, + 'refuse_rate' => 0, + 'trans_rate' => 0, + 'good_rate' => 0, + 'avg_time_diff' => 0, + ], + ]; + } + } + $worker_score_map = []; + + foreach ($worker_scores as $worker_score){ + $worker_score_map [$worker_score['id']] = $this->scoreWorker($worker_score); + } + + // 根据得分排序(从高到低) + arsort($worker_score_map); // 保持键名不变 + + // 取出第一个 key(就是得分最高的 ID) + return array_key_first($worker_score_map); + } + + + function scoreWorker($worker, $maxDistance = 10000, $requiredSkills = []) { + // 距离得分(越近分数越高) + $geoScore = max(0, 1 - ($worker['distance'] / $maxDistance)); // 范围 [0,1] + + // 技能得分(假设这个人不匹配) + $skillScore = count($requiredSkills) ? 0 : 1; + + // 当前状态得分(未完成订单越少越好) + $doing = $worker['doing']; + $statusScore = $doing >= 10 ? 0 : (10 - $doing) / 10; + + // 历史表现得分(各项 0~1) + $s = $worker['status']; + + $historyScore = ( + 0.4 * (floatval($s['good_rate']) / 100) + + 0.25 * (floatval($s['trans_rate']) / 100) + + 0.2 * (floatval($s['arrive_rate']) / 100) + + 0.1 * (1 - min(floatval($s['avg_time_diff']) / 10, 1)) + // 联系时间越短越好 + 0.05 * (1 - floatval($s['refuse_rate']) / 100) // 拒单率越低越好 + ); + + // 综合得分(加权) + $totalScore = + 0.3 * $geoScore + + 0.2 * $skillScore + + 0.2 * $statusScore + + 0.3 * $historyScore; + + return round($totalScore, 4); + } + + + + + private function getWorkerRate($worker_ids){ + + $filter = [ + 'start_time' => now()->modify('-31 days')->format('Y-m-d H:i:s'), + 'end_timne'=> now()->format('Y-m-d H:i:s') + ]; + + //派单表 + $dispatchSubsql = $this->dispatchSubsql($filter); + //订单表 + $orderSubsql = $this->oderSubsql($filter); + //评分表 + $viewSubsql = $this->viewSubsql($filter); + + $list = (new Worker())->alias('fa_worker') + ->whereIn('id',$worker_ids) + ->field([ + 'fa_worker.*', + 'IFNULL(a.dispatch_count, 0) AS dispatch_count', + 'IFNULL(a.get_count, 0) AS get_count', + 'IFNULL(a.refuse_count, 0) AS refuse_count', + 'IFNULL(a.arrive_count, 0) AS arrive_count', + 'IFNULL(a.avg_time_diff, 0) AS avg_time_diff', + 'IFNULL(b.finish_num, 0) AS finish_num', + 'IFNULL(b.total, 0) AS total', + 'IFNULL(b.performance, 0) AS performance', + 'IFNULL(b.refund_total, 0) AS refund_total', + 'IFNULL(b.refund_count, 0) AS refund_count', + 'IFNULL(b.cost, 0) AS cost', + 'IFNULL(c.good_count, 0) AS good_count' + ]) + ->join([$dispatchSubsql => 'a'], 'fa_worker.id = a.worker_id', 'LEFT') + ->join([$orderSubsql => 'b'], 'fa_worker.id = b.worker_id', 'LEFT') + ->join([$viewSubsql => 'c'], 'fa_worker.id = c.worker_id', 'LEFT') + ->select(); + + + $this->_toList($list); + $out = []; + foreach ($list as $item){ + $out [] = [ + 'worker_id' => $item->id, + 'arrive_rate' => $item->arrive_rate, + 'refuse_rate' => $item->refuse_rate, + 'trans_rate' => $item->trans_rate, + 'good_rate' => $item->good_rate, + 'avg_time_diff' => $item->avg_time_diff, + + + ]; + } + return $out; + + } + + + /** + * @return bool|Collection|PDOStatement|string + * @throws DbException + */ + public function viewSubsql($filter=[]){ + //评分表 + $viewSubsql = OrderReview::where('worker_star',5)->field(['worker_id','count(*) as good_count']); + + if(!empty($filter['start_time'])){ + $viewSubsql->where('create_time','>=',$filter['start_time']); + } + if(!empty($filter['end_time'])){ + $viewSubsql->where('create_time','<=',$filter['start_time']); + } + return $viewSubsql->group('worker_id')->buildSql(); + } + + /** + * @return bool|PDOStatement|string|Collection + * @throws DbException + */ + public function dispatchSubsql($filter): Collection|bool|string|PDOStatement + { + $builder = new OrderDispatch(); + $fields = [ + 'worker_id', + // 使用 IFNULL 确保结果为 null 时返回 0 + "IFNULL(COUNT(*), 0) AS dispatch_count", //分配数 + "IFNULL(COUNT(CASE WHEN status NOT IN (0, -10) THEN 1 END), 0) AS get_count", //接单数 + //"COUNT(CASE WHEN status IN (60) THEN 1 END) AS finish_count", //完成数 + "IFNULL(COUNT(CASE WHEN status NOT IN (-10) THEN 1 END), 0) AS refuse_count", //拒绝数 + "IFNULL(COUNT(arrive_time), 0) AS arrive_count", //上门数 + "IFNULL(AVG(CASE WHEN status = 60 AND arrive_time IS NOT NULL THEN UNIX_TIMESTAMP(arrive_time) - UNIX_TIMESTAMP(create_time) END), 0) AS avg_time_diff", //联系时效 + ]; + $builder->field($fields); + + if(!empty($filter['start_time'])){ + $builder->where('create_time','>=',$filter['start_time']); + } + if(!empty($filter['end_time'])){ + $builder->where('create_time','<=',$filter['start_time']); + } + + $builder->group('worker_id'); + return $builder->buildSql(); + } + + + + //图表统计 + + /** + * @throws DbException + */ + public function oderSubsql($filter = []): Collection|bool|string|PDOStatement + { + + $orderValid = implode(',',(new Order())->tabStatus(Order::TAB_VALID)); + + //"COUNT(CASE WHEN status IN (".$orderValid.") THEN 1 END) AS ing_num", + $fields = [ + 'worker_id', + // 使用 IFNULL 确保结果为 null 时返回 0 + "IFNULL(COUNT(CASE WHEN status = 60 THEN 1 END), 0) AS finish_num", //完成数 + //"COUNT(CASE WHEN status IN (".$orderValid.") THEN 1 END) AS count_num", //总订单数 (排除取消 和草稿) + "IFNULL(SUM(CASE WHEN status = 60 THEN total END), 0) AS total", //成效额 + "IFNULL(SUM(CASE WHEN status = 60 THEN performance END), 0) AS performance", //业绩 + "IFNULL(SUM(CASE WHEN status = 60 THEN cost END), 0) AS cost", //成效额 + + // "SUM(CASE WHEN status = 60 THEN (cost + material_cost) END) AS cost_total", //总成本 + + "IFNULL(SUM(CASE WHEN status = 60 THEN (refund_amount + worker_refund_amount) END), 0) AS refund_total", //退款总数 + "IFNULL(COUNT(CASE WHEN refund_amount > 0 OR worker_refund_amount > 0 THEN 1 END), 0) AS refund_count", //退款订单数量 + //"AVG(CASE WHEN status > 10 THEN UNIX_TIMESTAMP(dispatch_time) - UNIX_TIMESTAMP(create_time) END) AS avg_time_diff", //派单时效 + // "SUM(CASE WHEN status = 60 THEN (field1 + field2) END) AS performance", + ]; + + $builder = (new Order())->field($fields); + + if(!empty($filter['start_time'])){ + $builder->where('create_time','>=',$filter['start_time']); + } + if(!empty($filter['end_time'])){ + $builder->where('create_time','<=',$filter['start_time']); + } + + //->where('dispatch_admin_id','>',0); + return $builder->group('worker_id')->buildSql(); + + } + + private function _toList($list) + { + foreach ($list as &$datum){ + //利润率 = 总业绩/总成效额 + $datum->performance_rate = $this->_calc($datum->performance,$datum->total,4,true); + //转化率 = 完单数 / 总接单数 + $datum->trans_rate = $this->_calc($datum->finish_num,$datum->get_count,4,true); + //变现值 = 总业绩 / 总接单数 + $datum->cash_value = $this->_calc($datum->performance,$datum->get_count,2); + //拒单率 = 拒绝数 / 派单数 + $datum->refuse_rate = $this->_calc($datum->refuse_count,$datum->dispatch_count,4,true); + //上门率 = 打卡数 / 接单数 + $datum->arrive_rate = $this->_calc($datum->arrive_count,$datum->get_count,4,true); + //好评率 + $datum->good_rate = $this->_calc($datum->good_count,$datum->finish_num,4,true); + + $datum->avg_time_diff = $this->_calc($datum->avg_time_diff,3600,2); + } + } + + + private function _calc($a, $b, int $scale=4, bool $is_percent=false): int|string + { + $a = $a??0; + $b = $b??0; + $val = $b > 0 ? bcdiv($a,$b,$scale) : '0.00'; + + if($is_percent){ + return bcmul($val,100,2); + } + return $val; + } +} diff --git a/application/admin/controller/statistics/Item.php b/application/admin/controller/statistics/Item.php index 1dbfa30..bb266f6 100644 --- a/application/admin/controller/statistics/Item.php +++ b/application/admin/controller/statistics/Item.php @@ -42,13 +42,12 @@ class Item extends Backend { $build = new Order(); - $start = now()->modify('-14 days')->format('Y-m-d'); + $start = now()->modify('-7 days')->format('Y-m-d'); $end_at = now()->format('Y-m-d 23:29:59'); - $filter = json_decode(request()->get('filter','[]'),true); - - if (!empty($filter['daterange'])) { - $arr = explode(' - ', $filter['daterange']); + $filter = request()->get('range',''); + if (!empty($filter)){ + $arr = explode(' - ', $filter); if (trim($arr[0])) { $start = trim($arr[0]); } @@ -56,6 +55,14 @@ class Item extends Backend $end_at = trim($arr[1]) . ' 23:29:59'; } } + $area_id = request()->get('area_id'); + + if ($area_id) { + $area_id = $this->trimSpecialZeros($area_id); + $build->where('area_id', 'like', $area_id . '%'); + } + + $build->whereBetween('create_time', [$start, $end_at]) ->field([ 'item_title name', // 类型 @@ -99,6 +106,21 @@ class Item extends Backend } + function trimSpecialZeros($str) { + if (strlen($str) !== 6) { + return $str; // 非6位字符串直接返回 + } + + if (substr($str, -4) === "0000") { + return substr($str, 0, 2); // 去掉后4位 + } elseif (substr($str, -2) === "00") { + return substr($str, 0, 4); // 去掉后2位 + } + + return $str; // 不符合条件则原样返回 + } + + public function chartData() { $build = new Order(); @@ -159,16 +181,32 @@ class Item extends Backend $refundRate[] = $total > 0 ? round($refund / $total * 100, 2) : 0; $monetizedValue[] = $total / $count; } - $result = [ - 'xAxis' => $xAxis, - 'series' => [ - ['name' => '总业绩(¥)', 'type' => 'bar', 'yAxisIndex' => 0, 'data' => $totalPerformance], - ['name' => '转化率(%)', 'type' => 'bar', 'yAxisIndex' => 1, 'data' => $conversionRate], - ['name' => '利润率(%)', 'type' => 'bar', 'yAxisIndex' => 1, 'data' => $profitRate], - ['name' => '退款率(%)', 'type' => 'bar', 'yAxisIndex' => 1, 'data' => $refundRate], - ['name' => '变现值(¥)', 'type' => 'line', 'yAxisIndex' => 0, 'data' => $monetizedValue], - ] - ]; + + if ($totalPerformance){ + $result = [ + 'xAxis' => $xAxis, + 'series' => [ + ['name' => '总业绩(¥)', 'type' => 'bar', 'yAxisIndex' => 0, 'data' => $totalPerformance], + ['name' => '转化率(%)', 'type' => 'bar', 'yAxisIndex' => 1, 'data' => $conversionRate], + ['name' => '利润率(%)', 'type' => 'bar', 'yAxisIndex' => 1, 'data' => $profitRate], + ['name' => '退款率(%)', 'type' => 'bar', 'yAxisIndex' => 1, 'data' => $refundRate], + ['name' => '变现值(¥)', 'type' => 'line', 'yAxisIndex' => 0, 'data' => $monetizedValue], + ] + ]; + }else{ + $result = [ + 'xAxis' => ['无'], + 'series' => [ + ['name' => '总业绩(¥)', 'type' => 'bar', 'yAxisIndex' => 0, 'data' => [0]], + ['name' => '转化率(%)', 'type' => 'bar', 'yAxisIndex' => 1, 'data' => [0]], + ['name' => '利润率(%)', 'type' => 'bar', 'yAxisIndex' => 1, 'data' => [0]], + ['name' => '退款率(%)', 'type' => 'bar', 'yAxisIndex' => 1, 'data' => [0]], + ['name' => '变现值(¥)', 'type' => 'line', 'yAxisIndex' => 0, 'data' => [0]], + ] + ]; + } + + return $result; } diff --git a/application/admin/view/statistics/item/index.html b/application/admin/view/statistics/item/index.html index 2613f89..8a0c7a7 100644 --- a/application/admin/view/statistics/item/index.html +++ b/application/admin/view/statistics/item/index.html @@ -44,11 +44,14 @@