ShunfengAdapter.php 17 KB

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