ShunfengAdapter.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523
  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\platform\shunfeng\Shunfeng;
  6. use Yii;
  7. class ShunfengAdapter extends Shunfeng implements Adapter
  8. {
  9. public function cityList(){}
  10. /**
  11. * 格式化订单数据为顺丰同城API所需格式
  12. *
  13. * @param array $order 订单信息
  14. * @param string $orderType 订单类型:xhOrder 或 xhGhsOrder
  15. * @param array $shop 店铺信息
  16. * @param array $params 额外参数
  17. * @return array 格式化后的订单数据
  18. */
  19. public function formatOrderData($order, $orderType, $shop, $params)
  20. {
  21. // 获取订单商品信息
  22. if($orderType == 'xhGhsOrder'){
  23. $itemInfos = OrderItemClass::getAllByCondition(
  24. ['orderSn' => $order['orderSn']],
  25. null,
  26. 'id, name, unitPrice, num, unitWeight'
  27. );
  28. }elseif($orderType == 'xhOrder'){
  29. $itemInfos = OrderGoodsClass::getAllByCondition(
  30. ['orderSn' => $order['orderSn']],
  31. null,
  32. 'id, name, unitPrice, num'
  33. );
  34. }
  35. // 计算商品总重量(单位:克)
  36. $totalWeight = round($params['weight'], 1) * 1000;
  37. $productNum = 0; // 物品个数
  38. $productTypeNum = 0; // 物品种类个数
  39. $productDetail = []; // 物品详情
  40. foreach ($itemInfos as $item) {
  41. // 累加重量(转换为克)
  42. // $itemWeight = (float)($item['unitWeight'] ?? 0) * (int)($item['num'] ?? 1) * 1000;
  43. // $totalWeight += (int)$itemWeight;
  44. // 累加商品数量和种类
  45. $productNum += (int)($item['num'] ?? 1);
  46. $productTypeNum++;
  47. // 构建商品详情
  48. $productDetail[] = [
  49. 'product_name' => $item['name'] ?? '鲜花',
  50. 'product_num' => (int)($item['num'] ?? 1),
  51. 'product_price' => (int)(($item['unitPrice'] ?? 0) * 100), // 转换为分
  52. ];
  53. }
  54. // 如果没有商品信息,使用订单重量
  55. // if ($totalWeight == 0 && isset($order['weight'])) {
  56. // $totalWeight = (int)($order['weight'] * 1000);
  57. // }
  58. // 默认重量为1000克(1公斤)
  59. // if ($totalWeight == 0) {
  60. // $totalWeight = 1000;
  61. // }
  62. // 如果没有商品详情,添加默认商品
  63. if (empty($productDetail)) {
  64. $productDetail[] = [
  65. 'product_name' => '鲜花',
  66. 'product_num' => 1,
  67. ];
  68. $productNum = 1;
  69. $productTypeNum = 1;
  70. }
  71. // 构建必填参数
  72. $data = [
  73. // ==================== 必填字段 ====================
  74. 'shop_order_id' => $order['orderSn'],
  75. 'order_ver' => time(), // 订单版本号 使用相同的商家订单号,同城侧会幂等返回,可以使用此参数+1控制生成新的同城订单
  76. 'order_source' => $params['order_source'] ?? '花店系统', // 订单接入来源
  77. 'order_time' => isset($order['addTime']) ? strtotime($order['addTime']) : time(), // 用户下单时间(秒级时间戳)
  78. 'push_time' => time(), // 推单时间(秒级时间戳)
  79. 'version' => $this->apiVersion, // API版本号
  80. // 收货人信息
  81. 'receive' => [
  82. 'user_name' => $order['customName'],
  83. 'user_phone' => Functions::getMobile($order),
  84. 'user_address' => $order['fullAddress'],
  85. 'user_lng' => (string)($order['long']),
  86. 'user_lat' => (string)($order['lat']),
  87. 'city_name' => $order['city'] ?? '', // 发单城市,用于校验是否跨城
  88. ],
  89. // 发货店铺信息
  90. 'shop' => [
  91. 'shop_name' => $shop['shopName'] == '首店' ? $shop['merchantName'] : $shop['merchantName'].'-'.$shop['shopName'],
  92. 'shop_address' => $shop['address'] ?? '',
  93. 'shop_lng' => (string)($shop['long'] ?? ''),
  94. 'shop_lat' => (string)($shop['lat'] ?? ''),
  95. 'shop_phone' => $shop['mobile'] ?? '',
  96. ],
  97. // 订单详情
  98. 'order_detail' => [
  99. 'total_price' => (int)(($order['actPrice'] ?? 0) * 100), // 订单总金额(单位:分)
  100. 'product_type' => 14, // 物品类型:14-鲜花
  101. 'weight_gram' => $totalWeight, // 物品重量(单位:克)
  102. 'product_num' => $productNum, // 物品个数
  103. 'product_type_num' => $productTypeNum, // 物品种类个数
  104. 'product_detail' => $productDetail, // 物品详情
  105. ],
  106. // ==================== 可选字段 ====================
  107. 'lbs_type' => 2, // 坐标类型(1:百度坐标,2:高德坐标(国测坐标),默认为高德坐标系)
  108. 'is_insured' => 0, // 是否保价(0:非保价)
  109. 'is_person_direct' => isset($params['type']) && $params['type'] == '专人直送' ? 1 : 0, // 是否专人直送(0:否,1:是)
  110. 'vehicle' => 0, // 配送交通工具(0:否,1:电动车,2:小轿车)
  111. 'declared_value' => 0, // 保价金额(单位:分)
  112. 'gratuity_fee' => 0, // 订单小费(单位:分)
  113. 'remark' => $params['remark'] ?? '', // 订单备注,string(512)
  114. 'rider_pick_method' => 1, // 物流流向(1:从门店取件送至用户)
  115. 'return_flag' => 511, // 返回字段控制标志位(511:全部返回)
  116. 'payment_method' => 1, // 支付方式(1:立即支付,2:到付)
  117. 'order_sequence' => $order['sendNum'],
  118. ];
  119. // 用户期望上门时间 -- expect_pickup_time
  120. $expectPickupTime = isset($params['pickupTime']) ? strtotime($params['pickupTime']) : time();
  121. if($expectPickupTime > time() + 3600){ // 用户期望上门时间大于当前时间+1小时,则认为是预约单
  122. $data['is_appoint'] = 1; // 是否预约单(0:非预约单)-- 修改为预约单
  123. $data['appoint_type'] = 2;
  124. $data['expect_pickup_time'] = $expectPickupTime;
  125. }
  126. return $data;
  127. }
  128. /**
  129. * @param $data
  130. * @return array
  131. */
  132. public function createOrder($data)
  133. {
  134. $resp = $this->httpRequest('createorder', $data);
  135. // 处理响应
  136. if (isset($resp['error_code']) && $resp['error_code'] == 0 && isset($resp['result'])) {
  137. // business_data 是 JSON 字符串,需要解析
  138. $respData = is_string($resp['result']) ? json_decode($resp['result'], true) : $resp['result'];
  139. if (isset($respData['sf_order_id'])) {
  140. return [
  141. 'code' => 0,
  142. 'platform' => 'shunfeng',
  143. 'data' => [
  144. 'orderId' => $respData['sf_order_id'],
  145. 'fee' => $respData['real_pay_money'],
  146. 'distance' => $respData['delivery_distance_meter'],
  147. ]
  148. ];
  149. }
  150. }
  151. return [
  152. 'code' => $resp['error_code'] ?? -1,
  153. 'msg' => $resp['error_msg'] ?? 'Unknown error',
  154. 'data' => null
  155. ];
  156. }
  157. /**
  158. * 取消订单
  159. *
  160. * 当商家处发生异常需要取消配送时,调用此接口对订单进行取消操作。
  161. * @param array $data 预取消查询参数,结构参考:
  162. * [
  163. * // ========== 必填参数(二选一)==========
  164. * 'order_id' => '订单ID',
  165. * 'partner_order_code' => '外部订单号',
  166. * // 注意:order_id 和 partner_order_code 必填一个
  167. *
  168. * // ========== 必填参数 ==========
  169. * 'order_cancel_code' => '取消原因code(从可用取消原因列表接口返回结果选择)',
  170. * ]
  171. * @param string $orderId 顺丰订单号(SF订单号或商家订单号)
  172. * @param array $reason 取消原因信息
  173. * [
  174. * 'cancel_code' => int, // 可选:取消原因代码(默认313=其他)
  175. * 'cancel_reason' => string, // 可选:其他取消原因说明
  176. * 'order_type' => int // 可选:1-顺丰订单号,2-商家订单号(默认1)
  177. * ]
  178. * @return array 取消结果
  179. */
  180. public function cancelOrder($orderId, $data)
  181. {
  182. // 构建请求参数
  183. $payload = [
  184. 'order_id' => $orderId,
  185. 'push_time' => time(), // 取消时间戳(秒级)
  186. ];
  187. $payload['cancel_code'] = $data['order_cancel_code'];
  188. if(isset($data['order_cancel_reason'])){
  189. $payload['cancel_reason'] = $data['order_cancel_reason'];
  190. }
  191. if (isset($data['order_type'])) {
  192. $payload['order_type'] = $data['order_type'];
  193. }
  194. // 调用API
  195. $resp = $this->httpRequest('cancelorder', $payload);
  196. // 处理响应
  197. if (isset($resp['error_code']) && $resp['error_code'] == 0 && isset($resp['result'])) {
  198. $result = $resp['result'];
  199. return [
  200. 'code' => 0,
  201. 'platform' => 'shunfeng',
  202. 'data' => [
  203. 'deductionFee' => $result['deduction_detail']['deduction_fee'], //取消收费金额(单位:分)
  204. 'sf_order_id' => $result['sf_order_id'],
  205. 'shop_order_id' => $result['shop_order_id'],
  206. 'shop_cancel_times' => $result['deduction_detail']['shop_cancel_times'],
  207. 'free_cancel_times' => $result['deduction_detail']['free_cancel_times'],
  208. 'push_time' => $result['push_time'],
  209. ]
  210. ];
  211. }
  212. return [
  213. 'code' => (int)($resp['error_code'] ?? -1),
  214. 'platform' => 'shunfeng',
  215. 'msg' => $resp['error_msg'] ?? 'Cancel order failed',
  216. 'data' => null
  217. ];
  218. }
  219. /**
  220. * 预取消订单
  221. *
  222. * 并非真正取消订单,用来验证是否可以取消订单并返回计价等信息。
  223. *
  224. * @param array $data 预取消订单参数
  225. * [
  226. * 'order_id' => string, // 必须:订单ID(顺丰订单号或商家订单号)
  227. * 'cancel_reason' => string, // 可选:取消原因
  228. * 'shop_id' => string, // 可选:店铺ID
  229. * 'shop_type' => int // 可选:店铺类型(1-顺丰,2-接入方)
  230. * ]
  231. * @return array 预取消结果(包含是否可以取消、计费信息等)
  232. */
  233. public function preCancelOrder($data)
  234. {
  235. // 验证必填参数
  236. if (empty($data['order_id'])) {
  237. return [
  238. 'code' => -1,
  239. 'msg' => 'order_id is required',
  240. 'data' => null
  241. ];
  242. }
  243. // 构建请求参数
  244. $payload = [
  245. 'order_id' => $data['order_id'],
  246. 'push_time' => time(), // 取消时间戳(秒级)
  247. ];
  248. // 添加可选参数
  249. if (isset($data['cancel_reason']) && !empty($data['cancel_reason'])) {
  250. $payload['cancel_reason'] = $data['cancel_reason'];
  251. }
  252. if (isset($data['shop_id']) && !empty($data['shop_id'])) {
  253. $payload['shop_id'] = $data['shop_id'];
  254. }
  255. if (isset($data['shop_type'])) {
  256. $payload['shop_type'] = $data['shop_type'];
  257. }
  258. // 调用API
  259. $resp = $this->httpRequest('precancelorder', $payload);
  260. // 处理响应
  261. if (isset($resp['error_code']) && $resp['error_code'] == 0 && isset($resp['result'])) {
  262. $result = $resp['result'];
  263. return [
  264. 'code' => 0,
  265. 'platform' => 'shunfeng',
  266. 'data' => [
  267. 'order_type' => $result['order_type'],
  268. 'could_cancel' => $result['could_cancel'],
  269. 'promise_delivery_time' => $result['promise_delivery_time'], // 承诺配送时长(分)
  270. 'delivery_type' => $result['delivery_type'], // 订单类型(0-预约送达,1-立即单,2-预约上门)
  271. 'is_cancel_charge_price_rule' => $result['is_cancel_charge_price_rule'],
  272. 'is_over_free_cancel_times' => $result['is_over_free_cancel_times'], // 是否超出免费取消次数
  273. 'is_deduction_fee' => $result['is_deduction_fee'], // 是否有取消收费
  274. 'deduction_fee' => $result['deduction_fee'], // 取消收费(单位:分)
  275. 'cancel_charge_price_rule' => $result['cancel_charge_price_rule'],
  276. 'shop_cancel_times' => $result['shop_cancel_times'],
  277. 'free_cancel_times' => $result['free_cancel_times'],
  278. 'sf_order_id' => $result['sf_order_id'],
  279. 'shop_order_id' => $result['shop_order_id'],
  280. 'push_time' => $result['push_time'],
  281. ]
  282. ];
  283. }
  284. return [
  285. 'code' => (int)($resp['error_code'] ?? -1),
  286. 'msg' => $resp['error_msg'] ?? 'Pre-cancel order failed',
  287. 'data' => null
  288. ];
  289. }
  290. /**
  291. * 查询订单状态
  292. * @param array $data
  293. * @return array
  294. */
  295. public function selectOrder($orderId)
  296. {
  297. if (empty($orderId)) {
  298. return [
  299. 'code' => -1,
  300. 'platform' => 'shunfeng',
  301. 'msg' => 'order_id不能为空',
  302. 'data' => null,
  303. ];
  304. }
  305. $payload = [
  306. 'order_id' => $orderId,
  307. 'push_time' => time(),
  308. ];
  309. if (isset($data['order_type'])) {
  310. $payload['order_type'] = (int)$data['order_type'];
  311. }
  312. if (!empty($data['shop_id'])) {
  313. $payload['shop_id'] = $data['shop_id'];
  314. }
  315. if (isset($data['shop_type'])) {
  316. $payload['shop_type'] = (int)$data['shop_type'];
  317. }
  318. $resp = $this->httpRequest('getorderstatus', $payload);
  319. if (isset($resp['error_code']) && $resp['error_code'] == 0 && isset($resp['result'])) {
  320. return [
  321. 'code' => 0,
  322. 'platform' => 'shunfeng',
  323. 'data' => $resp['result']
  324. ];
  325. }
  326. return [
  327. 'code' => (int)($resp['error_code'] ?? -1),
  328. 'platform' => 'shunfeng',
  329. 'msg' => $resp['error_msg'] ?? '查询订单状态失败',
  330. 'data' => null
  331. ];
  332. }
  333. /**
  334. * @deprecated
  335. * @param $data
  336. */
  337. public function getPrice($data)
  338. {
  339. // $requestData = [
  340. // "user_lng" => "116.339392",
  341. // "user_lat" => "40.002349",
  342. // "user_address" => "北京市海淀区国泰大厦",
  343. // "weight" => 1000,
  344. // "product_type" => 4,
  345. // "push_time" => 1639558442
  346. // ];
  347. // $method = 'precreateorder';
  348. // $ret = $this->httpRequest($method, $requestData);
  349. }
  350. /**
  351. * 处理运费报价响应
  352. *
  353. * @param array $resp API 响应
  354. * @return array|null 解析后的报价结果
  355. */
  356. public function processPriceResponse($resp)
  357. {
  358. if (isset($resp['error_code']) && $resp['error_code'] == 0 && isset($resp['result'])) {
  359. $result = $resp['result'];
  360. return [
  361. 'platform' => 'shunfeng',
  362. 'total_price' => $result['total_price'], // 配送费总额
  363. 'distance' => $result['delivery_distance_meter'], //配送距离
  364. 'total_pay_money' => $result['total_pay_money'], //总支付费用(优惠前),当return_flag中包含32时返回,单位分
  365. 'real_pay_money' => $result['real_pay_money'], //实际支付金额,当return_flag中包含64时返回,单位分(实际支付金额=总支付费用-优惠券总金额)
  366. // 还有其它返回值没加进来
  367. ];
  368. } else {
  369. Yii::error('shunfeng 报价失败:'.json_encode($resp, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
  370. return [
  371. 'platform' => 'shunfeng',
  372. 'error' => isset($resp['error_msg']) ? $resp['error_msg'] : '',
  373. ];
  374. }
  375. }
  376. /**
  377. * 构建运费报价请求信息(供并发请求使用)
  378. * @param array $data 报价参数
  379. * @param string $pickupTime 取件时间
  380. * @return array|null 包含 url, payload, headers, timeout 的请求信息
  381. */
  382. public function buildPriceRequest($data, $pickupTime)
  383. {
  384. unset($data['platform']);
  385. // 根据 $pickupTime ,调整 $data 中的部分参数
  386. $timestamp = strtotime($pickupTime);
  387. if($timestamp > time() + 300){ // 5分钟后算预约单
  388. $data['is_appoint'] = 1; // 是否预约单(0:非预约单;1:预约单)-- 修改为预约单
  389. $data['appoint_type'] = 2; // 预约类型 -- 1:预约送达单;2:预约上门单 (预约单的时候传入,即时单的时候不传)
  390. $data['expect_pickup_time'] = $timestamp; // 期望取件时间(秒级时间戳)
  391. }
  392. // 构建签名用的完整payload
  393. $payload = [
  394. 'dev_id' => $this->devId,
  395. 'shop_id' => $this->shopId,
  396. 'shop_type' => 1, //1:顺丰店铺ID 2:接入方店铺ID
  397. ];
  398. $payload = array_merge($payload, $data);
  399. // 计算签名(SignHelper 会自动递归过滤空值)
  400. $sign = $this->generateSignature($payload);
  401. $request = [
  402. 'url' => $this->baseUrl . 'precreateorder?sign=' . $sign,
  403. 'data' => $payload,
  404. 'headers' => ["Content-Type" => "application/json"],
  405. 'timeout' => 10.0,
  406. ];
  407. // Yii::info('顺丰构建运费报价请求信息 -- ' .json_encode($request, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
  408. return $request;
  409. }
  410. public function addTip($data)
  411. {
  412. $orderId = $data['orderId'] ?? null;
  413. $gratuityFee = $data['tips'] ?? null;
  414. if (empty($orderId)) {
  415. return [
  416. 'code' => -1,
  417. 'platform' => 'shunfeng',
  418. 'msg' => 'order_id不能为空',
  419. 'data' => null,
  420. ];
  421. }
  422. if (!isset($gratuityFee) || (int)$gratuityFee < 100) {
  423. return [
  424. 'code' => -1,
  425. 'platform' => 'shunfeng',
  426. 'msg' => 'gratuity_fee必须≥100(单位:分)',
  427. 'data' => null,
  428. ];
  429. }
  430. $payload = [
  431. 'order_id' => $orderId,
  432. 'gratuity_fee' => (int)$gratuityFee,
  433. 'push_time' => isset($data['push_time']) ? (int)$data['push_time'] : time(),
  434. ];
  435. if (isset($data['order_type'])) {
  436. $payload['order_type'] = (int)$data['order_type'];
  437. }
  438. if (!empty($data['shop_id'])) {
  439. $payload['shop_id'] = $data['shop_id'];
  440. }
  441. if (isset($data['shop_type'])) {
  442. $payload['shop_type'] = (int)$data['shop_type'];
  443. }
  444. if (!empty($data['serial_number'])) {
  445. $payload['serial_number'] = (string)$data['serial_number'];
  446. }
  447. $resp = $this->httpRequest('addordergratuityfee', $payload);
  448. if (isset($resp['error_code']) && (int)$resp['error_code'] === 0) {
  449. $result = is_string($resp['result'] ?? null) ? json_decode($resp['result'], true) : ($resp['result'] ?? []);
  450. return [
  451. 'code' => 0,
  452. 'platform' => 'shunfeng',
  453. 'data' => $result,
  454. ];
  455. }
  456. Yii::error('[ShunfengAdapter] addTip failed: ' . json_encode($resp, JSON_UNESCAPED_UNICODE));
  457. return [
  458. 'code' => (int)($resp['error_code'] ?? -1),
  459. 'platform' => 'shunfeng',
  460. 'msg' => $resp['error_msg'] ?? '添加小费失败',
  461. 'data' => null,
  462. ];
  463. }
  464. }