allocatr/application/admin/controller/statistics/Worker.php
2025-07-03 12:27:12 +08:00

610 lines
23 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(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')
->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',
// 使用 IFNULL 确保结果为 null 时返回 0
"IFNULL(COUNT(*), 0) AS dispatch_count", //分配数
"IFNULL(COUNT(CASE WHEN status NOT IN (0,-30, -10) THEN 1 END), 0) AS get_count", //接单数
"IFNULL(COUNT(CASE WHEN status IN (30, 60) THEN 1 END), 0) AS get_js_count", //接单数
//"COUNT(CASE WHEN status IN (60) THEN 1 END) AS finish_count", //完成数
"IFNULL(COUNT(CASE WHEN status 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['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',
// 使用 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('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','get_count','refuse_count','arrive_count'];
$bsortFields = ['finish_num','total','performance','refund_total','refund_count','cost'];
$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];
}
}