merchantId = $merchantId; $this->loadConfig(); $this->loadMapConfig(); } /** * 加载配置 */ private function loadConfig() { $configModel = M('RiderConfig'); $this->config = $configModel->where(array('merchant_id' => $this->merchantId))->find(); if (!$this->config) { // 使用默认配置 $this->config = array( 'delivery_fee_type' => 1, 'base_fee' => 5.00, 'distance_fee' => 1.50, 'free_distance' => 3, 'max_distance' => 20, 'time_fee_enabled' => 0, 'night_fee_rate' => 0.5, 'night_start_time' => '22:00', 'night_end_time' => '06:00', 'weather_fee_enabled' => 0, 'weather_fee_rate' => 1.0, 'urgent_fee_enabled' => 1, 'urgent_fee' => 3.00, 'min_order_amount' => 20.00, 'packaging_fee' => 2.00 ); } } /** * 加载地图配置与密钥 */ private function loadMapConfig() { // 与 getAddress 同源的配置表 // 包含 server_map_type: 1=百度 2=高德 3=腾讯 // 以及对应的 serverkeyid/gould_key/tencent_key $this->mapConfig = M('sms_config')->find(); if (!is_array($this->mapConfig)) { $this->mapConfig = array( 'server_map_type' => '2', 'serverkeyid' => '', 'gould_key' => '', 'tencent_key' => '' ); } } /** * 计算两点间距离(使用百度地图API或高德地图API) * @param float $lat1 起点纬度 * @param float $lng1 起点经度 * @param float $lat2 终点纬度 * @param float $lng2 终点经度 * @return float 距离(公里) */ public function calculateDistance($lat1, $lng1, $lat2, $lng2) { // 使用Haversine公式计算球面距离 $earthRadius = 6371; // 地球半径(公里) $lat1Rad = deg2rad($lat1); $lng1Rad = deg2rad($lng1); $lat2Rad = deg2rad($lat2); $lng2Rad = deg2rad($lng2); $deltaLat = $lat2Rad - $lat1Rad; $deltaLng = $lng2Rad - $lng1Rad; $a = sin($deltaLat / 2) * sin($deltaLat / 2) + cos($lat1Rad) * cos($lat2Rad) * sin($deltaLng / 2) * sin($deltaLng / 2); $c = 2 * atan2(sqrt($a), sqrt(1 - $a)); $distance = $earthRadius * $c; return round($distance, 2); } /** * 使用地图API计算路线距离(默认骑行距离),自动根据 server_map_type 选择服务商 * 若API失败则回退到直线距离 * @param float $lat1 起点纬度(GCJ02) * @param float $lng1 起点经度(GCJ02) * @param float $lat2 终点纬度(GCJ02) * @param float $lng2 终点经度(GCJ02) * @param string $mode 路线模式,支持 'bicycling' * @return float 距离(公里) */ public function calculateRouteDistance($lat1, $lng1, $lat2, $lng2, $mode = 'bicycling') { try { $type = isset($this->mapConfig['server_map_type']) ? (string)$this->mapConfig['server_map_type'] : '2'; $distanceMeters = 0; switch ($type) { case '1': // 百度(坐标需使用BD09) // 输入为GCJ02,转换为BD09 $o = \Convert_GCJ02_To_BD09($lat1, $lng1); $d = \Convert_GCJ02_To_BD09($lat2, $lng2); $ak = $this->mapConfig['serverkeyid']; if (empty($ak)) { break; } // 百度骑行路线API $url = 'http://api.map.baidu.com/direction/v2/riding?origin=' . $o['lat'] . ',' . $o['lng'] . '&destination=' . $d['lat'] . ',' . $d['lng'] . '&ak=' . $ak; $resp = @file_get_contents($url); if ($resp) { $json = json_decode($resp, true); if (is_array($json) && isset($json['status']) && intval($json['status']) === 0) { // routes[0].distance 单位:米 if (isset($json['result']['routes'][0]['distance'])) { $distanceMeters = floatval($json['result']['routes'][0]['distance']); } } } break; case '2': // 高德(GCJ02,origin/destination 为 lng,lat) $key = $this->mapConfig['gould_key']; if (empty($key)) { break; } // 高德骑行路线 v4 $url = 'https://restapi.amap.com/v4/direction/bicycling?origin=' . $lng1 . ',' . $lat1 . '&destination=' . $lng2 . ',' . $lat2 . '&key=' . $key; $resp = @file_get_contents($url); if ($resp) { $json = json_decode($resp, true); // 兼容可能的返回结构 if (isset($json['errcode']) && intval($json['errcode']) === 0 && isset($json['data']['paths'][0]['distance'])) { $distanceMeters = floatval($json['data']['paths'][0]['distance']); } elseif (isset($json['data']['paths']) && isset($json['data']['paths'][0]['distance'])) { $distanceMeters = floatval($json['data']['paths'][0]['distance']); } elseif (isset($json['paths'][0]['distance'])) { $distanceMeters = floatval($json['paths'][0]['distance']); } } break; case '3': // 腾讯(GCJ02,from/to 为 lat,lng) $key = $this->mapConfig['tencent_key']; if (empty($key)) { break; } $url = 'https://apis.map.qq.com/ws/direction/v1/bicycling/?from=' . $lat1 . ',' . $lng1 . '&to=' . $lat2 . ',' . $lng2 . '&key=' . $key; $resp = @file_get_contents($url); if ($resp) { $json = json_decode($resp, true); if (is_array($json) && isset($json['status']) && intval($json['status']) === 0) { if (isset($json['result']['routes'][0]['distance'])) { $distanceMeters = floatval($json['result']['routes'][0]['distance']); } } } break; default: // 未知类型,走回退 break; } if ($distanceMeters > 0) { return round($distanceMeters / 1000, 3); } } catch (\Exception $e) { // 忽略并回退到直线距离 } // 回退:使用直线距离 return $this->calculateDistance($lat1, $lng1, $lat2, $lng2); } /** * 通过地址计算距离 * @param string $fromAddress 起始地址 * @param string $toAddress 目标地址 * @return array */ public function calculateDistanceByAddress($fromAddress, $toAddress) { // 这里可以调用百度地图或高德地图的地理编码API // 将地址转换为经纬度,然后计算距离 // 示例:模拟API调用 $fromCoords = $this->geocodeAddress($fromAddress); $toCoords = $this->geocodeAddress($toAddress); if (!$fromCoords || !$toCoords) { return array( 'success' => false, 'message' => '地址解析失败', 'distance' => 0 ); } $distance = $this->calculateDistance( $fromCoords['lat'], $fromCoords['lng'], $toCoords['lat'], $toCoords['lng'] ); return array( 'success' => true, 'distance' => $distance, 'from_coords' => $fromCoords, 'to_coords' => $toCoords ); } /** * 地址地理编码(需要接入地图API) * @param string $address * @return array|false */ private function geocodeAddress($address) { // 这里应该调用实际的地图API // 示例返回格式 return array( 'lat' => 39.915119, 'lng' => 116.403963, 'formatted_address' => $address ); } /** * 计算配送费用 * @param array $params 参数数组 * @return array */ public function calculateDeliveryFee($params) { $distance = floatval($params['distance']); $orderAmount = floatval($params['order_amount']); $isUrgent = isset($params['is_urgent']) ? intval($params['is_urgent']) : 0; $isNight = isset($params['is_night']) ? intval($params['is_night']) : $this->isNightTime(); $isBadWeather = isset($params['is_bad_weather']) ? intval($params['is_bad_weather']) : 0; // 验证配送距离 if ($distance > $this->config['max_distance']) { return array( 'success' => false, 'message' => '超出最大配送距离(' . $this->config['max_distance'] . '公里)', 'delivery_fee' => 0 ); } // 验证最小起送金额 if ($orderAmount < $this->config['min_order_amount']) { return array( 'success' => false, 'message' => '未达到最小起送金额(' . $this->config['min_order_amount'] . '元)', 'delivery_fee' => 0 ); } $deliveryFee = 0; $feeDetails = array(); // 基础配送费计算 switch ($this->config['delivery_fee_type']) { case 1: // 按距离计算 if ($distance <= $this->config['free_distance']) { $deliveryFee = $this->config['base_fee']; $feeDetails[] = array( 'type' => 'base_fee', 'name' => '基础费用', 'amount' => $this->config['base_fee'], 'description' => '免费配送距离内' ); } else { $extraDistance = $distance - $this->config['free_distance']; $distanceFee = $extraDistance * $this->config['distance_fee']; $deliveryFee = $this->config['base_fee'] + $distanceFee; $feeDetails[] = array( 'type' => 'base_fee', 'name' => '基础费用', 'amount' => $this->config['base_fee'], 'description' => '基础配送费' ); $feeDetails[] = array( 'type' => 'distance_fee', 'name' => '距离费用', 'amount' => $distanceFee, 'description' => $extraDistance . '公里 × ' . $this->config['distance_fee'] . '元/公里' ); } break; case 2: // 固定费用 $deliveryFee = $this->config['base_fee']; $feeDetails[] = array( 'type' => 'fixed_fee', 'name' => '固定配送费', 'amount' => $this->config['base_fee'], 'description' => '统一配送费用' ); break; case 3: // 阶梯计费 $deliveryFee = $this->calculateTieredFee($distance); $feeDetails[] = array( 'type' => 'tiered_fee', 'name' => '阶梯计费', 'amount' => $deliveryFee, 'description' => '根据距离阶梯计费' ); break; } // 包装费 if ($this->config['packaging_fee'] > 0) { $deliveryFee += $this->config['packaging_fee']; $feeDetails[] = array( 'type' => 'packaging_fee', 'name' => '包装费', 'amount' => $this->config['packaging_fee'], 'description' => '商品包装费用' ); } // 加急费 if ($isUrgent && $this->config['urgent_fee_enabled']) { $deliveryFee += $this->config['urgent_fee']; $feeDetails[] = array( 'type' => 'urgent_fee', 'name' => '加急费', 'amount' => $this->config['urgent_fee'], 'description' => '加急配送费用' ); } // 夜间加价 if ($isNight && $this->config['time_fee_enabled']) { $nightFee = $deliveryFee * $this->config['night_fee_rate']; $deliveryFee += $nightFee; $feeDetails[] = array( 'type' => 'night_fee', 'name' => '夜间加价', 'amount' => $nightFee, 'description' => '夜间配送加价 ' . ($this->config['night_fee_rate'] * 100) . '%' ); } // 恶劣天气加价 if ($isBadWeather && $this->config['weather_fee_enabled']) { $weatherFee = $this->config['weather_fee_rate']; $deliveryFee += $weatherFee; $feeDetails[] = array( 'type' => 'weather_fee', 'name' => '恶劣天气加价', 'amount' => $weatherFee, 'description' => '恶劣天气配送加价' ); } return array( 'success' => true, 'delivery_fee' => round($deliveryFee, 2), 'fee_details' => $feeDetails, 'distance' => $distance, 'order_amount' => $orderAmount, 'config' => array( 'max_distance' => $this->config['max_distance'], 'min_order_amount' => $this->config['min_order_amount'], 'free_distance' => $this->config['free_distance'] ) ); } /** * 阶梯计费计算 * @param float $distance * @return float */ private function calculateTieredFee($distance) { // 阶梯计费规则示例 $tiers = array( array('min' => 0, 'max' => 3, 'fee' => 5.00), array('min' => 3, 'max' => 8, 'fee' => 8.00), array('min' => 8, 'max' => 15, 'fee' => 12.00), array('min' => 15, 'max' => 999, 'fee' => 18.00) ); foreach ($tiers as $tier) { if ($distance >= $tier['min'] && $distance < $tier['max']) { return $tier['fee']; } } return $this->config['base_fee']; } /** * 判断是否为夜间时间 * @return bool */ private function isNightTime() { $currentTime = date('H:i'); $nightStart = $this->config['night_start_time']; $nightEnd = $this->config['night_end_time']; // 处理跨天的情况 if ($nightStart > $nightEnd) { return $currentTime >= $nightStart || $currentTime <= $nightEnd; } else { return $currentTime >= $nightStart && $currentTime <= $nightEnd; } } /** * 获取配送费用预估 * @param string $fromAddress 起始地址 * @param string $toAddress 目标地址 * @param float $orderAmount 订单金额 * @param array $options 其他选项 * @return array */ public function estimateDeliveryFee($fromAddress, $toAddress, $orderAmount, $options = array()) { // 计算距离 $distanceResult = $this->calculateDistanceByAddress($fromAddress, $toAddress); if (!$distanceResult['success']) { return $distanceResult; } // 计算费用 $params = array( 'distance' => $distanceResult['distance'], 'order_amount' => $orderAmount, 'is_urgent' => isset($options['is_urgent']) ? $options['is_urgent'] : 0, 'is_night' => isset($options['is_night']) ? $options['is_night'] : $this->isNightTime(), 'is_bad_weather' => isset($options['is_bad_weather']) ? $options['is_bad_weather'] : 0 ); $feeResult = $this->calculateDeliveryFee($params); if ($feeResult['success']) { $feeResult['distance_info'] = $distanceResult; } return $feeResult; } /** * 获取配置信息 * @return array */ public function getConfig() { return $this->config; } /** * 验证配送可行性 * @param array $params * @return array */ public function validateDelivery($params) { $distance = floatval($params['distance']); $orderAmount = floatval($params['order_amount']); $errors = array(); if ($distance > $this->config['max_distance']) { $errors[] = '配送距离超出限制(最大' . $this->config['max_distance'] . '公里)'; } if ($orderAmount < $this->config['min_order_amount']) { $errors[] = '订单金额未达到起送标准(最低' . $this->config['min_order_amount'] . '元)'; } return array( 'valid' => empty($errors), 'errors' => $errors, 'distance' => $distance, 'order_amount' => $orderAmount ); } }