|
|
@@ -0,0 +1,370 @@
|
|
|
+<?php
|
|
|
+namespace common\components\delivery\services\adapter;
|
|
|
+
|
|
|
+use common\components\delivery\helpers\HttpClient;
|
|
|
+use common\components\delivery\platform\didi\Didi;
|
|
|
+use Yii;
|
|
|
+
|
|
|
+class DidiAdapter extends Didi implements Adapter
|
|
|
+{
|
|
|
+ /**
|
|
|
+ * 物品类型映射
|
|
|
+ * Didi: FOOD, FLOWER, CAKE, FILE, FRUIT, ELECTRONIC, DRESS, MEDICINAL, TEXTILE, FRESH, CAR_PART, OTHER
|
|
|
+ */
|
|
|
+ const GOODS_TYPE_MAP = [
|
|
|
+ '1' => 'FILE', // 文件
|
|
|
+ '2' => 'FOOD', // 餐饮
|
|
|
+ '3' => 'CAKE', // 蛋糕
|
|
|
+ '4' => 'FLOWER', // 鲜花
|
|
|
+ '5' => 'ELECTRONIC',// 数码
|
|
|
+ '6' => 'MEDICINAL', // 医药
|
|
|
+ '8' => 'OTHER', // 其他
|
|
|
+ // 根据实际情况补充
|
|
|
+ ];
|
|
|
+
|
|
|
+ public function cityList()
|
|
|
+ {
|
|
|
+ // 滴滴快送暂无城市列表接口,返回空
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建订单计费请求信息(供并发请求使用)
|
|
|
+ *
|
|
|
+ * @param array $data 订单数据
|
|
|
+ * @param string $orderTime 配送时间
|
|
|
+ * @return array 包含 url, data, headers, timeout 的请求信息
|
|
|
+ */
|
|
|
+ public function buildPriceRequest($data, $orderTime)
|
|
|
+ {
|
|
|
+ $sender = $data['sender'];
|
|
|
+ // 滴滴目前主要支持单点对单点,取第一个收件人
|
|
|
+ $receiver = $data['receiverList'][0] ?? [];
|
|
|
+ if (empty($receiver)) {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 服务参数列表 (询价时可以传入多个,获取不同服务的价格)
|
|
|
+ // 假设这里主要请求快送(BizType=12)
|
|
|
+ $serviceCategoryList = [
|
|
|
+ [
|
|
|
+ 'bizType' => 12, // 业务类型 -- 快送
|
|
|
+ 'carType' => 1018, // 车型 -- 两轮车
|
|
|
+ 'serviceLevel' => 0, // 服服务等级
|
|
|
+ 'serviceType' => 1, // 服务类型(快送专用,拉货不需要传) -- 直送
|
|
|
+ ],
|
|
|
+ [
|
|
|
+ 'bizType' => 12,
|
|
|
+ 'carType' => 1017, // 小轿车
|
|
|
+ 'serviceLevel' => 0,
|
|
|
+ 'serviceType' => 1,
|
|
|
+ ]
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 构造起点信息
|
|
|
+ $fromAddress = [
|
|
|
+ 'name' => $sender['fromSenderName'],
|
|
|
+ 'phone' => $sender['fromMobile'],
|
|
|
+ 'poiName' => $sender['fromAddress'],
|
|
|
+ 'poiAddress' => $sender['fromAddress'],
|
|
|
+ 'addressInfo' => $sender['fromAddressDetail'],
|
|
|
+ 'latitude' => (float)$sender['fromLatitude'],
|
|
|
+ 'longitude' => (float)$sender['fromLongitude'],
|
|
|
+ 'virtualPhoneType' => 0, // 0-真实号码
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 构造终点信息
|
|
|
+ $toAddress = [
|
|
|
+ 'name' => $receiver['toReceiverName'],
|
|
|
+ 'phone' => $receiver['toMobile'],
|
|
|
+ 'poiName' => $receiver['toAddress'],
|
|
|
+ 'poiAddress' => $receiver['toAddress'],
|
|
|
+ 'addressInfo' => $receiver['toAddressDetail'],
|
|
|
+ 'latitude' => (float)$receiver['toLatitude'],
|
|
|
+ 'longitude' => (float)$receiver['toLongitude'],
|
|
|
+ 'virtualPhoneType' => 0,
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 物品信息
|
|
|
+ $weight = (int)($receiver['weight'] ?? 1); // kg
|
|
|
+ $weightGram = $weight * 1000; // 转为克
|
|
|
+
|
|
|
+ $goodTypeInt = $receiver['goodType'] ?? '8';
|
|
|
+ $goodsType = self::GOODS_TYPE_MAP[$goodTypeInt] ?? 'FLOWER';
|
|
|
+
|
|
|
+ // 预约单处理
|
|
|
+ $modeType = 1; // 1-实时单
|
|
|
+ $useTime = null;
|
|
|
+ if ($orderTime) {
|
|
|
+ $ts = strtotime($orderTime);
|
|
|
+ if ($ts > time() + 1800) { // 30分钟后算预约? 滴滴快送暂不支持预约单,这里保留逻辑备用
|
|
|
+ // $modeType = 2;
|
|
|
+ // $useTime = $ts * 1000;
|
|
|
+ // 注意:文档说“快送暂不支持预约单都按照实时单处理”,所以这里可能无需处理,或者传了也没用
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ------------------ 入参构建 ------------------
|
|
|
+ $businessParams = [
|
|
|
+ 'thirdUid' => $sender['fromMobile'], // 使用发件人手机作为接入方用户唯一标识
|
|
|
+ 'serviceCategoryList' => $serviceCategoryList,
|
|
|
+ 'fromAddress' => $fromAddress,
|
|
|
+ 'toAddress' => $toAddress,
|
|
|
+ 'goodsType' => $goodsType,
|
|
|
+ 'weight' => $weightGram,
|
|
|
+ 'needPickupCode' => false,
|
|
|
+ 'needReceiveCode' => true, // 通常需要收件码
|
|
|
+ 'modeType' => $modeType,
|
|
|
+ ];
|
|
|
+
|
|
|
+ if ($useTime) {
|
|
|
+ $businessParams['useTime'] = $useTime;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 构建请求Payload
|
|
|
+ $payload = $this->buildRequestPayload($businessParams);
|
|
|
+ $apiUrl = $this->getApiUrl('freight.open.platform.channel.standard.estimate');
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'url' => $apiUrl,
|
|
|
+ 'data' => $payload,
|
|
|
+ 'headers' => [],
|
|
|
+ 'timeout' => 10.0,
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理询价响应
|
|
|
+ */
|
|
|
+ public function processPriceResponse($resp)
|
|
|
+ {
|
|
|
+ if (isset($resp['code']) && $resp['code'] == 0 && isset($resp['data'])) {
|
|
|
+ $data = $resp['data'];
|
|
|
+ $estimateList = $data['estimateList'] ?? [];
|
|
|
+ if (empty($estimateList)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 选取第一个结果,或者根据策略选取(比如最便宜) TODO
|
|
|
+ // 这里简单取第一个
|
|
|
+ $estimate = $estimateList[0];
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'platform' => 'didi',
|
|
|
+ 'estimate_id' => $estimate['estimateId'], // 预估ID,发单用
|
|
|
+ 'total_distance' => $estimate['distance'] ?? 0,
|
|
|
+ 'total_amount' => $estimate['fee'] ?? 0, // 分
|
|
|
+ 'total_fee_after_save' => $estimate['fee'] ?? 0,
|
|
|
+ 'coupon_save_fee' => $estimate['discountFee'] ?? 0,
|
|
|
+ 'fee_info_list' => $estimate['feeList'] ?? [],
|
|
|
+ // 补充其他字段以匹配通用结构
|
|
|
+ 'total_weight' => 0, // 响应里没返回重量
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 发单
|
|
|
+ */
|
|
|
+ public function createOrder($order)
|
|
|
+ {
|
|
|
+ // 解析 $order 数据,注意这里的 $order 结构可能与 buildPriceRequest 的 $data 不同
|
|
|
+ // 通常 service 层会将 price check 的结果合并进来,或者前端传回 estimateId
|
|
|
+
|
|
|
+ $estimateId = $order['estimate_id'] ?? null;
|
|
|
+ if (!$estimateId) {
|
|
|
+ return [
|
|
|
+ 'code' => -1,
|
|
|
+ 'platform' => 'didi',
|
|
|
+ 'msg' => '缺少estimate_id',
|
|
|
+ 'data' => []
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ $sender = $order['sender'];
|
|
|
+ $receiver = $order['receiverList'][0];
|
|
|
+
|
|
|
+ $fromAddress = [
|
|
|
+ 'name' => $sender['fromSenderName'],
|
|
|
+ 'phone' => $sender['fromMobile'],
|
|
|
+ 'poiName' => $sender['fromAddress'],
|
|
|
+ 'poiAddress' => $sender['fromAddress'],
|
|
|
+ 'addressInfo' => $sender['fromAddressDetail'],
|
|
|
+ 'latitude' => (float)$sender['fromLatitude'],
|
|
|
+ 'longitude' => (float)$sender['fromLongitude'],
|
|
|
+ 'virtualPhoneType' => 0,
|
|
|
+ ];
|
|
|
+
|
|
|
+ $toAddress = [
|
|
|
+ 'name' => $receiver['toReceiverName'],
|
|
|
+ 'phone' => $receiver['toMobile'],
|
|
|
+ 'poiName' => $receiver['toAddress'],
|
|
|
+ 'poiAddress' => $receiver['toAddress'],
|
|
|
+ 'addressInfo' => $receiver['toAddressDetail'],
|
|
|
+ 'latitude' => (float)$receiver['toLatitude'],
|
|
|
+ 'longitude' => (float)$receiver['toLongitude'],
|
|
|
+ 'virtualPhoneType' => 0,
|
|
|
+ ];
|
|
|
+
|
|
|
+ $weight = (int)($receiver['weight'] ?? 1);
|
|
|
+ $weightGram = $weight * 1000;
|
|
|
+
|
|
|
+ $goodTypeInt = $receiver['goodType'] ?? '8';
|
|
|
+ $goodsType = self::GOODS_TYPE_MAP[$goodTypeInt] ?? 'OTHER';
|
|
|
+
|
|
|
+ // ------------------ 入参构建 ------------------
|
|
|
+ $businessParams = [
|
|
|
+ 'estimateIdList' => [$estimateId], // 预估ID
|
|
|
+ 'outOrderNo' => $order['issOrderNo'], // 接入方订单号
|
|
|
+ 'fromAddress' => $fromAddress,
|
|
|
+ 'toAddress' => $toAddress,
|
|
|
+ 'goodsType' => $goodsType,
|
|
|
+ 'weight' => $weightGram,
|
|
|
+ 'needPickupCode' => false,
|
|
|
+ 'needReceiveCode' => true,
|
|
|
+ 'thirdUid' => $sender['fromMobile'],
|
|
|
+ 'remark' => $order['remark'] ?? '',
|
|
|
+ ];
|
|
|
+
|
|
|
+ $payload = $this->buildRequestPayload($businessParams);
|
|
|
+ $url = $this->getApiUrl('freight.open.platform.channel.standard.createOrder');
|
|
|
+
|
|
|
+ $resp = HttpClient::post($url, $payload);
|
|
|
+
|
|
|
+ if (isset($resp['code']) && $resp['code'] == 0 && isset($resp['data'])) {
|
|
|
+ $respData = $resp['data'];
|
|
|
+ return [
|
|
|
+ 'code' => 0,
|
|
|
+ 'platform' => 'didi',
|
|
|
+ 'data' => [
|
|
|
+ 'order_id' => $respData['didiOrderNo'], // 滴滴订单号
|
|
|
+ // 可以附带其他信息
|
|
|
+ ]
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ $msg = $resp['msg'] ?? 'Unknown error';
|
|
|
+ if (isset($resp['code']) && isset($resp['subCode'])) {
|
|
|
+ $msg .= " ({$resp['code']}-{$resp['subCode']})";
|
|
|
+ }
|
|
|
+
|
|
|
+ Yii::error('[DidiAdapter] createOrder failed: ' . json_encode($resp, JSON_UNESCAPED_UNICODE));
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'code' => -1,
|
|
|
+ 'platform' => 'didi',
|
|
|
+ 'msg' => $msg,
|
|
|
+ 'data' => []
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 添加小费
|
|
|
+ */
|
|
|
+ public function addTip($data)
|
|
|
+ {
|
|
|
+ $outOrderNo = $data['orderId'] ?? null; // 这里假设传入的是接入方订单号,或者需要映射
|
|
|
+ // 注意:ShansongAdapter用的是 issOrderNo (内部订单号),Didi addTip需要 outOrderNo (接入方订单号)
|
|
|
+ // 假设 $data['orderId'] 就是 outOrderNo
|
|
|
+
|
|
|
+ $fee = $data['tips'] ?? 0; // 分
|
|
|
+
|
|
|
+ if (empty($outOrderNo)) {
|
|
|
+ return ['code' => -1, 'platform' => 'didi', 'msg' => '订单号不能为空'];
|
|
|
+ }
|
|
|
+ if ($fee <= 0) {
|
|
|
+ return ['code' => -1, 'platform' => 'didi', 'msg' => '小费金额必须大于0'];
|
|
|
+ }
|
|
|
+
|
|
|
+ $businessParams = [
|
|
|
+ 'outOrderNo' => $outOrderNo,
|
|
|
+ 'fee' => (int)$fee,
|
|
|
+ ];
|
|
|
+
|
|
|
+ $payload = $this->buildRequestPayload($businessParams);
|
|
|
+ $url = $this->getApiUrl('freight.open.platform.channel.standard.addTipFee');
|
|
|
+
|
|
|
+ $resp = HttpClient::post($url, $payload);
|
|
|
+
|
|
|
+ if (isset($resp['code']) && $resp['code'] == 0) {
|
|
|
+ return [
|
|
|
+ 'code' => 0,
|
|
|
+ 'platform' => 'didi',
|
|
|
+ 'data' => $resp['data'] ?? []
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'code' => $resp['code'] ?? -1,
|
|
|
+ 'platform' => 'didi',
|
|
|
+ 'msg' => $resp['msg'] ?? 'Unknown error',
|
|
|
+ 'data' => null
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 取消订单
|
|
|
+ */
|
|
|
+ public function cancelOrder($orderId)
|
|
|
+ {
|
|
|
+ // $orderId 应该是 outOrderNo (接入方订单号)
|
|
|
+ // Didi API 需要 outOrderNo 和 didiOrderNo。
|
|
|
+ // 如果我们只有 one of them,可能需要先查询?
|
|
|
+ // 接口文档说: outOrderNo 是必须, didiOrderNo 是必须.
|
|
|
+ // 这有点麻烦。通常 Adapter 的 input 只包含一个 ID。
|
|
|
+ // 假设 $orderId 是 outOrderNo。
|
|
|
+ // 此时我们可能没有 didiOrderNo,或者它需要作为第二个参数传入?
|
|
|
+ // 参考 ShansongAdapter,它只传了一个 $orderId (issOrderNo).
|
|
|
+ // 如果必须两个都传,可能需要修改调用方,或者我们只传 outOrderNo 试试,或者 look up mapping.
|
|
|
+ // 文档: outOrderNo (String, 是), didiOrderNo (Long, 是).
|
|
|
+ // 如果无法提供 didiOrderNo,可能无法取消。
|
|
|
+ // 但通常业务系统会保存 didiOrderNo。
|
|
|
+ // 这里假设调用方会在参数里传,或者 $orderId 是一个包含多个ID的数组?
|
|
|
+ // 按照 ShansongAdapter: cancelOrder($orderId).
|
|
|
+ // 只能假设 $orderId 结构可能包含 didiOrderNo,或者我们尝试只传 outOrderNo 看是否可行 (文档说是必须)。
|
|
|
+ // 为了兼容性,如果 $orderId 是数组,则取值;如果是字符串,视为 outOrderNo,且 didiOrderNo 填 0 或空?
|
|
|
+ // 让我们看看能不能只传 outOrderNo. "接口根据接入方订单号幂等...".
|
|
|
+ // 还是严格按照文档吧。如果调用方没传 didiOrderNo,这步会失败。
|
|
|
+
|
|
|
+ // 临时处理:检查 $orderId 是否是数组
|
|
|
+ $didiOrderNo = 0;
|
|
|
+ $outOrderNo = $orderId;
|
|
|
+
|
|
|
+ if (is_array($orderId)) {
|
|
|
+ $outOrderNo = $orderId['order_no'] ?? '';
|
|
|
+ $didiOrderNo = $orderId['platform_order_no'] ?? 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ $businessParams = [
|
|
|
+ 'outOrderNo' => (string)$outOrderNo,
|
|
|
+ 'didiOrderNo' => (int)$didiOrderNo,
|
|
|
+ 'cancelSource' => 1, // 用户取消
|
|
|
+ 'cancelReason' => '暂不需要',
|
|
|
+ ];
|
|
|
+
|
|
|
+ $payload = $this->buildRequestPayload($businessParams);
|
|
|
+ $url = $this->getApiUrl('freight.open.platform.channel.standard.cancel');
|
|
|
+
|
|
|
+ $resp = HttpClient::post($url, $payload);
|
|
|
+
|
|
|
+ if (isset($resp['code']) && $resp['code'] == 0 && isset($resp['data'])) {
|
|
|
+ $data = $resp['data'];
|
|
|
+ return [
|
|
|
+ 'code' => 0,
|
|
|
+ 'platform' => 'didi',
|
|
|
+ 'data' => [
|
|
|
+ 'deductionFee' => $data['cancelFee'] ?? 0, // 分
|
|
|
+ 'cancelResult' => $data['cancelResult'],
|
|
|
+ ]
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'code' => $resp['code'] ?? -1,
|
|
|
+ 'platform' => 'didi',
|
|
|
+ 'msg' => $resp['msg'] ?? 'Unknown error',
|
|
|
+ 'data' => null
|
|
|
+ ];
|
|
|
+ }
|
|
|
+}
|