508 行
18 KiB
PHP
508 行
18 KiB
PHP
<?php
|
|
namespace Retail\Controller;
|
|
use Think\Controller;
|
|
|
|
/**
|
|
* 骑手工作状态监控控制器
|
|
*/
|
|
class RiderMonitorController extends Controller {
|
|
|
|
/**
|
|
* 监控首页
|
|
*/
|
|
public function index() {
|
|
$this->display();
|
|
}
|
|
|
|
/**
|
|
* 获取在线骑手列表
|
|
*/
|
|
public function getOnlineRiders() {
|
|
$page = I('page', 1, 'intval');
|
|
$pageSize = I('pageSize', 20, 'intval');
|
|
$keyword = I('keyword', '', 'trim');
|
|
$area_id = I('area_id', 0, 'intval');
|
|
$work_status = I('work_status', '', 'trim');
|
|
|
|
$where = array('u.user_type' => 'rider', 'u.status' => 1);
|
|
|
|
// 关键词搜索
|
|
if (!empty($keyword)) {
|
|
$where['_string'] = "(u.nickname LIKE '%{$keyword}%' OR u.phone LIKE '%{$keyword}%')";
|
|
}
|
|
|
|
// 工作状态筛选
|
|
if ($work_status !== '') {
|
|
$where['rs.work_status'] = $work_status;
|
|
}
|
|
|
|
// 区域筛选
|
|
if ($area_id > 0) {
|
|
$where['ra.area_id'] = $area_id;
|
|
}
|
|
|
|
$model = M('users');
|
|
$total = $model->alias('u')
|
|
->join('ls_rider_status rs ON u.id = rs.rider_id', 'LEFT')
|
|
->join('ls_rider_area ra ON u.id = ra.rider_id', 'LEFT')
|
|
->where($where)
|
|
->count('DISTINCT u.id');
|
|
|
|
$list = $model->alias('u')
|
|
->field('u.*, rs.work_status, rs.current_lat, rs.current_lng, rs.last_update_time, rs.order_count_today')
|
|
->join('ls_rider_status rs ON u.id = rs.rider_id', 'LEFT')
|
|
->join('ls_rider_area ra ON u.id = ra.rider_id', 'LEFT')
|
|
->where($where)
|
|
->group('u.id')
|
|
->order('rs.last_update_time desc')
|
|
->page($page, $pageSize)
|
|
->select();
|
|
|
|
// 格式化数据
|
|
foreach ($list as &$item) {
|
|
$item['work_status_text'] = $this->getWorkStatusText($item['work_status']);
|
|
$item['last_update_time_format'] = $item['last_update_time'] ? date('Y-m-d H:i:s', $item['last_update_time']) : '';
|
|
|
|
// 计算在线时长
|
|
if ($item['work_status'] == 'online') {
|
|
$online_duration = time() - strtotime(date('Y-m-d 00:00:00'));
|
|
$item['online_duration'] = $this->formatDuration($online_duration);
|
|
} else {
|
|
$item['online_duration'] = '0分钟';
|
|
}
|
|
|
|
// 获取当前订单信息
|
|
$current_order = M('order')->where(array(
|
|
'rider_id' => $item['id'],
|
|
'status' => array('in', array('accepted', 'picked_up', 'delivering'))
|
|
))->find();
|
|
$item['current_order'] = $current_order;
|
|
|
|
// 获取配送区域
|
|
$areas = M('rider_area')->alias('ra')
|
|
->field('da.area_name')
|
|
->join('ls_delivery_area da ON ra.area_id = da.id')
|
|
->where(array('ra.rider_id' => $item['id']))
|
|
->select();
|
|
$item['areas'] = $areas;
|
|
}
|
|
|
|
$this->ajaxReturn(array(
|
|
'code' => 0,
|
|
'msg' => 'success',
|
|
'data' => array(
|
|
'list' => $list,
|
|
'total' => $total
|
|
)
|
|
));
|
|
}
|
|
|
|
/**
|
|
* 获取骑手实时位置
|
|
*/
|
|
public function getRiderLocation() {
|
|
$rider_id = I('rider_id', 0, 'intval');
|
|
if (empty($rider_id)) {
|
|
$this->ajaxReturn(array('code' => 1, 'msg' => '参数错误'));
|
|
}
|
|
|
|
$location = M('rider_status')->where(array('rider_id' => $rider_id))->find();
|
|
if (empty($location)) {
|
|
$this->ajaxReturn(array('code' => 1, 'msg' => '未找到位置信息'));
|
|
}
|
|
|
|
// 获取骑手信息
|
|
$rider = M('users')->where(array('id' => $rider_id))->find();
|
|
|
|
$data = array(
|
|
'rider_id' => $rider_id,
|
|
'nickname' => $rider['nickname'],
|
|
'phone' => $rider['phone'],
|
|
'current_lat' => floatval($location['current_lat']),
|
|
'current_lng' => floatval($location['current_lng']),
|
|
'work_status' => $location['work_status'],
|
|
'work_status_text' => $this->getWorkStatusText($location['work_status']),
|
|
'last_update_time' => date('Y-m-d H:i:s', $location['last_update_time'])
|
|
);
|
|
|
|
// 获取最近的位置轨迹
|
|
$tracks = M('rider_location_track')->where(array(
|
|
'rider_id' => $rider_id,
|
|
'create_time' => array('egt', time() - 3600) // 最近1小时
|
|
))->order('create_time desc')->limit(50)->select();
|
|
|
|
$track_points = array();
|
|
foreach ($tracks as $track) {
|
|
$track_points[] = array(
|
|
'lat' => floatval($track['latitude']),
|
|
'lng' => floatval($track['longitude']),
|
|
'time' => date('H:i:s', $track['create_time'])
|
|
);
|
|
}
|
|
|
|
$data['track_points'] = $track_points;
|
|
|
|
$this->ajaxReturn(array(
|
|
'code' => 0,
|
|
'msg' => 'success',
|
|
'data' => $data
|
|
));
|
|
}
|
|
|
|
/**
|
|
* 获取骑手工作统计
|
|
*/
|
|
public function getRiderWorkStats() {
|
|
$rider_id = I('rider_id', 0, 'intval');
|
|
$date = I('date', date('Y-m-d'), 'trim');
|
|
|
|
if (empty($rider_id)) {
|
|
$this->ajaxReturn(array('code' => 1, 'msg' => '参数错误'));
|
|
}
|
|
|
|
$start_time = strtotime($date . ' 00:00:00');
|
|
$end_time = strtotime($date . ' 23:59:59');
|
|
|
|
// 基础统计
|
|
$stats = array();
|
|
|
|
// 订单统计
|
|
$order_where = array(
|
|
'rider_id' => $rider_id,
|
|
'create_time' => array('between', array($start_time, $end_time))
|
|
);
|
|
|
|
$stats['order_count'] = M('order')->where($order_where)->count();
|
|
$stats['completed_count'] = M('order')->where(array_merge($order_where, array('status' => 'completed')))->count();
|
|
$stats['cancelled_count'] = M('order')->where(array_merge($order_where, array('status' => 'cancelled')))->count();
|
|
|
|
// 收入统计
|
|
$stats['total_income'] = M('order')->where(array_merge($order_where, array('status' => 'completed')))->sum('delivery_fee') ?: 0;
|
|
$stats['total_income_format'] = number_format($stats['total_income'], 2);
|
|
|
|
// 在线时长统计
|
|
$online_logs = M('rider_online_log')->where(array(
|
|
'rider_id' => $rider_id,
|
|
'date' => $date
|
|
))->find();
|
|
|
|
$stats['online_duration'] = $online_logs ? $online_logs['online_duration'] : 0;
|
|
$stats['online_duration_format'] = $this->formatDuration($stats['online_duration']);
|
|
|
|
// 平均配送时间
|
|
$avg_delivery_time = M('order')->where(array_merge($order_where, array('status' => 'completed')))
|
|
->avg('delivery_duration');
|
|
$stats['avg_delivery_time'] = $avg_delivery_time ? round($avg_delivery_time / 60, 1) : 0;
|
|
|
|
// 准时率
|
|
$timeout_count = M('order')->where(array_merge($order_where, array(
|
|
'status' => 'completed',
|
|
'is_timeout' => 1
|
|
)))->count();
|
|
|
|
$stats['punctuality_rate'] = $stats['completed_count'] > 0 ?
|
|
round((($stats['completed_count'] - $timeout_count) / $stats['completed_count']) * 100, 2) : 0;
|
|
|
|
$this->ajaxReturn(array(
|
|
'code' => 0,
|
|
'msg' => 'success',
|
|
'data' => $stats
|
|
));
|
|
}
|
|
|
|
/**
|
|
* 获取实时监控数据
|
|
*/
|
|
public function getRealtimeData() {
|
|
$area_id = I('area_id', 0, 'intval');
|
|
|
|
$where = array('u.user_type' => 'rider', 'u.status' => 1);
|
|
if ($area_id > 0) {
|
|
$where['ra.area_id'] = $area_id;
|
|
}
|
|
|
|
// 在线骑手统计
|
|
$online_count = M('users')->alias('u')
|
|
->join('ls_rider_status rs ON u.id = rs.rider_id', 'LEFT')
|
|
->join('ls_rider_area ra ON u.id = ra.rider_id', 'LEFT')
|
|
->where(array_merge($where, array('rs.work_status' => 'online')))
|
|
->count('DISTINCT u.id');
|
|
|
|
// 忙碌骑手统计
|
|
$busy_count = M('users')->alias('u')
|
|
->join('ls_rider_status rs ON u.id = rs.rider_id', 'LEFT')
|
|
->join('ls_rider_area ra ON u.id = ra.rider_id', 'LEFT')
|
|
->where(array_merge($where, array('rs.work_status' => 'busy')))
|
|
->count('DISTINCT u.id');
|
|
|
|
// 离线骑手统计
|
|
$offline_count = M('users')->alias('u')
|
|
->join('ls_rider_status rs ON u.id = rs.rider_id', 'LEFT')
|
|
->join('ls_rider_area ra ON u.id = ra.rider_id', 'LEFT')
|
|
->where(array_merge($where, array('rs.work_status' => array('in', array('offline', '')))))
|
|
->count('DISTINCT u.id');
|
|
|
|
// 今日订单统计
|
|
$today_start = strtotime(date('Y-m-d 00:00:00'));
|
|
$today_end = strtotime(date('Y-m-d 23:59:59'));
|
|
|
|
$order_where = array('create_time' => array('between', array($today_start, $today_end)));
|
|
if ($area_id > 0) {
|
|
$order_where['delivery_area_id'] = $area_id;
|
|
}
|
|
|
|
$today_orders = M('order')->where($order_where)->count();
|
|
$completed_orders = M('order')->where(array_merge($order_where, array('status' => 'completed')))->count();
|
|
$pending_orders = M('order')->where(array_merge($order_where, array('status' => 'pending')))->count();
|
|
|
|
$data = array(
|
|
'rider_stats' => array(
|
|
'online_count' => $online_count,
|
|
'busy_count' => $busy_count,
|
|
'offline_count' => $offline_count,
|
|
'total_count' => $online_count + $busy_count + $offline_count
|
|
),
|
|
'order_stats' => array(
|
|
'today_orders' => $today_orders,
|
|
'completed_orders' => $completed_orders,
|
|
'pending_orders' => $pending_orders,
|
|
'completion_rate' => $today_orders > 0 ? round(($completed_orders / $today_orders) * 100, 2) : 0
|
|
)
|
|
);
|
|
|
|
$this->ajaxReturn(array(
|
|
'code' => 0,
|
|
'msg' => 'success',
|
|
'data' => $data
|
|
));
|
|
}
|
|
|
|
/**
|
|
* 获取骑手分布地图数据
|
|
*/
|
|
public function getRiderMapData() {
|
|
$area_id = I('area_id', 0, 'intval');
|
|
|
|
$where = array(
|
|
'u.user_type' => 'rider',
|
|
'u.status' => 1,
|
|
'rs.work_status' => array('in', array('online', 'busy'))
|
|
);
|
|
|
|
if ($area_id > 0) {
|
|
$where['ra.area_id'] = $area_id;
|
|
}
|
|
|
|
$riders = M('users')->alias('u')
|
|
->field('u.id, u.nickname, u.phone, rs.current_lat, rs.current_lng, rs.work_status')
|
|
->join('ls_rider_status rs ON u.id = rs.rider_id')
|
|
->join('ls_rider_area ra ON u.id = ra.rider_id', 'LEFT')
|
|
->where($where)
|
|
->group('u.id')
|
|
->select();
|
|
|
|
$map_data = array();
|
|
foreach ($riders as $rider) {
|
|
if (empty($rider['current_lat']) || empty($rider['current_lng'])) {
|
|
continue;
|
|
}
|
|
|
|
$map_data[] = array(
|
|
'rider_id' => $rider['id'],
|
|
'nickname' => $rider['nickname'],
|
|
'phone' => $rider['phone'],
|
|
'lat' => floatval($rider['current_lat']),
|
|
'lng' => floatval($rider['current_lng']),
|
|
'work_status' => $rider['work_status'],
|
|
'work_status_text' => $this->getWorkStatusText($rider['work_status'])
|
|
);
|
|
}
|
|
|
|
$this->ajaxReturn(array(
|
|
'code' => 0,
|
|
'msg' => 'success',
|
|
'data' => $map_data
|
|
));
|
|
}
|
|
|
|
/**
|
|
* 强制下线骑手
|
|
*/
|
|
public function forceOffline() {
|
|
$rider_id = I('rider_id', 0, 'intval');
|
|
$reason = I('reason', '', 'trim');
|
|
|
|
if (empty($rider_id)) {
|
|
$this->ajaxReturn(array('code' => 1, 'msg' => '参数错误'));
|
|
}
|
|
|
|
if (empty($reason)) {
|
|
$this->ajaxReturn(array('code' => 1, 'msg' => '请填写下线原因'));
|
|
}
|
|
|
|
// 检查骑手是否有进行中的订单
|
|
$active_order = M('order')->where(array(
|
|
'rider_id' => $rider_id,
|
|
'status' => array('in', array('accepted', 'picked_up', 'delivering'))
|
|
))->find();
|
|
|
|
if ($active_order) {
|
|
$this->ajaxReturn(array('code' => 1, 'msg' => '该骑手有进行中的订单,无法强制下线'));
|
|
}
|
|
|
|
// 更新骑手状态
|
|
$result = M('rider_status')->where(array('rider_id' => $rider_id))->save(array(
|
|
'work_status' => 'offline',
|
|
'last_update_time' => time()
|
|
));
|
|
|
|
if ($result !== false) {
|
|
// 记录操作日志
|
|
M('rider_operation_log')->add(array(
|
|
'rider_id' => $rider_id,
|
|
'operator_id' => session('admin_id'),
|
|
'action' => 'force_offline',
|
|
'reason' => $reason,
|
|
'create_time' => time()
|
|
));
|
|
|
|
$this->ajaxReturn(array('code' => 0, 'msg' => '操作成功'));
|
|
} else {
|
|
$this->ajaxReturn(array('code' => 1, 'msg' => '操作失败'));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 发送通知给骑手
|
|
*/
|
|
public function sendNotification() {
|
|
$rider_id = I('rider_id', 0, 'intval');
|
|
$message = I('message', '', 'trim');
|
|
|
|
if (empty($rider_id)) {
|
|
$this->ajaxReturn(array('code' => 1, 'msg' => '参数错误'));
|
|
}
|
|
|
|
if (empty($message)) {
|
|
$this->ajaxReturn(array('code' => 1, 'msg' => '请输入通知内容'));
|
|
}
|
|
|
|
// 这里可以集成推送服务
|
|
// 示例:调用推送接口发送通知
|
|
|
|
// 记录通知日志
|
|
M('rider_notification_log')->add(array(
|
|
'rider_id' => $rider_id,
|
|
'sender_id' => session('admin_id'),
|
|
'message' => $message,
|
|
'create_time' => time()
|
|
));
|
|
|
|
$this->ajaxReturn(array('code' => 0, 'msg' => '通知发送成功'));
|
|
}
|
|
|
|
/**
|
|
* 导出监控数据
|
|
*/
|
|
public function exportMonitorData() {
|
|
$date = I('date', date('Y-m-d'), 'trim');
|
|
$area_id = I('area_id', 0, 'intval');
|
|
|
|
$start_time = strtotime($date . ' 00:00:00');
|
|
$end_time = strtotime($date . ' 23:59:59');
|
|
|
|
$where = array('u.user_type' => 'rider', 'u.status' => 1);
|
|
if ($area_id > 0) {
|
|
$where['ra.area_id'] = $area_id;
|
|
}
|
|
|
|
$list = M('users')->alias('u')
|
|
->field('u.*, rs.work_status, rs.last_update_time, rs.order_count_today')
|
|
->join('ls_rider_status rs ON u.id = rs.rider_id', 'LEFT')
|
|
->join('ls_rider_area ra ON u.id = ra.rider_id', 'LEFT')
|
|
->where($where)
|
|
->group('u.id')
|
|
->select();
|
|
|
|
// 导出Excel
|
|
vendor('PHPExcel.PHPExcel');
|
|
$objPHPExcel = new \PHPExcel();
|
|
$objPHPExcel->setActiveSheetIndex(0);
|
|
$objActSheet = $objPHPExcel->getActiveSheet();
|
|
|
|
// 设置表头
|
|
$objActSheet->setCellValue('A1', '骑手ID');
|
|
$objActSheet->setCellValue('B1', '姓名');
|
|
$objActSheet->setCellValue('C1', '手机号');
|
|
$objActSheet->setCellValue('D1', '工作状态');
|
|
$objActSheet->setCellValue('E1', '今日订单数');
|
|
$objActSheet->setCellValue('F1', '今日收入');
|
|
$objActSheet->setCellValue('G1', '在线时长');
|
|
$objActSheet->setCellValue('H1', '最后更新时间');
|
|
|
|
// 填充数据
|
|
$row = 2;
|
|
foreach ($list as $item) {
|
|
// 计算今日收入
|
|
$today_income = M('order')->where(array(
|
|
'rider_id' => $item['id'],
|
|
'status' => 'completed',
|
|
'create_time' => array('between', array($start_time, $end_time))
|
|
))->sum('delivery_fee') ?: 0;
|
|
|
|
// 获取在线时长
|
|
$online_log = M('rider_online_log')->where(array(
|
|
'rider_id' => $item['id'],
|
|
'date' => $date
|
|
))->find();
|
|
$online_duration = $online_log ? $this->formatDuration($online_log['online_duration']) : '0分钟';
|
|
|
|
$objActSheet->setCellValue('A' . $row, $item['id']);
|
|
$objActSheet->setCellValue('B' . $row, $item['nickname']);
|
|
$objActSheet->setCellValue('C' . $row, $item['phone']);
|
|
$objActSheet->setCellValue('D' . $row, $this->getWorkStatusText($item['work_status']));
|
|
$objActSheet->setCellValue('E' . $row, $item['order_count_today'] ?: 0);
|
|
$objActSheet->setCellValue('F' . $row, number_format($today_income, 2));
|
|
$objActSheet->setCellValue('G' . $row, $online_duration);
|
|
$objActSheet->setCellValue('H' . $row, $item['last_update_time'] ? date('Y-m-d H:i:s', $item['last_update_time']) : '');
|
|
$row++;
|
|
}
|
|
|
|
$filename = '骑手监控数据_' . $date . '_' . date('His') . '.xlsx';
|
|
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
|
header('Content-Disposition: attachment;filename="' . $filename . '"');
|
|
header('Cache-Control: max-age=0');
|
|
|
|
$objWriter = \PHPExcel_IOFactory::createWriter($objPHPExcel, 'Excel2007');
|
|
$objWriter->save('php://output');
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* 格式化时长
|
|
*/
|
|
private function formatDuration($seconds) {
|
|
$hours = floor($seconds / 3600);
|
|
$minutes = floor(($seconds % 3600) / 60);
|
|
|
|
if ($hours > 0) {
|
|
return $hours . '小时' . $minutes . '分钟';
|
|
} else {
|
|
return $minutes . '分钟';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取工作状态文本
|
|
*/
|
|
private function getWorkStatusText($status) {
|
|
$statusMap = array(
|
|
'online' => '在线空闲',
|
|
'busy' => '配送中',
|
|
'offline' => '离线',
|
|
'rest' => '休息中'
|
|
);
|
|
return isset($statusMap[$status]) ? $statusMap[$status] : '未知';
|
|
}
|
|
}
|
|
?>
|