497 行
18 KiB
PHP
497 行
18 KiB
PHP
<?php
|
||
namespace Retail\Service;
|
||
|
||
/**
|
||
* 配送费用计算服务
|
||
* Class DeliveryFeeService
|
||
* @package Retail\Service
|
||
*/
|
||
class DeliveryFeeService
|
||
{
|
||
private $merchantId;
|
||
private $config;
|
||
private $mapConfig;
|
||
|
||
public function __construct($merchantId)
|
||
{
|
||
$this->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
|
||
);
|
||
}
|
||
} |