418 lines
14 KiB
PHP
418 lines
14 KiB
PHP
<?php
|
||
|
||
namespace app\admin\controller\statistics;
|
||
|
||
use app\admin\model\Admin;
|
||
use app\admin\model\kpi\Template;
|
||
use app\admin\model\Kpiorder;
|
||
use app\admin\model\Order;
|
||
use app\common\controller\Backend;
|
||
use think\Db;
|
||
use think\exception\DbException;
|
||
use think\response\Json;
|
||
use app\admin\model\kpi\Item as KpiItem;
|
||
use function Symfony\Component\Clock\now;
|
||
|
||
/**
|
||
* 订单列管理
|
||
*
|
||
* @icon fa fa-circle-o
|
||
*/
|
||
class Kpidispatcher extends Backend
|
||
{
|
||
|
||
/**
|
||
* Kpiorder模型对象
|
||
* @var Order
|
||
*/
|
||
protected $model = null;
|
||
|
||
|
||
const GROUP_ID = 6; //派单员ID
|
||
|
||
public function _initialize()
|
||
{
|
||
parent::_initialize();
|
||
$this->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'];
|
||
}
|
||
|
||
$items = [
|
||
'ZHL' => ['name' => '转化率', 'unit' => '%'],
|
||
'LRL' => ['name' => '利润率'],
|
||
'PDSX' => ['name' => '派单时效'],
|
||
'PCCGL' => ['name' => '派出成功率'],
|
||
'GDJSL' => ['name' => '跟单及时率'],
|
||
'LRSFS' => ['name' => '录入师傅数'],
|
||
];
|
||
|
||
$item_title = [];
|
||
|
||
foreach ($items as $key => $info) {
|
||
$weight = !empty($itemRate[$key]) ? '(权重'.$itemRate[$key].'%)' : '';
|
||
$item_title[$key] = [
|
||
'title' => $info['name'] . $weight,
|
||
'isshow' => !empty($itemRate[$key]),
|
||
];
|
||
if (isset($info['unit'])) {
|
||
$item_title[$key]['unit'] = $info['unit'];
|
||
}
|
||
}
|
||
|
||
/*$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',
|
||
// 完成数
|
||
"COUNT(CASE WHEN status = 60 THEN 1 END) 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 + material_cost) END), 0) AS cost_total",
|
||
// 退款总数
|
||
"IFNULL(SUM(CASE WHEN status = 60 THEN (refund_amount + worker_refund_amount) END), 0) AS refund_total",
|
||
// 退款订单数量
|
||
"COUNT(CASE WHEN refund_amount > 0 OR worker_refund_amount > 0 THEN 1 END) AS refund_count",
|
||
// 派单时效(过滤状态大于10)
|
||
"IFNULL(AVG(CASE WHEN status > 10 THEN UNIX_TIMESTAMP(dispatch_time) - UNIX_TIMESTAMP(create_time) END), 0) AS avg_time_diff",
|
||
];
|
||
|
||
$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();
|
||
}
|
||
|
||
}
|