FengniaoAdapter.php 60 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419
  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. $data['third_index_id'] = time();
  966. // return [
  967. // 'code' => -1,
  968. // 'msg' => 'third_index_id is required for idempotency',
  969. // 'data' => null
  970. // ];
  971. }
  972. // 验证 add_tip_amount_cent(加小费金额)
  973. if (!isset($data['add_tip_amount_cent']) || $data['add_tip_amount_cent'] <= 0) {
  974. Yii::warning("[FengniaoAdapter] addTip failed: add_tip_amount_cent must be greater than 0");
  975. return [
  976. 'code' => -1,
  977. 'msg' => 'add_tip_amount_cent is required and must be greater than 0',
  978. 'data' => null
  979. ];
  980. }
  981. // 构建业务数据
  982. $businessData = [];
  983. // 订单标识(二选一)
  984. if (!empty($orderId)) {
  985. $businessData['order_id'] = (int)$orderId;
  986. }
  987. if (!empty($partnerOrderCode)) {
  988. $businessData['partner_order_code'] = $partnerOrderCode;
  989. }
  990. // 必填参数
  991. $businessData['third_index_id'] = (int)$data['third_index_id'];
  992. $businessData['add_tip_amount_cent'] = (int)$data['add_tip_amount_cent'];
  993. // 构建请求参数
  994. $payload = $this->buildRequestPayload('addTip', $businessData);
  995. // 发送请求
  996. $url = $this->baseUrl . '/addTip';
  997. $resp = HttpClient::post($url, $payload, [
  998. 'Content-Type' => 'application/json',
  999. ]);
  1000. Yii::info("[FengniaoAdapter] addTip Response: " . json_encode($resp));
  1001. // 处理响应
  1002. if (isset($resp['code']) && $resp['code'] == '200' && isset($resp['business_data'])) {
  1003. // business_data 是 JSON 字符串,需要解析
  1004. $respData = is_string($resp['business_data']) ? json_decode($resp['business_data'], true) : $resp['business_data'];
  1005. if (is_null($respData)) {
  1006. Yii::error("[FengniaoAdapter] Failed to parse business_data: " . $resp['business_data']);
  1007. return [
  1008. 'code' => -1,
  1009. 'msg' => 'Failed to parse response data',
  1010. 'data' => null
  1011. ];
  1012. }
  1013. return [
  1014. 'code' => 0,
  1015. 'data' => [
  1016. 'result' => (bool)($respData['result'] ?? false),
  1017. ]
  1018. ];
  1019. }
  1020. return [
  1021. 'code' => (int)($resp['code'] ?? -1),
  1022. 'msg' => $resp['msg'] ?? 'Unknown error',
  1023. 'data' => null
  1024. ];
  1025. }
  1026. /**
  1027. * 查询订单详情
  1028. *
  1029. * 根据蜂鸟官方文档获取订单的详细信息。
  1030. * 包括订单状态、配送员信息、事件节点、价格明细等完整信息。
  1031. *
  1032. * @param array $data 查询参数,结构参考:
  1033. * [
  1034. * // ========== 必填参数(二选一)==========
  1035. * 'order_id' => '订单ID',
  1036. * 'partner_order_code' => '外部订单号',
  1037. * // 注意:order_id 和 partner_order_code 必填一个
  1038. * ]
  1039. * @return array API响应结果
  1040. */
  1041. //public function getOrderDetail($data = [])
  1042. public function selectOrder($data)
  1043. {
  1044. // 验证 order_id 和 partner_order_code 必填一个
  1045. $orderId = $data['order_id'] ?? $data;
  1046. $partnerOrderCode = null;
  1047. // 如果 $orderId 是 XSD 开头,则给 partnerOrderCode 赋值为 $orderId
  1048. if (strpos($orderId, 'XSD') === 0) {
  1049. $partnerOrderCode = $orderId;
  1050. $orderId = null;
  1051. }
  1052. if (empty($orderId) && empty($partnerOrderCode)) {
  1053. Yii::warning("[FengniaoAdapter] getOrderDetail failed: order_id and partner_order_code cannot both be empty");
  1054. return [
  1055. 'code' => -1,
  1056. 'msg' => 'order_id and partner_order_code must have one',
  1057. 'data' => null
  1058. ];
  1059. }
  1060. // 构建业务数据
  1061. $businessData = [];
  1062. // 订单标识(二选一)
  1063. if (!empty($orderId)) {
  1064. $businessData['order_id'] = $orderId;
  1065. }
  1066. if (!empty($partnerOrderCode)) {
  1067. $businessData['partner_order_code'] = $partnerOrderCode;
  1068. }
  1069. // 构建请求参数
  1070. $payload = $this->buildRequestPayload('getOrderDetail', $businessData);
  1071. // 发送请求
  1072. $url = $this->baseUrl . '/getOrderDetail';
  1073. $resp = HttpClient::post($url, $payload, [
  1074. 'Content-Type' => 'application/json',
  1075. ]);
  1076. Yii::info("[FengniaoAdapter] getOrderDetail Response: " . json_encode($resp));
  1077. // 处理响应
  1078. if (isset($resp['code']) && $resp['code'] == '200' && isset($resp['business_data'])) {
  1079. // business_data 是 JSON 字符串,需要解析
  1080. $respData = is_string($resp['business_data']) ? json_decode($resp['business_data'], true) : $resp['business_data'];
  1081. if (is_null($respData)) {
  1082. Yii::error("[FengniaoAdapter] Failed to parse business_data: " . $resp['business_data']);
  1083. return [
  1084. 'code' => -1,
  1085. 'msg' => 'Failed to parse response data',
  1086. 'data' => null
  1087. ];
  1088. }
  1089. // 解析价格明细
  1090. $priceDetail = [];
  1091. if (!empty($respData['price_detail']) && is_array($respData['price_detail'])) {
  1092. $pd = $respData['price_detail'];
  1093. $priceDetail = [
  1094. 'start_price_cent' => (int)($pd['start_price_cent'] ?? 0),
  1095. 'distance_price_cent' => (int)($pd['distance_price_cent'] ?? 0),
  1096. 'weight_price_cent' => (int)($pd['weight_price_cent'] ?? 0),
  1097. 'pressure_surcharge_cent' => (int)($pd['pressure_surcharge_cent'] ?? 0),
  1098. 'time_period_surcharge_cent' => (int)($pd['time_period_surcharge_cent'] ?? 0),
  1099. 'river_crossing_surcharge_cent' => (int)($pd['river_crossing_surcharge_cent'] ?? 0),
  1100. 'category_surcharge_cent' => (int)($pd['category_surcharge_cent'] ?? 0),
  1101. 'temporary_surcharge_cent' => (int)($pd['temporary_surcharge_cent'] ?? 0),
  1102. 'order_price_surcharge_cent' => (int)($pd['order_price_surcharge_cent'] ?? 0),
  1103. ];
  1104. }
  1105. // 解析事件日志列表
  1106. $eventLogDetails = [];
  1107. if (!empty($respData['event_log_details']) && is_array($respData['event_log_details'])) {
  1108. foreach ($respData['event_log_details'] as $log) {
  1109. $eventLogDetails[] = [
  1110. 'order_status' => (int)($log['order_status'] ?? 0),
  1111. 'occur_time' => (int)($log['occur_time'] ?? 0),
  1112. 'carrier_driver_name' => $log['carrier_driver_name'] ?? '',
  1113. 'carrier_driver_phone' => $log['carrier_driver_phone'] ?? '',
  1114. ];
  1115. }
  1116. }
  1117. // 解析逆向单运单事件节点
  1118. $reverseEventLogDetails = [];
  1119. if (!empty($respData['reverse_event_log_details']) && is_array($respData['reverse_event_log_details'])) {
  1120. $reverse = $respData['reverse_event_log_details'];
  1121. $reverseEventLogDetails = [
  1122. 'order_status' => (int)($reverse['order_status'] ?? 0),
  1123. 'occur_time' => (int)($reverse['occur_time'] ?? 0),
  1124. 'carrier_driver_name' => $reverse['carrier_driver_name'] ?? '',
  1125. 'carrier_driver_phone' => $reverse['carrier_driver_phone'] ?? '',
  1126. ];
  1127. }
  1128. // 解析天气信息
  1129. $weatherInfo = [];
  1130. if (!empty($respData['weather_info']) && is_array($respData['weather_info'])) {
  1131. $weather = $respData['weather_info'];
  1132. $weatherInfo = [
  1133. 'weatherRate' => (int)($weather['weatherRate'] ?? 0),
  1134. 'weatherDesc' => $weather['weatherDesc'] ?? '',
  1135. ];
  1136. }
  1137. // 解析取货照片列表
  1138. $deliveryFetchPhotos = [];
  1139. if (!empty($respData['delivery_fetch_photos']) && is_array($respData['delivery_fetch_photos'])) {
  1140. $deliveryFetchPhotos = $respData['delivery_fetch_photos'];
  1141. }
  1142. return [
  1143. 'code' => 0,
  1144. 'data' => [
  1145. // 基本信息
  1146. 'order_id' => (int)($respData['order_id'] ?? 0),
  1147. 'partner_order_code' => $respData['partner_order_code'] ?? '',
  1148. 'tracking_id' => (int)($respData['tracking_id'] ?? 0),
  1149. 'order_status' => (int)($respData['order_status'] ?? 0),
  1150. 'serial_number' => $respData['serial_number'] ?? '',
  1151. 'remark' => $respData['remark'] ?? '',
  1152. // 配送员信息
  1153. 'carrier_driver_id' => $respData['carrier_driver_id'] ?? '',
  1154. 'carrier_driver_name' => $respData['carrier_driver_name'] ?? '',
  1155. 'carrier_driver_phone' => $respData['carrier_driver_phone'] ?? '',
  1156. 'temperature' => $respData['temperature'] ?? '',
  1157. // 金额信息(单位:分)
  1158. 'order_total_amount_cent' => (int)($respData['order_total_amount_cent'] ?? 0),
  1159. 'order_actual_amount_cent' => (int)($respData['order_actual_amount_cent'] ?? 0),
  1160. 'order_tip_amount_cent' => (int)($respData['order_tip_amount_cent'] ?? 0),
  1161. 'overtime_compensation_cost_cent' => (int)($respData['overtime_compensation_cost_cent'] ?? 0),
  1162. // 配送信息
  1163. 'order_distance' => (double)($respData['order_distance'] ?? 0),
  1164. 'estimate_arrive_time' => (int)($respData['estimate_arrive_time'] ?? 0),
  1165. 'if_can_add_tip' => (int)($respData['if_can_add_tip'] ?? 0),
  1166. 'fetch_code' => $respData['fetch_code'] ?? '',
  1167. 'write_off_code' => $respData['write_off_code'] ?? '',
  1168. 'cancel_code' => $respData['cancel_code'] ?? '',
  1169. // 价格明细
  1170. 'price_detail' => $priceDetail,
  1171. // 事件日志
  1172. 'event_log_details' => $eventLogDetails,
  1173. 'reverse_event_log_details' => $reverseEventLogDetails,
  1174. // 异常信息
  1175. 'abnormal_code' => $respData['abnormal_code'] ?? '',
  1176. 'abnormal_desc' => $respData['abnormal_desc'] ?? '',
  1177. // 索赔信息
  1178. 'claim_id' => (int)($respData['claim_id'] ?? 0),
  1179. 'claim_status' => (int)($respData['claim_status'] ?? 0),
  1180. 'claim_status_desc' => $respData['claim_status_desc'] ?? '',
  1181. 'claim_reason_desc' => $respData['claim_reason_desc'] ?? '',
  1182. // 投诉信息
  1183. 'complaint_id' => (int)($respData['complaint_id'] ?? 0),
  1184. 'complaint_status' => (int)($respData['complaint_status'] ?? 0),
  1185. 'complaint_response_desc' => $respData['complaint_response_desc'] ?? '',
  1186. 'complaint_reason_desc' => $respData['complaint_reason_desc'] ?? '',
  1187. // 其他
  1188. 'delivery_fetch_photos' => $deliveryFetchPhotos,
  1189. 'weather_info' => $weatherInfo,
  1190. ]
  1191. ];
  1192. }
  1193. return [
  1194. 'code' => (int)($resp['code'] ?? -1),
  1195. 'msg' => $resp['msg'] ?? 'Unknown error',
  1196. 'data' => null
  1197. ];
  1198. }
  1199. /**
  1200. * 查询骑手信息
  1201. *
  1202. * 根据蜂鸟官方文档获取订单对应的骑手(配送员)的实时位置和联系信息。
  1203. * 返回骑手的ID、姓名、电话以及实时位置坐标(高德地图坐标系)。
  1204. *
  1205. * @param array $data 查询参数,结构参考:
  1206. * [
  1207. * // ========== 必填参数(二选一)==========
  1208. * 'order_id' => '订单ID',
  1209. * 'partner_order_code' => '外部订单号',
  1210. * // 注意:order_id 和 partner_order_code 必填一个
  1211. * ]
  1212. * @return array API响应结果
  1213. * 成功:[
  1214. * 'code' => 0,
  1215. * 'data' => [
  1216. * 'carrier_driver_id' => '骑手ID',
  1217. * 'carrier_driver_name' => '骑手姓名',
  1218. * 'carrier_driver_phone' => '骑手电话',
  1219. * 'carrier_driver_latitude' => '骑手位置纬度',
  1220. * 'carrier_driver_longitude' => '骑手位置经度',
  1221. * // 返回的是高德地图的经纬度
  1222. * ]
  1223. * ]
  1224. * 失败:['code' => 错误码, 'msg' => 错误信息, 'data' => null]
  1225. */
  1226. public function getKnightInfo($data = [])
  1227. {
  1228. // 验证 order_id 和 partner_order_code 必填一个
  1229. $orderId = $data['order_id'] ?? null;
  1230. $partnerOrderCode = $data['partner_order_code'] ?? null;
  1231. if (empty($orderId) && empty($partnerOrderCode)) {
  1232. Yii::warning("[FengniaoAdapter] getKnightInfo failed: order_id and partner_order_code cannot both be empty");
  1233. return [
  1234. 'code' => -1,
  1235. 'msg' => 'order_id and partner_order_code must have one',
  1236. 'data' => null
  1237. ];
  1238. }
  1239. // 构建业务数据
  1240. $businessData = [];
  1241. // 订单标识(二选一)
  1242. if (!empty($orderId)) {
  1243. $businessData['order_id'] = (int)$orderId;
  1244. }
  1245. if (!empty($partnerOrderCode)) {
  1246. $businessData['partner_order_code'] = $partnerOrderCode;
  1247. }
  1248. // 构建请求参数
  1249. $payload = $this->buildRequestPayload('getKnightInfo', $businessData);
  1250. // 发送请求
  1251. $url = $this->baseUrl . '/getKnightInfo';
  1252. $resp = HttpClient::post($url, $payload, [
  1253. 'Content-Type' => 'application/json',
  1254. ]);
  1255. Yii::info("[FengniaoAdapter] getKnightInfo Response: " . json_encode($resp));
  1256. // 处理响应
  1257. if (isset($resp['code']) && $resp['code'] == '200' && isset($resp['business_data'])) {
  1258. // business_data 是 JSON 字符串,需要解析
  1259. $respData = is_string($resp['business_data']) ? json_decode($resp['business_data'], true) : $resp['business_data'];
  1260. if (is_null($respData)) {
  1261. Yii::error("[FengniaoAdapter] Failed to parse business_data: " . $resp['business_data']);
  1262. return [
  1263. 'code' => -1,
  1264. 'msg' => 'Failed to parse response data',
  1265. 'data' => null
  1266. ];
  1267. }
  1268. return [
  1269. 'code' => 0,
  1270. 'data' => [
  1271. 'carrier_driver_id' => $respData['carrier_driver_id'] ?? '',
  1272. 'carrier_driver_name' => $respData['carrier_driver_name'] ?? '',
  1273. 'carrier_driver_phone' => $respData['carrier_driver_phone'] ?? '',
  1274. 'carrier_driver_latitude' => $respData['carrier_driver_latitude'] ?? '',
  1275. 'carrier_driver_longitude' => $respData['carrier_driver_longitude'] ?? '',
  1276. ]
  1277. ];
  1278. }
  1279. return [
  1280. 'code' => (int)($resp['code'] ?? -1),
  1281. 'msg' => $resp['msg'] ?? 'Unknown error',
  1282. 'data' => null
  1283. ];
  1284. }
  1285. }