allocatr/application/admin/controller/statistics/Kpidispatcher.php
2025-07-22 16:58:05 +08:00

418 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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();
}
}