model = new Order(); $template = Template::where('group_id',self::GROUP_ID)->with(['kpiitem'])->find()->toArray(); $itemUnits = []; foreach ($template['kpiitem'] as $item) { $itemRate[$item['attr']] = $item['pivot']['rate']; $itemUnits[$item['attr']] = $item['unit']; } $item_title = []; $item_title['ZHL'] = [ 'title' => '转化率'.(!empty($itemRate['ZHL'])?'(权重'.$itemRate['ZHL'].'%)':''), 'isshow' => !empty($itemRate['ZHL']), 'unit' => '%' ]; $item_title['LRL'] = [ 'title' => '利润率'.(!empty($itemRate['LRL'])?'(权重'.$itemRate['LRL'].'%)':''), 'isshow' =>!empty($itemRate['LRL']), ]; $item_title['PDSX'] = [ 'title' => '派单时效'.(!empty($itemRate['PDSX'])?'(权重'.$itemRate['PDSX'].'%)':''), 'isshow' =>!empty($itemRate['PDSX']), ]; $item_title['PCCGL'] = [ 'title' => '派出成功率'.(!empty($itemRate['PCCGL'])?'(权重'.$itemRate['PCCGL'].'%)':''), 'isshow' =>!empty($itemRate['PCCGL']), ]; $item_title['GDJSL'] = [ 'title' => '跟单及时率'.(!empty($itemRate['GDJSL'])?'(权重'.$itemRate['GDJSL'].'%)':''), 'isshow' =>!empty($itemRate['GDJSL']), ]; $item_title['LRSFS'] = [ 'title' => '录入师傅数'.(!empty($itemRate['LRSFS'])?'(权重'.$itemRate['LRSFS'].'%)':''), 'isshow' =>!empty($itemRate['LRSFS']), ]; /*$item_title['LRL'] = '利润率'.(!empty($itemRate['LRL'])?$itemRate['LRL'].'%':''); $item_title['PDSX'] = '派单时效'.(!empty($itemRate['PDSX'])?$itemRate['LRL'].'%':''); $item_title['PCCGL'] = '派出成功率'.(!empty($itemRate['PCCGL'])?$itemRate['LRL'].'%':''); $item_title['GDJSL'] = '跟单及时率'.(!empty($itemRate['GDJSL'])?$itemRate['LRL'].'%':''); $item_title['LRSFS'] = '录入师傅数'.(!empty($itemRate['LRSFS'])?$itemRate['LRL'].'%':'');*/ $this->assignconfig('item_titles',$item_title); } /** * 查看 * * @return string|Json * @throws \think\Exception * @throws DbException */ public function index() { //$this->chart(); //$today = now()->sub('')->format('Y-m-d' ); $month = now()->format('Y-m'); //设置过滤方法 $this->request->filter(['strip_tags', 'trim']); if (false === $this->request->isAjax()) { $this->assignconfig('month',$month); return $this->view->fetch(); } $filter = $this->request->param('filter'); $filter = json_decode($filter,true); if(empty($filter['monthrange'])){ /*$arr = explode(' - ',$filter['daterange']); if(trim($arr[0])){ $filter['start_time'] = trim($arr[0]); } if(trim($arr[1])){ $filter['end_time'] = trim($arr[1]); }*/ $filter['monthrange'] = date('Y-m'); } $this->getMonthTimeRange($filter); $filter['group_id'] = self::GROUP_ID; if(!empty($filter['admin_user'])){ $adminIds = Admin::where('username','like','%'.$filter['admin_user'].'%')->column('id'); $filter['admin_user_ids'] = $adminIds; } $list = $this->chart($filter,false); $result = array("total" => $list->total(), "rows" => $list->items()); return json($result); } public function chartData() { $filter = $this->request->post(); if(!empty($filter['daterange'])){ $arr = explode(' - ',$filter['daterange']); if(trim($arr[0])){ $filter['start_time'] = trim($arr[0]); } if(trim($arr[1])){ $filter['end_time'] = trim($arr[1]).' 23:59:59'; } } $data = $this->chart($filter,true); $newData = [ ['派单员','总业绩','转化率','利润率','变现值'] ]; foreach ($data as $datum){ $newData[] = [ $datum['admin_user'], $datum['performance'], $datum['trans_rate'], $datum['performance_rate'], $datum['cash_value'], ]; } return $newData; } //图表统计 public function chart($filter,$getAll=false): \think\Collection|\think\Paginator|bool|array|string|\PDOStatement { $template = Template::where('group_id',self::GROUP_ID)->with(['kpiitem'])->find(); if(empty($template) || empty($template->kpiitem)){ return []; } $kpiItem = []; foreach ($template->kpiitem as $item){ $kpiItem[$item->attr] = $item->toArray(); } $orderValid = implode(',',$this->model->tabStatus(Order::TAB_VALID)); //"COUNT(CASE WHEN status IN (".$orderValid.") THEN 1 END) AS ing_num", $fields = [ 'dispatch_admin_id', 'worker_num', // 使用 IFNULL 确保结果为 null 时返回 0 "IFNULL(COUNT(CASE WHEN status = 60 THEN 1 END), 0) AS finish_num", //完成数 "IFNULL(COUNT(CASE WHEN status IN (".$orderValid.") THEN 1 END), 0) 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 + material_cost) END), 0) 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", //退款订单数量 "IFNULL(AVG(CASE WHEN status > 10 THEN UNIX_TIMESTAMP(dispatch_time) - UNIX_TIMESTAMP(create_time) END), 0) AS avg_time_diff", //派单时效 // "SUM(CASE WHEN status = 60 THEN (field1 + field2) END) AS performance", ]; $builder = $this->model ->where('status',Order::STATUS_FINISHED) ->field($fields); //->where('dispatch_admin_id','>',0); if(isset($filter['admin_user_ids'])){ $builder->whereIn('dispatch_admin_id',$filter['admin_user_ids']); } if(!empty($filter['start_time']) && !empty($filter['end_time'])){ //$time_by = $filter['time_by'] ??1; /* if($time_by == 1){ //按派单时间 $time_field = 'dispatch_time'; }else{ //按录单时间 $time_field = 'create_time'; }*/ $time_field = 'audit_time'; $builder->whereBetween($time_field,[$filter['start_time'],$filter['end_time']]); } $subsql = $this->_subsql($filter); $builder->join([$subsql => 'a'], 'a.admin_id = dispatch_admin_id', 'LEFT'); //城市 if(!empty($filter['area_id'])){ $builder->where('area_id',$filter['area_id']); } //项目 if(!empty($filter['item_id'])){ $builder->where('item_id',$filter['item_id']); } if($getAll){ $data = $builder->group('dispatch_admin_id')->limit(50)->select(); }else{ $data = $builder->group('dispatch_admin_id')->paginate(); } $newData = []; $max_score = $template->max_score??100; if(!empty($data)){ foreach ($data as &$datum) { // 常规字段计算 $datum->performance_rate = $this->_calc($datum->performance, $datum->total, 4, true); $datum->trans_rate = $this->_calc($datum->finish_num, $datum->count_num, 4, true); $datum->cash_value = $this->_calc($datum->performance, $datum->count_num, 2); $datum->performance_avg = $this->_calc($datum->performance, $datum->finish_num, 2); $datum->total_avg = $this->_calc($datum->total, $datum->finish_num, 2); $datum->avg_time_diff = $this->_calc($datum->avg_time_diff, 3600, 4); // 管理员名称 //派单员数量不多,循环中查 $datum->admin_user = !empty($datum->dispatch_admin_id) ? Admin::where('id', $datum->dispatch_admin_id)->value('nickname') ?? '系统' : '系统'; $datum->id = $datum->dispatch_admin_id; // 初始化 KPI 总分 $kpi_total = 0; // 定义 KPI 计算配置 $kpiMap = [ KpiItem::ATTR_ZHL => ['field' => 'trans_rate', 'score_field' => 'zhl_score', 'target_field' => 'zhl_target_value'], KpiItem::ATTR_LRL => ['field' => 'performance_rate','score_field' => 'lrl_score', 'target_field' => 'lrl_target_value'], KpiItem::ATTR_PDSX => ['field' => 'avg_time_diff', 'score_field' => 'pdsx_score', 'target_field' => 'pdsx_target_value', 'reverse' => true], KpiItem::ATTR_PCCGL => ['field' => 'succ_rate', 'score_field' => 'pccgl_score', 'target_field' => 'pccgl_target_value'], KpiItem::ATTR_LRSFS => ['field' => 'worker_num', 'score_field' => 'lrsfs_score', 'target_field' => 'lrsfs_target_value'], ]; // 单独处理 success rate(派单成功率) $datum->succ_rate = $this->_calc($datum->finish_num, $datum->count_num, 4, true); $datum->worker_num = $datum->worker_num ?? 0; // 批量处理 KPI 项 foreach ($kpiMap as $attr => $conf) { $item = $kpiItem[$attr] ?? null; $datum->{$conf['score_field']} = 0; $datum->{$conf['target_field']} = $item['target_value'] ?? 0; if ($item) { $reverse = $conf['reverse'] ?? false; $value = $datum->{$conf['field']}; $score = $this->_kpi_score($value, $item, $reverse); $datum->{$conf['score_field']} = $score; $kpi_total += $score; } } // KPI 总分和比值 $datum->kpi_total = bcadd($kpi_total, 0, 2); $datum->kpi_value = $this->_calc($kpi_total, $template->score ?: 1, 2); // KPI 奖金 if ($datum->kpi_value <= 0) { $datum->kpi_money = 0; } else { $money = bcmul($datum->performance, $datum->kpi_value, 4); $datum->kpi_money = $money > 0 ? bcdiv($money, 100, 2) : 0; } $newData[] = $datum->toArray(); } } if($getAll){ return $newData; }else{ return $data; } //dump($newData);exit; } /** * @param $a * @param $b * @param int $scale * @return int|string */ private function _calc($a, $b, int $scale=4, $is_percent=false): int|string { $a = $a??0; $b = $b??0; $val = $b > 0 ? bcdiv($a,$b,$scale) : 0; if($is_percent){ return bcmul($val,100,2); } return $val; } private function _kpi_score($num, $item, $fande = false) { $target = $item['target_value'] ?? 1; $target = $target == 0 ? 1 : $target; // 避免除以0 // 计算得分比例 if ($fande) { $diff = bcsub($num, $target, 4); $rate = bcdiv(abs($diff), $target, 4); // 方向判断:小于目标加分,超过目标扣分 $rate = $num > $target ? bcsub(1, $rate, 4) : bcadd(1, $rate, 4); } else { $rate = bcdiv($num, $target, 4); } // 得分 = 比例 × 指标分数 × 权重 $score = bcmul($item['score'], $rate, 4); $weight = isset($item['pivot']['rate']) ? $item['pivot']['rate'] : 100; $score = bcmul($score, bcdiv($weight, 100, 4), 2); return max($score, 0); // 保证非负 } /** * 获取指定年月的开始和结束时间戳(到秒) * @param string $yearMonth 格式为 "YYYY-MM" 的日期字符串 * @return array 包含开始时间戳和结束时间戳的数组 */ function getMonthTimeRange(&$filter) { $yearMonth = $filter['monthrange']; // 解析输入的年月字符串 list($year, $month) = explode('-', $yearMonth); // 获取当月第一天的时间戳(00:00:00) $startTime = strtotime("{$year}-{$month}-01 00:00:00"); // 获取下个月第一天的时间戳 $nextMonth = strtotime("+1 month", $startTime); // 当月最后一天的时间戳(23:59:59) $endTime = $nextMonth - 1; $filter['start_time'] = date('Y-m-d H:i:s',$startTime); $filter['end_time'] = date('Y-m-d H:i:s',$endTime); return $filter; } public function _subsql($filter){ $builder = new \app\admin\model\Worker(); $fields = [ 'admin_id', "count(*) as worker_num", //完成数 ]; $builder->field($fields)->whereBetween('create_time',[$filter['start_time'],$filter['end_time']]); $builder->group('admin_id'); return $builder->buildSql(); } }