You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

261 lines
12 KiB

  1. <?php
  2. namespace App\Service\v3;
  3. use App\Exception\BusinessException;
  4. use Hyperf\Guzzle\ClientFactory;
  5. use Hyperf\Utils\ApplicationContext;
  6. /**
  7. * 顺丰同城相关
  8. * 文档:https://commit-openic.sf-express.com/open/api/docs/index#/apidoc
  9. */
  10. class SfExpress
  11. {
  12. private string $dev_id;
  13. private string $dev_key;
  14. private string $shop_id;
  15. private string $host = 'https://openic.sf-express.com';
  16. private ClientFactory $clientFactory;
  17. private static ?SfExpress $_instance = NULL;
  18. public function __construct()
  19. {
  20. $this->dev_id = env('SF_EXPRESS_DEV_ID');
  21. $this->dev_key = env('SF_EXPRESS_DEV_KEY');
  22. $this->shop_id = env('SF_EXPRESS_SHOP_ID');
  23. $this->clientFactory = ApplicationContext::getContainer()->get(ClientFactory::class);
  24. }
  25. public static function getInstance(): SfExpress
  26. {
  27. if (is_null(self::$_instance)) {
  28. self::$_instance = new static();
  29. }
  30. return self::$_instance;
  31. }
  32. /**
  33. * 创建订单
  34. */
  35. public function createOrder(array $params): array
  36. {
  37. if (!isset($params['shop_order_id'], $params['order_time'], $params['receive'], $params['order_detail'])) {
  38. throw new BusinessException(500, '商户订单号、用户下单时间、收货信息、订单信息不能为空');
  39. }
  40. $params = array_merge([
  41. 'dev_id' => $this->dev_id, //必填
  42. 'shop_id' => $this->shop_id, //必填
  43. 'shop_type' => 1, //1:顺丰店铺ID 2:接入方店铺ID
  44. 'shop_order_id' => '', //商户订单号
  45. 'shop_preparation_time' => 15, //商家预计备餐时长,分钟级时间 比如: 10 分钟 则传入 10
  46. 'order_source' => '街链平台', //必填。订单接入来源 1:美团;2:饿了么;3:百度;4:口碑;其他请直接填写中文字符串值
  47. // 'order_sequence' => '', //取货序号 与order_source配合使用 如:饿了么10号单,表示如下:order_source=2;order_sequence=10。用于骑士快速寻找配送物
  48. // 'lbs_type' => 2, //坐标类型,1:百度坐标,2:高德坐标
  49. 'pay_type' => 1, //必填,用户支付方式:1、已支付 0、货到付款
  50. 'order_time' => time(),
  51. 'is_appoint' => 0, //必填,是否是预约单,0:非预约单;1:预约单
  52. 'appoint_type' => 0, //预约单类型,1:预约单送达单;2:预约单上门单,默认:0
  53. // 'expect_time' => '', //用户期望送达时间,若传入自此段且时间大于配送时效,则按照预约送达单处理,时间小于配送时效按照立即单处理;appoint_type=1时需必传,秒级时间戳
  54. // 'expect_pickup_time' => '', //用户期望上门时间,appoint_type=2时需必传,秒级时间戳
  55. // 'shop_expect_time' => 0, //商家期望送达时间 只展示给骑士,不参与时效考核;秒级时间戳
  56. 'is_insured' => 0, //必填,是否保价,0:非保价;1:保价
  57. 'is_person_direct' => 0, //必填,是否是专人直送订单,0:否;1:是
  58. // 'vehicle' => 0, //配送交通工具,0:否;1:电动车;2:小轿车
  59. // 'declared_value' => 0, //保价金额
  60. // 'gratuity_fee' => 0, //订单小费,不传或者传0为不加小费,单位分,加小费最低不能少于100分
  61. 'remark' => '无备注', //订单备注
  62. 'rider_pick_method' => 1, //物流流向,1:从门店取件送至用户;2:从用户取件送至门店
  63. 'return_flag' => 511, //1:商品总价格,2:配送距离,4:物品重量,8:起送时间,16:期望送达时间,32:支付费用,64:实际支持金额,128:优惠券总金额,256:结算方式,例如全部返回为填入511
  64. 'push_time' => time(), //必填,推单时间,秒级时间戳(格林尼治时间)
  65. 'version' => 17, //版本号 参照文档主版本号填写 如:文档版本号1.7,version=17
  66. 'receive' => [ //收货人信息 Obj,详见receive结构
  67. 'user_name' => '', //必填,用户姓名
  68. 'user_phone' => '', //必填,用户电话
  69. 'user_address' => '', //必填,用户详细地址
  70. 'user_lng' => 0, //必填,用户地址经度
  71. 'user_lat' => 0, //必填,用户地址纬度
  72. // 'city_name' => '', //发单城市 用来校验是否跨城;请填写城市的中文名称,如北京市、深圳市
  73. ],
  74. /*'shop' => [ //发货店铺信息;obj,详见shop结构,平台级开发者需要传入
  75. 'shop_name' => '店铺姓名',
  76. 'shop_phone' => '店铺电话',
  77. 'shop_address' => '店铺地址',
  78. 'shop_lng' => '店铺经度',
  79. 'shop_lat' => '店铺纬度',
  80. ],*/
  81. 'order_detail' => [ // 必填,订单详情
  82. 'total_price' => 0, //用户订单商品总金额(单位:分) 100 表示1元(最大值为100万, 超过此值则按100万计算)
  83. 'product_type' => 10, //必填,物品类型。1:快餐,2:药品,3:百货,4:脏衣服收,5:干净衣服派,6:生鲜,其它详见文档
  84. // 'user_money' => 0, // 用户实付商家金额(单位:分) 100 表示1元
  85. // 'shop_money' => 0, // 商家实收用户金额(单位:分) 100 表示1元
  86. 'weight_gram' => 0, // 必填,物品重量(单位:克) 100 表示100g
  87. // 'volume_litre' => 0, // 物品体积(单位:升) 1 表示1升
  88. // 'delivery_money' => 0, // 商家收取用户的配送费(单位:分) 100 表示1元
  89. 'product_num' => 1, // 必填,物品个数
  90. 'product_type_num' => 1, // 必填,物品种类个数
  91. 'product_detail' => [ // 必填,物品种类个数
  92. [
  93. 'product_name' => '', // 必填,物品名称
  94. // 'product_id' => 1, // 物品ID
  95. 'product_num' => 1, // 必填,物品数量
  96. // 'product_price' => 0, // 物品价格
  97. // 'product_unit' => 0, // 物品单位
  98. // 'product_remark' => 0, // 备注
  99. // 'item_detail' => 0, // 详情
  100. ],
  101. ],
  102. ],
  103. /*'multi_pickup_info' => [ //多点取货信息
  104. [
  105. 'pickup_shop_address' => '取货点地址', // 必填,取货点地址
  106. 'pickup_shop_phone' => '取货点店铺手机号', // 必填,取货点店铺手机号
  107. 'pickup_shop_name' => '取货点店铺手机号', // 必填,取货点店铺名称
  108. 'pickup_lng' => '取货点经度', // 必填,取货点经度
  109. 'pickup_lat' => '取货点纬度', // 必填,取货点纬度
  110. 'pickup_products' => '', // 必填,取货点店铺物品信息
  111. ],
  112. ],*/
  113. ], $params);
  114. return $this->send('/open/api/external/createorder', $params);
  115. }
  116. /**
  117. * 取消订单
  118. */
  119. public function cancelOrder(array $params): array
  120. {
  121. $params = array_merge([
  122. 'dev_id' => $this->dev_id,
  123. 'order_id' => $params['order_id'],
  124. 'shop_id' => $this->shop_id, // 使用商家订单号,需要传入shop_id
  125. 'shop_type' => 1, // 输入shop_id,shop_type需必传
  126. 'order_type' => 2, // 订单ID类型,1、顺丰订单号 2、商家订单号
  127. 'cancel_code' => 300,
  128. 'push_time' => time(),
  129. ], $params);
  130. return $this->send('/open/api/external/cancelorder', $params);
  131. }
  132. /**
  133. * 获取顺丰同城配送费
  134. */
  135. public function getDeliveryCost(array $params): float
  136. {
  137. $res = $this->preCreateOrder($params);
  138. $real_pay_money = $res['result']['real_pay_money'] ?? null;
  139. if ($real_pay_money === null) {
  140. throw new BusinessException(500, '获取配送费失败');
  141. }
  142. return (float)bcdiv($real_pay_money, 100, 2);
  143. }
  144. /**
  145. * 获取原始配送费
  146. */
  147. public function getOriginDeliveryCost(float $deliveryCost): float
  148. {
  149. return (float)bcadd($deliveryCost,3.50,2);
  150. }
  151. /**
  152. * 预创建订单
  153. */
  154. private function preCreateOrder(array $params): array
  155. {
  156. if (!isset($params['user_lng'], $params['user_lat'], $params['user_address'], $params['weight'], $params['shop'])) {
  157. throw new BusinessException(500, '经度、纬度、收货地址、重量、发货店铺信息不能为空');
  158. }
  159. $params = array_merge([
  160. 'dev_id' => $this->dev_id, //必填
  161. 'shop_id' => $this->shop_id, //必填
  162. 'shop_type' => 1, //1:顺丰店铺ID 2:接入方店铺ID
  163. 'user_lng' => 0, //必填,用户地址经度
  164. 'user_lat' => 0, //必填,用户地址纬度
  165. 'user_address' => '', //必填,用户详细地址
  166. 'weight' => 0, //必填,物品重量(单位:克)
  167. 'product_type' => 6, //必填,物品类型。1:快餐,2:药品,3:百货,4:脏衣服收,5:干净衣服派,6:生鲜,其它详见文档
  168. // 'total_price' => 0, //用户订单总金额(单位:分)
  169. 'is_appoint' => 0, //必填,是否是预约单,0:非预约单;1:预约单
  170. 'appoint_type' => 0, //预约单类型,1:预约单送达单;2:预约单上门单,默认:0
  171. // 'expect_time' => '', //用户期望送达时间,若传入自此段且时间大于配送时效,则按照预约送达单处理,时间小于配送时效按照立即单处理;appoint_type=1时需必传,秒级时间戳
  172. // 'expect_pickup_time' => '', //用户期望上门时间,appoint_type=2时需必传,秒级时间戳
  173. // 'lbs_type' => 2, //坐标类型,1:百度坐标,2:高德坐标
  174. 'pay_type' => 1, //必填,用户支付方式:1、已支付 0、货到付款
  175. // 'receive_user_money' => 0, //代收金额,单位:分
  176. 'is_insured' => 0, //必填,是否保价,0:非保价;1:保价
  177. 'is_person_direct' => 0, //必填,是否是专人直送订单,0:否;1:是
  178. // 'declared_value' => 0, //保价金额
  179. // 'gratuity_fee' => 0, //订单小费,不传或者传0为不加小费,单位分,加小费最低不能少于100分
  180. 'rider_pick_method' => 1, //物流流向,1:从门店取件送至用户;2:从用户取件送至门店
  181. 'return_flag' => 511, //1:商品总价格,2:配送距离,4:物品重量,8:起送时间,16:期望送达时间,32:支付费用,64:实际支持金额,128:优惠券总金额,256:结算方式,例如全部返回为填入511
  182. 'push_time' => time(), //必填,推单时间,秒级时间戳(格林尼治时间)
  183. /*'shop' => [ //发货店铺信息;obj,详见shop结构,平台级开发者需要传入
  184. 'shop_name' => '店铺姓名',
  185. 'shop_phone' => '店铺电话',
  186. 'shop_address' => '店铺地址',
  187. 'shop_lng' => '店铺经度',
  188. 'shop_lat' => '店铺纬度',
  189. ],
  190. 'multi_pickup_info' => [ //多点取货信息
  191. 'pickup_shop_address' => '取货点地址',
  192. 'pickup_lng' => '取货点经度',
  193. 'pickup_lat' => '取货点纬度',
  194. ],*/
  195. ], $params);
  196. return $this->send('/open/api/external/precreateorder', $params);
  197. }
  198. /**
  199. * 发送请求
  200. */
  201. public function send(string $url, array $params): array
  202. {
  203. $url = $this->host . $url;
  204. $url = $url . (str_contains($url, '?') ? '&sign=' . $this->getSign($params) : '?sign=' . $this->getSign($params));
  205. $res = json_decode($this->clientFactory->create()->post($url, ['json' => $params])->getBody()->getContents(), true);
  206. if (!$res || !isset($res['error_code']) || $res['error_code'] != 0) {
  207. throw new BusinessException(500, ($res['error_code'] ?? '') . ':' . ($res['error_msg'] ?? '请求异常'));
  208. }
  209. return $res;
  210. }
  211. /**
  212. * 计算请求签名
  213. */
  214. private function getSign(array $params): string
  215. {
  216. $post_data = json_encode($params);
  217. $sign_char = $post_data . "&{$this->dev_id}&{$this->dev_key}";
  218. return base64_encode(md5($sign_char));
  219. }
  220. /**
  221. * 前端传上来的送达时间(如:11:30 - 12:00)转顺丰的expect_time等预约参数
  222. */
  223. public function deliveryTimeNote2expectTime(string $deliveryTimeNote): array
  224. {
  225. if (!empty($deliveryTimeNote) && $deliveryTimeNote != '尽快送达' && strpos($deliveryTimeNote, '-') !== false) {
  226. $arr = array_map(function ($v) {
  227. return trim($v);
  228. }, explode('-', $deliveryTimeNote));
  229. if ($arr[0] && $arr[1]) {
  230. $startTime = strtotime($arr[0]);
  231. $endTime = strtotime($arr[1]);
  232. # 判断是否是有效时间戳
  233. if ($startTime > 1650000000 && $endTime > 1650000000) {
  234. $sfParams['is_appoint'] = 1;
  235. $sfParams['appoint_type'] = 1;
  236. $sfParams['expect_time'] = floor(($startTime + $endTime) / 2);
  237. }
  238. }
  239. }
  240. return $sfParams ?? [];
  241. }
  242. }