FengniaoAdapter.php 60 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418
  1. <?php
  2. namespace common\components\delivery\services\adapter;
  3. use bizGhs\order\classes\OrderItemClass;
  4. use bizHd\order\classes\OrderGoodsClass;
  5. use common\components\delivery\helpers\HttpClient;
  6. use common\components\delivery\platform\fengniao\Fengniao;
  7. use common\components\util;
  8. use Yii;
  9. /**
  10. * 蜂鸟适配器
  11. *
  12. * 基于蜂鸟开放平台API进行封装
  13. * 文档参考:蜂鸟业务接口调用说明
  14. *
  15. * 接口规范:
  16. * - 调用方式:HTTPS/POST
  17. * - 编码格式:UTF-8
  18. * - Content-Type:application/json
  19. * - 正式环境:https://open-anubis.ele.me/anubis-webapi/v3/invoke/{method}
  20. * - 沙箱环境:https://exam-anubis.ele.me/anubis-webapi/v3/invoke/{method}
  21. */
  22. class FengniaoAdapter extends Fengniao implements Adapter
  23. {
  24. /**
  25. * 获取已开通城市列表
  26. *
  27. * 通过此接口可获取到蜂鸟所有已开通的城市列表信息
  28. *
  29. * @return array 包含城市列表的响应数据
  30. */
  31. public function cityList()
  32. {
  33. $businessData = [];
  34. // 构建请求参数
  35. $payload = $this->buildRequestPayload('cityList', $businessData);
  36. // 发送请求
  37. $url = $this->baseUrl . '/cityList';
  38. $resp = HttpClient::post($url, $payload, [
  39. 'Content-Type' => 'application/json',
  40. ]);
  41. Yii::info("[FengniaoAdapter] cityList Response: " . json_encode($resp));
  42. return $resp;
  43. }
  44. /**
  45. * 转化订单数据
  46. * @param $order
  47. * @param string $type 订单类型:xhOrder 或 xhGhsOrder
  48. * @param $shop
  49. * @param $params
  50. * @return array
  51. * @throws \Exception
  52. */
  53. public function formatOrderData($order, $type, $shop, $params)
  54. {
  55. // 订单类型为预约单时必传,如需要送达时间 – 推单时间 < 60min,则蜂鸟配送开放平台自动将订单类型置为即时单
  56. $timeDiff = strtotime($params['pickupTime']) - time();
  57. $orderType = 1;
  58. if ($timeDiff > 3600) {
  59. $orderType = 3;
  60. }
  61. $sn = $order['orderSn'];
  62. // 批发
  63. if($type == 'xhGhsOrder'){
  64. $itemInfos = OrderItemClass::getAllByCondition(['orderSn' => $sn], null, 'id, name, unitPrice, num, unitWeight');
  65. //$goodsWeight = 0;
  66. $goodsItemList = [];
  67. foreach ($itemInfos as $item){
  68. $goodsItemList[] = [
  69. 'item_actual_amount_cent' => $item['unitPrice'] * 100 * $item['num'],
  70. 'item_amount_cent' => $item['unitPrice'] * 100,
  71. 'item_id' => $item['id'],
  72. 'item_name' => $item['name'],
  73. 'item_quantity' => $item['num'],
  74. ];
  75. //$goodsWeight += $item['unitWeight'];
  76. }
  77. $goodsCount = count($goodsItemList);
  78. // 零售
  79. }else if($type == 'xhOrder'){
  80. $itemInfos = OrderGoodsClass::getAllByCondition(['orderSn' => $sn], null, 'id, name, unitPrice, num');
  81. //$goodsWeight = 0;
  82. $goodsItemList = [];
  83. foreach ($itemInfos as $item){
  84. $goodsItemList[] = [
  85. 'item_actual_amount_cent' => $item['unitPrice'] * 100 * $item['num'],
  86. 'item_amount_cent' => $item['unitPrice'] * 100,
  87. 'item_id' => $item['id'],
  88. 'item_name' => $item['name'],
  89. 'item_quantity' => $item['num'],
  90. ];
  91. }
  92. $goodsCount = count($goodsItemList);
  93. }else{
  94. Yii::error('订单编号出错: '.$sn);
  95. util::fail('订单编号出错');
  96. }
  97. $goodsWeight = floatval($params['weight']);//取客户端传来的重量
  98. $orderData = [
  99. //'out_shop_code' => $params['mainId'], // 当使用门店发单 out_shop_code 和 chain_store_id 必填1个
  100. 'chain_store_id' => $this->getShopId(), // 动态修改为门店号 -- (点对点不传)
  101. 'partner_order_code' => $order['orderSn'],
  102. 'receiver_primary_phone' => Functions::getMobile($order),
  103. 'receiver_name' => $order['customName'],
  104. 'receiver_latitude' => (float)$order['lat'],
  105. 'receiver_longitude' => (float)$order['long'],
  106. 'receiver_address' => $order['fullAddress'],
  107. 'position_source' => 3, // 坐标经纬度来源 -- 3 高德地图
  108. 'goods_count' => $goodsCount,
  109. 'goods_weight' => (float)$goodsWeight,
  110. 'goods_total_amount_cent' => (int)($order['prePrice'] * 100), //订单商品总金额
  111. 'goods_actual_amount_cent' => (int)($order['actPrice'] * 100), //订单货物实付金额
  112. 'goods_item_list' => $goodsItemList,
  113. 'order_type' => $orderType,
  114. 'order_remark' => $params['remark'],
  115. 'serial_number' => $order['sendNum'] // 配送编号,方便寻找订单对应的花材
  116. ];
  117. if($orderType == 3) {
  118. // 预计出餐时间 单位:毫秒 -- 预约单该字段必填
  119. $orderData['expect_fetch_time'] = strtotime($params['pickupTime']) * 1000;
  120. // 需要送达时间 = 取件时间 + 30分钟
  121. $orderData['require_receive_time'] = (strtotime($params['pickupTime']) + 1800) * 1000;
  122. }
  123. return $orderData;
  124. }
  125. /**
  126. * 创建订单(需用户授权)
  127. *
  128. * 根据蜂鸟官方文档实现下单功能。
  129. * 若需校验配送费价格,需先走预下单接口,并在下单时填上预下单信息。
  130. *
  131. * @param array $data 订单数据,结构参考:
  132. * [
  133. * // ========== 必填参数 ==========
  134. * 'partner_order_code' => '外部订单号(用于幂等)',
  135. * 'receiver_primary_phone' => '收货人主要联系方式(手机号、400电话、座机等)',
  136. * 'receiver_name' => '收货人姓名',
  137. * 'receiver_latitude' => '收货人纬度',
  138. * 'receiver_longitude' => '收货人经度',
  139. * 'receiver_address' => '收货人地址',
  140. * 'position_source' => '经纬度来源(1:腾讯, 2:百度, 3:高德,推荐3)',
  141. * 'goods_count' => '货物件数',
  142. * 'goods_weight' => '货物总重量(kg)',
  143. * 'goods_total_amount_cent' => '订单商品总金额(分)',
  144. * 'goods_actual_amount_cent' => '订单货物实付金额(分)',
  145. * 'goods_item_list' => '货物明细列表',
  146. * 'order_type' => '订单类型(1:即时单, 3:预约单)',
  147. *
  148. * // ========== 可选参数 ==========
  149. * // 门店信息(二选一)
  150. * 'chain_store_id' => '门店ID',
  151. * 'out_shop_code' => '外部门店ID',
  152. *
  153. * // 预下单关联
  154. * 'pre_create_order_t_index_id' => '预下单标识(用于校验配送费价格)',
  155. * 'actual_delivery_amount_cent' => '优惠后配送费总价(分,不为null则校验价格)',
  156. *
  157. * // 取货点信息(点对点特殊场景)
  158. * 'transport_address' => '取货点地址',
  159. * 'transport_name' => '取货点名称',
  160. * 'transport_latitude' => '取货点纬度',
  161. * 'transport_longitude' => '取货点经度',
  162. * 'transport_tel' => '取货点联系人电话',
  163. *
  164. * // 时间相关
  165. * 'order_add_time' => '下单时间(毫秒)',
  166. * 'require_receive_time' => '需要送达时间(毫秒,预约单时重要)',
  167. * 'expect_fetch_time' => '预计出餐时间(毫秒,预约单必填)',
  168. *
  169. * // 订单备注和来源
  170. * 'order_remark' => '用户备注',
  171. * 'order_source' => '商户订单来源(0:手发单, 2:美团, 4:口碑, 6:饿了么等)',
  172. * 'order_source_order_id' => '订单来源单号',
  173. * 'serial_number' => '商家订单流水号(长度<=8)',
  174. *
  175. * // 骑手交互
  176. * 'fetch_code' => '取餐码',
  177. * 'write_off_code' => '核销码',
  178. * 'cancel_code' => '取消码',
  179. * 'customer_ext_tel' => '分机号',
  180. *
  181. * // 收货人备用信息
  182. * 'receiver_second_phone' => '收货人备用联系方式',
  183. *
  184. * // 小费和优惠
  185. * 'order_tip_amount_cent' => '订单小费金额(分)',
  186. * 'use_coupon' => '是否使用优惠券(0:否, 1:是,默认使用)',
  187. * 'warehouse_id' => '优惠券记录ID',
  188. *
  189. * // 商品选择
  190. * 'base_goods_id' => '基础商品ID',
  191. * 'service_goods_id' => '服务商品ID',
  192. * 'appoint_extra_goods_ids' => '指定增值商品ID列表',
  193. *
  194. * 'goods_item_list' => [ // 货物明细(必填)
  195. * [
  196. * 'item_id' => '商品编号',
  197. * 'item_name' => '商品名称',
  198. * 'item_amount_cent' => '商品原价(分,必填)',
  199. * 'item_actual_amount_cent' => '商品实际支付金额(分,必填)',
  200. * 'item_size' => '商品尺寸(1:小, 2:中, 3:大)',
  201. * 'item_quantity' => '商品数量',
  202. * 'item_remark' => '商品备注',
  203. * ],
  204. * ],
  205. * ]
  206. * @return array API响应结果
  207. * 成功:['code' => 0, 'data' => ['order_id' => 订单ID]]
  208. * 失败:['code' => 错误码, 'msg' => 错误信息, 'data' => null]
  209. */
  210. public function createOrder($data)
  211. {
  212. // 构建货物明细列表
  213. $goodsItemList = [];
  214. if (!empty($data['goods_item_list']) && is_array($data['goods_item_list'])) {
  215. foreach ($data['goods_item_list'] as $item) {
  216. $goodsItem = [
  217. 'item_actual_amount_cent' => isset($item['item_actual_amount_cent']) ? (int)$item['item_actual_amount_cent'] : 0,
  218. 'item_amount_cent' => isset($item['item_amount_cent']) ? (int)$item['item_amount_cent'] : 0,
  219. ];
  220. if (!empty($item['item_id'])) {
  221. $goodsItem['item_id'] = $item['item_id'];
  222. }
  223. if (!empty($item['item_name'])) {
  224. $goodsItem['item_name'] = $item['item_name'];
  225. }
  226. if (isset($item['item_size'])) {
  227. $goodsItem['item_size'] = (int)$item['item_size'];
  228. }
  229. if (isset($item['item_quantity'])) {
  230. $goodsItem['item_quantity'] = (int)$item['item_quantity'];
  231. }
  232. if (!empty($item['item_remark'])) {
  233. $goodsItem['item_remark'] = $item['item_remark'];
  234. }
  235. $goodsItemList[] = $goodsItem;
  236. }
  237. }
  238. // 构建业务数据 - 必填字段
  239. $businessData = [
  240. 'partner_order_code' => $data['partner_order_code'],
  241. 'receiver_primary_phone' => $data['receiver_primary_phone'],
  242. 'receiver_name' => $data['receiver_name'],
  243. 'receiver_latitude' => (double)($data['receiver_latitude']),
  244. 'receiver_longitude' => (double)($data['receiver_longitude']),
  245. 'receiver_address' => $data['receiver_address'],
  246. 'position_source' => $data['position_source'],
  247. 'goods_count' => (int)$data['goods_count'],
  248. 'goods_weight' => (float)$data['goods_weight'],
  249. 'goods_total_amount_cent' => $data['goods_total_amount_cent'],
  250. 'goods_actual_amount_cent' => $data['goods_actual_amount_cent'],
  251. 'goods_item_list' => $goodsItemList,
  252. 'order_type' => $data['order_type'],
  253. ];
  254. // 可选参数 - 门店信息(二选一:chain_store_id 或 out_shop_code)
  255. if (!empty($data['chain_store_id'])) {
  256. $businessData['chain_store_id'] = (int)$data['chain_store_id'];
  257. }
  258. if (!empty($data['out_shop_code'])) {
  259. $businessData['out_shop_code'] = $data['out_shop_code'];
  260. }
  261. // 可选参数 - 预下单关联(用于价格校验)
  262. if (!empty($data['pre_create_order_t_index_id'])) {
  263. $businessData['pre_create_order_t_index_id'] = $data['pre_create_order_t_index_id'];
  264. }
  265. if (isset($data['actual_delivery_amount_cent']) && $data['actual_delivery_amount_cent'] !== null) {
  266. $businessData['actual_delivery_amount_cent'] = (int)$data['actual_delivery_amount_cent'];
  267. }
  268. // 可选参数 - 取货点信息(点对点特殊场景)
  269. if (!empty($data['transport_address'])) {
  270. $businessData['transport_address'] = $data['transport_address'];
  271. }
  272. if (!empty($data['transport_name'])) {
  273. $businessData['transport_name'] = $data['transport_name'];
  274. }
  275. if (isset($data['transport_latitude']) && $data['transport_latitude'] !== '') {
  276. $businessData['transport_latitude'] = (double)$data['transport_latitude'];
  277. }
  278. if (isset($data['transport_longitude']) && $data['transport_longitude'] !== '') {
  279. $businessData['transport_longitude'] = (double)$data['transport_longitude'];
  280. }
  281. if (!empty($data['transport_tel'])) {
  282. $businessData['transport_tel'] = $data['transport_tel'];
  283. }
  284. // 可选参数 - 时间相关
  285. if (!empty($data['order_add_time'])) { // 单位:毫秒
  286. $businessData['order_add_time'] = (int)$data['order_add_time'];
  287. }
  288. if (!empty($data['require_receive_time'])) {
  289. $businessData['require_receive_time'] = $data['require_receive_time'];
  290. }
  291. // 预计出餐时间 单位:毫秒 预约单该字段必填
  292. if (!empty($data['expect_fetch_time'])) {
  293. $businessData['expect_fetch_time'] = (int)$data['expect_fetch_time'];
  294. }
  295. // 可选参数 - 订单备注和来源
  296. if (!empty($data['order_remark'])) {
  297. $businessData['order_remark'] = $data['order_remark'];
  298. }
  299. if (!empty($data['order_source'])) {
  300. $businessData['order_source'] = $data['order_source'];
  301. }
  302. if (!empty($data['order_source_order_id'])) {
  303. $businessData['order_source_order_id'] = $data['order_source_order_id'];
  304. }
  305. if (!empty($data['serial_number'])) {
  306. $businessData['serial_number'] = $data['serial_number'];
  307. }
  308. // 可选参数 - 骑手交互
  309. if (isset($data['fetch_code'])) {
  310. $businessData['fetch_code'] = (int)$data['fetch_code'];
  311. }
  312. if (isset($data['write_off_code'])) {
  313. $businessData['write_off_code'] = (int)$data['write_off_code'];
  314. }
  315. if (isset($data['cancel_code'])) {
  316. $businessData['cancel_code'] = (int)$data['cancel_code'];
  317. }
  318. if (!empty($data['customer_ext_tel'])) {
  319. $businessData['customer_ext_tel'] = $data['customer_ext_tel'];
  320. }
  321. // 可选参数 - 收货人备用信息
  322. if (!empty($data['receiver_second_phone'])) {
  323. $businessData['receiver_second_phone'] = $data['receiver_second_phone'];
  324. }
  325. // 可选参数 - 小费和优惠
  326. if (isset($data['order_tip_amount_cent'])) {
  327. $businessData['order_tip_amount_cent'] = (int)$data['order_tip_amount_cent'];
  328. }
  329. if (isset($data['use_coupon'])) {
  330. $businessData['use_coupon'] = (int)$data['use_coupon'];
  331. }
  332. if (!empty($data['warehouse_id'])) {
  333. $businessData['warehouse_id'] = (int)$data['warehouse_id'];
  334. }
  335. // 可选参数 - 商品选择
  336. // 注意:基础商品ID和服务商品ID不能传0(0有意义),应传null或不传
  337. if (isset($data['base_goods_id']) && $data['base_goods_id'] !== null && $data['base_goods_id'] != 0) {
  338. $businessData['base_goods_id'] = (int)$data['base_goods_id'];
  339. }
  340. if (isset($data['service_goods_id']) && $data['service_goods_id'] !== null && $data['service_goods_id'] != 0) {
  341. $businessData['service_goods_id'] = (int)$data['service_goods_id'];
  342. }
  343. if (!empty($data['appoint_extra_goods_ids']) && is_array($data['appoint_extra_goods_ids'])) {
  344. $businessData['appoint_extra_goods_ids'] = array_map('intval', $data['appoint_extra_goods_ids']);
  345. }
  346. // 构建请求参数
  347. $payload = $this->buildRequestPayload('createOrder', $businessData);
  348. // 发送请求
  349. $url = $this->baseUrl . '/createOrder';
  350. $resp = HttpClient::post($url, $payload, [
  351. 'Content-Type' => 'application/json',
  352. ]);
  353. Yii::info("[FengniaoAdapter] createOrder Response: " . json_encode($resp));
  354. // 处理响应
  355. if (isset($resp['code']) && $resp['code'] == '200' && isset($resp['business_data'])) {
  356. // business_data 是 JSON 字符串,需要解析
  357. $respData = is_string($resp['business_data']) ? json_decode($resp['business_data'], true) : $resp['business_data'];
  358. if (is_null($respData)) {
  359. Yii::error("[FengniaoAdapter] Failed to parse business_data: " . $resp['business_data']);
  360. return [
  361. 'code' => -1,
  362. 'msg' => 'Failed to parse response data',
  363. 'data' => null
  364. ];
  365. }
  366. if (isset($respData['order_id'])) {
  367. return [
  368. 'code' => 0,
  369. 'platform' => 'fengniao',
  370. 'data' => [
  371. 'orderId' => $respData['order_id'],
  372. 'fee' => 0, // 请从"查询订单详情"接口获取 -- order_actual_amount_cent
  373. 'distance' => 0, // 请从"查询订单详情"接口获取 -- order_distance
  374. ]
  375. ];
  376. }
  377. }
  378. return [
  379. 'code' => (int)($resp['code'] ?? -1),
  380. 'msg' => $resp['msg'] ?? 'Unknown error',
  381. 'data' => null
  382. ];
  383. }
  384. /**
  385. * 构建运费报价请求信息(供并发请求使用)
  386. *
  387. * @param array $data 报价参数
  388. * @param string $orderTime 配送时间
  389. * @return array|null 包含 url, payload, headers, timeout 的请求信息
  390. */
  391. public function buildPriceRequest($data, $orderTime)
  392. {
  393. // 订单类型为预约单时必传,如需要送达时间 – 推单时间 < 60min,则蜂鸟配送开放平台自动将订单类型置为即时单
  394. $timeDiff = strtotime($orderTime) - time();
  395. $orderType = 1;
  396. if ($timeDiff > 3600 ) {
  397. $orderType = 3;
  398. }
  399. // 构建货物明细列表
  400. $goodsItemList = [];
  401. if (!empty($data['goods_item_list']) && is_array($data['goods_item_list'])) {
  402. foreach ($data['goods_item_list'] as $item) {
  403. $goodsItem = [
  404. 'item_actual_amount_cent' => isset($item['item_actual_amount_cent']) ? (int)$item['item_actual_amount_cent'] : 0,
  405. 'item_amount_cent' => isset($item['item_amount_cent']) ? (int)$item['item_amount_cent'] : 0,
  406. ];
  407. if (!empty($item['item_id'])) {
  408. $goodsItem['item_id'] = $item['item_id'];
  409. }
  410. if (!empty($item['item_name'])) {
  411. $goodsItem['item_name'] = $item['item_name'];
  412. }
  413. if (isset($item['item_size'])) {
  414. $goodsItem['item_size'] = (int)$item['item_size'];
  415. }
  416. if (isset($item['item_quantity'])) {
  417. $goodsItem['item_quantity'] = (int)$item['item_quantity'];
  418. }
  419. if (!empty($item['item_remark'])) {
  420. $goodsItem['item_remark'] = $item['item_remark'];
  421. }
  422. $goodsItemList[] = $goodsItem;
  423. }
  424. }
  425. // 构建业务数据 - 必填字段
  426. $businessData = [
  427. 'partner_order_code' => $data['partner_order_code'],
  428. 'position_source' => isset($data['position_source']) ? (int)$data['position_source'] : 3, // 默认高德地图
  429. 'receiver_address' => $data['receiver_address'],
  430. 'receiver_longitude' => (string)($data['receiver_longitude']),
  431. 'receiver_latitude' => (string)($data['receiver_latitude']),
  432. 'goods_total_amount_cent' => $data['goods_total_amount_cent'],
  433. 'goods_actual_amount_cent' => $data['goods_actual_amount_cent'],
  434. 'goods_weight' => (float)$data['goods_weight'],
  435. 'goods_count' => (int)$data['goods_count'],
  436. 'goods_item_list' => $goodsItemList,
  437. 'order_type' => $orderType,
  438. ];
  439. // 可选参数 - 门店信息(二选一:chain_store_id 或 out_shop_code)
  440. if (!empty($data['chain_store_id'])) {
  441. $businessData['chain_store_id'] = (int)$data['chain_store_id'];
  442. }
  443. if (!empty($data['out_shop_code'])) {
  444. $businessData['out_shop_code'] = $data['out_shop_code'];
  445. }
  446. // 可选参数 - 取货点信息
  447. if (!empty($data['transport_address'])) {
  448. $businessData['transport_address'] = $data['transport_address'];
  449. }
  450. if (isset($data['transport_longitude']) && $data['transport_longitude'] !== '') {
  451. $businessData['transport_longitude'] = (double)$data['transport_longitude'];
  452. }
  453. if (isset($data['transport_latitude']) && $data['transport_latitude'] !== '') {
  454. $businessData['transport_latitude'] = (double)$data['transport_latitude'];
  455. }
  456. if (!empty($data['transport_tel'])) {
  457. $businessData['transport_tel'] = $data['transport_tel'];
  458. }
  459. // 可选参数 - 时间相关
  460. if (!empty($data['order_add_time'])) {
  461. $businessData['order_add_time'] = (int)$data['order_add_time'];
  462. }
  463. // 订单类型为预约单时必传,如需要送达时间 –--- 推单时间 < 60min,则蜂鸟配送开放平台自动将订单类型置为即时单
  464. if ($orderType == 3) {
  465. $businessData['require_receive_time'] = strtotime($orderTime)*1000;
  466. }
  467. // 可选参数 - 订单相关
  468. if (isset($data['order_tip_amount_cent'])) {
  469. $businessData['order_tip_amount_cent'] = (int)$data['order_tip_amount_cent'];
  470. }
  471. if (!empty($data['order_source'])) {
  472. $businessData['order_source'] = $data['order_source'];
  473. }
  474. if (!empty($data['order_source_order_id'])) {
  475. $businessData['order_source_order_id'] = $data['order_source_order_id'];
  476. }
  477. // 可选参数 - 优惠券及商品选择
  478. if (isset($data['use_coupon'])) {
  479. $businessData['use_coupon'] = (int)$data['use_coupon'];
  480. }
  481. // 注意:基础商品ID和服务商品ID不能传0(0有意义),应传null或不传
  482. if (isset($data['base_goods_id']) && $data['base_goods_id'] !== null && $data['base_goods_id'] != 0) {
  483. $businessData['base_goods_id'] = (int)$data['base_goods_id'];
  484. }
  485. if (isset($data['service_goods_id']) && $data['service_goods_id'] !== null && $data['service_goods_id'] != 0) {
  486. $businessData['service_goods_id'] = (int)$data['service_goods_id'];
  487. }
  488. if (!empty($data['appoint_extra_goods_ids']) && is_array($data['appoint_extra_goods_ids'])) {
  489. $businessData['appoint_extra_goods_ids'] = array_map('intval', $data['appoint_extra_goods_ids']);
  490. }
  491. // 构建请求参数
  492. $payload = $this->buildRequestPayload('preCreateOrder', $businessData);
  493. $url = $this->baseUrl . '/preCreateOrder';
  494. return [
  495. 'url' => $url,
  496. 'data' => $payload,
  497. 'headers' => ['Content-Type' => 'application/json'],
  498. 'timeout' => 5.0,
  499. ];
  500. }
  501. /**
  502. * 处理运费报价响应
  503. *
  504. * @param array $resp API 响应
  505. * @return array|null 解析后的报价结果
  506. */
  507. public function processPriceResponse($resp)
  508. {
  509. //Yii::info("[FengniaoAdapter] preCreateOrder Response: " . json_encode($resp));
  510. // 处理响应
  511. if (isset($resp['code']) && $resp['code'] == '200' && isset($resp['business_data'])) {
  512. // business_data 是 JSON 字符串,需要解析
  513. $respData = is_string($resp['business_data']) ? json_decode($resp['business_data'], true) : $resp['business_data'];
  514. if (is_null($respData)) {
  515. Yii::error("[FengniaoAdapter] Failed to parse business_data: " . $resp['business_data']);
  516. return null;
  517. }
  518. // 解析商品信息列表
  519. $goodsInfos = [];
  520. if (!empty($respData['goods_infos']) && is_array($respData['goods_infos'])) {
  521. foreach ($respData['goods_infos'] as $goodsInfo) {
  522. // 解析价格明细
  523. $priceDetail = [];
  524. if (!empty($goodsInfo['price_detail']) && is_array($goodsInfo['price_detail'])) {
  525. $pd = $goodsInfo['price_detail'];
  526. $priceDetail = [
  527. 'start_price_cent' => (int)($pd['start_price_cent'] ?? 0),
  528. 'distance_price_cent' => (int)($pd['distance_price_cent'] ?? 0),
  529. 'weight_price_cent' => (int)($pd['weight_price_cent'] ?? 0),
  530. 'pressure_surcharge_cent' => (int)($pd['pressure_surcharge_cent'] ?? 0),
  531. 'time_period_surcharge_cent' => (int)($pd['time_period_surcharge_cent'] ?? 0),
  532. 'river_crossing_surcharge_cent' => (int)($pd['river_crossing_surcharge_cent'] ?? 0),
  533. 'category_surcharge_cent' => (int)($pd['category_surcharge_cent'] ?? 0),
  534. 'temporary_surcharge_cent' => (int)($pd['temporary_surcharge_cent'] ?? 0),
  535. 'order_price_surcharge_cent' => (int)($pd['order_price_surcharge_cent'] ?? 0),
  536. ];
  537. }
  538. $goodsInfos[] = [
  539. 'base_goods_id' => (int)($goodsInfo['base_goods_id'] ?? 0),
  540. 'service_goods_id' => (int)($goodsInfo['service_goods_id'] ?? 0),
  541. 't_index_id' => $goodsInfo['t_index_id'] ?? '',
  542. 'is_valid' => (int)($goodsInfo['is_valid'] ?? 0),
  543. 'disable_reason' => $goodsInfo['disable_reason'] ?? '',
  544. 'slogan' => $goodsInfo['slogan'] ?? '',
  545. 'actual_delivery_amount_cent' => (int)($goodsInfo['actual_delivery_amount_cent'] ?? 0),
  546. 'total_delivery_amount_cent' => (int)($goodsInfo['total_delivery_amount_cent'] ?? 0),
  547. 'predict_delivery_minutes' => (int)($goodsInfo['predict_delivery_minutes'] ?? 0),
  548. 'predict_delivery_time' => (int)($goodsInfo['predict_delivery_time'] ?? 0),
  549. 'can_add_tip' => (int)($goodsInfo['can_add_tip'] ?? 0),
  550. 'warehouse_id' => $goodsInfo['warehouse_id'] ?? null,
  551. 'price_detail' => $priceDetail,
  552. ];
  553. }
  554. }
  555. return [
  556. 'platform' => 'fengniao',
  557. 'distance' => (int)($respData['distance'] ?? 0),
  558. 'time' => (int)($respData['time'] ?? 0),
  559. 'goods_infos' => $goodsInfos,
  560. ];
  561. }
  562. Yii::error('fengniao 报价失败:'.json_encode($resp));
  563. return [
  564. 'platform' => 'fengniao',
  565. 'code' => $resp['code'],
  566. 'error' => $resp['msg']
  567. ];
  568. }
  569. /**
  570. * @deprecated
  571. * 获取运费报价(预下单接口)
  572. *
  573. * 根据蜂鸟官方文档实现报价功能,返回所有可选的商品列表
  574. * 每项包含当前使用该商品下单时对应的价格等信息
  575. *
  576. * @param array $data 报价参数
  577. * @param string $orderTime 配送时间
  578. * @return array|null 包含可用商品列表及报价信息的结果
  579. * @deprecated 使用 buildPriceRequest 和 processPriceResponse 替代
  580. */
  581. public function getPrice($data, $orderTime)
  582. {
  583. // $request = $this->buildPriceRequest($data, $orderTime);
  584. // $resp = HttpClient::post($request['url'], $request['data'], $request['headers']);
  585. // return $this->processPriceResponse($resp);
  586. }
  587. /**
  588. * 获取可用订单取消原因列表
  589. *
  590. * 根据蜂鸟官方文档获取该订单可用的取消原因列表。
  591. * 用户取消订单时需先调用此接口获取可选的取消原因。
  592. *
  593. * @param array $data 查询参数,结构参考:
  594. * [
  595. * // ========== 必填参数(二选一)==========
  596. * 'order_id' => '订单ID',
  597. * 'partner_order_code' => '外部订单号',
  598. * // 注意:order_id 和 partner_order_code 必填一个
  599. * ]
  600. * @return array API响应结果
  601. * 成功:[
  602. * 'code' => 0,
  603. * 'data' => [
  604. * 'cancel_reason_list' => [
  605. * [
  606. * 'order_cancel_code' => 取消原因code,
  607. * 'order_cancel_desc' => '取消原因描述',
  608. * ],
  609. * // ... 更多原因
  610. * ]
  611. * ]
  612. * ]
  613. * 失败:['code' => 错误码, 'msg' => 错误信息, 'data' => null]
  614. */
  615. public function getCancelReasonList($data)
  616. {
  617. // 验证 order_id 和 partner_order_code 必填一个
  618. $orderId = $data['order_id'] ?? $data;
  619. $partnerOrderCode = null;
  620. // 如果 $orderId 是 XSD 开头,则给 partnerOrderCode 赋值为 $orderId
  621. if (strpos($orderId, 'XSD') === 0) {
  622. $partnerOrderCode = $orderId;
  623. $orderId = null;
  624. }
  625. if (empty($orderId) && empty($partnerOrderCode)) {
  626. Yii::warning("[FengniaoAdapter] getCancelReasonList failed: order_id and partner_order_code cannot both be empty");
  627. return [
  628. 'code' => -1,
  629. 'msg' => 'order_id and partner_order_code must have one',
  630. 'data' => null
  631. ];
  632. }
  633. // 构建业务数据
  634. $businessData = [];
  635. // 订单标识(二选一)
  636. if (!empty($orderId)) {
  637. $businessData['order_id'] = (int)$orderId;
  638. }
  639. if (!empty($partnerOrderCode)) {
  640. $businessData['partner_order_code'] = $partnerOrderCode;
  641. }
  642. // 构建请求参数
  643. $payload = $this->buildRequestPayload('getCancelReasonList', $businessData);
  644. // 发送请求
  645. $url = $this->baseUrl . '/getCancelReasonList';
  646. $resp = HttpClient::post($url, $payload, [
  647. 'Content-Type' => 'application/json',
  648. ]);
  649. //Yii::info("[FengniaoAdapter] getCancelReasonList Response: " . json_encode($resp));
  650. // 处理响应
  651. if (isset($resp['code']) && $resp['code'] == '200' && isset($resp['business_data'])) {
  652. // business_data 是 JSON 字符串,需要解析
  653. $respData = is_string($resp['business_data']) ? json_decode($resp['business_data'], true) : $resp['business_data'];
  654. if (is_null($respData)) {
  655. Yii::error("[FengniaoAdapter] Failed to parse business_data: " . $resp['business_data']);
  656. return [
  657. 'code' => -1,
  658. 'msg' => 'Failed to parse response data',
  659. 'data' => null
  660. ];
  661. }
  662. // 解析取消原因列表
  663. $cancelReasonList = [];
  664. if (!empty($respData['cancel_reason_list']) && is_array($respData['cancel_reason_list'])) {
  665. foreach ($respData['cancel_reason_list'] as $reason) {
  666. $cancelReasonList[] = [
  667. 'order_cancel_code' => (int)($reason['order_cancel_code'] ?? 0),
  668. 'order_cancel_desc' => $reason['order_cancel_desc'] ?? '',
  669. ];
  670. }
  671. }
  672. return [
  673. 'code' => 0,
  674. 'data' => [
  675. 'reasonList' => $cancelReasonList,
  676. ]
  677. ];
  678. }
  679. return [
  680. 'code' => (int)($resp['code'] ?? -1),
  681. 'msg' => $resp['msg'] ?? 'Unknown error',
  682. 'data' => null
  683. ];
  684. }
  685. /**
  686. * 订单取消
  687. *
  688. * 根据蜂鸟官方文档实现订单取消功能。
  689. * 支持商户取消和用户取消两种场景。
  690. *
  691. * @param array $data 取消参数,结构参考:
  692. * [
  693. * // ========== 必填参数(二选一)==========
  694. * 'order_id' => '订单ID',
  695. * 'partner_order_code' => '外部订单号',
  696. * // 注意:order_id 和 partner_order_code 必填一个
  697. *
  698. * // ========== 必填参数 ==========
  699. * 'order_cancel_role' => '取消的角色(1:商户取消, 2:用户取消)',
  700. * 'order_cancel_code' => '取消原因code',
  701. * 'order_cancel_other_reason' => '取消原因补充(20字以内)',
  702. *
  703. * // ========== 可选参数 ==========
  704. * 'need_reverse_order' => '是否期望发起售中逆向(Boolean)',
  705. * 'actual_cancel_cost_cent' => '取消价格(分)',
  706. * ]
  707. * @return array API响应结果
  708. * 成功:['code' => 0, 'data' => ['result' => true]]
  709. * 失败:['code' => 错误码, 'msg' => 错误信息, 'data' => null]
  710. */
  711. public function cancelOrder($orderId, $params)
  712. {
  713. // 验证 order_id 和 partner_order_code 必填一个
  714. $partnerOrderCode = null;
  715. // 如果 $orderId 是 XSD_ 开头,则给 partnerOrderCode 赋值为 $orderId
  716. if (strpos($orderId, 'XSD') === 0) {
  717. $partnerOrderCode = $orderId;
  718. $orderId = null;
  719. }
  720. if (empty($orderId) && empty($partnerOrderCode)) {
  721. Yii::warning("[FengniaoAdapter] cancelOrder failed: order_id and partner_order_code cannot both be empty");
  722. return [
  723. 'code' => -1,
  724. 'msg' => 'order_id and partner_order_code must have one',
  725. 'data' => null
  726. ];
  727. }
  728. // 构建业务数据
  729. $businessData = [];
  730. // 必填参数 - 订单标识(二选一)
  731. if (!empty($orderId)) {
  732. $businessData['order_id'] = $orderId;
  733. }
  734. if (!empty($partnerOrderCode)) {
  735. $businessData['partner_order_code'] = $partnerOrderCode;
  736. }
  737. // 必填参数 - 取消角色
  738. if (isset($params['order_cancel_role'])) {
  739. $businessData['order_cancel_role'] = (int)$params['order_cancel_role'];
  740. } else {
  741. Yii::warning("[FengniaoAdapter] cancelOrder failed: order_cancel_role is required");
  742. return [
  743. 'code' => -1,
  744. 'msg' => 'order_cancel_role is required (1:商户取消, 2:用户取消)',
  745. 'data' => null
  746. ];
  747. }
  748. // 必填参数 - 取消原因code
  749. if (isset($params['order_cancel_code'])) {
  750. $businessData['order_cancel_code'] = (int)$params['order_cancel_code'];
  751. } else {
  752. Yii::warning("[FengniaoAdapter] cancelOrder failed: order_cancel_code is required");
  753. return [
  754. 'code' => -1,
  755. 'msg' => 'order_cancel_code is required',
  756. 'data' => null
  757. ];
  758. }
  759. // 必填参数 - 取消原因补充
  760. if (!empty($params['order_cancel_reason'])) {
  761. $businessData['order_cancel_other_reason'] = $params['order_cancel_reason'];
  762. } else {
  763. Yii::warning("[FengniaoAdapter] cancelOrder failed: order_cancel_other_reason is required");
  764. return [
  765. 'code' => -1,
  766. 'msg' => 'order_cancel_other_reason is required (20字以内)',
  767. 'data' => null
  768. ];
  769. }
  770. // 可选参数 - 是否期望发起售中逆向
  771. if (isset($params['need_reverse_order']) && $params['need_reverse_order'] !== null) {
  772. $businessData['need_reverse_order'] = (bool)$params['need_reverse_order'];
  773. }
  774. // 可选参数 - 取消价格
  775. if (isset($params['actual_cancel_cost_cent']) && $params['actual_cancel_cost_cent'] !== null) {
  776. $businessData['actual_cancel_cost_cent'] = (int)$params['actual_cancel_cost_cent'];
  777. }
  778. // 构建请求参数
  779. $payload = $this->buildRequestPayload('cancelOrder', $businessData);
  780. // 发送请求
  781. $url = $this->baseUrl . '/cancelOrder';
  782. $resp = HttpClient::post($url, $payload, [
  783. 'Content-Type' => 'application/json',
  784. ]);
  785. Yii::info("[FengniaoAdapter] cancelOrder Response: " . json_encode($resp));
  786. // 处理响应
  787. if (isset($resp['code']) && $resp['code'] == '200' && isset($resp['business_data'])) {
  788. // business_data 是 JSON 字符串,需要解析
  789. $respData = is_string($resp['business_data']) ? json_decode($resp['business_data'], true) : $resp['business_data'];
  790. if (is_null($respData)) {
  791. Yii::error("[FengniaoAdapter] Failed to parse business_data: " . $resp['business_data']);
  792. return [
  793. 'code' => -1,
  794. 'platform' => 'fengniao',
  795. 'msg' => 'Failed to parse response data',
  796. 'data' => null
  797. ];
  798. }
  799. return [
  800. 'code' => 0,
  801. 'platform' => 'fengniao',
  802. 'data' => [
  803. 'result' => (bool)($respData['result'] ?? false),
  804. 'reverse_order_ok' => (bool)($respData['reverse_order_ok'] ?? false),
  805. ]
  806. ];
  807. }
  808. return [
  809. 'code' => (int)($resp['code'] ?? -1),
  810. 'platform' => 'fengniao',
  811. 'msg' => $resp['msg'] ?? 'Unknown error',
  812. 'data' => null
  813. ];
  814. }
  815. /**
  816. * 订单预取消接口
  817. *
  818. * 根据蜂鸟官方文档获取取消价格。
  819. * 需要查询并校验取消价格时使用,若强校验价格需在正式取消接口中把出参价格传过去。
  820. *
  821. * @param array $data 预取消查询参数,结构参考:
  822. * [
  823. * // ========== 必填参数(二选一)==========
  824. * 'order_id' => '订单ID',
  825. * 'partner_order_code' => '外部订单号',
  826. * // 注意:order_id 和 partner_order_code 必填一个
  827. *
  828. * // ========== 必填参数 ==========
  829. * 'order_cancel_code' => '取消原因code(从可用取消原因列表接口返回结果选择)',
  830. * ]
  831. * @return array API响应结果
  832. * 成功:[
  833. * 'code' => 0,
  834. * 'data' => [
  835. * 'actual_cancel_cost_cent' => '取消实际需金额(分,默认使用已有免责取消权益使用后金额为0)',
  836. * ]
  837. * ]
  838. * 失败:['code' => 错误码, 'msg' => 错误信息, 'data' => null]
  839. *
  840. * @note
  841. * - 获取取消价格,用于查询并校验取消价格
  842. * - actual_cancel_cost_cent 为取消实际需要支付的金额(分),通常为 0(使用免责取消权益)
  843. * - 若需强校验价格,应在正式取消接口(cancelOrder)中把此出参价格传过去
  844. * - 流程:先调用此方法获取取消价格 → 校验价格 → 调用 cancelOrder 正式取消
  845. */
  846. public function preCancelOrder($data)
  847. {
  848. // 验证 order_id 和 partner_order_code 必填一个
  849. $orderId = $data['order_id'];
  850. $partnerOrderCode = null;
  851. // 如果 $orderId 是 XSD_ 开头,则给 partnerOrderCode 赋值为 $orderId
  852. if (strpos($orderId, 'XSD') === 0) {
  853. $partnerOrderCode = $orderId;
  854. $orderId = null;
  855. }
  856. if (empty($orderId) && empty($partnerOrderCode)) {
  857. Yii::warning("[FengniaoAdapter] preCancelOrder failed: order_id and partner_order_code cannot both be empty");
  858. return [
  859. 'code' => -1,
  860. 'msg' => 'order_id and partner_order_code must have one',
  861. 'data' => null
  862. ];
  863. }
  864. // 验证 order_cancel_code(取消原因code)必填
  865. if (!isset($data['order_cancel_code'])) {
  866. Yii::warning("[FengniaoAdapter] preCancelOrder failed: order_cancel_code is required");
  867. return [
  868. 'code' => -1,
  869. 'msg' => 'order_cancel_code is required (from getCancelReasonList)',
  870. 'data' => null
  871. ];
  872. }
  873. // 构建业务数据
  874. $businessData = [];
  875. // 订单标识(二选一)
  876. if (!empty($orderId)) {
  877. $businessData['order_id'] = (int)$orderId;
  878. }
  879. if (!empty($partnerOrderCode)) {
  880. $businessData['partner_order_code'] = $partnerOrderCode;
  881. }
  882. // 必填参数 - 取消原因code
  883. $businessData['order_cancel_code'] = (int)$data['order_cancel_code'];
  884. // 构建请求参数
  885. $payload = $this->buildRequestPayload('preCancelOrder', $businessData);
  886. // 发送请求
  887. $url = $this->baseUrl . '/preCancelOrder';
  888. $resp = HttpClient::post($url, $payload, [
  889. 'Content-Type' => 'application/json',
  890. ]);
  891. Yii::info("[FengniaoAdapter] preCancelOrder Response: " . json_encode($resp));
  892. // 处理响应
  893. if (isset($resp['code']) && $resp['code'] == '200' && isset($resp['business_data'])) {
  894. // business_data 是 JSON 字符串,需要解析
  895. $respData = is_string($resp['business_data']) ? json_decode($resp['business_data'], true) : $resp['business_data'];
  896. if (is_null($respData)) {
  897. Yii::error("[FengniaoAdapter] Failed to parse business_data: " . $resp['business_data']);
  898. return [
  899. 'code' => -1,
  900. 'msg' => 'Failed to parse response data',
  901. 'data' => null
  902. ];
  903. }
  904. return [
  905. 'code' => 0,
  906. 'data' => [
  907. 'actual_cancel_cost_cent' => (int)($respData['actual_cancel_cost_cent'] ?? 0),
  908. ]
  909. ];
  910. }
  911. return [
  912. 'code' => (int)($resp['code'] ?? -1),
  913. 'msg' => $resp['msg'] ?? 'Unknown error',
  914. 'data' => null
  915. ];
  916. }
  917. /**
  918. * 追加小费
  919. *
  920. * 根据蜂鸟官方文档为订单追加小费。
  921. * 订单小费总金额为每次累加。
  922. *
  923. * @param array $data 加小费参数,结构参考:
  924. * [
  925. * // ========== 必填参数(二选一)==========
  926. * 'order_id' => '订单ID',
  927. * 'partner_order_code' => '外部订单号',
  928. * // 注意:order_id 和 partner_order_code 必填一个
  929. *
  930. * // ========== 必填参数 ==========
  931. * 'third_index_id' => '本次加小费唯一标识(Long类型)',
  932. * 'add_tip_amount_cent' => '加小费金额(分)',
  933. * ]
  934. * @return array API响应结果
  935. * 成功:['code' => 0, 'data' => ['result' => true]]
  936. * 失败:['code' => 错误码, 'msg' => 错误信息, 'data' => null]
  937. *
  938. * @note
  939. * - 追加小费,订单小费总金额为每次累加
  940. * - 不是所有订单都支持加小费,受合同服务 & 接单运力 & 当时订单状态等多个因素影响
  941. * - 订单详情接口有返回是否可加小费字段(if_can_add_tip)
  942. * - 联调环境使用加小费接口需先使用联调工具把订单状态改为1,否则订单状态会不支持加小费
  943. * - third_index_id用于幂等,相同third_index_id的后续请求会被忽略
  944. */
  945. public function addTip($data = [])
  946. {
  947. // 验证 order_id 和 partner_order_code 必填一个
  948. $orderId = $data['orderId'] ?? null;
  949. $partnerOrderCode = $data['partner_order_code'] ?? null;
  950. $tips = $data['tips'] ?? null;
  951. if($tips != null){
  952. $data['add_tip_amount_cent'] = $tips;
  953. }
  954. if (empty($orderId) && empty($partnerOrderCode)) {
  955. Yii::warning("[FengniaoAdapter] addTip failed: order_id and partner_order_code cannot both be empty");
  956. return [
  957. 'code' => -1,
  958. 'msg' => 'order_id and partner_order_code must have one',
  959. 'data' => null
  960. ];
  961. }
  962. // 验证 third_index_id(幂等标识)
  963. if (!isset($data['third_index_id'])) {
  964. Yii::warning("[FengniaoAdapter] addTip failed: third_index_id is required");
  965. return [
  966. 'code' => -1,
  967. 'msg' => 'third_index_id is required for idempotency',
  968. 'data' => null
  969. ];
  970. }
  971. // 验证 add_tip_amount_cent(加小费金额)
  972. if (!isset($data['add_tip_amount_cent']) || $data['add_tip_amount_cent'] <= 0) {
  973. Yii::warning("[FengniaoAdapter] addTip failed: add_tip_amount_cent must be greater than 0");
  974. return [
  975. 'code' => -1,
  976. 'msg' => 'add_tip_amount_cent is required and must be greater than 0',
  977. 'data' => null
  978. ];
  979. }
  980. // 构建业务数据
  981. $businessData = [];
  982. // 订单标识(二选一)
  983. if (!empty($orderId)) {
  984. $businessData['order_id'] = (int)$orderId;
  985. }
  986. if (!empty($partnerOrderCode)) {
  987. $businessData['partner_order_code'] = $partnerOrderCode;
  988. }
  989. // 必填参数
  990. $businessData['third_index_id'] = (int)$data['third_index_id'];
  991. $businessData['add_tip_amount_cent'] = (int)$data['add_tip_amount_cent'];
  992. // 构建请求参数
  993. $payload = $this->buildRequestPayload('addTip', $businessData);
  994. // 发送请求
  995. $url = $this->baseUrl . '/addTip';
  996. $resp = HttpClient::post($url, $payload, [
  997. 'Content-Type' => 'application/json',
  998. ]);
  999. Yii::info("[FengniaoAdapter] addTip Response: " . json_encode($resp));
  1000. // 处理响应
  1001. if (isset($resp['code']) && $resp['code'] == '200' && isset($resp['business_data'])) {
  1002. // business_data 是 JSON 字符串,需要解析
  1003. $respData = is_string($resp['business_data']) ? json_decode($resp['business_data'], true) : $resp['business_data'];
  1004. if (is_null($respData)) {
  1005. Yii::error("[FengniaoAdapter] Failed to parse business_data: " . $resp['business_data']);
  1006. return [
  1007. 'code' => -1,
  1008. 'msg' => 'Failed to parse response data',
  1009. 'data' => null
  1010. ];
  1011. }
  1012. return [
  1013. 'code' => 0,
  1014. 'data' => [
  1015. 'result' => (bool)($respData['result'] ?? false),
  1016. ]
  1017. ];
  1018. }
  1019. return [
  1020. 'code' => (int)($resp['code'] ?? -1),
  1021. 'msg' => $resp['msg'] ?? 'Unknown error',
  1022. 'data' => null
  1023. ];
  1024. }
  1025. /**
  1026. * 查询订单详情
  1027. *
  1028. * 根据蜂鸟官方文档获取订单的详细信息。
  1029. * 包括订单状态、配送员信息、事件节点、价格明细等完整信息。
  1030. *
  1031. * @param array $data 查询参数,结构参考:
  1032. * [
  1033. * // ========== 必填参数(二选一)==========
  1034. * 'order_id' => '订单ID',
  1035. * 'partner_order_code' => '外部订单号',
  1036. * // 注意:order_id 和 partner_order_code 必填一个
  1037. * ]
  1038. * @return array API响应结果
  1039. */
  1040. //public function getOrderDetail($data = [])
  1041. public function selectOrder($data)
  1042. {
  1043. // 验证 order_id 和 partner_order_code 必填一个
  1044. $orderId = $data['order_id'] ?? $data;
  1045. $partnerOrderCode = null;
  1046. // 如果 $orderId 是 XSD 开头,则给 partnerOrderCode 赋值为 $orderId
  1047. if (strpos($orderId, 'XSD') === 0) {
  1048. $partnerOrderCode = $orderId;
  1049. $orderId = null;
  1050. }
  1051. if (empty($orderId) && empty($partnerOrderCode)) {
  1052. Yii::warning("[FengniaoAdapter] getOrderDetail failed: order_id and partner_order_code cannot both be empty");
  1053. return [
  1054. 'code' => -1,
  1055. 'msg' => 'order_id and partner_order_code must have one',
  1056. 'data' => null
  1057. ];
  1058. }
  1059. // 构建业务数据
  1060. $businessData = [];
  1061. // 订单标识(二选一)
  1062. if (!empty($orderId)) {
  1063. $businessData['order_id'] = $orderId;
  1064. }
  1065. if (!empty($partnerOrderCode)) {
  1066. $businessData['partner_order_code'] = $partnerOrderCode;
  1067. }
  1068. // 构建请求参数
  1069. $payload = $this->buildRequestPayload('getOrderDetail', $businessData);
  1070. // 发送请求
  1071. $url = $this->baseUrl . '/getOrderDetail';
  1072. $resp = HttpClient::post($url, $payload, [
  1073. 'Content-Type' => 'application/json',
  1074. ]);
  1075. Yii::info("[FengniaoAdapter] getOrderDetail Response: " . json_encode($resp));
  1076. // 处理响应
  1077. if (isset($resp['code']) && $resp['code'] == '200' && isset($resp['business_data'])) {
  1078. // business_data 是 JSON 字符串,需要解析
  1079. $respData = is_string($resp['business_data']) ? json_decode($resp['business_data'], true) : $resp['business_data'];
  1080. if (is_null($respData)) {
  1081. Yii::error("[FengniaoAdapter] Failed to parse business_data: " . $resp['business_data']);
  1082. return [
  1083. 'code' => -1,
  1084. 'msg' => 'Failed to parse response data',
  1085. 'data' => null
  1086. ];
  1087. }
  1088. // 解析价格明细
  1089. $priceDetail = [];
  1090. if (!empty($respData['price_detail']) && is_array($respData['price_detail'])) {
  1091. $pd = $respData['price_detail'];
  1092. $priceDetail = [
  1093. 'start_price_cent' => (int)($pd['start_price_cent'] ?? 0),
  1094. 'distance_price_cent' => (int)($pd['distance_price_cent'] ?? 0),
  1095. 'weight_price_cent' => (int)($pd['weight_price_cent'] ?? 0),
  1096. 'pressure_surcharge_cent' => (int)($pd['pressure_surcharge_cent'] ?? 0),
  1097. 'time_period_surcharge_cent' => (int)($pd['time_period_surcharge_cent'] ?? 0),
  1098. 'river_crossing_surcharge_cent' => (int)($pd['river_crossing_surcharge_cent'] ?? 0),
  1099. 'category_surcharge_cent' => (int)($pd['category_surcharge_cent'] ?? 0),
  1100. 'temporary_surcharge_cent' => (int)($pd['temporary_surcharge_cent'] ?? 0),
  1101. 'order_price_surcharge_cent' => (int)($pd['order_price_surcharge_cent'] ?? 0),
  1102. ];
  1103. }
  1104. // 解析事件日志列表
  1105. $eventLogDetails = [];
  1106. if (!empty($respData['event_log_details']) && is_array($respData['event_log_details'])) {
  1107. foreach ($respData['event_log_details'] as $log) {
  1108. $eventLogDetails[] = [
  1109. 'order_status' => (int)($log['order_status'] ?? 0),
  1110. 'occur_time' => (int)($log['occur_time'] ?? 0),
  1111. 'carrier_driver_name' => $log['carrier_driver_name'] ?? '',
  1112. 'carrier_driver_phone' => $log['carrier_driver_phone'] ?? '',
  1113. ];
  1114. }
  1115. }
  1116. // 解析逆向单运单事件节点
  1117. $reverseEventLogDetails = [];
  1118. if (!empty($respData['reverse_event_log_details']) && is_array($respData['reverse_event_log_details'])) {
  1119. $reverse = $respData['reverse_event_log_details'];
  1120. $reverseEventLogDetails = [
  1121. 'order_status' => (int)($reverse['order_status'] ?? 0),
  1122. 'occur_time' => (int)($reverse['occur_time'] ?? 0),
  1123. 'carrier_driver_name' => $reverse['carrier_driver_name'] ?? '',
  1124. 'carrier_driver_phone' => $reverse['carrier_driver_phone'] ?? '',
  1125. ];
  1126. }
  1127. // 解析天气信息
  1128. $weatherInfo = [];
  1129. if (!empty($respData['weather_info']) && is_array($respData['weather_info'])) {
  1130. $weather = $respData['weather_info'];
  1131. $weatherInfo = [
  1132. 'weatherRate' => (int)($weather['weatherRate'] ?? 0),
  1133. 'weatherDesc' => $weather['weatherDesc'] ?? '',
  1134. ];
  1135. }
  1136. // 解析取货照片列表
  1137. $deliveryFetchPhotos = [];
  1138. if (!empty($respData['delivery_fetch_photos']) && is_array($respData['delivery_fetch_photos'])) {
  1139. $deliveryFetchPhotos = $respData['delivery_fetch_photos'];
  1140. }
  1141. return [
  1142. 'code' => 0,
  1143. 'data' => [
  1144. // 基本信息
  1145. 'order_id' => (int)($respData['order_id'] ?? 0),
  1146. 'partner_order_code' => $respData['partner_order_code'] ?? '',
  1147. 'tracking_id' => (int)($respData['tracking_id'] ?? 0),
  1148. 'order_status' => (int)($respData['order_status'] ?? 0),
  1149. 'serial_number' => $respData['serial_number'] ?? '',
  1150. 'remark' => $respData['remark'] ?? '',
  1151. // 配送员信息
  1152. 'carrier_driver_id' => $respData['carrier_driver_id'] ?? '',
  1153. 'carrier_driver_name' => $respData['carrier_driver_name'] ?? '',
  1154. 'carrier_driver_phone' => $respData['carrier_driver_phone'] ?? '',
  1155. 'temperature' => $respData['temperature'] ?? '',
  1156. // 金额信息(单位:分)
  1157. 'order_total_amount_cent' => (int)($respData['order_total_amount_cent'] ?? 0),
  1158. 'order_actual_amount_cent' => (int)($respData['order_actual_amount_cent'] ?? 0),
  1159. 'order_tip_amount_cent' => (int)($respData['order_tip_amount_cent'] ?? 0),
  1160. 'overtime_compensation_cost_cent' => (int)($respData['overtime_compensation_cost_cent'] ?? 0),
  1161. // 配送信息
  1162. 'order_distance' => (double)($respData['order_distance'] ?? 0),
  1163. 'estimate_arrive_time' => (int)($respData['estimate_arrive_time'] ?? 0),
  1164. 'if_can_add_tip' => (int)($respData['if_can_add_tip'] ?? 0),
  1165. 'fetch_code' => $respData['fetch_code'] ?? '',
  1166. 'write_off_code' => $respData['write_off_code'] ?? '',
  1167. 'cancel_code' => $respData['cancel_code'] ?? '',
  1168. // 价格明细
  1169. 'price_detail' => $priceDetail,
  1170. // 事件日志
  1171. 'event_log_details' => $eventLogDetails,
  1172. 'reverse_event_log_details' => $reverseEventLogDetails,
  1173. // 异常信息
  1174. 'abnormal_code' => $respData['abnormal_code'] ?? '',
  1175. 'abnormal_desc' => $respData['abnormal_desc'] ?? '',
  1176. // 索赔信息
  1177. 'claim_id' => (int)($respData['claim_id'] ?? 0),
  1178. 'claim_status' => (int)($respData['claim_status'] ?? 0),
  1179. 'claim_status_desc' => $respData['claim_status_desc'] ?? '',
  1180. 'claim_reason_desc' => $respData['claim_reason_desc'] ?? '',
  1181. // 投诉信息
  1182. 'complaint_id' => (int)($respData['complaint_id'] ?? 0),
  1183. 'complaint_status' => (int)($respData['complaint_status'] ?? 0),
  1184. 'complaint_response_desc' => $respData['complaint_response_desc'] ?? '',
  1185. 'complaint_reason_desc' => $respData['complaint_reason_desc'] ?? '',
  1186. // 其他
  1187. 'delivery_fetch_photos' => $deliveryFetchPhotos,
  1188. 'weather_info' => $weatherInfo,
  1189. ]
  1190. ];
  1191. }
  1192. return [
  1193. 'code' => (int)($resp['code'] ?? -1),
  1194. 'msg' => $resp['msg'] ?? 'Unknown error',
  1195. 'data' => null
  1196. ];
  1197. }
  1198. /**
  1199. * 查询骑手信息
  1200. *
  1201. * 根据蜂鸟官方文档获取订单对应的骑手(配送员)的实时位置和联系信息。
  1202. * 返回骑手的ID、姓名、电话以及实时位置坐标(高德地图坐标系)。
  1203. *
  1204. * @param array $data 查询参数,结构参考:
  1205. * [
  1206. * // ========== 必填参数(二选一)==========
  1207. * 'order_id' => '订单ID',
  1208. * 'partner_order_code' => '外部订单号',
  1209. * // 注意:order_id 和 partner_order_code 必填一个
  1210. * ]
  1211. * @return array API响应结果
  1212. * 成功:[
  1213. * 'code' => 0,
  1214. * 'data' => [
  1215. * 'carrier_driver_id' => '骑手ID',
  1216. * 'carrier_driver_name' => '骑手姓名',
  1217. * 'carrier_driver_phone' => '骑手电话',
  1218. * 'carrier_driver_latitude' => '骑手位置纬度',
  1219. * 'carrier_driver_longitude' => '骑手位置经度',
  1220. * // 返回的是高德地图的经纬度
  1221. * ]
  1222. * ]
  1223. * 失败:['code' => 错误码, 'msg' => 错误信息, 'data' => null]
  1224. */
  1225. public function getKnightInfo($data = [])
  1226. {
  1227. // 验证 order_id 和 partner_order_code 必填一个
  1228. $orderId = $data['order_id'] ?? null;
  1229. $partnerOrderCode = $data['partner_order_code'] ?? null;
  1230. if (empty($orderId) && empty($partnerOrderCode)) {
  1231. Yii::warning("[FengniaoAdapter] getKnightInfo failed: order_id and partner_order_code cannot both be empty");
  1232. return [
  1233. 'code' => -1,
  1234. 'msg' => 'order_id and partner_order_code must have one',
  1235. 'data' => null
  1236. ];
  1237. }
  1238. // 构建业务数据
  1239. $businessData = [];
  1240. // 订单标识(二选一)
  1241. if (!empty($orderId)) {
  1242. $businessData['order_id'] = (int)$orderId;
  1243. }
  1244. if (!empty($partnerOrderCode)) {
  1245. $businessData['partner_order_code'] = $partnerOrderCode;
  1246. }
  1247. // 构建请求参数
  1248. $payload = $this->buildRequestPayload('getKnightInfo', $businessData);
  1249. // 发送请求
  1250. $url = $this->baseUrl . '/getKnightInfo';
  1251. $resp = HttpClient::post($url, $payload, [
  1252. 'Content-Type' => 'application/json',
  1253. ]);
  1254. Yii::info("[FengniaoAdapter] getKnightInfo Response: " . json_encode($resp));
  1255. // 处理响应
  1256. if (isset($resp['code']) && $resp['code'] == '200' && isset($resp['business_data'])) {
  1257. // business_data 是 JSON 字符串,需要解析
  1258. $respData = is_string($resp['business_data']) ? json_decode($resp['business_data'], true) : $resp['business_data'];
  1259. if (is_null($respData)) {
  1260. Yii::error("[FengniaoAdapter] Failed to parse business_data: " . $resp['business_data']);
  1261. return [
  1262. 'code' => -1,
  1263. 'msg' => 'Failed to parse response data',
  1264. 'data' => null
  1265. ];
  1266. }
  1267. return [
  1268. 'code' => 0,
  1269. 'data' => [
  1270. 'carrier_driver_id' => $respData['carrier_driver_id'] ?? '',
  1271. 'carrier_driver_name' => $respData['carrier_driver_name'] ?? '',
  1272. 'carrier_driver_phone' => $respData['carrier_driver_phone'] ?? '',
  1273. 'carrier_driver_latitude' => $respData['carrier_driver_latitude'] ?? '',
  1274. 'carrier_driver_longitude' => $respData['carrier_driver_longitude'] ?? '',
  1275. ]
  1276. ];
  1277. }
  1278. return [
  1279. 'code' => (int)($resp['code'] ?? -1),
  1280. 'msg' => $resp['msg'] ?? 'Unknown error',
  1281. 'data' => null
  1282. ];
  1283. }
  1284. }