$this->clientId, "accessToken" => $this->accessToken, "timestamp" => (int) (microtime(true) * 1000), ]; $sign = SignHelper::makeSign($payload, $this->appSecret, 'md5', true, 'shansong'); $payload['sign'] = $sign; $resp = HttpClient::post("{$this->baseUrl}/openapi/developer/v5/openCitiesLists", $payload); $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 $shop * @param $params * @return array */ public function formatOrderData($order, $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; $resp = HttpClient::post("{$this->baseUrl}/openapi/developer/v5/orderPlace", $payload); // 处理响应 if (isset($resp['status']) && $resp['status'] == 200 && isset($resp['data'])) { $respData = $resp['data']; return [ 'code' => 0, 'platform' => 'shansong', 'data' => [ 'order_id' => $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 $orderTime 配送时间 * @return array 包含 url, data, headers, timeout 的请求信息 */ public function buildPriceRequest($data, $orderTime) { // 预约类型 $timeDiff = strtotime($orderTime) - time(); $appointType = 0; if ($timeDiff > 3600 ) { $appointType = 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'] ?? '', // TODO: 需要添加备注 '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'] : null, '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'] = $orderTime; } $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' => [], '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, // 预计完单时长,字段弃用 ]; } return null; } /** * @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); $resp = HttpClient::post($request['url'], $request['data']); 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; // 发送请求到计费接口 $resp = HttpClient::post("{$this->baseUrl}/openapi/developer/v5/orderInfo", $payload); // 处理响应 if (isset($resp['status']) && $resp['status'] == 200 && isset($resp['data'])) { $respData = $resp['data']; return [ 'platform' => 'shansong', 'data' => $respData ]; } return 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; // 发送请求到计费接口 $resp = HttpClient::post("{$this->baseUrl}/openapi/developer/v5/abortOrder", $payload); // 处理响应 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; $resp = HttpClient::post("{$this->baseUrl}/openapi/developer/v5/addition", $payload); 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; } }