$this->clientId, "accessToken" => $this->accessToken, "timestamp" => (int) (microtime(true) * 1000), ]; $sign = SignHelper::makeSign($payload, $this->appSecret, 'md5', true, 'shansong'); $payload['sign'] = $sign; $headers = ["Content-Type" => "application/x-www-form-urlencoded;charset=utf-8"]; $resp = HttpClient::post("{$this->baseUrl}/openapi/developer/v5/openCitiesLists", $payload, $headers); $newCitiesArr = []; $citiesArr = $resp['data']; foreach($citiesArr as $block) { foreach ($block['cities'] as $city) { $newCitiesArr[$city['name']] = $city; } } // 把newCitiesArr保存到 platform/huolala/cities.php 文件中 $citiesFile = Yii::getAlias('@common/components/delivery/platform/shansong/cities.php'); // 如果文件不存在,则创建文件 if (!file_exists($citiesFile)) { file_put_contents($citiesFile, ''); } // 文件内容要包含命名空间 $content = ''; file_put_contents($citiesFile, $content); return $resp; } /** * 转化订单数据 * @param $order * @param $orderType * @param $shop * @param $params * @return array */ public function formatOrderData($order, $orderType, $shop, $params) { $orderData = ['issOrderNo' => $params['issOrderNo']]; return $orderData; } // 提交订单 public function createOrder($data) { $payload = [ "clientId" => $this->clientId, "accessToken" => $this->accessToken, "timestamp" => (int) (microtime(true) * 1000), "data" => json_encode($data, JSON_UNESCAPED_UNICODE), ]; $sign = SignHelper::makeSign($payload, $this->appSecret, 'md5', true, 'shansong'); $payload['sign'] = $sign; $headers = ['Content-Type' => 'application/x-www-form-urlencoded;charset=utf-8']; $resp = HttpClient::post("{$this->baseUrl}/openapi/developer/v5/orderPlace", $payload, $headers); // 处理响应 if (isset($resp['status']) && $resp['status'] == 200 && isset($resp['data'])) { $respData = $resp['data']; return [ 'code' => 0, 'platform' => 'shansong', 'data' => [ 'orderId' => $respData['orderNumber'], 'fee' => $respData['totalAmount'], 'distance' => $respData['totalDistance'], ] ]; } Yii::error('status=' . $resp['status'] . ', msg=' . $resp['msg']); return [ 'code' => -1, 'platform' => 'shansong', 'msg' => $resp['msg'] ?? 'Unknown error', 'data' => [] ]; } /** * 构建订单计费请求信息(供并发请求使用) * @param array $data 订单数据 * @param string $pickupTime 取件时间 * @return array 包含 url, data, headers, timeout 的请求信息 */ public function buildPriceRequest($data, $pickupTime) { // 预约类型 $timeDiff = strtotime($pickupTime) - time(); $appointType = 0; //0立即单 if ($timeDiff > 3600 ) { $appointType = 1; //1预约单 } // 构建发件人信息 $sd = $data['sender'];// $senderData $sender = [ 'fromAddress' => $sd['fromAddress'], 'fromAddressDetail' => $sd['fromAddressDetail'], 'fromSenderName' => $sd['fromSenderName'], 'fromMobile' => $sd['fromMobile'], 'fromLatitude' => $sd['fromLatitude'], 'fromLongitude' => $sd['fromLongitude'], ]; // 构建收件人信息列表 $receiverList = []; foreach ($data['receiverList'] as $receiver) { $receiverList[] = [ 'orderNo' => $receiver['orderNo'], 'toAddress' => $receiver['toAddress'], 'toAddressDetail' => $receiver['toAddressDetail'], 'toReceiverName' => $receiver['toReceiverName'], 'toMobile' => $receiver['toMobile'], 'toLatitude' => $receiver['toLatitude'], 'toLongitude' => $receiver['toLongitude'], 'goodType' => $receiver['goodType'], // 物品类型,默认10-其他 'weight' => (int) ($receiver['weight'] ?? 1), // 物品重量(必须是整数kg) 'remarks' => $receiver['remarks'] ?? '', // 备注 //'orderingSourceType' => 5, // 订单来源枚举: 1.闪送 5.其他平台 //'orderingSourceNo' => $data['sendNum'], // 对应orderingSourceType流水号,传参数时必须和orderingSourceType成对出现 'expectStartTime' => isset($receiver['expectStartTime']) ? (int) $receiver['expectStartTime'] : null, 'expectEndTime' => isset($receiver['expectEndTime']) ? (int) $receiver['expectEndTime'] : null, ]; // 移除null值的期望时间字段 if ($receiverList[count($receiverList) - 1]['expectStartTime'] === null) { unset($receiverList[count($receiverList) - 1]['expectStartTime']); } if ($receiverList[count($receiverList) - 1]['expectEndTime'] === null) { unset($receiverList[count($receiverList) - 1]['expectEndTime']); } } // 构建请求体 $requestData = [ 'cityName' => $data['cityName'], 'appointType' => $appointType, 'sender' => $sender, 'receiverList' => $receiverList, //'storeId' => isset($data['storeId']) ? (int) $data['storeId'] : $this->storeId, // shansong 报价返回错误: 店铺Id错误,请更新所在平台的sotreId或thirdStoreId~ 'travelWay' => (int) ($data['travelWay'] ?? 0), 'deliveryType' => (int) ($data['deliveryType'] ?? 1), ]; // 如果有预约日期,添加到请求体 if ($appointType == 1) { // yyyy-MM-dd HH:mm格式(例如:2020-02-02 22:00),指的是预约取件时间,只支持一个小时以后两天以内 $requestData['appointmentDate'] = $pickupTime; } $requestData = $this->filterEmptyValues($requestData); $requestData = json_encode($requestData, JSON_UNESCAPED_UNICODE); // 构建签名用的完整payload $payload = [ 'clientId' => $this->clientId, 'accessToken' => $this->accessToken, 'timestamp' => (int) (microtime(true) * 1000), 'data' => $requestData, ]; // 计算签名(SignHelper 会自动递归过滤空值) $sign = SignHelper::makeSign($payload, $this->appSecret, 'md5', true, 'shansong'); $payload['sign'] = $sign; return [ 'url' => "{$this->baseUrl}/openapi/developer/v5/orderCalculate", 'data' => $payload, 'headers' => ["Content-Type" => "application/x-www-form-urlencoded;charset=utf-8"], 'timeout' => 10.0, ]; } /** * 处理闪送报价响应 * * @param array $resp API 响应 * @return array|null 解析后的报价结果 */ public function processPriceResponse($resp) { // 处理响应 if (isset($resp['status']) && $resp['status'] == 200 && isset($resp['data'])) { $respData = $resp['data']; return [ 'platform' => 'shansong', 'order_number' => $respData['orderNumber'] ?? '', // 闪送订单号(有效期30分钟) 'total_distance' => $respData['totalDistance'] ?? 0, // 总距离,单位:米 'total_weight' => $respData['totalWeight'] ?? 0, // 总重量,单位:kg 'total_amount' => $respData['totalAmount'] ?? 0, // 订单总金额(分) 'coupon_save_fee' => $respData['couponSaveFee'] ?? 0, // 优惠额度(分) 'total_fee_after_save' => $respData['totalFeeAfterSave'] ?? 0, // 实际支付费用(分) 'fee_info_list' => $respData['feeInfoList'] ?? [], // 费用明细 'interest_dto_list' => $respData['interestDtoList'] ?? [], // 增值服务明细 // 弃用字段 'estimate_grab_second' => $respData['estimateGrabSecond'] ?? -1, // 预计接单时长,字段弃用 'estimate_receive_second' => $respData['estimateReceiveSecond'] ?? -1, // 预计完单时长,字段弃用 ]; } Yii::error('shansong 报价失败:'.json_encode($resp, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); return [ 'platform' => 'shansong', 'status' => $resp['status'], 'error' => $resp['msg'], ]; } /** * @deprecated * * 下单询价 - 订单计费接口 * 根据闪送官方文档 /openapi/developer/v5/orderCalculate * * @param $data * @param $orderTime * @return array|null * @deprecated 使用 buildPriceRequest 和 processPriceResponse 替代 */ public function getPrice($data, $orderTime) { $request = $this->buildPriceRequest($data, $orderTime); $headers = ['Content-Type' => 'application/x-www-form-urlencoded;charset=utf-8']; $resp = HttpClient::post($request['url'], $request['data'], $headers); return $this->processPriceResponse($resp); } /** * 查询订单详情 * @param $orderId * @param $thirdOrderNo * @return array|null */ public function selectOrder($orderId, $thirdOrderNo) { // 构建请求体 $requestData = [ 'issOrderNo' => $orderId, 'thirdOrderNo' => $thirdOrderNo, ]; $requestData = json_encode($requestData, JSON_UNESCAPED_UNICODE); // 构建签名用的完整payload $payload = [ 'clientId' => $this->clientId, 'accessToken' => $this->accessToken, 'timestamp' => (int) (microtime(true) * 1000), 'data' => $requestData, ]; // 计算签名(SignHelper 会自动递归过滤空值) $sign = SignHelper::makeSign($payload, $this->appSecret, 'md5', true, 'shansong'); $payload['sign'] = $sign; $headers = ['Content-Type' => 'application/x-www-form-urlencoded;charset=utf-8']; // 发送请求到计费接口 $resp = HttpClient::post("{$this->baseUrl}/openapi/developer/v5/orderInfo", $payload, $headers); // 处理响应 if (isset($resp['status']) && $resp['status'] == 200 && isset($resp['data'])) { $respData = $resp['data']; return [ 'code' => 0, 'data' => $respData ]; } return [ 'code' => (int)($resp['200'] ?? -1), 'msg' => $resp['msg'] ?? 'Unknown error', 'data' => null ]; } public function cancelOrder($orderId) { // 构建请求体 $requestData = [ 'issOrderNo' => $orderId ]; //'deductFlag' => false, $requestData = json_encode($requestData, JSON_UNESCAPED_UNICODE); // 构建签名用的完整payload $payload = [ 'clientId' => $this->clientId, 'accessToken' => $this->accessToken, 'timestamp' => (int) (microtime(true) * 1000), 'data' => $requestData, ]; // 计算签名(SignHelper 会自动递归过滤空值) $sign = SignHelper::makeSign($payload, $this->appSecret, 'md5', true, 'shansong'); $payload['sign'] = $sign; $headers = ['Content-Type' => 'application/x-www-form-urlencoded;charset=utf-8']; // 发送请求到计费接口 $resp = HttpClient::post("{$this->baseUrl}/openapi/developer/v5/abortOrder", $payload, $headers); // 处理响应 if (isset($resp['status']) && $resp['status'] == 200 && isset($resp['data'])) { $respData = $resp['data']; return [ 'code' => 0, 'platform' => 'shansong', 'data' => [ 'deductionFee' => $respData['deductAmount'], //取消收费金额(单位:分) 'abortType' => $respData['abortType'], 'abortReason' => $respData['abortReason'] ] ]; } return null; } public function addTip($data) { $orderNo = $data['orderId'] ?? null; $additionAmount = $data['tips'] ?? null; if (empty($orderNo)) { return [ 'code' => -1, 'platform' => 'shansong', 'msg' => 'issOrderNo不能为空', 'data' => null, ]; } if (!isset($additionAmount) || (int)$additionAmount <= 0) { return [ 'code' => -1, 'platform' => 'shansong', 'msg' => 'additionAmount必须为正整数(单位:分)', 'data' => null, ]; } if (((int)$additionAmount) % 100 !== 0) { return [ 'code' => -1, 'platform' => 'shansong', 'msg' => 'additionAmount需被100整除(最小单位为1元)', 'data' => null, ]; } $requestData = json_encode([ 'issOrderNo' => $orderNo, 'additionAmount' => (int)$additionAmount, ], JSON_UNESCAPED_UNICODE); $payload = [ 'clientId' => $this->clientId, 'accessToken' => $this->accessToken, 'timestamp' => (int)(microtime(true) * 1000), 'data' => $requestData, ]; $sign = SignHelper::makeSign($payload, $this->appSecret, 'md5', true, 'shansong'); $payload['sign'] = $sign; $headers = ['Content-Type' => 'application/x-www-form-urlencoded;charset=utf-8']; $resp = HttpClient::post("{$this->baseUrl}/openapi/developer/v5/addition", $payload, $headers); if (isset($resp['status']) && $resp['status'] == 200) { return [ 'code' => 0, 'platform' => 'shansong', 'data' => $resp['data'] ?? null, ]; } Yii::error('[ShansongAdapter] addTip failed: ' . json_encode($resp, JSON_UNESCAPED_UNICODE)); return [ 'code' => $resp['status'] ?? -1, 'platform' => 'shansong', 'msg' => $resp['msg'] ?? 'Unknown error', 'data' => null, ]; } /** * 递归过滤数组中的空值(null、空字符串、空数组) * 确保签名时不包含空值参数 * * @param array $data 需要过滤的数组 * @return array 过滤后的数组 */ private function filterEmptyValues($data) { $filtered = []; foreach ($data as $key => $value) { // 过滤掉 null、空字符串、空数组 if ($value === null || $value === '' || (is_array($value) && empty($value))) { continue; } // 如果是数组,递归处理 if (is_array($value)) { $value = $this->filterEmptyValues($value); // 递归过滤后如果变成空数组,也要跳过 if (empty($value)) { continue; } } $filtered[$key] = $value; } return $filtered; } }