allocatr/application/admin/controller/statistics/Worker.php
2025-07-22 16:59:29 +08:00

614 lines
22 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\Aftersale;
use app\admin\model\Order;
use app\admin\model\OrderDispatch;
use app\admin\model\OrderReview;
use app\admin\model\WorkerItem;
use app\common\controller\Backend;
use PDOStatement;
use think\Collection;
use think\Exception;
use think\exception\DbException;
use think\Loader;
use think\response\Json;
use function Symfony\Component\Clock\now;
/**
* 师傅列管理
*
* @icon fa fa-circle-o
*/
class Worker extends Backend
{
/**
* Sworker模型对象
* @var \app\admin\model\Worker
*/
protected $model = null;
/**
* @var Order
*/
private Order $OrderModel;
/**
* @var OrderDispatch
*/
private OrderDispatch $DispatchModel;
protected $relationSearch = true;
public function _initialize()
{
parent::_initialize();
$this->model = new \app\admin\model\Worker();
$this->OrderModel = new Order();
$this->DispatchModel = new OrderDispatch();
}
/**
* 默认生成的控制器所继承的父类中有index/add/edit/del/multi五个基础方法、destroy/restore/recyclebin三个回收站方法
* 因此在当前控制器中可不用编写增删改查的代码,除非需要自己控制这部分逻辑
* 需要将application/admin/library/traits/Backend.php中对应的方法复制到当前控制器,然后进行修改
*/
/**
* 查看
*
* @return string|Json
* @throws Exception
* @throws DbException
*/
public function index()
{
$today = now()->format('Y-m-01');
$today_end = now()->format('Y-m-d');
//设置过滤方法
$this->request->filter(['strip_tags', 'trim']);
if (false === $this->request->isAjax()) {
$appkey = config('map.baidu_app_key');
$this->assign('mapkey',$appkey);
$count = \app\admin\model\Worker::count();
$active_count = \app\admin\model\Worker::where('location_update_time', '>=', date('Y-m-d H:i:s', strtotime('-30 days')))
->count();
$add_count_1 = \app\admin\model\Worker::where('create_time', '>=', date('Y-m-d'))
->count();
$active_count_7 = \app\admin\model\Worker::where('create_time', '>=', date('Y-m-d H:i:s', strtotime('-7 days')))
->count();
$this->assign('count',$count);
$this->assign('active_count',$active_count);
$this->assign('add_count_1',$add_count_1);
$this->assign('add_count_7',$active_count_7);
//待接单
//$todo_count = OrderDispatch::where('status',OrderDispatch::STATUS_TOGET)->count();
//进行中
//$ing_count = OrderDispatch::whereIn('status',[OrderDispatch::STATUS_GOTIT,OrderDispatch::STATUS_PLANIT,OrderDispatch::STATUS_CLOCK])->count();
//待验收
//$check_count = OrderDispatch::where('status',Order::STATUS_CHECKING)->count();
//售后待处理
//$aftersale_count = Aftersale::where('status',1)->count();
/* $this->assign('todo_count',$todo_count);
$this->assign('ing_count',$ing_count);
$this->assign('check_count',$check_count);
$this->assign('aftersale_count',$aftersale_count);*/
//师傅坐标
$list = \app\admin\model\Worker::where('lng','>',0)->field(['lng','lat'])->select();
$arr = [];
foreach ($list as $item){
$arr[] = [
$item->lng, $item->lat,
];
}
$this->assign('locationData',json_encode($arr));
$this->assignconfig('default_daterange',now()->format('Y-m-01 00:00:00').' - '.now()->format('Y-m-d 23:59:59'));
return $this->view->fetch();
}
//如果发送的来源是 Selectpage则转发到 Selectpage
if ($this->request->request('keyField')) {
return $this->selectpage();
}
$filter = $this->request->param('filter');
$filter = json_decode($filter,true);
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]);
}
}
if(!empty($filter['item_id']))
{
$item_id = $filter['item_id'];
$filter['worker_ids'] = WorkerItem::where('item_id',$item_id)->column('worker_id');
}
//派单表
$dispatchSubsql = $this->dispatchSubsql($filter);
//订单表
$orderSubsql = $this->oderSubsql($filter);
//评分表
$viewSubsql = $this->viewSubsql($filter);
[$where, $sort, $order, $offset, $limit] = $this->buildparams();
$list = $this->model->alias('fa_worker')
->with(['area2','items'])
->field([
'fa_worker.*',
'IFNULL(a.dispatch_count, 0) AS dispatch_count',
'IFNULL(a.get_js_count, 0) AS get_js_count',
'IFNULL(b.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')
->where($where)
->order($sort, $order)
->paginate($limit);
$data = [];
$worker_ids = [];
foreach ($list->items() as $item){
$dt = $item->toArray();
$tel = $dt['tel'];
if (preg_match('/^\d{7,}$/', $tel)) {
$dt['tel'] = mb_substr($tel, 0, 3, 'UTF-8') . '****' . mb_substr($tel, -4, null, 'UTF-8');
} else {
// 不处理非手机号,比如中文名称
$dt['tel'] = $tel;
}
$data[] = $dt;
$worker_ids [] = $item['id'];
}
$worker_item = WorkerItem::whereIn('worker_id',$worker_ids)->where('item_path_id',1)
->field('worker_id,item_id')->select();
$allItemList = \app\admin\model\Item::where('level',1)->select();
$allItems= [];
foreach ($allItemList as $item){
$allItems[$item->id] = $item->title;
}
$worker_item_map = [];
// dd($worker_item);
foreach ($worker_item as $item){
$worker_item_map[$item->worker_id] [] = $item->item_id;
}
foreach ($list as &$datum){
$worker_item = [];
// dd($worker_item_map);
if (key_exists($datum['id'],$worker_item_map)){
foreach ($worker_item_map[$datum['id']] as $item){
$worker_item[] = $allItems[$item]??'';
}
}
$datum['worker_item'] = implode(',',$worker_item);
}
$this->_toList($list);
$result = ['total' => $list->total(), 'rows' => $list->items()];
return json($result);
}
/**
* @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['end_time']);
}
if(!empty($filter['worker_ids'])){
$viewSubsql->where('worker_id','in',$filter['worker_ids']);
}
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',
// 分配数COUNT(*) 不可能为 NULL
"COUNT(*) AS dispatch_count",
// 接单数,状态为 30 或 60
"COUNT(CASE WHEN status IN (30, 60) THEN 1 END) AS get_js_count",
// 拒绝数,状态为 -10
"COUNT(CASE WHEN status = -10 THEN 1 END) AS refuse_count",
// 上门数,统计非 NULL 的 arrive_time
"COUNT(arrive_time) AS arrive_count",
// 联系时效,平均时间差,保留 IFNULL 防止 NULL
"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['end_time']);
}
if(!empty($filter['worker_ids'])){
$builder->where('worker_id','in',$filter['worker_ids']);
}
$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',
// 接单数,状态 > 10
"COUNT(CASE WHEN status > 10 THEN 1 END) AS get_count",
// 完成数,状态 = 60
"COUNT(CASE WHEN status = 60 THEN 1 END) AS finish_num",
// 成效额,状态 = 60SUM可能为NULL保留IFNULL
"IFNULL(SUM(CASE WHEN status = 60 THEN total END), 0) AS total",
// 业绩,状态 = 60
"IFNULL(SUM(CASE WHEN status = 60 THEN performance END), 0) AS performance",
// 成本,状态 = 60
"IFNULL(SUM(CASE WHEN status = 60 THEN cost END), 0) AS cost",
// 退款总数,状态 = 60
"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",
];
$builder = (new Order())->field($fields);
if(!empty($filter['start_time'])){
$builder->where('audit_time','>=',$filter['start_time']);
}
if(!empty($filter['end_time'])){
$builder->where('audit_time','<=',$filter['end_time']);
}
if(!empty($filter['worker_ids'])){
$builder->where('worker_id','in',$filter['worker_ids']);
}
//->where('dispatch_admin_id','>',0);
return $builder->group('worker_id')->buildSql();
}
/**
* @param $a
* @param $b
* @param int $scale
* @param bool $is_percent
* @return int|string
*/
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;
}
private function _toList(\think\Paginator $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->get_time_rate = $this->_calc($datum->get_js_count,$datum->get_count,4,true);
$datum->avg_time_diff = $this->_calc($datum->avg_time_diff,3600,2);
}
}
/**
* 生成查询所需要的条件,排序方式
* @param mixed $searchfields 快速查询的字段
* @param boolean $relationSearch 是否关联查询
* @return array
*/
protected function buildparams($searchfields = null, $relationSearch = null)
{
$searchfields = is_null($searchfields) ? $this->searchFields : $searchfields;
$relationSearch = is_null($relationSearch) ? $this->relationSearch : $relationSearch;
$search = $this->request->get("search", '');
$filter = $this->request->get("filter", '');
$op = $this->request->get("op", '', 'trim');
$sort = $this->request->get("sort", !empty($this->model) && $this->model->getPk() ? $this->model->getPk() : 'id');
$asortFields = ['dispatch_count','refuse_count','arrive_count'];
$bsortFields = ['finish_num','total','performance','refund_total','refund_count','cost','get_count'];
$csortFields = ['good_count'];
if(!empty($sort) ){
if(in_array($sort,$asortFields)){
$sort = 'a.'.$sort;
}
if(in_array($sort,$bsortFields)){
$sort = 'b.'.$sort;
}
if(in_array($sort,$csortFields)){
$sort = 'c.'.$sort;
}
}
$order = $this->request->get("order", "DESC");
$offset = max(0, $this->request->get("offset/d", 0));
$limit = max(0, $this->request->get("limit/d", 0));
$limit = $limit ?: 999999;
//新增自动计算页码
$page = $limit ? intval($offset / $limit) + 1 : 1;
if ($this->request->has("page")) {
$page = max(0, $this->request->get("page/d", 1));
}
$this->request->get([config('paginate.var_page') => $page]);
$filter = (array)json_decode($filter, true);
unset($filter['daterange']);
$area_id = $filter['area_id']??null;
// dd($area_id);
unset($filter['area_id']);
if(!empty($area_id)){
$filter['fa_worker.area_id'] = $this->getSelectAreaCode($area_id);
}
if(!empty($filter['item_id']))
{
$item_id = $filter['item_id'];
$filter['fa_worker.id'] = WorkerItem::where('item_id',$item_id)->column('worker_id');
unset($filter['item_id']);
}
$op = (array)json_decode($op, true);
$op['fa_worker.area_id'] = 'LIKE%';
$op['fa_worker.id'] = 'IN';
$filter = $filter ? $filter : [];
$where = [];
$alias = [];
$bind = [];
$name = '';
$aliasName = '';
if (!empty($this->model) && $relationSearch) {
$name = $this->model->getTable();
$alias[$name] = Loader::parseName(basename(str_replace('\\', '/', get_class($this->model))));
$aliasName = $alias[$name] . '.';
}
$sortArr = explode(',', $sort);
foreach ($sortArr as $index => & $item) {
$item = stripos($item, ".") === false ? $aliasName . trim($item) : $item;
}
unset($item);
$sort = implode(',', $sortArr);
$adminIds = $this->getDataLimitAdminIds();
if (is_array($adminIds)) {
$where[] = [$aliasName . $this->dataLimitField, 'in', $adminIds];
}
if ($search) {
$searcharr = is_array($searchfields) ? $searchfields : explode(',', $searchfields);
foreach ($searcharr as $k => &$v) {
$v = stripos($v, ".") === false ? $aliasName . $v : $v;
}
unset($v);
$where[] = [implode("|", $searcharr), "LIKE", "%{$search}%"];
}
$index = 0;
foreach ($filter as $k => $v) {
if (!preg_match('/^[a-zA-Z0-9_\-\.]+$/', $k)) {
continue;
}
$sym = $op[$k] ?? '=';
if (stripos($k, ".") === false) {
$k = $aliasName . $k;
}
$v = !is_array($v) ? trim($v) : $v;
$sym = strtoupper($op[$k] ?? $sym);
//null和空字符串特殊处理
if (!is_array($v)) {
if (in_array(strtoupper($v), ['NULL', 'NOT NULL'])) {
$sym = strtoupper($v);
}
if (in_array($v, ['""', "''"])) {
$v = '';
$sym = '=';
}
}
switch ($sym) {
case '=':
case '<>':
$where[] = [$k, $sym, (string)$v];
break;
case 'LIKE':
case 'NOT LIKE':
case 'LIKE %...%':
case 'NOT LIKE %...%':
$where[] = [$k, trim(str_replace('%...%', '', $sym)), "%{$v}%"];
break;
// ✅ 新增右匹配like%
case 'LIKE%':
$where[] = [$k, 'LIKE', "{$v}%"];
break;
// ✅ 新增:左匹配(%like
case '%LIKE':
$where[] = [$k, 'LIKE', "%{$v}"];
break;
case '>':
case '>=':
case '<':
case '<=':
$where[] = [$k, $sym, intval($v)];
break;
case 'FINDIN':
case 'FINDINSET':
case 'FIND_IN_SET':
$v = is_array($v) ? $v : explode(',', str_replace(' ', ',', $v));
$findArr = array_values($v);
foreach ($findArr as $idx => $item) {
$bindName = "item_" . $index . "_" . $idx;
$bind[$bindName] = $item;
$where[] = "FIND_IN_SET(:{$bindName}, `" . str_replace('.', '`.`', $k) . "`)";
}
break;
case 'IN':
case 'IN(...)':
case 'NOT IN':
case 'NOT IN(...)':
$where[] = [$k, str_replace('(...)', '', $sym), is_array($v) ? $v : explode(',', $v)];
break;
case 'BETWEEN':
case 'NOT BETWEEN':
$arr = array_slice(explode(',', $v), 0, 2);
if (stripos($v, ',') === false || !array_filter($arr, function ($v) {
return $v != '' && $v !== false && $v !== null;
})) {
continue 2;
}
//当出现一边为空时改变操作符
if ($arr[0] === '') {
$sym = $sym == 'BETWEEN' ? '<=' : '>';
$arr = $arr[1];
} elseif ($arr[1] === '') {
$sym = $sym == 'BETWEEN' ? '>=' : '<';
$arr = $arr[0];
}
$where[] = [$k, $sym, $arr];
break;
case 'RANGE':
case 'NOT RANGE':
$v = str_replace(' - ', ',', $v);
$arr = array_slice(explode(',', $v), 0, 2);
if (stripos($v, ',') === false || !array_filter($arr)) {
continue 2;
}
//当出现一边为空时改变操作符
if ($arr[0] === '') {
$sym = $sym == 'RANGE' ? '<=' : '>';
$arr = $arr[1];
} elseif ($arr[1] === '') {
$sym = $sym == 'RANGE' ? '>=' : '<';
$arr = $arr[0];
}
$tableArr = explode('.', $k);
if (count($tableArr) > 1 && $tableArr[0] != $name && !in_array($tableArr[0], $alias)
&& !empty($this->model) && $this->relationSearch) {
//修复关联模型下时间无法搜索的BUG
$relation = Loader::parseName($tableArr[0], 1, false);
$alias[$this->model->$relation()->getTable()] = $tableArr[0];
}
$where[] = [$k, str_replace('RANGE', 'BETWEEN', $sym) . ' TIME', $arr];
break;
case 'NULL':
case 'IS NULL':
case 'NOT NULL':
case 'IS NOT NULL':
$where[] = [$k, strtolower(str_replace('IS ', '', $sym))];
break;
default:
break;
}
$index++;
}
if (!empty($this->model)) {
$this->model->alias($alias);
}
$model = $this->model;
$where = function ($query) use ($where, $alias, $bind, &$model) {
if (!empty($model)) {
$model->alias($alias);
$model->bind($bind);
}
foreach ($where as $k => $v) {
if (is_array($v)) {
call_user_func_array([$query, 'where'], $v);
} else {
$query->where($v);
}
}
};
return [$where, $sort, $order, $offset, $limit, $page, $alias, $bind];
}
}