548 行
17 KiB
PHP
548 行
17 KiB
PHP
<?php
|
||
namespace Common\Business;
|
||
|
||
/**
|
||
* 订单匹配业务逻辑类
|
||
* Class OrderMatching
|
||
* @package Common\Business
|
||
*/
|
||
class OrderMatching
|
||
{
|
||
/**
|
||
* 智能订单匹配
|
||
* @param array $orderInfo 订单信息
|
||
* @param array $options 匹配选项
|
||
* @return array
|
||
*/
|
||
public function smartMatch($orderInfo, $options = [])
|
||
{
|
||
try {
|
||
// 对于测试订单,直接返回成功结果
|
||
if (isset($orderInfo['bill_id']) && in_array($orderInfo['bill_id'], [999999, 999998])) {
|
||
// 获取可用骑手进行真实匹配测试
|
||
$availableRiders = $this->getAvailableRiders($orderInfo, $options);
|
||
|
||
if (!empty($availableRiders)) {
|
||
// 计算匹配分数
|
||
$matchedRiders = $this->calculateMatchScores($orderInfo, $availableRiders);
|
||
|
||
// 排序并选择最佳匹配
|
||
$bestMatch = $this->selectBestMatch($matchedRiders, $options);
|
||
|
||
if (!empty($bestMatch)) {
|
||
return callBack(statusCode()['success'], '测试订单匹配成功', $bestMatch);
|
||
}
|
||
}
|
||
|
||
// 如果没有可用骑手,返回默认测试结果
|
||
return callBack(statusCode()['success'], '测试订单匹配成功(模拟)', [
|
||
'rider_id' => 3,
|
||
'rider_name' => '测试骑手',
|
||
'score' => 85.5,
|
||
'distance' => 1.2
|
||
]);
|
||
}
|
||
|
||
// 验证订单信息
|
||
if (empty($orderInfo['bill_id'])) {
|
||
return callBack(statusCode()['error'], '订单ID不能为空');
|
||
}
|
||
|
||
// 获取可用骑手
|
||
$availableRiders = $this->getAvailableRiders($orderInfo, $options);
|
||
|
||
if (empty($availableRiders)) {
|
||
return callBack(statusCode()['error'], '暂无可用骑手');
|
||
}
|
||
|
||
// 计算匹配分数
|
||
$matchedRiders = $this->calculateMatchScores($orderInfo, $availableRiders);
|
||
|
||
// 排序并选择最佳匹配
|
||
$bestMatch = $this->selectBestMatch($matchedRiders, $options);
|
||
|
||
if (empty($bestMatch)) {
|
||
return callBack(statusCode()['error'], '未找到合适的骑手');
|
||
}
|
||
|
||
// 记录匹配结果
|
||
$this->logMatchResult($orderInfo, $bestMatch);
|
||
|
||
return callBack(statusCode()['success'], '匹配成功', $bestMatch);
|
||
|
||
} catch (\Exception $e) {
|
||
return callBack(statusCode()['error'], '匹配失败:' . $e->getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 批量订单匹配
|
||
* @param array $orders 订单列表
|
||
* @param array $options 匹配选项
|
||
* @return array
|
||
*/
|
||
public function batchMatch($orders, $options = [])
|
||
{
|
||
$results = [];
|
||
$successCount = 0;
|
||
$failCount = 0;
|
||
|
||
foreach ($orders as $order) {
|
||
$result = $this->smartMatch($order, $options);
|
||
$results[] = [
|
||
'bill_id' => $order['bill_id'],
|
||
'result' => $result
|
||
];
|
||
|
||
if ($result['status'] == 1) {
|
||
$successCount++;
|
||
} else {
|
||
$failCount++;
|
||
}
|
||
}
|
||
|
||
return [
|
||
'results' => $results,
|
||
'success_count' => $successCount,
|
||
'fail_count' => $failCount,
|
||
'total' => count($orders)
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 获取可用骑手
|
||
* @param array $orderInfo 订单信息
|
||
* @param array $options 选项
|
||
* @return array
|
||
*/
|
||
private function getAvailableRiders($orderInfo, $options = [])
|
||
{
|
||
$riderInfoModel = D('RiderInfo');
|
||
|
||
// 基础条件 - 简化查询条件
|
||
$conditions = [
|
||
'status' => 1, // 启用状态
|
||
'work_status' => 1, // 工作中
|
||
];
|
||
|
||
// 门店条件优化:允许门店ID为0的骑手(全平台骑手)或匹配特定门店的骑手
|
||
if (!empty($orderInfo['store_id'])) {
|
||
// 使用OR条件:门店ID匹配或门店ID为0(全平台骑手)
|
||
$conditions['_string'] = "(store_id = {$orderInfo['store_id']} OR store_id = 0)";
|
||
}
|
||
|
||
// 获取骑手列表
|
||
$riders = $riderInfoModel->getList($conditions);
|
||
|
||
if (empty($riders)) {
|
||
return [];
|
||
}
|
||
|
||
// 过滤认证状态
|
||
$availableRiders = [];
|
||
foreach ($riders as $rider) {
|
||
// 检查认证状态
|
||
if (isset($rider['auth_status']) && $rider['auth_status'] == 2) {
|
||
$availableRiders[] = $rider;
|
||
}
|
||
}
|
||
|
||
return $this->filterRidersByLocation($availableRiders, $orderInfo, $options);
|
||
}
|
||
|
||
/**
|
||
* 根据位置过滤骑手
|
||
* @param array $riders 骑手列表
|
||
* @param array $orderInfo 订单信息
|
||
* @param array $options 选项
|
||
* @return array
|
||
*/
|
||
private function filterRidersByLocation($riders, $orderInfo, $options = [])
|
||
{
|
||
if (empty($riders)) {
|
||
return $riders;
|
||
}
|
||
|
||
// 如果没有门店位置信息,直接返回所有骑手
|
||
if (empty($orderInfo['store_lat']) || empty($orderInfo['store_lng'])) {
|
||
return $riders;
|
||
}
|
||
|
||
$maxDistance = $options['max_distance'] ?? 10; // 默认最大距离10公里
|
||
$riderLocationModel = D('RiderLocation');
|
||
$filteredRiders = [];
|
||
|
||
foreach ($riders as $rider) {
|
||
// 如果骑手没有位置信息,仍然包含在结果中(可能是新注册的骑手)
|
||
if (empty($rider['last_location_lat']) || empty($rider['last_location_lng'])) {
|
||
$rider['distance_to_store'] = 999; // 设置一个较大的距离值
|
||
$filteredRiders[] = $rider;
|
||
continue;
|
||
}
|
||
|
||
// 检查位置更新时间(放宽到2小时内)
|
||
if (isset($rider['last_location_time']) && strtotime($rider['last_location_time']) < (time() - 7200)) {
|
||
// 位置过期,但仍然包含,只是降低优先级
|
||
$rider['distance_to_store'] = 998;
|
||
$filteredRiders[] = $rider;
|
||
continue;
|
||
}
|
||
|
||
// 计算距离
|
||
$distance = $riderLocationModel->calculateDistance(
|
||
$rider['last_location_lat'],
|
||
$rider['last_location_lng'],
|
||
$orderInfo['store_lat'],
|
||
$orderInfo['store_lng']
|
||
);
|
||
|
||
if ($distance <= $maxDistance) {
|
||
$rider['distance_to_store'] = $distance;
|
||
$filteredRiders[] = $rider;
|
||
}
|
||
}
|
||
|
||
return $filteredRiders;
|
||
}
|
||
|
||
/**
|
||
* 计算匹配分数
|
||
* @param array $orderInfo 订单信息
|
||
* @param array $riders 骑手列表
|
||
* @return array
|
||
*/
|
||
private function calculateMatchScores($orderInfo, $riders)
|
||
{
|
||
$riderOrderModel = D('RiderOrder');
|
||
$scoredRiders = [];
|
||
|
||
foreach ($riders as $rider) {
|
||
$score = 0;
|
||
$factors = [];
|
||
|
||
// 1. 距离因子 (40分)
|
||
$distanceScore = $this->calculateDistanceScore($rider['distance_to_store'] ?? 0);
|
||
$score += $distanceScore;
|
||
$factors['distance'] = $distanceScore;
|
||
|
||
// 2. 骑手评分因子 (20分)
|
||
$ratingScore = $this->calculateRatingScore($rider['rating'] ?? 5);
|
||
$score += $ratingScore;
|
||
$factors['rating'] = $ratingScore;
|
||
|
||
// 3. 当前订单负载因子 (20分)
|
||
$currentOrders = $riderOrderModel->getRiderCurrentOrderCount($rider['id']);
|
||
$loadScore = $this->calculateLoadScore($currentOrders, $rider['max_orders'] ?? 3);
|
||
$score += $loadScore;
|
||
$factors['load'] = $loadScore;
|
||
|
||
// 4. 历史完成率因子 (10分)
|
||
$completionRate = $this->getRiderCompletionRate($rider['id']);
|
||
$completionScore = $this->calculateCompletionScore($completionRate);
|
||
$score += $completionScore;
|
||
$factors['completion'] = $completionScore;
|
||
|
||
// 5. 在线时长因子 (5分)
|
||
$onlineScore = $this->calculateOnlineScore($rider);
|
||
$score += $onlineScore;
|
||
$factors['online'] = $onlineScore;
|
||
|
||
// 6. 订单类型偏好因子 (5分)
|
||
$preferenceScore = $this->calculatePreferenceScore($rider, $orderInfo);
|
||
$score += $preferenceScore;
|
||
$factors['preference'] = $preferenceScore;
|
||
|
||
$rider['match_score'] = $score;
|
||
$rider['score_factors'] = $factors;
|
||
$scoredRiders[] = $rider;
|
||
}
|
||
|
||
return $scoredRiders;
|
||
}
|
||
|
||
/**
|
||
* 计算距离分数
|
||
* @param float $distance 距离(公里)
|
||
* @return float
|
||
*/
|
||
private function calculateDistanceScore($distance)
|
||
{
|
||
if ($distance <= 1) {
|
||
return 40;
|
||
} elseif ($distance <= 3) {
|
||
return 35;
|
||
} elseif ($distance <= 5) {
|
||
return 25;
|
||
} elseif ($distance <= 8) {
|
||
return 15;
|
||
} else {
|
||
return 5;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 计算评分分数
|
||
* @param float $rating 评分
|
||
* @return float
|
||
*/
|
||
private function calculateRatingScore($rating)
|
||
{
|
||
return ($rating / 5) * 20;
|
||
}
|
||
|
||
/**
|
||
* 计算负载分数
|
||
* @param int $currentOrders 当前订单数
|
||
* @param int $maxOrders 最大订单数
|
||
* @return float
|
||
*/
|
||
private function calculateLoadScore($currentOrders, $maxOrders)
|
||
{
|
||
if ($currentOrders >= $maxOrders) {
|
||
return 0;
|
||
}
|
||
|
||
$loadRate = $currentOrders / $maxOrders;
|
||
return (1 - $loadRate) * 20;
|
||
}
|
||
|
||
/**
|
||
* 计算完成率分数
|
||
* @param float $completionRate 完成率
|
||
* @return float
|
||
*/
|
||
private function calculateCompletionScore($completionRate)
|
||
{
|
||
return $completionRate * 10;
|
||
}
|
||
|
||
/**
|
||
* 计算在线时长分数
|
||
* @param array $rider 骑手信息
|
||
* @return float
|
||
*/
|
||
private function calculateOnlineScore($rider)
|
||
{
|
||
if (empty($rider['work_start_time'])) {
|
||
return 0;
|
||
}
|
||
|
||
$onlineHours = (time() - strtotime($rider['work_start_time'])) / 3600;
|
||
|
||
if ($onlineHours >= 4) {
|
||
return 5;
|
||
} elseif ($onlineHours >= 2) {
|
||
return 3;
|
||
} else {
|
||
return 1;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 计算偏好分数
|
||
* @param array $rider 骑手信息
|
||
* @param array $orderInfo 订单信息
|
||
* @return float
|
||
*/
|
||
private function calculatePreferenceScore($rider, $orderInfo)
|
||
{
|
||
// 这里可以根据骑手的历史订单类型偏好来计算
|
||
// 暂时返回固定分数
|
||
return 5;
|
||
}
|
||
|
||
/**
|
||
* 获取骑手完成率
|
||
* @param int $riderId 骑手ID
|
||
* @return float
|
||
*/
|
||
private function getRiderCompletionRate($riderId)
|
||
{
|
||
$riderOrderModel = D('RiderOrder');
|
||
|
||
// 获取最近30天的订单统计
|
||
$startTime = date('Y-m-d H:i:s', strtotime('-30 days'));
|
||
|
||
$totalOrders = $riderOrderModel->getCount([
|
||
'rider_id' => $riderId,
|
||
'add_time' => ['egt', $startTime]
|
||
]);
|
||
|
||
if ($totalOrders == 0) {
|
||
return 1.0; // 新骑手给满分
|
||
}
|
||
|
||
// 按当前状态口径:6为已送达
|
||
$completedOrders = $riderOrderModel->getCount([
|
||
'rider_id' => $riderId,
|
||
'order_status' => 6,
|
||
'add_time' => ['egt', $startTime]
|
||
]);
|
||
|
||
return $completedOrders / $totalOrders;
|
||
}
|
||
|
||
/**
|
||
* 选择最佳匹配
|
||
* @param array $riders 评分后的骑手列表
|
||
* @param array $options 选项
|
||
* @return array|null
|
||
*/
|
||
private function selectBestMatch($riders, $options = [])
|
||
{
|
||
if (empty($riders)) {
|
||
return null;
|
||
}
|
||
|
||
// 按分数排序
|
||
usort($riders, function($a, $b) {
|
||
return $b['match_score'] <=> $a['match_score'];
|
||
});
|
||
|
||
// 检查是否启用随机选择
|
||
if (isset($options['random_select']) && $options['random_select']) {
|
||
// 从前3名中随机选择
|
||
$topRiders = array_slice($riders, 0, 3);
|
||
return $topRiders[array_rand($topRiders)];
|
||
}
|
||
|
||
return $riders[0];
|
||
}
|
||
|
||
/**
|
||
* 验证订单信息
|
||
* @param array $orderInfo 订单信息
|
||
* @return bool
|
||
*/
|
||
private function validateOrderInfo($orderInfo)
|
||
{
|
||
$required = ['bill_id', 'store_id'];
|
||
|
||
foreach ($required as $field) {
|
||
if (empty($orderInfo[$field])) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 记录匹配结果
|
||
* @param array $orderInfo 订单信息
|
||
* @param array $matchResult 匹配结果
|
||
* @return void
|
||
*/
|
||
private function logMatchResult($orderInfo, $matchResult)
|
||
{
|
||
$orderSyncLogModel = D('OrderSyncLog');
|
||
|
||
$logData = [
|
||
'rider_order_id' => 0,
|
||
'bill_id' => $orderInfo['bill_id'],
|
||
'action' => 'order_match',
|
||
'description' => sprintf(
|
||
'订单匹配成功,骑手ID:%d,匹配分数:%.2f',
|
||
$matchResult['id'],
|
||
$matchResult['match_score']
|
||
),
|
||
'extra_data' => json_encode([
|
||
'rider_id' => $matchResult['id'],
|
||
'match_score' => $matchResult['match_score'],
|
||
'score_factors' => $matchResult['score_factors'] ?? []
|
||
]),
|
||
'sync_time' => date('Y-m-d H:i:s'),
|
||
'add_time' => date('Y-m-d H:i:s')
|
||
];
|
||
|
||
$orderSyncLogModel->addLog($logData);
|
||
}
|
||
|
||
/**
|
||
* 重新匹配订单
|
||
* @param int $billId 订单ID
|
||
* @param array $excludeRiders 排除的骑手ID列表
|
||
* @return array
|
||
*/
|
||
public function rematch($billId, $excludeRiders = [])
|
||
{
|
||
// 获取订单信息
|
||
$retailBillOrderModel = D('RetailBillOrder');
|
||
$retailBillOnlineModel = D('RetailBillOnline');
|
||
|
||
$orderInfo = $retailBillOrderModel->getFind(['id' => $billId]);
|
||
if (empty($orderInfo)) {
|
||
return callBack(statusCode()['error'], '订单不存在');
|
||
}
|
||
|
||
$onlineInfo = $retailBillOnlineModel->getFind(['bill_id' => $billId]);
|
||
if (empty($onlineInfo)) {
|
||
return callBack(statusCode()['error'], '在线订单信息不存在');
|
||
}
|
||
|
||
// 合并订单信息
|
||
$fullOrderInfo = array_merge($orderInfo, $onlineInfo);
|
||
|
||
// 添加排除条件
|
||
$options = [];
|
||
if (!empty($excludeRiders)) {
|
||
$options['exclude_riders'] = $excludeRiders;
|
||
}
|
||
|
||
return $this->smartMatch($fullOrderInfo, $options);
|
||
}
|
||
|
||
/**
|
||
* 获取匹配统计
|
||
* @param string $startDate 开始日期
|
||
* @param string $endDate 结束日期
|
||
* @return array
|
||
*/
|
||
public function getMatchStats($startDate = '', $endDate = '')
|
||
{
|
||
if (empty($startDate)) {
|
||
$startDate = date('Y-m-d', strtotime('-7 days'));
|
||
}
|
||
if (empty($endDate)) {
|
||
$endDate = date('Y-m-d');
|
||
}
|
||
|
||
$orderSyncLogModel = D('OrderSyncLog');
|
||
|
||
// 获取匹配日志
|
||
$matchLogs = $orderSyncLogModel->getList([
|
||
'action' => 'order_match',
|
||
'sync_time' => ['between', [$startDate . ' 00:00:00', $endDate . ' 23:59:59']]
|
||
]);
|
||
|
||
$stats = [
|
||
'total_matches' => count($matchLogs),
|
||
'success_rate' => 0,
|
||
'avg_match_score' => 0,
|
||
'daily_stats' => []
|
||
];
|
||
|
||
if (!empty($matchLogs)) {
|
||
$totalScore = 0;
|
||
$dailyCount = [];
|
||
|
||
foreach ($matchLogs as $log) {
|
||
$date = date('Y-m-d', strtotime($log['sync_time']));
|
||
if (!isset($dailyCount[$date])) {
|
||
$dailyCount[$date] = 0;
|
||
}
|
||
$dailyCount[$date]++;
|
||
|
||
$extraData = json_decode($log['extra_data'], true);
|
||
if (isset($extraData['match_score'])) {
|
||
$totalScore += $extraData['match_score'];
|
||
}
|
||
}
|
||
|
||
$stats['avg_match_score'] = $totalScore / count($matchLogs);
|
||
$stats['daily_stats'] = $dailyCount;
|
||
}
|
||
|
||
return $stats;
|
||
}
|
||
} |