DidiAdapter.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. <?php
  2. namespace common\components\delivery\services\adapter;
  3. use common\components\delivery\helpers\HttpClient;
  4. use common\components\delivery\platform\didi\Didi;
  5. use Yii;
  6. class DidiAdapter extends Didi implements Adapter
  7. {
  8. /**
  9. * 物品类型映射
  10. * Didi: FOOD, FLOWER, CAKE, FILE, FRUIT, ELECTRONIC, DRESS, MEDICINAL, TEXTILE, FRESH, CAR_PART, OTHER
  11. */
  12. const GOODS_TYPE_MAP = [
  13. '1' => 'FILE', // 文件
  14. '2' => 'FOOD', // 餐饮
  15. '3' => 'CAKE', // 蛋糕
  16. '4' => 'FLOWER', // 鲜花
  17. '5' => 'ELECTRONIC',// 数码
  18. '6' => 'MEDICINAL', // 医药
  19. '8' => 'OTHER', // 其他
  20. // 根据实际情况补充
  21. ];
  22. public function cityList()
  23. {
  24. // 滴滴快送暂无城市列表接口,返回空
  25. return [];
  26. }
  27. /**
  28. * 构建订单计费请求信息(供并发请求使用)
  29. *
  30. * @param array $data 订单数据
  31. * @param string $orderTime 配送时间
  32. * @return array 包含 url, data, headers, timeout 的请求信息
  33. */
  34. public function buildPriceRequest($data, $orderTime)
  35. {
  36. $sender = $data['sender'];
  37. // 滴滴目前主要支持单点对单点,取第一个收件人
  38. $receiver = $data['receiverList'][0] ?? [];
  39. if (empty($receiver)) {
  40. return [];
  41. }
  42. // 服务参数列表 (询价时可以传入多个,获取不同服务的价格)
  43. // 假设这里主要请求快送(BizType=12)
  44. $serviceCategoryList = [
  45. [
  46. 'bizType' => 12, // 业务类型 -- 快送
  47. 'carType' => 1018, // 车型 -- 两轮车
  48. 'serviceLevel' => 0, // 服服务等级
  49. 'serviceType' => 1, // 服务类型(快送专用,拉货不需要传) -- 直送
  50. ],
  51. [
  52. 'bizType' => 12,
  53. 'carType' => 1017, // 小轿车
  54. 'serviceLevel' => 0,
  55. 'serviceType' => 1,
  56. ]
  57. ];
  58. // 构造起点信息
  59. $fromAddress = [
  60. 'name' => $sender['fromSenderName'],
  61. 'phone' => $sender['fromMobile'],
  62. 'poiName' => $sender['fromAddress'],
  63. 'poiAddress' => $sender['fromAddress'],
  64. 'addressInfo' => $sender['fromAddressDetail'],
  65. 'latitude' => (float)$sender['fromLatitude'],
  66. 'longitude' => (float)$sender['fromLongitude'],
  67. 'virtualPhoneType' => 0, // 0-真实号码
  68. ];
  69. // 构造终点信息
  70. $toAddress = [
  71. 'name' => $receiver['toReceiverName'],
  72. 'phone' => $receiver['toMobile'],
  73. 'poiName' => $receiver['toAddress'],
  74. 'poiAddress' => $receiver['toAddress'],
  75. 'addressInfo' => $receiver['toAddressDetail'],
  76. 'latitude' => (float)$receiver['toLatitude'],
  77. 'longitude' => (float)$receiver['toLongitude'],
  78. 'virtualPhoneType' => 0,
  79. ];
  80. // 物品信息
  81. $weight = (int)($receiver['weight'] ?? 1); // kg
  82. $weightGram = $weight * 1000; // 转为克
  83. $goodTypeInt = $receiver['goodType'] ?? '8';
  84. $goodsType = self::GOODS_TYPE_MAP[$goodTypeInt] ?? 'FLOWER';
  85. // 预约单处理
  86. $modeType = 1; // 1-实时单
  87. $useTime = null;
  88. if ($orderTime) {
  89. $ts = strtotime($orderTime);
  90. if ($ts > time() + 1800) { // 30分钟后算预约? 滴滴快送暂不支持预约单,这里保留逻辑备用
  91. // $modeType = 2;
  92. // $useTime = $ts * 1000;
  93. // 注意:文档说“快送暂不支持预约单都按照实时单处理”,所以这里可能无需处理,或者传了也没用
  94. }
  95. }
  96. // ------------------ 入参构建 ------------------
  97. $businessParams = [
  98. 'thirdUid' => $sender['fromMobile'], // 使用发件人手机作为接入方用户唯一标识
  99. 'serviceCategoryList' => $serviceCategoryList,
  100. 'fromAddress' => $fromAddress,
  101. 'toAddress' => $toAddress,
  102. 'goodsType' => $goodsType,
  103. 'weight' => $weightGram,
  104. 'needPickupCode' => false,
  105. 'needReceiveCode' => true, // 通常需要收件码
  106. 'modeType' => $modeType,
  107. ];
  108. if ($useTime) {
  109. $businessParams['useTime'] = $useTime;
  110. }
  111. // 构建请求Payload
  112. $payload = $this->buildRequestPayload($businessParams);
  113. $apiUrl = $this->getApiUrl('freight.open.platform.channel.standard.estimate');
  114. return [
  115. 'url' => $apiUrl,
  116. 'data' => $payload,
  117. 'headers' => [],
  118. 'timeout' => 10.0,
  119. ];
  120. }
  121. /**
  122. * 处理询价响应
  123. */
  124. public function processPriceResponse($resp)
  125. {
  126. if (isset($resp['code']) && $resp['code'] == 0 && isset($resp['data'])) {
  127. $data = $resp['data'];
  128. $estimateList = $data['estimateList'] ?? [];
  129. if (empty($estimateList)) {
  130. return null;
  131. }
  132. // 选取第一个结果,或者根据策略选取(比如最便宜) TODO
  133. // 这里简单取第一个
  134. $estimate = $estimateList[0];
  135. return [
  136. 'platform' => 'didi',
  137. 'estimate_id' => $estimate['estimateId'], // 预估ID,发单用
  138. 'total_distance' => $estimate['distance'] ?? 0,
  139. 'total_amount' => $estimate['fee'] ?? 0, // 分
  140. 'total_fee_after_save' => $estimate['fee'] ?? 0,
  141. 'coupon_save_fee' => $estimate['discountFee'] ?? 0,
  142. 'fee_info_list' => $estimate['feeList'] ?? [],
  143. // 补充其他字段以匹配通用结构
  144. 'total_weight' => 0, // 响应里没返回重量
  145. ];
  146. }
  147. return null;
  148. }
  149. /**
  150. * 发单
  151. */
  152. public function createOrder($order)
  153. {
  154. // 解析 $order 数据,注意这里的 $order 结构可能与 buildPriceRequest 的 $data 不同
  155. // 通常 service 层会将 price check 的结果合并进来,或者前端传回 estimateId
  156. $estimateId = $order['estimate_id'] ?? null;
  157. if (!$estimateId) {
  158. return [
  159. 'code' => -1,
  160. 'platform' => 'didi',
  161. 'msg' => '缺少estimate_id',
  162. 'data' => []
  163. ];
  164. }
  165. $sender = $order['sender'];
  166. $receiver = $order['receiverList'][0];
  167. $fromAddress = [
  168. 'name' => $sender['fromSenderName'],
  169. 'phone' => $sender['fromMobile'],
  170. 'poiName' => $sender['fromAddress'],
  171. 'poiAddress' => $sender['fromAddress'],
  172. 'addressInfo' => $sender['fromAddressDetail'],
  173. 'latitude' => (float)$sender['fromLatitude'],
  174. 'longitude' => (float)$sender['fromLongitude'],
  175. 'virtualPhoneType' => 0,
  176. ];
  177. $toAddress = [
  178. 'name' => $receiver['toReceiverName'],
  179. 'phone' => $receiver['toMobile'],
  180. 'poiName' => $receiver['toAddress'],
  181. 'poiAddress' => $receiver['toAddress'],
  182. 'addressInfo' => $receiver['toAddressDetail'],
  183. 'latitude' => (float)$receiver['toLatitude'],
  184. 'longitude' => (float)$receiver['toLongitude'],
  185. 'virtualPhoneType' => 0,
  186. ];
  187. $weight = (int)($receiver['weight'] ?? 1);
  188. $weightGram = $weight * 1000;
  189. $goodTypeInt = $receiver['goodType'] ?? '8';
  190. $goodsType = self::GOODS_TYPE_MAP[$goodTypeInt] ?? 'OTHER';
  191. // ------------------ 入参构建 ------------------
  192. $businessParams = [
  193. 'estimateIdList' => [$estimateId], // 预估ID
  194. 'outOrderNo' => $order['issOrderNo'], // 接入方订单号
  195. 'fromAddress' => $fromAddress,
  196. 'toAddress' => $toAddress,
  197. 'goodsType' => $goodsType,
  198. 'weight' => $weightGram,
  199. 'needPickupCode' => false,
  200. 'needReceiveCode' => true,
  201. 'thirdUid' => $sender['fromMobile'],
  202. 'remark' => $order['remark'] ?? '',
  203. ];
  204. $payload = $this->buildRequestPayload($businessParams);
  205. $url = $this->getApiUrl('freight.open.platform.channel.standard.createOrder');
  206. $resp = HttpClient::post($url, $payload);
  207. if (isset($resp['code']) && $resp['code'] == 0 && isset($resp['data'])) {
  208. $respData = $resp['data'];
  209. return [
  210. 'code' => 0,
  211. 'platform' => 'didi',
  212. 'data' => [
  213. 'order_id' => $respData['didiOrderNo'], // 滴滴订单号
  214. // 可以附带其他信息
  215. ]
  216. ];
  217. }
  218. $msg = $resp['msg'] ?? 'Unknown error';
  219. if (isset($resp['code']) && isset($resp['subCode'])) {
  220. $msg .= " ({$resp['code']}-{$resp['subCode']})";
  221. }
  222. Yii::error('[DidiAdapter] createOrder failed: ' . json_encode($resp, JSON_UNESCAPED_UNICODE));
  223. return [
  224. 'code' => -1,
  225. 'platform' => 'didi',
  226. 'msg' => $msg,
  227. 'data' => []
  228. ];
  229. }
  230. /**
  231. * 添加小费
  232. */
  233. public function addTip($data)
  234. {
  235. $outOrderNo = $data['orderId'] ?? null; // 这里假设传入的是接入方订单号,或者需要映射
  236. // 注意:ShansongAdapter用的是 issOrderNo (内部订单号),Didi addTip需要 outOrderNo (接入方订单号)
  237. // 假设 $data['orderId'] 就是 outOrderNo
  238. $fee = $data['tips'] ?? 0; // 分
  239. if (empty($outOrderNo)) {
  240. return ['code' => -1, 'platform' => 'didi', 'msg' => '订单号不能为空'];
  241. }
  242. if ($fee <= 0) {
  243. return ['code' => -1, 'platform' => 'didi', 'msg' => '小费金额必须大于0'];
  244. }
  245. $businessParams = [
  246. 'outOrderNo' => $outOrderNo,
  247. 'fee' => (int)$fee,
  248. ];
  249. $payload = $this->buildRequestPayload($businessParams);
  250. $url = $this->getApiUrl('freight.open.platform.channel.standard.addTipFee');
  251. $resp = HttpClient::post($url, $payload);
  252. if (isset($resp['code']) && $resp['code'] == 0) {
  253. return [
  254. 'code' => 0,
  255. 'platform' => 'didi',
  256. 'data' => $resp['data'] ?? []
  257. ];
  258. }
  259. return [
  260. 'code' => $resp['code'] ?? -1,
  261. 'platform' => 'didi',
  262. 'msg' => $resp['msg'] ?? 'Unknown error',
  263. 'data' => null
  264. ];
  265. }
  266. /**
  267. * 取消订单
  268. */
  269. public function cancelOrder($orderId)
  270. {
  271. // $orderId 应该是 outOrderNo (接入方订单号)
  272. // Didi API 需要 outOrderNo 和 didiOrderNo。
  273. // 如果我们只有 one of them,可能需要先查询?
  274. // 接口文档说: outOrderNo 是必须, didiOrderNo 是必须.
  275. // 这有点麻烦。通常 Adapter 的 input 只包含一个 ID。
  276. // 假设 $orderId 是 outOrderNo。
  277. // 此时我们可能没有 didiOrderNo,或者它需要作为第二个参数传入?
  278. // 参考 ShansongAdapter,它只传了一个 $orderId (issOrderNo).
  279. // 如果必须两个都传,可能需要修改调用方,或者我们只传 outOrderNo 试试,或者 look up mapping.
  280. // 文档: outOrderNo (String, 是), didiOrderNo (Long, 是).
  281. // 如果无法提供 didiOrderNo,可能无法取消。
  282. // 但通常业务系统会保存 didiOrderNo。
  283. // 这里假设调用方会在参数里传,或者 $orderId 是一个包含多个ID的数组?
  284. // 按照 ShansongAdapter: cancelOrder($orderId).
  285. // 只能假设 $orderId 结构可能包含 didiOrderNo,或者我们尝试只传 outOrderNo 看是否可行 (文档说是必须)。
  286. // 为了兼容性,如果 $orderId 是数组,则取值;如果是字符串,视为 outOrderNo,且 didiOrderNo 填 0 或空?
  287. // 让我们看看能不能只传 outOrderNo. "接口根据接入方订单号幂等...".
  288. // 还是严格按照文档吧。如果调用方没传 didiOrderNo,这步会失败。
  289. // 临时处理:检查 $orderId 是否是数组
  290. $didiOrderNo = 0;
  291. $outOrderNo = $orderId;
  292. if (is_array($orderId)) {
  293. $outOrderNo = $orderId['order_no'] ?? '';
  294. $didiOrderNo = $orderId['platform_order_no'] ?? 0;
  295. }
  296. $businessParams = [
  297. 'outOrderNo' => (string)$outOrderNo,
  298. 'didiOrderNo' => (int)$didiOrderNo,
  299. 'cancelSource' => 1, // 用户取消
  300. 'cancelReason' => '暂不需要',
  301. ];
  302. $payload = $this->buildRequestPayload($businessParams);
  303. $url = $this->getApiUrl('freight.open.platform.channel.standard.cancel');
  304. $resp = HttpClient::post($url, $payload);
  305. if (isset($resp['code']) && $resp['code'] == 0 && isset($resp['data'])) {
  306. $data = $resp['data'];
  307. return [
  308. 'code' => 0,
  309. 'platform' => 'didi',
  310. 'data' => [
  311. 'deductionFee' => $data['cancelFee'] ?? 0, // 分
  312. 'cancelResult' => $data['cancelResult'],
  313. ]
  314. ];
  315. }
  316. return [
  317. 'code' => $resp['code'] ?? -1,
  318. 'platform' => 'didi',
  319. 'msg' => $resp['msg'] ?? 'Unknown error',
  320. 'data' => null
  321. ];
  322. }
  323. }