334 lines
12 KiB
PHP
334 lines
12 KiB
PHP
<?php
|
||
|
||
namespace app\admin\controller\orders;
|
||
|
||
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\Db;
|
||
use think\exception\DbException;
|
||
use function Symfony\Component\Clock\now;
|
||
|
||
/**
|
||
* 自动派单
|
||
*
|
||
* @icon
|
||
*/
|
||
class DispatchLogic
|
||
{
|
||
|
||
|
||
function getMaxScoreWorker($order){
|
||
$worker_info = Db::query("SELECT id, `name`, lng, lat, distance,star
|
||
FROM (
|
||
SELECT id, `name`, lng, lat,star,
|
||
ST_Distance_Sphere(
|
||
point(lng, lat),
|
||
point(?, ?)
|
||
) AS distance
|
||
FROM fa_worker where deletetime is null and type = 1 and status = 1
|
||
) AS t
|
||
WHERE distance < 40000
|
||
ORDER BY distance;",[$order->lng,$order->lat]);
|
||
|
||
|
||
|
||
$worker_ids = array_column($worker_info,'id');
|
||
|
||
$items = model('item')->getAll();
|
||
$ids = $this->getParentIdsFromArray($order->item_id,$items);
|
||
|
||
|
||
$worker_items_ids = (new WorkerItem())
|
||
->whereIn('item_id', $ids)
|
||
->whereIn('worker_id', $worker_ids)
|
||
->column('worker_id');
|
||
|
||
$out_worker = OrderDispatch::where('order_id',$order->id)
|
||
->where('status',-30)->column('worker_id');
|
||
|
||
$out_worker_ids = array_intersect($worker_ids, $worker_items_ids);
|
||
$out_worker_ids = array_values(array_diff($out_worker_ids, $out_worker));
|
||
// dd($out_worker_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;
|
||
}
|
||
|
||
function getParentIdsFromArray($id, $items) {
|
||
$map = [];
|
||
foreach ($items as $item) {
|
||
$map[$item['id']] = $item['pid'];
|
||
}
|
||
|
||
$result = [];
|
||
while (isset($map[$id]) && $map[$id] != 0) {
|
||
$id = $map[$id];
|
||
$result[] = $id;
|
||
}
|
||
|
||
return $result;
|
||
}
|
||
}
|