allocatr/application/admin/controller/orders/DispatchLogic.php
2025-07-03 23:10:02 +08:00

335 lines
12 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\orders;
use app\admin\model\Item;
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 = Item::all();
$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;
}
}